1Dシャドウマッピングを使用してUnityで高速の2Dシャドウを実装する







はじめに



最近、Unityで2Dシャドウシステムの実装を開始しました。これは実際のゲームで使用できます。 プロの開発者が知っているように、技術的なデモで達成できることと、実装された機会が多くの1つに過ぎないフルゲームへの統合に適用できるものとの間には大きな違いがあります。 CPU、ビデオプロセッサ、およびメモリへの影響は、ゲーム内の他のすべてとバランスが取れている必要があります。 実際には、プロジェクトごとに異なる制限がありますが、処理時間が数ミリ秒以下で、メモリが数メガバイト以下のシステムを作成することにしました。



この制限により、見つけた影を計算するための多くの既存の方法を破棄しました。 いくつかの手法が一般的でした。 1つは、CPUに実装された光線追跡を使用し、遮光ジオメトリのシルエットの境界を定義します。 別の方法では、光に対するすべての障害物がテクスチャにレンダリングされ、その後、シャドウマップを作成するために、複数のパスでレイステッピングのようなアルゴリズムが実行されました。 これらの手法は通常、1組の光源のみで使用され、選択した制限に従って数十の光源を使用することは絶対にできません。



シャドウオーバーレイ



そのため、ほとんどの現代の3Dゲームで使用されている影の計算方法の2Dアナログを作成することにしました:光源の視点からジオメトリをレンダリングし、各ピクセルをレンダリングするときに光源から見えるかどうかを判断できる深度バッファーを作成します。 この手法は、シャドウマッピングと呼ばれます。 3Dでは、2次元のテクスチャを作成します。つまり、2Dでは、1次元のテクスチャを作成します。 以下のスクリーンショットは、Unityリソースビューでの完成したライティングマップを示しています。 実際には1つの光源ではなく、64の場合です。テクスチャ内のピクセルの各行は、個別の光源のシャドウマップです。









このメソッドは、極座標を使用して、2D位置を別のオブジェクトに対する角度と距離(または深度)に変換します。



inline float2 CartesianToPolar(float2 point, float2 origin) { float2 d = point - origin; // x -   , y -  return float2(atan2(dy, dx), length(d)); }
      
      





つまり、シャドウマップの各線は光源の周りの360度を表し、各ピクセルは光源からこの方向の最も近い不透明なジオメトリまでの距離を表します。 テクスチャの水平解像度が高いほど、結果の影の精度が高くなります。



以下でブロッキングジオメトリと呼ぶ、不透明なジオメトリキャストシャドウは、行のリストとして渡されます。 頂点の各ペアは極空間に位置のペアを作成し、その後、ピクセルシェーダーはライティングマップの対応するセグメントを塗りつぶします。 各ピクセルでは、標準のzバッファーテストを使用して、ジオメトリに最も近いピクセルのみが保存されます。 ピクセルシェーダーの極深度を単純に補間してz座標を取得するのは間違っています。これは、直線エッジの曲線シャドウを取得する方法だからです。 代わりに、現在の角度での線分と光線の交点を計算する必要がありますが、これはスカラー積と分割の数個の問題であり、最新のビデオプロセッサにとってはそれほど高価ではありません。



難しさ



軟膏の1回のフライではない場合、これはすべて非常に簡単です-極座標を使用する場合、境界の両側で360度の極座標で直線セグメントを取得すると深刻な問題が発生します。 一般的な解決策は、ラインセグメントを2つの別々の部分に分割することです。最初の部分は360度で終わり、もう1つ(セグメントの残り)は0で始まります。ただし、頂点シェーダーは1つの頂点のみを取得し、1つの結果を生成し、2つの部分を出力する方法はありませんセグメント。 このアプローチの主な難点は、この問題を解決することです。



この方法で解決できます:シャドウマップの元のラインを360度ではなく、0〜540の180度を追加します。極空間のラインセグメントは180度以下であるため、これは近くにあるセグメントに対応するのに十分です。これは、必要に応じて、各ラインセグメントがピクセルシェーダーの出力に1つのラインセグメントを作成することを意味します。









この方法の欠点はそれです。 線の最初の部分(0から180)と最後の部分(360から540)は、実際には極空間の1つの領域です。 シャドウマップに対してピクセルをチェックするには、極角がこの領域に収まるかどうかを判断し、そうであれば、2つの場所からサンプルを取得し、少なくとも2つの深度を選択する必要があります。 これは私が目指していたものとはまったく異なります-分岐と追加のサンプリングは、特に最も近いパーセントフィルタリング(PCF)の複数のテクスチャサンプリングを実行するときにパフォーマンスにひどい影響を与えます(この手法は、シャドウマップに基づいて滑らかなシャドウを作成するために広く使用されています)。 私の解決策(シャドウマップのすべての行を埋めた後)は次のとおりです。別のビデオプロセッサを実行してシャドウマップを通過し、リサンプリングして最初の180度と最後の180度を組み合わせます。 通常のフレームバッファ標準では、シャドウカードのテクスチャは非常に小さいため、ビデオプロセッサに少し時間がかかります。 その結果、現在のピクセルが特定の光源で照らされているかどうかを判断するのに1つのサンプルで十分な既製のシャドウマップテクスチャが得られます。



このシステムの主な欠点は、境界線のケースの認識と処理のために、ブロッキングジオメトリが特別な形式を持たなければならないことです。 各線分セグメントの各頂点は、線分セグメントの他端の位置を保存するため、線分セグメントに類似しています。 これは、アプリケーションの実行中または以前に、この形式でジオメトリを構築する必要があることを意味します。 レンダリングに使用されるジオメトリを単に伝えることはできません。 ただし、これには良い面があります。この特別なジオメトリを構築した後、少なくとも不要なデータを送信しない、つまり効率が高くなります。



このシステムのもう1つの優れた機能は、完成したシャドウマップをビデオプロセッサに書き戻すことができることです。これにより、レイトレーシングを必要とせずにCPUを介して可視性要求を実行できます。 テクスチャをビデオプロセッサにコピーして戻すのは非常にコストのかかる作業になる可能性があり、Unity 2018ではビデオプロセッサの非同期gpuリードバックの待望の実装があるという事実にもかかわらず、この関数は実際の必要なしに使用すべきではありません。



アルゴリズム



基本的に、アルゴリズムは次のように機能します。



  1. シャドウキャストジオメトリのラインセグメントのメッシュを作成します。 ジオメトリが変更されない場合、各フレームでジオメトリを再構築する必要はありません。 2つのメッシュを使用することもできます。1つは静的なジオメトリ用で、もう1つは動的なメッシュで、不必要な再構築を回避します。
  2. 影を落とす光源ごとにこのブロッキングジオメトリをレンダリングします。 デモでは、これに個別のレンダリングコールが使用されますが、これは、メッシュがビデオプロセッサに1回だけ転送されるように、インスタンス化を使用する絶好の機会です。 各光源にはシャドウマップ内のラインが割り当てられ、このデータはシェーダー定数を介してシェーダーに送信されます。これにより、記録する適切なY座標を作成できます。
  3. 頂点シェーダーでは、ラインセグメントが極空間に変換されます。
  4. ビデオプロセッサは、現在の行のラインセグメントをシャドウテクスチャで覆い隠し、最も近いピクセルのみを保存するzテストを実行します。
  5. 完成したシャドウマップを別のテクスチャに再サンプリングして、同じ極領域に関連する部分を削除します。
  6. 各光源に対して、光源の最大範囲(たとえば、点光源の半径)をカバーする四角形をレンダリングします。 各ピクセルについて、光源に従って極座標を計算し、それを使用してシャドウマップをサンプリングします。 極距離がシャドウマップから読み取られた値よりも大きい場合、ピクセルは光源によって照らされません。つまり、照明はピクセルに適用されません。


そして、ここに完成した結果があります。 ストレステストでは、64個の移動可能なシャドウキャスティングポイント光源を設定し、いくつかの回転する不透明なオブジェクトの間をランダムサイズの照明コーンが移動します。









費用はいくらですか? ブロッキングジオメトリが静的であり、インスタンス化を使用すると仮定すると、任意の数のソース(テクスチャサイズの制限の対象)の完全なシャドウマップを1回の描画呼び出しでビデオプロセッサに転送できます。 シャドウマップでの再描画の量は、照明をブロックするジオメトリの複雑さによって決まりますが、テクスチャは最新のフレームバッファに比べて非常に小さいため、ビデオプロセッサキャッシュのパフォーマンスは素晴らしいはずです。 同様に、シャドウマップをサンプリングする場合、シャドウマップがはるかに小さくなることを除いて、3Dのシャドウマッピングと違いはありません。



私たちのゲームは単一の大きな環境で構成されており、64個の常時アクティブなシャドウキャスト光源があるため、サイズ1024x64のシャドウマップのテクスチャを使用しました。 フレーム計算の全体的な予算内のコストは最小限でした。



追加機能



このシステムを拡張したい場合は、いくつかの興味深い機能を提供できます。 シャドウマップを処理して重複する2つの領域を削除する場合、機会を使用して値を変換して指数シャドウマップを作成し、それをぼかします(水平方向にのみぼかしる必要があることを忘れないでください。そうしないと、互いに関係のないソースに影響します! ) これにより、シャドウマップをマルチサンプリングすることなく滑らかなシャドウを作成できます。 2つ目:先ほど述べたように、デモでは各ソースのシェーディングジオメトリを転送するために個別の描画呼び出しを行っていますが、光源の位置と他のパラメーターをマトリックスにパックすると、インスタンス化を使用して1つの描画呼び出しで簡単に実行できます。



さらに、CPU側で追加の作業をほとんど行うことなく、単一反射のラジオシティ照明をシステムの拡張として実装できると考えています。 これを行うには、次の原則を使用できます。ビデオプロセッサは、最後のフレームのシャドウマップを使用して、シーン内の光線の反射を計算できます。 今のところ、このシステムをまだ実装していないため、これ以上詳細なことは言えません。 動作する場合、CPUによって実行されるレイトレーシングを使用する通常のVirtual Point Light実装よりもはるかに効率的です。



さらに、このシステムは多くの興味深い方法で使用できます。 たとえば、光源をサウンドエミッターに置き換えた場合、このシステムを使用して吸音率を計算できます。 または、AIプロシージャの視野を決定するために使用できます。 一般に、レイトレーシングをテクスチャ検索に変えることができます。



完了



これで、1次元シャドウイングシステムの実装の詳細について説明しました。 質問がある場合は、 元の記事へのコメントで質問してください



デモソースコード



https://www.double11.com/misc/uploads/1DShadowMapDemo.zip



All Articles