HN 日本語サマリー

← 一覧へ戻る
AI・機械学習

Gaussian Splats Part 3: いつでもより大きなシーンがある

Gaussian Splats part 3: There's always a bigger scene (magnopus.com)

26 pointsby ibobev4 コメント

要約

この記事は、Gaussian Splats(ガウシアン スプラット)レンダリングにおける「無制限」という目標への道のりを詳述しています。当初、GPUシーンのインスタンス数制限(約1600万)に直面しましたが、カスタム描画パイプラインへの移行でこれを克服しました。次に、VRAM容量の限界(1枚のテクスチャあたり約6700万スプラット)に直面し、アセットのテクスチャ幅を広げることで約2億6800万スプラットまで対応できるようになりました。さらに、シーン全体をメモリに保持できない問題に対し、LOD(レベル オブ ディテール)とストリーミング技術を導入し、メモリ予算内で任意のサイズのシーンをレンダリングするシステムを構築しました。

全文翻訳

Gaussian Splats Part 3: いつでもより大きなシーンがある

[画像]

前回(OKOにて)…

時は2025年、前回書いたように、業界はGaussian Splatsに夢中になり、Magnopusもそれに続きました。私はしばらくの間、UnrealのスプラットレンダラーをNiagaraから引き剥がし、エンジンのインスタンスレンダリングを中心に再構築していました。各スプラットはインスタンスとして登録され、毎フレームGPU側でソートが行われて透明度を正しい順序で合成し、マテリアルがそれぞれをソフトな楕円形にシェーディングしていました。

それは機能し、十分によく機能したので、私はそれについて記事を一本書きました。自分自身にかなり満足していました。

さて、実際には、無制限のスプラットをレンダリングできると主張したとき、私はあまりにも早くお祝いしてしまったようです。現実には、いくつかの壁があり、最初の壁は予想よりもずっと近くにありました。

それほど無制限ではなかった…

最初の壁

私がこの前の記事を公開した後に学んだ、謙虚なことがあります。私が披露していたレンダラーには、アセットあたり約1600万スプラットというハードキャップがありました。全く知りませんでした。テストしたすべてのアセットはその下にあったのですが、制限は常に存在していました。私の傲慢さゆえに、それに気づかなかっただけです。

その制限は、スプラットごとのデータが実際にどこに格納されるかに起因していました。各スプラットをUnrealの最新のメッシュインスタンスとして登録することは、エンジンのGPUシーンにそれを渡すことを意味しました。GPUシーンは、通常、GPU上に存在するワールド内のほぼすべてのものの中心的な台帳です。

そして、GPUシーンはその機能を実行する上で素晴らしいですが、それは数千のメッシュインスタンスをサポートするために構築されたものであり、数千万、数億という数ではありません。内部的には、特定のメッシュのインスタンスは最大1600万という上限があります。それを超えると、物事はバラバラになり、アセットの一部しか一度に見えなくなり、グラフィカルな破損が多発し、ビューポートに失敗したことを知らせる警告が表示されます。

単純な解決策、そして私にとってうまくいったのは、スプラットを追跡するためにGPUシーンの使用を完全にやめることでした。代わりに、私は完全に昔ながらの方法に切り替え、より伝統的なインスタンスレンダリングパイプラインに依存しました。これは、一般的にDrawIndexedInstancedIndirectとして知られているものの直接的な呼び出しにほぼ相当しました。

これにはいくつかの重要なステップが必要でした:

  1. GPUシーンへのインスタンス登録をスキップし、代わりにクラシックな間接インスタンス描画を行います。
  2. 間接描画バッファに、毎フレームソートキーを生成しながらすべてのスプラットを反復処理していたソートキー生成シェーダーからの描画カウントを供給します。
  3. すべてスプラットインスタンスデータをテクスチャ経由でGPUに格納します(これは前回の記事から無料で入手しました)。
  4. 小さな頂点ファクトリ(頂点シェーダーとそのマテリアルバインディングのUnrealの抽象化)を書き、頂点シェーダーがソート済みスプラットバッファ(前回の記事を参照)をサンプリングし、適切なスプラット属性を取得してマテリアルに渡すようにします。

レンダリングされた画像は、以前とピクセル単位で同一でしたが、それを課していたものがループから外れたため、1600万の上限は消滅しました。約3000万スプラットのテストアセットは、問題なくレンダリングされました。さらに良くなりました。

GPUシーンの最大インスタンス数の制約から解放された。

カスタム頂点ファクトリのアプローチからは、他にもいくつかの良い点が生まれました。スプラットメッシュは常に単純な四角形なので、シェーダー自体で手続き的に導出するのが簡単です。これにより、実際のUnrealメッシュを指定する必要がなくなり、システムは頂点シェーダーで頂点属性を導出するだけで、すべてがよりターンキーになりました。

そして、間接インスタンス描画が実質的にバインドレスレンダリングアプローチに移行したという他の利点もあります。

毎フレームすべてスプラットを反復処理してソートキーを生成していたため、より高度なスプラットごとのカリングもそこで行うことができ、その後、間接描画バッファに表示可能なスプラットの合計数を書き込むことができました。今では、カメラのフラスタム内に表示されるスプラットの数だけを描画しており、カリングロジックは完全にGPU上にありました。

[画像]

以前 – GPUシーンの実装

[画像]

以後 – 間接インスタンス描画とスプラットごとのカリング

これはパフォーマンスの向上という点で素晴らしいですが、注目すべきは、毎フレーム描画されるスプラットの数が真に可変になり、完全にGPUで決定されるようになったことです。パイプラインは、レベルオブディテール(LOD)システムに必要な形状に成長しました。毎フレーム異なるサブセットを描画し、可能な限り遅い時点で決定されます。

私はLODへの参入路を構築しようとしていたわけではありません。しかし、結果的にそうなりました。

傲慢さ:再訪

インスタンス上限を引き上げたことは、フィニッシュラインのように感じられました。しかし、1週間も経たないうちに次の壁にぶつかりました。

GPUシーンからのエクスフルト(exfiltration)は、描画できるスプラットの数を変えましたが、保持できる数ではありませんでした。シーン内のすべてスプラットは常にGPUメモリに存在していました。シーン全体が収まる場合は問題ありません。しかし、明らかにスプラットはメモリの観点から無料ではありません。位置、スケール、向き、不透明度、球面調和係数など、すべてを合わせると、できる限りタイトにパックしても、1スプラットあたり約32バイトになります。

これは、VRAMにかなりの数のスプラットを投入できることを意味しますが、実際のサイトのフォトグラメトリキャプチャに遭遇すると、現実は残念ながら高解像度であり、VRAM予算を気にしないことを発見します。

私たちが人に見せたかったキャプチャは、マシンの容量を超えていました。そして、それ以上巧妙なパッキングはできませんでした。したがって、シーン全体を一度に常駐させることはできません。つまり、その一部しか常駐できません。つまり、レンダラーは毎フレーム、どの部分をレンダリングするかを決定し、それを取得して画面に描画する必要があります。前に聞いたことがあるなら、私を止めてください。

スプラットにおけるLODの意味

リアルタイムグラフィックスに触れたことがあるなら、LODについて知っているでしょう。遠くのものはより安価な代替品になります。遠くの木のメッシュは、低ポリゴンの木になり、次にビルボードになり、次にシミになります。距離に応じて物事は安価になります。

スプラットには捨てるポリゴンはありませんが、それよりも良いものがあります。数が多すぎ、その密度の多くは40メートル離れていて幅6ピクセルしかない領域では無駄になります。したがって、スプラットのLODは、領域のデシメート(間引き)されたコピーです。同じブロブクラウドで、ブロブのほとんどが削除され、残りが拡大されてギャップを埋めます。LODレベル0では、すべてスプラットを表示し、より高いLODレベルでは、徐々に少なく表示します。

私たちが選択したフォーマット、PlayCanvasのStreamed SOGは、これをあなたのために焼き付けます。それは本質的にシーンを領域の空間ツリー(「リーフ」)に分割し、各リーフはいくつかのデシメートされたLODレベルを記述します。ルートにある小さなlod-meta.jsonが、ツリー全体の構造と各チャンクの場所を記述します。

[画像]

LODレベル0 – 高密度

[画像]

LODレベル3 – 低密度

描画するものを決定する

さて。LODデータはあります。では、毎フレーム、表示可能な各領域について、LODレベルを選択します。素晴らしい、具体的にどうやって?

私の最初の本能は、何か巧妙なものに手を伸ばすことでした。私の2番目の、より良い本能は、重要なのは画面上でどれだけ間違って見えるかだけであり、「どれだけ間違っているか」はピクセルで測定されることを思い出すことでした。

これには、スクリーン空間誤差(SSE)という数十年前のアイデアがあります。これは、指定されたレベルがどれだけの視覚的誤差を導入するかを計算し、それを現在のカメラ距離でピクセルに投影し、誤差があるしきい値を下回る最も粗いレベルを選択するというものです。しきい値をτ(タウ)と呼びます。グラフィックスではすべてが最終的にギリシャ文字になるからです。

スプラットには測定するメッシュはありませんが、密度でいくらかごまかすことができます。領域がその体積にどれだけのスプラットを詰め込んでいるかから、それらの間の近似間隔が得られ、それをピクセルサイズに投影し、τと比較します。画面上で大きく高密度ですか?レベル0を獲得しました。小さく遠いですか?粗いものをどうぞ。誰も知りません。

SSEメトリックに基づいてLODレベルを切り替えるレンダラーの誇張されたビュー。

最終的に、τはユーザーに公開されたダイヤルになり、プロジェクト設定で低/中/高品質オプションとして人間が理解できる用語でフレーミングされました。高はデフォルトであり、「詳細を自由に使う」ことを意味しますが、低は「遠距離ではケチになる」ことを意味します。

華やかでない機械

これのあまりセクシーでない側面は、LODデータをそもそもGPUに転送するために必要な機械です。

これらのシーンのいずれかを最初に見たとき、何もロードされていません。レンダラーが起動し、プロキシが存在し、テクスチャプールには何もありません。LOD選択が実行され、「これらのチャンクが欲しい」と判断しますが、それらはまだ存在しません。それらはロードされる(あるいはダウンロードされる)必要があります。そのため、レンダラーは現在持っているものだけを描画します。最初は何もありません。次に、それらがトリクルインするにつれて最も粗いレベル、次に実際に表示している場所の細かいディテールです。

アイデアは、シーンがあなたの目の前で、粗いものから細かいものへと、ある期間にわたって自己組織化することです。

HTTP経由で配信されるスプラット、最も粗いものから最も細かいものへ。

そして、もう一つの部分は、採用したLODフォーマットがストリーミングSOGと呼ばれる本当の理由を利用することです。これは、リーフごとに、LODレベルごとにネットワーク経由で配信されるように設計されています。つまり、コンテンツを見るためにアセット全体をローカルに持つ必要はなく、周囲のリーフのLODレベルを取得するだけでよいということです。

概念的には確かにシンプルですが、それを実装することは私が最も作業するのが難しいと感じた部分でした。正しく機能させるために多くの細かい機械が必要でした。私が正しく理解するのが最も難しいと感じた概念は次のとおりです。

  • すべて非同期: チャンクのロードはファイルの読み取りを意味するかもしれませんが、それは別の大陸のサーバーへのHTTPリクエストを意味する可能性もあります。明らかに、後者のケース(あるいはどちらのケースでも)でゲームスレッドをブロックすることはできません。したがって、すべてがリクエスト/配信モデルである必要があります。チャンクを要求すると、後で、作業を完了したスレッドで発生するコールバックを介して通知されます。多くの細かいスレッド同期メカニズムが登場します。
  • 粗いLOD優先: まず領域の最も粗いレベルを要求するため、画面上に安定した低詳細バージョンをできるだけ早く取得し、その後、より細かいチャンクが着陸するとシャープになります。
  • チャンクアップロードのフェンシング: スレッド同期メカニズムといえば、チャンクのアップロードは特別な言及に値します。GPUメモリへのアップロードと、CPU側で「描画準備完了」とマークすることは、2つの異なるタイムテーブル上の2つの異なるイベントです。順序を間違えると、ゴミのような三角形の1フレームを楽しむことになります。チャンクは、アップロードが証明された後にのみ描画可能になります。
  • マルチビューサポート: 仮想プロダクションシナリオでスプラットを頻繁に使用します。そこでは、常に複数のカメラが単一のUnrealインスタンスで同時にレンダリングされています。スプラットデータは同じなので、一度だけ存在できますが、すべてのLOD選択決定、それらの決定の記録、およびそれらの決定の正しいカメラへのGPUへの送信はすべて考慮する必要があります。
最大スプラット数の引き上げ

壁は、確立されたように、一連に来ます。ストリーミングがエンドツーエンドで機能した頃、私は実際に巨大なキャプチャの1つを投入しましたが、それは次のようなエラーで失敗しました:Texture2Dを作成できません、このRHIの最大次元(16384)を超えています。

ああ。つまり、それらのスプラットはすべてテクスチャに格納されており、私は1行あたり4096個のスプラットを配置していました。テクスチャは数行しか高くできません(この場合16384行)。これは、単一テクスチャあたりの最大スプラット数が約6700万であることを意味します。その数を超えると、ドライバー/RHIレイヤーはテクスチャの作成を拒否し、すべてがバラバラになります。

最終的に、ここで修正する必要があることは2つありました。

最初のことは、恥ずかしい逸話です。初期の頃、私は1行あたり4096スプラットという定数行サイズを使用するという選択をしました。これは、1行あたり潜在的な他の(16384 - 4096)テクセルをデータで埋める機会を逃していたことを意味します。テクスチャには基本的に多くの水平スペースがありましたが、使用していませんでした。

そこで…テクスチャを広くしました。行を4096からハードウェア最大値に向かって広くすると、同じテクスチャが潜在的に2億6800万スプラット(VRAM約8.6GBに相当)を格納できるようになり、その時点で単一テクスチャの最大潜在ストレージによって実際に制限されることになります。

最終的には非常に小さな変更でした。行幅は、さまざまな場所にハードコードされた定数ではなく、アセットごとに計算される数値になりましたが、私はそれをパラブルとして気に入っています。私は長い間、それが実際には数ヶ月前に私が入力した任意の定数であったものを、根本的な制限として扱っていました。仮定について言われることは真実だと思います。

[画像]

ついに6700万のキャップを突破。1億スプラットをレンダリング(LODなし)。クレジット:Poland Jastrzebia Gora、Andrii Shramko

対処する必要があったもう1つのことは、スプラットが実際に単一のテクスチャに収まりきらない場合にどうするかということです。3億スプラットのアセット(このペースでは火曜日に発生するでしょう)に初めて遭遇したときはどうなるでしょうか?そして、答えは、メモリ制約のあるシナリオでは常にそうであるように、予算を管理することです。シーン全体を常駐させようとせず、ワーキングセットを保持します。

プールをメモリ上限にサイズ設定し、領域が表示範囲に入るとチャンクをストリーミングし、部屋がなくなったら、最も遠いものを最初にというヒューリスティックを使用して、誰も見ていないものを追放します。

これにより、ディスク上のシーンは任意のサイズにすることができます。なぜなら、メモリ内には常に限定されたスライスしか存在しないからです。ついに、レンダラーはこの言葉「無制限」に値するようになったと思います。たぶん。私の自信はまだ傷ついています。

すべてをまとめて

したがって、サーバー上のファイルから画面上のブロブまで、全体の大まかな概要は次のとおりです。

  1. ロードとデコード: ワーカー スレッドで、ディスクまたは HTTP からチャンクを取得し、GPU 用に解凍して展開し、完了したらゲームおよびレンダリング スレッドに通知します。
  2. 常駐とストリーミング: チャンクを固定テクスチャ プールにアップロードし、必要に応じて行を配布し、オンデマンドで粗いものから順にストリーミングし、メモリ予算の下で追放して、任意のサイズのシーンが収まるようにします。
  3. LOD選択: ビューごとに、フレームごとに、SSE メトリックに基づいて領域ごとにレベルを選択し、理想的なダウンロード中に最良のものを示します。
  4. ソートとカリング: コンピュート シェーダーが、カメラ距離で各表示可能なスプラットにキーを付け、オフスクリーンにあるものを破棄します。Unreal のラディックス ソートがそれを背面から前面に並べ替えるため、透明度が正しく合成されます。
  5. そして描画: 頂点ファクトリが、メモリ内にジオメトリがない各生存スプラットに対してカメラに面した四角形を生成します。マテリアルはスプラットごとのデータを受け取り、それに応じてシェーディングします。

最も優れた構造的な成果は、LODやストリーミングのない、普通の単一ファイルのスプラットシーンが、退化ケースとしてそこから派生したことです。1つのチャンクは常に常駐しており、LOD選択は単にショートカットされます。すべてが同じコードパスに従います。これは、保守/複雑性の観点から素晴らしいことです。

[画像]

HTTP経由でLODレベルを動的に取得するストリーミングSOGアセット。クレジット:Church of Saints Peter and Paul、PlayCanvas

次回まで?

それは本当に無制限でしょうか?ほぼ、だと思います。

少なくとも、これまで以上に大きなスプラット数に対して回復力があります。ディスク上のシーンは、GPUよりも大きく、HTTP経由でデータをプルするときはマシンよりも大きくても、レンダリングされ、ユーザー指定のメモリ予算内に収まります。

[画像]

数ヶ月前に私の午後を台無しにした最初の3000万スプラットは、今では数秒で開き、時間とともにシャープになります。

いつものように、地平線にはまだたくさんのことがあります。LOD選択ポリシーの実行はかなり遅いです。追放ポリシーは非常にシンプルであり、より賢くしたいです。メモリ予算をシーン全体に分散させるのではなく、実際にフレーム内にあるものに集中させること(おそらくSOGメタで定義された空間ツリー上の階層的なカリング?)オーバードローは、レンダリングパフォーマンスの点で間違いなく主要な制限です。それを減らす方法について、いつか天才的なひらめきが訪れることを願っています。

そして、私はまだ4DGSに触れていません。次の記事では、そこで冒険したことのまとめになることを強く願っています。

しかし、スプラットで構築している場合、OKO UnrealとOKO Webの両方でストリーミングSOGサポートを実装しました。ぜひ触ってみてください。壊して、それを失敗させるような巨大なキャプチャを教えてください。そして、どう思ったか教えてください。

次回まで!✌️