GameTech/URP 渲染实战

案例 6.8DepthOnly 与 DepthNormals Pass入门

目标:弄清这两个 Pass 什么时候被调用、缺了会怎样,并写出可复用的实现。

除了颜色 Pass 和阴影 Pass,URP 还会在某些情况下单独渲染一遍场景的深度或深度+法线:

Assets/Cookbook/Ch06/DepthOnlyPass.hlsl
// DepthOnly Pass:只写深度。URP 做深度预渲染(Depth Prepass)、生成 _CameraDepthTexture 时会用到它。
#ifndef COOKBOOK_CH06_DEPTH_ONLY_PASS_INCLUDED
#define COOKBOOK_CH06_DEPTH_ONLY_PASS_INCLUDED

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct DepthOnlyAttributes
{
    float4 positionOS : POSITION;
    float2 texcoord   : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct DepthOnlyVaryings
{
#if defined(_ALPHATEST_ON)
    float2 uv         : TEXCOORD0;
#endif
    float4 positionCS : SV_POSITION;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

DepthOnlyVaryings DepthOnlyVertex(DepthOnlyAttributes input)
{
    DepthOnlyVaryings output = (DepthOnlyVaryings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
#if defined(_ALPHATEST_ON)
    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
#endif
    return output;
}

half DepthOnlyFragment(DepthOnlyVaryings input) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
#if defined(_ALPHATEST_ON)
    clip(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a * _BaseColor.a - _Cutoff);
#endif
    // 深度由硬件写入;某些路径会把深度作为颜色写进 R 通道(Pass 里 ColorMask R),所以照 URP 的做法返回 z
    return input.positionCS.z;
}

#endif
// DepthOnly Pass:只写深度。URP 做深度预渲染(Depth Prepass)、生成 _CameraDepthTexture 时会用到它。(GLSL 版,对应 Ch06/DepthOnlyPass.hlsl)

#if defined(_ALPHATEST_ON)
VARYING vec2 uv;
#endif

#ifdef VERTEX
void main()
{
    gl_Position = TransformObjectToHClip(positionOS.xyz);
#if defined(_ALPHATEST_ON)
    uv = TransformTex(texcoord0.xy, _BaseMap_ST);
#endif
}
#endif

#ifdef FRAGMENT
void main()
{
#if defined(_ALPHATEST_ON)
    if (texture(_BaseMap, uv).a * _BaseColor.a - _Cutoff < 0.0) discard;
#endif
    // 深度由硬件写入;某些路径会把深度作为颜色写进 R 通道(Pass 里 ColorMask R),所以照 URP 的做法返回 z
    fragColor = vec4(gl_FragCoord.z);
}
#endif
Assets/Cookbook/Ch06/DepthNormalsPass.hlsl
// DepthNormals Pass:同时写深度和世界空间法线到 _CameraNormalsTexture。
// SSAO(Depth Normals 模式)、贴花、部分自定义屏幕空间效果都依赖它。
#ifndef COOKBOOK_CH06_DEPTH_NORMALS_PASS_INCLUDED
#define COOKBOOK_CH06_DEPTH_NORMALS_PASS_INCLUDED

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct DepthNormalsAttributes
{
    float4 positionOS : POSITION;
    float3 normalOS   : NORMAL;
    float2 texcoord   : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct DepthNormalsVaryings
{
    float4 positionCS : SV_POSITION;
    float2 uv         : TEXCOORD0;
    float3 normalWS   : TEXCOORD1;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

DepthNormalsVaryings DepthNormalsVertex(DepthNormalsAttributes input)
{
    DepthNormalsVaryings output = (DepthNormalsVaryings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
    output.normalWS = TransformObjectToWorldNormal(input.normalOS);
    output.uv = input.texcoord;
#if defined(_ALPHATEST_ON)
    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
#endif
    return output;
}

void DepthNormalsFragment(DepthNormalsVaryings input
    , out half4 outNormalWS : SV_Target0
#ifdef _WRITE_RENDERING_LAYERS
    , out float4 outRenderingLayers : SV_Target1
#endif
)
{
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
#if defined(_ALPHATEST_ON)
    clip(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a * _BaseColor.a - _Cutoff);
#endif
    // URP 14 的 _CameraNormalsTexture 直接存归一化的世界空间法线(不做 0.5 偏移)
    outNormalWS = half4(NormalizeNormalPerPixel(input.normalWS), 0.0);
#ifdef _WRITE_RENDERING_LAYERS
    // 开启“渲染层”且有贴花等需求时,URP 会让这个 Pass 顺便写出网格的渲染层
    outRenderingLayers = float4(EncodeMeshRenderingLayer(GetMeshRenderingLayer()), 0, 0, 0);
#endif
}

#endif
// DepthNormals Pass:同时写深度和世界空间法线到 _CameraNormalsTexture。(GLSL 版,对应 Ch06/DepthNormalsPass.hlsl)
// SSAO(Depth Normals 模式)、贴花、部分自定义屏幕空间效果都依赖它。

VARYING vec2 uv;
VARYING vec3 normalWS;

#ifdef VERTEX
void main()
{
    gl_Position = TransformObjectToHClip(positionOS.xyz);
    normalWS = TransformObjectToWorldNormal(normalOS);
    uv = texcoord0.xy;
#if defined(_ALPHATEST_ON)
    uv = TransformTex(texcoord0.xy, _BaseMap_ST);
#endif
}
#endif

#ifdef FRAGMENT
void main()
{
#if defined(_ALPHATEST_ON)
    if (texture(_BaseMap, uv).a * _BaseColor.a - _Cutoff < 0.0) discard;
#endif
    // URP 14 的 _CameraNormalsTexture 直接存归一化的世界空间法线(不做 0.5 偏移)
    // (网页版不支持“渲染层”贴图,_WRITE_RENDERING_LAYERS 那一路省略)
    fragColor = vec4(NormalizeNormalPerPixel(normalWS), 0.0);
}
#endif

两点说明:其一,DepthOnly 的片元返回 positionCS.z 并在 Pass 上声明 ColorMask R,与 URP 的 DepthOnlyPass.hlsl 一致——某些路径下深度会被写进一张颜色格式的纹理,这时 R 通道的值才是深度。其二,URP 14 的法线纹理直接存归一化的世界空间法线;只有 Deferred 开启 Accurate G-buffer normals 时才会用八面体编码(_GBUFFER_NORMALS_OCT),而 Lit 的 DepthNormals Pass 并不声明这个关键字。_WRITE_RENDERING_LAYERS 来自 RenderingLayers.hlsl(用 #include_with_pragmas 引入),开启渲染层并且贴花需要它时,这个 Pass 会顺便写出网格的渲染层。

图 6-20Renderer 上加了 SSAO(Depth Normals 模式)。左:关闭物体材质上的深度法线 Pass,物体没有进入 _CameraNormalsTexture,SSAO 看不到它,物体与地面接触处没有任何遮蔽;右:恢复后,立方体底部、纽结与地面之间出现自然的接触阴影

UniversalForward 还是 UniversalForwardOnly?

URP 的 Deferred 路径会把有 UniversalGBuffer Pass 的材质写进 G-Buffer,再统一计算光照;没有 G-Buffer Pass 的材质,只有标记为 UniversalForwardOnly(或无标签的旧式 Pass)时,才会在 Deferred 的“Forward Only”通道里前向绘制。只标记 UniversalForward 又没有 G-Buffer Pass 的材质,在 Deferred 下根本不会被绘制。所以光照模型无法写进 G-Buffer 的自定义 Shader(Blinn-Phong、卡通、皮肤……)应该用 UniversalForwardOnly,它在 Forward / Forward+ 下与 UniversalForward 的行为完全相同。配套地,深度法线 Pass 要标 DepthNormalsOnly:Deferred 的深度法线预渲染只收集这个标签;Forward 下 DepthNormals 和 DepthNormalsOnly 都会被绘制,所以两者只能选一个。URP 自带的 Complex Lit 就是这样组织的。