オブジェクトの影を落とす時、ShadowCasterを設定する、または、設定されているシェーダ(Standardシェーダ、Diffuseシェーダなど)を使うことがUnityでは最もお手軽な方法だと思います。
影の描画って実際どうなっているかを調べました。
デプスシャドウ技法
UnityのShadowCasterを設定した場合に落ちる影は、デプスシャドウ技法と呼ばれる方法で実装されています。 デプスシャドウ技法とは、ライトから見たDepthTextureを予め生成し、ピクセルを描画する際、その「ピクセルとライト間の距離」と「DepthTextureの深度」を比べて影かどうかを判定する、現状メジャーな影描画方法です。
処理の流れ
- ライトから見たDepthTextureを生成してシェーダに渡す
- ライトから見た射影変換行列をシェーダに渡す
- ライトから見た射影変換行列で変換した座標をフラグメントシェーダに渡す
- フラグメントシェーダ内でデプス値を比較して影描画
1.ライトから見たDepthTextureを生成してシェーダに渡す
まずここでつまづきました。
UnityでCameraコンポーネントを使わずにDepthTextureを生成する方法 が分かりませんでした。
今回はデプスシャドウ技法の概念が分かれば良いので、細かいことは気にせず、カメラを無駄に1つ使って対策します。
デプステクスチャ生成にはCameraコンポーネントが必須です
※Unite2017でUnityの中の人に教えてもらいました
- DepthTextureフォーマットのRenderTextureを予め生成し、今回作成するShadowMapシェーダのMaterialに予めセット
- ライトから見たDepthTexture生成用のCameraコンポーネントをライトのGameObjectにアタッチ
- ライト視点のCameraコンポーネントのTargetTextureに先のRenderTextureをセット
という手順を踏み、RenderTextureにライトから見たDepthを書き込みます。
※画面左上に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をオブジェクトに投影するとこんな感じになります。
DepthTexture投影※下記ShadowMap.shader#L58コメントアウトを外した状態
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の値が小さい場合は、ライトとオブジェクトの間に遮蔽物が存在するということなので影ということになり、影を描画します。
まとめ
影の描画って難しかったです。
ボケ足のきれいな影とか作りたいと思っているのですが、ここからどうアプローチするか今後の課題です。
最後に本記事のサンプルコードを。
- Unityシェーダのマクロとマルチコンパイル - 渋谷ほととぎす通信
- シェーダプロパティアクセスが2.5倍早くなるPropertyToID関数 - 渋谷ほととぎす通信
- GLSL SandboxをUnityに移植する方法その1 - 渋谷ほととぎす通信
今週のお題「ゴールデンウィーク2017」として書かせてもらいました。