GameTech/URP 渲染实战
源码 / shaders/Ch22/ScreenSpaceDecal.glsl
shaders/Ch22/ScreenSpaceDecal.glsl
// 案例 22.4–22.7:自写屏幕空间贴花——画一个单位立方体,用深度重建世界坐标,投到盒子本地空间做 UV(GLSL 版)
// 约定与 URP Decal Projector 相同:沿物体 +Z 方向投影,盒子是 [-0.5, 0.5]³
Shader "Cookbook/Ch22/ScreenSpaceDecal"
{
    SubShader
    {
        // 在不透明物体之后、普通透明物体之前画:此时 _CameraDepthTexture 已经可用
        Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent-100" "IgnoreProjector"="True" "DisableBatching"="True" }

        Pass
        {
            Name "ScreenSpaceDecal"
            Tags { "LightMode"="UniversalForward" }
            Cull Front        // 画盒子的“背面”:相机钻进盒子里也不会丢
            ZTest Greater     // 背面比场景表面“更远”才通过 = 场景表面在盒子里(或盒子前方)
            ZWrite Off
            Blend [_SrcBlend] [_DstBlend]

            GLSLPROGRAM
            #pragma shader_feature_local_fragment _NORMALMAP
            #pragma shader_feature_local_fragment _DECAL_LIT

            uniform vec4 _BaseMap_ST;
            uniform vec4 _BaseColor;
            uniform float _BumpScale, _Smoothness;
            uniform float _AngleStart, _AngleEnd, _DepthFade;
            uniform float _Modulate, _Debug;
            uniform sampler2D _BaseMap;
            uniform sampler2D _BumpMap;

            #ifdef VERTEX
            void main()
            {
                gl_Position = TransformObjectToHClip(positionOS.xyz);
            }
            #endif

            #ifdef FRAGMENT
            vec4 frag()
            {
                if (_Debug > 0.5 && _Debug < 1.5) return vec4(1.0, 0.35, 0.1, 0.6);   // 盒子覆盖到的像素

                // 1) 屏幕 UV → 场景深度 → 世界坐标
                vec2 screenUV = GetNormalizedScreenSpaceUV(gl_FragCoord);
                float rawDepth = SampleSceneDepth(screenUV);
                // HLSL 版在 OpenGL 类平台要先把 [0,1] 深度换成 [-1,1];GLSL 版的 ComputeWorldSpacePosition 内部已经做了
                vec3 positionWS = ComputeWorldSpacePosition(screenUV, rawDepth, UNITY_MATRIX_I_VP);

                // 2) 世界坐标 → 贴花盒子的本地空间;盒子外的像素直接丢掉
                vec3 positionOS = TransformWorldToObject(positionWS);
                if (any(lessThan(0.5 - abs(positionOS), vec3(0.0)))) discard;

                if (_Debug > 1.5 && _Debug < 2.5) return vec4(positionOS + 0.5, 1);

                // 3) 用屏幕空间导数重建表面法线(平面法线,物体轮廓处会有噪点)
                vec3 normalWS = normalize(cross(dFdx(positionWS), dFdy(positionWS)));
                normalWS *= sign(dot(normalWS, GetWorldSpaceViewDir(positionWS)));   // 统一朝向相机,免去平台 Y 方向差异

                // 4) 角度衰减:表面越“侧”对投影方向,贴花越淡,避免在侧壁上拉出长条
                vec3 projDirWS = normalize(TransformObjectToWorldDir(vec3(0, 0, 1)));
                float facing = dot(normalWS, -projDirWS);                            // 1 = 正对投影器
                float cosStart = cos(radians(_AngleStart)), cosEnd = cos(radians(_AngleEnd));
                float angleFade = saturate((facing - cosEnd) / max(cosStart - cosEnd, 1e-4));
                // 盒子前后两端也淡出,贴花不会在投影深度的尽头被一刀切断
                float depthFade = saturate((0.5 - abs(positionOS.z)) / _DepthFade);

                if (_Debug > 2.5) return vec4(normalWS * 0.5 + 0.5, 1);

                vec2 boxUV = positionOS.xy + 0.5;              // 盒子本地 xy → 0..1 的贴花 UV
                vec2 uv = TransformTex(boxUV, _BaseMap_ST);
                vec4 decal = texture(_BaseMap, uv) * _BaseColor;
                float alpha = decal.a * angleFade * depthFade;

                vec3 color = decal.rgb;
            #if defined(_DECAL_LIT)
                vec3 n = normalWS;
                #if defined(_NORMALMAP)
                    // 贴花自己的法线贴图:切线空间就是投影器的 X/Y 轴,“上”是 -投影方向
                    vec3 nTS = UnpackNormalScale(texture(_BumpMap, uv), _BumpScale);
                    vec3 T = normalize(TransformObjectToWorldDir(vec3(1, 0, 0)));
                    vec3 B = normalize(TransformObjectToWorldDir(vec3(0, 1, 0)));
                    // 把法线贴图的扰动量(xy)叠加到重建的表面法线上(UDN 式混合)
                    n = normalize(n + (T * nTS.x + B * nTS.y) * decal.a);
                #endif
                InputData inputData = InitInputData();
                inputData.positionWS = positionWS;
                inputData.normalWS = n;
                inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(positionWS);
                inputData.shadowCoord = TransformWorldToShadowCoord(positionWS);
                inputData.bakedGI = SampleSH(n);
                inputData.normalizedScreenSpaceUV = screenUV;
                inputData.shadowMask = vec4(1, 1, 1, 1);
                SurfaceData surface = InitSurfaceData();
                surface.albedo = decal.rgb;
                surface.metallic = 0.0;
                surface.smoothness = _Smoothness;
                surface.occlusion = 1.0;
                surface.alpha = alpha;
                color = UniversalFragmentPBR(inputData, surface).rgb;
            #endif
                // 正片叠底模式:alpha 折算进颜色(透明处 = 白 = 不改变背景)
                color = mix(color, mix(vec3(1.0), decal.rgb, alpha), _Modulate);
                return vec4(color, alpha);
            }
            void main() { fragColor = frag(); }
            #endif
            ENDGLSL
        }
    }
}