HLSL シェーダーの魔導書を読む
途中までやってサボっていたのでちゃんと勉強する意思表示として内容を残します
目的
- シェーダーの勉強をしたい
- アウトプットするようにしたい
この本をやっていく
Chapter1 レンダリングパイプライン入門
CPUからドローコールが実行された際にGPUが行う描画手順のこと。
※必要最低限(テッセレーション、ジオメトリシェーダーなどがある)
- 入力アセンブラ:描画に必要な頂点バッファーやインデックスバッファーなどのデータをグラフィックスメモリから読み込む
- 頂点シェーダー:頂点データをスクリーン空間に変換する
- ラスタライザー:描画するピクセルを決定する
- ピクセルシェーダー:ピクセルの色を決定する
頂点シェーダー
3Dモデルの頂点座標をスクリーン空間に変換する。
座標の変換にはベクトルと行列の乗算で行われる。
スクリーン空間に変換するには以下の処理が必要
- ローカル座標系の頂点をワールド座標系に変換する
ローカル座標 * ワールド行列 - ワールド座標系の頂点をカメラ座標系に変換する
ワールド座標 * カメラ行列 - カメラ座標系の頂点をスクリーン座標系に変換する
カメラ座標 * プロジェクション行列
Chapter2 はじめてのシェーダー
頂点シェーダー
// 頂点シェーダーへの入力頂点構造体
struct VSInput
{
float4 pos : POSITION;
};
// 頂点シェーダーの出力
struct VSOutput
{
float4 pos : SV_POSITION;
};
// 頂点シェーダー
// 1. 引数は変換前の頂点情報
// 2. 戻り値は変換後の頂点情報
VSOutput VSMain(VSInput In)
{
VSOutput vsOut = (VSOutput)0;
// 入力された頂点座標を出力データに代入する
vsOut.pos = In.pos;
return vsOut;
}
VSInputで入力される頂点データを定義する。
VSOutoutで出力される頂点データを定義する。
VSMain内で、引数VSInputで入力された頂点座標をもとに頂点座標を操作し、出力VSOutputに渡す。
float4
float4はx, y, z, wの4つの要素からなるベクトル型
他にもfloat3やfloat2, 行列であるfloat3x3やfloat4x4などがある
セマンティクス
float4 pos : POSITION;
POSITIONのことをセマンティクスと呼ぶ。データの使われ方を指定するもので、POSITIONは、頂点データのうち、頂点座標として使用することを指定している。
他にも色を指定するCOLORや法線を指定するNORMALなどがある。
出力される構造体の場合はPOSITIONではなくSV_POSITIONとなる。
ピクセルシェーダー
// ピクセルシェーダー
float4 PSMain(VSOutput vsOut) : SV_Target0
{
// 赤色を出力している
return float4(1.0f, 0.0f , 0.0f, 1.0f);
}
引数VSOutputは頂点シェーダーから出力された頂点情報をもとに計算されたデータ。返り値はピクセルのカラーとなり、RGBA。
RGBは基本的に0~255の範囲で表現されるが、DirectXでは0~1の範囲で正規化される。
頂点シェーダーから渡された頂点と頂点の間のピクセルは3つの頂点から補完されて計算される。また、補完の計算はラスタライザーで行われている。
// 頂点情報として渡された値をそのままピクセルシェーダーに渡している
float4 color;
color.x = vsOut.color.x;
color.y = vsOut.color.y;
color.z = vsOut.color.z;
color.w = 1.0f;
return color;
Chapter3 シェーダープログラミングの基礎
座標変換
3Dモデルのキャラクターを画面上で移動させるということはモデルのすべての頂点座標に対して座標変換を行っているということ。3Dモデルの頂点数は10万を超えることもあり、その結果、座標変換はCPUよりGPUで行うほうが適切。
ベクトルに行列を乗算すると、別の空間ベクトルに変換することができる。基本的に3つの行列でスクリーン空間に変換できる。
- ワールド行列
- カメラ行列
- 透視変換行列
ワールド行列
モデル空間は、モデル自体の座標空間。3Dモデルをモデル空間からワールド空間に変換するための行列。
モデル空間に対して、キャラクター座標、回転や拡大率などから作られるワールド行列を乗算することで、ワールド空間に変換する。
カメラ行列
カメラ空間はカメラを原点とする空間。3Dモデルをカメラ空間委変換するための行列。3Dモデルの頂点に対して、カメラ行列を乗算する。
透視変換行列
スクリーン空間は画面の中心を原点とする2次元空間。ワールド空間やカメラ空間は3次元空間だが、それを2次元空間に変換するための行列。
テクスチャマッピング
UV座標
テクスチャの左上(0, 0)右上(1, 1)とする座標系。横方向をU座標、縦方向をV座標として表される。
テクスチャ座標とも呼ばれる。
Chapter4 ライティングの基礎
ライトの種類
- ディレクションライト
- ポイントライト
- スポットライト
ディレクションライト
ライトの方向とカラーしか持たないライト
struct DirectionLight
{
float direction[3];// ライトの方向
float color[3];// ライトのカラー
}
現実のライトは、光源位置かrなお距離が離れれば離れるほど、光が弱くなる。また、ライトと物体の位置関係によって、光の当たる方向も変わる。しかし、ディレクションライトには位置情報がないので、どこにいても光の強さと方向が変わらない。
ポイントライト
ライトの位置、カラー、影響範囲の情報を持っているライト。光源からの距離が離れれば離れるほど、ライトの影響は弱くなる。
struct PointLight
{
float position[3];// ライトの位置
float color[3];// ライトのカラー
float infiuenceRange;// 影響範囲。単位はメートル
}
スポットライト
ライトの位置、カラー、影響範囲、放射方向、放射角度の情報を持つライト
struct SpotLight
{
float position[3];// ライトの位置
float color[3];// ライトのカラー
float direction[3];// 放射方向
float angle;// 放射角度
float infiuenceRange;// 影響範囲。単位はメートル
}
反射:Phongの反射モデル
Phongの反射モデルは、以下の3つの反射光を合成したもの。
- 拡散反射光
- 鏡面反射光
- 環境光
拡散反射光:Lambert拡散反射モデル
拡散反射を計算するためのモデル。光が強く当たっている面は明るく、あまり当たってない面は暗くなっていくことを表現する。Lambert拡散反射モデルは、法線とライトの方向との内積を計算する。
Lambert拡散反射モデルは以下の流れで表す
- ライトの方向と面の法線とで内積を計算する
- 1.で求めた結果に-1を乗算してライトの強さを求める
- 2.で求めたライトの強さを使ってライティングを行う
float4 PSMain(SPSIn psIn) : SV_Target0
{
// 1. ライトの方向と面の法線とで内積を計算する
float t = dot(psIn.normal, ligDirection);
// 1.で求めた結果に-1を乗算してライトの強さを求める
t *= -1.0f;
// ライトの強さにマイナスの値は必要ないため
if (t < 0.0f)
t = 0.0f;
// 2.で求めたライトの強さを使ってライティングを行う
float3 diffuseLig = ligColor * t;
float4 finalColor = g_texture.Sample(g_sampler, psIn.uv);
finalColor.xyz *= diffuseLig;
return finalColor;
}
鏡面反射光:Phong鏡面反射モデル
Phong脅威面反射モデルは、金属のような反射を表現するモデル。
Phogn鏡面反射の強さは、反射した光がどれだけ目に飛び込んでくるかを計算して求める。
Phong鏡面反射モデルは以下の流れで表す
- ライトが面に入射して、反射したベクトルを求める
- 光が入射した面から視点に向かって伸びるベクトルを求める
- 1と2で求めたベクトルの内積を使って、鏡面反射の強さを求める
- 3で求めた鏡面反射の強さを絞って、最終的な反射の強さを求める
1. ライトが面に入射して、反射したベクトルを求める
ライトベクトルをL, 法線をNとしたとき、反射ベクトルRは次の計算で求められる
R = L + 2 * (-N * L) * N
ChatGPT4.0より解説
- N⋅L: まず、法線ベクトルNと入射光ベクトルLの内積を計算します。この内積は、LがNに対してどの程度「向かっている」かを量的に示します。値が正であれば、LはNの同じ側にあります。値が負であれば、反対側にあります。
- −N⋅L: N⋅L の値にマイナスをつけることで、LがNに向かっている方向を反転させます。これは、入射光が表面に当たって反射する際の「反射角は入射角に等しい」という物理法則に基づいています。
- 2⋅(−N⋅L)⋅N: 次に、この値を2倍して、さらに法線ベクトルNにスケールアップしたベクトルを計算します。これにより、入射光Lが表面に当たって反射する方向への補正量を得ます。
- L+2⋅(−N⋅L)⋅N: 最後に、この補正量を元の入射光ベクトルLに加えることで、最終的な反射光ベクトルRを得ます。これにより、光が表面からどのように反射するかを示すベクトルが計算されます。
2. 光が入射した面から視点に向かって伸びるベクトルを求める
面から視点に向かって伸びるベクトルは、ベクトルの引き算で求めることができる。
面のワールド座標をsurfacePos, 視点の座標をeyePosとしたときに、視点に向かって伸びるベクトルtoEyeはeyePos - surfacePos
となる。
float3 toEye = eyePos - surfacePos;
// 向きだけが欲しいので正規化
toEye = normalize(toEye);
3. 1と2で求めたベクトルの内積を使って、鏡面反射の強さを求める
「光の反射ベクトル」と「視線ベクトル」の向きが近ければ近いほど、鏡面反射の強さが強いことになる。
この二つのベクトルも内積で求める。内積はベクトルの向きが同じなら1になり、向きが離れると値が小さくなる性質を持つため。
4. 3で求めた鏡面反射の強さを絞って、最終的な反射の強さを求める
反射の強さを絞ることで、ハイライトの大きさを調整することができる。
鏡面反射を累乗することで表す
pow(鏡面反射, 累乗数);
最後に拡散反射光と鏡面反射光を足すことで光の反射を表現する
float4 PSMain(SPSIn psIn) : SV_Target0
{
....
// 拡散反射光を求める
float3 diffuseLig = ligColor * t;
// 1. ライトが面に入射して、反射したベクトルを求める
float3 refVec = reflect(ligDirection, psIn.normal);
//2. 光が入射した面から視点に向かって伸びるベクトルを求める
float3 toEye = eyePos - psIn.worldPos;
toEye = normalize(toEye);
// 1と2で求めたベクトルの内積を使って、鏡面反射の強さを求める
t = dot(refVec, toEye);
// 内積の結果が0以下なら0にする
if (t < 0.0f)
{
t = 0.0f;
}
// step-7 鏡面反射の強さを絞る
t = pow(t, 5.0f);
// 4. 3で求めた鏡面反射の強さを絞って、最終的な反射の強さを求める
float3 specularLig = ligColor * t;
// 拡散反射光と鏡面反射光を足し算して、最終的な光を求める
float3 lig = diffuseLig + specularLig;
// テクスチャからカラーをフェッチする
float4 finalColor = g_texture.Sample(g_sampler, psIn.uv);
// テクスチャカラーに求めた光を乗算して最終出力カラーを求める
finalColor.xyz *= lig;
return finalColor;
}
環境光
地面や壁などを反射して物体に当たっている光。
拡散反射光や鏡面反射光の光の強さに対して、何かしらの適当な値を計算する。
一律で光の底上げを行う。
Chapter5 ライティング発展
ポイントライト
ポイントライトは位置情報を持っているライト。
ディレクションライト同様に、Lambert拡散反射とPhong鏡面反射で表すことができる。しかし、ディレクションライトと違い以下の2点に気を付ける。
- 入射してくる光の方向
- 光源との距離による光の減衰
入射してくる光の方向
面に入射する光の方向は、面のワールド座標からポイントライトの座標を引いて、結果を正規化することで求められる。
// 面のワールド座標 - ポイントライトの座標
float3 ligDir = surface.worldPosition - pointLight.position;
// 正規化する
ligDir = normalize(ligDir);
光源との距離による光の減衰
ポイントライトの光の影響は、光源からの距離に応じて減衰する。そのため、距離に比例して光の影響が0になる。
ポイントライトと面の距離をD、ポイントライトの影響範囲をRとするとき、影響力をAとする。
// 面とポイントライトの距離
float D = length(surface.worldPosition - pointLight.position);
// 影響力を計算する
float A = 1.0f - 1.0f / pointLight.range * D;
// 影響力がマイナスになる場合(範囲を超えている)
if (A < 0.0f)
A = 0.0f;
// 2乗することで影響力の変化を指数関数的にする
A = pow(A, 2.0f);
影響力を2乗することで、強さが真っ直ぐな線形から指数関数的な変化になる。
ポイントライトの実装は以下のような流れで表す
- ポイントライトの座標と面のワールド座標で入社してくる光の方向を求める
- 1で求めた光の方向を使って、Lambert拡散反射光と鏡面反射光を求める
- ポイントライトと面との距離を使って、ライトの影響力を求める
- 3で求めたライトの影響力を拡散反射光と鏡面反射光に乗算して、最終的な反射光を求める
スポットライト
ポイントライトとほとんど同じ。ポイントライトに光の放射方向と放射角度が追加されたもの。
以下の流れで表す
- スポットライトの位置を光源とみなして、ポイントライトの計算をする
- スポットライトの位置から面に向かって伸びるベクトルを計算する
- 求めたベクトルとスポットライトの放射方向とに内積を使って、角度を求める
- 求めた角度を使って、ライトの影響力を計算する
2. スポットライトの位置から面に向かって伸びるベクトルを計算する
面のワールド座標からスポットライトの座標を引くことで求められる。また、向きが欲しいので正規化する
3. 求めたベクトルとスポットライトの放射方向とに内積を使って、角度を求める
この角度は、内積の以下の性質を利用することで求めることができる。
内積を計算し、そのベクトルのなす角度の余弦の値をacos()という関数に渡すことで、角度を求めることができる。
// ベクトルv1, v2の内積を求める
float t = dot(v1, v2);
// 角度を求める
float angle = acos(t);
4. 求めた角度を使って、ライトの影響力を計算する
3で求めた角度を使って、ライトの影響力を減衰させる。ポイントライトの考え方と同じで、角度が大きくなるに従い、ライトの影響力を下げることで実現する。
リムライト
逆光ライトとも呼ばれる。輪郭が光る。
リムライトは以下の2つを考える
- 面の法線と光の入射方向
- 面の法線と視線の方向
1. 面の法線と光の入射方向
リムライトは後ろから光が当たった時に輪郭が光るので、光の向きと、面の法線が垂直に近い箇所で強く光る。つまり、リムライトの強さは、光の向きと面の法線との内積を利用することで求めることができる。それは内積には以下の性質があるため。
光の向きと面の法線が垂直(90度)に近づくほどリムライトの影響を強くするので、以下のような計算で求める
ライトの方向と法線の結果が0(90度)の場合1になる
2. 面の法線と視線の方向
見る方向によってもリムライトの強さは変わってくる。リムライトは面の法線と視線がなす角度が90度に近いほど強くなる性質がある。これも面の法線と光の入射方向の計算と似た求め方で出すことができる。
視線の方向が光の入射方向と逆向きなので-1を乗算する
リムライトの強さを乗算する
リムライトの強さに影響を与える2つの要素を計算したら、その2つを乗算することで、最終的なリムライトの強さを求めることができる。
半球ライト
地面の色(地面から反射した光)と天求色(空の色)を考慮したライト。物体の面の法線と地面の内積を計算するだけのシンプルな計算で表すことができる。そのため、スペックの低い環境などでよく使われた。
半球ライトの計算
半球ライトの計算を行うには、地面の色、空の色、地面の法線、面の法線が必要となる。
struct HemisphereLight
{
Vector3 skyColor;
Vector3 groundColor;
Vector3 groundNormal;
}
半球ライトは地面の法線と面の法線の内積で表す。面が空を向いている場合は1、地面のほうを向いている場合は-1となる。
この計算の結果を利用して、地面の色と空の色を線形補間する。ただし、線形補間で使用する値は0.0 ~ 1.0になっている必要があるので、内積の結果に1.0を加算して2で割ることでその範囲に収まるように変換する。
0.0 ~ 1.0に変換された値を利用して、半球ライトを求める。
float t = (内積の結果 + 1.0) / 2.0
float3 hemiLight = groundColor * (1.0f - t) + skyColor * t
// 上と同じ lerp(groundColor, skyColor, t);
Chapter6 さまざまなテクスチャの利用
さまざまなテクスチャ
ディフューズマップ:物体の模様を表すテクスチャ
法線マップ:凸凹を表現するテクスチャ
ハイトマップ:高さを表すテクスチャ
透明度マップ:透明度を表すテクスチャ
法線マップ
法線マップには種類があり、オブジェクトスペース法線マップとタンジェントスペース法線マップと呼ばれるものがある。
現在の主流タンジェントスペース法線マップで、よく見かける法線マップはこれになる。
タンジェントスペース法線マップ
「法線マップを貼り付けるポリゴンの法線座標系」から見た法線が書き込まれている。
- ポリゴンの法線ベクトル
- ポリゴンの法線と直行している接ベクトル(タンジェント)
- 法線と接ベクトルの外積で求めた従法線ベクトル
法線Nが(0.707, 0.0, 0.707)というベクトルだった場合、同じ向きのベクトルをタンジェントスペースに変換すると(0, 0, 1)になる。これをテクスチャのRGBに変換すると(0, 0, 255)のよく見かける青いカラーになる。
法線空間からワールド空間への変換
ライティングを行うには、法線をワールド空間に変換する必要がある。
ワールド空間に変換するためには、法線空間の基底ベクトルが必要になる。法線空間の基底ベクトルexが接ベクトル、eyが従法線、ezがサーフェイスの法線となる。
基底ベクトルを利用して、法線をワールド空間に変換する
// 法線マップから法線をサンプリング
float3 normalLocal = normalMap.Sample(sampler.In.uv).xyz;
// 法線マップに書き込まれている法線は0.0 ~ 1.0なので、-1.0 ~ 1.0に変換する
normalLocal = (normal - 0.5f) * 2.0f;
// ワールド空間の法線を計算する
// 接ベクトルはtangent, 従法線はbinormal
// サーフェスの法線はnormalに記録されているものとする
float3 normalWorld = normalLocal.x * psIn.tangent + normalLocal.y * psIn.binormal + normalLocal.z * psIn.normal;
スペキュラマップ
金属のような質感を表現するための「鏡面反射」の強さを書き込んだテクスチャ。
スペキュラマップはグレースケールのテクスチャになっていることが多く、白(255, 255, 255)に近づくほど反射が強くなる。
// 鏡面反射の強さをサンプリングする
float specPower = g_specularMap.Sample(g_sampler, psIn.uv).r;
rチャンネルに鏡面反射の強さが書き込まれているため、rチャンネルから取得する。
アンビエントオクルージョンマップ(AOマップ)
環境光の強さを表すテクスチャ。
環境光は光源からの光を反射するので、負荷の重い処理になる。そのため、環境光の強さを描いたテクスチャを用意し、それっぽく見せる手法が活用される。
// AOマップから環境光の強さをサンプリングする
float ambientPower = g_aoMap.Sample(g_sampler, psIn.uv);
Chapter7 PBR(物理ベースレンダリング)
... ちょっと難しかったので後回し
Chapter8 2D描画の基礎
基本的には3D描画と同じ。3D描画の機能を使って板状のポリゴンを描画し、それを2Dのように見せている。
αブレンディング
ブレンドステートというデータを使って、ピクセルシェーダーで計算されたカラーをカラーバッファにどのように書き込むかを指定する。
例えば、地面を描画してからキャラクターを描画すると、地面のカラーはキャラクターに上書きされてしまう。それをもし「ガラス」などの半透明のオブジェクトで表現するためには、上書きではなく、混ぜ合わせる必要がある。その方法の一つにα値を用いる方法がある。それがαブレンディング。代表的な例として半透明合成と加算合成がある。
また、ブレンディングでは、これから書き込むカラーをソースカラー、すでに書き込まれているカラーをデスティネーションカラーという。
半透明合成
ソースカラーとデスティネーションカラーを混ぜ合わせることで実現する。
ソースカラー(RGB)を
Chapter9 発展的な2D描画
場面転換などで利用されるワイプ演出と、画像加工について。
リニアワイプ
画像が左から右へ消えていく演出。
ワイプのサイズを表すパラメーターをシェーダーに拡張定数バッファーとして渡す。これにより、シェーダー側でテクスチャの操作が可能になる。
clip()関数でワイプ処理を実装する。
clip(In.pos.x - wipeSize);
clip()関数は引数で渡される値がマイナスになるとピクセルキル(ピクセルシェーダーの結果が出力されないように)してくれる関数。
例えばwipeSizeが50の場合、X座標が50以下のピクセルは(x - wipeSize)でマイナスになるので、描画されなくなる。
方向を指定するリニアワイプ
方向をもつパラメーターをワイプサイズと同様にシェーダー渡す。
float t = dot(wipeDirection, In.pos.xy);
clip(t - wipeSize);
変数tは「wipeDirectionの無限線分上にIn.pos.xyを射影した長さ」が格納される。
これは、ある2つのベクトルの内積の結果は、片方のベクトルが単位ベクトルであれば、その単位ベクトルの無限線分上に垂線を落として射影した長さになるという性質を利用している。
円形ワイプ
基本的にはリニアワイプと同じ。
float2 center = float2(640.f, 360.0f);
float2 posFromCenter = In.pos.xy - center;
clip(length(posFromCenter) - wipeSize);
posFromCenter
は各ピクセルの画面中心からの相対位置ベクトルを取得する。それをlength(posFromCenter)
で処理することで、中心からの距離が分かる。
縦じまワイプ・横じまワイプ
基本的にはリニアワイプと同じ。
ピクセルのx座標を割った余り - ワイプサイズで実装。
float t = (int) fmod(In.pos.x, 64.0f);
clip(t - wipeSize);
fmod(x, y)関数
はxをyで割った余りを返す。
今回はピクセルのx座標を64で割った余りを求め、その値を利用してピクセルキルをしている。
wipeSizeが64になると全てのピクセルが描画されなくなる。
横じまワイプはfmodで割る値をピクセルのy座標に変更することで実装できる。
モノクロ加工・セピア調加工・ネガポジ反転
基本的には、それぞれの画像の色変化をそれぞれのアルゴリズムで用意し、それをlerp()関数
で線形補完で行う。
color.xyz = lerp(color, monochromeColor, monochromeRate);
Chapter10 ポストエフェクト
ポストエフェクトとは、レンダリングした絵に対してレタッチを行って、エフェクトを追加していく処理のこと。
オフスクリーンレンダリング
画面に表示されないレンダリングのこと。画面に直接描画するレンダリングをオンスクリーンレンダリングという。
ポストエフェクトの流れ
一度レンダリングした絵に対してエフェクトをかけるには、レンダリングした絵をテクスチャ化する必要がある。ポストエフェクトをかけるには、カラーバッファーに直接レンダリングするのではなく、オフスクリーンにレンダリングを行う。
ブルーム
オブジェクトの光があふれ出して周囲に影響を与える現象。
通常のカラー値の範囲は0 ~ 255の256段階あり、これの範囲を広げて光を扱うレンダリングをHDR(High Dynamic Range)レンダリングという。そのため、浮動小数点テクスチャをカラーバッファーとして利用する。
RenderTarget mainRenderTarget;
mainRenderTarget.Create(
1280,
720,
1,
1,
DXGI_FORMAT_R32G32B32A32_FLOAT, // カラーバッファーのフォーマットをHDRの32bit浮動小数点に
DXGI_FORMAT_D32_FLOAT
);
ブルームのアルゴリズム
- シーンをメインレンダリングターゲットにレンダリング
- シーンの輝度をテクスチャとして抽出
- 抽出した輝度テクスチャにブラーをかけてぼかす
- ぼかした輝度テクスチャをメインレンダリングターゲットに加算合成
※ピクセルシェーダーで、特定の明るさ(1以下とか)のピクセルをピクセルキルすることで、通常の絵とぼかした輝度テクスチャを加算合成する。
川瀬式ブルームフィルターというものもある。
被写界深度
被写界深度(Depth of Field : DoF)とは、リアルタイムCGの分野ではカメラのピンボケ現象全般を指す。
DoFの実装は、Z値(カメラ空間の深度値)が被写界深度内の範囲外にあるピクセルにぼかしをかけていって、それをシーンに合成する。そのため、カメラからみたZ値をテクスチャ化する必要がある。
- シーンをメインレンダリングターゲットに描画
- 深度値記録用のレンダリングターゲットにカメラ空間でのZ値を描画
- メインレンダリングターゲットのシーンテクスチャにブラーをかけて、ボケ画像を作成
- ボケ画像とシーンの深度テクスチャを利用して、ボケ画像をメインレンダリングターゲットに合成
Chapter11 シャドウィング
投影シャドウ
影描画の基本となる、影生成アルゴリズム。
シャドウマップとよばれるテクスチャを利用して影を落とすアルゴリズム。
シャドウマップはオフスクリーンレンダリングを行い、ライトをカメラと見立て、影を生成したいオブジェクトをシャドウマップに対してレンダリングする。
- シャドウマップ描画用レンダリングターゲットを作成する
- 影描画用のライトカメラを作成する
- シャドウマップ描画用のモデルを用意する
- 影を生成したいモデルをシャドウマップに描画する
- シャドウマップ描画用のピクセルシェーダーを作成する
デプスシャドウ
投影シャドウには以下のような欠点がある。
- 影が落ちないはずの場所に落ちてしまう
- セルフシャドウが行えない
デプスシャドウは投影シャドウを発展させ、それら問題を解決したもの。
基本的には、シャドウマップを作成し、それを利用して影を落とすという考え方。投影シャドウと違うのは、シャドウマップに書き込む値がグレースケールではなく、ライトスクリーン空間のZ値ということ。Z値(深度値)なので、デプスシャドウと呼ばれる。
PCF(Percentage Closer Filtering)
現実の影で生じる輪郭の柔らかい影を指すソフトシャドウを簡単に表現する実装方法。
基本的にはデプスシャドウと同じで、影が落とされるモデルを描画する際のピクセルシェーダーが違う。デプスシャドウはシャドウマップから深度値をサンプリングし、そのピクセルが光源から遮蔽されているかどうかを判断していた。PCFでは、深度値のサンプリングを1点だけでなく、複数点から行い、どれだけ遮蔽されているかを決定する。
VSM(Variance Shadow Maps)
PCFよりも品質の高いソフトシャドウを実現する、VSMと呼ばれる分散シャドウマップ。シャドウマップに書き込まれた深度値の局所的な分散を利用する。
VSMでは、シャドウマップを格子模様で分割し、それぞれの深度値の分散を調べていく。
「分散が大きい」とは、「テクセルの深度値の幅が広い」といえる。直接的にいうと、影の境界線が深度値の分散が大きい。
カスケードシャドウ
現在のハイエンドゲームの影生成方法は「カスケードシャドウ+ソフトシャドウ」がディファクトスタンダード。
これまで紹介したシャドウマップ法では、シャドウマップと影の投影面の解像度の違いでジャギーが発生する問題がある。シャドウマップに書き込まれたオブジェクトの影しか落とすことができないので、広範囲に落とそうとすると解像度が足りなくなり、ジャギーが発生してしまう。
カスケードシャドウマップも同様の問題が発生するが、実装難易度や品質のバランスが高く、そのためディファクトスタンダードになっている。
カスケードシャドウマップは、複数枚のシャドウマップを利用することで影の品質を向上している。例えば、近距離用、中距離用、遠距離用など、カメラからの距離に応じて使用するシャドウマップを変更する。そして、近距離の描画ほど高い解像度で狭いエリアに描画する。
- 分割エリアの定義
- 分割エリアを描画するためのライトビュープロジェクション行列の計算
- 複数枚のシャドウマップにシャドウキャスターモデルの描画
- 複数枚のシャドウマップを利用してシャドウレシーバーの描画
1の部分で、近距離、中距離、遠距離を定義する。
Chapter12 ディファードレンダリング
フォワードレンダリングとディファードレンダリング
フォワードレンダリングを端的に説明すると、ポリゴンをレンダリングするときにライティングの計算を行うと言える。モデルを描画する際に最終的なピクセルカラーを決め、そのタイミングでライティング処理を行う。
ディファードレンダリングは現在の主流になっているレンダリング手法。こちらはポリゴンをレンダリングするときにライティングの計算を行わず、そのあとに行う手法。遅延レンダリングとも呼ばれる。
ディファードレンダリングではMRT Multi Rendering Targetを活用して、G-Bufferと呼ばれる複数枚のテクスチャにテクスチャカラー、法線情報、スペキュラ強度、深度値などを書き込む。つまり、ディファードレンダリングでは、ライティングに必要な情報をG-Bufferに書き込むということ。
Chapter13 ディファードレンダリングとフォワードレンダリングの融合
現在主流になっているディファードレンダリングだが、それだけではなく、フォワードレンダリングも利用したレンダリング方法が活用されている。
半透明問題
ディファードレンダリングは、半透明オブジェクトを描画する際にG-Bufferの内容が上書きされてしまうため、半透明の描画が不向きである。
G-Bufferは手前に描画されたピクセルの情報で上書きされてしまうので、奥のピクセル情報が失われる。
この問題を解決する簡単な方法は、不透明オブジェクト(奥のオブジェクト)をディファードレンダリング、半透明オブジェクト(手前のオブジェクト)をフォワードレンダリングで描画する方法。
- 不透明オブジェクトをG-Bufferにレンダリングする
- G-Bufferの内容でディファードレンダリングでライティングする
- 半透明オブジェクトをフォワードレンダリングで描画する
一旦ここまで。
残りの15. コンピュートシェーダー、16. TBR、17. レイトレーシングは内容としてはシェーダーの応用だと思うので、基礎をChapter13までをしっかり理解することを優先する。
Chapter14は本書で用意されている描画エンジンについての説明なので割愛。(参考になるので読みはする)