Hey all, I have a problem getting additional lights working for my volumetric fog shader. I have it working great with directional lights, but the additional light stuff seems to have some issues. How the system currently works is that I have a function that draws a sphere using ray marching at a point and with a certain radius. This was working great, so I began looking through the URP source code to find how additional lights are handled. I found the functions GetAdditionalLightsCount() and GetAdditionalLight() among several others that seemed promising. After running some tests though, I found an issue where I would only be able to get the light data at certain camera angles (See attached GIF). I have no idea why this is happening and I am starting to lose my mind lol. Thanks so much for any help. here is my code (ignore the class names, I haven't updated them yet... I was preoccupied with the issue lol): Code (HLSL): Shader "Hidden/Worlds End/VolumeSpotLights" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; struct SpotLight { float3 pos; float3 dir; float4 color; }; sampler2D _MainTex; float3 _Pos; float _Intensity; float _Scattering; float _Steps; float _JitterVolumetric; float _MaxDistance; float GetDepth(float2 uv) { #if UNITY_REVERSED_Z float depth = SampleSceneDepth(uv); #else // Adjust z to match NDC for OpenGL float depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uv)); #endif return depth; } //Unity already has a function that can reconstruct world space position from depth float3 GetWorldPos(float2 uv) { float depth = GetDepth(uv); return ComputeWorldSpacePosition(uv, depth, UNITY_MATRIX_I_VP); } float random( float2 p ) { return frac(sin(dot(p, float2(41, 289)))*45758.5453 )-0.5; } float random01( float2 p ) { return frac(sin(dot(p, float2(41, 289)))*45758.5453 ); } //from Ronja https://www.ronja-tutorials.com/post/047-invlerp_remap/ float invLerp(float from, float to, float value) { return (value - from) / (to - from); } float remap(float origFrom, float origTo, float targetFrom, float targetTo, float value) { float rel = invLerp(origFrom, origTo, value); return lerp(targetFrom, targetTo, rel); } float2 raySphere(float3 sphereCentre, float sphereRadius, float3 rayOrigin, float3 rayDir) { float3 offset = rayOrigin - sphereCentre; float a = 1; // Set to dot(rayDir, rayDir) if rayDir might not be normalized float b = 2 * dot(offset, rayDir); float c = dot (offset, offset) - sphereRadius * sphereRadius; float d = b * b - 4 * a * c; // Discriminant from quadratic formula // Number of intersections: 0 when d < 0; 1 when d = 0; 2 when d > 0 if (d > 0) { float s = sqrt(d); float dstToSphereNear = max(0, (-b - s) / (2 * a)); float dstToSphereFar = (-b + s) / (2 * a); // Ignore intersections that occur behind the ray if (dstToSphereFar >= 0) { return float2(dstToSphereNear, dstToSphereFar - dstToSphereNear); } } // Ray did not intersect sphere return float2(1.#INF, 0); } float ShadowAtten(int lightIndex, float3 samplePoint, half3 lightDir) { return AdditionalLightRealtimeShadow(lightIndex, TransformWorldToShadowCoord(samplePoint), lightDir); } float pointLightFogAtten(in float2 hitData, float3 lightPos, float lightRange, float3 samplePos, float intensity) { float dstToLight = length(samplePos - lightPos); // float accumFog = 1; if(dstToLight > lightRange) { dstToLight = lightRange; } float accumFog = exp(-dstToLight / (lightRange / 10)) * intensity; return accumFog; } float fogMarch(in float2 uv, in float3 spherePos, int index) { float3 worldPos = GetWorldPos(uv); //we find out our ray info, that depends on the distance to the camera float3 startPosition = _WorldSpaceCameraPos; float3 rayVector = worldPos - startPosition; float3 rayDirection = normalize(rayVector); float rayLength = length(rayVector); //Depth float nonlin_depth = SampleSceneDepth(uv); float depth = LinearEyeDepth(nonlin_depth, _ZBufferParams.y) * rayLength; //Sphere info float2 hitData = raySphere(spherePos, _Scattering, startPosition, rayDirection); float dstToSphere = hitData.x; float dstInsideSphere = hitData.y; float stepLength = dstInsideSphere / _Steps; float dstLimit = min(depth - dstToSphere, dstInsideSphere); float randomOffset = random01(uv) * stepLength * _JitterVolumetric / 100; float dstTravelled = randomOffset; float accumFog = 0; while(dstTravelled < dstLimit) { float3 samplePoint = startPosition + rayDirection * (dstToSphere + dstTravelled); float shadowAtten = ShadowAtten(index, samplePoint, 0); if(shadowAtten > 0) { accumFog += pointLightFogAtten(hitData, spherePos, _Scattering, samplePoint, _Intensity); } dstTravelled += stepLength; } //we need the average value, so we divide between the amount of samples accumFog /= _Steps; return accumFog; } v2f vert (appdata v) { v2f o; o.vertex = TransformWorldToHClip(v.vertex.xyz); o.uv = v.uv; return o; } float4 frag (v2f i) : SV_Target { float worldPos = GetWorldPos(i.uv); float4 col = tex2D(_MainTex, i.uv); float fog = 0; half3 color = half3(0, 0, 0); uint lightCount = GetAdditionalLightsCount(); for(uint l = 0; l < lightCount; l++) { Light light = GetAdditionalLight(l, worldPos, 1); color += light.color; fog += fogMarch(i.uv, _Pos, l); } return col + (fog * float4(color.r, color.g, color.b, 1)); } ENDHLSL } } } Code (CSharp): using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class VolumeSpotLights : ScriptableRendererFeature { public enum DownSample { off = 1, half = 2, third = 3, quarter = 4 }; [System.Serializable] public class Settings { public DownSample downsampling = DownSample.off; [Space(10)] [Header("Apperance")] public Color tint = Color.white; public float intensity = 1; public float scattering = 0; public Vector3 position; [Space(10)] [Header("Performance")] [Min(0)] public float steps = 24; public float maxDistance = 75; [Min(0)] public float jitter = 250; [Space(10)] [Header("Initialization")] public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; } public Settings settings = new Settings(); class Pass : ScriptableRenderPass { public Settings settings; private RenderTargetIdentifier source; RenderTargetHandle tempTexture; // RenderTargetHandle lowResDepthRT; Material material; private string profilerTag; public void Setup(RenderTargetIdentifier source) { this.source = source; } public Pass(string profilerTag) { this.profilerTag = profilerTag; } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { var original = cameraTextureDescriptor; int divider = (int)settings.downsampling; if (Camera.current != null) //This is necessary so it uses the proper resolution in the scene window { cameraTextureDescriptor.width = (int)Camera.current.pixelRect.width / divider; cameraTextureDescriptor.height = (int)Camera.current.pixelRect.height / divider; original.width = (int)Camera.current.pixelRect.width; original.height = (int)Camera.current.pixelRect.height; } else //regular game window { cameraTextureDescriptor.width /= divider; cameraTextureDescriptor.height /= divider; } //R8 has noticeable banding cameraTextureDescriptor.colorFormat = RenderTextureFormat.ARGB32; //we dont need to resolve AA in every single Blit cameraTextureDescriptor.msaaSamples = 1; //we need to assing a different id for every render texture // lowResDepthRT.id = 1; cmd.GetTemporaryRT(tempTexture.id, cameraTextureDescriptor); ConfigureTarget(tempTexture.Identifier()); // cmd.GetTemporaryRT(lowResDepthRT.id, cameraTextureDescriptor); // ConfigureTarget(lowResDepthRT.Identifier()); ConfigureClear(ClearFlag.All, Color.black); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd = CommandBufferPool.Get(profilerTag); cmd.Clear(); //it is very important that if something fails our code still calls //CommandBufferPool.Release(cmd) or we will have a HUGE memory leak try { material = new Material(Shader.Find("Hidden/Worlds End/VolumeSpotLights")); material.SetVector("_Pos", settings.position); material.SetFloat("_Scattering", settings.scattering); material.SetFloat("_Steps", settings.steps); material.SetFloat("_JitterVolumetric", settings.jitter); material.SetFloat("_MaxDistance", settings.maxDistance); material.SetFloat("_Intensity", settings.intensity); // material.SetFloat("_GaussSamples", settings.gaussBlur.samples); // material.SetFloat("_GaussAmount", settings.gaussBlur.amount); // material.SetColor("_Tint", settings.tint); //raymarch cmd.Blit(source, tempTexture.Identifier(), material, 0); //bilateral blu X, we use the lowresdepth render texture for other things too, it is just a name // cmd.Blit(tempTexture.Identifier(), lowResDepthRT.Identifier(), material, 1); //bilateral blur Y // cmd.Blit(lowResDepthRT.Identifier(), tempTexture.Identifier(), material, 2); //save it in a global texture // cmd.SetGlobalTexture("_volumetricTexture", tempTexture.Identifier()); //downsample depth // cmd.Blit(source, lowResDepthRT.Identifier(), material, 4); // cmd.SetGlobalTexture("_LowResDepth", lowResDepthRT.Identifier()); //upsample and composite // cmd.Blit(source, temptexture3.Identifier(), material, 3); cmd.Blit(tempTexture.Identifier(), source); context.ExecuteCommandBuffer(cmd); } catch { Debug.LogError("Error"); } cmd.Clear(); CommandBufferPool.Release(cmd); } } Pass pass; RenderTargetHandle renderTextureHandle; public override void Create() { pass = new Pass("Volumetric Spot Light"); name = "Volumetric Spot Light"; pass.settings = settings; pass.renderPassEvent = settings.renderPassEvent; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { var cameraColorTargetIdent = renderer.cameraColorTarget; pass.Setup(cameraColorTargetIdent); renderer.EnqueuePass(pass); } }
Also, that is only with point lights, spotlights don't work at all, and directional lights work perfectly.
After further testing in a blank project with none of the ray march stuff, the additional light stuff fails completely with all of the light types.