WebGPU版のThree.jsで大量オブジェクトの描画負荷を最適化するBundleGroup

はてなブックマークへ投稿
Xへポスト
URLをコピー
共有

Three.jsで大量のオブジェクトを並べると、形状が単純でもフレームレートが落ちることがあります。原因のひとつは、オブジェクトごとに描画命令を準備するCPU側の処理です。

WebGPU版のThree.jsには、複数の描画命令をまとめて記録し、繰り返し使えるBundleGroupバンドルグループクラスがあります。BundleGroupを使うことで描画命令の負荷を軽減し、大量のオブジェクトの描画を最適化できます。

BundleGroupは形状(ジオメトリー)や色・質感(マテリアル)の異なるオブジェクトにも使え、それぞれの位置や回転も変更できるのが特徴です。

この記事では、デモでBundleGroupの使い方を確認し、既存のシーンに組み込む方法と注意点を紹介します。WebGPU対応のThree.jsを使用しますので、Three.jsの基礎について知りたい方はぜひ下記の記事を一読ください。

本文のコードは、2026年10月時点の最新版であるThree.js r186を前提としています。

デモで描画の違いを確認しよう

次のデモでは、形や色・質感の異なるオブジェクトを配置しています。画面右上のInspectorの[Parameters]タブから操作します。[Use BundleGroup]チェックボックスでBundleGroup使用の有無を切り替えられます。[Animation]をオンにすると、オブジェクトが個別にアニメーションします。

1秒間あたりに何枚の画像(フレーム)を画面に表示できているかを示す値FPSエフピーエス(Frames Per Second)を画面右上のInspectorで確認できます。

効果の大きさは端末や表示する個数によって変わります。[Use BundleGroup]のチェックをいったん外して、[Num]を増やし、FPSが下がりはじめる個数を探します。その個数のまま[Use BundleGroup]をオンにすると、FPSの差を比べやすくなります。

筆者の環境では、BundleGroupを使用すると、60FPSを維持できるオブジェクト数がアニメーションなしでは約1.4倍、個別のアニメーションありでは約1.1倍に増加し、最適化の効果が現れています。

▼ 60FPSを維持できるオブジェクト数。多いほうがパフォーマンスが高い

条件 Group(通常) BundleGroup
アニメーションなし 4000 5900
アニメーションあり 3500 3900

検証環境:MacBook Pro / Apple M1 / Google Chrome 156

ドローコールの最適化手法

Three.jsでは、ジオメトリーで形状を、マテリアルで色や質感を指定します。両者を組み合わせた描画対象がメッシュです。位置や回転の情報はメッシュ単位で設定します。

皆さんが使っているパソコンやスマートフォンには、CPUとGPUが搭載されています。WebGPUでは、CPUが描画命令を準備し、GPUがその命令にしたがって描画します。GPUにメッシュの描画を指示する呼び出しをドローコールと呼びます。

ドローコールが増えると、CPUの負荷も高くなります。今回紹介する最適化手法は、CPU負荷を減らすための仕組みです。

ドローコールに伴う処理負荷を抑える手法はいくつかあります。ほかの最適化手法と、使えるオブジェクトや操作の違いを比べてみましょう。

手法 まとめられるオブジェクト 個別の位置・回転の変更
① インスタンシング 同じジオメトリー・同じマテリアルのメッシュ 個別の位置・回転を行列で指定できる
② ジオメトリーのマージ 異なるジオメトリー・同じマテリアルのメッシュ 結合後はひとつのメッシュとして扱うため、個別操作できない
③ BundleGroup 異なるジオメトリー・異なるマテリアルのメッシュ メッシュごとに位置・回転を変更できる

① インスタンシング

インスタンシングは同一のジオメトリーを一度のドローコールで複数描画できる機能です。そのため、同じ形のものを大量に描く場合に有効です。Three.jsではInstancedMeshクラスを使用します。位置・回転などを個別に変更できます。インスタンシングの概念について、詳しくは記事『サンプルで理解するWebGL 2.0 - WebGL 2.0の利点とは』で紹介しています。

② ジオメトリーのマージ

ジオメトリーのマージは複数のジオメトリーを結合してひとつのジオメトリーにします。そのため、動かさない背景などに向いています。結合後はひとつのメッシュになるため、元のオブジェクトを個別に回転させたい場合には不向きです。複数のマテリアルで描画範囲をわけると、その範囲ごとにドローコールが発生します。詳しくは、記事『Three.jsの高速化手法:ジオメトリの結合』で紹介しています。

③ BundleGroup

本記事で詳しく紹介するBundleGroupは、描画命令を再利用してCPU側の負荷を軽減する手法です。異なるジオメトリーやマテリアルのメッシュをまとめられます。

※デモの[Draw calls]は、renderer.info.render.drawCallsの値です。[Draw calls: 1]と表示されても、GPUが実行する描画命令の数が1になるという意味ではありません。この表示には、再利用したバンドル内の描画命令が含まれていないためです。

BundleGroupの使い方

BundleGroupの使い方はシンプルで、導入の基本は、メッシュの追加先をBundleGroupに変更することです。BundleGroupはWebGPUのみの機能のため、WebGPURendererクラスを使用します。

メッシュをシーンやGroupに追加するscene.add(mesh)の処理を、次の3行に置き換えます。カメラやメッシュを動かす場合は、後述の設定も必要です。

const bundleGroup = new THREE.BundleGroup();
scene.add(bundleGroup);
bundleGroup.add(mesh);

以下は、ジオメトリーとマテリアルの異なる2個のメッシュを個別に回転させる場合のおおまかな流れです。

import * as THREE from "three/webgpu";

// 必ずWebGPURendererを使用する
const renderer = new THREE.WebGPURenderer();

// レンダラー、シーン(scene)、カメラ(camera)のセットアップ(省略)

// ① BundleGroupの作成
const bundleGroup = new THREE.BundleGroup();
// メッシュの位置・回転の変更を反映する場合の設定
bundleGroup.static = false;
scene.add(bundleGroup);

// 異なるジオメトリー・マテリアルのMesh、boxおよびtorusを用意する(省略)
const meshes = [box, torus];

// ② 最適化したいメッシュをBundleGroupに追加する
for (const mesh of meshes) {
  // フラスタムカリングを無効化する
  mesh.frustumCulled = false;
  bundleGroup.add(mesh);
}

// ③ アニメーションループで個別にメッシュの回転を変更する
renderer.setAnimationLoop(() => {
  box.rotation.y += 0.01;
  torus.rotation.x += 0.05;
  renderer.render(scene, camera);
});

メッシュの位置や回転を動的に変える場合は、BundleGroupのstaticプロパティをfalseにします。

メッシュのfrustumCulledプロパティは、画面外のメッシュを描画対象から除くかどうかの設定です。BundleGroupでは、画面外のメッシュも描画命令に含めるために、各メッシュのfrustumCulledプロパティをfalseにします。詳しくは後述の「カメラの表示範囲外にあるオブジェクトも描画対象になる」で説明します。

BundleGroupの仕組み

WebGPUは、WebGLと比べて描画命令を効率的に処理できる設計です。それでも、JavaScriptから毎フレーム何千回も命令を記録すると、CPU側に負荷がかかります。

WebGPUのRenderBundleレンダーバンドルは、マテリアルに応じた描画の設定やドローコールなど、一連の命令を記録して保存する仕組みです。Three.jsのBundleGroupは、内部でこのRenderBundleを使用します。

たとえば、3つのメッシュを描く場合、通常は毎フレーム、それぞれの描画命令をdraw()で記録します。RenderBundleでは3つの描画命令をあらかじめ一度だけ記録し、毎フレームexecuteBundles()でまとめて呼び出します。

これにより、毎フレームの描画命令の記録やAPI呼び出しを減らし、CPU側の負荷を軽減できます。GPUが描画するメッシュは、どちらも毎フレーム3つです。

▼ 3つのメッシュを描画する際の処理の比較。上段は通常の描画、下段はRenderBundleを使用した描画。

使うときの注意点

カメラの表示範囲外にあるオブジェクトも描画対象になる

Three.jsは通常、メッシュがカメラの表示範囲内にあるかをCPUでおおまかに判定し、範囲外のメッシュを描画対象から除きます。この処理をフラスタムカリングと呼びます。画面に見える範囲だけを描くことで負荷を抑えます。

BundleGroupでは、記録済みの描画命令から、カメラの動きに応じて一部のメッシュだけを取り除くことはできません。反対に、記録時に画面外だったメッシュの命令が含まれていなければ、あとからカメラを向けても描画されません。

各メッシュのfrustumCulledプロパティをfalseにすることで、画面外も含めた全メッシュの描画命令を記録できます。

ただし、GPUは画面外のメッシュの描画命令も実行するため、見えない頂点の処理にも負荷がかかります。広い街を歩き回るシーンなど、画面外の物体が多い場面では、BundleGroupを使わないほうが速いこともあります。

▼ フラスタムカリングの有無による比較。青は描画命令の対象、灰色は対象外。各メッシュのフラスタムカリングを無効にしても、画面に映る範囲は変わらない。

デモでは、BundleGroupの使用をオフにした場合には通常通りフラスタムカリングを行う設定にしています。カメラを近づけて画面に映るメッシュの数を減らすと[Draw calls]が減り、FPSが高くなることがわかります。

BundleGroupの使用をオンにすると、カメラを近づけても常にすべてのメッシュを描画し、描画命令の数は変わりません。CPUの処理負荷を減らしたいのかGPUの処理負荷を減らしたいのか、シーンによって慎重に見極めましょう。

※画面外のメッシュを描画する場合、負荷が大きくなりやすいピクセルの色の計算は省かれるため、GPU負荷の増加は抑えられます。

半透明のオブジェクトは描画順序に注意

半透明のオブジェクトは、先に描いた背景の色と混ぜ合わせて表示します。順序が見た目に影響するため、通常は不透明な物体を先に描き、その後に半透明の物体を奥から手前へ描きます。

▼ 半透明の赤・緑・青の四角を不透明な画像の上に重ねた例。矢印はカメラから見た手前から奥への並びを示す。左右で赤(R)と青(B)の前後関係を入れ替えているため、奥から手前へ正しく描画すると、重なった部分の色が変わる。

RenderBundle内の命令の順序は、再利用している間は変わりません。カメラから見たメッシュの奥と手前の位置関係が逆転しても最初の描画順序が維持されます。そのため、メッシュの前後関係に合わない色で重なることがあります。

また、Three.jsのBundleGroupでは、グループ内の半透明メッシュを奥から手前に並べ替える処理も行われません。カメラやメッシュを動かさない場合でも、正しく重なるとは限りません。半透明なメッシュを含むシーンでは、BundleGroup適用前後の見た目を比較して重なり方や色が意図どおりか確認してください。

メッシュを追加・削除したら命令を記録し直す

命令の記録後にグループにメッシュを追加・削除する場合、add()やremove()を呼んだだけでは画面に反映されません。追加・削除のタイミングでBundleGroupのneedsUpdateプロパティをtrueにすることにより、次の描画時に命令が記録し直されます。

次の例では、追加するメッシュをnewMesh変数に代入してあるものとします。

newMesh.frustumCulled = false;
bundleGroup.add(newMesh);
// BundleGroupの構成を更新したい場合はneedsUpdateをtrueにする
bundleGroup.needsUpdate = true;

BundleGroup内のメッシュについて、次の変更をする場合にもbundleGroup.needsUpdate = trueを指定します。

  • visibleプロパティによる表示・非表示の切り替え
  • ジオメトリーの変更
  • マテリアルの変更
    • マテリアル自体の差し替え
    • テクスチャの差し替え
    • wireframeなど描画命令を変える設定の変更

ただし、頻繁に記録し直すと、再利用による利点がなくなります。描画命令を毎フレーム記録し直すなど最悪のケースではBundleGroupを使用しない場合より処理が重くなることもあるので注意が必要です。

まとめ

最適化を行う際、BundleGroupの採用について、おおまかに下記の観点で判断すると良いでしょう。

  • オブジェクトが2個以上あるシーン
    • BundleGroup採用価値あり
  • 視点の変化や動きによって、各オブジェクトがカメラ内外に激しく出入りするシーン
    • BundleGroupを使用しないほうがパフォーマンスが出る可能性あり。採用は計測して判断
  • 半透明のオブジェクトが混在するシーン
    • 意図しない見た目になる可能性あり。採用は慎重に。ただしパフォーマンスとトレードオフで見た目の不正確さを許容できるかも検討する

BundleGroupクラスは、CPU側の描画処理が重い場面で効果が期待できます。柔軟性も高く、既存のコードへの組み込みもかんたんです。筆者としては、複数のメッシュを表示するシーンであればまず試してみる価値のある最適化だと考えています。この記事でも紹介した注意点に気をつけて、ぜひ試してみてください。

Google検索でICS MEDIAの記事を見つけやすくしよう

優先するソースに追加すると、GoogleのトップニュースやAI検索でICS MEDIAの記事が表示されやすくなります。

Googleで優先するソースとして追加
SNSでシェアしよう
シェアいただくと、サイト運営の励みになります!
はてなブックマークへ投稿
Xへポスト
URLをコピー
共有
川勝 研太郎

インタラクティブディベロッパー。ゲーム技術、GPUとその周辺技術について日々勉強中。自宅周辺の移動手段は自転車。

この担当の記事一覧
新着記事のお知らせCSSだけでつくる! スクロールトリガーアニメーション