GameTech/URP 渲染实战

案例 26.7像素化与调色板抖动入门

目标:复古像素风:画面由大块的“像素”组成,颜色只有少数几级,并用有序抖动(Ordered Dithering)模拟中间色。

原理:把 UV 吸附到“大像素”格子的中心,用点采样取色——同一格里所有屏幕像素读到同一个颜色。色阶量化在 sRGB 空间进行,因为人眼对暗部更敏感,在线性空间均匀分级会让暗部台阶过粗。抖动用 4×4 Bayer 矩阵:给每个大像素一个 [−0.5, 0.5) 的阈值偏移再四舍五入,相邻格子在两个色阶之间交替,远看就是中间色:

Assets/Cookbook/Ch26/Pixelate.shader(节选:frag)
half4 Frag(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 screen = _ScreenParams.xy;
    float size = _PixelateParams.x;

    // 1) 当前像素属于哪个“大像素”,取它中心的颜色(点采样,避免边缘被双线性混色)
    float2 block = floor(input.texcoord * screen / size);
    float2 uv = (block + 0.5) * size / screen;
    half3 c = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_PointClamp, uv).rgb;

    // 2) 色阶量化:在 sRGB 空间均匀分级(线性空间分级会让暗部台阶过粗)
    float levels = _PixelateParams.y;
    if (levels > 0.5)
    {
        half3 s = LinearToSRGB(saturate(c));
        float threshold = (Bayer4((uint2)block) - 0.5) * _PixelateParams.z;   // 以大像素为单位抖动
        s = floor(s * (levels - 1.0) + 0.5 + threshold) / (levels - 1.0);
        c = SRGBToLinear(saturate(s));
    }
    return half4(c, 1.0);
}
vec4 Frag()
{
    vec2 screen = _ScreenParams.xy;
    float size = _PixelateParams.x;

    // 1) 当前像素属于哪个“大像素”,取它中心的颜色(点采样,避免边缘被双线性混色)
    vec2 block = floor(texcoord * screen / size);
    vec2 uv = (block + 0.5) * size / screen;
    // GLSL 不能按调用指定 sampler_PointClamp:用 texelFetch 按整数纹素坐标取
    ivec2 texSize = textureSize(_BlitTexture, 0);
    vec3 c = texelFetch(_BlitTexture, clamp(ivec2(uv * vec2(texSize)), ivec2(0), texSize - 1), 0).rgb;

    // 2) 色阶量化:在 sRGB 空间均匀分级(线性空间分级会让暗部台阶过粗)
    float levels = _PixelateParams.y;
    if (levels > 0.5)
    {
        vec3 s = LinearToSRGB(saturate(c));
        float threshold = (Bayer4(uvec2(block)) - 0.5) * _PixelateParams.z;   // 以大像素为单位抖动
        s = floor(s * (levels - 1.0) + 0.5 + threshold) / (levels - 1.0);
        c = SRGBToLinear(saturate(s));
    }
    return vec4(c, 1.0);
}
图 26-14左:5 像素一格,不量化;中:每通道 6 级、无抖动,天空与地面出现明显的色带;右:每通道 6 级、Bayer 抖动,色带被打散成规则的网点图案
提示抖动的坐标用的是“大像素”坐标 block,而不是屏幕像素坐标——否则一个大像素内部会出现细碎的网点,破坏像素风。另一个常见需求是“像素不随相机平移而游动”:屏幕空间像素化在相机移动时,格子相对于场景在滑动,会显得闪烁。真正的像素风游戏通常是让相机以低分辨率渲染到 RT、并把相机位置吸附到像素网格上,再整数倍放大显示,那属于相机层面的方案。