こんにちわ、オオバです。

オブジェクトの影を落とす時、ShadowCasterを設定する、または、設定されているシェーダー(Standardシェーダー、Diffuseシェーダーなど)を使うことがUnityでは最もお手軽な方法だと思います。

影の描画って実際どうなっているかを調べました。

📝 目次

デプスシャドウ技法

UnityのShadowCasterを設定した場合に落ちる影は、デプスシャドウ技法と呼ばれる方法で実装されています。
デプスシャドウ技法とは、ライトから見たDepthTextureを予め生成し、ピクセルを描画する際、その「ピクセルとライト間の距離」と「DepthTextureの深度」を比べて影かどうかを判定する、現状メジャーな影描画方法です。

処理の流れ

  1. ライトから見たDepthTextureを生成してシェーダーに渡す
  2. ライトから見た射影変換行列をシェーダーに渡す
  3. ライトから見た射影変換行列で変換した座標をフラグメントシェーダーに渡す
  4. フラグメントシェーダー内でデプス値を比較して影描画

1.ライトから見たDepthTextureを生成してシェーダーに渡す

まずここでつまづきました。
UnityでCameraコンポーネントを使わずにDepthTextureを生成する方法 が分かりませんでした。
今回はデプスシャドウ技法の概念が分かれば良いので、細かいことは気にせず、カメラを無駄に1つ使って対策します。

デプステクスチャ生成にはCameraコンポーネントが必須です
※Unite2017でUnityの中の人に教えてもらいました

  1. DepthTextureフォーマットのRenderTextureを予め生成し、今回作成するShadowMapシェーダーのMaterialに予めセット
  2. ライトから見たDepthTexture生成用のCameraコンポーネントをライトのGameObjectにアタッチ
  3. ライト視点のCameraコンポーネントのTargetTextureに先のRenderTextureをセット

という手順を踏み、RenderTextureにライトから見たDepthを書き込みます。

Unity デプスシャドウ技法を自前で書いて影を落としてみる_53

※画面左上にuGUIでDepthTextureをデバッグ表示

2.ライトから見た射影変換行列をシェーダーに渡す

var lightVMatrix = cam.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);  

変数camはライトにくっつけている、ライト視点のカメラです。
このカメラを使ってライトから見た射影変換行列lightVPを作ります。

このあとDepthTextureをオブジェクトに投影し影を描画するわけですが、ここで工夫が必要です。頂点シェーダー内でlightVPと各頂点を乗算してクリッピング座標系に変換するわけですが、クリッピング座標系X軸とY軸の範囲は[-1.0 ~ 1.0] です。DepthTextureをオブジェクトに投影するには、[0.0 ~ 1.0]のUV座標系に変換しなければ、きれいに投影できません。
その補正をかける行列biasMat(サイズを半分にして、0.5正の方向へずらす変換行列)を予め乗算しておきます。

この時のDepthTextureをオブジェクトに投影するとこんな感じになります。

Unity デプスシャドウ技法を自前で書いて影を落としてみる_82

3.ライトから見た射影変換行列で変換した座標をフラグメントシェーダーに渡す

o.shadowVertex = mul(_LightVP, v.vertex);  

頂点シェーダーでライトから見た射影変換行列で座標変換し、予め定義したshadowVertexに代入してフラグメントシェーダーへ渡します。

o.shadowVertexのxとy要素が、DepthTextureのUV座標になっていることが分かります。

4.フラグメントシェーダー内でデプス値を比較

float4 lightDepth = tex2D(_LightDepthTex, i.shadowVertex.xy);  

float diff = i.shadowVertex.z - lightDepth.r;  
if (diff >= 0)  
{
    shadowRatio = _ShadowValue;  
}

頂点シェーダーから渡ってきたshadowVertex変数のxとyが、ライトから見たDepthTextureのUV座標になっているため、そこから算出したDepthTextureの深度(lightDepth)と、ライト空の距離(i.shadowVertex.z)を比較します。

比較した結果DepthTextureの値が小さい場合は、ライトとオブジェクトの間に遮蔽物が存在するということなので影ということになり、影を描画します。

まとめ

影の描画って難しかったです。
ボケ足のきれいな影とか作りたいと思っているのですが、ここからどうアプローチするか今後の課題です。

最後に本記事のサンプルコードを。
ShadowMap.cs · GitHub

👉 オススメ記事

2021秋 Asset Refreshセール
100以上のアセットがなんと50%OFF!!オオバもいくつか買いました!
期間 : 10月2日午後3時59分まで