Search Unity

Question Blur Shadows (Custom soft shadows)

Discussion in 'Shaders' started by oukibt_unity, Sep 16, 2022.

  1. oukibt_unity

    oukibt_unity

    Joined:
    May 6, 2022
    Posts:
    19
    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
     
  2. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    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 :-(
     
    Last edited: Sep 18, 2022
    oukibt_unity likes this.
  3. oukibt_unity

    oukibt_unity

    Joined:
    May 6, 2022
    Posts:
    19
    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
    Code (CSharp):
    1. shader = Shader.Find("Hidden/MyShader");
    2. material = new Material(shader);

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

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    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.
     
    oukibt_unity likes this.
  5. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    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:
    Code (CSharp):
    1. using System;
    2. using System.Reflection;
    3. using UnityEngine;
    4. using UnityEngine.Experimental.Rendering;
    5. using UnityEngine.Rendering;
    6. using UnityEngine.Rendering.Universal;
    7. using UObject = UnityEngine.Object;
    8. using UDebug = UnityEngine.Debug;
    9.  
    10. namespace burningmime.unity.graphics
    11. {
    12.     class BlurScreenSpaceShadowsFeature : ScriptableRendererFeature
    13.     {
    14.         [SerializeField] private float _blurRadius = 2;
    15.         [SerializeField] private float _maxDepthDiff =  .00075f;
    16.         [SerializeField] private float _maxNormalDiff = .1f;
    17.         private ScriptableRenderPass _pass;
    18.  
    19.         public override void Create()
    20.         {
    21.             // defer creating the pass to the AddRenderPasses() to support domain-safe editor reloading, changing
    22.             // render modes, etc, etc.
    23.         }
    24.  
    25.         public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData rd)
    26.         {
    27.             if(_pass == null)
    28.             {
    29.                 _pass ??= new BlurScreenSpaceShadowsPass(this);
    30.                 RenderPassEvent baseEvent = Utils.isDeferredUrp(renderer) ? RenderPassEvent.AfterRenderingGbuffer : RenderPassEvent.AfterRenderingPrePasses;
    31.                 _pass.renderPassEvent = (RenderPassEvent) ((int) baseEvent + 1);
    32.             }
    33.        
    34.             renderer.EnqueuePass(_pass);
    35.         }
    36.    
    37.         private class BlurScreenSpaceShadowsPass : ScriptableRenderPass
    38.         {
    39.             private static readonly int ID_SHADOWS_TEXTURE = Shader.PropertyToID("_ScreenSpaceShadowmapTexture");
    40.             private static readonly int ID_INTERMEDIATE_TEXTURE = Shader.PropertyToID("_BlurScreenSpaceShadows_Temp");
    41.             private static readonly int ID_BLUR_RADIUS = Shader.PropertyToID("_BlurScreenSpaceShadows_blurRadius");
    42.             private static readonly int ID_MAX_DEPTH_DIFF = Shader.PropertyToID("_BlurScreenSpaceShadows_maxDepthDiff");
    43.             private static readonly int ID_MAX_NORMAL_DIFF = Shader.PropertyToID("_BlurScreenSpaceShadows_maxNormalDiff");
    44.  
    45.             private readonly BlurScreenSpaceShadowsFeature _owner;
    46.             private readonly ProfilingSampler _profilingSampler = new(nameof(BlurScreenSpaceShadowsPass));
    47.             private readonly Material _material = Utils.newMaterial("Hidden/burningmime/BlurScreenSpaceShadows");
    48.  
    49.             public BlurScreenSpaceShadowsPass(BlurScreenSpaceShadowsFeature owner)
    50.                 => _owner = owner;
    51.        
    52.             public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cd)
    53.             {
    54.                 ConfigureInput(ScriptableRenderPassInput.Depth | ScriptableRenderPassInput.Normal);
    55.                 ConfigureClear(ClearFlag.None, Color.clear);
    56.             }
    57.  
    58.             public override void Execute(ScriptableRenderContext ctx, ref RenderingData rd)
    59.             {
    60.                 CommandBuffer cmd = CommandBufferPool.Get();
    61.                 using(new ProfilingScope(cmd, _profilingSampler))
    62.                     executeMain(cmd, ref rd);
    63.                 ctx.ExecuteCommandBuffer(cmd);
    64.                 CommandBufferPool.Release(cmd);
    65.             }
    66.  
    67.             private void executeMain(CommandBuffer cmd, ref RenderingData rd)
    68.             {
    69.                 cmd.GetTemporaryRT(ID_INTERMEDIATE_TEXTURE,
    70.                     rd.cameraData.cameraTargetDescriptor.width, rd.cameraData.cameraTargetDescriptor.height, 0,
    71.                     FilterMode.Bilinear, GraphicsFormat.R8_UNorm);
    72.                 cmd.SetGlobalFloat(ID_BLUR_RADIUS, _owner._blurRadius);
    73.                 cmd.SetGlobalFloat(ID_MAX_DEPTH_DIFF, _owner._maxDepthDiff);
    74.                 cmd.SetGlobalFloat(ID_MAX_NORMAL_DIFF, _owner._maxNormalDiff);
    75.                 Blit(cmd, ID_SHADOWS_TEXTURE, ID_INTERMEDIATE_TEXTURE, _material, 0);
    76.                 Blit(cmd, ID_INTERMEDIATE_TEXTURE, ID_SHADOWS_TEXTURE, _material, 1);
    77.                 cmd.ReleaseTemporaryRT(ID_INTERMEDIATE_TEXTURE);
    78.             }
    79.         }
    80.    
    81.     #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
    82.         private static class Utils
    83.         {
    84.             private static readonly Func<UniversalRenderer, RenderingMode> _getRenderingMode =
    85.                 createDelegate<Func<UniversalRenderer, RenderingMode>>(typeof(UniversalRenderer), "renderingMode", CreateDelegateSource.GET_PROPERTY);
    86.             public static bool isDeferredUrp(ScriptableRenderer renderer) => renderer is UniversalRenderer urp && _getRenderingMode(urp) == RenderingMode.Deferred;
    87.        
    88.             public static Material newMaterial(string shaderName) => new(findShader(shaderName));
    89.             private static Shader findShader(string shaderName) { Shader shader = Shader.Find(shaderName); if(!shader) throw new Exception("Could not find shader " + shaderName); return shader; }
    90.        
    91.             private enum CreateDelegateSource { METHOD, GET_PROPERTY, SET_PROPERTY }
    92.             private static TDelegate createDelegate<TDelegate>(Type type, string name, CreateDelegateSource source = CreateDelegateSource.METHOD) where TDelegate : Delegate
    93.             {
    94.                 const BindingFlags BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Instance;
    95.                 MethodInfo method;
    96.                 if(source == CreateDelegateSource.METHOD)
    97.                     method = type.GetMethod(name, BINDING_FLAGS) ?? throw new Exception($"Could not find method {type.FullName}#{name}");
    98.                 else {
    99.                     PropertyInfo property = type.GetProperty(name, BINDING_FLAGS) ?? throw new Exception($"Could not find property {type.FullName}#{name}");
    100.                     method = source == CreateDelegateSource.GET_PROPERTY ? property.GetGetMethod(true) : property.GetSetMethod(true);
    101.                     if(method == null)
    102.                         throw new Exception($"Did not find the required get/set method kind on property {type.FullName}#{name}"); }
    103.                 Delegate dg = Delegate.CreateDelegate(typeof(TDelegate), method);
    104.                 if(dg == null)
    105.                     throw new Exception($"Error creating delegate of {typeof(TDelegate)} for {type.FullName}#{name}");
    106.                 return (TDelegate) dg;
    107.             }
    108.         }
    109.     #endregion
    110.     }
    111. }

    The shader:

    Code (CSharp):
    1. Shader "Hidden/burningmime/BlurScreenSpaceShadows"
    2. {
    3. HLSLINCLUDE
    4. #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    5. #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"
    6. #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
    7. TEXTURE2D(_MainTex);
    8. SAMPLER(sampler_linear_clamp);
    9. float4 _MainTex_TexelSize;
    10. float _BlurScreenSpaceShadows_blurRadius;
    11. float _BlurScreenSpaceShadows_maxDepthDiff;
    12. float _BlurScreenSpaceShadows_maxNormalDiff;
    13.  
    14. // vertex shader
    15. struct a2v { float3 pos : POSITION; float2 uv : TEXCOORD0; };
    16. struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
    17. v2f fullscreen_VS(a2v IN)
    18. {
    19.     v2f OUT;
    20.     OUT.pos = TransformObjectToHClip(IN.pos);
    21.     OUT.uv = IN.uv;
    22.     if(_ProjectionParams.x < 0)
    23.         OUT.uv.y = 1 - OUT.uv.y;
    24.     return OUT;
    25. }
    26.  
    27. // offsetting coordinates
    28. float2 blurCoords(float offset, bool isX, float2 centerUV)
    29. {
    30.     return isX
    31.         ? float2(saturate(centerUV.x + offset * _MainTex_TexelSize.x * _BlurScreenSpaceShadows_blurRadius), centerUV.y)
    32.         : float2(centerUV.x, saturate(centerUV.y + offset * _MainTex_TexelSize.y * _BlurScreenSpaceShadows_blurRadius));
    33. }
    34. // basic idea is to blur but use the center value if the depth or normals are too different. ideally, we would be using
    35. // the closest valid value (eg clamping), and we could also scale the max difference by distance. not doing either of
    36. // those things for now to keep the shader simple, but ultimately might need to do one or both to cut down on artifacts
    37. float blurShadow(bool isX, float2 uvc)
    38. {
    39.     // these need to be in the function itself; if they were consts outside then they would be part of the CBUFFER and
    40.     // the shader code would be a ton more complex
    41.     int nSamples = 9;
    42.     float blurOffsets[9] = { 0, -4, -3, -2, -1, 1, 2, 3, 4 };
    43.     float blurWeights[9] = { 0.18, 0.05, 0.09, 0.12, 0.15, 0.15, 0.12, 0.09, 0.05 };
    44.     float centerValue = SAMPLE_TEXTURE2D(_MainTex, sampler_linear_clamp, uvc).r;
    45.     float centerDepth = SampleSceneDepth(uvc);
    46.     float3 centerNormal = SampleSceneNormals(uvc);
    47.     float sum = centerValue * blurWeights[0];
    48.     UNITY_UNROLL for(int i = 1; i < nSamples; ++i)
    49.     {
    50.         float2 uv = blurCoords(blurOffsets[i], isX, uvc);
    51.         float sampleValue = SAMPLE_TEXTURE2D(_MainTex, sampler_linear_clamp, uv).r;
    52.         float sampleDepth = SampleSceneDepth(uv);
    53.         float3 sampleNormal = SampleSceneNormals(uv);
    54.         float value =
    55.             abs(sampleDepth - centerDepth) > _BlurScreenSpaceShadows_maxDepthDiff
    56.             || dot(sampleNormal, centerNormal) < 1 - _BlurScreenSpaceShadows_maxNormalDiff
    57.             ? centerValue : sampleValue;
    58.         sum += value * blurWeights[i];
    59.     }
    60.     return sum;
    61. }
    62. // actual shader functions are all just thin wrappers
    63. float4 blurShadowX_PS(v2f IN) : SV_Target { return float4(blurShadow(true, IN.uv), 0, 0, 1); }
    64. float4 blurShadowY_PS(v2f IN) : SV_Target { return float4(blurShadow(false, IN.uv), 0, 0, 1); }
    65. ENDHLSL
    66.  
    67.     Properties { [HideInInspector] _MainTex("_MainTex", 2D) = "white" { } }
    68.     SubShader
    69.     {
    70.         Tags { "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }
    71.         Pass
    72.         {
    73.             Tags { "LightMode" = "SRPDefaultUnlit" }
    74.             ZTest Off ZWrite Off Cull Off
    75.             HLSLPROGRAM
    76.                 #pragma vertex fullscreen_VS
    77.                 #pragma fragment blurShadowX_PS
    78.             ENDHLSL
    79.         }
    80.         Pass
    81.         {
    82.             Tags { "LightMode" = "SRPDefaultUnlit" }
    83.             ZTest Off ZWrite Off Cull Off
    84.             HLSLPROGRAM
    85.                 #pragma vertex fullscreen_VS
    86.                 #pragma fragment blurShadowY_PS
    87.             ENDHLSL
    88.         }
    89.     }
    90. }
    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 http://c0de517e.blogspot.com/2011/05/shadowmap-bias-notes.html -- 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.
     
    Last edited: Sep 18, 2022
    oukibt_unity likes this.