Somehow referencing a spotlight shadow map in a fullscreen shader

Hi there, I’m working on a volumetric fog effect for a demo. I’m looking to replicate the effect of a torch shining through fog so the only light-source in my scene is a single spotlight.

I’m using a full-screen URP RenderFeature to achieve this and so far I’ve been able to cast rays from the camera to determine the thickness of the spotlight cone, taking into account occlusion by solid objects.

Next I need to march through the volume to accumulate light/shadow but to do this I need to be able to access the shadow map generated by the spotlight. I gather I’d be able to do this with the AdditionalLightRealtimeShadow() method if this were a material shader, but I’ve read that full-screen shaders don’t have access to additional light data.

So I’m looking for a way to somehow generate or reference the shadow map of the spotlight inside the shader. The best I can think of doing is slapping a camera on the spotlight, getting it to render a depth texture and passing that to the fullscreen shader, but I’m not even sure of the best way to do that.

If it’s relevant I’m using HLSL rather than CG. Any help would be super appreciated I’ve been stuck on this forever!

Here’s the code that generates the image so far:

Shader "Custom/VolumetricFog"
{
    Properties
    {
    }
    SubShader
    {
        Tags 
        { 
            "RenderPipeline" = "UniversalPipeline" 
            "LightMode" = "UniversalForward" 
        }

        Pass
        {
            Name "Volumetric Fog"

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

            float4 _SpotLightPos;
            float4 _SpotLightDir;
            float _SpotLightAngle;

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
            };

            // Ray-cone intersection method thanks to: 
            // https://lousodrome.net/blog/light/2017/01/03/intersection-of-a-ray-and-a-cone/
            //
            // Treats being inside the cone as non-intersecting
            //
            // Returns (entryDist, exitDist):
            //  Ray doesn't enter: entryDist = INF
            //  Ray doesn't exit: exitDist = INF
            float2 RayConeIntersection(float3 rayPos, float3 rayDir, float3 conePos, float3 coneDir, float coneAngle)
            {
                const float INF = 1.0 / 0.0;

                float3 coneToRay = rayPos - conePos;

                float x = cos(coneAngle / 2);
                float y = dot(rayDir, coneDir);
                float z = dot(coneToRay, coneDir);

                float a = y * y - x * x;
                float b = y * z - dot(coneToRay, rayDir) * x * x;
                float c = z * z - dot(coneToRay, coneToRay) * x * x;

                float d = b * b - a * c;

                // No intersections
                if (d < 0) return float2(INF, INF);
                
                float t1 = (-b + sqrt(d)) / a;
                float t2 = (-b - sqrt(d)) / a;

                if (t1 < 0 || dot(coneToRay + t1 * rayDir, coneDir) < 0) t1 = INF;
                if (t2 < 0 || dot(coneToRay + t2 * rayDir, coneDir) < 0) t2 = INF;
                
                return float2(t1, t2);
            }

            Varyings vert (Attributes IN)
            {
                Varyings OUT;
                OUT.positionCS = IN.positionOS - 0.5;
                OUT.uv = float2(IN.uv.x, 1 - IN.uv.y);
                OUT.positionWS = ComputeWorldSpacePosition(OUT.uv, CAMERA_NEAR_PLANE, UNITY_MATRIX_I_VP);
                return OUT;
            }

            half4 frag (Varyings IN) : SV_Target
            {
                float3 rayPos = _WorldSpaceCameraPos;
                float3 rayDir = normalize(IN.positionWS - rayPos);
                float3 camFwd = mul(float3(0,0,-1), UNITY_MATRIX_V);

                float depth = SampleSceneDepth(IN.uv);
                float linearDepth = LinearEyeDepth(depth, _ZBufferParams);

                float2 intersectDists = RayConeIntersection(rayPos, rayDir, _SpotLightPos, _SpotLightDir, _SpotLightAngle * PI / 180);

                // Get the forward component of distances
                intersectDists *= dot(rayDir, camFwd);

                // Discard if pixel occluded
                if (intersectDists.x >= linearDepth) return half4(0, 0, 0, 1);
                
                float fogDist = min(intersectDists.y, linearDepth) - intersectDists.x;

                fogDist /= 100;

                return half4(fogDist, fogDist, fogDist, 1);
            }
            ENDHLSL
        }
    }
}

Ok so after a lot of experimenting and digging through other people’s similar projects (CristianQiu’s implementation), I found out that while the three-parameter function does NOT work:

AdditionalLightRealtimeShadow(int lightIndex, float3 positionWS, half3 lightDirection)

The five parameter version DOES work:

AdditionalLightRealtimeShadow(int lightIndex, float3 positionWS, half3 lightDirection, half4 shadowParams, ShadowSamplingData shadowSamplingData)

This is strange and I don’t really understand why, seeing as the former function just calls the latter anyway. It also only works if the camera is relatively close to the light-source, otherwise the shadows just disappear.

EDIT: Nevermind, the disappearing shadows were just a case of increasing the shadow max distance in the URP asset