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 |
4.2 几何体参数详解
每个几何体都有自己的一套参数。但有个规律:分段数(Segments) 几乎无处不在。它决定了模型的平滑度。
拿 SphereGeometry 举例:
const sphere = new THREE.SphereGeometry(
1, // 半径
32, // 水平分段数
16 // 垂直分段数
);
分段数越高,球越圆,但顶点数也越多。我在项目中做过一个移动端AR应用,球体分段数设到64,结果帧率直接掉到20。后来改成32,肉眼几乎看不出区别,帧率却稳在60。
再比如 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 性能建议
最后聊点实际的。几何体用多了,性能会成问题。我的经验是:
- 复用几何体 — 同一个几何体可以给多个
Mesh使用,不会增加内存。 - 合并几何体 — 用
BufferGeometryUtils.mergeGeometries()把多个几何体合并成一个,减少 draw call。 - 控制分段数 — 够用就行,别盲目追求平滑。
- 使用索引 — 共享顶点能省不少内存。
嗯,几何体这块内容就这些。下一章咱们聊聊材质,让这些光秃秃的几何体穿上衣服。