【ThreeJS】InstancedMesh 实战:从20000个Mesh到1个Draw Call
前言:不知道有没有小伙伴跟我一样,刚接触web3D,接触ThreeJS,然后在做类似智慧站房这种项目的时候,兴致冲冲的把设备放至到场景,每一个设备摆放的位置都恰到好处,包括管道,流向动画等等,觉得这样应该就万无一失了,然而实际运行起来却发现,卧槽,好卡!!!
正题: 关于如何优化ThreeJS里设备一多就卡的问题,今天我们就来讲讲“InstancedMesh”,这是ThreeJS官方的一个API,官方是这么描述的:
这是一个支持实例化渲染的特殊网格模型。如果您需要渲染大量具有相同几何体和材质但世界变换不同的对象,请使用此类。使用“InstancedMesh”有助于减少绘制调用次数,从而提高应用程序的整体渲染性能。
不知道大伙懵了没,反正我第一次看到这描述的时候,也是云里雾里的,不过现在我自认为算是比较了解了,就给大家大白话翻译一下,意思就是:这个API可以让你场景里的模型无损影分身,就是不耗费多的性能,但能让场景里的模型分身多个出来,怎么样,是不是像魔法一样,那它是怎么做到的呢?
这时候就不得不提3d场景的绘制原理了,我相信大部分小伙伴都知道显卡这个东西,也知道它是用来为电脑渲染图形,web3d当然也是靠它渲染,但是,我要说但是了,虽然渲染是它来做,但别忘了咱们电脑老大哥CPU啊,事实就是,像这种场景多个模型卡顿的原因,可能瓶颈并不在GPU身上,而是CPU身上,怎么样,是不是又蒙了,我当初了解到也疑惑了一下,我给大家说一下,其实原因很简单
打个比方,把CPU比作一家餐馆的接单播报员,而GPU就是后厨接单的厨子,但是接单员只有一个(需要处理各种复杂的订单、优惠券叠加、会员判定、退单逻辑、库存检查),而厨子有很多(只需要固定几个步骤炒菜,其它都不用管)
现在店里来了一百个客人,这些客人要的都是蛋炒饭,但接单员每一单都跑去后厨说有一桌要蛋炒饭,有一桌要蛋炒饭...直到派完这一百桌蛋炒饭,在喊的过程中,其它厨子是闲着的状态,因为还没给它派单,现在大伙发现有哪里不太对劲吗?没错!那就是为什么接单员明明一百桌都是同样的菜,但为什么要分一百次派单呢,一次派完不行吗,嘿嘿嘿,这时候如果你用的是普通Mesh,那么答案就是不行,这是由Mesh这个API决定的
这时候就讲到了我们今天的主题,InstancedMesh,它就是那个对这种重复需求更专业的接单员,有十桌蛋炒饭它就会直接跟后厨说现在需要炒十桌蛋炒饭,不会来回去通知,这样后厨就可以火力全开,一次性把十桌蛋炒饭都做好,现在明白为什么我说瓶颈不在GPU上了吧,Three.js的卡顿,往往不是"显卡不够好",而是"CPU在疯狂传话,没让显卡满负荷干活"。
开始我们今天的实验,我现在创建了20000个BoxMesh,帧数只有可怜的10fps,要知道我这可是rtx3080,要是显卡差一点的,我都不敢想象。。。
![]()
大伙也可以复制以下代码自己去试试自己的电脑能跑多少FPS,可以把FPS打在评论区
// 200 个 box:200 列 x 100 行,整齐排列
const cols = 200;
const rows = 100;
const spacing = 1.3;
const geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const box = new THREE.Mesh(geometry, material);
box.position.x = (j - cols / 2 + 0.5) * spacing;
box.position.z = (i - rows / 2 + 0.5) * spacing;
scene.add(box);
}
}
我相信看到这的大伙对于性能优化都有一定的追求,对于这种情况是很难容忍的,所以,我们可以用InstancedMesh开始优化它了!
我先卖个关子,给大家看看优化后的截图:
![]()
| 指标 | 20000个Mesh | InstancedMesh | 优化倍数 |
|---|---|---|---|
| Draw Calls | 20000 | 1 | 20000x |
| 帧率(FPS) | 10 | 144 | 14x |
| 内存占用 | ~500MB+ | ~80MB | 6x |
| CPU占用 | 90% | 15% | 6x |
我就说一句,牛不牛!刚刚只有10fps,没眼看,现在一下把我显示器的刷新率跑满了,144FPS,并且从原来20000的Draw Call,降到了1Draw Call,也就是说接单员一次性跟后厨说要做这么多份蛋炒饭,然后后厨火力全开,一次性搞定!
好了,魔术总有揭秘的那天,下面就是优化后的代码:
// ---------- InstancedMesh:200 列 x 100 行 = 20000 个 box,单次 draw call ----------
const cols = 200;
const rows = 100;
const count = cols * rows;
const spacing = 1.3; // 实例之间的间距
const geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
// 设置为静态绘制,避免每帧更新实例矩阵
instancedMesh.instanceMatrix.setUsage(THREE.StaticDrawUsage);
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
let idx = 0;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
// 以场景中心为原点,按行列均匀排布
position.set(
(j - cols / 2 + 0.5) * spacing,
0,
(i - rows / 2 + 0.5) * spacing
);
matrix.setPosition(position);
instancedMesh.setMatrixAt(idx, matrix);
idx++;
}
}
// 更新包围球,便于视锥剔除等优化
instancedMesh.computeBoundingSphere();
scene.add(instancedMesh);
有几个明显的不同,第一是没有new mesh这个环节了,关于新增mesh都放在了 new THREE.InstancedMesh(geometry, material, count); 这个count就是告诉InstanceMesh需要新增多少个Mesh,然后在遍历之后把这个InstanceMesh添加到Scene里,中间的环节只是决定InstanceMesh里的Mesh应该在什么位置。
我再给大家讲几个InstanceMesh的坑,新手非常容易忽略
1.动态更新位置的正确姿势
// 错误:每帧都设置 needsUpdate,卡成PPT
function animate() {
for(let i=0; i<20000; i++) {
instancedMesh.setMatrixAt(i, newMatrix); // 别在循环里干这个!
}
instancedMesh.instanceMatrix.needsUpdate = true;
requestAnimationFrame(animate);
}
// 正确:只更新变化的,且设置 DynamicDrawUsage
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
// 只在数据真的变了才 setMatrixAt + needsUpdate = true
2.Raycaster(射线) 咋交互InstanceMesh实例
const intersection = raycaster.intersectObject(instancedMesh);
if (intersection.length > 0) {
const instanceId = intersection[0].instanceId;
// 高亮该实例:需要配合 setColorAt
instancedMesh.setColorAt(instanceId, new THREE.Color(0xff0000));
instancedMesh.instanceColor.needsUpdate = true;
}
3.dispose 内存泄漏 -----(这个一定要重视,我有过很惨痛的经历....)
// 组件卸载时一定要:
geometry.dispose();
material.dispose();
instancedMesh.dispose(); // 释放GPU缓冲区
scene.remove(instancedMesh);
}
总结:InstancedMesh 不是魔法,它只是让 CPU 从 "发了20000个快递包裹" (每次都要打包、贴单、叫车)变成了 "发了一车货" (整车直达)。在智慧站房、智慧工厂这种 "设备多但型号少" 的场景,这是性价比最高的优化方案。
但别急着走——如果你的设备需要各自不同的贴图(比如每台机器显示不同的温度数字),InstancedMesh就力不从心了。这时候该用 Texture Atlas(纹理图集) 还是 合并几何体(MergeGeometry) ?
关注下篇: 【ThreeJS】多材质设备优化:一张贴图管理1000个设备的独立显示 ,咱们继续折腾性能优化。
互动:你的项目最多渲染过多少个Mesh?在评论区晒晒帧率,看看谁的优化更狠😏