URP 渲染实战 / 第 23 章 阴影进阶 / 案例 23.6
案例 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 通道,带透明纹理的物体(比如彩色玻璃、树叶的半透明边缘)也能投出对应的花纹。

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