GameTech/URP 渲染实战
源码 / Ch14/ToonFace.shader
Ch14/ToonFace.shader
// 面部 SDF 阴影:脸上的明暗交界不再由法线决定,而是查一张“阈值图”——
// 图里每个像素存的是“光从正面转到多少度时,这里开始进入阴影”。
Shader "Cookbook/Ch14/ToonFace"
{
    Properties
    {
        [MainColor] _BaseColor ("Skin Color", Color) = (1.0, 0.86, 0.78, 1)
        _ShadeColor ("Shade Color", Color) = (0.9, 0.62, 0.62, 1)
        [KeywordEnum(Normal, SDF)] _FaceShadow ("Face Shadow", Float) = 1
        [NoScaleOffset] _FaceSdf ("Face Threshold Map (R)", 2D) = "white" {}
        _SdfSoftness ("Edge Softness", Range(0, 0.1)) = 0.01
        [Toggle] _DrawFeatures ("Draw Eyes/Mouth", Float) = 1
        _OutlineColor ("Outline Color", Color) = (0.15, 0.1, 0.12, 1)
        _OutlineWidth ("Outline Width (px)", Range(0, 10)) = 0
    }

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

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
        CBUFFER_START(UnityPerMaterial)
            half4 _BaseColor, _ShadeColor;
            half  _SdfSoftness, _DrawFeatures;
            half4 _OutlineColor;
            half  _OutlineWidth;
        CBUFFER_END
        TEXTURE2D(_FaceSdf); SAMPLER(sampler_FaceSdf);
        ENDHLSL

        Pass
        {
            Name "ToonForward"
            Tags { "LightMode"="UniversalForwardOnly" }

            HLSLPROGRAM
            #pragma target 3.0
            #pragma vertex ToonVert
            #pragma fragment frag
            #pragma shader_feature_local_fragment _FACESHADOW_NORMAL _FACESHADOW_SDF
            #pragma multi_compile_fog
            #pragma multi_compile_instancing
            #include "Assets/Cookbook/Ch14/ToonCommon.hlsl"

            // 简笔五官(只为让球看起来像一张脸):两只眼睛、腮红和嘴
            half3 DrawFeatures(half3 color, float2 uv, float frontMask)
            {
                float2 e1 = (uv - float2(0.36, 0.56)) / float2(0.045, 0.07);
                float2 e2 = (uv - float2(0.64, 0.56)) / float2(0.045, 0.07);
                float eye = step(min(dot(e1, e1), dot(e2, e2)), 1.0);
                float2 h1 = (uv - float2(0.35, 0.58)) / 0.018, h2 = (uv - float2(0.63, 0.58)) / 0.018;
                float hl = step(min(dot(h1, h1), dot(h2, h2)), 1.0);
                float2 b1 = (uv - float2(0.3, 0.45)) / float2(0.07, 0.035), b2 = (uv - float2(0.7, 0.45)) / float2(0.07, 0.035);
                float blush = smoothstep(1.0, 0.3, min(dot(b1, b1), dot(b2, b2)));
                float2 m = uv - float2(0.5, 0.36);
                float mouth = step(abs(length(m / float2(1, 0.7) + float2(0, 0.05)) - 0.06), 0.006) * step(m.y, -0.005);
                color = lerp(color, color * half3(1.0, 0.75, 0.75), blush * 0.6 * frontMask);
                color = lerp(color, half3(0.18, 0.12, 0.2), max(eye, mouth) * frontMask);
                color = lerp(color, half3(1, 1, 1), hl * frontMask);
                return color;
            }

            half4 frag(ToonVaryings i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);
                Light light = GetMainLight();
                float2 faceUV = i.positionOS.xy + 0.5;           // 正面投影:物体空间 xy → 0~1
                half lit;

            #if defined(_FACESHADOW_NORMAL)
                // 对照组:按法线做色阶,鼻子、眼窝的起伏会让阴影边缘碎成一块一块
                half3 N = normalize(i.normalWS);
                lit = ToonBand(dot(N, light.direction) * 0.5 + 0.5, 0.5, 0.01);
            #else
                // 1) 头部的朝向(物体空间 -Z 是脸的正前方,+X 是画面右侧)
                float3 F = normalize(TransformObjectToWorldDir(float3(0, 0, -1)));
                float3 R = normalize(TransformObjectToWorldDir(float3(1, 0, 0)));
                float3 U = normalize(TransformObjectToWorldDir(float3(0, 1, 0)));
                // 2) 光方向投影到头部的水平面,只关心“从哪一侧、转了多少度”
                float3 Lh = light.direction - U * dot(light.direction, U);
                Lh = normalize(Lh + 1e-5);
                float phi = acos(clamp(dot(F, Lh), -1.0, 1.0)) / PI;   // 0 = 正面光,1 = 正背光
                // 3) 阈值图是按“光从右边来”画的,光从左边来就把 U 镜像
                float2 uv = faceUV;
                if (dot(R, Lh) < 0) uv.x = 1.0 - uv.x;
                half threshold = SAMPLE_TEXTURE2D(_FaceSdf, sampler_FaceSdf, uv).r;
                // 4) 当前角度小于该像素的阈值 → 亮
                lit = smoothstep(phi - _SdfSoftness, phi + _SdfSoftness, threshold);
            #endif

                half3 color = lerp(_ShadeColor.rgb, _BaseColor.rgb, lit) * light.color;
                color += SampleSH(half3(0, 0, 0)) * _BaseColor.rgb * 0.25;
                if (_DrawFeatures > 0.5)
                    color = DrawFeatures(color, faceUV, step(i.positionOS.z, 0.0));
                return half4(MixFog(color, i.fogFactor), 1);
            }
            ENDHLSL
        }

        Pass
        {
            Name "Outline"
            Tags { "LightMode"="SRPDefaultUnlit" }
            Cull Front
            ZWrite On
            HLSLPROGRAM
            #pragma vertex OutlineVert
            #pragma fragment OutlineFrag
            #pragma multi_compile_fog
            #pragma multi_compile_instancing
            #include "Assets/Cookbook/Ch14/ToonOutlinePass.hlsl"
            ENDHLSL
        }
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0 Cull Back
            HLSLPROGRAM
            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment
            #pragma multi_compile_instancing
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            // URP 自带的阴影 Pass 只在 _ALPHATEST_ON 时才读 _BaseMap,其余部分与材质无关,直接复用
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }
        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R Cull Back
            HLSLPROGRAM
            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment
            #pragma multi_compile_instancing
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
            ENDHLSL
        }
        Pass
        {
            Name "DepthNormals"
            Tags { "LightMode"="DepthNormals" }
            ZWrite On Cull Back
            HLSLPROGRAM
            #pragma vertex DepthNormalsVertex
            #pragma fragment DepthNormalsFragment
            #pragma multi_compile_instancing
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthNormalsPass.hlsl"
            ENDHLSL
        }
    }
}