Skip to main content

第 11 节:性能优化机制深度解析

🎯 学习目标

  • 理解 InstancedMesh / InstancedBufferGeometry 的实例化渲染机制
  • 掌握 BufferGeometryUtils.mergeGeometries() 的几何体合并策略
  • 追踪 LOD 系统的距离切换算法
  • 理解 Frustum Culling 的 BoundingSphere vs BoundingBox 选择
  • 掌握 dispose 事件系统和 WebGLInfo 内存追踪

❓ 带着问题读源码

  1. 1000 棵树的场景,InstancedMesh 如何把 1000 个 Draw Call 变成 1 个?
  2. mergeGeometriesInstancedMesh 各适合什么场景?有什么区别?
  3. LOD 的切换有没有"闪烁"问题?Three.js 是怎么处理的?
  4. 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 CallsCPU 开销GPU 开销适用场景
10000 个 Mesh10000极高(JS 循环 + 状态切换)中等不推荐
InstancedMesh1极低大量相同几何体
Merge Geometry1低(初始化时合并)静态场景

🔗 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 MergeInstancedMesh
几何体是否相同不需要必须相同
独立变换不支持(合并后是一个整体)支持(每实例独立矩阵)
独立材质通过 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.jsOGL
InstancingInstancedMesh 类 + InstancedBufferAttributeGeometry 上设置 instanced 属性
LOD内置 LOD 类 + hysteresis不内置
MergeBufferGeometryUtils.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 的概念。


📝 本节总结

  1. InstancedMesh:共享几何体 + 材质,每个实例独立变换矩阵,1 次 Draw Call 绘制 N 个实例
  2. Geometry Merge:把多个几何体的顶点数据拼接为一个大 BufferGeometry,适合静态场景
  3. LOD:根据相机距离切换不同精度的几何体,hysteresis 防止频繁切换闪烁
  4. Frustum Culling:BoundingSphere 做快速检测(O(1)),惰性计算 + 缓存
  5. 资源释放:必须手动调用 dispose() 释放 GPU 资源,否则内存泄漏
  6. WebGLInfo:实时统计 Draw Call、三角面数、几何体数、纹理数

➡️ 下一节预告

第 12 节:设计启示——从 Three.js 到自研引擎

最后一节将整合所有知识,提炼设计经验:

  • Three.js 的设计哲学与代价
  • 值得借鉴的 4 大模式
  • 需要精简的 3 个方向
  • 自研引擎的 10 条架构原则
  • Three.js vs OGL 逐模块全面对比