プログラミング
ブロックゲームにおけるソフトウェアオクルージョンカリング
Software occlusion culling in Block Game (enikofox.com)
要約
統合GPUを持つ低スペックPCでも快適に動作するブロックゲーム開発のため、ソフトウェアレンダリングによるオクルージョンカリング手法を開発しました。CPU側で低解像度のデプスバッファをレンダリングし、チャンク内の可視性を判定することで、特に地下や屋内での描画負荷を大幅に削減することに成功しました。
全文翻訳
私のGPUは、AMD Ryzen 7 5700G CPUに付属する統合Radeon Vega 8です。これは、私のワークステーションがグラフィカルなコンピューティングの強力なマシンではないことを知っておいてほしいからです。実際、かなり弱いです。
私の統合GPUは、私が2012年に購入した低スペックハードウェアターゲットのラップトップのGPUよりもUserBenchmarkで48%高速であることが示されています。(余談ですが、UserBenchmarkの精度に関する非難は認識していますが、それほど深刻ではありません。最近手に入れたiGPUが、当時それほど良くないと見なされていた14年前のラップトップGPUよりも有利に比較されないのは面白いと思います。)
それに加えて、私のゲームがポテトのようなマシンでもうまく動作してほしいという理由から、最近、開発中のブロックゲーム(仮称)のためにソフトウェアレンダリングのオクルージョンカリングソリューションを試してみることにしました。なぜなら、私は常にそのアイデアに興味があったからです。ブロックとチャンクは軸配置の立方体であり、物事を容易にします。ブロックゲームは、地下の洞窟の形で隠されたジオメトリを大量に持つ傾向があります。
これらをカリングする他の方法もありますが、アルゴリズムはかなり複雑になりがちで、この方法はその複雑さを回避し、非常に概念的にシンプルなものに留まる良い方法のように思えました。
この記事では、開発プロセスと最終的にたどり着いたソリューションについて説明します。もしよろしければ、MastodonとBlueskyに投稿した開発スレッドを読むこともできます。
始める前に、これが予想以上にうまく機能したことを言っておきたいと思います。スレッド化されているため、60 FPS以下で半フレームで動作し、一般的にフラスタムカリングを生き残ったチャンクの少なくとも50%をカリングします。
地上では、地平線に向かってまっすぐ見ると約50〜60%のチャンクがカリングされますが、屋内や洞窟の地下では95%以上のチャンクをカリングでき、私の弱いシステムでも400 FPS以上のフレームレートが得られます。全体として大成功ですが、最後に触れるいくつかのケースで破綻します。
デプスオクルージョンカリングの比較(オン/オフ)、左がオフ、右がオン。
デプスベースのオクルージョンカリング
他の人がデプスベースのオクルージョンカリングの概念を私よりも上手に説明していますが、簡単に説明します。
シーンのデプスバッファを取得し、カリング可能な各オブジェクトについて、そのデプスバッファをチェックして、そのピクセルが表示されるかどうかを確認します。表示される場合は、表示されます。表示されない場合は、カリングされます。
これには多くのことができます。比較的高い解像度でレンダリングし、コンサバティブにバッファをダウンサンプリング(常に最も遠い距離を使用)することができます。これは階層的Zバッファと呼ばれます。
最新のテクノロジーを使用すると、GPUの助けを借りてこれを行うことができます。モーションベクトルを使用して、前のフレームのバッファを再利用できます。
非同期リードバックを使用して、GPUをブロックせずにCPUで分析するために実際のデプスバッファを取得できます。
最終的なシステムを最初に説明し、そのシステムに実際にたどり着いたプロセスを順を追って説明します。
Block Gameのオクルージョンカリング
私がやったことははるかにシンプルで、上記のような派手なものは何も含まれていません。
FNAを使用してBlock Gameを作成しているため、古いテクノロジーに縛られており、GPUの機能を利用できません。
GPUからリードバックして実際のデプスバッファを取得することはできますが、非同期ではないため、必然的にGPUのストールが発生し、それは良くありません。
そのため、CPUでレンダリングしています。
256x128ピクセルの解像度でレンダリングします。ブロックゲームではすべてが立方体であるため、デプスバッファ(単純なfloatの配列)に立方体をレンダリングし、次にバッファに対して他の立方体をチェックします。
チャンクが変更によって再構築されると、オクルーディングサブチャンクの「ミップマップ」チェーンのようなものを構築します。
私のチャンクは16x16x16のサイズなので、5つのレベルがあります。
これは、各8x8x8ブロックサイズの2x2x2立方体のチャンク全体です。
各4x4x4ブロックサイズの4x4x4立方体です。
各2x2x2ブロックサイズの8x8x8立方体です。
各1ブロックに適合する16x16x16立方体です。
レベル4から処理を開始し、完全に不透明なブロックで表示面を持つものをオクルーダーとしてマークします。
次にレベルを下げていき、表示面を持ち、完全に不透明なブロックで構成されている立方体をオクルーディングサブチャンクとしてマークします。
レベル1〜4のサブチャンク、それぞれ単独でレンダリング。
同じチャンク内のサブチャンクは同様の色をしています。
フレームのカメラ位置が更新されると、ゲームはレンダラーにオクルージョンカリングを開始するように指示します。
これにより、プレイヤーの周りのチャンクが集められ、表示面やエンティティを含まないものは破棄され、残りはワールドスペースでフラスタムカリングされます。
次に、残りのチャンクをオクルージョンカラーに送信し、すべてのオクルーダーを収集します。
最も高いレベルである個々のブロックのオクルーダーは、カメラから20ブロックの半径内のオクルーダーで構成されます。
次に、次のレベル1〜3は、カメラがいるチャンクからの距離に基づいて収集されます。
8x8x8ブロックサイズのレベル1オクルーダーはたくさんありますが、16x16x16オクルーダーは決して見つかりませんが、理論的にはそれらを追加することもできます。
誤ったオクルージョンを防ぐために、各オクルーダーの立方体を水平および垂直方向に1ピクセル縮小します。これにより、十分な距離で消滅し、合理的に見える距離よりも遠くの立方体をレンダリングする意味がなくなります。
このようにオクルーダーをスキップすることで、ワークロードが大幅に削減されます。
オクルージョンカラーは、候補となる各チャンクの位置、サイズ、チャンクインデックスをリストに格納します。
このすべてのデータが収集されると、オクルージョンカラーは実際のチャンクのデータに触れる必要がなくなるため、スレッドセーフが達成され、バックグラウンドスレッドにウェイクアップして実際の作業を実行するようにシグナルが送られます。
バックグラウンドスレッドでは、まずすべてのオクルーダーがフラスタムカリングされます。なぜなら、以前のフラスタムカリングはチャンク全体にのみ適用され、サブチャンクには適用されず、最大8つの頂点変換はフラスタムカリングに必要なドット積よりもはるかに高価だからです。
次に、残りのオクルーダーのコーナーは、デプスバッファのx/y座標と深度に変換されます。
完全な4x4プロジェクションマトリックス乗算のオーバーヘッドを回避するために、ビューマトリックスを使用して回転し、加算で並進し、バッファが通常の32ビットfloatで構成されているため線形ビュー深度を使用します。
これは操作が少なく、数千もの頂点を扱う場合に大幅に高速です。
オクルーダーの深度値は、計算されたすべての線形深度値の最大値になります。
変換されたコーナーのいずれかがカメラの前方平面の後ろにあるために無効な場合、オクルーダーは完全にスキップされますが、そうでない場合は、その「形状」がトレースされます。
三角形や面をレンダリングする代わりに、デプスバッファの各行の最小および最大x座標を保持する1D配列を使用します。
形状のトレースは、8つの立方体コーナーすべての最小および最大y座標を見つけ、すべての12の立方体エッジをステップ実行し、各行の最小および最大x値を記録することを意味します。
オクルーダーの描画は、最小y(+1)から最大(-1)までの各行をステップ実行し、1D配列から最小および最大x(+1および-1も)を検索し、オクルーダーの最も遠い深度値でデプスバッファを水平に埋めることで、簡単になります。
このプラスとマイナスの1は、デプスを非常に低い解像度でレンダリングしているためです。
立方体を1ピクセル縮小することで、この低解像度ではチャンクがまったく見えなくなるエッジケースを回避できますが、フル解像度では見えるようになります。
シーンにギャップが見えますが、オクルージョンカラーの低解像度では1ピクセルのインセットなしではギャップが完全に覆われ、誤ったオクルージョンを引き起こします。
オクルージョンカラーは、オクルージョン候補すべてに対して同様のことを行いますが、今回は最も遠い距離ではなく最も近い距離を記録し、1ピクセルのインセットはなくなります。
深度が、いずれかの