WebGPUにおけるUE5 Nanite技術
(github.com/Scthe)- Nanite WebGPUは、WebGPUによってブラウザ内でUE5のNanite方式レンダリングを実装したプロジェクトであり、meshlet LOD階層、ソフトウェアラスタライザー、billboard impostor、インスタンス/meshlet単位のカリングを含む
- 実装は、OBJ入力をブラウザで前処理するシンプルさと、チェックボックスやスライダーで設定を変更しながら性能変化を実験することを主な目標としている
- デモはJinxシーンの640M triangles、Lucy/Dragonsなど最大1.7B trianglesのシーンを提供し、Jinxの白い三角形はWebGPUの制約により32ビットに圧縮して処理したソフトウェアラスタライズ結果である
- WebGPUには
atomic<u64>がないため、visibility bufferベースの効率的なソフトウェアラスタライザーをそのまま実装しにくく、現在の実装はdepthu16とoctahedron normal2*u8を32ビットに詰めて精度アーティファクトを受け入れている - 実際のNanite方式実装で中核となる難題は単純な三角形数ではなく、mesh simplificationとerror metricであり、著者は独自実装がUE5とは異なり、単純化・ストリーミング・圧縮・visibility buffer・2-pass occlusion cullingなどを備えていないと明記している
プロジェクト概要
- Nanite WebGPUは、WebGPUを使ってWebブラウザでNanite方式レンダリングを実装したプロジェクトである
- 含まれる主な要素は、meshlet LOD hierarchy、ソフトウェアラスタライザー、billboard impostor、インスタンスおよびmeshlet単位のカリングである
- テクスチャモデルとper-vertex normalをサポートし、各設定にスライダーやチェックボックスが用意されている
- WebGPUはChromeでのみ利用できる
- アプリ全体はDenoを使ってオフラインでも実行可能で、シェーダーの単体テストもこの方法で書かれている
デモシーンと規模
- Jinxデモは120×120インスタンス、640M triangles規模である
- 単一のJinxモデルは44k trianglesから3k triangles、59個のroot meshletsへと単純化される
- 白い三角形は、ハードウェアラスタライズ領域と遠景のimpostorの間にあるソフトウェアラスタライズ三角形である
- Lucy and dragonsは2つのオブジェクトをそれぞれ70×70インスタンスで配置し、1.7B trianglesを構成している
- サンプルシーンでは三角形のほぼ98%がソフトウェアラスタライズされ、この経路がハードウェアよりはるかに高速だとしている
- Lucyは110×110インスタンス、1.2B trianglesのシーンである
- 単一のLucy statueは100k trianglesから86 trianglesの単一root meshletへと単純化される
- Dragonsは70×70インスタンス、1.2B trianglesのシーンである
- 単一のdragonは250k trianglesから102 trianglesの単一root meshletへと単純化される
- Bunniesは500×500インスタンス、1.2B trianglesのシーンである
- 単一のbunnyは5k trianglesから96 trianglesの単一root meshletへと単純化される
- bunnyはサイズが小さいため、ほとんどがfrustum cullingされる
実装された機能
-
Meshlet LOD hierarchy
- mesh preprocessingはブラウザ上で実行される
- meshoptimizerとMETISをWebAssemblyで使用している
- ページ更新のたびに前処理を待たなくて済むよう、ファイルexporterも提供されている
-
Software rasterizer
- WebGPUには効率的な実装に必要な
atomic<u64>がない - 現在の実装はdepth
u16とoctahedron-encoded normals2*u8を32ビットにパックしている - 32ビット制限のため精度を大きく犠牲にしており、目的はラスタライザーが動作することを示す点にある
- この制約はocclusion cullingに使うdepth pyramidにも影響する
- WebGPUには効率的な実装に必要な
-
Billboard impostors
- UP軸の周囲にある12枚の画像を使い、カメラ位置に応じてditheringでblendする
- 上下視点は処理しない
- diffuseとnormalの両方を含め、ランタイムshadingを行う
- Impostors previewはimpostor texture sizeを大きくしたデモである
カリングとレンダリング経路
- カリングは複数段階で構成される
- Per-instance: frustum cullingとocclusion culling
- Per-meshlet: frustum cullingとocclusion culling
- Per-triangle: hardware backface cullingとz-buffer
- WebGPUにはearly-zがない
- occlusion cullingは、前フレームのdepth bufferから作成したdepth pyramidのみを使用する
- reprojectionやtwo-pass方式はない
- 現在の実装だけでも多くの三角形をカリングし、性能改善を判断するには十分だとみている
- GPU-driven renderingとシンプルなCPU実装を切り替えられる
- CPU版は多くの最適化をしていないが、デバッガでステップ実行できる
- “Freeze culling”機能を使うと、カメラを動かしながら最後のフレームで描画されたものだけを確認できる
使い勝手とデバッグ
- 移動は
[W, S, A, D]、上昇/下降は[Z, SPACEBAR]、高速移動は[Shift]を使う - おかしな結果が見えた場合は、cullingオプションをオン/オフして確認できる
- 実装にはいくつか小さなバグがある
- 白い三角形はソフトウェアラスタライズされた三角形である
- WebGPUで
atomic<u64>が使えないため、depthとnormalを32ビットに圧縮している - 16-bit depthはz-fightingやleakのようなアーティファクトを多く生む可能性がある
- ソフトウェアラスタライザーを無効にするとrawなNanite meshletsを見やすくなるが、大きな性能低下が生じる可能性がある
- WebGPUで
- FPSはブラウザの強制VSyncのために揺れることがあり、タイミング確認には“Profile”ボタンを使う
設計目標
- プロジェクトの第一の目標は単純さである
- OBJファイルから始め、すべての処理をアプリ内で行う
- Blender exportのような別個の前処理ステップを必要としない
loadObjFile()にブレークポイントを置き、最初のフレームが終わるまで追えるような流れを目指している
- 第二の目標は実験性である
- RustやVulkanではなくWebページとして作ったため、リンクをクリックし、チェックボックスやスライダーを変えながら性能変化を見られる
- 設定を1つ変えたときにFPSが大きく落ちるといった形で、どのオプションが重要かを自分で確認できる
- 多くのコードは最適化可能だが、simplificationの問題が解決するまでは、その最適化は重要ではないと考えている
UE5 Naniteとの違い
- error metricには単純なprojected simplification errorを使用する
- meshlet simplificationは単純な方式である
- two-pass occlusion cullingがない
- 追加は難しくないが、デバッグが煩雑でGUI設定との相互作用も生じる
- コードの可読性を損なうため、目標に合わない
- visibility bufferがない
atomic<u64>の制限のため実装できない
- built-in shadowsとmultiviewがない
- シェーダー内のwork queueがない
- meshlet cullingとLOD selectionには、meshletごとにthreadをdispatchする
- 未使用LODのVRAM evictionとstreamingがない
- compressionがない
- 多数の異なるオブジェクトには拡張性が低い
- デモケースでは予測可能なメモリ使用量に焦点を当てている
- stage間のデータを格納するbufferのために、drawn meshletsの上限を事前に把握しておく必要がある
- instance用のBVHや階層実装がない
- すべてのinstanceを対象にfrustumおよびocclusion cullingを行う
三角形数が意味する条件
- 「数十億triangles」という数字だけでは性能を判断しにくく、性能はさまざまな要因に左右される
-
Dense meshes
- 近くにdense meshが多いと性能に悪影響を与える可能性がある
- ただし、画面を大きく覆うほど近ければocclusion cullingが機能する
- dense geometryはmeshletが小さく、画面上で占める面積が小さいため、occlusion/cone cullingされやすい
-
Instance count
- 各instanceは
mat4x3transform matrixを持ち、VRAMを消費する - フレーム中にはレンダリングする項目のリストも保存しなければならない
- 最悪の場合、各instanceが最もdenseなmeshletsをレンダリングする
- 実装上は
instanceCount * bottomLevelMeshletsCount * sizeof(vec2u)bytesを割り当てる - ChromeのWebGPU storage bufferの基本上限は128MBであり、デモシーンはこれを反映して調整されている
- 各instanceは
-
Scene arrangement
- アプリのシーンではオブジェクトを正方形状に配置する
- 遠いオブジェクトは一部しか見えないが、coarse meshlet LODがより広い領域を含むためoverdrawが発生する
- dense grid placementは近くの三角形をより多くレンダリングするが、深度差が小さいためocclusion cullingには有利である
- 離れたオブジェクトでは、任意の遠方ピクセルがdepth pyramidを汚染する可能性がある
Jinxシーンが難しい理由
- Jinxキャラクターは細身で、腕と胴体の間に空間があるためocclusion cullingが難しい
- モデルがうまく単純化されず、最もcoarseなLODでも3k trianglesを持つ
- 1ピクセルサイズの多数の三角形が蓄積する状況が生じる
- ソフトウェアラスタライザが大きく役立つが、シーン配置のため大半のinstanceはimpostorとしてレンダリングされる
- 近距離領域ではhardware rasterizerが担当し、hardware rasterizer・software rasterizer・impostorはそれぞれ異なる強みを持つ
Nanite実装で驚く点
- DAGの目標は、単に遠いオブジェクトに少ない三角形を使うことではなく、画面全体で1 pixel == 1 triangleに近い一貫性を維持することである
- discrete LOD meshは各LODが別個のgeometryであるため、中間段階が必要なときに対応しにくく、continuous LODが必要になる
- meshlet hierarchyは、選択した詳細度でgeometryをsamplingできるようにする
- Nanite自体よりも、cullingとmeshlet作業に多くの時間がかかる
- meshlet LOD hierarchyは動作させるのは簡単だが、効率的にするのは難しい
- meshがきれいに単純化されないと、Jinxのように単一ピクセルを覆う約3,000 trianglesのような状況が生じる
- pixel-sized trianglesを狙うならソフトウェアラスタライザが必要で、billboard impostorも安定性重視のfallbackとして有用である
Mesh simplification問題
- 単純化は「meshを受け取ってtrianglesをX%に減らす」問題ではなく、meshletとMETISの文脈の中で行われる問題である
- UE5は独自のmesh simplification codeをasset pipelineの最初の段階で使用している
- ここで削減したコストは、その後のシステム全体に連鎖的な利益をもたらす
- Brian KarisのSIGGRAPH資料では、LOD graphが単一のroot clusterで終わり、どのモデルも128 trianglesまで単純化できると述べている
- Jinxモデルでは、単純化がある時点で止まってしまう問題があった
- 入力にX trianglesを与えても、同じX trianglesが返ってくる状況が起きた
- アルゴリズムがassertionでcrashした
- その後、十分に減らないmesh部分については、DAGに複数のrootを許可するように変更した
- bunnyモデルは当初、単一の128 tris meshletに単純化されたが、条件を変えると単一rootにならない問題が明らかになった
- 多くのmeshletが十分にtriangle数を減らせず、128 triangles未満の「fullではない」meshletも多かった
- 関連議論:
Error metric
- 20,000,000トライアングルのメッシュをmeshlet hierarchyでレンダリングする際、どのmeshletが「正しい」meshletなのかを選択する必要がある
- この選択問題こそがNaniteの中核であり、simplification、meshlet DAG、software rasterizerはこれを扱うための前提条件に近い
- 実装ではmeshoptimizerが提供するprojected simplification errorを使用している
- 筆者はこのmetricはNaniteに適したmetricではなく、他のvertex attributeも関数に含めるべきだと見ている
- attributeごとにweightを与えられる必要がある
- Jinxの顔のnormalが大きな問題だった
- 参考にした資料:
ソフトウェアラスタライザがテクスチャを出力しない理由
- hardware rasterizerではdepth testの結果に応じて、depth texture、color、normalをまとめて書き込める
- ソフトウェアラスタライザで複数のthreadが同じピクセルに書き込むとrace conditionが発生する
- 一般的な解決策はvisibility bufferである
- 各pixelに最も近いtriangleの
sceneUniqueTriangleIdを出力する - これを32-bit depthと組み合わせて64-bit値にし、64-bit atomic operationを使う
- 別passでtriangleを再度rasterizeし、barycentric coordinatesを計算してshadingする
- 各pixelに最も近いtriangleの
- WebGPUには64-bit atomicsがないため、この方式は使えない
- このプロジェクトの目標はsoftware rasterizeが動作することを示すことであり、そのため背景のsoftware rasterizeモデルは白色で、reasonable shadingのみを提供する
独自Nanite実装に対する判断
- 最も単純な答えはUE5を使うことだ
- 既存エンジンにこの技術を組み込むには、まずcomputeベースのgraphics pipelineとGPU-driven rendering pipelineを実装する段階が必要だと見ている
- multi-step culling、scene/world chunk management、animated meshesのような問題が先に現れる
- これらの段階が安定した後でsoftware rasterizerを試し、その次にNanite-like techを追加する順序が示されている
- toy rendererで基本的なmeshlet hierarchyを作るのは週末プロジェクトになり得るが、実際の実装ではsimplificationとerror metricの問題を扱わなければならない
使用されたツールと参考資料
- meshoptimizerがプロジェクト実装の中核的な基盤となっている
- newer meshoptimizer versionsにはNanite clone向けの
meshopt_SimplifySparseがある - プロジェクトは開発中にテストした状態を維持するため、そのバージョンには更新していない
- newer meshoptimizer versionsにはNanite clone向けの
- METISを使用している
- EmscriptenはmeshoptimizerとMETISをブラウザで実行するために使われている
- JinxモデルにはSketchfabのArcane - Jinxを使用し、テクスチャの結合、UV調整、武器の除去を行っている
1件のコメント
Hacker News のコメント
おお、いいですね。Nanite 再生のサードパーティ実装が出てきたわけです
Nanite はグラフィックスメッシュを非常に賢く表現します。木ではなく有向非巡回グラフなので、繰り返しはコピーではなくリンクになり、メッシュがサブメッシュを共有し、そのサブメッシュもさらに共有できます。メッシュ内部の詳細度(LOD)にも対応していて、サブメッシュが十分小さくなると省かれます
そのため、有限のデータと高速なレンダリング時間で、非常に大きな反復コンテンツを扱えます。画面にはピクセル数の上限があるのだから、実際に必要なレンダリング作業量にも上限がある、という洞察です
Epic の人が説明している SIGGRAPH の動画が非常に良いです。現在の GPU 設計は Nanite とあまり相性がよくないため、より多くの処理を GPU で行うには新しいハードウェア演算が必要です。Nvidia が AI 市場に気を取られている状況で、そうした変化が起きるかは疑問です
この方式が効果を発揮するには、シーンにインスタンシングが多く必要です。Unreal Engine デモに出てくる同じ彫像のホールのように、限られた数のオブジェクトを再利用して広いコンテンツを作る AAA ゲームにはよく合います。Cyberpunk 2077 の映像を見ながら手すりやゴミの山を探すと、同じものがまったく異なる文脈で何度も繰り返されているのが分かります
Nanite メッシュの作成は内部リンクのオフセットが多く複雑で、これまでは Unreal Engine エディタだけが処理していました。再生側がオープンソースになったので、誰かが作成ツールも作る可能性は高いでしょう
ただし、フォーマット内の内部オフセットは攻撃対象領域になり得て、悪意を持って作られた Microsoft Word の .doc ファイルのように、細工されたコンテンツで悪用される可能性がありそうです
DAG は頂点データをもとに動的に作られるもので、アーティストがサブメッシュをどう構成したかとは関係ありません。「繰り返しがリンクになる」という話でもありません
また、オブジェクトインスタンスごとにグラフカットの結果が異なるため、従来のインスタンシングは使えません。なぜシーンにインスタンシングが多いほど効果が出ると考えているのか分かりません
その時、Nanite に似たものをかなり進めていて、圧縮メッシュと呼んでいました。典型的な、見当違いのエンジニアリング上のうぬぼれでした
初期の作業は有望に見えましたが、問題を掘り下げるほど全体がどんどん複雑になっていきました。アセット生成パイプライン全体を作らなければならず、見栄えの良い結果を出しつつメモリを破綻させない水準で、期限内に仕上げるには大きすぎました
大規模なメッシュを段階的な詳細度でレンダリングしているように見えるところまでは作れましたが、非常に遅く、GPU を酷使してサブピクセル精度を合わせないとひどく見えました。面白い実験ではありましたが、ハードウェアにも荷が重く、一人で担うには課題が大きすぎました
Epic が Nanite を公開したときは本当に驚きました。私ができなかったことを、私が想像した最善よりもはるかに優れた形で成し遂げていたのです。登場した時に誇張ではなく本物の解決策だった技術の一つであり、現代グラフィックス世界の技術的な宝石です。Epic が上場企業だったなら、Nanite 技術だけを見てもかなり大きな金額を投じる気になったと思います
短い高レベルの紹介動画も良かったです。「What is virtualized micropolygon geometry? An explainer on Nanite」です: https://www.youtube.com/watch?v=-50MJf7hyOw
この機能の作者の一人が書いた技術的な深掘り記事もあります: https://jms55.github.io/posts/2024-06-09-virtual-geometry-be...
私が理解した Nanite の考え方は、メッシュ精度をおおむね 1 ピクセル精度に合わせて維持するというものです。たとえば透視投影後の誤差が半ピクセル程度に収まるなら、座標を 10 ビット程度に丸めた低ディテールのメッシュを使えます
量子化は二つの役割を果たしていたと、ぼんやり覚えています。データ保存サイズを減らすだけでなく、頂点を空間上の同じ位置にスナップして詳細度生成にも役立ち、その後で重複を除去できます
このスレッドで何度か触れられていますが、Bevy にも Nanite のアイデアの実装があります。仮想ジオメトリと呼ばれることもあります。私がその作者なので、質問に答えられます :)
このプロジェクトについては Scthe が本当にうまくやりました。作業過程のさまざまな部分について一緒に話し合い、その経験をもとに Bevy のコードも改善されました: https://github.com/bevyengine/bevy/pull/15023
この分野により多くの人が参入するのは、いつでも歓迎です。Nanite には本当に素晴らしいアイデアがたくさんあります
デモが互換性判定にユーザーエージェント文字列を使っているのでしょうか? それはよくありません。機能の互換性は、特定の機能を直接検出するか、実際に使ってみながらケースごとに判断すべきです
私は Chrome ではなく Chromium を使っていて、WebGPU も常用していますが、デモは Chrome を使えと言ってきます。倫理的にそれはできません。ぜひデモを試してみたいのですが、かなり労力のかかったプロジェクトに見えます
ブラウザー向けコンポーネントフレームワークを自作していたときは、検出がしばしば不可能で、ブラウザースニッフィングに頼らざるを得ませんでした。Modernizr のコードには、機能検出のための非常に巧妙なハック、ときにはかなり汚いハックがたくさんあります。信頼できる検出コードを作るために膨大な作業が注ぎ込まれていて、検出もたいてい副作用を通じて行われていました
私の推測では、Web3D の機能検出は単純ではありません。ざっと検索しても、明確な Web3D 機能検出ライブラリは見当たりませんでした
Modernizr の
:checkedサポート検出コードの一部はこんな感じです:Modernizr.addTest('checked', function(){
return Modernizr.testStyles('#modernizr input {width:100px} #modernizr :checked {width:200px;display:block}', function(elem, rule){
サイト設定でそのサイトの WebGL 権限を有効にしましたか? 私の場合、デフォルトではオフになっていた気がします
WebGPU error [frame][validation]: Fill size (7160950) is not a multiple of 4 bytes.
Nanite の基になった動的メッシュ簡略化アルゴリズムを発明した Federico Ponchio の、やや埋もれた 2009 年の博士論文です。図も多いです
https://vcg.isti.cnr.it/~ponchio/download/ponchio_phd.pdf 107 ページです
ある程度動いているのは素晴らしいですが、WebGPU に64ビットアトミック演算がないため、かなり粗い妥協をせざるを得ませんでした
いつかオプション拡張として追加されるといいですね。少なくともデスクトップ級ハードウェアでは、ハードウェアサポートはほぼ普遍的です。AMD と Nvidia にはかなり前からあり、Apple は M3 からサポートしました
iPhone 12 Pro Max には WebGPU がないと表示されますが、実験的機能で有効にすると別の WebGPU デモサイト[1]は動作します。iPhone でこれを成功させた人はいますか? Web アプリが何に失敗したのか、もっと詳しく教えてくれるとよいのですが
[1] https://webgpu.github.io/webgpu-samples/?sample=texturedCube
WebGPU error [init][validation]: 6 errors generated while compiling the shader: 50:22: unresolved call target 'pack4x8snorm' 50:9: cannot bitcast from 'â¥' to 'f32' 54:10: unresolved call target 'unpack4x8snorm' 59:22: unresolved call target 'pack4x8unorm' 59:9: cannot bitcast from 'â¥' to 'f32' 63:9: unresolved call target 'unpack4x8unorm'
名前と説明が非常に紛らわしく、商標権侵害の可能性もありそうです。主張とは異なり、UE5 の実際の Nanite とは完全に無関係で、UE5 と関係のない人が似たものを実装したように見えます
似た機能を提供する Bevy の Virtual Geometry もあります。Rust で書かれていてゲームエンジンに統合されているため、はるかに有用である可能性が高いです: https://jms55.github.io/posts/2024-06-09-virtual-geometry-be...
私が「OpenAI の GPT-3 を JS で実装した」と言ったら、ホワイトペーパーのアーキテクチャを持ってきて再実装したという意味だと受け取るでしょう
これは実際にWebGPU 上で動く UE5 Naniteではありません。Nanite と同じアイデアを独立に実装したものです
この手法はいろいろなところで現れ始めています。Nanite がそのアイデアを有名にしたのは確かですが、Nanite は手法の名前ではなく、特定の実装の名前です
ほかのエンジンが、LOD や類似システムでどの程度なのか気になります
Godot には自動 LOD 機能があり、それ自体はかなり良さそうです: https://docs.godotengine.org/en/stable/tutorials/3d/mesh_lod...
Unity にも LOD システムはありますが、エンジンの人気の割には、LOD モデルを手作業で作る必要があります: https://docs.unity3d.com/Manual/LevelOfDetail.html アセットストアでプラグインを探さない限りは。
あまり知られていない NeoAxis というエンジンでも、興味深いアプローチを見ました: https://www.neoaxis.com/docs/html/NeoAxis_Levels.htm ただし、私の以前の RX580 では性能が非常に悪く、今の A580 ではまだ試していません
私の見るところ、画面上に大量のものを表示する能力では、Unreal は競合よりかなり先を行っています。欠点は、アーティストがより高品質なアセットをゲームに入れたくなり、インストール容量が大きく膨らみかねないことです
性能コストも大きいです。Nanite や仮想ジオメトリは、現代の GPU とあまり相性がよくありません。記憶では、最大フィルレートは 1/4 程度だったと思います。GPU ラスタライズはシェーダーのようにピクセル単位ではなく、2x2 のクアッド単位で動作するためです
このように反復的な静的ジオメトリが多いレンダリングシーンを見るたびに、昔のベイパーウェア動画で「無限のディテール」を吹聴していた、あのいら立たしい人の声を思い出します
Nanite は、その古い何かとは違って、本当にその問題を解決したように見えます。そちらはオクツリーのようなものを使うという話があったと記憶しています
ネット上の噂ではオクツリーを使っているという話がありましたが、その後の Euclidean の動画ではそれをきっぱり否定していました