3. 基本几何体生成:球体、立方体、圆柱体、环面的参数化生成算法
说到基本几何体,很多人觉得不就是调个API吗?glCreateSphere() 一行代码搞定。但实际做项目时你会发现,不懂参数化生成,连个像样的球体都调不出来。我早年做三维扫描重建时,就因为球体顶点分布不均匀,导致光照计算出现奇怪的条纹,排查了整整两天。
说白了,参数化生成就是「用数学公式描述几何体表面每个点的位置」。你给出一组参数(半径、分段数、角度范围),程序就能算出所有顶点坐标。这是所有复杂几何建模的基石。
3.1 球体参数化生成
球体是最常用的基本体。它的参数化公式其实很简单——就是球坐标系转直角坐标系。
// 球体顶点生成伪代码
for (j = 0; j <= latSegments; j++) {
theta = j * PI / latSegments; // 极角 0~PI
for (i = 0; i <= lonSegments; i++) {
phi = i * 2 * PI / lonSegments; // 方位角 0~2PI
x = radius * sin(theta) * cos(phi);
y = radius * cos(theta);
z = radius * sin(theta) * sin(phi);
// 同时计算法线(归一化位置向量)
nx = x / radius;
ny = y / radius;
nz = z / radius;
// 纹理坐标
u = (float)i / lonSegments;
v = (float)j / latSegments;
}
}
这里有个坑,我踩过。极角 theta 从 0 到 PI,不是 0 到 2PI。如果搞反了,球体上下会颠倒。另外,latSegments 和 lonSegments 的取值直接影响性能——游戏里用 16x16 就够了,但做高精度渲染我建议至少 64x64。
3.2 立方体参数化生成
立方体看起来简单,但参数化生成反而比球体麻烦。为什么?因为球体是光滑表面,立方体有棱边和角,需要处理 6 个独立的面。
我建议的做法是:分别生成 6 个面,每个面是一个独立的网格。这样纹理坐标和法线都好处理。
// 立方体单个面生成(以正面为例)
function generateFace(faceDir, size, segments) {
half = size / 2;
step = size / segments;
for (j = 0; j <= segments; j++) {
for (i = 0; i <= segments; i++) {
// 根据 faceDir 确定三个轴的分量
// 例如正面:x = -half + i*step, y = -half + j*step, z = half
// 法线固定为 (0, 0, 1)
// 纹理坐标 u = i/segments, v = j/segments
}
}
}
你想想看,6 个面各生成一次,顶点总数是 6 * (segments+1)^2。如果 segments=1,就是最简的 8 顶点立方体;segments=2 以上,每个面就有更多细分,可以做曲面细分或贴图。
3.3 圆柱体参数化生成
圆柱体可以看作「球体的简化版」——一个方向是直线,另一个方向是圆。它由三部分组成:顶盖、底盖、侧面。
| 部件 | 生成方式 | 顶点数 | 法线方向 |
|---|---|---|---|
| 侧面 | 沿高度分段 + 沿圆周分段 | (hSeg+1) × (rSeg+1) | 径向向外 |
| 顶盖 | 扇形网格(从中心到边缘) | rSeg+1(含中心点) | (0, 1, 0) |
| 底盖 | 同上,法线朝下 | rSeg+1 | (0, -1, 0) |
// 圆柱体侧面生成
for (j = 0; j <= heightSegments; j++) {
y = -height/2 + j * height / heightSegments;
for (i = 0; i <= radialSegments; i++) {
angle = i * 2 * PI / radialSegments;
x = radius * cos(angle);
z = radius * sin(angle);
// 法线 = (cos(angle), 0, sin(angle))
// 纹理坐标 u = i/radialSegments, v = j/heightSegments
}
}
嗯,这里要注意顶盖和底盖的三角形连接方式。侧面用四边形网格,顶盖用扇形三角形(中心点 + 边缘一圈)。如果不做顶盖,就是个空心管——这在管道建模中很常用。
3.4 环面参数化生成
环面(Torus)是这四种几何体里最「绕」的一个。它有两个半径:主半径 R(管心到环心)和副半径 r(管截面半径)。
公式看起来复杂,但拆开看其实就两步:先绕大圆走一圈,再绕小圆走一圈。
// 环面顶点生成
for (j = 0; j <= majorSegments; j++) {
u = j * 2 * PI / majorSegments; // 大圆角度
for (i = 0; i <= minorSegments; i++) {
v = i * 2 * PI / minorSegments; // 小圆角度
// 先算小圆中心位置
centerX = R * cos(u);
centerZ = R * sin(u);
// 再算小圆上的点
x = centerX + r * cos(v) * cos(u);
y = r * sin(v);
z = centerZ + r * cos(v) * sin(u);
// 法线 = 从管心指向表面
nx = cos(v) * cos(u);
ny = sin(v);
nz = cos(v) * sin(u);
}
}
我在做轮胎建模时用过这个。当时需要生成一个带花纹的环面,就是在副半径上叠加了一个正弦波——r = r0 + amplitude * sin(frequency * v)。效果还不错,但分段数不够时会出现锯齿,后来我把 minorSegments 提到了 48。
3.5 四种几何体的对比与选型
| 几何体 | 参数维度 | 典型分段数 | 顶点数公式 | 适用场景 |
|---|---|---|---|---|
| 球体 | 2(经、纬) | 32×32 | (lat+1)×(lon+1) | 星球、灯泡、碰撞体 |
| 立方体 | 2(每个面) | 1~4 | 6×(seg+1)^2 | 建筑、箱子、边界框 |
| 圆柱体 | 2(高、径向) | 16×16 | (h+1)×(r+1) + 2×(r+1) | 柱子、管道、轴类 |
| 环面 | 2(大圆、小圆) | 32×16 | (major+1)×(minor+1) | 轮胎、甜甜圈、线圈 |
选型时我一般这么判断:需要光滑表面用球体或环面,需要平面用立方体,需要旋转对称用圆柱体。如果要做布尔运算(比如挖洞),立方体最好处理,因为它的拓扑结构最简单。
3.6 知识结构总览
下面这张图把四种几何体的生成逻辑串起来了。你可以看到,它们本质上都是「二维参数空间到三维空间的映射」。
(segments, size, ...) 的形式。这样上层调用时只需要改参数,不用关心内部实现。我在三个项目里复用同一套代码,省了不少事。
好了,基本几何体的参数化生成就讲到这里。记住一个原则:参数化不是死记公式,而是理解「如何用两个方向的控制点描述一个曲面」。掌握了这个思路,后面讲贝塞尔曲面和NURBS时你会觉得豁然开朗。