第 11 节:性能优化机制深度解析
🎯 学习目标
- 理解
InstancedMesh/InstancedBufferGeometry的实例化渲染机制 - 掌握
BufferGeometryUtils.mergeGeometries()的几何体合并策略 - 追踪 LOD 系统的距离切换算法
- 理解 Frustum Culling 的 BoundingSphere vs BoundingBox 选择
- 掌握 dispose 事件系统和 WebGLInfo 内存追踪
❓ 带着问题读源码
- 1000 棵树的场景,
InstancedMesh如何把 1000 个 Draw Call 变成 1 个? mergeGeometries和InstancedMesh各适合什么场景?有什么区别?- LOD 的切换有没有"闪烁"问题?Three.js 是怎么处理的?
renderer.info显示 texture memory 500MB——是真的占了 500MB GPU 内存吗?
⚡ Instanced Rendering
问题:大量相同几何体
场景:森林中 10000 棵相同的树
方案 A:10000 个 Mesh → 10000 次 gl.drawElements → GPU 饥饿
方案 B:InstancedMesh → 1 次 gl.drawElementsInstanced → 高效
InstancedMesh
源码位置:src/objects/InstancedMesh.js
class InstancedMesh extends Mesh {
constructor(geometry, material, count) {
super(geometry, material);
this.isInstancedMesh = true;
this.count = count;
// 每个实例的 4×4 变换矩阵,打包为 Float32Array
this.instanceMatrix = new InstancedBufferAttribute(
new Float32Array(count * 16), 16
);
// 可选:每个实例的颜色
this.instanceColor = null;
}
getMatrixAt(index, matrix) {
matrix.fromArray(this.instanceMatrix.array, index * 16);
}
setMatrixAt(index, matrix) {
matrix.toArray(this.instanceMatrix.array, index * 16);
}
setColorAt(index, color) {
if (this.instanceColor === null) {
this.instanceColor = new InstancedBufferAttribute(
new Float32Array(this.count * 3), 3
);
}
color.toArray(this.instanceColor.array, index * 3);
}
}
InstancedBufferAttribute
class InstancedBufferAttribute extends BufferAttribute {
constructor(array, itemSize, normalized, meshPerAttribute = 1) {
super(array, itemSize, normalized);
this.isInstancedBufferAttribute = true;
this.meshPerAttribute = meshPerAttribute;
// meshPerAttribute = 1 表示每个实例使用 1 份数据
// 这对应 gl.vertexAttribDivisor(location, 1)
}
}
渲染路径
在 renderBufferDirect() 中:
if (object.isInstancedMesh) {
renderer.renderInstances(drawStart, drawCount, object.count);
// 内部调用:
// gl.drawElementsInstanced(mode, count, type, start, instanceCount)
// 或 gl.drawArraysInstanced(mode, start, count, instanceCount)
}
Shader 中的实例数据
// Three.js 自动注入的实例化相关代码
attribute mat4 instanceMatrix; // 每实例变换矩阵
#ifdef USE_INSTANCING_COLOR
attribute vec3 instanceColor;
#endif
void main() {
#ifdef USE_INSTANCING
mat4 finalModelMatrix = modelMatrix * instanceMatrix;
#else
mat4 finalModelMatrix = modelMatrix;
#endif
vec4 mvPosition = viewMatrix * finalModelMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPosition;
}
性能对比
| 方案 | Draw Calls | CPU 开销 | GPU 开销 | 适用场景 |
|---|---|---|---|---|
| 10000 个 Mesh | 10000 | 极高(JS 循环 + 状态切换) | 中等 | 不推荐 |
| InstancedMesh | 1 | 极低 | 低 | 大量相同几何体 |
| Merge Geometry | 1 | 低(初始化时合并) | 低 | 静态场景 |
🔗 Geometry Merging
源码位置:examples/jsm/utils/BufferGeometryUtils.js
mergeGeometries()
function mergeGeometries(geometries, useGroups = false) {
const mergedGeometry = new BufferGeometry();
const mergedAttributes = {};
const mergedIndex = [];
let indexOffset = 0;
for (let i = 0; i < geometries.length; i++) {
const geometry = geometries[i];
// 合并每种属性
for (const name in geometry.attributes) {
if (mergedAttributes[name] === undefined) {
mergedAttributes[name] = [];
}
mergedAttributes[name].push(geometry.attributes[name]);
}
// 合并索引(偏移校正)
if (geometry.index) {
const index = geometry.index;
for (let j = 0; j < index.count; j++) {
mergedIndex.push(index.array[j] + indexOffset);
}
}
// 添加 group 以支持多材质
if (useGroups) {
const count = geometry.index ? geometry.index.count : geometry.attributes.position.count;
mergedGeometry.addGroup(indexOffset, count, i);
}
indexOffset += geometry.attributes.position.count;
}
// 拼接属性数组
for (const name in mergedAttributes) {
const arrays = mergedAttributes[name];
const mergedArray = mergeArrays(arrays);
mergedGeometry.setAttribute(name, new BufferAttribute(mergedArray, arrays[0].itemSize));
}
if (mergedIndex.length > 0) {
mergedGeometry.setIndex(mergedIndex);
}
return mergedGeometry;
}
Merge vs Instance 对比
| 特性 | Geometry Merge | InstancedMesh |
|---|---|---|
| 几何体是否相同 | 不需要 | 必须相同 |
| 独立变换 | 不支持(合并后是一个整体) | 支持(每实例独立矩阵) |
| 独立材质 | 通过 groups 支持 | 不支持(共享材质) |
| 动态增删 | 困难(需要重新合并) | 简单(修改 count) |
| 内存 | 存储所有顶点数据 | 共享一份几何数据 + 实例矩阵 |
| 最佳场景 | 静态场景中的不同几何体 | 大量相同物体(草、树、建筑) |
🔭 LOD (Level of Detail)
源码位置:src/objects/LOD.js
class LOD extends Object3D {
constructor() {
super();
this.isLOD = true;
this.type = 'LOD';
this._currentLevel = 0;
this.autoUpdate = true;
this.levels = [];
}
addLevel(object, distance = 0, hysteresis = 0) {
distance = Math.abs(distance);
const levels = this.levels;
let l;
for (l = 0; l < levels.length; l++) {
if (distance < levels[l].distance) break;
}
levels.splice(l, 0, {
distance: distance,
hysteresis: hysteresis,
object: object,
});
this.add(object);
return this;
}
update(camera) {
const levels = this.levels;
if (levels.length > 1) {
// 计算 LOD 到相机的距离
_v1.setFromMatrixPosition(camera.matrixWorld);
_v2.setFromMatrixPosition(this.matrixWorld);
const distance = _v1.distanceTo(_v2) / camera.zoom;
// 找到合适的 LOD 级别
levels[0].object.visible = true;
let i, l;
for (i = 1, l = levels.length; i < l; i++) {
let levelDistance = levels[i].distance;
// hysteresis 防止频繁切换
if (levels[i].hysteresis > 0) {
if (i === this._currentLevel) {
levelDistance -= levelDistance * levels[i].hysteresis;
}
}
if (distance >= levelDistance) {
levels[i - 1].object.visible = false;
levels[i].object.visible = true;
} else {
break;
}
}
this._currentLevel = i - 1;
// 隐藏更低精度的级别
for (; i < l; i++) {
levels[i].object.visible = false;
}
}
}
}
LOD 使用示例
const lod = new THREE.LOD();
const highGeometry = new THREE.SphereGeometry(1, 64, 64); // 高精度
const medGeometry = new THREE.SphereGeometry(1, 16, 16); // 中精度
const lowGeometry = new THREE.SphereGeometry(1, 4, 4); // 低精度
lod.addLevel(new THREE.Mesh(highGeometry, material), 0); // 距离 < 10 用高精度
lod.addLevel(new THREE.Mesh(medGeometry, material), 10); // 距离 10-50 用中精度
lod.addLevel(new THREE.Mesh(lowGeometry, material), 50); // 距离 > 50 用低精度
scene.add(lod);
Hysteresis 防闪烁
距离阈值 = 50m
hysteresis = 0.1 (10%)
相机从远处接近:
distance < 50m → 切换到高精度
相机从近处远离:
distance > 50m × (1 - 0.1) = 45m → 才切回低精度
(而非 > 50m 立即切换)
这 5m 的缓冲区防止了相机在 50m 附近来回移动导致的频繁切换
🔍 Frustum Culling 优化细节
BoundingSphere 的自动计算
// BufferGeometry.computeBoundingSphere()
computeBoundingSphere() {
const position = this.attributes.position;
// 第一遍:找到中心点(用 AABB 中心作为近似)
this.computeBoundingBox();
const center = this.boundingSphere.center;
this.boundingBox.getCenter(center);
// 第二遍:计算最大半径
let maxRadiusSq = 0;
for (let i = 0; i < position.count; i++) {
_vector.fromBufferAttribute(position, i);
maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(_vector));
}
this.boundingSphere.radius = Math.sqrt(maxRadiusSq);
}
惰性计算策略
// Mesh.raycast() 和 projectObject() 中的模式
if (geometry.boundingSphere === null) geometry.computeBoundingSphere();
BoundingSphere 只在第一次需要时才计算,之后缓存。如果几何体的顶点数据变化了(动态几何),用户需要手动调用 geometry.computeBoundingSphere() 更新。
InstancedMesh 的 Culling 问题
// InstancedMesh 的 BoundingSphere 包含所有实例
// 这意味着如果实例分散很远,球体会非常大,几乎永远不会被剔除
// 解决方案:
// 1. 手动分组(多个 InstancedMesh,每个覆盖一个区域)
// 2. 使用 InstancedMesh.computeBoundingSphere() 自定义计算
🗑️ 资源释放系统
dispose 事件传播
geometry.dispose() → WebGLAttributes 删除 GPU Buffers
material.dispose() → WebGLPrograms 释放 Program(引用计数归零时)
texture.dispose() → WebGLTextures 删除 GPU Texture
renderTarget.dispose() → 删除 FBO + 附件纹理
renderer.dispose() → 释放所有 WebGL 资源
完整释放示例
function disposeObject(object) {
if (object.geometry) object.geometry.dispose();
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(mat => disposeMaterial(mat));
} else {
disposeMaterial(object.material);
}
}
}
function disposeMaterial(material) {
for (const key in material) {
const value = material[key];
if (value && value.isTexture) {
value.dispose();
}
}
material.dispose();
}
function disposeScene(scene) {
scene.traverse(disposeObject);
}
常见内存泄漏
泄漏 1:只从场景中 remove,不 dispose
scene.remove(mesh); // ✗ GPU 资源仍存在
mesh.geometry.dispose(); // ✓ 必须手动释放
mesh.material.dispose(); // ✓
泄漏 2:替换纹理不释放旧纹理
material.map = newTexture; // ✗ 旧纹理未释放
oldTexture.dispose(); // ✓ 先释放旧纹理
material.map = newTexture;
泄漏 3:RenderTarget 未释放
const rt = new WebGLRenderTarget(w, h);
// ... 使用后
rt.dispose(); // ✓ 必须手动释放
📊 WebGLInfo 统计
源码位置:src/renderers/webgl/WebGLInfo.js
class WebGLInfo {
constructor(gl) {
this.memory = {
geometries: 0,
textures: 0,
};
this.render = {
frame: 0,
calls: 0,
triangles: 0,
points: 0,
lines: 0,
};
this.programs = null; // 所有已编译的 Program 列表
this.autoReset = true;
}
update(count, mode, instanceCount) {
this.render.calls++;
switch (mode) {
case gl.TRIANGLES:
this.render.triangles += instanceCount * (count / 3);
break;
case gl.LINES:
this.render.lines += instanceCount * (count / 2);
break;
case gl.POINTS:
this.render.points += instanceCount * count;
break;
}
}
reset() {
this.render.calls = 0;
this.render.triangles = 0;
this.render.points = 0;
this.render.lines = 0;
this.render.frame++;
}
}
使用方式
// 每帧检查渲染统计
console.log('Draw Calls:', renderer.info.render.calls);
console.log('Triangles:', renderer.info.render.triangles);
console.log('Geometries:', renderer.info.memory.geometries);
console.log('Textures:', renderer.info.memory.textures);
console.log('Programs:', renderer.info.programs.length);
🛠️ Mini 实现
为前面的极简引擎添加 Instancing 支持:
class MiniInstancedMesh {
constructor(geometry, material, count) {
this.geometry = geometry;
this.material = material;
this.count = count;
this.instanceMatrices = new Float32Array(count * 16);
this._matrixBuffer = null;
this._dirty = true;
for (let i = 0; i < count; i++) {
mat4Identity(this.instanceMatrices, i * 16);
}
}
setMatrixAt(index, matrix) {
this.instanceMatrices.set(matrix, index * 16);
this._dirty = true;
}
}
class MiniInstancedRenderer {
constructor(gl) {
this.gl = gl;
}
renderInstanced(instancedMesh, program) {
const gl = this.gl;
const { geometry, count, instanceMatrices } = instancedMesh;
gl.useProgram(program);
// 绑定几何属性(position, normal, uv...)
this._bindGeometry(geometry, program);
// 上传实例矩阵
if (!instancedMesh._matrixBuffer) {
instancedMesh._matrixBuffer = gl.createBuffer();
}
gl.bindBuffer(gl.ARRAY_BUFFER, instancedMesh._matrixBuffer);
if (instancedMesh._dirty) {
gl.bufferData(gl.ARRAY_BUFFER, instanceMatrices, gl.DYNAMIC_DRAW);
instancedMesh._dirty = false;
}
// mat4 需要 4 个 vec4 attribute location
const loc = gl.getAttribLocation(program, 'instanceMatrix');
for (let i = 0; i < 4; i++) {
const attribLoc = loc + i;
gl.enableVertexAttribArray(attribLoc);
gl.vertexAttribPointer(attribLoc, 4, gl.FLOAT, false, 64, i * 16);
gl.vertexAttribDivisor(attribLoc, 1); // 每个实例切换一次
}
// 发出实例化绘制命令
if (geometry.index) {
gl.drawElementsInstanced(
gl.TRIANGLES,
geometry.index.count,
gl.UNSIGNED_SHORT,
0,
count
);
} else {
gl.drawArraysInstanced(
gl.TRIANGLES,
0,
geometry.attributes.position.count,
count
);
}
// 重置 divisor
for (let i = 0; i < 4; i++) {
gl.vertexAttribDivisor(loc + i, 0);
}
}
_bindGeometry(geometry, program) {
const gl = this.gl;
for (const [name, attr] of Object.entries(geometry.attributes)) {
const loc = gl.getAttribLocation(program, name);
if (loc < 0) continue;
gl.bindBuffer(gl.ARRAY_BUFFER, attr._glBuffer);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.itemSize, gl.FLOAT, false, 0, 0);
}
if (geometry.index) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.index._glBuffer);
}
}
}
function mat4Identity(array, offset = 0) {
array.fill(0, offset, offset + 16);
array[offset] = array[offset + 5] = array[offset + 10] = array[offset + 15] = 1;
}
// 使用示例
const instancedMesh = new MiniInstancedMesh(treeGeometry, treeMaterial, 1000);
for (let i = 0; i < 1000; i++) {
const matrix = new Float32Array(16);
mat4Identity(matrix);
matrix[12] = Math.random() * 100 - 50; // x
matrix[13] = 0; // y
matrix[14] = Math.random() * 100 - 50; // z
instancedMesh.setMatrixAt(i, matrix);
}
instancedRenderer.renderInstanced(instancedMesh, treeProgram);
🔍 OGL 对比
| 维度 | Three.js | OGL |
|---|---|---|
| Instancing | InstancedMesh 类 + InstancedBufferAttribute | Geometry 上设置 instanced 属性 |
| LOD | 内置 LOD 类 + hysteresis | 不内置 |
| Merge | BufferGeometryUtils.mergeGeometries() | 不内置 |
| Frustum Culling | 自动计算 BoundingSphere | 内置 computeBoundingSphere |
| Info 统计 | WebGLInfo 自动统计 | 不内置 |
OGL 的 Instancing
// OGL 的实例化——在 Geometry 上直接设置
const geometry = new Geometry(gl, {
position: { size: 3, data: positionData },
instanceMatrix: {
instanced: 1, // divisor = 1
size: 16,
data: instanceMatrixData,
},
});
const mesh = new Mesh(gl, { geometry, program });
mesh.geometry.instancedCount = 1000;
OGL 不需要单独的 InstancedMesh 类,而是在 Geometry 的属性上标记 instanced: 1,更灵活但需要用户理解 vertexAttribDivisor 的概念。
📝 本节总结
- InstancedMesh:共享几何体 + 材质,每个实例独立变换矩阵,1 次 Draw Call 绘制 N 个实例
- Geometry Merge:把多个几何体的顶点数据拼接为一个大 BufferGeometry,适合静态场景
- LOD:根据相机距离切换不同精度的几何体,hysteresis 防止频繁切换闪烁
- Frustum Culling:BoundingSphere 做快速检测(O(1)),惰性计算 + 缓存
- 资源释放:必须手动调用
dispose()释放 GPU 资源,否则内存泄漏 - WebGLInfo:实时统计 Draw Call、三角面数、几何体数、纹理数
➡️ 下一节预告
第 12 节:设计启示——从 Three.js 到自研引擎
最后一节将整合所有知识,提炼设计经验:
- Three.js 的设计哲学与代价
- 值得借鉴的 4 大模式
- 需要精简的 3 个方向
- 自研引擎的 10 条架构原则
- Three.js vs OGL 逐模块全面对比