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。

我的习惯: 球体分段数用 2 的幂次(16、32、64),这样纹理映射时不容易出现锯齿。另外,记得把纬线首尾的顶点合并,避免出现缝隙。

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/r 的比值决定了环面的「胖瘦」。R/r > 3 时看起来像甜甜圈;R/r 接近 1 时,中间的洞会变得很小,几乎像个球。

我在做轮胎建模时用过这个。当时需要生成一个带花纹的环面,就是在副半径上叠加了一个正弦波——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 知识结构总览

下面这张图把四种几何体的生成逻辑串起来了。你可以看到,它们本质上都是「二维参数空间到三维空间的映射」。

基本几何体参数化生成知识结构 参数化生成 球体 立方体 圆柱体 环面 核心参数:半径/边长、分段数(经/纬、高/径向、大圆/小圆)、角度范围 球体属性 · 经纬网格 · 法线=位置归一化 · 纹理:UV映射 立方体属性 · 6个独立面 · 法线面法方向 · 顶点不共享 圆柱体属性 · 侧面+顶底盖 · 径向法线 · 扇形顶盖 环面属性 · 双半径R/r · 大圆+小圆 · 法线从管心 生成流程总结 确定参数 → 双重循环遍历参数空间 → 计算顶点坐标/法线/纹理坐标 → 构建三角形索引 优化方向:LOD分级、顶点缓存优化、法线平滑处理
我的建议: 把这四种几何体的生成函数封装成工具库。参数接口统一为 (segments, size, ...) 的形式。这样上层调用时只需要改参数,不用关心内部实现。我在三个项目里复用同一套代码,省了不少事。

好了,基本几何体的参数化生成就讲到这里。记住一个原则:参数化不是死记公式,而是理解「如何用两个方向的控制点描述一个曲面」。掌握了这个思路,后面讲贝塞尔曲面和NURBS时你会觉得豁然开朗。