← コラム一覧へ
THREE.JS2026.07.11読了 3 分 · デモあり

InstancedMesh で描画コールを 1/200 に減らす

同じジオメトリを大量に置くときの定石。属性の持たせ方とフラスタムカリングの落とし穴について。

同じ形を 200 個並べたいとき、Mesh を 200 個作ると描画コールも 200 回になります。 InstancedMesh を使えばこれが 1 回で済みます。

const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial();
const mesh = new THREE.InstancedMesh(geo, mat, 200);

const m = new THREE.Matrix4();
for (let i = 0; i < 200; i++) {
  m.setPosition(Math.random() * 20 - 10, 0, Math.random() * 20 - 10);
  mesh.setMatrixAt(i, m);
}
scene.add(mesh);

更新するなら必ずフラグを立てる

インスタンスの位置を毎フレーム動かす場合、行列を書き換えただけでは反映されません。

mesh.instanceMatrix.needsUpdate = true;

これを忘れると、初期配置のまま固まります。エラーは出ないので、 「動かしているつもりなのに動かない」ときは最初にここを疑ってください。

毎フレーム全インスタンスを更新するなら、instanceMatrix の usage を THREE.DynamicDrawUsage にしておくと転送が少し軽くなります。

色を個別に持たせる

インスタンスごとに色を変えたいときは setColorAt を使います。

const c = new THREE.Color();
mesh.setColorAt(i, c.setHSL(i / 200, 0.6, 0.5));
mesh.instanceColor.needsUpdate = true;

マテリアルは 1 つのまま、色だけインスタンス属性として渡る形です。 これ以上の情報(たとえばインスタンスごとの寿命)を持たせたいなら、 InstancedBufferAttribute を自分で足して、シェーダを差し替えることになります。

フラスタムカリングの落とし穴

ここが一番はまりました。InstancedMesh のカリングは、インスタンス単位ではなく メッシュ全体のバウンディングスフィアで判定されます。

つまり、インスタンスを広い範囲にばらまくと、バウンディングスフィアが巨大になり、 実質いつでも「画面内」と判定されて、カリングが効きません。逆に、初期状態の バウンディングスフィアのままインスタンスを遠くへ動かすと、画面に映っているのに 消えるという症状が出ます。

対処はどちらかです。

// 1. 動かしたあとに再計算する
mesh.computeBoundingSphere();

// 2. そもそもカリングを切る(範囲が広く、常に映るならこちらが素直)
mesh.frustumCulled = false;

広い地形にオブジェクトを敷き詰めるようなケースでは、範囲ごとに InstancedMesh を 分割して、塊単位でカリングさせるのが結局いちばん素直でした。

どのくらい効いたか

ボクセルのデモで、1 万個のキューブを個別 Mesh で置いた場合と比べて、 フレーム時間が 40ms 台から 3ms 台になりました。描画コールの削減がそのまま効いています。