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 台になりました。描画コールの削減がそのまま効いています。