案例 19.2深度着色:深的地方更深入门
目标:让水的颜色和透明度随水深变化:浅处清澈泛青,深处浓重发蓝;岸边水深趋于 0 时平滑过渡,不出现硬边。
原理:从深度图重建水底
水面在透明队列里绘制时,深度图 _CameraDepthTexture 里存着水底(以及水中所有不透明物体)的深度。在水面的每个像素上,我们用同一个屏幕 UV 去采样深度图,就得到“透过这个像素看下去,水底在哪里”。有了原始深度,可以用 URP Core 库里的 ComputeWorldSpacePosition( 把它还原成世界坐标——这个函数用逆视图投影矩阵把 NDC 坐标变换回世界空间(第 1 章、第 28 章)。
有了水底的世界坐标,就能定义两种“水深”:
- 竖直水深:水面 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(,水深趋于 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);
片元函数的其余部分会在后面的案例里逐段展开。

注意深度图必须开启(URP Asset 的 Depth Texture,或相机上的覆盖选项)。深度图里不包含透明物体,所以两片重叠的水面彼此“看不见”;水面下方的半透明物体(比如水草用的 Alpha Blend 材质)也不会被算作水底。