GameTech/URP 渲染实战
源码 / shaders/Ch15/RetroPS1.glsl
shaders/Ch15/RetroPS1.glsl
// 复古像素风(PS1 风格):顶点吸附到低分辨率网格、仿射纹理映射(不做透视校正)、(GLSL 版,对应 Ch15/RetroPS1.shader)
// 逐顶点光照、点采样,以及“抖动 + 色深量化”。每一项都能单独开关,方便对比。
Shader "Cookbook/Ch15/RetroPS1"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        GLSLINCLUDE
        uniform vec4  _BaseMap_ST;
        uniform vec4  _BaseColor;
        uniform vec4  _VirtualRes;
        uniform float _VertexSnap, _Affine, _PointSample, _Quantize, _ColorBits;

        // 把顶点吸附到 _VirtualRes 分辨率的像素格上。
        // 先除 w 得到 NDC,取整后再乘回 w,保持深度与透视不变,只让 xy 跳格。
        vec4 SnapToPixelGrid(vec4 positionCS, vec2 res)
        {
            vec2 halfRes = res * 0.5;
            vec2 ndc = positionCS.xy / positionCS.w;
            ndc = floor(ndc * halfRes + 0.5) / halfRes;
            positionCS.xy = ndc * positionCS.w;
            return positionCS;
        }
        ENDGLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            GLSLPROGRAM
            uniform sampler2D _BaseMap;

            VARYING vec2 uv;          // 正常的(透视校正的)UV
            VARYING vec3 uvAffine;    // (uv * w, w),用来抵消透视校正
            VARYING vec3 lighting;    // 逐顶点光照(Gouraud)

            #ifdef VERTEX
            void main()
            {
                vec3 positionWS = TransformObjectToWorld(positionOS.xyz);
                vec4 positionCS = TransformWorldToHClip(positionWS);
                if (_VertexSnap > 0.5) positionCS = SnapToPixelGrid(positionCS, _VirtualRes.xy);
                gl_Position = positionCS;

                uv = TransformTex(texcoord0.xy, _BaseMap_ST);
                // 透视校正插值对属性 A 实际做的是:插值 A/w 和 1/w 再相除。
                // 传入 A·w 和 w,两者都被“校正”后相除,w 抵消,剩下的就是屏幕空间线性插值的 A。
                uvAffine = vec3(uv * positionCS.w, positionCS.w);

                // 逐顶点 Lambert + 环境光:当年的硬件只在顶点上算光
                vec3 N = TransformObjectToWorldNormal(normalOS);
                Light L = GetMainLight();
                lighting = saturate(dot(N, L.direction)) * L.color + SampleSH(N);
            }
            #endif

            #ifdef FRAGMENT
            const float kBayer4[16] = float[16](
                 0.0,  8.0,  2.0, 10.0,
                12.0,  4.0, 14.0,  6.0,
                 3.0, 11.0,  1.0,  9.0,
                15.0,  7.0, 13.0,  5.0);

            // 先加一个有序抖动偏移,再截断到 bits 位:渐变变成规则的网点而不是色带
            vec3 DitherQuantize(vec3 c, vec2 pixel, float bits)
            {
                ivec2 p = ivec2(mod(pixel, 4.0));
                float threshold = (kBayer4[p.y * 4 + p.x] + 0.5) / 16.0 - 0.5;   // -0.5..0.5
                float levels = exp2(bits) - 1.0;
                return floor(saturate(c) * levels + 0.5 + threshold) / levels;
            }

            // HLSL 版用内联采样器 sampler_PointRepeat;GLSL 不能按调用换采样器,
            // 这里手动做“最近邻 + 最近 mip + 重复寻址”:先由导数估算 mip 级别,再 texelFetch
            vec4 SamplePointRepeat(sampler2D tex, vec2 st)
            {
                vec2 size0 = vec2(textureSize(tex, 0));
                vec2 dx = dFdx(st * size0), dy = dFdy(st * size0);
                float lod = 0.5 * log2(max(max(dot(dx, dx), dot(dy, dy)), 1e-8));
                int maxLod = int(log2(max(size0.x, size0.y)));
                int level = clamp(int(floor(lod + 0.5)), 0, maxLod);
                ivec2 size = textureSize(tex, level);
                ivec2 p = ivec2(fract(st) * vec2(size));
                return texelFetch(tex, clamp(p, ivec2(0), size - 1), level);
            }

            vec4 frag()
            {
                vec2 st = mix(uv, uvAffine.xy / uvAffine.z, _Affine);
                vec4 texLinear = texture(_BaseMap, st);
                vec4 texPoint  = SamplePointRepeat(_BaseMap, st);
                vec4 tex = mix(texLinear, texPoint, _PointSample);

                vec3 col = tex.rgb * _BaseColor.rgb * lighting;
                if (_Quantize > 0.5)
                {
                    // 抖动图案也按“虚拟分辨率”放大,模拟低分辨率帧缓冲上的抖动
                    vec2 virtualPixel = floor(gl_FragCoord.xy * _VirtualRes.xy / _ScreenParams.xy);
                    // 在 sRGB(显示)空间量化,色阶才均匀
                    col = SRGBToLinear(DitherQuantize(LinearToSRGB(col), virtualPixel, _ColorBits));
                }
                return vec4(col, 1.0);
            }
            void main() { fragColor = frag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            GLSLPROGRAM
            // 深度也要按同样方式吸附,否则深度图与颜色错位
            #ifdef VERTEX
            void main()
            {
                vec4 positionCS = TransformObjectToHClip(positionOS.xyz);
                if (_VertexSnap > 0.5) positionCS = SnapToPixelGrid(positionCS, _VirtualRes.xy);
                gl_Position = positionCS;
            }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = vec4(0.0); }
            #endif
            ENDGLSL
        }
    }
}