URPでVolumeShadow

2024/08/23 : 初稿
Unity : 2022.3.21f1
URP : 14.0.10
マシン : M1Mac(Metal)

# やりたいこと

https://qiita.com/kochiyukimoto/items/1d092a885cd31620f2a7
以前書いた「窓から差し込む光」↑とは逆に、影になる空間を暗くする。
ポストエフェクトで。

![スクリーンショット 2024-08-23 13.08.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072360/f74ec09c-0cd6-7cd8-ab3d-801b7b093fd8.png)

今回も同じくとにかく動かすことを目標に。
マルチプラットフォームとか処理負荷、実用性は置いときます。

# 実現方法

考え方は前回とほぼ同じなので省略。

前回は自前で光源から見たDepthをレンダリングしましたが、今回はシャドウマップを使ってみます。

# 実装

ポストエフェクト用のシェーダーはこちら。こいつで画面全体に対して描画します。

UniversalRenderPipelineAssetでDepthTextureにチェックを入れておくことを忘れずに。

いつの間にか_MAIN_LIGHT_SHADOWSの#pragma指定方法が変わったらしいので注意。
```VolumeShadow.shader
Shader "ProjectMain/PostEffects/VolumeShadow"
{
    Properties
    {
        _SamplingCount("SamplingCount", Float) = 100
        _SamplingDisMin("SamplingDisMin", Float) = 0.1
        _SamplingDisMax("SamplingDisMax", Float) = 30
        _ShadowColor("ShadowColor", Color) = (0, 0, 0, 0.1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        
        Pass
        {
            ZTest Always
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
            //#pragma multi_compile _ _SHADOWS_SOFT

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 world : TEXCOORD1;
            };
            
            // Depth
            TEXTURE2D(_CameraDepthTexture);
            SAMPLER(sampler_CameraDepthTexture);

            // サンプリング
            float _SamplingCount;
            float _SamplingDisMax;
            float _SamplingDisMin;

            // 色
            float4 _ShadowColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                o.world = TransformObjectToWorld(v.vertex.xyz);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // メインカメラのdepth : Liner01/EyeDepthはnear-farではなく0-far
                float mainZDis = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv), _ZBufferParams);

                // カメラからのベクトル
                float3 vec = i.world - _WorldSpaceCameraPos;

                // カメラの視線ベクトル
                float3 camVec = GetViewForwardDir();

                // カメラからのベクトルをZ方向で正規化
                vec /= dot(camVec, vec);

                // メインカメラから指定距離までの間の座標をいくつかサンプリングして影になっているか調べる
                float addDis = (_SamplingDisMax - _SamplingDisMin) / _SamplingCount;
                float maxShadowAmount = 0;
                [loop]
                for (float l = 0; l < _SamplingCount; l++) {
                    // サンプリングするワールド座標
                    float dis = addDis * l + _SamplingDisMin;
                    if (dis >= mainZDis - 0.1) { // この先は不透明物体があるので不要
                        break;
                    }
                    float3 sampleWorldPos = _WorldSpaceCameraPos + vec * dis;

                    // そこのシャドウマップを取得
                    float4 shadowCoords = TransformWorldToShadowCoord(sampleWorldPos);
                    if (shadowCoords.w < 0.0001) { // なんか0除算のwarning出るので応急処置
                        shadowCoords.w = 0.0001;
                    }
                    float shadowAmount = 1 - MainLightRealtimeShadow(shadowCoords);
                    maxShadowAmount = max(maxShadowAmount, shadowAmount);
                }

                // 暗くする
                return float4(_ShadowColor.rgb, _ShadowColor.a * maxShadowAmount);
            }
            ENDHLSL
        }
    }
}
```
このシェーダーを使ったマテリアルを作成してMainMaterialとし、適当なScriptableRendererFeature継承クラスで以下のように呼び出します。
```VolumeShadowRendererFeature.cs
Material MainMaterial; // 上記のシェーダー(VolumeShadow.shader)を用いたマテリアル
Mesh QuadMesh; // 後述の関数で作成しておきます。

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    // SceneViewはスキップ
    if (renderingData.cameraData.isSceneViewCamera) {
        return;
    }

    // 処理すべきタイミングか確認
    if (renderPassEvent != RenderPassEvent.BeforeRenderingPostProcessing) {
        return;
    }

    // 処理対象のカメラか確認
    var cam = renderingData.cameraData.camera;
    if (cam != /*処理したいカメラ*/) {
        return;
    }

    // コマンドバッファ取得
    var cmd = CommandBufferPool.Get("VolumeShadow");
    
    // カメラ全体を覆うようにマトリクス設定
    var w = cam.pixelWidth;
    var h = cam.pixelHeight;
    var n = cam.nearClipPlane;
    var f = cam.farClipPlane;
    var halfFovY = cam.fieldOfView * 0.5f;
    var tanY = Mathf.Tan(Mathf.Deg2Rad * halfFovY);
    var tanX = tanY * w / h;
    var z = (n + f) * 0.5f;
    var halfW = z * tanX;
    var halfH = z * tanY;
    var camTrans = cam.transform;
    var toWorld = camTrans.localToWorldMatrix;
    var center = toWorld.MultiplyPoint(Vector3.forward * z);
    var matrix = Matrix4x4.TRS(center, camTrans.rotation, new Vector3(halfW, halfH, 1));

    // 画面全体の描画
    cmd.DrawMesh(QuadMesh, matrix, MainMaterial);
    
    // CommandBuffer実行
    context.ExecuteCommandBuffer(cmd);

    // CommandBuffer解放
    CommandBufferPool.Release(cmd);
}
```
QuadMeshは下記のコードで作成しておきます(Unityに内蔵されているQuadのメッシュでもいいかも)。
```CreateQuadMesh.cs
public static Mesh CreateQuadMesh()
{
    // mesh
    var mesh = new Mesh();

    // 頂点
    Vector3[] vertices = new Vector3[]
    {
        new Vector3(-1, -1, 0),
        new Vector3( 1, -1, 0),
        new Vector3(-1,  1, 0),
        new Vector3( 1,  1, 0),
    };
    Vector2[] uvs = new Vector2[]
    {
        new Vector2(0, 0),
        new Vector2(1, 0),
        new Vector2(0, 1),
        new Vector2(1, 1),
    };
    int[] tris = new int[]
    {
        0, 2, 1,
        1, 2, 3,
    };

    // メッシュ構築
    mesh.vertices = vertices;
    mesh.uv = uvs;
    mesh.triangles = tris;
    mesh.RecalculateNormals();
    mesh.RecalculateTangents();
    mesh.RecalculateBounds();
    return mesh;
}
```
# 実行負荷について

特にShadowMapのサンプリングが重いようです。

対処としては_SamplingCountを落とすのが初手ですが、クオリティが落ちてしまいます。

他にはこんな感じですかね。
- 直接フレームバッファに描画するのではなく、小さいサイズのテクスチャにレンダリングしてからフレームバッファに合成する
- ShadowMapを使うのを諦めて、自前で光源から見たdepthをテクスチャにレンダリングする