Blur Shadows (Custom soft shadows)

Hello. Is there any way to make own soft shadow shader?
I wanna make own shadow caster shader because unity standard shadows looks not good in some cases.
I’ve make screenshot with this hard shadow

go to photoshop → filter → blur → Gaussian blur → 9.0 px
and i got a pretty good soft shadow

Can i make something like this through shader or something else?
I’ve tried use this
https://github.com/unitycoder/IndieSoftShadow
but it’s just deprecated now

If you’re using URP in Unity 2021.3 with the ScreenSpaceShadows render feature…

You can pretty much do whatever to the
_ScreenSpaceShadowmapTexture (bind to it by name). Just inject your blur pass at some point after the SSS have been drawn but before the final render happens (use (RenderPassEvent) ((int) RenderPassEvent.AfterRenderingPrePasses + 1) for forward or (RenderPassEvent) ((int) RenderPassEvent.AfterRenderingGbuffer + 1) for deferred to ensure it runs right after the ScreenSpaceShadowsPass). I used this technique to get stencil shadows working. It’s a bit of a PITA but if you’re familiar with writing custom render passes it shouldn’t be too much of a burden.

However, there’s a bit of a catch if you’re planning on doing this – you need to be depth-normal aware to prevent the texture from bleeding into places it shouldn’t.

(EDIT: fixed version below)

If you’re using HDRP or built-in…

I have no idea; sorry :frowning:

1 Like

It looks good but i have a problem with implementing this.
I use URP and i turned on SSS

Okay, I created new shader, and place your code there, but it’s hlsl, no unity shader. How i can use/implement it if it’s not shader that can be attached to material or found throgh code like in Image Effect
csharp*__ <strong>*shader = Shader.Find("Hidden/MyShader"); material = new Material(shader);*</strong> __*

Is there any example on the internet that fits this case?

It’s not plug-and-play; you’ll need to wrap it in a shader and then add the feature on the C# side, configure inputs, etc (it’s not an image effect to apply to the final frame; you need to do it to the screen space shadows texture at the right point). And URP makes it a bit of a pain to add render features; it should be a lot easier than it is to just do a simple one that injects a pass.

If I have a chance this weekend; I’ll throw together a quick demo project getting it to work.

1 Like

This seems to work in the URP sample project; haven’t spent too much time testing but basically just drop the script and shader there, and add the ScreenSpaceShadowsFeature and the BlurScreenSpaceShadowsFeature both to the renderer. Mess with the blur radius and clamp values as desired.

The script:

using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UObject = UnityEngine.Object;
using UDebug = UnityEngine.Debug;

namespace burningmime.unity.graphics
{
    class BlurScreenSpaceShadowsFeature : ScriptableRendererFeature
    {
        [SerializeField] private float _blurRadius = 2;
        [SerializeField] private float _maxDepthDiff =  .00075f;
        [SerializeField] private float _maxNormalDiff = .1f;
        private ScriptableRenderPass _pass;

        public override void Create()
        {
            // defer creating the pass to the AddRenderPasses() to support domain-safe editor reloading, changing
            // render modes, etc, etc.
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData rd)
        {
            if(_pass == null)
            {
                _pass ??= new BlurScreenSpaceShadowsPass(this);
                RenderPassEvent baseEvent = Utils.isDeferredUrp(renderer) ? RenderPassEvent.AfterRenderingGbuffer : RenderPassEvent.AfterRenderingPrePasses;
                _pass.renderPassEvent = (RenderPassEvent) ((int) baseEvent + 1);
            }
       
            renderer.EnqueuePass(_pass);
        }
   
        private class BlurScreenSpaceShadowsPass : ScriptableRenderPass
        {
            private static readonly int ID_SHADOWS_TEXTURE = Shader.PropertyToID("_ScreenSpaceShadowmapTexture");
            private static readonly int ID_INTERMEDIATE_TEXTURE = Shader.PropertyToID("_BlurScreenSpaceShadows_Temp");
            private static readonly int ID_BLUR_RADIUS = Shader.PropertyToID("_BlurScreenSpaceShadows_blurRadius");
            private static readonly int ID_MAX_DEPTH_DIFF = Shader.PropertyToID("_BlurScreenSpaceShadows_maxDepthDiff");
            private static readonly int ID_MAX_NORMAL_DIFF = Shader.PropertyToID("_BlurScreenSpaceShadows_maxNormalDiff");

            private readonly BlurScreenSpaceShadowsFeature _owner;
            private readonly ProfilingSampler _profilingSampler = new(nameof(BlurScreenSpaceShadowsPass));
            private readonly Material _material = Utils.newMaterial("Hidden/burningmime/BlurScreenSpaceShadows");

            public BlurScreenSpaceShadowsPass(BlurScreenSpaceShadowsFeature owner)
                => _owner = owner;
       
            public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cd)
            {
                ConfigureInput(ScriptableRenderPassInput.Depth | ScriptableRenderPassInput.Normal);
                ConfigureClear(ClearFlag.None, Color.clear);
            }

            public override void Execute(ScriptableRenderContext ctx, ref RenderingData rd)
            {
                CommandBuffer cmd = CommandBufferPool.Get();
                using(new ProfilingScope(cmd, _profilingSampler))
                    executeMain(cmd, ref rd);
                ctx.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
            }

            private void executeMain(CommandBuffer cmd, ref RenderingData rd)
            {
                cmd.GetTemporaryRT(ID_INTERMEDIATE_TEXTURE,
                    rd.cameraData.cameraTargetDescriptor.width, rd.cameraData.cameraTargetDescriptor.height, 0,
                    FilterMode.Bilinear, GraphicsFormat.R8_UNorm);
                cmd.SetGlobalFloat(ID_BLUR_RADIUS, _owner._blurRadius);
                cmd.SetGlobalFloat(ID_MAX_DEPTH_DIFF, _owner._maxDepthDiff);
                cmd.SetGlobalFloat(ID_MAX_NORMAL_DIFF, _owner._maxNormalDiff);
                Blit(cmd, ID_SHADOWS_TEXTURE, ID_INTERMEDIATE_TEXTURE, _material, 0);
                Blit(cmd, ID_INTERMEDIATE_TEXTURE, ID_SHADOWS_TEXTURE, _material, 1);
                cmd.ReleaseTemporaryRT(ID_INTERMEDIATE_TEXTURE);
            }
        }
   
    #region I copied a bunch of this stuff from my internal code 'cause I'm too lazy to rewrite it all, but all the improtant stuff is above anyways
        private static class Utils
        {
            private static readonly Func<UniversalRenderer, RenderingMode> _getRenderingMode =
                createDelegate<Func<UniversalRenderer, RenderingMode>>(typeof(UniversalRenderer), "renderingMode", CreateDelegateSource.GET_PROPERTY);
            public static bool isDeferredUrp(ScriptableRenderer renderer) => renderer is UniversalRenderer urp && _getRenderingMode(urp) == RenderingMode.Deferred;
       
            public static Material newMaterial(string shaderName) => new(findShader(shaderName));
            private static Shader findShader(string shaderName) { Shader shader = Shader.Find(shaderName); if(!shader) throw new Exception("Could not find shader " + shaderName); return shader; }
       
            private enum CreateDelegateSource { METHOD, GET_PROPERTY, SET_PROPERTY }
            private static TDelegate createDelegate<TDelegate>(Type type, string name, CreateDelegateSource source = CreateDelegateSource.METHOD) where TDelegate : Delegate
            {
                const BindingFlags BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Instance;
                MethodInfo method;
                if(source == CreateDelegateSource.METHOD)
                    method = type.GetMethod(name, BINDING_FLAGS) ?? throw new Exception($"Could not find method {type.FullName}#{name}");
                else {
                    PropertyInfo property = type.GetProperty(name, BINDING_FLAGS) ?? throw new Exception($"Could not find property {type.FullName}#{name}");
                    method = source == CreateDelegateSource.GET_PROPERTY ? property.GetGetMethod(true) : property.GetSetMethod(true);
                    if(method == null)
                        throw new Exception($"Did not find the required get/set method kind on property {type.FullName}#{name}"); }
                Delegate dg = Delegate.CreateDelegate(typeof(TDelegate), method);
                if(dg == null)
                    throw new Exception($"Error creating delegate of {typeof(TDelegate)} for {type.FullName}#{name}");
                return (TDelegate) dg;
            }
        }
    #endregion
    }
}

The shader:

Shader "Hidden/burningmime/BlurScreenSpaceShadows"
{
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
TEXTURE2D(_MainTex);
SAMPLER(sampler_linear_clamp);
float4 _MainTex_TexelSize;
float _BlurScreenSpaceShadows_blurRadius;
float _BlurScreenSpaceShadows_maxDepthDiff;
float _BlurScreenSpaceShadows_maxNormalDiff;

// vertex shader
struct a2v { float3 pos : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
v2f fullscreen_VS(a2v IN)
{
    v2f OUT;
    OUT.pos = TransformObjectToHClip(IN.pos);
    OUT.uv = IN.uv;
    if(_ProjectionParams.x < 0)
        OUT.uv.y = 1 - OUT.uv.y;
    return OUT;
}

// offsetting coordinates
float2 blurCoords(float offset, bool isX, float2 centerUV)
{
    return isX
        ? float2(saturate(centerUV.x + offset * _MainTex_TexelSize.x * _BlurScreenSpaceShadows_blurRadius), centerUV.y)
        : float2(centerUV.x, saturate(centerUV.y + offset * _MainTex_TexelSize.y * _BlurScreenSpaceShadows_blurRadius));
}
// basic idea is to blur but use the center value if the depth or normals are too different. ideally, we would be using
// the closest valid value (eg clamping), and we could also scale the max difference by distance. not doing either of
// those things for now to keep the shader simple, but ultimately might need to do one or both to cut down on artifacts
float blurShadow(bool isX, float2 uvc)
{
    // these need to be in the function itself; if they were consts outside then they would be part of the CBUFFER and
    // the shader code would be a ton more complex
    int nSamples = 9;
    float blurOffsets[9] = { 0, -4, -3, -2, -1, 1, 2, 3, 4 };
    float blurWeights[9] = { 0.18, 0.05, 0.09, 0.12, 0.15, 0.15, 0.12, 0.09, 0.05 };
    float centerValue = SAMPLE_TEXTURE2D(_MainTex, sampler_linear_clamp, uvc).r;
    float centerDepth = SampleSceneDepth(uvc);
    float3 centerNormal = SampleSceneNormals(uvc);
    float sum = centerValue * blurWeights[0];
    UNITY_UNROLL for(int i = 1; i < nSamples; ++i)
    {
        float2 uv = blurCoords(blurOffsets[i], isX, uvc);
        float sampleValue = SAMPLE_TEXTURE2D(_MainTex, sampler_linear_clamp, uv).r;
        float sampleDepth = SampleSceneDepth(uv);
        float3 sampleNormal = SampleSceneNormals(uv);
        float value =
            abs(sampleDepth - centerDepth) > _BlurScreenSpaceShadows_maxDepthDiff
            || dot(sampleNormal, centerNormal) < 1 - _BlurScreenSpaceShadows_maxNormalDiff
            ? centerValue : sampleValue;
        sum += value * blurWeights[i];
    }
    return sum;
}
// actual shader functions are all just thin wrappers
float4 blurShadowX_PS(v2f IN) : SV_Target { return float4(blurShadow(true, IN.uv), 0, 0, 1); }
float4 blurShadowY_PS(v2f IN) : SV_Target { return float4(blurShadow(false, IN.uv), 0, 0, 1); }
ENDHLSL
 
    Properties { [HideInInspector] _MainTex("_MainTex", 2D) = "white" { } }
    SubShader
    {
        Tags { "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            Tags { "LightMode" = "SRPDefaultUnlit" }
            ZTest Off ZWrite Off Cull Off
            HLSLPROGRAM
                #pragma vertex fullscreen_VS
                #pragma fragment blurShadowX_PS
            ENDHLSL
        }
        Pass
        {
            Tags { "LightMode" = "SRPDefaultUnlit" }
            ZTest Off ZWrite Off Cull Off
            HLSLPROGRAM
                #pragma vertex fullscreen_VS
                #pragma fragment blurShadowY_PS
            ENDHLSL
        }
    }
}

The better way would be to determine the max normal diff, max depth diff, and possibly even blur radius based on the depth, projection params, and some screen space derivatives. This would make it adapt to the scene. A picture of mountains would need different numbers than a picture of flowers. A picture taken from the side of an object would need different numbers than a picture taken from an angle on top of the object.

If you’re interested in this, you could adapt some existing techniques for shadow biasing eg C0DE517E: Shadowmap bias notes – this is a different problem but a similar technique could work.

You could also just get rid of the maxDepthDiff and maxNormalDiff stuff entirely. It might look OK in your project.

1 Like

Hello, I’m new to render features and I’m trying to make this work in URP 14.0.11.
Judging by this link Access to _ScreenSpaceShadowmapTexture does not seem to work. (URP 14) .
It seems we can’t access the “_ScreenSpaceShadowmapTexture” anymore.
“Reflexions” could be a solutions but I’m not sure how to do it…

You can still get the shadowmap. Here’s simple demo code that works as of 14.0.11:

using UnityEngine;
using System.IO;

public class ScreenSpaceShadowmapCapture : MonoBehaviour
{
    [SerializeField] private string outputFileName = "ScreenSpaceShadowmap.png";
    [SerializeField] private KeyCode captureKey = KeyCode.F5;
    
    private void Update()
    {
        if (Input.GetKeyDown(captureKey))
        {
            CaptureAndSaveShadowmap();
        }
    }

    private void CaptureAndSaveShadowmap()
    {
        // Make sure we're using the right rendering path
        if (Camera.main.actualRenderingPath != RenderingPath.DeferredShading)
        {
            Debug.LogWarning("Main camera is not using deferred rendering. Screen space shadows might not be available.");
        }

        // Ensure shadows are enabled
        if (QualitySettings.shadows == ShadowQuality.Disable)
        {
            Debug.LogWarning("Shadows are disabled in quality settings.");
        }

        // Get the shadow texture
        Texture shadowTex = Shader.GetGlobalTexture("_ScreenSpaceShadowmapTexture");
        
        if (shadowTex == null)
        {
            Debug.LogError("_ScreenSpaceShadowmapTexture is null. Make sure shadows are enabled and working.");
            return;
        }
        
        Debug.Log($"Found shadowmap: {shadowTex.width}x{shadowTex.height}, format: {shadowTex.graphicsFormat}");

        // Create a render texture that we can read from
        RenderTexture tempRT = RenderTexture.GetTemporary(
            shadowTex.width,
            shadowTex.height,
            0,
            RenderTextureFormat.ARGB32
        );

        // Copy the shadow texture to our readable texture
        Graphics.Blit(shadowTex, tempRT);
        
        // Create a texture2D and read from the render texture
        Texture2D result = new Texture2D(tempRT.width, tempRT.height, TextureFormat.RGBA32, false);
        RenderTexture prevActive = RenderTexture.active;
        RenderTexture.active = tempRT;
        
        result.ReadPixels(new Rect(0, 0, tempRT.width, tempRT.height), 0, 0);
        result.Apply();
        
        // Restore previous active render texture
        RenderTexture.active = prevActive;
        RenderTexture.ReleaseTemporary(tempRT);
        
        // Save to file
        string path = Path.Combine(Application.persistentDataPath, outputFileName);
        byte[] bytes = result.EncodeToPNG();
        File.WriteAllBytes(path, bytes);
        
        Debug.Log($"Saved shadowmap to: {path}");
        
        // Clean up
        Destroy(result);
    }
}