GameTech/URP 渲染实战

案例 2.2Depth Texture 与 Opaque Texture 的开关入门

目标:写一个极简水面,同时用到深度纹理(深浅颜色、岸边白沫)和不透明纹理(折射),然后分别关掉 URP Asset 里的两个开关,看看会发生什么。

原理:水面自己的观察深度就是 positionNDC.w(第 1 章:clip.w 即观察深度);从深度纹理里读出同一像素“水底”的观察深度,两者相减就是视线穿过的水的厚度。厚度越大越接近深水色,厚度接近 0 的地方就是水面与物体相交处,叠一圈白沫。折射则是用扰动过的屏幕 UV 采样 Opaque Texture。完整的水在第 19 章。

Assets/Cookbook/Ch02/SceneTextures.shader(节选:frag)
half4 frag(Varyings input) : SV_Target
{
    float2 screenUV = input.positionNDC.xy / input.positionNDC.w;

    // 1) 深度纹理:水底(不透明物体)离相机多远
    float sceneEye   = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
    float surfaceEye = input.positionNDC.w;          // 透视投影下 clip.w 就是水面自己的观察深度
    float thickness  = max(sceneEye - surfaceEye, 0); // 视线穿过的水的厚度(米)

    // 2) 不透明纹理:水面背后的画面,加一点随世界坐标变化的扰动
    float2 p = input.positionWS.xz * _WaveScale;
    float2 wave = float2(sin(p.x + p.y * 0.7), cos(p.y * 1.3 - p.x * 0.4));
    float2 refrUV = screenUV + wave * _Distortion * saturate(thickness);
    half3 behind = SampleSceneColor(refrUV);

    // 越深越接近深水色;离岸(厚度接近 0)的地方叠一圈白沫
    half t = saturate(thickness / _DepthFade);
    half3 col = lerp(behind * _ShallowColor.rgb, _DeepColor.rgb, t);
    half foam = 1.0 - saturate(thickness / _FoamWidth);
    col = lerp(col, half3(0.95, 0.98, 1.0), foam);
    return half4(col, 1);
}
#ifdef FRAGMENT
void main()
{
    vec2 screenUV = vPositionNDC.xy / vPositionNDC.w;

    // 1) 深度纹理:水底(不透明物体)离相机多远
    float sceneEye   = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
    float surfaceEye = vPositionNDC.w;                  // 透视投影下 clip.w 就是水面自己的观察深度
    float thickness  = max(sceneEye - surfaceEye, 0.0); // 视线穿过的水的厚度(米)

    // 2) 不透明纹理:水面背后的画面,加一点随世界坐标变化的扰动
    vec2 p = vPositionWS.xz * _WaveScale;
    vec2 wave = vec2(sin(p.x + p.y * 0.7), cos(p.y * 1.3 - p.x * 0.4));
    vec2 refrUV = screenUV + wave * _Distortion * saturate(thickness);
    vec3 behind = SampleSceneColor(refrUV);

    // 越深越接近深水色;离岸(厚度接近 0)的地方叠一圈白沫
    float t = saturate(thickness / _DepthFade);
    vec3 col = mix(behind * _ShallowColor.rgb, _DeepColor.rgb, t);
    float foam = 1.0 - saturate(thickness / _FoamWidth);
    col = mix(col, vec3(0.95, 0.98, 1.0), foam);
    fragColor = vec4(col, 1);
}
#endif
图 2-8左:两张纹理都开启;中:关闭 Depth Texture(厚度恒为“无穷大”,全是深水色,白沫消失);右:关闭 Opaque Texture(折射内容变成灰色占位图)

中图最能说明问题:深度纹理关闭后,URP 把它设成了“远平面”,Shader 算出的厚度永远很大,于是浅滩、白沫、水下方块的轮廓统统消失。右图里,深度效果都还在,但透过水面看到的不再是水底,而是一片灰色——关掉 Opaque Texture 后这一帧不会写入 _CameraOpaqueTexture,Shader 读到什么取决于之前谁设置过这个全局纹理,可能是上一帧、上一台相机留下的旧图,也可能是引擎的默认纹理。为了让配图结果确定,拍摄脚本显式放了一张灰色占位图。实际项目里这种“读到旧图”的 bug 更隐蔽:画面看起来似乎对,换个机位或分辨率就出错。

提示如果只是某台相机需要深度/不透明纹理(比如只有主相机看得到水),不必在 URP Asset 里全局打开:每台相机的 Rendering 面板里有 Depth Texture / Opaque Texture 的覆盖选项(Use Pipeline Settings / On / Off)。Renderer Feature 则应在 Pass 里调用 ConfigureInput(ScriptableRenderPassInput.Depth) 等方式声明需求,URP 会按需生成。