经纬志
首页 / three.js / 教程

几何体与缓冲

先看结论

内置方块和球体只是方便,真实地形、建筑轮廓、点云都要自己往缓冲里写数据。 理解顶点属性与索引的关系,比记住某个几何体构造函数更有用。

最后验证 2026-11-03 · three.js 0.160.0 · 难度:进阶 · 核验 2026-10-29 · 引用本文

四个核心属性

属性分量作用是否必需
position3(x, y, z)顶点位置必需
index1(顶点序号)复用顶点,定义三角面可选,没有则按顺序每三个一组
uv2(u, v)纹理坐标需要贴图时必需
normal3表面朝向,决定明暗受光材质必需
color3 或 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)。