GameTech/URP 渲染实战
源码 / shaders/Ch15/Halftone.glsl
shaders/Ch15/Halftone.glsl
// 半调网点(Halftone):在屏幕上铺一张旋转过的网格,每格画一个圆点,(GLSL 版,对应 Ch15/Halftone.shader)
// 圆点面积 = 该处“墨量”。单色模式只用黑墨;CMYK 模式四色各自用不同网角叠印。
Shader "Cookbook/Ch15/Halftone"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        GLSLINCLUDE
        uniform vec4 _BaseColor, _PaperColor;
        uniform float _CellSize;
        uniform float _BlackGen, _Contrast;
        ENDGLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            GLSLPROGRAM
            #pragma shader_feature_local_fragment _STYLE_OFF _STYLE_MONO _STYLE_CMYK
            #define NPR_VARYINGS
            #include "Ch15/NPRCommon.glsl"

            #ifdef VERTEX
            void main() { NprVert(); }
            #endif

            #ifdef FRAGMENT
            // 返回当前像素被网点覆盖的比例(0..1,已抗锯齿)
            // pixel:屏幕像素坐标;cell:网格边长(像素);angle:网角(度);ink:该处墨量 0..1
            float HalftoneDot(vec2 pixel, float cell, float angle, float ink)
            {
                float s = sin(radians(angle)), c = cos(radians(angle));
                // HLSL 的 mul(float2x2(c, -s, s, c), p):float2x2 按行构造,GLSL 的 mat2 按列,所以写成 p * mat2(...)
                vec2 p = (pixel * mat2(c, -s, s, c)) / cell;   // 旋转后的网格坐标
                vec2 local = fract(p) - 0.5;                    // 格子内坐标,中心为 0
                float d = length(local);
                // 圆面积 πr² 与格子面积 1 之比 = 墨量,所以半径与墨量的平方根成正比
                float r = sqrt(saturate(ink) / PI);
                float aa = max(fwidth(d), 1e-4);                // 约等于一个像素宽
                return 1.0 - smoothstep(r - aa, r + aa, d);
            }

            vec4 frag()
            {
                vec3 N = normalize(normalWS);
                NprLight L = GetNprLight(positionWS, N);
                vec3 lit = _BaseColor.rgb * (saturate(L.ndotl) * L.shadow * L.color + SampleSH(N));
                lit = saturate(lit);
            #if defined(_STYLE_OFF)
                return vec4(lit, 1.0);
            #else
                // 加一点对比度,印刷品的网点通常比连续色调“硬”
                lit = saturate((lit - 0.5) * _Contrast + 0.5);
                vec2 px = gl_FragCoord.xy;                      // = HLSL 的 i.positionCS.xy(像素坐标)
              #if defined(_STYLE_MONO)
                // 单色版只表现明暗:用光照本身的亮度,本色只轻微参与,否则深色物体会糊成一片灰
                vec3 lighting = saturate(L.ndotl) * L.shadow * L.color + SampleSH(N);
                float tone = Luminance(saturate(lighting)) * mix(1.0, Luminance(_BaseColor.rgb), 0.3);
                float ink = 1.0 - saturate((tone - 0.5) * _Contrast + 0.5);
                float cover = HalftoneDot(px, _CellSize, 45.0, ink);
                return vec4(mix(_PaperColor.rgb, vec3(0.08, 0.08, 0.1), cover), 1.0);
              #else
                // RGB → CMY,再把三色共同的“灰”部分交给黑墨(K),减少用墨、让暗部更干净
                vec3 cmy = 1.0 - lit;
                float k = min(cmy.r, min(cmy.g, cmy.b)) * _BlackGen;
                cmy = (cmy - k) / max(1.0 - k, 1e-3);

                // 经典网角:C 15°、M 75°、Y 0°、K 45°,错开角度避免摩尔纹
                float c  = HalftoneDot(px, _CellSize, 15.0, cmy.r);
                float m  = HalftoneDot(px, _CellSize, 75.0, cmy.g);
                float y  = HalftoneDot(px, _CellSize,  0.0, cmy.b);
                float kk = HalftoneDot(px, _CellSize, 45.0, k);

                // 减色叠印:每种墨把纸的颜色乘上自己的透射色
                vec3 col = _PaperColor.rgb;
                col *= mix(vec3(1.0), vec3(0.0, 0.66, 0.92), c);
                col *= mix(vec3(1.0), vec3(0.92, 0.1, 0.55), m);
                col *= mix(vec3(1.0), vec3(1.0, 0.92, 0.05), y);
                col *= mix(vec3(1.0), vec3(0.1, 0.1, 0.12), kk);
                return vec4(col, 1.0);
              #endif
            #endif
            }
            void main() { fragColor = frag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            GLSLPROGRAM
            #define NPR_SHADOW_PASS
            #include "Ch15/NPRCommon.glsl"
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            GLSLPROGRAM
            #define NPR_DEPTH_PASS
            #include "Ch15/NPRCommon.glsl"
            ENDGLSL
        }
    }
}