【问题标题】:Draw a sphere using 3D pixels (voxels)使用 3D 像素(体素)绘制球体
【发布时间】:2012-01-31 17:35:42
【问题描述】:

您能否建议一种算法,该算法可以仅使用基本的plot(x,y,z) 基元(将绘制单个体素)在 3D 空间中绘制球体?

我希望有类似于 Bresenham's circle algorithm 的东西,但是是 3D 而不是 2D。

仅供参考,我正在开发一个使用 3 维 LED 矩阵的低分辨率 3D 显示器的硬件项目,因此我需要实际绘制一个球体,而不仅仅是一个 2D 投影(即圆形)。

项目与此非常相似:

...或查看它的实际操作here。

我想到的一种可能性是:

  • 计算两极的 Y 坐标(给定半径)(对于以原点为中心的球体,这些坐标为 -r 和 +r)
  • 切片球体:对于这些坐标之间的每个水平面pi,计算通过该平面与球体相交得到的圆的半径 => ri。
  • 使用 Bresenham 算法在平面 pi 上绘制半径为 ri 的实际圆。

FWIW,我使用的是.NET micro-framework microprocessor,所以编程是 C#,但我不需要 C# 中的答案。

【问题讨论】:

  • 根据 LED 球体判断,我们是否应该假设您还需要以某种方式绘制球体的内部?
  • @NominSim 我不这么认为。在这种情况下,他不会谈论 Bresenham 的圆光栅化,而可以使用 japreiss 的蛮力解决方案。
  • @Christian 实际上,我希望有两种选择。
  • 在这种情况下,我建议使用蛮力算法生成实体版本,然后使用形态学运算得到表面。 Solid - Erode(Solid) 应该可以解决问题。
  • 我不确定,但我可能已经用类似 Bresenham 的球体算法回答了您的问题:stackoverflow.com/questions/9683965/…

标签: algorithm graphics 3d .net-micro-framework


【解决方案1】:

简单的蛮力方法是遍历网格中的每个体素并计算其与球体中心的距离。如果距离小于球体半径,则为体素着色。您可以通过消除平方根并将点积与半径平方进行比较来节省大量指令。

当然,远非最佳。但是在如图所示的 8x8x8 网格上,您需要在每个球体上执行 512 次此操作。如果球心在网格上,并且它的半径是一个整数,你只需要整数数学。点积是 3 次乘法和 2 次加法。乘法很慢;假设他们每个人接受 4 条指令。另外,您需要进行比较。加上加载和存储,假设每个体素需要 20 条指令。每个球体有 10240 条指令。

以 16 MHz 运行的 Arduino 每秒可以推动 1562 个球体。除非你在做大量其他数学和 I/O,否则这个算法应该足够好。

【讨论】:

  • 嗯,问题是他似乎不想要一个实心球体。在这种情况下,您遇到了防止球体表面出现孔洞和膨胀的经典光栅化问题。尽管他仍然可以使用您的解决方案并通过检查它们的 6 个邻居来删除所有内部体素。
  • 哦,对了,你不需要第二遍来移除内部体素,因为对象是隐式定义的。但在这种情况下,您必须平均每个体素进行 1 次以上的距离计算。
  • 是的,起初我以为它是在演示视频中填充的,但仔细观察它看起来只是外壳。我同意你的第一条评论,做一个形态学操作是最简单的。
【解决方案2】:

我不相信在到达两极后在每一层上运行中点圆算法会产生所需的结果,因为在 LED 未点亮的表面上会有间隙。但是,这可能会给出您想要的结果,因此这取决于美学。这篇文章是基于使用中点圆算法来确定通过中间两个垂直八分圆的层的半径,然后在绘制每个圆时同时设置极坐标八分圆的点。

我认为基于@Nick Udall 的评论和回答here,使用圆形算法来确定水平切片的半径将适用于我在对他的回答的评论中提出的修改。应该修改圆算法以将初始误差作为输入,并绘制极坐标八分圆的附加点。

  • 在y0 + y1和y0 - y1处绘制标准圆算法点:x0 +/- x, z0 +/- z, y0 +/- y1,x0 +/- z, z0 +/- x, y0 +/- y1,共16个点。这构成了球体的大部分垂直方向。
  • 另外画点x0 +/- y1, z0 +/- x, y0 +/- z和x0 +/- x, z0 +/- y1, y0 +/- z,共16个点,形成球体的极冠。

通过将外部算法的误差传递给圆算法,它将允许对每一层的圆进行子体素调整。在不将误差传递给内部算法的情况下,圆的赤道将近似为一个圆柱体,x、y、z 轴上的每个近似球面将形成一个正方形。包括误差在内,给定足够大半径的每个面将近似为一个实心圆。


以下代码修改自维基百科的Midpoint circle algorithm。 DrawCircle 算法将命名法更改为在 xz 平面中运行,添加了第三个初始点 y0、y 偏移量 y1 和初始误差 error0。 DrawSphere 是从同一个函数修改而来的,以获取第三个初始点 y0 并调用 DrawCircle 而不是 DrawPixel

public static void DrawCircle(int x0, int y0, int z0, int y1, int radius, int error0)
{
  int x = radius, z = 0;
  int radiusError = error0; // Initial error state passed in, NOT 1-x

  while(x >= z)
  {
    // draw the 32 points here.
    z++;
    if(radiusError<0)
    {
      radiusError+=2*z+1;
    }
    else
    {
      x--;
      radiusError+=2*(z-x+1);
    }
  }
}

public static void DrawSphere(int x0, int y0, int z0, int radius)
{
  int x = radius, y = 0;
  int radiusError = 1-x;

  while(x >= y)
  {
    // pass in base point (x0,y0,z0), this algorithm's y as y1,
    // this algorithm's x as the radius, and pass along radius error.
    DrawCircle(x0, y0, z0, y, x, radiusError);
    y++;
    if(radiusError<0)
    {
      radiusError+=2*y+1;
    }
    else
    {
      x--;
      radiusError+=2*(y-x+1);
    }
  }
}

对于半径为 4 的球体(实际上需要 9x9x9),这将运行 DrawCircle 例程的三次迭代,第一次绘制一个典型的半径为 4 的圆(三个步骤),第二次绘制一个半径为 4 的圆初始误差为 0(也是三个步骤),然后第三个绘制一个半径为 3 的圆,初始误差为 0(也是三个步骤)。最终得到 9 个计算点,每个点绘制 32 个像素。 这使得 32(每个圆的点)x 3(每个点的加法或减法操作)+ 6(每次迭代的加法、减法、移位操作)=每个计算点的 102 次加法、减法或移位操作。在此示例中,每个圆圈 3 个点 = 每层 306 次操作。半径算法还每层增加了 6 次操作,迭代 3 次,所以 306 + 6 * 3 = 936 基本算术运算对于示例半径为 4。 这里的代价是您将重复设置一些像素而无需额外的条件检查(即 x = 0、y = 0 或 z = 0),因此如果您的 I/O 很慢,您最好添加条件检查。假设所有 LED 在开始时都被清除,示例圆圈将设置 288 个 LED,而由于重复设置,实际点亮的 LED 会少得多。

对于适合 8​​x8x8 网格的所有球体,这看起来比蛮力法的表现要好,但蛮力法无论半径如何都具有一致的时序,而在绘制大半径球体时,这种方法会减慢速度,其中只有部分将被显示。然而,随着显示立方体分辨率的提高,该算法时序将保持一致,而蛮力将增加。

【讨论】:

  • 我不明白绘制情节的实际部分在哪里
【解决方案3】:

假设你已经有你所说的绘图功能:

    public static void DrawSphere(double r, int lats, int longs)
    {
        int i, j;
        for (i = 0; i <= lats; i++)
        {
            double lat0 = Math.PI * (-0.5 + (double)(i - 1) / lats);
            double z0 = Math.Sin(lat0) * r;
            double zr0 = Math.Cos(lat0) * r;

            double lat1 = Math.PI * (-0.5 + (double)i / lats);
            double z1 = Math.Sin(lat1) * r;
            double zr1 = Math.Cos(lat1) * r;

            for (j = 0; j <= longs; j++)
            {
                double lng = 2 * Math.PI * (double)(j - 1) / longs;
                double x = Math.Cos(lng);
                double y = Math.Sin(lng);

                plot(x * zr0, y * zr0, z0);
                plot(x * zr1, y * zr1, z1);
            }
        }
    }

该函数应该在原点绘制一个具有指定纬度和经度分辨率的球体(根据您的立方体判断,您可能需要大约 40 或 50 作为粗略猜测)。虽然该算法不会“填充”球体,因此它只会提供轮廓,但使用半径应该可以让您填充内部,可能会随着沿途经纬度的分辨率降低。

【讨论】:

  • 你不需要在绘图调用中将坐标乘以 r 吗?因为它是 r 未使用,这意味着它将始终绘制一个半径为 1 的球体。
  • 为什么是 2 个地块?有什么区别。
【解决方案4】:

刚刚发现了一个关于生成球体网格的旧问答,但上面的答案实际上给了你一小段伪代码来生成你的 X、Y 和 Z:

(x, y, z) = (sin(Pi * m/M) cos(2Pi * n/N), sin(Pi * m/M) sin(2Pi * n/N), cos(Pi * m/M))

查看此问答了解详情:) procedurally generate a sphere mesh

【讨论】:

  • 这不只是 NominSim 的解决方案吗?
【解决方案5】:

我的解决方案使用浮点数学而不是整数数学并不理想,但它可以工作。

private static void DrawSphere(float radius, int posX, int poxY, int posZ)
{
    // determines how far apart the pixels are
    float density = 1;

    for (float i = 0; i < 90; i += density)
    {
        float x1 = radius * Math.Cos(i * Math.PI / 180);
        float y1 = radius * Math.Sin(i * Math.PI / 180);

        for (float j = 0; j < 45; j += density)
        {
            float x2 = x1 * Math.Cos(j * Math.PI / 180);
            float y2 = x1 * Math.Sin(j * Math.PI / 180);
            
            int x = (int)Math.Round(x2) + posX;
            int y = (int)Math.Round(y1) + posY;
            int z = (int)Math.Round(y2) + posZ;

            DrawPixel(x, y, z);
            DrawPixel(x, y, -z);
            DrawPixel(-x, y, z);
            DrawPixel(-x, y, -z);

            DrawPixel(z, y, x);
            DrawPixel(z, y, -x);
            DrawPixel(-z, y, x);
            DrawPixel(-z, y, -x);

            DrawPixel(x, -y, z);
            DrawPixel(x, -y, -z);
            DrawPixel(-x, -y, z);
            DrawPixel(-x, -y, -z);

            DrawPixel(z, -y, x);
            DrawPixel(z, -y, -x);
            DrawPixel(-z, -y, x);
            DrawPixel(-z, -y, -x);
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-12-07
    • 1970-01-01
    • 2022-06-18
    • 2014-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-19
    相关资源
    最近更新 更多