使用3D像素(体素)绘制球体

时间:2012-01-31 17:35:42

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

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

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

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

该项目与此非常相似:

3D LED cube

...或在行动here中查看。

我想到的一个可能性是:

  • 计算极点的Y坐标(给定半径)(对于以原点为中心的球体,这些将是-r+r
  • 切割球体:对于这些坐标之间的每个水平面p i ,计算通过将所述平面与球体相交得到的圆的半径=&gt; r <子> I
  • 使用Bresenham算法在平面r i 上绘制半径p i 的实际圆圈。

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

5 个答案:

答案 0 :(得分:8)

简单的强力方法是在网格中的每个体素上循环并计算它与球体中心的距离。如果体素的距离小于球体半径,则对体素着色。您可以通过消除平方根并将点积与半径平方进行比较来保存大量指令。

远非最佳,当然。但是如图所示,在8x8x8网格上,每个球体需要执行512次此操作。如果球体中心位于网格上,并且其半径是整数,则只需要整数数学。点积为3乘以2加。乘法很慢;假设他们各自收到4条指令。另外,你需要进行比较。添加加载和存储,假设每个体素需要20条指令。这是每个球体10240条指令。

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

答案 1 :(得分:4)

假设你已经有了一个像你说的那样的情节函数:

    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左右的粗略猜测)。虽然这个算法不会“填充”球体,所以它只会提供一个轮廓,但是使用半径可以让你填充内部,可能会降低沿途的拉力和长度。

答案 2 :(得分:4)

我不相信在每个层上运行中点圆算法一旦到达极点就会得到所需的结果,因为在没有点亮LED的表面会有间隙。然而,这可能会给出您想要的结果,因此这将达到美学效果。这篇文章基于使用中点圆算法来确定通过中间两个垂直八分圆的层的半径,然后在绘制每个圆时也设置极性八分圆的点。

我认为根据@Nick Udall的评论并回答here使用圆形算法确定水平切片的半径将与我在回答评论中提出的修改一起使用。应修改圆算法以将初始误差作为输入,并绘制极性八分圆的附加点。

  • y0 + y1y0 - y1x0 +/- x, z0 +/- z, y0 +/- y1x0 +/- z, z0 +/- x, y0 +/- y1,总共16个点绘制标准圆算法点。这形成了球体垂直的大部分。
  • 另外绘制点x0 +/- y1, z0 +/- x, y0 +/- zx0 +/- x, z0 +/- y1, y0 +/- z,总共16个点,这将构成球体的极限。

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


以下代码是从维基百科的Midpoint circle algorithm修改而来的。 DrawCircle算法的命名法已更改为在xz平面中运行,添加了第三个初始点y0,y偏移y1和初始错误error0DrawSphere已从同一功能修改为第三个初始点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个操作。 radius算法还为每层添加6个操作并迭代3次,因此306 + 6 * 3 = 936示例半径为4的基本算术运算。 这里的成本是你将重复设置一些没有附加条件检查的像素(即x = 0,y = 0或z = 0),所以如果你的I / O很慢,你可能最好添加条件检查。假设所有LED在开始时都被清除,示例圈将设置288个LED,而由于重复设置,实际上将点亮的LED数量更少。

对于适合8x8x8网格的所有球体而言,这似乎比bruteforce方法表现更好,但是无论半径如何,暴力方法都会有一致的时间,而这种方法在绘制大半径球体时会减慢部分将显示。然而,随着显示立方体分辨率的增加,这种算法时间将保持一致,而暴力会增加。

答案 3 :(得分:1)

刚刚找到了一个关于生成Sphere Mesh的旧q&amp; a,但最佳答案实际上为您提供了一小段伪代码来生成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))

查看此Q&amp; A了解详情:) procedurally generate a sphere mesh

答案 4 :(得分:0)

我的解决方案使用浮点数学而不是整数数学并不理想,但它有效。

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);
        }
    }
}