OpenGL如何在不使用gluSphere()函数的情况下绘制球体?

85

有没有教程可以解释如何在OpenGL中绘制球体而不使用gluSphere()

许多关于OpenGL的3D教程只涉及立方体。我已经搜索了很多,但是大多数绘制球体的解决方案都是使用gluSphere()。也有网站提供了绘制球体的代码,链接为这个,但是它没有解释绘制球体的数学原理。该链接还提供了其他绘制球体的版本,使用的是多边形而不是四边形。但是同样地,我不理解代码如何绘制球体。我希望能够可视化球体,以便以后需要时进行修改。


3
请查阅球坐标系以获取数学解释(特别是从球坐标系到直角坐标系的转换)。 - Ned Bingham
10个回答

293

你可以通过使用一个具有三角形面的正多面体来完成这个任务,例如一个八面体。然后,将每个三角形递归地分割成更小的三角形,如下所示:

recursively drawn triangles

当您拥有足够数量的点时,您需要对它们的向量进行归一化,使它们都与固体中心保持恒定距离。这会导致侧面凸出成类似于球体的形状,并且随着点数增加,平滑度也会增加。

在这里,归一化意味着移动一个点,使其相对于另一个点的角度相同,但它们之间的距离不同。以下是一个二维示例。

enter image description here

A和B之间相距6个单位。但是假设我们想要在AB线上找到一个离A点12个单位远的点。

enter image description here

我们可以说C是相对于A归一化后距离为12的B的标准形式。我们可以使用以下代码获取C:
#returns a point collinear to A and B, a given distance away from A. 
function normalize(a, b, length):
    #get the distance between a and b along the x and y axes
    dx = b.x - a.x
    dy = b.y - a.y
    #right now, sqrt(dx^2 + dy^2) = distance(a,b).
    #we want to modify them so that sqrt(dx^2 + dy^2) = the given length.
    dx = dx * length / distance(a,b)
    dy = dy * length / distance(a,b)
    point c =  new point
    c.x = a.x + dx
    c.y = a.y + dy
    return c

如果我们对许多点进行归一化处理,所有点都相对于同一点A并且与相同距离R相关,则归一化后的点将全部位于以A为圆心,半径为R的圆弧上。

bulging line segment

这里,黑色点从一条线开始并“凸出”成弧线。

这个过程可以扩展到三维空间,这时你会得到一个球体而不是圆。只需在normalize函数中添加一个dz分量即可。

normalized polygons

level 1 bulging octahedron level 3 bulging octahedron

如果你看Epcot的球体,你可以看到这种技术在起作用。它是一个十二面体,其面部凸出使其看起来更加圆润。


1
我宁愿删除对Epcot球的链接。这可能会让初学者感到困惑,因为每个三角形都再次被分成三个等腰三角形(类似于sqrt(3)-subdivision的第一部分)。我相信你可以找到更好的例子。 - Christian Rau
1
@xEnOn,我已经编辑了我的答案,更详细地解释了规范化。我认为问题在于规范化并不是我试图解释的实际技术术语,因此你很难在其他任何地方找到更多信息。对此我感到抱歉。 - Kevin
为什么这是对这种技术唯一好的描述?非常感谢! - Cosine
1
也许更好的解释“规范化”过程是将点投影到球体上。此外,请注意,结果取决于规范化/投影是仅在最后一次应用(在所有细分之后,这似乎是建议的)还是与(递归)细分步骤交错应用。似乎仅在最后一次投影会产生靠近初始八面体顶点的顶点聚集,而交错细分和投影则会产生顶点之间的均匀距离。 - Tyler Streeter
喜欢这些动画!特别是它们改变旋转方向的方式 ;) - Ciprian Tomoiagă
显示剩余6条评论

32
我将进一步解释使用纬度和经度生成球体的流行方法(另一种名为 icospheres 的方法已在撰写本文时的最受欢迎答案中解释过)。
一个球可以用以下参数方程表示: F(u, v) = [ cos(u)*sin(v)*r, cos(v)*r, sin(u)*sin(v)*r ]
其中:
- r 是半径; - u 是经度,范围从 0 到 2π;以及 - v 是纬度,范围从 0 到 π。
然后,生成球体涉及在固定间隔处评估参数函数。
例如,要生成 16 条经线,则沿着 u 轴将有 17 条网格线,步长为 π/8 (2π/16)(第 17 条线绕回)。
下面的伪代码通过在规则间隔处评估参数函数来生成三角形网格(这适用于 任何 参数曲面函数,而不仅仅是球体)。
在下面的伪代码中,UResolution 是沿 U 轴(这里是经线)的网格点数,VResolution 是沿 V 轴(这里是纬线)的网格点数。
var startU=0
var startV=0
var endU=PI*2
var endV=PI
var stepU=(endU-startU)/UResolution // step size between U-points on the grid
var stepV=(endV-startV)/VResolution // step size between V-points on the grid
for(var i=0;i<UResolution;i++){ // U-points
 for(var j=0;j<VResolution;j++){ // V-points
 var u=i*stepU+startU
 var v=j*stepV+startV
 var un=(i+1==UResolution) ? endU : (i+1)*stepU+startU
 var vn=(j+1==VResolution) ? endV : (j+1)*stepV+startV
 // Find the four points of the grid
 // square by evaluating the parametric
 // surface function
 var p0=F(u, v)
 var p1=F(u, vn)
 var p2=F(un, v)
 var p3=F(un, vn)
 // NOTE: For spheres, the normal is just the normalized
 // version of each vertex point; this generally won't be the case for
 // other parametric surfaces.
 // Output the first triangle of this grid square
 triangle(p0, p2, p1)
 // Output the other triangle of this grid square
 triangle(p3, p1, p2)
 }
}

踩票似乎有点过分了。这是唯一一个通过球的参数方程提到离散构造示例的答案之一。从基础上来说,它也可能更容易理解,因为球可以被视为一堆随着接近极点而缩小的圆。 - Spacen Jasset
2
你好,我只是想指出p0、p1、p2、p3的每个值的第二个应该是v或vn,而不是u或un。 - nicole

10

这个示例中的代码很容易理解。你应该查看函数void drawSphere(double r, int lats, int longs)

void drawSphere(double r, int lats, int longs) {
    int i, j;
    for(i = 0; i <= lats; i++) {
        double lat0 = M_PI * (-0.5 + (double) (i - 1) / lats);
        double z0  = sin(lat0);
        double zr0 =  cos(lat0);

        double lat1 = M_PI * (-0.5 + (double) i / lats);
        double z1 = sin(lat1);
        double zr1 = cos(lat1);

        glBegin(GL_QUAD_STRIP);
        for(j = 0; j <= longs; j++) {
            double lng = 2 * M_PI * (double) (j - 1) / longs;
            double x = cos(lng);
            double y = sin(lng);

            glNormal3f(x * zr0, y * zr0, z0);
            glVertex3f(r * x * zr0, r * y * zr0, r * z0);
            glNormal3f(x * zr1, y * zr1, z1);
            glVertex3f(r * x * zr1, r * y * zr1, r * z1);
        }
        glEnd();
    }
}

参数lat定义了球体中要有多少个水平线,lon定义了要有多少个垂直线。而r则是你的球体的半径。

现在进行了一个双重迭代来计算顶点坐标,使用简单的三角函数来计算。

计算得到的顶点现在使用glVertex...()作为GL_QUAD_STRIP发送到你的GPU,这意味着你正在发送每两个顶点,这些顶点与先前发送的两个顶点形成一个四边形。

现在你只需要理解三角函数的工作原理,但我想你可以很容易地弄清楚。


2
半径缺失。 - tomasantunes
1
第一个参数“double r”未被使用。 - ollydbg23
1
没错。代码示例不是我的原始答案的一部分。@genpfault:您在编辑中添加了代码示例。您能修复一下这个例子吗? - Constantinius
1
感谢您的支持 :) - Constantinius
这段代码简洁且运行良好。你还可以用一些有趣的技巧来玩它。例如,lat = 2,使用大型longs - 你会得到一个双锥体。当lat = 2和long = 4时,你会得到一个八面体。减少外部for循环中的迭代次数,你就可以得到部分球体(除以2得到半球)。 - Locutus
显示剩余5条评论

2

这是一个关于如何使用“三角带”绘制“极坐标”球体的示例,它需要成对地绘制点:

const float PI = 3.141592f;
GLfloat x, y, z, alpha, beta; // Storage for coordinates and angles        
GLfloat radius = 60.0f;
int gradation = 20;

for (alpha = 0.0; alpha < GL_PI; alpha += PI/gradation)
{        
    glBegin(GL_TRIANGLE_STRIP);
    for (beta = 0.0; beta < 2.01*GL_PI; beta += PI/gradation)            
    {            
        x = radius*cos(beta)*sin(alpha);
        y = radius*sin(beta)*sin(alpha);
        z = radius*cos(alpha);
        glVertex3f(x, y, z);
        x = radius*cos(beta)*sin(alpha + PI/gradation);
        y = radius*sin(beta)*sin(alpha + PI/gradation);
        z = radius*cos(alpha + PI/gradation);            
        glVertex3f(x, y, z);            
    }        
    glEnd();
}

第一个点(glVertex3f)遵循参数方程,第二个点则向下一个平行线移动了一个alpha角度的步长。


底部存在问题,在行模式下可能会绘制两次,不确定具体情况,似乎是一种常见的问题。另外一条线在竖直方向上可见。 - oOo

2

enter image description here

   void draw_sphere(float r)
    {
    
        float pi = 3.141592;
        float di = 0.02;
        float dj = 0.04;
        float db = di * 2 * pi;
        float da = dj * pi;
    
    
        for (float i = 0; i < 1.0; i += di) //horizonal
            for (float j = 0; j < 1.0; j += dj) //vertical
            {
                float b = i * 2 * pi;      //0     to  2pi
                float a = (j - 0.5) * pi;  //-pi/2 to pi/2
    
    
                //normal
                glNormal3f(
                    cos(a + da / 2) * cos(b + db / 2),
                    cos(a + da / 2) * sin(b + db / 2),
                    sin(a + da / 2));
    
                glBegin(GL_QUADS);
                //P1
                    glTexCoord2f(i, j);
                    glVertex3f(
                        r * cos(a) * cos(b),
                        r * cos(a) * sin(b),
                        r * sin(a));
                //P2
                    glTexCoord2f(i + di, j);//P2
                    glVertex3f(
                        r * cos(a) * cos(b + db),
                        r * cos(a) * sin(b + db),
                        r * sin(a));
                //P3
                    glTexCoord2f(i + di, j + dj);
                    glVertex3f(
                        r * cos(a + da) * cos(b + db),
                        r * cos(a + da) * sin(b + db),
                        r * sin(a + da));
                //P4
                    glTexCoord2f(i, j + dj);
                    glVertex3f(
                        r * cos(a + da) * cos(b),
                        r * cos(a + da) * sin(b),
                        r * sin(a + da));
                glEnd();
            }
    }

1
在哪里放置半径? - oOo
似乎在使用GLScene和GLMesh获取这些顶点时无法正常工作。 - oOo
目前我对此持有保留意见,直到半径被加入其中,不过纹理选项还是有点有趣的。 - oOo

2

如果你想像狡猾的狐狸一样,可以从GLU中复制代码。请查看MesaGL源代码(http://cgit.freedesktop.org/mesa/mesa/)。


5
虽然我理解在这个上下文中“half-inch”的意思,但我认为你可能想要编辑一下,以便其他95%不熟悉伦敦俚语的读者也能理解。 - Flexo

2

1

@Constantinius 的回答的 Python 版本:

lats = 10
longs = 10
r = 10

for i in range(lats):
    lat0 = pi * (-0.5 + i / lats)
    z0 = sin(lat0)
    zr0 = cos(lat0)

    lat1 = pi * (-0.5 + (i+1) / lats)
    z1 = sin(lat1)
    zr1 = cos(lat1)

    glBegin(GL_QUAD_STRIP)
    for j in range(longs+1):
        lng = 2 * pi * (j+1) / longs
        x = cos(lng)
        y = sin(lng)

        glNormal(x * zr0, y * zr0, z0)
        glVertex(r * x * zr0, r * y * zr0, r * z0)
        glNormal(x * zr1, y * zr1, z1)
        glVertex(r * x * zr1, r * y * zr1, r * z1)

    glEnd()

1
虽然被接受的答案解决了问题,但最后有一点误解。十二面体是正多面体,所有面积相同。这似乎是Epcot的情况(顺便说一下,它根本不是十二面体)。由于@Kevin提出的解决方案没有提供这个特征,所以我想我可以添加一个具有此特征的方法。
生成N面多面体的好方法,使所有顶点都位于同一球体中,并且所有面的面积/表面类似,是从二十面体开始,迭代地细分和归一化其三角形面(如接受的答案所建议的)。例如,十二面体实际上是截短的二十面体。

正二十面体有20个面(12个顶点),可以通过3个黄金矩形轻松构建;只需要以这个作为起点而不是一个八面体。你可以在这里找到一个例子。

我知道这有点偏题,但我相信如果有人寻找这个特定情况的话,这可能会有所帮助。


0
一种方法是创建一个面向相机的四边形,编写顶点和片段着色器,渲染出类似于球体的东西。可以使用互联网上找到的圆/球的方程式。
一个好处是,球体的轮廓从任何角度看起来都一样。但是,如果球体不在透视图中心,则可能看起来更像椭圆形。你可以求出这个方程,并将其放在片段着色器中。然后,光照需要随着玩家移动而改变,如果你确实有一个在3D空间中围绕球体运动的玩家。
有人尝试过这种方法吗?或者这种方法会过于昂贵,无法实现?

这只适用于平行投影。如果使用透视投影,则渲染输出中的球体轮廓通常不是圆形。 - Reto Koradi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接