GameTech/URP 渲染实战

案例 19.7岸边泡沫进阶

目标:岸线、礁石和柱子周围出现白色泡沫:贴岸的一条不规则实心带,外面还有一圈圈向岸边推进的细泡沫线。

原理:竖直水深就是“离岸距离”

岸边泡沫出现在水浅的地方。竖直水深在岸线处为 0,向外逐渐增大,所以 shore = saturate(水深 / 泡沫带宽度) 就是一个“离岸距离”:岸线 0,泡沫带外沿 1。在它上面做两件事:

  1. 实心带:shore 小于某个阈值的地方全是泡沫。阈值被噪声上下扰动,带子的外沿就呈现不规则的锯齿。
  2. 推进线:frac(shore × 2.5 − t × 0.3) 是一组“等深线”,随时间减小,等深线就一圈圈向岸边移动;取其中很窄的一段作为细线,再用噪声把线打断成一截一截。这正是海浪拍岸时泡沫线的运动方式。
Assets/Cookbook/Ch19/WaterCore.hlsl(节选:foam)
// 岸边泡沫 = 贴岸的一条实心带 + 一圈圈向岸边推进的细线,都用噪声打散
half ShoreFoam(float2 xz, float verticalDepth, half3 normalTS, float t)
{
    half shore = saturate(verticalDepth / _FoamDistance);          // 0 在岸线,1 在泡沫带外沿
    float2 uv = xz * _FoamTiling + normalTS.xy * 0.1 + float2(t * 0.02, t * 0.015);
    half n = SAMPLE_TEXTURE2D(_FoamNoise, sampler_FoamNoise, uv).r;

    // ① 实心带:阈值被噪声上下扰动,边缘呈不规则的锯齿
    half core = 1.0h - smoothstep(0.25h - _FoamSoftness, 0.25h + _FoamSoftness, shore + (n - 0.5h) * 0.35h);
    // ② 推进线:shore 是“离岸距离”,减去时间就得到一圈圈朝岸边移动的等深线
    half ripple = frac(shore * 2.5h - t * 0.3h + n * 0.6h);
    half lines = smoothstep(0.8h, 0.9h, ripple) * step(0.45h, n) * (1.0h - shore);
    return saturate(core + lines);
}
// 岸边泡沫 = 贴岸的一条实心带 + 一圈圈向岸边推进的细线,都用噪声打散
float ShoreFoam(vec2 xz, float verticalDepth, vec3 normalTS, float t)
{
    float shore = saturate(verticalDepth / _FoamDistance);          // 0 在岸线,1 在泡沫带外沿
    vec2 uv = xz * _FoamTiling + normalTS.xy * 0.1 + vec2(t * 0.02, t * 0.015);
    float n = texture(_FoamNoise, uv).r;

    // ① 实心带:阈值被噪声上下扰动,边缘呈不规则的锯齿
    float core = 1.0 - smoothstep(0.25 - _FoamSoftness, 0.25 + _FoamSoftness, shore + (n - 0.5) * 0.35);
    // ② 推进线:shore 是“离岸距离”,减去时间就得到一圈圈朝岸边移动的等深线
    float ripple = fract(shore * 2.5 - t * 0.3 + n * 0.6);
    float lines = smoothstep(0.8, 0.9, ripple) * step(0.45, n) * (1.0 - shore);
    return saturate(core + lines);
}

噪声采样坐标也加了一点法线扰动,泡沫会跟着水面波纹轻微晃动;泡沫颜色乘了 waterLight,在阴影里会变暗,不会像贴纸一样浮在画面上。

图 19-10左:没有泡沫;中:调试视图,泡沫遮罩——岸线的实心带、外圈的推进线,以及顶部接近水面的暗礁和柱子周围的泡沫;右:最终效果。

深度泡沫的局限

注意中图柱子周围的泡沫:只有柱子朝向相机的一侧有泡沫带。原因是泡沫靠“这个像素看下去,水底有多深”来判断:柱子前方的水面像素,看下去是柱子的水下部分,离水面很近,所以是浅水;柱子后方的水面像素被柱子本身挡住了,根本没有被画出来;而柱子两侧的水面像素,看下去是远处的水底,是深水。这是所有基于屏幕深度的泡沫(以及护盾交接高亮)的共同局限:它测的是“视线方向上的距离”,而不是“到物体表面的真实距离”。

要做到四周均匀的泡沫,需要真正的距离信息:例如离线烘焙一张水面的“到岸/到障碍物的距离场”贴图,或者用 SDF。动态物体(船、角色)周围的泡沫,常见做法是由物体自己在一张俯视的 RT 上绘制“泡沫源”,水面再采样这张 RT——这和第 20 章草地交互的思路一样。