第 3 节:Geometry 系统——BufferGeometry 如何驯服 VBO
BufferGeometry 是 Three.js 用来描述"几何体"的最核心数据结构。它不存顶点位置,而存"怎么读顶点的描述"。理解它,就理解了 Three.js 如何把 JS 端的 Float32Array 变成 GPU 端的 VBO。
🏗️ 数据结构总览
BufferGeometry
├── attributes: { position: BufferAttribute, normal: BufferAttribute, uv: BufferAttribute }
├── index: BufferAttribute (Uint16/32, 可选)
├── groups: [ { start, count, materialIndex } ] (多材质分割)
├── boundingBox / boundingSphere (用于视锥剔除)
└── drawRange: { start, count } (局部绘制)
BufferAttribute 是最小单元,结构非常克制:
// 简化源码:src/core/BufferAttribute.js
class BufferAttribute {
constructor(array, itemSize, normalized = false) {
this.array = array; // 真正的数据(Float32Array)
this.itemSize = itemSize; // 几个分量组成一个属性
this.count = array.length / itemSize;
this.normalized = normalized;
this.usage = StaticDrawUsage; // 默认 STATIC_DRAW
this.version = 0; // 脏标记
this.needsUpdate = false; // 同步开关
}
}
🧬 上传机制:update() 的三种策略
Three.js 在 WebGLAttributes.update() 中,根据 attribute 的变化量决定上传方式:
// 简化源码:src/renderers/webgl/WebGLAttributes.js
update(attribute, bufferType) {
const data = buffers.get(attribute);
if (data === undefined) {
// 1. 首次创建:bufferData
buffers.set(attribute, createBuffer(attribute, bufferType));
} else if (data.version < attribute.version) {
// 2. 部分更新:bufferSubData
updateBuffer(data.buffer, attribute, bufferType);
data.version = attribute.version;
}
}
| 场景 | 行为 | 性能影响 |
|---|---|---|
首次 setAttribute | gl.bufferData 整体上传 | O(n) 一次性 |
改 array[i] += 1 | 必须 needsUpdate = true → bufferSubData | O(Δ) 局部 |
改 usage | 重新 bufferData 整体上传 | O(n) 完整重传 |
"BufferAttribute 改了 1 个 float,仍要重新上传整段 buffer"——这是 WebGL 的硬件限制,不是 Three.js 的锅。要避免就只能用 Uniform 变量 传单值。
🔀 Interleaved 优化:一次绑定,多个属性
当几何体同时有 position、normal、uv、color 时,有两种存储方式:
// 方式 1:分离 Buffer(SoA)
const posBuffer = new BufferAttribute(positions, 3); // 12 字节/顶点
const uvBuffer = new BufferAttribute(uvs, 2); // 8 字节/顶点
// 缺点:GPU 需要多次 cache line 跳转
// 方式 2:交错存储(AoS / Interleaved)
const interleaved = new InterleavedBuffer([pos, normal, uv], stride);
const posAttr = new InterleavedBufferAttribute(interleaved, 3, 0); // offset 0
const normalAttr = new InterleavedBufferAttribute(interleaved, 3, 3); // offset 12
const uvAttr = new InterleavedBufferAttribute(interleaved, 2, 6); // offset 24
Three.js 默认推荐 Interleaved——因为 GPU 按顶点粒度消费数据,cache 命中率高。
🧹 Dispose:GPU 资源的"借还"
WebGL 创建的 VBO、EBO 不会随 JS 对象被 GC 自动释放——它们占用显存。Three.js 用事件机制解决这个问题:
// 简化源码
class BufferGeometry {
dispose() {
this.dispatchEvent({ type: 'dispose' });
}
}
// WebGLRenderer 监听
function onGeometryDispose(event) {
const geometry = event.target;
// 找到这个 geometry 对应的所有 VBO/EBO,调用 gl.deleteBuffer
attributes.delete(geometry.attributes.position);
attributes.delete(geometry.index);
}
最佳实践:从场景中永久移除某个几何体时,必须手动调用 geometry.dispose(),否则会显存泄漏。
🎯 Mini 实现
50 行实现一个最简 BufferGeometry:
class MiniBufferAttribute {
constructor(array, itemSize) {
this.array = array;
this.itemSize = itemSize;
this.count = array.length / itemSize;
this.needsUpdate = true;
}
}
class MiniBufferGeometry {
constructor() {
this.attributes = {};
this.index = null;
}
setAttribute(name, attr) {
this.attributes[name] = attr;
}
upload(gl, program) {
for (const name in this.attributes) {
const attr = this.attributes[name];
if (!attr.buffer) {
attr.buffer = gl.createBuffer();
attr.needsUpdate = true;
}
if (attr.needsUpdate) {
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buffer);
gl.bufferData(gl.ARRAY_BUFFER, attr.array, gl.STATIC_DRAW);
const loc = gl.getAttribLocation(program, name);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.itemSize, gl.FLOAT, false, 0, 0);
attr.needsUpdate = false;
}
}
}
}
🆚 OGL 对比
OGL 的 Geometry 用更少的代码实现相同功能:
// OGL: src/Geometry.js (~150 行 vs Three.js BufferGeometry ~700 行)
class Geometry {
constructor(gl, attributes = {}) {
this.gl = gl;
this.attributes = attributes; // 直接是 { name: { data, size, ... } }
}
// 没有版本号、没有 dispose 事件、没有 groups、没有 drawRange
// 但支持 Instanced 属性(OGL 把 Instancing 视为几何体的内建能力)
}
设计取舍:OGL 砍掉了版本控制和事件机制,换取了更小的体积。Three.js 选择完备性,代价是源码复杂度。
🎓 深度思考
为什么
BufferAttribute的version字段至关重要?
WebGL 驱动没法知道"JS 端数组变了",需要 JS 主动通知。version+++needsUpdate就是这套通知协议的核心。InstancedMesh 和普通 Mesh 共享
BufferGeometry吗?
共享。它们通过instanceMatrix这个特殊的BufferAttribute(类型为InstancedBufferAttribute)来携带 per-instance 数据。setAttribute时会不会自动上传 GPU?
不会。Three.js 故意把 "JS 端设置" 和 "GPU 端上传" 解耦——后者发生在renderer.render()内部的WebGLAttributes.update()中。