GameTech/URP 渲染实战

案例 23.6半透明物体的抖动阴影进阶

Assets/Cookbook/Ch23/DitherShadow.shader(节选:dither)
// 4×4 Bayer 矩阵:相邻阈值尽量分散,丢弃的像素在空间上均匀
static const half kBayer4x4[16] =
{
     0,  8,  2, 10,
    12,  4, 14,  6,
     3, 11,  1,  9,
    15,  7, 13,  5
};

half4 frag(Varyings i) : SV_Target
{
    half alpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).a * _BaseColor.a * _ShadowAlphaScale;
    if (_DitherShadow > 0.5)
    {
        // 在 ShadowCaster 里,SV_POSITION.xy 就是阴影贴图上的像素坐标
        uint2 p = uint2(i.positionCS.xy) & 3;
        half threshold = (kBayer4x4[p.y * 4 + p.x] + 0.5) / 16.0;
        clip(alpha - threshold);
    }
    return 0;
}
// 4×4 Bayer 矩阵:相邻阈值尽量分散,丢弃的像素在空间上均匀
const float kBayer4x4[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
);

void main()
{
    float alpha = texture(_BaseMap, uv).a * _BaseColor.a * _ShadowAlphaScale;
    if (_DitherShadow > 0.5)
    {
        // 在 ShadowCaster 里,gl_FragCoord.xy 就是阴影贴图上的像素坐标
        uvec2 p = uvec2(gl_FragCoord.xy) & 3u;
        float threshold = (kBayer4x4[p.y * 4u + p.x] + 0.5) / 16.0;
        if (alpha - threshold < 0.0) discard;
    }
    fragColor = vec4(0.0);
}

ShadowCaster 片元着色器里的 SV_POSITION.xy 是阴影贴图上的像素坐标,直接对 4 取模就能得到 Bayer 矩阵的位置。Bayer 矩阵的特点是相邻阈值在空间上尽量分散,所以任意 alpha 下保留的像素都比较均匀,平均后不会出现明显的斑块。透明度同时乘上了贴图的 alpha 通道,带透明纹理的物体(比如彩色玻璃、树叶的半透明边缘)也能投出对应的花纹。

图 23-17半透明玻璃板和玻璃球的阴影。左:alpha = 0.4,但投射完全不透明的阴影(相当于 URP Lit 透明材质的默认行为);中:抖动阴影,影子浓度随透明度变淡;右:alpha = 0.15,影子更淡。前面的不透明方块作为对照

这个方法依赖软阴影:硬阴影下网点会直接显现成规则的花纹。阴影分辨率较低、网点大于屏幕像素时也会看到纹理感,此时可以把 Bayer 换成蓝噪声贴图,或者在接收端用 High 档 PCF。另一个要注意的是,透明物体自己的 Forward Pass 一般不接收自身的抖动阴影(会看到自己身上的网点),本例的透明 Pass 干脆不接收阴影。