绘制调用的代价
每次绘制调用(draw call),CPU 都要做一堆准备:切换着色器、绑定缓冲、上传这个物体的矩阵 uniform、做驱动校验,然后才把命令交给 GPU。几千个小物体时,瓶颈往往不在 GPU 画三角形,而在 CPU 发命令。状态栏里的 draw calls 来自 renderer.info.render.calls。
三种策略
- separate:N 个
Mesh共享同一几何体,但每个都是一个场景对象,就是 N 次绘制调用。最灵活,最慢。 - instanced:一个
InstancedMesh,一次绘制调用。每个实例的变换矩阵和颜色放在 GPU 缓冲里(instanceMatrix、instanceColor),顶点着色器按实例编号去取。 - merged:把所有副本预先变换好,合并成一个大
BufferGeometry,也只有一次绘制调用;但各个小物体再也不能单独运动了。
实例化的写法
js
const inst = new THREE.InstancedMesh(geo, mat, count);
inst.setMatrixAt(i, m.compose(pos, quat, scale));
inst.instanceMatrix.needsUpdate = true;每帧更新所有矩阵,然后一次上传整个缓冲,代替 N 次 uniform 更新。用 setUsage(THREE.DynamicDrawUsage) 告诉驱动这个缓冲会频繁改写。
在着色器层面,实例化就是 gl.drawArraysInstanced(WebGPU 里是 draw(vertexCount, instanceCount)),顶点着色器多了一个内置的实例编号。
怎么选
- 很多相同几何体、各自运动:实例化。
- 静态、不再移动的场景(建筑、地形装饰):合并,连每帧更新矩阵的 CPU 循环都省了。
- 少量需要独立材质、独立交互的物体:普通网格就好。
看数字
把 count 调到 30000,在 separate 模式下观察 CPU update + render() 的毫秒数和帧率,再切到 instanced 和 merged。三角形数量完全一样,差别几乎全来自 CPU 一侧。
注意 instanced 模式下 CPU 时间主要花在 JS 循环里重算 3 万个矩阵;如果把运动挪到顶点着色器(或计算着色器)里,CPU 就几乎无事可做了。
核心 API
THREE.InstancedMeshinstancedMesh.setMatrixAtinstancedMesh.setColorAtinstanceMatrix.needsUpdateTHREE.DynamicDrawUsagemergeGeometriesrenderer.info.rendergl.drawArraysInstanced动手试试
- 1把 count 拉到 30000,依次切换三种模式,记下每种的 CPU 毫秒数和 draw calls。
- 2关掉 animate objects,再看 instanced 模式的 CPU 时间降了多少。
- 3把 IcosahedronGeometry 的细分从 0 改成 3,三角形变多后,哪种模式最先吃力?
还没有输出。console.log 的内容会显示在这里。
运行中