ShadowVolume サンプル

このサンプルでは、リアルタイム シャドウをレンダリングするためのシャドウ ボリュームと呼ばれる一般的なテクニックについて説明します。このサンプルのシャドウは、オクルーディング ジオメトリの面 (ライトとは反対方向に向いた面) を押し出して、3D 空間の影付きの領域を表すボリュームを形成します。ここでのステンシル バッファーは、追加のジオメトリをレンダリングするためのマスクとして使用され、ジオメトリがレンダリングされると更新されます。

Bb147373.ShadowVolume(ja-jp,VS.85).jpg

リアルタイム シャドウのレンダリングは、3D コンピューター グラフィックでは高度なトピックでした。このサンプルは、ステンシル バッファーと頂点シェーダーを使用してシャドウ ボリューム テクニックを実装します。まず、オクルーディング ジオメトリを検証し、シャドウ ボリュームを表す別のジオメトリを作成します。シーンをレンダリングするときは、深度フェイル ステンシル テクニックを使用し、頂点を押し出す頂点シェーダーによりシャドウ ボリューム ジオメトリをレンダリングします。ステンシル バッファーはこの処理の間に更新されます。ステンシル バッファーをマスクとして使用すると、シャドウ ボリュームに存在するピクセルはライティングを受け取りません。深度フェイル シャドウ テクニックでは、シャドウ ボリュームは閉じたボリュームである必要があります。

このサンプルは、最大で 6 つのライトをサポートし、以下の 3 とおりの方法でシーンをレンダリングできます。

  • シーンを通常見ている場合と同様にシャドウを使用する。
  • シャドウとシャドウ ボリュームを使用する。
  • シャドウではなく、シャドウ ボリュームのみを使用する。このオプションは、各ピクセルでのシャドウ ボリュームの複雑さ (つまり、ピクセルがシャドウ ボリュームでレンダリングされる回数) を基にピクセルごとにカラー コードをレンダリングします。

Path

ソース : (SDK ルート)\Samples\C++\Direct3D\ShadowVolume
実行可能ファイル : (SDK ルート)\Samples\C++\Direct3D\Bin\x86 or x64\ShadowVolume.exe

サンプルが動作するしくみ

オブジェクトのシャドウ ボリュームとは、シーン内の、特定の光源によってできたオブジェクトのシャドウによってカバーされる領域です。シーンをレンダリングする際、シャドウ ボリューム内にあるすべてのジオメトリを、その特定の光源でライティングすることはできません。閉じたシャドウ ボリュームは 3 つの部分から成ります。つまり、フロント キャップ、バック キャップ、サイドです。フロント キャップとバック キャップは、シャドウ キャスティング ジオメトリから作成できます。つまり、フロント キャップは光源の方向に向いている面から、バック キャップは光源から離れた側の面から作成します。さらに、バック キャップはライトの方向に沿った大きな距離に変換され、シャドウ ボリュームはシーンのジオメトリをカバーできる程度に十分長くなります。通常、側面は、まずシャドウ キャスティング ジオメトリのシルエット エッジを決定し、次にライトの方向に沿った大きな距離に対して押し出されたシルエット エッジを表す面が作成されます。図 1 は、シャドウ ボリュームの種々の部分を示しています。

Bb147373.ShadowVolume1(ja-jp,VS.85).png

図 1:シャドウ ボリュームの作成フロント キャップ (青) とバック キャップ (赤) は、オクルーダのジオメトリから作成されます。バック キャップを移動してシャドウ ボリュームを引き伸ばし、シャドウ ボリュームを囲むようにサイド フェース (紫) を生成します。

このサンプルは、シャドウ ボリュームの 1 つの具体的な実装を示しています。CPU でシルエットを決定し、シャドウ ボリューム ジオメトリを生成する代わりに、このサンプルでは、まずライトの方向に関係なしに押し出されているジオメトリのシャドウ ボリュームを表すことができるメッシュを作成し、シャドウ ボリューム メッシュをレンダリングするときに頂点シェーダーを使用して頂点の押し出しを実行します。この基礎となっている考え方は、光源に面している三角形をそのままシャドウ ボリュームのフロント キャップとして使用できるということです。ライトとは反対の方向を向いている三角形の場合、頂点は各頂点でのライトの方向に沿った大きな距離に変換されてからバック キャップとして使用できます。しかし、シルエット エッジでは、1 つの三角形が光源の方向を向き、その隣の三角形が光源から離れた側を向くため、問題が起こります。この場合、各三角形ではその頂点が異なって処理され、2 つの三角形により共有される頂点の処理方法を決定する必要があります。

この問題を解決するには、各三角形が独自の 3 つの頂点を持つように、共有されている頂点を複製することで 2 つの三角形を分割します。三角形間の共通のエッジがシルエット エッジになる場合は、1 つの三角形をその場所に維持し、もう一方の三角形をライトの方向に沿って動かします。これによって 2 つの三角形間にギャップが生じますが、閉じたシャドウ ボリュームはギャップや穴を持つことはできません。これは、2 つの三角形間のシャドウ ボリューム メッシュにクワッドを追加することで解決できます。2 つの三角形により共有されるエッジが分割され、4 つの頂点がクワッドを定義します。押し出しの前に、三角形は互いに隣接するため、クワッドは縮退です。ただし、三角形が十分離れている場合、クワッドは引き伸ばされ、シャドウ ボリュームの側面を自動的に形成します。図 2. はこの処理を示しています。

Bb147373.ShadowVolume2(ja-jp,VS.85).png

図 2:2 つの面の分割処理左 : 2 つの面は頂点 A と B を共有しています。中央 :頂点 A' (A の複製) と B' (B の複製) を生成し、いずれかの面でこれらを参照するようにします。右:頂点 A、A'、B、および B' を使用してクワッドを生成します。便宜上、A' と B' は A と B から一定の距離に描画されています。実際は、A と A' はメッシュの頂点バッファーでは一致しており、B と B' も同様です。このため、新規に作成されたクワッドは縮退です。

シャドウ ボリューム メッシュの生成

シャドウ ボリュームの静的メッシュの生成、および頂点シェーダーによる頂点の押し出しの最大の利点は、シャドウのレンダリングに必要な CPU サイクルがきわめて少ないことです。頂点シェーダーはサンプルから頂点を受け取ると頂点を正しい方向に押し出すことができるため、シャドウ ボリューム メッシュは一旦生成すると、ライトの位置がどこにあっても変更する必要はありません。シャドウ ボリュームを生成する関数は GenerateShadowMesh です。この関数は、入力メッシュを取得し、入力メッシュのシャドウ ボリュームを表す別のメッシュを出力します。この関数は、入力メッシュの適切なシャドウ ボリュームを生成するために複数のことを実行します。

入力メッシュのすべてのエッジについて、この関数はエッジを共有する 2 つの面を効果的に分離することで、入力メッシュのエッジを 2 つのエッジに分割する必要があります。次に、2 つの分割エッジを接続するクワッド (または 2 つの三角形) を作成します。図 3. はこの処理を視覚化しています。既定では、分割エッジは共直線であるため、これらのクワッドは縮退です。ただし、ある面が押し出され、もう一方が押し出されない場合、これらの間のクワッドは引き伸ばされ、シャドウ ボリュームの側面を形成します。

Bb147373.ShadowVolume3(ja-jp,VS.85).png

図 3:メッシュ面 (緑) は分割され、クワッド (赤) が挿入されます。

シャドウ ボリューム メッシュを作成するアルゴリズムは、入力メッシュの面を反復処理することで機能します。反復処理された面ごとに、3 つのことが発生します。

  • まず、シャドウ ボリューム メッシュに対して 3 つの新しい頂点と 1 つの新しい面が生成されます。これらの面は縮退クワッドにより分離されるため、各面は独自の 3 つの頂点を持つ必要があります。
  • 次に、図 4. に示すように、新しい頂点の法線が計算され、新しい面の法線が得られます。この処理が必要な理由は、頂点の押し出しが頂点シェーダーにより実行され、頂点シェーダーが面の法線ではなく頂点の法線のみを扱うためです。面の法線に合わせて頂点の法線を設定することにより、頂点が属する面がライトとは反対側にあるときに頂点シェーダーは頂点を正しく押し出します。
  • 最後に、面の 3 つのエッジはエッジ マッピング テーブルに追加されます。エッジ マッピングのエントリは、入力メッシュのエッジを表す 1 つのソース エッジ、および出力メッシュの分割エッジを表す 2 つの出力エッジを含んでいます。基本的に、このテーブルにはソース メッシュのエッジ、および出力メッシュで分割されるエッジを記録します。この情報は、後でクワッドを生成するときに必要になります。このアルゴリズムは、追加された面のエッジごとにエッジ マッピング テーブルを検索し、ソース エッジの既存のエントリが見つからなかった場合、ソース エッジを作成して初期化し、エッジ マッピング エントリの出力エッジを作成します。ただし、このアルゴリズムはソース エッジのエントリが既にテーブルにあり、このエッジにクワッドの 4 つの頂点があることを確認した場合、クワッドの 2 つの面を出力メッシュに追加し、エッジ マッピング エントリをテーブルから削除します。

Bb147373.ShadowVolume4(ja-jp,VS.85).png

図 4:頂点の法線は面の法線と同じに設定されます。

この時点で、出力メッシュは入力メッシュにあるすべての面を含んでおり、入力メッシュのすべてのエッジは出力メッシュのクワッドに変換されています。入力メッシュで共有されていないエッジを表すマッピング テーブルには、エッジの一覧もあります。このようなエッジが存在することは、入力メッシュに穴があり、シャドウ メッシュが閉じたボリュームになるように穴をパッチする必要があることを意味します。これは、深度フェイル ステンシル テクニックを使用してシャドウをレンダリングするときに重要です。パッチ アルゴリズムは、マッピング テーブルを検索し、元のメッシュの頂点を共有している 2 つのエッジを見つけます。次に、新しい面、および 2 つの隣接するエッジの頂点以外の 3 つの新しい頂点を生成して穴をパッチします。その後、2 つのクワッドを生成してパッチ面を出力メッシュの既存のジオメトリに接続します。この処理を図 5. に示します。

Bb147373.ShadowVolume5(ja-jp,VS.85).png

図 5:閉じたボリューム シャドウ メッシュの作成

シャドウのレンダリング

トップ レベルでは、レンダリングの手順は次のようになります。

  • アンビエント ライティングを有効にしている場合、アンビエントのみを使用してシーン全体をレンダリングします。
  • シーンのライトごとに、以下の手順を実行します。
    • 深度バッファーおよびフレーム バッファーの書き込みを無効にします。
    • シャドウ ボリュームのレンダリングのために、ステンシル バッファーのレンダリング ステートを準備します。
    • 頂点押し出しシェーダーを使用して、シャドウ ボリューム メッシュをレンダリングします。これにより、ピクセルがシャドウ ボリュームの中にあるかどうかに従って、ステンシル バッファーがセットアップされます。
    • ライティングのために、ステンシル バッファーのレンダリング ステートを準備します。
    • 加法ブレンディング モードを準備します。
    • 処理中のライトのみを使用して、ライティングするシーンをレンダリングします。

シーン内のライトは、別々に処理しなければなりません。これは、ライトの位置によって必要とされるシャドウ ボリュームが異なり、異なるステンシル ビットが更新されるからです。コードがシーン内の各ライトを処理する方法を、以下で詳しく説明します。はじめに、シャドウ ボリューム メッシュをレンダリングします。このとき、深度バッファーとフレーム バッファーへの書き込みは行いません。これらのバッファーは無効にしておく必要があります。これは、シャドウ ボリュームのレンダリングの目的は、単にシャドウによってカバーされるピクセルのステンシル ビットを設定することであり、シャドウ ボリューム メッシュそのものはシーンに表示してはならないからです。シャドウ メッシュは、深度フェイル ステンシル シャドウ テクニックと頂点押し出しシェーダーを使用してレンダリングされます。シェーダーでは、頂点の法線が調べられます。法線が光源の方向を向いている場合、頂点はそのままの位置に置かれます。また、法線が光源から離れた側を向いている場合、頂点は無限に押し出されます。これは、頂点のワールド座標を、W 値が 0 の光源から頂点へのベクトルと同じにすることによって行われます。この処理の結果、光源から離れた側を向いているすべての面が、ライトの方向に沿って無限に投影されます。面はクワッドによって接続されているので、1 つの面が投影され、それに隣接する面が投影されない場合、それらの面の間のクワッドは縮退しません。このクワッドは引き伸ばされ、シャドウ ボリュームのサイドになります。図 6 は、これを示しています。

Bb147373.ShadowVolume6(ja-jp,VS.85).png

図 6:シャドウ ボリュームの光源から離れた側を向いている面 (左、赤で示す) の頂点が、頂点シェーダーによって押し出され、シャドウによってカバーされる領域を囲むボリュームが作成されます (右)。

深度フェイル テクニックを使用してシャドウ メッシュをレンダリングする場合、コードは最初に、シャドウ メッシュのすべての後ろ向きの三角形をレンダリングします。ピクセルの深度値が深度比較でフェイルした場合 (通常これは、ピクセルの深度が深度バッファーの値よりも大きいことを意味します)、そのピクセルのステンシル値がインクリメントされます。次に、コードはすべての前向きの三角形をレンダリングし、ピクセルの深度が深度比較でフェイルした場合、そのピクセルのステンシル値がデクリメントされます。この方法で、シャドウ ボリューム メッシュ全体がレンダリングされると、シャドウ ボリュームによってカバーされているシーンの中のピクセルは、ステンシル値が 0 以外の値になり、それ以外のすべてのピクセルはステンシル値が 0 になります。次に、シーン全体をレンダリングし、ステンシル値が 0 になっている場合のみピクセルを書き出すことによって、処理しているライトのライティングを行うことができます。

Bb147373.ShadowVolume7(ja-jp,VS.85).png

図 7:深度フェイル テクニック

図 7. は深度フェイル テクニックを示しています。オレンジ色のブロックは、シャドウ レシーバー ジオメトリを表しています。領域 A、B、C、D、E は、シャドウ ボリュームがレンダリングされる、フレーム バッファー内の 5 つの領域です。数値は、シャドウ ボリュームのフロント面とバック面がレンダリングされる際の、ステンシル値の変化を示します。領域 A と E では、シャドウ ボリュームのフロント面とバック面の両方が深度テストにフェイルするため、両方でステンシル値が変化します。領域 A では、オレンジ色のシャドウ レシーバーによって深度テストがフェイルし、領域 E ではキューブのジオメトリがテストにフェイルします。結果として、すべての面のレンダリングが終わったときに、この領域のステンシル値は 0 になっています。領域 B と D では、フロント面が深度テストにパスし、バック面がフェイルします。したがって、フロント面ではステンシル値は変化せず、結果的にステンシル値の変化は 1 になります。領域 C では、フロント面とバック面の両方が深度テストにパスするので、どちらもステンシル値が変化しません。この領域のステンシル値は 0 のままになります。シャドウ ボリュームが完全にレンダリングされたとき、領域 B と D のステンシル値だけが 0 以外の値になっています。これは、この特定のライトについては、領域 B と D だけがシャドウされる領域になることを正しく示しています。

パフォーマンスに関する考慮事項

頂点シェーダーでの頂点の押し出しには、いくつかの利点があります。メッシュ ジオメトリ用にシャドウ ボリューム メッシュを生成すると、メッシュを 3D デバイスに送信して、変更を加えることなくレンダリングできます。つまり、ホスト CPU でシルエットを決定する必要がなくなります。ただし、このテクニックには欠点もあります。静的シャドウ ボリューム メッシュは、視認できるジオメトリよりも多くの頂点を持つ傾向があります。これは、ジオメトリがその面を分割し、クワッドを生成して有効な閉じたシャドウ ボリュームを作成する必要があるためです。完全に結合されたメッシュは、3 倍の数の頂点を含むシャドウ メッシュを持ちます。頂点数が著しく増加し、頂点シェーダーでこれらを処理することによりパフォーマンスが実際に低下します。アプリケーションでは、CPU でのシルエットの決定など、代替テクニックを実装し、パフォーマンスの結果を比較して、シーンのシャドウ キャスティング オブジェクトごとにどのテクニックを使用するか適切な結論を出すことをお勧めします。

シャドウ キャスティング ジオメトリとそのシャドウ ボリュームに別々のメッシュを使用すると利点があります。追加の頂点や面が元のメッシュに存在する場合、これらの頂点がジオメトリのレンダリングにまったく役に立たない場合でも余分な頂点を処理しなければならないことはありません。これらの余分な頂点は、テクスチャー座標やマテリアル カラーなど、シャドウのレンダリングに使用されない頂点データも備えています。あまり正確ではないシャドウでも受け入れられる状況では、メッシュの詳細度が低いバージョンをシャドウ ボリューム レンダリングに使用できます。

別々のメッシュを使用すると、ジオメトリに対するメモリーの要件が増加します。サンプルでは、メッシュ オブジェクトに ID3DXMesh を使用しているため、これが当てはまります。ただし、頂点バッファー、インデックス バッファー、および頂点ストリームを直接使用することで、追加のメモリーの必要性をなくすことができます。まず、1 つの頂点バッファーが位置と法線を含み、他の頂点バッファーがマテリアル属性、テクスチャー座標、およびブレンディング パラメーターなど、残りの頂点データを含むように、シーンのメッシュの頂点バッファーを複数の頂点バッファーに分割する必要があります。ジオメトリをレンダリングするときは、複数の頂点ストリーム (頂点バッファーごとに 1 つ) を設定する必要があります。次に、シャドウ ボリューム メッシュを生成した後、入力メッシュに元々存在するすべての頂点が新しく作成された頂点の前に配置されるように、頂点バッファーの順序を並べ替えてインデックス バッファーを準備する必要があります。これが完了したら、位置と法線の入力メッシュの頂点バッファーはシャドウ メッシュの頂点バッファーの最初の半分と等しくなります。このため、入力メッシュの頂点バッファーを解放することが可能になり、メッシュはシャドウ メッシュの頂点バッファーを簡単に参照できます。それでも、2 つのメッシュは別々のインデックス バッファーを必要とすることに注意してください。このバッファー共有処理の視覚化については、図 8. を参照してください。

Bb147373.ShadowVolume8(ja-jp,VS.85).png

図 8:シーン メッシュとシャドウ ボリューム メッシュ間の頂点データの共有。この図のシーン メッシュ (緑の枠内) は、1 つの頂点バッファーは位置と法線用 (赤で表示) ともう 1 つはそれ以外用 (青で表示) の 2 つの異なる頂点バッファーにまたがる N 個の頂点を持っています。シャドウ ボリューム メッシュ (赤枠内) は、シーン メッシュよりも多くの頂点数を備えています。このため、頂点バッファー (赤) は N 個を超える頂点を含んでおり、最初の N 個の頂点はシーン メッシュで共有されています。

このテクニックを使用する欠点は、アプリケーションでメッシュの頂点バッファーおよびインデックス バッファーを管理する必要があり、ID3DXMesh インターフェイスをこれらのバッファーに使用することができなくなることです。 各アプリケーションでは、それ自体のニーズと目標に基づいてこの得失を評価する必要があります。

最後に、パフォーマンスの最適化が必要になるもう 1 つの領域があります。前に示したように、シャドウ ボリュームに関するレンダリング アルゴリズムでは、シーンを複数のパス (正確に言うと、シーン内のライトの数に 1 を加えた数) でレンダリングすることが必要とされます。シーンがレンダリングされるごとに、同じ頂点がデバイスに送信され、頂点シェーダーによって処理されます。アプリケーションで複数のレンダー ターゲットに対して遅延ライティングを使用すれば、この問題は避けられます。このテクニックでは、アプリケーションはシーンを一度レンダリングし、カラー マップ、法線マップ、位置マップを出力します。以降のパスでは、ピクセル シェーダーでこれらのマップの値を取得し、読み取ったカラー、法線、位置データを基にライティングを適用することができます。この方法には、非常に大きな利点があります。シーン内の各頂点は一度だけ (最初のパスで) 処理すればよく、その後のパスで各ピクセルが一度だけ処理されます。これにより 2 番目以降のパスでオーバードローが起こらなくなります。

シャドウ ボリュームのアーティファクト

シャドウ ボリュームは、欠陥のないシャドウ テクニックではありません。高いフィル レートの要件と、シルエットの決定のほかに、このテクニックによってレンダリングされたイメージが、図 9 に示すように、シルエットのエッジの近くにアーティファクトを含むことがあります。このアーティファクトの主な原因は、ジオメトリがそれ自体の上にシャドウをキャストするようにレンダリングされた場合、そのジオメトリの面は通常、面の法線が光源の方を向いているかどうかによって、完全にシャドウに入るか、完全にライティングされるという事実にあります。しかし、ライティングの計算では、面法線ではなく頂点法線が使用されます。そのため、ライトの方向にほぼ平行の面は、実際にはその一部だけがシャドウに入る場合でも、完全にライティングされるか完全にシャドウに入ります。これは、ステンシル シャドウ ボリューム テクニックの固有の欠点であり、シャドウのサポートを実装するときに考慮しなければなりません。このアーティファクトは、メッシュの詳細度を上げることによって減らすことができますが、その代償としてメッシュのレンダリング時間が長くなります。頂点法線が面法線に近くなるほど、アーティファクトは目立たなくなります。アプリケーションでアーティファクトを許容可能なレベルまで減らすことができない場合、他のタイプのシャドウ テクニック (シャドウ マッピングや事前計算済み放射輝度伝播など) を利用することも検討してください。

Bb147373.ShadowVolume9(ja-jp,VS.85).jpg

図 9:シルエット エッジの近くのシャドウ ボリューム アーティファクト

Bb147373.ShadowVolume10(ja-jp,VS.85).jpg

図 10:シャドウ ボリュームを表示すると、エッジのギザギザの原因が、実際には一部だけがシャドウに入っている面が、完全にシャドウの中にあるように処理されていることだったことがわかります。