渋谷ほととぎす通信

「Unityをわかりやすく」初心者のためのゲーム作りブログ

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

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

こんにちは、Unityエンジニアのオオバです。

光のあたったオブジェクトは地面に影を落とします。
当たり前の現象ですが、この当たり前をUnityは
ShadowCasterと呼ばれる機能で簡単に実装できます。

ShadowCasterに頼らず、
自前で影を描画して調べました。

👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

Unityの影はデプスシャドウ技法を使っている

UnityのShadowCasterを設定した場合に落ちる影は、
デプスシャドウ技法 と呼ばれる方法で実装されています。

デプスシャドウ技法 とは、
ライトから見た情報を使った技法です。

ライトから見た情報とは、
ライトから見た深度情報(Zバッファ) です。
(以下:ライト深度バッファと呼ぶ)

ライト深度バッファと描画するピクセルの距離を比較します。
ライト深度バッファに書き込まれた深度と、
描画するピクセルからライトまでの距離が一致する場合は
光があたっているということです。

つまり影ではない。

逆に距離が一致しない場合は影だということ。

執筆時点でメジャーな描画方法です。

ここから実装の解説に入ります。
ライト深度バッファはテクスチャ(ライト深度テクスチャ)に
書き込んで処理しています。

処理の流れ
  1. 生成したライト深度テクスチャをシェーダーに渡す
  2. ライト視点射影変換行列をシェーダーに渡す
  3. ライト視点射影変換行列で変換した座標をフラグメントシェーダーに渡す
  4. フラグメントシェーダー内で深度値を比較して影描画

1.生成したライト深度テクスチャをシェーダーに渡す

深度テクスチャ生成にはCameraコンポーネントが必須です。

※Unite2017でUnityの中の人に教えてもらいました
デプスシャドウ技法の理解が大事なのでカメラを無駄に1つ使っています。

まずはRenderTexutureにライト深度バッファを書き込み、
ライト深度テクスチャを作る必要があります。

以下手順です。

  • ライト深度テクスチャ生成用のCameraをライトのGameObjectに追加(以降:ライトカメラ)
  • DepthTextureフォーマット のRenderTextureを予め生成
  • ライトカメラ のTargetTextureにRenderTextureをセット

このようにしてライト深度テクスチャを作詞しました。

画面左上にuGUIでDepthTextureをデバッグ表示したキャプチャ

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

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で↑の補正をかけています。

このタイミングのライト深度テクスチャを
オブジェクトに投影するとこんな感じです。

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

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

オススメ記事