GameTech/URP 渲染实战

案例 19.2深度着色:深的地方更深入门

目标:让水的颜色和透明度随水深变化:浅处清澈泛青,深处浓重发蓝;岸边水深趋于 0 时平滑过渡,不出现硬边。

原理:从深度图重建水底

水面在透明队列里绘制时,深度图 _CameraDepthTexture 里存着水底(以及水中所有不透明物体)的深度。在水面的每个像素上,我们用同一个屏幕 UV 去采样深度图,就得到“透过这个像素看下去,水底在哪里”。有了原始深度,可以用 URP Core 库里的 ComputeWorldSpacePosition(uv, rawDepth, UNITY_MATRIX_I_VP) 把它还原成世界坐标——这个函数用逆视图投影矩阵把 NDC 坐标变换回世界空间(第 1 章、第 28 章)。

有了水底的世界坐标,就能定义两种“水深”:

水面 水底 相机 视线路径长度 path 竖直水深 水面像素 sceneWS
图 19-3两种水深。竖直水深 = 水面 y − 重建出的水底 y;视线路径 = 水面点到水底点的距离,斜看时远大于竖直水深。

严格地说,“竖直水深”应该取水面这一点正下方的水底高度,但我们只能拿到沿视线方向的那个水底点。代码里用的是“水面 y − 重建点 y”:水底起伏平缓时二者很接近,而且计算极其便宜。这是实时水面普遍采用的近似。

Assets/Cookbook/Ch19/WaterCore.hlsl(节选:depth)
// 水下深度:用水面这个像素的屏幕 UV 取场景深度,重建出水底的世界坐标
struct WaterDepth
{
    float3 sceneWS;     // 水底(或水中物体)表面的世界坐标
    float  vertical;    // 竖直水深 = 水面 y - 水底 y(与视角无关)
    float  path;        // 视线在水里走过的距离(与视角有关,斜看时更长)
    float  sceneEye;    // 场景的观察空间深度
};

WaterDepth SampleWaterDepth(float2 uv, float3 surfaceWS)
{
    WaterDepth o;
    float raw = SampleSceneDepth(uv);
    o.sceneWS = ComputeWorldSpacePosition(uv, raw, UNITY_MATRIX_I_VP);
    o.vertical = surfaceWS.y - o.sceneWS.y;
    o.path = distance(o.sceneWS, surfaceWS);
    o.sceneEye = LinearEyeDepth(raw, _ZBufferParams);
    return o;
}
// 水下深度:用水面这个像素的屏幕 UV 取场景深度,重建出水底的世界坐标
struct WaterDepth
{
    vec3  sceneWS;     // 水底(或水中物体)表面的世界坐标
    float vertical;    // 竖直水深 = 水面 y - 水底 y(与视角无关)
    float path;        // 视线在水里走过的距离(与视角有关,斜看时更长)
    float sceneEye;    // 场景的观察空间深度
};

WaterDepth SampleWaterDepth(vec2 uv, vec3 surfaceWS)
{
    WaterDepth o;
    float raw = SampleSceneDepth(uv);
    o.sceneWS = ComputeWorldSpacePosition(uv, raw, UNITY_MATRIX_I_VP);
    o.vertical = surfaceWS.y - o.sceneWS.y;
    o.path = distance(o.sceneWS, surfaceWS);
    o.sceneEye = LinearEyeDepth(raw, _ZBufferParams);
    return o;
}

深度着色本身很简单:竖直水深除以一个“完全变深”的深度 _DepthMax,在浅色和深色之间插值,透明度也同样插值;再乘一个 saturate(depth / _EdgeFade),水深趋于 0 时 alpha 也趋于 0,岸线就从硬边变成了柔和的过渡。颜色还乘了一个 waterLight(主光 × 阴影 + 环境光),让水体在柱子的阴影里也会变暗。

对应的代码在上面完整 Shader 片元函数的“深度”部分和 #else 分支(没有 _REFRACTION 时)里:

WaterDepth wd = SampleWaterDepth(screenUV, i.positionWS);
half3 waterLight = mainLight.color * saturate(mainLight.direction.y) * mainLight.shadowAttenuation
                 + SampleSH(half3(0, 1, 0));
// ……
half d01 = saturate(wd.vertical / _DepthMax);
color = lerp(_ShallowColor.rgb, _DeepColor.rgb, d01) * waterLight;
alpha = lerp(_ShallowColor.a, _DeepColor.a, d01) * saturate(wd.vertical / _EdgeFade);

片元函数的其余部分会在后面的案例里逐段展开。

图 19-4左:调试视图,竖直水深(除以 2 米,白=深);中:调试视图,视线路径长度(同样除以 2 米)——远处的水路径更长,更早变白;右:按竖直水深着色的水,浅处青绿透明、深处深蓝,岸线柔和过渡。
注意深度图必须开启(URP Asset 的 Depth Texture,或相机上的覆盖选项)。深度图里不包含透明物体,所以两片重叠的水面彼此“看不见”;水面下方的半透明物体(比如水草用的 Alpha Blend 材质)也不会被算作水底。