こんにちは、Unityエンジニアのオオバです。
光のあたったオブジェクトは地面に影を落とします。
当たり前の現象ですが、この当たり前をUnityは
ShadowCaster
と呼ばれる機能で簡単に実装できます。
ShadowCaster
に頼らず、
自前で影を描画して調べました。
👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!
Unityの影はデプスシャドウ技法を使っている
UnityのShadowCasterを設定した場合に落ちる影は、
デプスシャドウ技法 と呼ばれる方法で実装されています。
デプスシャドウ技法 とは、
ライトから見た情報
を使った技法です。
ライトから見た情報とは、
ライトから見た深度情報(Zバッファ) です。
(以下:ライト深度バッファと呼ぶ)
ライト深度バッファと描画するピクセルの距離を比較します。
ライト深度バッファに書き込まれた深度と、
描画するピクセルからライトまでの距離が一致する場合は
光があたっているということです。
つまり影ではない。
逆に距離が一致しない場合は影だということ。
執筆時点でメジャーな描画方法です。
ここから実装の解説に入ります。
ライト深度バッファはテクスチャ(ライト深度テクスチャ)に
書き込んで処理しています。
- 生成したライト深度テクスチャをシェーダーに渡す
ライト視点射影変換行列
をシェーダーに渡すライト視点射影変換行列
で変換した座標をフラグメントシェーダーに渡す- フラグメントシェーダー内で深度値を比較して影描画
1.生成したライト深度テクスチャをシェーダーに渡す
深度テクスチャ生成にはCameraコンポーネントが必須です。
まずはRenderTexutureにライト深度バッファを書き込み、
ライト深度テクスチャを作る必要があります。
以下手順です。
- ライト深度テクスチャ生成用のCameraをライトのGameObjectに追加(以降:ライトカメラ)
- DepthTextureフォーマット のRenderTextureを予め生成
- ライトカメラ のTargetTextureにRenderTextureをセット
このようにしてライト深度テクスチャを作詞しました。
2.,[object Object],をシェーダーに渡す
var lightVMatrix = lightCamera.worldToCameraMatrix;
var lightPMatrix = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false);
var lightVP = lightPMatrix * lightVMatrix;
// [-1, 1] => [0, 1]に補正する行列
var biasMat = new Matrix4x4();
biasMat.SetRow(0, new Vector4(0.5f, 0.0f, 0.0f, 0.5f));
biasMat.SetRow(1, new Vector4(0.0f, 0.5f, 0.0f, 0.5f));
biasMat.SetRow(2, new Vector4(0.0f, 0.0f, 0.5f, 0.5f));
biasMat.SetRow(3, new Vector4(0.0f, 0.0f, 0.0f, 1.0f));
// ライト視点射影変換行列をシェーダーに渡す
m_mat.SetMatrix("_LightVP", biasMat * lightVP);
ライトカメラ(変数lightCamera)を使って
ライト視点射影変換行列lightVPを作ります。
ライト深度テクスチャを使って影を描画するわけですが、ここで工夫が必要です。
頂点シェーダー内でlightVPと各頂点を乗算し、クリッピング座標に変換します。
クリッピング座標のX軸とY軸の範囲は[-1.0 ~ 1.0]。
ライト深度テクスチャをオブジェクトに投影するには、
[0.0 ~ 1.0]のUV座標系に変換しなければ、きれいに投影できません。
- サイズを半分
- 0.5正の方向へずらす
行列biasMatで↑の補正をかけています。
このタイミングのライト深度テクスチャを
オブジェクトに投影するとこんな感じです。
3.,[object Object],で変換した座標をフラグメントシェーダーに渡す
o.shadowVertex = mul(_LightVP, v.vertex);
頂点シェーダーでライト視点射影変換行列で座標変換し、
予め定義したshadowVertexに代入して
フラグメントシェーダーへ渡します。
o.shadowVertex
のxとy要素が、
ライト深度テクスチャののUV座標になります。
4.フラグメントシェーダー内でデプス値を比較
float4 lightDepth = tex2D(_LightDepthTex, i.shadowVertex.xy);
float diff = i.shadowVertex.z - lightDepth.r;
if (diff >= 0)
{
shadowRatio = _ShadowValue;
}
繰り返しになりますが、頂点シェーダーから渡ってきた
shadowVertex変数のxとyが、
ライト深度テクスチャのUV座標です。
算出したライト深度テクスチャの深度 lightDepth
と、
ライトからの距離 i.shadowVertex.z
を比較します。
比較した結果、深度の値が小さい場合は、
ライトとオブジェクトの間に遮蔽物が存在する
ということで影判断とになります。
まとめ : Unity デプスシャドウ技法を自前で書いて影を落としてみる
記事の内容を簡単にまとめます。
- Unityの影は、「ライト深度」と「描画ピクセルとライトとの距離」を比較するデプスシャドウ技法
- カメラを使わないと深度テクスチャは作れない
- ライトにカメラをセットしてライト深度テクスチャを作成
- 座標変換を自前で書く必要がある
こんな感じです。
影を表現したい場合は、
素直にUnityのShadowCasterをつけたら良いと思います。
影って大変ですね。
最後に本記事のサンプルコードを。
ShadowMap.cs · GitHub
この記事が気に入ったらフォローしよう