Unity Shader - Custom SSSM(Screen Space Shadow Map) 自定义屏幕空间阴影图

141 篇文章 32 订阅


刚学习了Shadow Map的原理: Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap

现在试试实现SSSM。

思路

  • 先拿到 光源空间下的 ShadowMap,具体,可以参考:Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap
  • 再拿到屏幕空间的深度图,具体,可以参考:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据
  • 绘制一个全屏的Quad,并且将 屏幕空间的深度图 与 光源空间的ShadowMap阴影图,计算出屏幕空间的阴影图。进一步说明就是:将屏幕空间的深度 重构一下 世界坐标,再构建出世界坐标懂光源你空间的坐标,将深度的世界坐标通过世界到光源矩阵转换光源空间下,这时再比较深度来判定是否在阴影中。最后将这个全屏Quad的渲染目标到一个RT,叫做:SSSM(Screen Space Shadow Map)阴影图。
  • 接收阴影处理:对SSSM采样。将SSSM阴影图进shader全局uniform,渲染其他的接收阴影的对象时,将顶点坐标做到屏幕坐标,传入FS,插值后的屏幕坐标来采样SSS阴影图,采样得到的衰减数据与光照做混合计算即可。

实践

获取光源空间ShadowMap[A]

先拿到 光源空间下的 ShadowMap,具体,可以参考:Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap

获取屏幕空间的深度图[B]

再拿到屏幕空间的深度图,具体,可以参考:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据

获取SSSM(Screen Space Shadow Map)

绘制一个全屏的Quad[C]

参考之前写的一个:Unity Shader - 模仿RenderImage制作全屏Quad

输出SSSM RT Shader

            fixed4 frag (v2f i) : SV_Target {
                // to world from depth
                float linear01depth = (DecodeFloatRG(tex2D(_CustomDepthMap, i.uv).rg));
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linear01depth;
                // return float4(wp, 1);

                // world to lightspace
                // 取得当前绘制顶点相对光源空间下的坐标,即:阴影映射坐标
                float4 shadowCoord = mul(_CustomShadowMapLightSpaceMatrix, float4(wp, 1));

                // output SSSM atten
                return GetAtten(shadowCoord);
            }
在全屏Quad[C]里,制作将屏幕空间深度重建屏幕世界坐标[D]

参考:Unity Shader - 根据片段深度重建片段的世界坐标

                // to world from depth
                float linear01depth = (DecodeFloatRG(tex2D(_CustomDepthMap, i.uv).rg));
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linear01depth;
                // return float4(wp, 1);
在将屏幕的世界坐标[D]转换到光源空间下的坐标[E]
                // world to lightspace
                // 取得当前绘制顶点相对光源空间下的坐标,即:阴影映射坐标
                float4 shadowCoord = mul(_CustomShadowMapLightSpaceMatrix, float4(wp, 1));
比较[E]与[A]对应的Shadow Map深度,确定是阴影的将在Quad[C]写入屏幕阴影像素
                // output SSSM atten
                return GetAtten(shadowCoord);
完成的输出SSSM RT Shader
// jave.lin 2020.04.14 - 根据屏幕深度重构世界坐标 - output SSSM atten
Shader "Custom/ReconstructSSSMFromDepth" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                uint vid : SV_VertexID;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 ray : TEXCOORD1;
            };
            // reconstruct depth variables
            sampler2D _CustomDepthMap;
            float4x4 _Ray;

            // shadow map veriables
            float4 _CustomShadowMap_TexelSize;
            sampler2D _CustomShadowMap;

            float4x4 _CustomShadowMapLightSpaceMatrix;
            float _CustomShadowStrengthen;
            float _CustomShadowBias;
            int _CustomShadowSmoothType;

            float _CustomShadowPCFSpread;
            float _CustomShadowBlurDistance;
            float _CustomShadowBlurWeight[25];

            // 获取光照衰减系数
            float GetAtten(float4 shadowCoord) {
                float2 uv =  shadowCoord.xy / shadowCoord.w;
                uv = uv * 0.5 + 0.5; // (-1,1)->(0,1)

                // clamp to edge color : 1
                if (uv.x > 1 || uv.y > 1 || uv.x < 0 || uv.y < 0) return 1;

                float fragDepth = shadowCoord.z / shadowCoord.w;
                #if defined (SHADER_TARGET_GLSL)
                fragDepth = fragDepth * 0.5 + 0.5; // (-1,1)->(0,1)
                #elif defined (UNITY_REVERSED_Z)
                fragDepth = 1 - fragDepth; // (1,0)->(0,1)
                #endif

                if (fragDepth > 1) return 1;

                float strengthen = _CustomShadowStrengthen;
                
                float atten = 1;
                
                if (_CustomShadowSmoothType == 0) {// hard
                    float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv).xy);
                    if (fragDepth - _CustomShadowBias > shadowMapDepth) {
                        atten = 1 - strengthen;
                    }
                } else if (_CustomShadowSmoothType == 1) {// PCFs_Like
                    float2 offset = 0;
                    float minus_step = strengthen / 9.0;
                    for(int i = -1; i < 2; ++i) {
                        for(int j = -1; j < 2; ++j) {
                            offset = float2(i, j) * _CustomShadowMap_TexelSize.xy * _CustomShadowPCFSpread;
                            float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv + offset).xy);
                            if (fragDepth - _CustomShadowBias > shadowMapDepth) {
                                atten -= minus_step;
                            }
                        }
                    }
                } else if (_CustomShadowSmoothType == 2) {// SDFs_Like
                    float center_shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv).xy);
                    float distance = 1;
                    if (fragDepth - _CustomShadowBias > center_shadowMapDepth) {
                        distance = saturate(fragDepth- _CustomShadowBias - center_shadowMapDepth);
                        distance /= _CustomShadowBlurDistance;
                    }

                    float2 offset = 0;
                    float w = 0;
                    float idx = 0;
                    int i =0, j=0;
                    for(i = -2; i < 3; ++i) {
                        for(j = -2; j < 3; ++j) {
                            idx = (j+2) + (i+2) * 5;
                            w = _CustomShadowBlurWeight[idx];
                            w *= strengthen;
                            offset = float2(i, j) * _CustomShadowMap_TexelSize.xy * _CustomShadowPCFSpread * distance;
                            float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv + offset).xy);
                            if (fragDepth - _CustomShadowBias > shadowMapDepth) {
                                atten -= w;
                            }
                        }
                    }
                }
                return atten;
            }

            v2f vert (appdata v) {
                v2f o;

                // CSharp层脚本的第三个z分量最好不要在这里设置
				// 最好在shader中判断是GL还是DX平台来设置为近截面的z值就好
                // jave.lin : 在这里我们处理GL与DX的差异
                // GL的Z:-1~1,DX的Z:0~1
                #if defined (SHADER_TARGET_GLSL)
                v.vertex.z = -1;
                #else
                v.vertex.z = 0;
                #endif
                
                o.vertex = v.vertex;
                o.uv = v.uv;
                o.ray = _Ray[v.vid];
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                // to world from depth
                float linear01depth = (DecodeFloatRG(tex2D(_CustomDepthMap, i.uv).rg));
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linear01depth;
                // return float4(wp, 1);

                // world to lightspace
                // 取得当前绘制顶点相对光源空间下的坐标,即:阴影映射坐标
                float4 shadowCoord = mul(_CustomShadowMapLightSpaceMatrix, float4(wp, 1));

                // output SSSM atten
                return GetAtten(shadowCoord);
            }
            ENDCG
        }
    }
}

接收阴影处理:对SSSM采样

// jave.lin 2020.04.14 接收SSSM(Screen Space Shadow Map)阴影
Shader "Custom/ReceiveSSSM" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1, 1, 1, 1)
    }
    CGINCLUDE
    #include "UnityCG.cginc"
    #include"Lighting.cginc"
    sampler2D _MainTex;
    fixed4 _MainColor;

    // shadow
    sampler2D _ScreenSpaceShadowMap;            // sssm
    int _CustomShadowSmoothType;                // shadow 的边界平滑模式
    int _CustomShadowEnable;                    // shaodw 是否开启

    // lighting
    int _CustomLightEnable;                     // 光照是否开启
    int _CustomLightHalfLambert;                // 光照是否开启 half lambert
    int _CustomShadowMixToLight;                // shadow 混合到光照

    struct a2v {
        float4 vertex : POSITION;
        half3 normal : NORMAL;
        float2 uv : TEXCOORD0;
    };
    struct v2f {
        float4 vertex : SV_POSITION;
        float2 uv : TEXCOORD0;
        float4 shadowCoord : TEXCOORD1;
        half3 worldNormal : TEXCOORD2;
        float3 worldPos : TEXCOORD3;
        float4 screenPos : TEXCOORD4;
    };
    // 获取SSSM光照衰减系数
    float GetAtten(v2f i) {
        return tex2Dproj(_ScreenSpaceShadowMap, i.screenPos).r;
    }
    // 着色处理
    fixed4 shading(v2f i, float atten) {
        if (_CustomLightEnable) {
            // code here:
            // ambient
            // diffuse
            // specular
            // etc ...

            // albedo
            fixed4 albedo = tex2D(_MainTex, i.uv);
            i.worldNormal = normalize(i.worldNormal);

            //viewDir后面高光用
            half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
            half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
            // ambient
            fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo.rgb;
            // diffuse
            fixed LdotN = dot(lightDir, i.worldNormal);
            fixed diffuse = _CustomLightHalfLambert ? 
                    LdotN * 0.5 + 0.5 : 
                    max(0, LdotN);
            // specular
            fixed specular = 0;

            bool appliedAtten = appliedAtten = atten == 1; // Hard
            if (_CustomShadowMixToLight) {
                // PCFs_Like || SDFs_Like
                if (_CustomShadowSmoothType == 1 || _CustomShadowSmoothType == 2) {
                    appliedAtten = true; // 意味着模糊边缘的话,specular会乘上atten来衰减
                }
            }

            if (LdotN > 0 && appliedAtten) {
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                specular = pow(HdotN, 64);
            }

            fixed4 combinedCol = 0;
            // ambient 是模拟各个方向的光所以不需要atten
            combinedCol.xyz = ambient + 
                    diffuse * atten * albedo.rgb * _LightColor0.rgb * _MainColor.rgb +
                    specular * atten * _LightColor0.rgb;
            return combinedCol;
        } else {
            return tex2D(_MainTex, i.uv) * _MainColor * atten;
        }
    }
    v2f vert (a2v v) {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = v.uv;
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        o.screenPos = ComputeScreenPos(o.vertex);
        return o;
    }
    fixed4 frag (v2f i) : SV_Target {
        float atten = _CustomShadowEnable ? GetAtten(i) : 1;
        return shading(i, atten);
    }
    ENDCG
    SubShader {
        Tags { "RenderType"="Opaque" "MyShadowMap"="1" }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

运行效果

在这里插入图片描述

但是Scene视图中的阴影就有问题了。
具体什么原因不知道,但我就不纠结了。如下图:
在这里插入图片描述

FrameDebugger查看三大绘制过程

在这里插入图片描述

屏幕空间的优缺点

优点

貌似还不知道有点有啥用,为了研究一下而学习。(有可能是为了可以根据 深度容差来区别阴影边缘 + 阴影模糊,就可以比较轻松的做到“软”阴影的效果)

但看到有些文章说是为了所CSM(cascade shadow map)而用的(那意思:不在屏幕空间下做不可以了?显然不是的,因为CSM的整体思路就是对绘制的片段在世界坐标下与镜头的距离来做SM的层级的选择确定后再采样的,所以屏幕空间阴影有什么优点,我也暂时没有兴趣了解,后面那天需要用到,我再去细究

缺点

没写如深度的物体间接受不了阴影。如:半透明。

在非SSSM中,我们可以对半透明物体采样Shadow map来着色阴影的,如下图:
在这里插入图片描述
虽然可以接收阴影了。
但是半透明的物体,没有投射阴影是很奇怪的。


Project

backup : UnityShader_CustomShadow_includeSSSM_2018.3.0f2


2021/10/21 在阅读 Unity 2019.4.30f1 的 URP 7.7.1 版本的 shader : Packages/com.unity.render-pipeline.universal/Shaders/Utils/ScreenSpaceShadows.shader 中的代码,发现有一段很简单的就描述了如何生成 SSSM:(已添加了一些注释便于阅读理解)

        half4 Fragment(Varyings input) : SV_Target
        {
            UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

			// jave.lin : 获取相机拍摄深度
            float deviceDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv.xy).r;
			// jave.lin : 根据查看下图了解,不同的一些渲染平台,深度正值增长方向都可能有所不同
#if UNITY_REVERSED_Z
            deviceDepth = 1 - deviceDepth;
#endif
            deviceDepth = 2 * deviceDepth - 1; //NOTE: Currently must massage depth before computing CS position.
			// jave.lin : 将深度值 转换到 view space pos
            float3 vpos = ComputeViewSpacePosition(input.uv.zw, deviceDepth, unity_CameraInvProjection);
            // jave.lin : 将 view space pos 转到 world space pos
            float3 wpos = mul(unity_CameraToWorld, float4(vpos, 1)).xyz;
			// jave.lin : 将 world space world 转到 shadow/light space pos
            //Fetch shadow coordinates for cascade.
            float4 coords = TransformWorldToShadowCoord(wpos);

            // Screenspace shadowmap is only used for directional lights which use orthogonal projection.
            ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
            half4 shadowParams = GetMainLightShadowParams();
            // 根据相关配置参数 + shadow space pos 采样 + 判断是否中阴影中
            return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), coords, shadowSamplingData, shadowParams, false);
        }

UNITY_REVERSED_Z 的宏定义在下面几个文件中有定义,可以自行搜索:
在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值