Volumetric Light Scattering as a Custom Renderer Feature in URP | Kodeco, the new raywenderlich.com

Learn how to create your own custom rendering features with Unity’s Universal Render Pipeline by adding some volumetric light scattering to a small animated scene.


This is a companion discussion topic for the original entry at https://www.kodeco.com/22027819-volumetric-light-scattering-as-a-custom-renderer-feature-in-urp

Hey, really nice effect and it works great in my project. But I have just one problem, and I would love a little help.

I’m using it in a first-person camera, so every time the player stays at 90 degrees aside from the sun, the effect starts to show. In that case, the sun isn’t in my field of view. Is there a way to change it to just show when I’m, let’s say, 45 degrees from the sun? thanks so much!

Hello Ignacio, very nice tutorial! I wasn’t able to get it to work in URP 15.0.6, so I took the liberty to adapt the code for this version :

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System.Collections.Generic;

public class VolumetricLightRenderFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class VolumetricLightSettings
    {
        [Header("Properties")]
        [Range(0.1f, 1f)]
        public float resolutionScale = 0.5f;

        [Range(0.0f, 1.0f)]
        public float intensity = 1.0f;

        [Range(0.0f, 1.0f)]
        public float blurWidth = 0.85f;
    }

    class VolumetricLightPass : ScriptableRenderPass
    {
        private readonly List<ShaderTagId> shaderTagIdList = new List<ShaderTagId>();
        private readonly Material occludersMaterial;
        private readonly Material radialBlurMaterial;
        private RTHandle occluders;
        private RTHandle cameraColor;
        private readonly float resolutionScale;
        private readonly float intensity;
        private readonly float blurWidth;
        private FilteringSettings filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
        RendererList otherRenderList;
        private readonly int occluderShaderId;
        private RendererListParams renderListParams;
        private DrawingSettings drawSettings;
        public VolumetricLightPass(VolumetricLightSettings settings)
        {
            shaderTagIdList.Add(new ShaderTagId("UniversalForward"));
            shaderTagIdList.Add(new ShaderTagId("UniversalForwardOnly"));
            shaderTagIdList.Add(new ShaderTagId("LightweightForward"));
            shaderTagIdList.Add(new ShaderTagId("SRPDefaultUnlit"));
            occludersMaterial = new Material(Shader.Find("Hidden/RW/UnlitColor"));
            radialBlurMaterial = new Material(Shader.Find("Hidden/RW/RadialBlur"));
            occluders = RTHandles.Alloc("_OccludersMap", name: "_OccludersMap");
            occluderShaderId = Shader.PropertyToID(occluders.name);
            resolutionScale = settings.resolutionScale;
            intensity = settings.intensity;
            blurWidth = settings.blurWidth;
        }

        public void SetCameraColorTarget(RTHandle cameraColor)
        {
            this.cameraColor = cameraColor;
        }

        // This method is called before executing the render pass.
        // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
        // When empty this render pass will render to the active camera render target.
        // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
        // The render pipeline will ensure target setup and clearing happens in a performant manner.
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            RenderTextureDescriptor cameraTextureDescriptor =
                renderingData.cameraData.cameraTargetDescriptor;

            cameraTextureDescriptor.depthBufferBits = 0;

            cameraTextureDescriptor.width = Mathf.RoundToInt(
                cameraTextureDescriptor.width * resolutionScale);
            cameraTextureDescriptor.height = Mathf.RoundToInt(
                cameraTextureDescriptor.height * resolutionScale);

            RenderingUtils.ReAllocateIfNeeded(ref occluders, cameraTextureDescriptor, FilterMode.Bilinear);

            ConfigureTarget(occluders);
        }

        // Here you can implement the rendering logic.
        // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
        // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
        // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
        public override void Execute(ScriptableRenderContext context,
     ref RenderingData renderingData)
        {
            // 1
            if (!occludersMaterial || !radialBlurMaterial || cameraColor == null)
                return;

            CommandBuffer cmd = CommandBufferPool.Get();


            Camera camera = renderingData.cameraData.camera;

            Vector3 sunDirectionWorldSpace = RenderSettings.sun.transform.forward;
            Vector3 cameraPositionWorldSpace = camera.transform.position;
            Vector3 sunPositionWorldSpace = cameraPositionWorldSpace + sunDirectionWorldSpace;
            Vector3 sunPositionViewportSpace = camera.WorldToViewportPoint(sunPositionWorldSpace);


            radialBlurMaterial.SetVector("_Center", new Vector4(sunPositionViewportSpace.x, sunPositionViewportSpace.y, 0, 0));
            radialBlurMaterial.SetFloat("_Intensity", intensity);
            radialBlurMaterial.SetFloat("_BlurWidth", blurWidth);

            using (new ProfilingScope(cmd,
                new ProfilingSampler("VolumetricLightScattering")))
            {
                context.ExecuteCommandBuffer(cmd);
                cmd.Clear();

                // Draw a white background
                cmd.SetRenderTarget(occluders);
                cmd.ClearRenderTarget(true, true, Color.white);


                // This is the way to add the occluders in URP 14+ 
                drawSettings = CreateDrawingSettings(shaderTagIdList, ref renderingData, SortingCriteria.CommonOpaque);
                drawSettings.overrideMaterial = occludersMaterial;

                renderListParams = new RendererListParams(renderingData.cullResults, drawSettings, filteringSettings);
                otherRenderList = context.CreateRendererList(ref renderListParams);

                cmd.DrawRendererList(otherRenderList);

                cmd.Blit(occluders, cameraColor, radialBlurMaterial);
            }

            // 4
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        // Cleanup any allocated resources that were created during the execution of this render pass.
        public override void OnCameraCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(occluderShaderId);
        }
    }

    VolumetricLightPass m_ScriptablePass;
    public VolumetricLightSettings settings = new VolumetricLightSettings();
    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePass = new VolumetricLightPass(settings);
        m_ScriptablePass.renderPassEvent =
            RenderPassEvent.BeforeRenderingPostProcessing;
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_ScriptablePass);
    }

    public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
    {
        base.SetupRenderPasses(renderer, renderingData);
        m_ScriptablePass.SetCameraColorTarget(renderer.cameraColorTargetHandle);
    }
}



Everything else should work as intended, only the custom renderer feature needed some changes, cheers!