Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】InstancedMesh in Three.js - one draw call for the parts you repeat

2026年10月21日に「Dev.to」が公開したITニュース「InstancedMesh in Three.js - one draw call for the parts you repeat」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Three.jsで多数の繰り返しオブジェクトを描画する際、個々のMeshを使うと描画呼び出しが増え、パフォーマンスが低下する。InstancedMeshは、一つのジオメトリで多数のインスタンスを一度に描画することで、描画負荷を大幅に削減し、フレームレートを改善する。特に100以上のオブジェクトや、毎フレーム更新するオブジェクトに有効だ。

ITニュース解説

現代のウェブアプリケーションにおいて、リアルタイムで動作する3Dコンテンツはますます一般的になってきている。特に、顧客が製品を自由にカスタマイズできる「製品コンフィギュレーター」のようなシステムでは、高性能な3D表示が不可欠となる。しかし、このようなシステムを開発する際には、しばしば性能上の課題に直面することがある。例えば、パーゴラ(日よけの屋根)のルーバーやフェンスの板、プールのタイルのように、同じ形状のオブジェクトが画面上に多数配置されるケースが典型的な例だ。

もし、これらの繰り返される部品を一つずつ、通常の3Dオブジェクトとして作成すると、パフォーマンスが著しく低下してしまう。Three.jsのような3Dライブラリで一般的なTHREE.Meshを使用して、それぞれの部品を作成しシーンに追加する場合を考えてみよう。例えば、パーゴラに40枚のルーバーがあるとして、それらを次のようにコードで記述するとする。

1for (let i = 0; i < 40; i++) {
2  const louvre = new THREE.Mesh(geometry, material)
3  louvre.position.z = i * 0.15
4  scene.add(louvre)
5}

このコードは、一見するとシンプルで理にかなっているように見える。しかし、この方法では、40個のルーバーがそれぞれ独立した描画対象となり、グラフィックボードに対して毎フレーム40回もの「描画呼び出し」(ドローコール)が発生することになる。ドローコールは、グラフィックボードにオブジェクトを描画するよう指示するコマンドであり、その回数が増えれば増えるほど、CPUやGPUに大きな負荷がかかる。結果として、特にスマートフォンのような処理能力が限られたデバイスでは、画面の動きがカクカクになり、フレームレート(1秒あたりの描画回数)が極端に低下する可能性がある。実際に、ミドルレンジのAndroidスマートフォンでこのような方法を用いたパーゴラコンフィギュレーターを動かしたところ、フレームレートはわずか12 FPSまで落ち込んでしまったという報告もある。さらに、ユーザーがルーバーの傾きを調整するような機能を追加した場合、毎フレーム40個のオブジェクトすべてに対して位置や回転の計算を行う必要が生じ、負荷はさらに増大してしまう。

このような問題を解決するために登場するのが、Three.jsのInstancedMeshという機能だ。InstancedMeshは、同じジオメトリ(形状)とマテリアル(素材)を持つ多数のオブジェクトを、一度の描画呼び出しで効率的に描画するための仕組みである。これは、グラフィックボードの機能を活用し、同じ形状のオブジェクトがいくつも存在する状況で、描画性能を劇的に向上させることができる。

InstancedMeshの基本的な使い方は以下の通りである。まず、形状と素材、そしてインスタンス(複製)の最大数を指定してInstancedMeshを生成する。

1const geo = new THREE.BoxGeometry(0.1, 0.02, 3) // 例:箱型のジオメトリ
2const mat = new THREE.MeshStandardMaterial()    // 例:標準マテリアル
3const louvres = new THREE.InstancedMesh(geo, mat, 40) // 40個のインスタンスを生成

次に、各インスタンスの位置、回転、スケールといった変換情報(トランスフォーム)を設定する。ここで重要なのは、個々のインスタンスに直接位置などを設定するのではなく、一時的なダミーオブジェクト(THREE.Object3D)を利用することだ。ダミーオブジェクトに希望する変換を設定し、その変換行列をInstancedMeshの各インスタンスに適用する。

1const dummy = new THREE.Object3D()
2for (let i = 0; i < 40; i++) {
3  dummy.position.set(0, 0, i * 0.15) // 位置を設定
4  dummy.rotation.x = userTilt         // 回転を設定
5  dummy.updateMatrix()                // ダミーオブジェクトの変換行列を更新
6  louvres.setMatrixAt(i, dummy.matrix) // i番目のインスタンスに変換行列を適用
7}
8louvres.instanceMatrix.needsUpdate = true // GPUにインスタンスデータの更新を通知
9scene.add(louvres)

この方法では、同じ40個のルーバーであっても、グラフィックボードへの描画呼び出しはたった1回で済む。これにより、以前は12 FPSだったフレームレートが、このInstancedMeshを用いることで60 FPSにまで向上したという結果が得られている。これは、大量のオブジェクトを描画する際のパフォーマンス問題を解決するための非常に強力な手段だ。

InstancedMeshを使う上で、いくつか注意すべき点がある。これらの点は、開発者がしばしば陥りやすい落とし穴であり、理解していないと意図しない動作やバグにつながる可能性がある。

一つ目は、dummy.updateMatrix()をsetMatrixAtの前に必ず呼び出すことだ。ダミーオブジェクトの位置や回転などのプロパティを変更しただけでは、その変換情報が内部の変換行列にすぐに反映されるわけではない。updateMatrix()を呼び出すことで、プロパティの変更が変換行列に適用され、その正確な行列がsetMatrixAtに渡される。これを忘れると、すべてのインスタンスが原点に積み重なった状態で描画されてしまい、エラーメッセージも出ないため、原因の特定に時間がかかることがある。

二つ目は、インスタンスの変換情報を変更した後、必ずinstanceMatrix.needsUpdate = trueを設定することだ。このフラグを設定することで、Three.jsはインスタンスの変換データが更新されたことを認識し、次回の描画時にグラフィックボードへ新しいデータを送信する。これを設定し忘れると、コード上で変換情報を更新しても、画面上のインスタンスは変化しない。これはInstancedMeshが壊れていると誤解されやすいポイントであり、注意が必要である。

三つ目は、インスタンスごとに異なる色を設定したい場合の方法だ。通常のTHREE.Meshでは、マテリアルのmaterial.colorを変更することで色を設定するが、InstancedMeshではこれとは異なる方法を用いる。インスタンスごとに色を変えるには、louvres.setColorAt(i, color)を呼び出し、その後louvres.instanceColor.needsUpdate = trueを設定する必要がある。この方法を使えば、一つのマテリアルを共有しながら、各インスタンスに異なる色を適用でき、それでも描画呼び出しは1回に保たれる。例えば、ユーザーがルーバーごとに色を選択できるような機能を実現する際に役立つ。

InstancedMeshは非常に強力なツールだが、すべての状況で使うべきではない。この機能が真価を発揮するのは、おおよそ100個から200個以上の繰り返しオブジェクトが存在する場合や、毎フレーム更新が必要な繰り返しオブジェクトがある場合だ。それ以下の数のオブジェクトであれば、通常のTHREE.Meshを使ってもパフォーマンスに大きな問題は生じにくく、コードもInstancedMeshを使うよりもシンプルに記述できるため、無理にInstancedMeshを導入する必要はない。例えば、たった3つのキューブを描画するためにInstancedMeshを使うのは、過剰な最適化であり、かえってコードの複雑性を増すだけだろう。

このように、InstancedMeshは3Dコンテンツにおける特定の性能課題に対する非常に効果的な解決策を提供する。特に、製品コンフィギュレーターのような、多数の繰り返し部品が動的に生成・操作されるアプリケーション開発において、この技術はスムーズなユーザー体験を実現するために不可欠な要素となるだろう。

関連コンテンツ

関連IT用語

関連ITニュース