WebGLとThree.jsのパフォーマンス

Table of Contents
ウェブ向けに複雑な3Dアプリケーションを開発する際、パフォーマンスは成功への最も重要な障壁となることがよくあります。WebGLは、ブラウザでハードウェアアクセラレーションされた3Dグラフィックスをレンダリングするための基本的なAPIを提供し、Three.jsは3Dシーンのオーサリングを簡素化する高レベルの抽象化レイヤーとして機能します。Three.jsが提供する利便性にもかかわらず、最適なパフォーマンスを達成するには、基盤となるレンダリングパイプライン、GPUアーキテクチャ、およびWebGL呼び出しを効率的に構成する方法について深く理解する必要があります。
この詳細な解説では、WebGLとThree.jsのパフォーマンスを最適化するための高度なテクニックを探求し、ドローコール削減、ジオメトリ最適化、シェーダーの複雑さ、メモリ管理、レンダリング戦略などのトピックをカバーします。
ドローコールのコスト
WebGLのパフォーマンスにおける最も重要なボトルネックの1つは、ドローコールの数です。ドローコールは、CPUがGPUに特定のジオメトリのバッチを特定の状態(シェーダー、テクスチャ、ユニフォーム)でレンダリングするよう指示を送信するときに発生します。このCPUからGPUへの通信には高いオーバーヘッドが伴います。
インスタンスレンダリング
異なる変換やマテリアルを持つ多数の同一オブジェクト(例:森の木々、群衆のキャラクター、パーティクルシステム)をレンダリングする場合、インスタンスレンダリングはドローコールを削減する最も効果的な方法です。Three.jsは、この目的のためにInstancedMeshを提供します。
数百または数千の個別のMeshオブジェクトを作成する代わりに、InstancedMeshを使用すると、単一のジオメトリの複数のインスタンスを単一のマテリアルで、たった1つのドローコールでレンダリングできます。各インスタンスの変換行列はインスタンス属性バッファに格納され、頂点シェーダーがこれを使用して各インスタンスを正しく配置します。
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshStandardMaterial( { color: 0xff0000 } );
const count = 10000;
const instancedMesh = new THREE.InstancedMesh( geometry, material, count );
const dummy = new THREE.Object3D();
for ( let i = 0; i < count; i ++ ) {
dummy.position.set( Math.random() * 100, Math.random() * 100, Math.random() * 100 );
dummy.updateMatrix();
instancedMesh.setMatrixAt( i, dummy.matrix );
}
scene.add( instancedMesh );
ジオメトリのマージ
同じマテリアルを共有するが同一ではない(またはインスタンス化が適さない)多くの静的オブジェクトがある場合、それらのジオメトリを単一の大きなジオメトリにマージすることもドローコールを削減できます。Three.jsのBufferGeometryUtils.mergeBufferGeometriesユーティリティは、複数のジオメトリを1つに結合できます。
ただし、マージにはトレードオフが伴います。個々のサブオブジェクトを効率的に個別にカリングしたり変換したりする機能が失われます。フラスタムカリングはマージされたバウンディングボックス全体に適用されるため、GPUが可視ではない頂点を処理する可能性があります。
ジオメトリとバッファの最適化
GPUに送信される頂点データの量と、その構造は、メモリ帯域幅と頂点シェーダーの処理時間の両方に直接影響します。
頂点属性とインターリーブ
WebGLでは、頂点データは通常ArrayBuffers(VBO)に格納されます。デフォルトでは、Three.jsは非インターリーブ属性を使用します。つまり、位置、法線、UVは別々のバッファに格納されます。これらの属性を単一のバッファにインターリーブすると、GPU上のメモリキャッシュの局所性が向上し、頂点フェッチが高速化されます。
精度とデータ型
すべての頂点属性が32ビット浮動小数点精度を必要とするわけではありません。たとえば、色、法線、UV座標は、正規化された属性(gl.vertexAttribPointerとnormalized = trueを使用)を使用して、16ビットまたは8ビットの整数形式にパックできることがよくあります。これにより、メモリフットプリントと帯域幅要件が削減されます。Three.jsは、BufferAttributeを作成する際に、Uint16ArrayやInt8Arrayのような型付き配列を介してこれらの形式をサポートしています。
シェーダーの複雑さとフィルレート
フラグメントシェーダーは、ラスタライズされたプリミティブがカバーするすべてのピクセル(またはフラグメント)に対して実行されます。フラグメントシェーダーの複雑度が高い場合や、重なり合う透明なオブジェクトが多すぎる場合(オーバードロー)、フィルレートとフレームレートに深刻な影響を与える可能性があります。
シェーダーの最適化
- 分岐を避ける: シェーダー内の条件文(
if/else)は、GPUアーキテクチャでワープダイバージェンスを引き起こし、並列処理を低下させる可能性があります。分岐の代わりに、step、smoothstep、またはmixのような数学関数を使用して値を補間します。 - 精度修飾子: 高精度を必要としない変数(例:色や正規化されたベクトル)には、GLSLで
mediumpまたはlowpの精度修飾子を使用して、特にモバイルGPUでのパフォーマンスを向上させます。 - 値を事前計算する: 目立ったアーティファクトなしにプリミティブ全体で結果を補間できる場合は、フラグメントシェーダーから頂点シェーダーに計算を移動します。ドローコール全体で定数である場合は、頂点シェーダーからCPUに計算を移動します。
オーバードローとデプステスト
オーバードローは、単一フレーム内で同じピクセルが複数回書き込まれるときに発生します。不透明なオブジェクトは、理想的には手前から奥へと描画され、早期デプステスト(Early-Zカリング)を利用して、フラグメントシェーダーを実行する前に、すでにレンダリングされたジオメトリの背後にあるフラグメントを破棄します。Three.jsは、デフォルトで不透明なオブジェクトを手前から奥へとソートしようとします。
透明なオブジェクトの場合、正しいブレンドのために奥から手前へのソートが必要であり、これは必然的にオーバードローを引き起こします。重なり合う透明なオブジェクトがカバーする画面領域を最小限に抑えます(例:ほとんどが空のスペースである大きなクワッドの代わりに、パーティクルスプライトにはよりタイトなバウンディングポリゴンを使用します)。
テクスチャメモリと帯域幅
テクスチャは、GPUメモリと帯域幅の大部分を消費します。大規模なアプリケーションでは、圧縮テクスチャの使用が不可欠です。
圧縮テクスチャ
PNGやJPEGのような標準的な画像形式をロードする代わりに、KTX2(Basis Universal圧縮を使用)のような圧縮テクスチャ形式を使用します。これらの形式はGPUメモリ内で圧縮されたままであり、サンプリング中のVRAM使用量とメモリ帯域幅を大幅に削減します。Three.jsは、これらのテクスチャを効率的にロードするためにKTX2Loaderを提供します。
ミップマッピングとフィルタリング
遠くから表示されるテクスチャには、常にミップマップを生成します。ミップマッピングはテクスチャキャッシュのコヒーレンスを向上させ、エイリアシングアーティファクトを削減します。minFilter = THREE.LinearMipmapLinearFilter(トリリニアフィルタリング)またはTHREE.LinearMipmapNearestFilter(バイリニアフィルタリング)を使用します。急な角度で表示されるサーフェスに必要でない限り、異方性フィルタリングは計算コストが高いため避けてください。
高度なレンダリング戦略
レベルオブディテール(LOD)
THREE.LODオブジェクトを使用すると、カメラが遠ざかるにつれて、詳細度の高いメッシュをより単純なメッシュに交換できます。これにより、遠くのオブジェクトの頂点数とラスタライズのワークロードが削減されます。
オフスクリーンキャンバスとWeb Worker
JavaScriptはシングルスレッドであり、重いCPU計算(例:物理演算、パスファインディング、複雑なシーングラフの更新)はメインスレッドをブロックし、スタッターを引き起こす可能性があります。WebGLレンダリングは、OffscreenCanvasを使用してWeb Workerにオフロードできます。これにより、メインスレッドがUIと入力を処理する間、Workerが3Dレンダリングと計算を処理できます。
ポストプロセッシングのオーバーヘッド
ポストプロセッシング効果(ブルーム、被写界深度、スクリーン空間アンビエントオクルージョンなど)は、シーンをオフスクリーンフレームバッファにレンダリングし、その後1つ以上のフルスクリーンパスを実行する必要があります。これはフィルレートに非常に大きな負担をかけます。Three.jsでEffectComposerを使用する場合、複数のパスを単一のカスタムシェーダーパスに結合して、レンダーターゲットとドローコールの数を減らすようにしてください。
ガベージコレクションとメモリリーク
JavaScriptでは、ガベージコレクション(GC)の一時停止が、リアルタイムレンダリングアプリケーションで目に見えるフレーム落ちやスタッター(ジャンク)を引き起こす可能性があります。これを軽減するには、レンダリングループ内でオブジェクトを動的に作成することを避けてください。代わりに、オブジェクト、ベクトル、行列、配列を事前に割り当て、フレーム間で再利用します。このオブジェクトプーリングとして知られる手法は、メモリ圧力を低く保ち、重要なレンダリングフェーズ中にガベージコレクタが介入するのを防ぎます。
さらに、Three.jsシーンからオブジェクトを削除する際、GPUリソース(ジオメトリバッファ、テクスチャ、シェーダー)はJavaScriptのガベージコレクタによって自動的に解放されないことを覚えておいてください。関連するWebGLメモリを解放するには、ジオメトリ、マテリアル、テクスチャに対して.dispose()メソッドを明示的に呼び出す必要があります。これを怠るとメモリリークが発生し、最終的にアプリケーションがクラッシュします。
まとめ
WebGLおよびThree.jsアプリケーションの最適化は、プロファイリングと測定を必要とする反復的なプロセスです。ブラウザのパフォーマンスプロファイラ、WebGLインスペクタ拡張機能(例:Spector.js)、Three.jsの組み込みWebGLRenderer.infoなどのツールを使用してボトルネックを特定します。ドローコールを慎重に管理し、ジオメトリとシェーダーを最適化し、圧縮テクスチャを利用し、高度なレンダリング戦略を採用することで、複雑な3Dシーンでもスムーズな60 FPS(またはそれ以上)のエクスペリエンスを実現できます。
こちらもおすすめです
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Next.jsとWebGLの統合:包括的なガイド
Three.jsキャンバス設定、React Three Fiber最適化、SSRハイドレーションの安全性、60FPSレンダリングを通じて、WebGLグラフィックスをNext.jsにシームレスに統合します。
Read more
開発者のための量子コンピューティング
Qiskitで量子アルゴリズムを記述し、量子ゲートを理解し、古典的なハードウェアで回路をシミュレートする方法を解説する、開発者向けの量子コンピューティングガイド。
Read more
OpenTelemetryによる分散トレーシング
OpenTelemetry分散トレーシングでマイクロサービスを計測し、サービス間のコンテキスト伝播、レイテンシーのボトルネックをトレースし、Jaegerにエクスポートします。
Read more