GameTech/URP 渲染实战

案例 19.9Gerstner 波进阶

目标:让水面真正起伏,有波峰波谷,并且波峰比波谷尖——这是 Gerstner 波和简单正弦波最明显的区别。

第 17 章已经推导过 Gerstner 波:水面上的每个点不只是上下运动,还会沿传播方向前后运动,做圆周运动。一个波由方向 d、陡度 Q、波长 L 决定:

k=2πL,c=gk,a=Qk,f=k(d·pxz−ct)
Δp=(dxacosf,asinf,dzacosf)

相速度 c 来自深水波的色散关系 ω² = gk:长波跑得快、短波跑得慢,多个波叠加时自然会“错开”,不会整齐地同步。Q = 1 时波峰恰好变成尖角,再大就会自交打结,所以多个波叠加时它们的陡度之和最好不超过 1。

移植到水面 Shader 时要注意两件事。一是法线必须解析地求:位移函数对 x、z 求偏导得到切线 T 和副切线 B,法线 = B × T。函数里用 inout 把每个波的偏导累加进去,三个波叠加完一次性得到正确的切线空间。二是切线空间要同步给法线贴图:波面倾斜后,细节法线也要跟着倾斜,所以顶点着色器把 T、B、N 一起传给片元,片元用它们构造 TBN 矩阵(案例 19.4)。

Assets/Cookbook/Ch19/WaterCore.hlsl(节选:gerstner)
// 单个 Gerstner 波。wave = (方向x, 方向z, 陡度 Q, 波长 L)。累加位移,同时累加切线/副切线用于求法线。
// 推导见第 17 章:k = 2π/L,相速度 c = sqrt(g/k),振幅 a = Q/k(Q=1 时波峰刚好变尖)
float3 GerstnerWave(float4 wave, float3 p, float t, inout float3 tangent, inout float3 binormal)
{
    float steep = wave.z;
    float k = 2.0 * PI / wave.w;
    float c = sqrt(9.8 / k);
    float2 d = normalize(wave.xy);
    float f = k * (dot(d, p.xz) - c * t);
    float a = steep / k;
    float s = sin(f), co = cos(f);

    tangent  += float3(-d.x * d.x * steep * s, d.x * steep * co, -d.x * d.y * steep * s);
    binormal += float3(-d.x * d.y * steep * s, d.y * steep * co, -d.y * d.y * steep * s);
    return float3(d.x * a * co, a * s, d.y * a * co);
}
// 单个 Gerstner 波。wave = (方向x, 方向z, 陡度 Q, 波长 L)。累加位移,同时累加切线/副切线用于求法线。
// 推导见第 17 章:k = 2π/L,相速度 c = sqrt(g/k),振幅 a = Q/k(Q=1 时波峰刚好变尖)
vec3 GerstnerWave(vec4 wave, vec3 p, float t, inout vec3 tangent, inout vec3 binormal)
{
    float steep = wave.z;
    float k = 2.0 * PI / wave.w;
    float c = sqrt(9.8 / k);
    vec2 d = normalize(wave.xy);
    float f = k * (dot(d, p.xz) - c * t);
    float a = steep / k;
    float s = sin(f), co = cos(f);

    tangent  += vec3(-d.x * d.x * steep * s, d.x * steep * co, -d.x * d.y * steep * s);
    binormal += vec3(-d.x * d.y * steep * s, d.y * steep * co, -d.y * d.y * steep * s);
    return vec3(d.x * a * co, a * s, d.y * a * co);
}
Assets/Cookbook/Ch19/Water.shader(节选:vert)
Varyings vert(Attributes v)
{
    Varyings o;
    float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
    // 平静水面:切线 +X、副切线 +Z、法线 +Y
    float3 tangent = float3(1, 0, 0);
    float3 binormal = float3(0, 0, 1);
#if defined(_GERSTNER)
    float t = WaterTime();
    float3 p = positionWS;
    positionWS += GerstnerWave(_WaveA, p, t, tangent, binormal);
    positionWS += GerstnerWave(_WaveB, p, t, tangent, binormal);
    positionWS += GerstnerWave(_WaveC, p, t, tangent, binormal);
#endif
    o.positionWS = positionWS;
    o.positionCS = TransformWorldToHClip(positionWS);
    o.tangentWS = normalize(tangent);
    o.bitangentWS = normalize(binormal);
    o.normalWS = normalize(cross(binormal, tangent));
    o.fogFactor = ComputeFogFactor(o.positionCS.z);
    return o;
}
void main()
{
    vec3 pWS = TransformObjectToWorld(positionOS.xyz);
    // 平静水面:切线 +X、副切线 +Z、法线 +Y
    vec3 tangent = vec3(1, 0, 0);
    vec3 binormal = vec3(0, 0, 1);
#if defined(_GERSTNER)
    float t = WaterTime();
    vec3 p = pWS;
    pWS += GerstnerWave(_WaveA, p, t, tangent, binormal);
    pWS += GerstnerWave(_WaveB, p, t, tangent, binormal);
    pWS += GerstnerWave(_WaveC, p, t, tangent, binormal);
#endif
    positionWS = pWS;
    gl_Position = TransformWorldToHClip(pWS);
    tangentWS = normalize(tangent);
    bitangentWS = normalize(binormal);
    normalWS = normalize(cross(binormal, tangent));
    fogFactor = ComputeFogFactor(gl_Position.z);
}

还有一个容易忽略的影响:顶点被 Gerstner 波抬高或压低之后,片元里的 positionWS 是位移后的位置,所以水深、泡沫、焦散都会随波起伏自动变化——波谷处水变浅,泡沫带向外扩;波峰处水变深。这种联动是“免费”的,因为后续计算全部基于实际的水面位置。

图 19-13低机位观察。左:平静水面;中:单个 Gerstner 波(波长 6 米、陡度 0.28),波峰尖、波谷宽;右:三个方向不同的波叠加,形成不规则的起伏,岸边泡沫随水位线一起移动。

网格密度与常见坑