はじめに
最近シャドウマップの実装を勉強しています。
元々カスケードシャドウの実装方法を勉強したかったのですがそもそもシャドウマップがどのように作成されるのかを理解しないとカスケードシャドウの実装を読めなかったりし、1からシャドウマップやシャドウについて勉強しています。
今回は比較的簡単にソフトシャドウを作ることのできる方法について勉強したので記事にしてみます。
そもそもシャドウマップを作る方法についてはこちら
の記事のソースコードを参考にしています。
こちらの記事を参考に作ったシャドウマップを改造してソフトシャドウにします。
ソースコード
ソフトシャドウについて
まず、地面に影を落とすためにはシャドウマップが必要です。
シャドウマップというのはこのようなテクスチャのことです。(見やすくなるようにLevelsを調整しています。)
Lightから見たオブジェクトを深度値としてテクスチャに保存しています。
この保存された深度値と描画するオブジェクトのカメラから見た深度値を比較することで地面に影を落としています。
詳しくはこちらの記事に書かれています。
この保存されているシャドウマップの情報を使うだけではパキっとしたドット感のあるハードシャドウになってしまいます。これをふわっとしたシャドウにするのがソフトシャドウです。
ぼかす方法
今回使う手法はPercentageCloserFiltering
という方法を使います。略すとPCF
となります。
シャドウマップをサンプリングする際にサンプリングする点とその周囲のピクセルを調べ、平均を取ります。
その平均を取ることでボケます。
平均を取ることで本当にボケるのか試してみます。
このようなテクスチャがあるとします。
自分のピクセルの周りの平均というのは青色の場合は赤色の領域になります。
この場合、(1+1+1+1+1+1+1+1+1)/9=1となり、白色です。
自分含めて周りが白いピクセルはこの水色範囲なのでここに関しては白色にしかなりません(1pxごと説明するのを省くためにこのように説明します。)
次にこのピクセルについて考えます。黒いマスが1つあるので(1+1+1+1+1+1+1+1+0)/9=0.888888889となり、薄い灰色になります。
次にこのピクセルについて考えます。黒いマスが2つあるので(1+1+1+1+1+1+1+0+0)/9=0.777777778となり、先ほどより少し濃い灰色になります。
さらに隣のピクセルも黒は2つしかなので同じになります。
この作業を続けていくと
float SampleCustomShadow_PCF(float depth, float2 shadowCoord, float2 texelSize)
{
static const int2 OFF[9] = {
int2(-1,-1), int2( 0,-1), int2( 1,-1),
int2(-1, 0), int2( 0, 0), int2( 1, 0),
int2(-1, 1), int2( 0, 1), int2( 1, 1)
};
float sum = 0.1;
[unroll]
for(int i = 0; i < 9; i++)
{
float2 uv = shadowCoord.xy + OFF[i] * texelSize;
// テクセル外はライトに当たってることにする
if (any(uv < 0) || any(uv > 1))
{
sum += 1;
continue;
}
float shadowMapDepth;
shadowMapDepth = SAMPLE_TEXTURE2D(_CharacterShadowMapTexture, sampler_CharacterShadowMapTexture, uv).r;
#if UNITY_REVERSED_Z
shadowMapDepth = 1.0 - shadowMapDepth;
#endif
// 比較する
sum += step(depth, shadowMapDepth);
}
return sum / 9.0; // 平均を取る
}
この作業をコードにするとこのようになります。
static const int2 OFF[9] = {
int2(-1,-1), int2( 0,-1), int2( 1,-1),
int2(-1, 0), int2( 0, 0), int2( 1, 0),
int2(-1, 1), int2( 0, 1), int2( 1, 1)
};
ここで自分のピクセルと調べたいその周囲の方向を決めています。
[unroll]
for(int i = 0; i < 9; i++)
平均を取るのでその数だけループさせます。
float2 uv = shadowCoord.xy + OFF[i] * texelSize;
OFF[9]
の値を直接使うとUVよりも大きい範囲を見に行ってしまうので、texelSizeを使い1px分だけその方向にずれたピクセルを見ます。
Unityの_テクスチャ名_TexelSize
のx,y要素には1/width、1/heightが入っており、この値が1テクセル分の移動になります。(例:1/1024 = 0.0009765625)
あとはこのuvでシャドウマップを見ます。
sum += step(depth, shadowMapDepth);
}
return sum / 9.0;
平均を取るのでループの中で加算をし、ループの後に9で割ります。
終わりに
平均を取るだけでボケを作り、ソフトシャドウを作ることができました。
UnityのソフトシャドウでもPCFを使用していますが内部実装を確認したところ別の計算方法でぼかしているようでした。
詳しく説明している記事はこちらにありました。(中国の記事です)
TentFilterというフィルター方法を使用しているようです。