几何体与缓冲
内置方块和球体只是方便,真实地形、建筑轮廓、点云都要自己往缓冲里写数据。 理解顶点属性与索引的关系,比记住某个几何体构造函数更有用。
四个核心属性
| 属性 | 分量 | 作用 | 是否必需 |
|---|---|---|---|
position | 3(x, y, z) | 顶点位置 | 必需 |
index | 1(顶点序号) | 复用顶点,定义三角面 | 可选,没有则按顺序每三个一组 |
uv | 2(u, v) | 纹理坐标 | 需要贴图时必需 |
normal | 3 | 表面朝向,决定明暗 | 受光材质必需 |
color | 3 或 4 | 逐顶点颜色 | 需要顶点色时添加 |
手工构建一块地形
规则格网地形本质是「顶点阵列 + 三角索引」。下面生成 64×64 的网格,
每个格点用 heightAt 取高程,再按行列关系拼出两个三角形。
const cols = 64;
const rows = 64;
const step = 20; // 每个格 20 米
const positions = [];
const uvs = [];
const indices = [];
function heightAt(x, z) {
return 18 * Math.sin(x * 0.015) * Math.cos(z * 0.02) + 40;
}
for (let r = 0; r <= rows; r++) {
for (let c = 0; c <= cols; c++) {
const x = c * step;
const z = r * step;
positions.push(x, heightAt(x, z), z);
uvs.push(c / cols, r / rows);
}
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const a = r * (cols + 1) + c;
const b = a + 1;
const d = a + cols + 1;
const e = d + 1;
indices.push(a, d, b, b, d, e);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.setIndex(indices);
geometry.computeVertexNormals();
索引顺序决定三角形的正面朝向。从上方看不到地形时,
先把 indices.push(a, b, d, ...) 的顺序反过来试试,
或者暂时给材质设置 side: THREE.DoubleSide 定位问题。
顶点数超过 65535 时索引会溢出。setIndex
接收普通数组会自动选 Uint16,超出范围后几何会断裂。
大规模格网改用 new THREE.Uint32BufferAttribute(indices, 1) 显式指定。
按高程写入顶点色
不想额外贴图时,可以把高程映射成颜色直接写进顶点属性。
材质打开 vertexColors 后,逐顶点颜色会与 material.color 相乘。
const pos = geometry.attributes.position;
const colors = new Float32Array(pos.count * 3);
const low = new THREE.Color('#1f5fd0');
const mid = new THREE.Color('#f0a020');
const high = new THREE.Color('#d0453a');
for (let i = 0; i < pos.count; i++) {
const h = pos.getY(i);
const t = THREE.MathUtils.clamp((h - 20) / 50, 0, 1);
const color = t < 0.5
? low.clone().lerp(mid, t * 2)
: mid.clone().lerp(high, (t - 0.5) * 2);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const terrain = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.95,
metalness: 0.0
}));
顶点色在着色器中线性插值,两个顶点之间的颜色会自然过渡,
不需要细分到每个像素。颜色值应是 0 到 1 的线性值,
THREE.Color 已经按 sRGB 转好,直接取分量即可。
更新已有缓冲
高程随时间变化或需要局部修改时,不必重建 Geometry,直接改数组并标记更新。
静态数据用 StaticDrawUsage,频繁更新用 DynamicDrawUsage。
const attr = geometry.attributes.position;
attr.setUsage(THREE.DynamicDrawUsage);
function updateHeights(scale) {
for (let i = 0; i < attr.count; i++) {
const x = attr.getX(i);
const z = attr.getZ(i);
attr.setY(i, heightAt(x, z) * scale);
}
attr.needsUpdate = true;
geometry.computeVertexNormals();
geometry.computeBoundingSphere(); // 射线拾取与视锥剔除依赖它
}
注意事项
computeVertexNormals()在每次改位置后都要重新调用,否则光照停留在旧朝向。- 手动改包围球会触发一次全量遍历,不要放在每帧循环里。
BufferAttribute的第二个参数是每条数据的分量数,写错会导致内存错位而不是报错。- 几何体是多个 Mesh 共用的,销毁前确认没有其它对象引用。
- 合并多个几何时使用
BufferGeometryUtils.mergeGeometries,属性名与分量必须完全一致。
经纬志(2026)。《几何体与缓冲》。https://www.jingweizhi.com/three/tutorial/geometry.html(访问日期 2026-11-03)。