GameTech/URP 渲染实战
源码 / Ch04/C04_09_SDFIcon.shader
Ch04/C04_09_SDFIcon.shader
// 案例 4.9:用 2D SDF 拼一个“定位图钉”图标,并加描边、外发光和投影。
Shader "Cookbook/Ch04/SDFIcon"
{
    Properties
    {
        [Enum(Field,0,Booleans,1,Flat,2,Styled,3)] _Mode ("Mode", Float) = 3
        _FillColor ("Fill", Color) = (0.91, 0.36, 0.20, 1)
        _StrokeColor ("Stroke", Color) = (1, 1, 1, 1)
        _StrokeWidth ("Stroke Width", Range(0, 0.1)) = 0.025
        [HDR] _GlowColor ("Glow", Color) = (1.2, 0.55, 0.25, 1)
        _GlowFalloff ("Glow Falloff", Float) = 9
        _ShadowOffset ("Shadow Offset (xy)", Vector) = (0.03, -0.05, 0, 0)
        _ShadowSoftness ("Shadow Softness", Range(0.001, 0.2)) = 0.06
        _BgColor ("Background", Color) = (0.13, 0.17, 0.22, 1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        Pass
        {
            Name "Unlit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex ProceduralVert
            #pragma fragment frag
            #include "Assets/Cookbook/Ch04/ProceduralCommon.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float  _Mode, _StrokeWidth, _GlowFalloff, _ShadowSoftness;
                float4 _ShadowOffset;
                half4  _FillColor, _StrokeColor, _GlowColor, _BgColor;
            CBUFFER_END

            // 图钉 = 圆(头) ∪ 旋转 45° 的方块(尖) − 小圆(孔)
            // 方块的两条下边恰好与圆相切:尖端到圆心距离 = R·√2
            float PinSDF(float2 p)
            {
                const float R = 0.30;
                float2 headCenter = float2(0.0, 0.15);
                float head = SdCircle(p - headCenter, R);
                float halfDiag = R * 0.70710678;                         // 方块半对角线 = R/√2
                float2 tipCenter = headCenter - float2(0.0, halfDiag);    // 方块中心在圆心正下方
                float2 tp = Rotate2D(p - tipCenter, radians(45.0));
                float tip = SdRoundBox(tp, halfDiag * 0.70710678, 0.012); // 半边长 = 半对角线/√2,小圆角让尖端不扎手
                float body = OpUnion(head, tip);   // 两者相切,直接取 min 就是光滑的;这里用平滑并集反而会鼓出一个包
                float hole = SdCircle(p - headCenter, 0.12);
                return OpSubtract(body, hole);
            }

            // 把距离场画出来:外部暖色、内部冷色,等高线间隔 0.05,零等值线为白
            half3 VisualizeField(float d)
            {
                half3 col = (d > 0.0) ? half3(0.90, 0.55, 0.30) : half3(0.25, 0.55, 0.85);
                col *= 1.0 - exp(-5.0 * abs(d));                   // 离边界越远越亮
                col *= 0.75 + 0.25 * cos(TWO_PI * d / 0.05);       // 等高线
                return lerp(col, half3(1, 1, 1), 1.0 - smoothstep(0.0, 0.012, abs(d)));
            }

            half3 Booleans(float2 uv)
            {
                // 2×2 网格,每格一种布尔运算
                float2 cellId = floor(uv * 2.0);
                float2 p = (frac(uv * 2.0) - 0.5) * 2.0;           // 每格内 [-1,1]
                float a = SdCircle(p - float2(-0.2, 0.05), 0.45);
                float b = SdBox(Rotate2D(p - float2(0.28, -0.08), 0.4), float2(0.32, 0.32));
                int op = (int)(cellId.x + (1.0 - cellId.y) * 2.0); // 左上 0 右上 1 左下 2 右下 3
                float d = (op == 0) ? OpUnion(a, b)
                        : (op == 1) ? OpSubtract(a, b)
                        : (op == 2) ? OpIntersect(a, b)
                        :             OpSmoothUnion(a, b, 0.25);
                half3 col = _BgColor.rgb;
                // 淡淡画出两个原始形状的轮廓
                col = lerp(col, half3(0.45, 0.5, 0.56), 1.0 - smoothstep(0.0, fwidth(a) * 1.5, abs(a)));
                col = lerp(col, half3(0.45, 0.5, 0.56), 1.0 - smoothstep(0.0, fwidth(b) * 1.5, abs(b)));
                col = lerp(col, _FillColor.rgb, FillAA(d) * 0.9);
                // 格子分隔线
                float2 g = abs(frac(uv * 2.0 + 0.5) - 0.5) / fwidth(uv * 2.0);
                return lerp(col, half3(0.9, 0.92, 0.95), 1.0 - saturate(min(g.x, g.y) - 0.5));
            }

            half4 frag (Varyings input) : SV_Target
            {
                float2 p = (input.uv - 0.5) * 1.1;
                if (_Mode > 0.5 && _Mode < 1.5) return half4(Booleans(input.uv), 1);
                float d = PinSDF(p);
                if (_Mode < 0.5) return half4(VisualizeField(d), 1);
                if (_Mode < 2.5) return half4(lerp(_BgColor.rgb, _FillColor.rgb, FillAA(d)), 1);

                half3 col = _BgColor.rgb;
                // 1) 投影:在偏移后的位置再求一次距离,用宽 smoothstep 得到软边
                float ds = PinSDF(p - _ShadowOffset.xy);
                col *= 1.0 - 0.6 * (1.0 - smoothstep(-_ShadowSoftness, _ShadowSoftness, ds));
                // 2) 外发光:只在形状外面,按距离指数衰减
                col += _GlowColor.rgb * exp(-max(d, 0.0) * _GlowFalloff) * 0.35;
                // 3) 填充:内部按距离做一点明暗,越靠近边越暗
                half3 fill = _FillColor.rgb * lerp(0.75, 1.15, saturate(-d * 8.0));
                col = lerp(col, fill, FillAA(d));
                // 4) 描边:|d| - 半宽 是“沿边界的一条带子”的 SDF
                float stroke = abs(d) - _StrokeWidth * 0.5;
                col = lerp(col, _StrokeColor.rgb, FillAA(stroke));
                return half4(col, 1);
            }
            ENDHLSL
        }
    }
}