普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月1日首页

【ThreeJS】InstancedMesh 实战:从20000个Mesh到1个Draw Call

作者 叶智辽
2026年1月31日 23:51

前言:不知道有没有小伙伴跟我一样,刚接触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,要是显卡差一点的,我都不敢想象。。。

图片.png

大伙也可以复制以下代码自己去试试自己的电脑能跑多少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开始优化它了!

我先卖个关子,给大家看看优化后的截图:

image.png

指标 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?在评论区晒晒帧率,看看谁的优化更狠😏

❌
❌