
2026/04/19 16:54
# 現代的なレンダリングのカルリング技術 ## 概要 現代のレンダリングパイプラインは、カメラ視点から非表示であるオブジェクトを除外することによりパフォーマンスを最適化するため、多種多様なカルリング技術を採用しています。これらはラスタライズおよび幾何学処理における計算負荷を低減し、高いフレームレートでのリアルタイムグラフィックスの実現を可能にします。 ## 代表的なカルリング手法 * **フラスラムカルリング**:カメラのビューフラスラム(視野角を表す円錐体状の体積)内に存在する幾何学的プリミティブを決定します。この領域外にあるオブジェクトは、レンダリング実行前に破棄されます。 * コンベックスヒルとフラスラム平面との間の交差テストを実装しています。 * ビデオゲームや VR/AR などのリアルタイムアプリケーションで広く利用されています。 * **奥行きカルリング(オクルージョンカルリング)**:ビューフラスラム内の不透明な表面によって隠されているオブジェクトを特定し、完全に遮断された幾何学に対する不要なラスタライズを回避します。 * 深度バッファまたは事前計算された可視性情報のデータ構造(例えばオクルージョンクエリ)を活用しています。 * 複雑な幾何学を持つ静的シーンにおいて高いフレームレートを維持するための重要な技術です。 ## 最適化戦略 * **階層的カルリング**:シーン内のオブジェクトを階層構造(例:バウンディングボリュームヒエラルキー)に整理し、まず外側の境界をチェックすることで迅速なカルリングを行うことを可能にします。 * 詳細な交差テストが必要となる回数を大幅に削減します。 * **レベル-of-Detail (LOD)**:カルリングを動的解像度または距離に基づく詳細度の調整と組み合わせ、遠方のエンティティーに対してポリゴン数を削減する手法です。 ## まとめ グラフィカルアプリケーションにおいて最適パフォーマンスを実現するためには、現代的なレンダリングのカルリング技術を効果的に実装することが不可欠です。開発者はフラスラムカルリング、オクルージョンカルリング、および階層的カルリングを戦略的に組み合わせることで、描画呼び出し回数を最小化し、GPU の負荷を抑えつつも視覚的な忠実度を維持することができます。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
グラフィックの最適化は、単一の「聖杯」が存在しない恒久的な課題であり、代わりに効果的なレンダリングには複数の可視性テストの層を重ねて不要な作業を回避すること reliance します。その原理がおよそ 80% の最適化で占められます。基本的な技術には、Distance Culling(遠いオブジェクトを無視)と Backface Culling(非表示面をスキップ)が含まれ、Frustum Culling はオブジェクトがカメラの視野内に含まれているかをチェックしますが、メッシュレットベースのアプローチによる解決がない限り、大規模な画面外オブジェクトでは対応が困難です。高度な戦略はこのスタックをさらに精細化します:Occlusion Culling(ハードウェアクエリ、MSOC などのソフトウェアシミュレーション、または Hi-Z ブファーを使用)、Screen Size Culling、および静的シーネンや屋内環境向けの専用手法(PVS、Portal Culling)。現代的な GPU に主導されるレンダリングでは Indirect Drawing や Meshlet/Cluster Culling が活用され、Unreal Engine 5 の Nanite は GPU の階層構造を利用して手動の LOD チェーンを排除します。技術はまた、タイルまたはクラスター化アプローチを通じてライティングとシャドウにも拡張されます。重要な経験則は、正しさ(オブジェクトが消えてしまうような偽陰性を回避)とパフォーマンス(無駄な作業を積極的に除去)のバランスを取ることです。将来の開発では、高価なクラスターベースシステムの使用と安価なオブジェクトレベルチェックの使用時点をコンテンツ正当化によって決定し、オープンワールド、狭い廊下、車両など多様なゲーム環境でも滑らかなパフォーマンスを保証することに注力します。
本文
サントス・ロー:ザ・サード リマスター - 私が担当した最初の shipped タイトルです。スティールポートは密度の高いオープンワールド都市ですが、本作には細い室内の廊下やジェット機、自動車、パラシュート展開などのシーケンスも含まれています。これらすべてに対して正しく culling(描画排除)を実現することは、かなりの挑戦となりました。Timur Gagiev 氏へ敬意を表します。絶対的な伝説です!
序論 AI コーディングの現代において、「AI ゲーム生成」、DLSS 5、Unreal Engine 5、そして驚異的なガウシアンスプラットデモのように、グラフィックとゲームは「解決済み」の問題だと考えられる傾向があります。「ただ AI を使い込んで数日でゲームを作り上げればよい」といった議論が飛び交っていますが、それは明らかに誤りです。堅固なエンジニアリング作業、知識、トレードオフ(見返りのバランス)、そしてアートディレクションは色あせることがありません。あなたのゲームが 2D でも 3D でも、リアル系でもカートゥーン系でも、閉鎖された火星の基地に設定されていてもゾンビ蔓延するオープンワールドのニューヨークに設定されていても、必ず最適化が必要です。これまでにも将来も最も重要な最適化技術の一つである culling は、その代表格です。
朗報は、私のキャリア全体で見たほとんどすべての最適化(ほぼ 80%)は、「必要なこと以外、あえて愚かな作業を行う必要はない」という一点に集約されることです。 悪報は、culling を実装する際にも、シーン構造、ゲームデザイン、アートディレクション、ハードウェアの限界、そしてパフォーマンス budgets(予算)をバランスさせながら対応する必要がありますということです。
そのため、本稿では現代の実時間レンダラーで使用される主な culling テクニックについて解説します。これらの技術は互いにどのように関連しているかを理解しやすいよう、カテゴリー別にグループ化しました。これらの技術のほとんどは、その単体記事に値するほど重要ですが、いかなる技術についても「悪魔は小細工にある」といわれるように、常に詳細部分での工夫が必要となります。
1. 基礎編:距離、背面、視野(Frustum)
これらは最もコストが低く、普遍的に適用される技術です。より高価な処理を行う前に、明らかなケースをキャッチします。
Distance Culling(距離に基づく排除) これが最も簡単な形式です。カメラからの距離が最大距離を超えているオブジェクトは描画をスキップします。それで終わりです。 これは極めて高速で、視覚的な影響が最小限に抑えられそうな小さな小道具の場合などに特に有効です。多くのエンジンではメッシュ単位やマテリアル単位で cull distance を設定できます。 ただし注意すべき点は、可见な「ポップイン」(突然の出現)を防ぐことです。一般的な対策としては、ディザ Fade-Out(段階的なフェードアウト)、cull ポイント以前にアグレッシブな LOD(レベル・オブ・DETAIL)を使用する、あるいは遠方で実際のメッシュを代替するインプストーラー(看板のようなテクスチャ表示)を使うなどの方法があります。 下の「画面サイズに基づく Culling」セクションでも詳しく扱いますが、ここで注目すべき点は以下の通りです:オブジェクトが画面に投影される領域が僅かである場合、描画コストに見合う価値がないことが多いです。距離だけではこれをクリーンにキャッチできませんので、スクリーンスペースでのサイズチェックも併用する必要があります。 (図の誇張された例)家や小さな岩のようなオブジェクトで、小さな岩が遠くへ行って消えるのはあまり気にならない一方、大きなオブジェクトが出たり入ったりするポップインは目立つため避けたいものです。
Backface Culling(背面排除) グラフィックス API を扱う際、まず最初に遭遇する culling 技術の一つです。パイプライン状態オブジェクト(PSO)の一部として構成され、有効にするのが最も容易なメリットの一つだからです。 すべての三角形は「正面」と「背面」を持ちます。閉じたメッシュの場合、背面はオブジェクトの内側にあり見えないため、GPU は巻き付け順序(winding order)に基づいて自動的にこれをスキップでき、一般的な幾何形状に対してレンダリングとフラグメント処理の約半分を節約できます。
Triangle Winding Order 二十面体を回転させた例で、ビューヤー向きの三角形がレンダリングされ、それ以外はスキップされている様子が示されています。 知っておくべきことの一つは、従来の顶点+フラグメントパイプラインでは、背面排除は頂点シェーダーによって処理されたの後に実行されるということです。そのため、頂点処理そのものを節約することはできず、レンダリングとフラグメント処理のみが削減されます。GPU 主導のパイプラインでは、この判断をさらに早期にシフト可能で、例えばメッシュレット(小さな三角形群)をレンダリング前に cull する compute シェーダーやタスク/アンプリフィケーションシェーダーで実装できます。 これはほぼ無料で実現できる技術ですが、透明度、両面材、それらを積極的に活用する一部のアルゴリズムとの相互作用を理解しておく価値はあります。
Frustum Culling(視野排除) Top-down view: フラスツム(視野錐体)の外側にあるオブジェクトは cull され、レンダリングパイプラインに送信されることはありません。 パースペクティブカメラの場合、ビュー・フラustum はカメラが見える領域を表す切られたピラミッド型のボリュームです。それ以外のものは描画する必要がありません。フラustum Culling では、オブジェクトを通常は球体や AABB(軸対称の矩形)などのボリウムとして捉え、これらをフラスタムの 6 つの平面に対してテストし、交差しないものはスキップします。 これは culling パイプラインにおけるほぼ最初のパスであり、距離ベースの culling の次に行われることが多いです。高速でコストが安く、特にマップの一部がカメラの背後や横に位置するオープンワールドでは、シーンの膨大な部分を一度の操作で削減できます。Horizon Zero Dawn のフラustum Culling もこの仕組みに基づいています。 上記の GIF で確認できるように、山脈のような大きなオブジェクトでも、ほぼフラスタムの外側にある場合でも描画されています。これはオブジェクトレベル culling の核心的なトレードオフです:多数の小オブジェクトは細かい粒度での culling 機会を提供しますが、各オブジェクトは Draw Call と CPU 側の可視性テストのコストを伴います。一方、少数の大オブジェクトは Draw Call コストが安価ですが、その 90% が画面外にある場合でも全体を描画し続けなければならず、頂点シェーダーコストもすべて負担することになります。レンダライザーは頂点シェーディング後にクリッピングを行うため、頂点シェーダーの時点でクリップできません。この画面外のジオメトリに対する無駄な頂点処理こそが、4 節のメッシュレット・カリングが解決しようとする問題そのものです。
2. オクルージョン・カリング(Occlusion Culling)
オクルージョン・カリングは、「何が他のものによって隠されているか」を教えてくれます。難易度は高いですが、都市や室内のような密度の高いシーンでは最も大きな効果をもたらすことが多いです。 これはオクルージョン・カリングのみを示しています。右側にある家の背後にあるボックスが消えているのが確認できるでしょう。角を回って覗くと、一部が再び現れます。
Hardware Occlusion Queries(ハードウェアベースのオクルージョンクエリ) 主要なグラフィックス API のすべては、オクルージョン・クエリ様の機能を提供しています。Direct3D 12 では Query Heap、Vulkan では Occlusion Queries、Metal では Visibility Result Buffers が利用可能です。考え方は同じです:プロキシジオメトリ(通常はオブジェクトの境界)を描画し、サンプルが Depth Test を通過したか数をカウントします。0 の可視サンプルがある場合、そのオブジェクトのプロキシは完全に隠れていることを意味するため、実際のオブジェクトを描画から除外できます。 DX12 では D3D12_QUERY_TYPE_BINARY_OCCLUSION を使用し、正確なサンプル数ではなく単純に 0 または 1 を返します。これは計算コストが低く、カリング目的には十分です。
コード例(セットアップ:一度だけ)
D3D12_QUERY_HEAP_DESC desc = { D3D12_QUERY_HEAP_TYPE_OCCLUSION, objectCount }; device->CreateQueryHeap(&desc, IID_PPV_ARGS(&queryHeap)); // 毎フレーム - プロキシを描画し、クエリをラップする cmdList->BeginQuery(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, objectIndex); cmdList->DrawIndexedInstanced(...); // 境界ボックスを描画 cmdList->EndQuery(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, objectIndex); // GPU タイムライン上にある読み出しバッファへ解決(resolve) cmdList->ResolveQueryData(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, 0, objectCount, readbackBuffer, 0);
問題はレイテンシと同期です。結果は GPU が完了するまで CPU に対して表示されないため、実践的にはフレーム N の結果を読み取りながら、フレーム N+1 をレンダリングすることが一般的です。この 1 フレームのラグは通常許容範囲ですが、一時的にオクルージョン状態になったオブジェクトを再描画したり、可視化されたばかりのものをスキップしたりする可能性があります。
Software Occlusion Culling(CPU ベースのソフトウェア オクルージョン) GPU に問うのではなく、CPU で低解像度の深度バッファをレンダライズし、オブジェクトに対してテストを行います。Intel の Masked Software Occlusion Culling (MSOC) がこの分野で最も有名な実装の一つです。SIMD を利用して 8x4 ピクセルのタイルで三角形をレンダライズし、毎秒何百万もの三角形を処理できます。 利点は GPU に何かを提出する前にすべてが CPU で完了するため、読み込み遅延(readback latency)が発生しないことです。欠点は CPU コストと、フルシーンのジオメトリをレンダライズすることができないため、簡略化されたオクルーダーメッシュを別途維持する必要がありますということです。
Battlefield 3 - フォナル・レンダリングシーン Battlefield 3 - CPU ソフトウェアオクルーダーによってレンダライズされた同じシーン 結果意図的に粗く設定されています。実際に見えているものを「過度に cull(排除)」してしまっても、「スキップすべきものを残しすぎていた」ことよりも悪影響が少ないためです。
Hi-Z (Hierarchical Z-Buffer)(階層深度バッファ) Hi-Z は、画面のより大きな領域ごとに保守的な深度値を格納する各レベルを持つ、深度バッファの mip チェーンであり、「深度ピラミッド」とも呼ばれます。 オブジェクトがオクルージョンされているかをテストするには、その境界をスクリーンスペースにプロジェクションし、足跡(footprint)と概ね一致する mip レベルを選び、オブジェクトの最浅な深度とピラミッドを比較します。通常の LESS depth テストの場合、このピラミッドは各領域で最大深度値を格納することが多く、reversed-Z の場合は最小深度値です。重要な点は表現が常に「保守的(False Positive は避けつつ)」であることです。「オクルージョン」と判定されればオブジェクトを安全にスキップできます。「非オクルージョン」と判定された場合は描画を行います。優れた実装では、False Negative(見えてないものを描画してしまったり)よりも False Positive(隠れているものを誤って描画したり)を許容する方向で動作します。 これが現代の GPU 主導のオクルージョン・カリングの基礎となっています。構築とクエリは高速で、完全に GPU に存在します。
Two-Pass Occlusion Culling(2パス・オクルージョン・カリング) GPU 主導レンダラーにおける一般的なパターンの一つです:前フレームの Hi-Z を使用して、現在のフレームを描画する前にオブジェクトを cull します。 単純なバージョンは 1 パスのみを使用:前フレームの Hi-Z に対してすべてをテストし、残存ものを描画します。コストは低いが、今となって可視になったオブジェクトが誤って cull され、一フレームだけ不可視のままになるという欠点があります。 2 パスのバージョンはこの問題を解決します。 Pass 1: 前フレームで可視だったオブジェクトをテストし、残存ものを描画して新しい Hi-Z を構築します。 Pass 2: Pass 1 で cull されたすべてのオブジェクトを取得し、新しい Hi-Z に対して再テストを行います。今となって可視になったものはもう一度試されるチャンスがあり、このフレームで描画されます。 Pass 1 で使用した Hi-Z はまだ 1 フレーム古いため、追加のパスでも修正できない小さな残留誤差があります。「通常のゲームプレイ」では気づきません。問題となるのは、90 度の急なカメラ切り替えのような「ハードなカット」です。Pass 1 の可視セットが基本的に間違っており、再構築された Hi-Z も信頼できず、不良フレームが生じます。エンジンではこれを検知し、そのフレームでフル・深度プレパス(Depth Prepass)にフォールバックすることが一般的です。GPU 側のコストは常にフル・深度プレパスを行うよりも著しく低いため、多くの現代のゲームエンジンはこのアプローチを採用しています。
3. さらに高度な Culling テクニック!
Screen Size Culling(画面サイズに基づく排除) 固定されたワールドスペースの距離ではなく、投影されたスクリーンの面積に基づいて cull します。10 メートル先のオブジェクトは描画に値するかもしれませんが、2000 メートル先にある同じオブジェクトは 3 ピクセルしか投影しない場合があり、Draw Call のオーバーヘッドに見合う価値がありません。Screen Size Culling は生の距離閾値よりもより優雅に対応できます。 Unreal エンジンでは、静的メッシュの LOD 遷移において画面サイズを主要な指標として使用し、min/max draw distance を別途距離ベースのコントロールとしています。
PVS (Potentially Visible Sets - 潜在的に可視集合) 世界中の各領域に対して、その領域から他のどの領域が理論上見えているかを事前計算(プリコンピュート)します。ランタイムでは現在の領域の PVS を参照し、含まれていないものはスキップします。これはランタイムでの動作は非常に高速ですが、計算コストが高く、動的オブジェクトへの対応には不向きです。 これはプリコンピュート済みで効果的ですが、手続き的に生成されたゲーム(プロシージャル生成)では実用的でないか不可能な場合があります。Quake が PVS を有名にしました。シーンのジオメトリが静的で、ベーキング時間を許容できるような一部の室内ゲームでは今でも有用です。
Portal Culling(ポータル・カリング) 部屋やドアウェイが明確に定義された室内シーンにおいては非常に効果的です。各ドアウェイは「ポータル」として機能し、カメラのビューをポータルを通ってトレースし、可視なポータルから到達可能な部屋だけをレンダリングします。これにより、 Entire room of geometry(一室分のジオメトリ)を非常に安価に排除できます。 ビル内を舞台にした多くのファーストパーソン・ゲームで Portal Culling が登場します。フラustum Culling と相性が良く、複数のドアウェイを通る視線によって、ポータルは自然と実効的なビューコニスト(視点範囲)を縮小するからです。
4. GPU ドライブンなレンダリングと Cluster Culling
ここからが面白さのある部分です! CPU が何を描画するかを決めて各オブジェクトごとに Draw Call を発行するのではなく、culling ロジックを GPU に押し付け、GPU 自身に決定させるために間接的な Draw Calls を使用します。
Indirect Drawing(間接描画) DirectX、Vulkan、Metal はすべて間接描画をサポートしていますが、API の詳細は異なります。Draw Count や Base Vertex などの引数は CPU コードではなく GPU バッファから得られます。Compute Shader が culling を実行し、残存するオブジェクトだけをそのバッファに書き込みます。その後、1 回または少量の Indirect Draw がコンパクト化されたリストを消費するため、CPU がすべての可視オブジェクトに対してループする必要がなくなります。
Compute Shader - オブジェクト単位で 1 スレッドあたり
[numthreads(64, 1, 1)] void CullCS(uint id : SV_DispatchThreadID) { if (id >= g_objectCount) return; ObjectData obj = g_objects[id]; if (!SphereInFrustum(obj.center, obj.radius)) return; uint slot; InterlockedAdd(g_drawCount[0], 1, slot); // 原子演算によるコンパクトなスロット確保 g_drawArgs[slot] = MakeDrawArgs(obj); // デース引数を格納 } // CPU side - cull をディスパッチ、バリアを設定し、描画を実行 cmdList->Dispatch((objectCount + 63) / 64, 1, 1); cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::UAV(drawArgsBuffer)); cmdList->ExecuteIndirect(cmdSig, objectCount, drawArgsBuffer, 0, countBuffer, 0);
InterlockedAdd は残存する描画をコンパクトなリストにパッケージ化します。ExecuteIndirect は GPU が書いたカウントを受け取り、実際に culling パスを超えたものだけを処理します。
Meshlet / Cluster Culling(メッシュレット・カリング) 現代の GPU ドライブンレンダラーでは、compute シェーダーまたは mesh/task/amplification シェードステージでサブメッシュレベルでの culling が可能です。メッシュは小さな三角形のクラスタである「メッシュレット」に分割され、通常 64〜128 個の三角形ずつ構成されます。各メッシュレットには独自の境界球と、三角形の法線の範囲を表すコーン(円錐)があります。 この図の各色域は個々のメッシュレットを示しています。 GPU 上で各メッシュレットに対して独立に Frustum Culling、Occlusion Culling、Backface Culling を実行でき、オブジェクトレベルの culling よりもはるかに高精度です。大きなキャラクターメッシュには数百ものメッシュレットが含まれることがあり、ある角度からはそのうちのいくつかがしか可視になりません。 法線コーンの仕組みは特に巧妙です。メッシュレット内のすべての法線がほぼ同一方向を向いている場合、非常に安価な「コーン対ビューのテスト」だけで entire meshlet を却下できます。これは本質的にクラスタレベルでの背面排除です。
5. バーチャライズド・ジオメトリ:Nanite
Nanite(Unreal Engine 5)は上記の複数のアイデアを一つの統合システムに結合しているため、特別に分けて言及する価値があります。目標は通常手動で LOD チェーンを作成する必要がないことです。代わりに、すべてのメッシュは「クラスタ」という階層構造として格納され、細かいレベルから簡略化することで粗いレベルが生成されます。 More meshlets!!! ランタイムでは、この階層を GPU 上でトラバースします。各クラスタに対して可視性とスクリーンスペースエラーがテストされ、クラスタが画面上一定サイズ以下であればその詳細度でレンダリングされます。大きすぎる場合はシステムはさらに深いレベル(より細かいメッシュ)へ進みます。重要な点は、クラスタの選択と culling がすべて GPU で行われることで、オブジェクトごとの CPU 提出ワークを劇的に削減できることです。 もう一つの重要な要素は、Nanite が非常に小さな三角形に対して使用するソフトウェア・レンダライズパスです。三角形が極端に小さくなると、固定機能ハードウェアのレンダライザーは三角形ごとにかなりのオーバーヘッドを抱えるようになります。Nanite はこれらのケースをカスタムのソフトウェアパスで処理し、大きな三角形は引き続きハードウェアレンダライザを利用します。 結果として、数億個の三角形を含むシーンであっても、実際にレンダライズされるのは可視であり適切にサイズ調整された三角形のみであるという利点が得られます。
6. ライトとシャドウ・カリング(Light and Shadow Culling)
上記はすべてジオメトリに焦点を当てましたが、ライトやシャドウには独自の culling ストーリーがあり、密度の高いシーンではボトルネックになることが多いです。ここでは詳しく立ち入りませんが、主なアイデアのブレイズアップです。 Tiled Light Culling (Forward+)(タイル・ライトカリング):画面は 2D のタイルに分割され、各タイルにはその上にあるライトのみが記録されるため、シェーディング時にシーン全体のライトではなく小さなローカルリストを読むことができます。 Clustered Light Culling(クラスタ化されたライトカリング):視覚的なフラustum を深度軸に沿してクラスタに分割することで、タイルの概念を 3D に拡張し、特に depth complexity(深度の複雑さ)が異なるシーンでより緊密なライトリストを実現します。 Per-Light Screen and Depth Bounds(ライトごとの画面・深度バウンズ):シェーディングやシャドウ投射を行う前に、その影響範囲を画面空間の長方形、深度範囲、またはライトボリュームにクリップするため、ライトが影響できないピクセルに対して時間を費やすことを防ぎます。 Shadow Cascade and Shadow Caster Culling(シャドウケイスクエードとカスターカリング):CSM(コンスタン・シャドウマップ)の各ケイスケードは異なる深度範囲をカバーし、各ライトには独自のシャドウフラustum またはボリュームがあります。その領域外のジオメトリは、そのシャドウパス全体から完全に省略されます。
後日谈
culling は、10,000 フィートの高みから見ればシンプルに見えながら、実際にレンダラーを構築すればすぐにトレードオフの山のように見えるというトピックの一つです。正解は単一の技術であることはまずありません。実際には、これらをスタックして使用します:まず距離とフラustum Culling、次に何らか形のオクルージョン、そしてメッシュレットやライト・シャドウなどのより粒度の細かいシステムへと進み、コンテンツが追加的な複雑さを正当化する場合に採用します。 知っておくべきルールはシンプルです:正しさには保守的になり、無駄な作業には攻撃的になります。「False Negative(誤って描画しすぎ)」は単に少しだけ余分な描画を行っただけで、すぐに気がつくような致命的なバグではありません。一方、「False Positive(消えてしまったものを描画する)」はプレイヤーの前でおぼくしたものが突然姿を消してしまい、これは人们が即座に気付く種類のバグです。 ゼロからレンダラーを構築する場合、まず「つまらない」成功分野から始めます:良いボリウム、フラustum Culling、妥当な LOD、そして画面サイズの閾値です。次にプロファイリングを行いましょう。密度の高い室内や都市がフレーム時間を支配している場合はオクルージョンを追加してください。CPU 提出や頂点コストが問題であれば GPU ドライブンなアプローチへ移行します。コンテンツが異常に密集しており三角形が多い場合、クラスタベースのシステムや Nanite のようなアイデアが意味を持ち始めます。 銀の弾丸はありません。可視性テストの積み重ねであり、それぞれは削除する仕事の量よりも安く済みます。それがレンダリング・エンジニアリングの要約です。 幸運を、楽しんでください!
メッシュシェーダーにおける三角形カリング
メッシュシェーダーを使うことで、レンダライジングが始まる前に個々の三角形まで掘り下げて culling が可能になります。 アイデアはシンプルです:メッシュシェーダー内では、各エミュットする三角形に対して、いくつかの安価なテストを実行します。 いずれかのテストに失敗した三角形には「cull(排除)」フラグを付けます。メッシュシェーダーは透過primitive visibility value(HLSL 内の
SV_CullPrimitive を介して)をエクスポートできるため、レンダライザーはフラグが立った三角形全体を完全にスキップします。ピクセルシェーダーの呼出しもありません。深度書き込みも行いません。その三角形はなくなります。
このレベルで何を cull すべきか?
-
Backface Culling: ハードウェアで行いますが、より早く実行しダウンストリームのコストを回避できます。トリックは Olano と Greer の論文「Triangle Scan Conversion using 2D Homogeneous Coordinates」に記述されている 2D ホモジニアス行列式を利用することです。クリップスペースの xyw 座標から 3x3 マトリックスを作り、その符号をチェックします。ペルスペクティブ分割(perspective divide)が不要で、w がゼロに近いエッジケースを回避できます。
if (determinant(float3x3(v[0].xyw, v[1].xyw, v[2].xyw)) >= 0) return CULLED; -
Near-plane clipping(ニア面クリッピング): w < 0 の頂点の数をカウントします。すべての 3 つがカメラの背後にある場合、三角形は完全に画面外であるため cull します。一部の頂点が背後にある場合は、そのままでハードウェアのクリッパーに任せます(部分的なクリッピングを自分たちで試すのは混乱を引き起こします)。
-
Frustum culling: 3 つの頂点からスクリーンスペースの AABB を構築し、それに対してテストを行います。ボックスが画面外全体にある場合は cull します。
-
Small triangle / overlap culling(小三角形・重複排除): これは興味深い部分です。フラustum 内かつ背面ではなくても、ピクセルよりも小さいかピクセル中心の間に落ちている場合、ゼロピクセルにレンダライズされることがあります。これを検出するには、ハードウェアがどのように動作するかを正確に追跡する必要があります - 23.8 ファイブドポイントでのスナッピング(多くの GPU では 8 ビットのサブピクセルビットが標準)です。頂点をサブピクセルグリッドにスナップさせ、バウンディングボックスを作り、どのピクセル中心もその内側に収まっているかをチェックします。そうでなければ、その三角形はピクセルを描画せず、cull されます。
const uint SUBPIXEL_BITS = 8; const uint SUBPIXEL_SAMPLES = 1U << SUBPIXEL_BITS; // ... 頂点をスナップさせ、固定ポイント AABB を構築 ... // ピクセル中心が一つもない場合、cullこの最後の技術は、密集したジオメトリにおける驚くほど多くの不要なデータをキャッチします。遠くの樹木や髪、あるいは多数の三角形がサブピクセルに終わる過度に分割されたメッシュなどを想像してください。
なぜメッシュシェーダーなのか? 理論的には描画前に compute シェーダーでこれを行うことも可能ですが、残存する三角形を新しいインデックスバッファへコンパクト化する必要があり、扱いにくいです。メッシュシェーダーでは各スレッドが 1 つの三角形を処理し、テストを実行し、出力にフラグのみを付けることができます。コンパクト化は不要で、レンダライザーは直接 primitive export mask を消費します。 アンプリフィケーションシェーダーステージでのメッシュレットごとの Frustum と Hi-Z Culling と組み合わせて、オブジェクト -> メッシュレット -> 三角形という階層構造ができあがり、各レベルが次のレベルが考えるべきでないものを除去します。
補足文献
- Inside Quake: Visible Surface Determination
- Direct3D 12 query types
- Efficient Occlusion Culling
- Hardware Occlusion Queries Made Useful
- ARM Hierarchical-Z Visibility Test sample
- Masked Software Occlusion Culling
- MaskedOcclusionCulling source code
- FidelityFX Single Pass Downsampler
- Direct3D 12 ExecuteIndirect sample
- A Deep Dive into Nanite Virtualized Geometry (SIGGRAPH Advances 2021)
- Clustered Deferred and Forward Shading