
2026/10/23 23:07
Gaussian Splats パート 3:常により大きなシーンが存在する
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
2025 年、著者は Unreal Engine にて GPU Scene のインスタンス制限により、アセットあたり約 1600 万個のスプラットという公式には明記されておらず実際上の上限値を発見しました。これはガウシアン・スプラットの真正に無限のレンダリングを妨げていました。この制約は、標準的な GPU Scene を廃止し、代わりに直接
DrawIndexedInstancedIndirect パイプラインを実装することで解決され、1600 万インスタンスという天井が撤廃されるとともに、バインドレス・レンダリングの実現、頂点ファクトリを用いた手動の四角形生成、および可視なスプラットのみを描画するための GPU 上で実行する個々のスプラットレベルのカリングが可能になりました。LOD システムは PlayCanvas の Streamed SOG フォーマットを採用し導入され、遠方のビューにおける Blob クラウド領域を細分化(デシメーション)させると同時に残存する要素を拡大する仕組みが構築されました。LOD 選択にはスクリーンスペース誤差計測値を用い、品質設定に応じてユーザーが閾値をカスタマイズできるようになりました。アセット読み込みは HTTP を介した非同期処理によって実行され、細かな詳細ストリーミング前に粗い LOD を優先的にロードし安定性を確保する方式を採用しています。主な技術的課題には、ゴミポリゴン(ゲルベージ三角形)が発生しないようチャンクアップロード時のスレッド同期を管理すること、およびバーチャルプロダクションシナリオにおけるマルチビュー・レンダリングのサポートがありました。テクスチャサイズ制限(RHI 最大横幅 16384)により 6700 万スプラットという上限に遭遇しましたが、これは行幅を固定の 4096 から動的に変更することで解消し、単一テクスチャでの容量は約 2.68 億スプラット(~8.6GB の VRAM)に引き上げられました。さらに、ユーザーが指定したメモリ予算内であり続けるようデータをストリーミングしながら動作するワーキングセットメモリ予算と、「最も遠距離からのものを優先して破棄する」というヒューリスティックな排除ロジックを組み合わせたことで、GPU メモリ容量またはマシン全体のサイズよりも大きいディスク上のシーンも滑らかに動作させることが可能になりました。現在、Streamed SOG のサポートは OKO Unreal および OKO Web の両プラットフォームで実装されています。将来の改善では、遅い LOD 選択ポリシーの最適化、単純な排除ロジックの強化、パフォーマンス制限によるオーバードロウの削減に焦点を当て、標準的なハードウェア上でのバーチャルプロダクションにおける超高密度レンダリングの実現を民主化する取り組みを進めます。本文
OKO エンジン開発記:無限のスプライト描画への道筋(その 2)
2025 年、業界は**ガウシアンスプライト(Gaussian Splat)技術に夢中でありました。当時は Unreal Engine のインスタンス化描画システムを再構築し、スプライト描画回りに時間を費やしました。しかし、「無限に多くのスプライトを描画できる」**と主張したことが早すぎたお祝いだったと後知りました。現実にはいくつかの技術的壁が存在していたのです。
最初の壁:GPU シーンのハードキャップ
自虐的な事実として、導入していたレンダラーにはアセットごとの約 1600 万個というスプライト数の制限がありました。これは GPU シーン(GPU Scene)内部の中央帳簿に組み込まれた制約であり、この数を超えるとシステムが崩壊します。
- 症状: アセットの一部しか表示されず、グラフィックの破綻や警告メッセージが発生する。
- 原因: GPU シーンは非常に強力ですが、インスタンス数は最大 1600 万個に厳格に制限されています。
安易な解決策:古典的な描画パイプラインへ回帰
この制限を回避するため、私は GPU シーンのデータ追跡を廃止し、従来の**
DrawIndexedInstancedIndirect**と呼ばれる間接型インスタンス描画パイプラインに回帰しました。これにより約 3000 万個のスプライトを描画することが可能となりました。
アプローチの概要
- GPU シーン登録のスキップ: インスタンスを GPU シーンに登録せず、古典的な間接描画を使用。
- バッファ供給: ソートキー生成中にカウント値を間接描画バッファに直接供給。
- データ格納: スプライトインスタンスデータをテクスチャ経由で GPU へ格納(メモリ効率向上)。
- Vertex Factory の活用: シンプルな四角形メッシュをシェーダー内で生成し、顶点属性を自動導出することで「ターンキー」型の使いやすさを確保。
得られたメリット
- 1600 万の天井消失: 制限を課していた要素そのものがパイプラインから消えたため、約 3000 万個のスプライトを快適に描画可能。
- 高度なクイールリング(Culling): フレーム毎に全スプライトを巡回するため、カメラのフラストラム内にあるスプライト数だけを記録・描画する効率的な LOD 判定が可能に。
- LOD システムへの進化: パイプラインはレベル・オブ・ディテール(LOD)システムに必要な形状へと成長しました。
傲慢:メモリ制限との対決
ハードキャップを取り払った直後、**「GPU メモリ上にすべてのスプライトを保持しなければならない」**という新たな壁に直面しました。
- メモリコスト: スプライトは位置、スケール、回転、不透明度など約 32 バイト/スプライトの容量を占有。
- 現実の問題: フォトグラメトリデータなどの高解像度キャプチャーは、マシンの VRAM 予算を超えるほど巨大です。
- 解決方針: シーン全体を同時にメモリ上に保持することは不可能であり、**「どの部分をレンダリングするか」**を毎フレーム決定する必要があります。
スプライトにおける LOD の意味
リアルタイムグラフィックスでは距離に応じたコスト削減(LOD)が不可欠です。スプライトにはポリゴン削減という概念はなく、代わりに**「数」**を調整します。
- ストリーミング SOG: PlayCanvas が採用する空間的なリーフツリー方式を採用。各リーフに複数の削減された LOD レベルを持たせ、ルートに
で構造を記述。lod-meta.json - LOD 戦略:
- Level 0: すべてのスプライトを表示(高密度)。
- レベル上昇: 表示数を減らし、ギャップを埋める方式(密度削減版)。
何を描画するかを決定する:スクリーン空間エラー(SSE)
可視領域に対して最適な LOD レベルを選択するため、**「スクリーン空間エラー(Screen Space Error, SSE)」**という数十年の歴史を持つ手法を利用します。
- 判定基準: 「画面で見えるほど誤っているかどうか」はピクセル単位で測定されるべき。
- SSE メトリック:
- 特定の LOD が導入する視覚的エラーを計算。
- カメラ距離に合わせてピクセル単位の閾値(τ)と比較。
- 誤差が閾値(τ)を下回る最も粗いレベルを選択。
- 密度活用: メッシュと比較できないスプライトでも、領域内のスプライト数から間隔を推定し、画面サイズと高密度性を考慮して判定。
ユーザー設定のフレームワーク
- High(高品質): デフォルト。「自由に使って詳細を出す」。
- Low(低品質): 「遠距離では吝嗇的になる」ことでリソースを節約。
華やかな機械装置の裏側:非同期 LOD ロード
LOD データはすべて GPU 上に存在していません。最初はロードされていないため、レンダラーは「これらのチャンクをください」と計算しますが、実際に取得するまで待たなければなりません。
ロードプロセス
- 開始: テクスチャプールに何もない状態からスタート。
- 流入: HTTP を介して粗い LOD から細かいものへと順次配信され、シーンが組み上がっていく。
- ストリーミング SOG の利点: ネットワーク経由でリーフ単位・LOD レベル単位を取得すれば良いため、コンテンツ全体をローカルに持つ必要がない。
インセンブル上の難問(非同期実装)
- すべて非同期: チャンクロードはファイル読取や HTTP リクエストのいずれでも可能。ゲームスレッドをブロックしないため、コールバックによる事後通知が必要。
- 粗い LOD を先に: 最も粗い LOD から要求し、画面に安定した低詳細版を表示させた後、徐々にシャープ化させる。
- チャンク・アップロードフェンシング: CPU の「描画準備完了」と GPU メモリへのアップロードは異なるタイムスケール。アップロード完了後のフレームから初めて描画可能とする。
- マルチビューサポート: バーチャルプロダクションで 1 つのインスタンスに複数のカメラが同時レンダリングされる場合、スプライトデータを共有しつつ、各カメラ向け LOD 決定を個別に伝達する必要がある。
最大スプラット数を引き上げる:テクスチャ制限の突破
巨大なキャプチャーを投げる際、
Texture2D の作成時に**「RHI の最大次元(16384)を超えました」**というエラーが発生しました。すべてのスプライトはテクスチャ内に存在するため、このサイズがボトルネックとなっていました。
解決策:行幅の拡大とワーキングセット戦略
- 行幅の最大化: 初期に行幅を 4096 に固定しましたが、ハードウェア最大値まで広げました。これにより単一テクスチャで約 2.68 億個のスプライト(約 8.6GB)を格納可能となりました。
- 教訓: 長く扱っていた「根本的な制限」が実際はプログラム側の定数であったことに気づく。
- ワーキングセット方式: スプライトが 1 テクスチャに収まらない場合、シーン全体を保持せず、プールサイズをメモリ予算に合わせて設定します。
- ストリーミング読み込み: 視野に入る領域のチャンクを読み込む。
- 淘汰(キックアウト): 空き容量不足時に「誰も見ないもの」や「最も遠いもの」から優先的にメモリから追い出す。
これでディスク上のシーンはあらゆるサイズに対応できるようになり、**「無限」**という表現を獲得しました。
プロセスの全体像:すべて同時に
サーバー上のファイルから画面へ至るまでのプロセスは以下の通りです。
- 読み込みとデコード: ワーカースレッドでディスク/HTTP からチャンクを読み込み、圧縮を解除して GPU 形式に変換。完了時にゲームスレッドとレンダリングスレッドに通知。
- 存在性(レジデンシー)管理: チャンクを固定テクスチャプールにアップロードし、必要に応じて粗い LOD を先にストリーミング。メモリ予算超時は古いデータを追い出す。
- LOD 選択: 各フレームで SSE メトリックに基づき、可視領域に対して最適なレベルを選択。
- ソートとクイールリング: GPU シェーダーがカメラ距離ごとにスプライトをキー化・ソートし、画面外のものを取り除く(Unreal の基数ソートによる透明合成保証)。
- 描画(Draw): Vertex Factory が四角形を生成。メモリ内には幾何形状は存在せず、マテリアルがデータを取得してシェーディングを実行。
システムの優しさ:単一ファイルとの親和性
このシステムにおいて、単一ファイルのスプライトシーン(LOD/ストリーミングなし)も縮退ケースとして自然に機能します。1 チャンクとして常にレジデントとなり、LOD 選択がショートサーキットするため、すべてが同じコードパスで処理されメンテナンス性が向上しています。
次回まで?:無限への近づく歩み
現在のシステムは**「真に無限ではありませんが、以前よりもはるかに堅牢」**です。
- ディスク容量 > GPU メモリ: デスク上のシーンの方が巨大でも、HTTP 経由でのデータ取得によりユーザーのメモリ予算内に収めてレンダリング可能。
- 高速化: 数千万個のスプライトも数秒で開き、時間とともにシャープになる。
- 今後の課題: LOD 選択ポリシーの最適化、追い出しポリシーの高度化、オーバー・ドロウ(Draw Call)削減など。
OKO Unreal と OKO Web でのストリーミング SOG サポート実装をぜひお試しください。壊してしまい、どの巨大なキャプチャーが崩れるかを観察し、ご感想をお聞かせください。