ShadowVolume10 サンプル

Ee416427.d3d10_sample_shadowvolume(ja-jp,VS.85).jpg

パス

ソース SDK ルート\Samples\C++\Direct3D10\ShadowVolume10
実行可能ファイル SDK ルート\Samples\C++\Direct3D10\Bin\x86 または x64\ShadowVolume10.exe

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

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

Ee416427.d3d10_sample_shadowvolume_1(ja-jp,VS.85).png図 1:シャドウ ボリュームの作成

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

このサンプルは、シャドウ ボリュームの 1 つの具体的な実装を示しています。従来の多くのシャドウ ボリューム処理方法では、シルエットの決定とシャドウ ボリューム ジオメトリの生成は CPU 上で行われていました。このサンプルでは、シルエットの決定はジオメトリ シェーダーの中で行われ、ジオメトリ シェーダーがラスタライザーに種々の量のデータを送信して、即座に新しいシャドウ ボリューム ジオメトリを作成できるという事実を用いています。この基礎となっている考え方は、光源に面している三角形をそのままシャドウ ボリュームのフロント キャップとして使用できるということです。バック キャップは、各頂点で、ライトの方向に沿って一定距離離れた場所に移された前向きの三角形から生成され、それらをバック キャップとして使用できます。しかし、シルエット エッジでは、1 つの三角形が光源の方向を向き、その隣の三角形が光源から離れた側を向くため、問題が起こります。この場合、ジオメトリ シェーダーが 2 つの新しい三角形を押し出し、シャドウ ボリュームのフロント キャップとバック キャップの間を調整するクワッドを作成します。

Ee416427.d3d10_sample_shadowvolume_2(ja-jp,VS.85).png

ジオメトリ シェーダーがシルエット エッジを探すためには、どの面とどの面が互いに隣接しているかを把握する必要があります。そのために、ジオメトリ シェーダーは新しいタイプの入力プリミティブ triangleadj をサポートしています。triangleadj は、1 つおきの頂点を隣接する頂点であると見なします。ジオメトリのインデックス バッファーを、メッシュの隣接性情報を反映するように修正する必要があります。この処理のために、CDXUTMesh10::ConvertToAdjacencyIndices は、1 つおきの値が現在の三角形とエッジを共有する三角形の隣接する頂点のインデックスとなる、インデックス バッファーを作成します。インデックス バッファーは、追加情報を格納するため、サイズが 2 倍になります。下の図は、隣接性情報の順序を示しています。

Ee416427.d3d10_sample_shadowvolume_3(ja-jp,VS.85).png

シャドウのレンダリング

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

  • アンビエント ライティングを有効にしている場合、アンビエントのみを使用してシーン全体をレンダリングします。

  • シーン内の各ライトに対して、以下の手順を実行します。

    • 深度バッファーおよびフレーム バッファーの書き込みを無効にします。

    • シャドウ ボリュームのレンダリングのために、ステンシル バッファーのレンダリング ステートを準備します。

    • 頂点押し出しシェーダーを使用して、シャドウ ボリューム メッシュをレンダリングします。これにより、ピクセルがシャドウ ボリュームの中にあるかどうかに従って、ステンシル バッファーがセットアップされます。

    • ライティングのために、ステンシル バッファーのレンダリング ステートを準備します。

    • 加法ブレンディング モードを準備します。

    • 処理中のライトのみを使用して、ライティングするシーンをレンダリングします。

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

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

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

Ee416427.d3d10_sample_shadowvolume_5(ja-jp,VS.85).png図 5:深度フェイル テクニック

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

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

現在のサンプルでは、同じメッシュを使用して、通常のレンダリングとシャドウ レンダリングを行います。メッシュ クラスは一度に 1 つのインデックス バッファーのみを追跡するので、隣接性情報がシャドウ計算には必要でない場合でも、この情報がシェーダーに送信されます。シェーダーは、ジオメトリ シェーダーでこの隣接性情報を削除するために、追加の作業を行う必要があります。パフォーマンスを向上させるために、アプリケーションは各メッシュに 2 つのインデックス バッファーを確保しておくことができます。1 つは標準のインデックス バッファーで、シャドウ レンダリング以外のレンダリングに使用します。もう 1 つは、隣接性情報を含み、シャドウ ボリュームを押し出す場合にのみ使用します。

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

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

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

Ee416427.d3d10_sample_shadowvolume_6(ja-jp,VS.85).jpg図 6:シルエット エッジの近くのシャドウ ボリューム アーティファクト

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