4、几何体(Geometry):内置几何体与BufferGeometry原理

几何体,说白了就是3D物体的「骨架」。没有几何体,你的模型就是一团空气。Three.js内置了十几种常用几何体,足够覆盖大部分日常需求。我个人习惯先把这些内置几何体玩熟,再考虑自定义。

4.1 内置几何体一览

Three.js的几何体都在 THREE 命名空间下。我挑几个最常用的说说:

几何体 构造函数 典型参数
立方体 BoxGeometry width, height, depth
球体 SphereGeometry radius, widthSegments, heightSegments
圆柱体 CylinderGeometry radiusTop, radiusBottom, height, radialSegments
平面 PlaneGeometry width, height
圆环 TorusGeometry radius, tube, radialSegments, tubularSegments
圆环结 TorusKnotGeometry radius, tube, tubularSegments, radialSegments, p, q
小提示: 圆环结的 p 和 q 参数控制缠绕方式。我刚开始调这两个参数时,经常绕出奇形怪状的东西。建议从 p=2, q=3 开始试。

4.2 几何体参数详解

每个几何体都有自己的一套参数。但有个规律:分段数(Segments) 几乎无处不在。它决定了模型的平滑度。

SphereGeometry 举例:

const sphere = new THREE.SphereGeometry(
  1,      // 半径
  32,     // 水平分段数
  16      // 垂直分段数
);

分段数越高,球越圆,但顶点数也越多。我在项目中做过一个移动端AR应用,球体分段数设到64,结果帧率直接掉到20。后来改成32,肉眼几乎看不出区别,帧率却稳在60。

注意: 分段数不是越高越好。32x32的球体有1024个顶点,64x64就是4096个。移动端建议不超过32。

再比如 CylinderGeometry

const cylinder = new THREE.CylinderGeometry(
  0.5,    // 顶部半径
  1,      // 底部半径
  2,      // 高度
  8,      // 径向分段数
  1,      // 高度分段数
  false   // 是否封闭两端
);

顶部和底部半径不同,就能做出锥体。我曾经用这个参数做过一个漏斗模型,效果很赞。

4.3 BufferGeometry 原理

好,重点来了。所有内置几何体,底层都是 BufferGeometry。它到底是什么?

说白了,BufferGeometry 就是一堆 缓冲区(Buffer) 的集合。每个缓冲区存储一种属性数据,比如位置、法线、颜色、UV坐标。

我画个简单的结构图:

BufferGeometry
├── attributes (属性集合)
│   ├── position  → Float32Array (顶点坐标)
│   ├── normal    → Float32Array (法线方向)
│   ├── uv        → Float32Array (纹理坐标)
│   └── color     → Float32Array (顶点颜色)
├── index        → Uint16Array (索引数组)
└── boundingBox  → Box3 (包围盒)

为什么要用缓冲区?因为 GPU 喜欢连续的内存块。你把数据打包成 Float32Array,一次性传给显卡,效率比零散传高得多。

4.3.1 手动创建几何体

光说不练假把式。咱们手动创建一个三角形:

const geometry = new THREE.BufferGeometry();

// 顶点数据:三个点,每个点有 x, y, z
const vertices = new Float32Array([
  0, 0, 0,   // 顶点A
  1, 0, 0,   // 顶点B
  0, 1, 0    // 顶点C
]);

// 设置 position 属性
geometry.setAttribute('position', 
  new THREE.BufferAttribute(vertices, 3)
);

// 法线数据(每个顶点的法线方向)
const normals = new Float32Array([
  0, 0, 1,   // 顶点A的法线
  0, 0, 1,   // 顶点B的法线
  0, 0, 1    // 顶点C的法线
]);

geometry.setAttribute('normal',
  new THREE.BufferAttribute(normals, 3)
);

嗯,这里要注意:BufferAttribute 的第二个参数是 每个顶点占几个分量。位置是3个(x,y,z),颜色是4个(r,g,b,a)。

4.3.2 索引几何体

如果两个三角形共享顶点,可以用索引来节省内存。举个例子,一个正方形由两个三角形组成:

const geometry = new THREE.BufferGeometry();

// 4个顶点
const vertices = new Float32Array([
  -1, -1, 0,  // 0
   1, -1, 0,  // 1
   1,  1, 0,  // 2
  -1,  1, 0   // 3
]);

geometry.setAttribute('position',
  new THREE.BufferAttribute(vertices, 3)
);

// 索引:两个三角形,共6个索引
const indices = new Uint16Array([
  0, 1, 2,   // 第一个三角形
  0, 2, 3    // 第二个三角形
]);

geometry.setIndex(
  new THREE.BufferAttribute(indices, 1)
);

你看,4个顶点 + 6个索引,就拼出了一个正方形。如果不使用索引,需要6个顶点(每个三角形3个)。顶点越多,索引的优势越明显。

核心要点: 索引几何体能减少顶点重复,降低内存占用。我在做复杂模型时,一定会用索引。

4.4 实战:自定义一个六边形

咱们把知识串起来,手动创建一个六边形几何体:

function createHexagon(radius = 1) {
  const geometry = new THREE.BufferGeometry();
  const segments = 6;
  const vertices = [];
  const indices = [];

  // 中心点
  vertices.push(0, 0, 0);

  // 外围6个顶点
  for (let i = 0; i < segments; i++) {
    const angle = (i / segments) * Math.PI * 2;
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    vertices.push(x, y, 0);
  }

  // 索引:中心点 + 相邻外围点
  for (let i = 1; i <= segments; i++) {
    const next = (i % segments) + 1;
    indices.push(0, i, next);
  }

  geometry.setAttribute('position',
    new THREE.BufferAttribute(new Float32Array(vertices), 3)
  );
  geometry.setIndex(
    new THREE.BufferAttribute(new Uint16Array(indices), 1)
  );

  return geometry;
}

const hexGeo = createHexagon(2);

这段代码生成一个六边形,由6个三角形组成。你想想看,如果改成 segments = 64,它就是一个圆。这就是 CircleGeometry 的底层原理。

4.5 常用几何体方法

所有 BufferGeometry 实例都有一些实用方法:

  • computeVertexNormals() — 自动计算法线。如果你手动创建几何体后发现光照不对,八成是忘了这步。
  • center() — 将几何体居中。基于包围盒计算。
  • translate(x, y, z) — 平移所有顶点。
  • rotateX(angle) — 绕X轴旋转。
  • scale(x, y, z) — 缩放顶点。

我曾经犯过一个错:手动创建了一个模型,忘了调用 computeVertexNormals(),结果模型看起来像被砸扁了一样。排查了半天才发现是法线没算。

避坑指南: 每次手动创建 BufferGeometry 后,记得调用 computeVertexNormals()。除非你明确知道自己在做什么。

4.6 性能建议

最后聊点实际的。几何体用多了,性能会成问题。我的经验是:

  1. 复用几何体 — 同一个几何体可以给多个 Mesh 使用,不会增加内存。
  2. 合并几何体 — 用 BufferGeometryUtils.mergeGeometries() 把多个几何体合并成一个,减少 draw call。
  3. 控制分段数 — 够用就行,别盲目追求平滑。
  4. 使用索引 — 共享顶点能省不少内存。

嗯,几何体这块内容就这些。下一章咱们聊聊材质,让这些光秃秃的几何体穿上衣服。