阴影扰动效果(URP阴影渲染)

比较简单普适的阴影效果实现思路

URP阴影渲染流程

Github源码传送门————>ShadowEdgeDistortion

Bilibili教程传送门————>小祥带你实现阴影扰动效果

阴影贴图

以官方URP中的SampleLit材质+平行光源(Directional Light)为例,过一遍阴影渲染流程

  1. ShadowCaster
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    Pass
    {
    Name "ShadowCaster"

    Tags
    {
    "LightMode" = "ShadowCaster"
    }

    // -------------------------------------
    // Render State Commands
    ZWrite On
    ZTest LEqual
    ColorMask 0
    Cull[_Cull]

    HLSLPROGRAM
    #pragma target 2.0
    // -------------------------------------
    // Shader Stages
    #pragma vertex ShadowPassVertex
    #pragma fragment ShadowPassFragment
    ENDHLSL
    }
  • 投射阴影的物体执行这个Pass,将其从光源视角(平行光的话就是光照方向摆一个正交摄像机)下的深度信息写入阴影贴图(ShadowMap)
  • 阴影贴图是例图中间那张,记录深度信息。可以看到包含四张不同分辨率贴图,这是因为引入了级联阴影贴图(Cascaded Shadow Maps),保证不论远近阴影的质量比较稳定(近处的阴影更靠近相机,在屏幕上占的像素更多,稍有锯齿就很明显,使用较高分辨率的贴图;而远处阴影即使有锯齿,也在屏幕上占很少像素,不容易察觉,使用较低分辨率的贴图)
  1. ForwardLit
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Pass
    {
    Name "ForwardLit"

    Tags
    {
    "LightMode" = "UniversalForward"
    }

    // -------------------------------------
    // Render State Commands
    // Use same blending / depth states as Standard shader
    Blend[_SrcBlend][_DstBlend], [_SrcBlendAlpha][_DstBlendAlpha]
    ZWrite[_ZWrite]
    Cull[_Cull]
    AlphaToMask[_AlphaToMask]

    HLSLPROGRAM
    #pragma target 2.0
    // -------------------------------------
    // Shader Stages
    #pragma vertex LitPassVertexSimple
    #pragma fragment LitPassFragmentSimple
    ENDHLSL
    }
  • 被投射阴影的物体在执行这个Pass的时候计算光照,阴影,环境光遮蔽等数据,将阴影绘制到物体上
  1. LitPassFragmentSimple
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    void LitPassFragmentSimple(
    Varyings input
    , out half4 outColor : SV_Target0
    #ifdef _WRITE_RENDERING_LAYERS
    , out float4 outRenderingLayers : SV_Target1
    #endif
    )
    {
    // ......

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
    SETUP_DEBUG_TEXTURE_DATA(inputData, UNDO_TRANSFORM_TEX(input.uv, _BaseMap));

    #if defined(_DBUFFER)
    ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
    #endif

    InitializeBakedGIData(input, inputData);

    half4 color = UniversalFragmentBlinnPhong(inputData, surfaceData);
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    color.a = OutputAlpha(color.a, IsSurfaceTypeTransparent(_Surface));

    outColor = color;

    // ......
    }
  • 可以看到在片元着色器中,初始化数据之后调用 BlinnPhong 光照模型对应的方法进行绘制
  1. UniversalFragmentBlinnPhong
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    half4 UniversalFragmentBlinnPhong(InputData inputData, SurfaceData surfaceData)
    {
    // ......

    uint meshRenderingLayers = GetMeshRenderingLayer();
    half4 shadowMask = CalculateShadowMask(inputData);
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
    Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);

    // ......

    return CalculateFinalColor(lightingData, surfaceData.alpha);
    }
  • GetMainLight方法计算阴影和光照
  1. GetMainLight
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Light GetMainLight(float4 shadowCoord)
    {
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightRealtimeShadow(shadowCoord);
    return light;
    }

    Light GetMainLight(float4 shadowCoord, float3 positionWS, half4 shadowMask)
    {
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightShadow(shadowCoord, positionWS, shadowMask, _MainLightOcclusionProbes);

    #if defined(_LIGHT_COOKIES)
    real3 cookieColor = SampleMainLightCookie(positionWS);
    light.color *= cookieColor;
    #endif

    return light;
    }
  • MainLightRealtimeShadow方法处理阴影
  1. MainLightRealtimeShadow
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    half MainLightRealtimeShadow(float4 shadowCoord, half4 shadowParams, ShadowSamplingData shadowSamplingData)
    {
    #if !defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    return half(1.0);
    #endif

    #if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT)
    return SampleScreenSpaceShadowmap(shadowCoord);
    #else
    return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_LinearClampCompare), shadowCoord, shadowSamplingData, shadowParams, false);
    #endif
    }
  • 这个方法是具体计算阴影的地方,可以看到有两种阴影采样路径
  • 一种是屏幕空间阴影,提前渲染好一张屏幕阴影贴图(_ScreenSpaceShadowmapTexture),例图最右侧那张,渲染好的阴影贴图直接拿来采样使用即可,只做一次全屏计算,性能较好
  • 一种是传统阴影计算路径,使用深度图(_MainLightShadowmapTexture),通过比较深度计算当前像素是否为阴影,每个片元都要采样,性能稍逊

阴影扰动效果

效果

那么阴影渲染流程清楚之后,要对阴影做效果思路也比较清晰了,一种是去修改被投射阴影物体的主渲染Pass中渲染阴影的那一部分内容,一种是以屏幕空间阴影贴图为基础进一步操作。那么相比较而言显然是后者更易操作一些。我一向是喜欢使用非侵入式的实现方式的,在不改动原有实现的基础上去做效果最好,我愿称之为游戏开发糊弄第一准则XD。选好实现方式,具体的效果实现实际上就没什么难度了。比如要做阴影扰动效果,对UV进行扰动之后进行重采样即可,要做阴影外描边效果,边缘检测之后绘制描边即可。这里以扰动效果为例讲一下:

  1. ShadowDistortionRendererFeature
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ShadowDistortionRendererFeature : ScriptableRendererFeature
    {
    public ShadowDistortionRenderPass shadowDistortionRenderPass;

    public Material shadowDistortionMaterial;

    public override void Create()
    {
    shadowDistortionRenderPass = new ShadowDistortionRenderPass(shadowDistortionMaterial);
    shadowDistortionRenderPass.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
    renderer.EnqueuePass(shadowDistortionRenderPass);
    }
    }
  • 屏幕空间阴影的渲染是在Gbuffer之后,阴影是在非透明物体阶段渲染的,那么我们的渲染顺序排在非透明物体渲染之前(BeforeRenderingOpaques)即可
  1. ShadowDistortionRenderPass
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class ShadowDistortionRenderPass : ScriptableRenderPass
    {
    private int shadowTexID;
    private Material shadowDistortionMaterial;

    class PassData
    {
    public Material material;
    public TextureHandle target;
    }

    public ShadowDistortionRenderPass(Material material)
    {
    shadowTexID = Shader.PropertyToID("_ScreenSpaceShadowmapTexture");
    shadowDistortionMaterial = material;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
    UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();

    var desc = cameraData.cameraTargetDescriptor;
    desc.depthStencilFormat = GraphicsFormat.None;
    desc.msaaSamples = 1;
    desc.graphicsFormat = SystemInfo.IsFormatSupported(GraphicsFormat.R8_UNorm, GraphicsFormatUsage.Blend)
    ? GraphicsFormat.R8_UNorm
    : GraphicsFormat.B8G8R8A8_UNorm;

    TextureHandle color = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "_TempScreenSpaceShadowmapTexture", true);

    using (var builder = renderGraph.AddRasterRenderPass<PassData>("ShadowDistortion", out var passData))
    {
    passData.material = shadowDistortionMaterial;
    passData.target = color;

    builder.SetRenderAttachment(color, 0, AccessFlags.Write);

    builder.SetGlobalTextureAfterPass(color, shadowTexID);

    builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
    {
    Blitter.BlitTexture(context.cmd, data.target, Vector4.one, data.material, 0);
    });
    }
    }
    }
  • 创建一张临时贴图,通过材质将屏幕空间阴影贴图进行处理,实现效果之后赋值到这张临时贴图,使用SetGlobalTextureAfterPass方法将我们的临时贴图设置为屏幕空间阴影,后续非透明物体渲染时采样的就是我们处理过的阴影贴图
  1. ShadowDistortion
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    Shader "Custom/ShadowDistortion"
    {
    Properties
    {
    _DistortionAmount("_DistortionAmount", Range(0, 5)) = 1.0
    _PixelSize("_PixelSize", Float) = 200.0
    }

    SubShader
    {
    Tags { "RenderType"="Transparent" "Queue"="Transparent" "RenderPipeline"="UniversalPipeline" }
    Blend SrcAlpha OneMinusSrcAlpha
    Cull Off
    ZWrite Off
    Pass
    {
    Name "ShadowDistortion"

    HLSLPROGRAM
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ImageBasedLighting.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

    #pragma vertex Vert
    #pragma fragment Frag

    float _DistortionAmount;
    float _PixelSize;

    float Noise(float2 uv)
    {
    return frac(sin(dot(uv * 1234.56 + _Time.y * 10, float2(12.9898, 78.233))) * 43758.5453) * 2 - 1;
    }

    float4 Frag(Varyings input) : SV_Target
    {
    float2 screenUV = GetNormalizedScreenSpaceUV(input.positionCS);

    float time = _Time.y;

    float2 distortedUV = screenUV;

    distortedUV.x += sin(time * 10.0 + screenUV.y * 100.0) * 0.002 * _DistortionAmount;
    distortedUV.y += Noise(screenUV * 10.0 + time * 3.0) * 0.002 * _DistortionAmount;

    float2 pixelSize = float2(_PixelSize, _PixelSize);
    distortedUV = floor(distortedUV * pixelSize) / pixelSize;
    float pushAmount = (screenUV.x - 0.5) * 0.05 * _DistortionAmount;
    distortedUV.x -= pushAmount;

    half shadowMapDistorted = SAMPLE_TEXTURE2D(_ScreenSpaceShadowmapTexture, sampler_PointClamp, distortedUV);

    return shadowMapDistorted;
    }
    ENDHLSL
    }
    }
    }
  • 我们操作的屏幕空间阴影贴图(_ScreenSpaceShadowmapTexture)是屏幕空间的废话,使用GetNormalizedScreenSpaceUV方法将uv转到屏幕空间
  • 简单做一个坐标扰动和像素化(其他效果类似),处理后的坐标去采样原贴图然后返回色值即可

完结撒花~

pid:132704750


阴影扰动效果(URP阴影渲染)
https://baifabaiquan.cn/2025/07/16/ShadowDistortion/
作者
白发败犬
发布于
2025年7月16日
许可协议