GameTech/URP 渲染实战

案例 20.9距离淡出:缩短与抖动裁剪进阶

目标:草在远处平滑地消失,而不是在某个距离“啪”地断掉。

草地必须有一个绘制距离:几十米外的草叶小于一个像素,画了也只是产生闪烁和过度绘制(Overdraw)。直接截断会在地面上留下一条清晰的边界线,相机一动边界线跟着移动,非常显眼。我们组合两种手段:

  1. 缩短:顶点着色器里按到相机的距离计算 fade,把整丛草的高度乘以 lerp(0.3, 1, fade)。远处的草先变矮,自然地“沉”进地面。
  2. 抖动裁剪(Dither Clip):片元着色器里用 4×4 Bayer 矩阵给每个像素一个阈值,clip(fade − 阈值)。fade 为 0.5 时恰好一半像素被丢弃。没有时间抗锯齿时能看到细小的网点(如图 20-17 右的远处),开启 TAA 后网点在帧间累积,看起来就像半透明;它仍然是不透明渲染,不需要排序、也能正常写深度。注意 MSAA 帮不上忙:clip 是按片元执行的,同一像素的多个采样一起被保留或丢弃。
Assets/Cookbook/Ch20/Grass.shader(节选:dither)
// 4×4 Bayer 抖动阈值(0..1):按屏幕像素位置给出不同阈值,用 clip 做“半透明”
float Bayer4x4(float2 pixel)
{
    const float m[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
    uint2 p = uint2(pixel) % 4;
    return (m[p.y * 4 + p.x] + 0.5) / 16.0;
}
// 4×4 Bayer 抖动阈值(0..1):按屏幕像素位置给出不同阈值,用 clip 做“半透明”
float Bayer4x4(vec2 pixel)
{
    const float m[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);
    uvec2 p = uvec2(pixel) % 4u;
    return (m[p.y * 4u + p.x] + 0.5) / 16.0;
}
图 20-17左:在 14 米处硬截断,草地边缘是一条整齐的线;右:9–15 米之间缩短 + 抖动淡出,草地逐渐稀疏、变矮,融入地面的草地贴图
移动端提示clip(Alpha Test)在 TBDR 架构的移动 GPU 上会破坏隐藏面剔除(HSR/Early-Z)的效率,大面积使用时代价明显。移动端可以只保留“缩短”,或者把淡出带放在非常远、覆盖像素很少的地方;更彻底的做法是按区块做 LOD:远处区块直接用更稀疏的实例数组。地面贴图的颜色也要与草的平均颜色接近,草消失后才不会露出一块不同色的地面——这也是为什么示范地形的草地层贴图偏深绿。