GameTech/URP 渲染实战

案例 11.9其余四个 Pass进阶

目标:给 MyPBR 补齐投射阴影、深度预渲染、深度法线与烘焙用的 Pass,让它在 URP 的所有功能里都是“一等公民”。

Assets/Cookbook/Ch11/MyPBR.shader(节选:pass_shadow)
Pass
{
    Name "ShadowCaster"
    Tags { "LightMode"="ShadowCaster" }
    ZWrite On
    ZTest LEqual
    ColorMask 0
    Cull Back

    HLSLPROGRAM
    #pragma target 2.0
    #pragma vertex MyShadowVert
    #pragma fragment MyShadowFrag
    #pragma shader_feature_local _ALPHATEST_ON
    #pragma multi_compile_instancing
    // 方向光与点/聚光的法线偏移公式不同,URP 用这个关键字区分
    #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
    #include "Assets/Cookbook/Ch11/MyPBR_DepthPasses.hlsl"
    ENDHLSL
}
Pass
{
    Name "ShadowCaster"
    Tags { "LightMode"="ShadowCaster" }
    ZWrite On
    ZTest LEqual
    ColorMask 0
    Cull Back

    GLSLPROGRAM
    #pragma shader_feature_local _ALPHATEST_ON
    // 方向光与点/聚光的法线偏移公式不同,URP 用 _CASTING_PUNCTUAL_LIGHT_SHADOW 关键字区分(网页版是 uniform)
    #include "Ch11/MyPBR_DepthPasses.glsl"
    #ifdef VERTEX
    void main() { MyShadowVert(); }
    #endif
    #ifdef FRAGMENT
    void main() { fragColor = MyShadowFrag(); }
    #endif
    ENDGLSL
}
Assets/Cookbook/Ch11/MyPBR_DepthPasses.hlsl(节选:shadow_caster)
// URP 在渲染阴影图时设置这两个变量:方向光用 _LightDirection,点/聚光用 _LightPosition
float3 _LightDirection;
float3 _LightPosition;

DepthVaryings MyShadowVert(DepthAttributes input)
{
    DepthVaryings o = (DepthVaryings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, o);

    float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
    float3 normalWS   = TransformObjectToWorldNormal(input.normalOS);
#if defined(_CASTING_PUNCTUAL_LIGHT_SHADOW)
    float3 lightDirWS = normalize(_LightPosition - positionWS);
#else
    float3 lightDirWS = _LightDirection;
#endif
    // 深度偏移 + 法线偏移(数值来自灯光的 Bias 设置),对抗 Shadow Acne
    float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS));
    // 把被近裁剪面切掉的投射体压到近平面上(Shadow Pancaking),否则阴影会出洞
#if UNITY_REVERSED_Z
    positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
    positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#endif
    o.positionCS = positionCS;
    o.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
    return o;
}

half4 MyShadowFrag(DepthVaryings i) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(i);
    MyAlphaClip(i.uv);
    return 0;
}
// URP 在渲染阴影图时设置这两个变量:方向光用 _LightDirection,点/聚光用 _LightPosition
// (网页版核心库 Lighting.glsl 已声明 _LightDirection、_LightPosition、_CastingPunctualLightShadow)
#ifdef VERTEX
void MyShadowVert()
{
    vec3 positionWS = TransformObjectToWorld(positionOS.xyz);
    vec3 nWS        = TransformObjectToWorldNormal(normalOS);
    // HLSL 里是 _CASTING_PUNCTUAL_LIGHT_SHADOW 关键字;网页版是 uniform
    vec3 lightDirWS = _CastingPunctualLightShadow > 0.5 ? normalize(_LightPosition - positionWS) : _LightDirection;
    // 深度偏移 + 法线偏移(数值来自灯光的 Bias 设置),对抗 Shadow Acne
    vec4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, nWS, lightDirWS));
    // 把被近裁剪面切掉的投射体压到近平面上(Shadow Pancaking),否则阴影会出洞
#if UNITY_REVERSED_Z
    positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
    positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE * positionCS.w);   // GL 的近平面在 z = -w
#endif
    gl_Position = positionCS;
    uv = TransformTex(texcoord0.xy, _BaseMap_ST);
}
#endif

#ifdef FRAGMENT
vec4 MyShadowFrag()
{
    MyAlphaClip(uv);
    return vec4(0.0);
}
#endif

ShadowCaster 的核心是 ApplyShadowBias:沿光线方向推远一点(Depth Bias)、沿法线方向往里缩一点(Normal Bias),两者的数值来自灯光组件的 Bias 设置,由 URP 通过 _ShadowBias 传入。_LightDirection 和 _LightPosition 不在任何 URP 头文件里声明,而是 ShadowCasterPass.hlsl 自己声明、由 C# 端 ShadowUtils.SetupShadowCasterConstantBuffer 设置的普通全局变量,所以我们也要自己声明。点光和聚光的光线方向在每个顶点都不同,所以用 _CASTING_PUNCTUAL_LIGHT_SHADOW 区分。最后把被近裁剪面切掉的顶点压到近平面上(Shadow Pancaking):方向光阴影的正交投影只覆盖级联范围附近的一段深度,位于近平面之前的投射体如果被直接裁掉,就会在阴影里留下洞。

Assets/Cookbook/Ch11/MyPBR.shader(节选:pass_depth)
Pass
{
    Name "DepthOnly"
    Tags { "LightMode"="DepthOnly" }
    ZWrite On
    ColorMask R
    Cull Back

    HLSLPROGRAM
    #pragma target 2.0
    #pragma vertex MyDepthVert
    #pragma fragment MyDepthFrag
    #pragma shader_feature_local _ALPHATEST_ON
    #pragma multi_compile_instancing
    #include "Assets/Cookbook/Ch11/MyPBR_DepthPasses.hlsl"
    ENDHLSL
}

Pass
{
    Name "DepthNormals"
    Tags { "LightMode"="DepthNormals" }
    ZWrite On
    Cull Back

    HLSLPROGRAM
    #pragma target 2.0
    #pragma vertex MyDepthNormalsVert
    #pragma fragment MyDepthNormalsFrag
    #pragma shader_feature_local _NORMALMAP
    #pragma shader_feature_local _ALPHATEST_ON
    #pragma multi_compile_instancing
    #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/RenderingLayers.hlsl"
    #include "Assets/Cookbook/Ch11/MyPBR_DepthPasses.hlsl"
    ENDHLSL
}
Pass
{
    Name "DepthOnly"
    Tags { "LightMode"="DepthOnly" }
    ZWrite On
    ColorMask R
    Cull Back

    GLSLPROGRAM
    #pragma shader_feature_local _ALPHATEST_ON
    #include "Ch11/MyPBR_DepthPasses.glsl"
    #ifdef VERTEX
    void main() { MyDepthVert(); }
    #endif
    #ifdef FRAGMENT
    void main() { fragColor = MyDepthFrag(); }
    #endif
    ENDGLSL
}

Pass
{
    Name "DepthNormals"
    Tags { "LightMode"="DepthNormals" }
    ZWrite On
    Cull Back

    GLSLPROGRAM
    #pragma shader_feature_local _NORMALMAP
    #pragma shader_feature_local _ALPHATEST_ON
    #include "Ch11/MyPBR_DepthPasses.glsl"
    #ifdef VERTEX
    void main() { MyDepthNormalsVert(); }
    #endif
    #ifdef FRAGMENT
    void main() { fragColor = MyDepthNormalsFrag(); }
    #endif
    ENDGLSL
}
Assets/Cookbook/Ch11/MyPBR_DepthPasses.hlsl(节选:depth_normals)
DepthVaryings MyDepthNormalsVert(DepthAttributes input)
{
    DepthVaryings o = (DepthVaryings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, o);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    o.positionCS = TransformObjectToHClip(input.positionOS.xyz);
    o.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
    VertexNormalInputs nrm = GetVertexNormalInputs(input.normalOS, input.tangentOS);
    o.normalWS = nrm.normalWS;
#if defined(_NORMALMAP)
    o.tangentWS = half4(nrm.tangentWS, input.tangentOS.w * GetOddNegativeScale());
#endif
    return o;
}

void MyDepthNormalsFrag(DepthVaryings i
    , out half4 outNormalWS : SV_Target0
#if defined(_WRITE_RENDERING_LAYERS)
    , out float4 outRenderingLayers : SV_Target1
#endif
    )
{
    UNITY_SETUP_INSTANCE_ID(i);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
    MyAlphaClip(i.uv);
#if defined(_NORMALMAP)
    // SSAO、贴花等读 _CameraNormalsTexture,带上法线贴图细节它们才对得上
    half3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uv), _BumpScale);
    float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz);
    float3 normalWS = TransformTangentToWorld(normalTS, half3x3(i.tangentWS.xyz, bitangent, i.normalWS));
#else
    float3 normalWS = i.normalWS;
#endif
    outNormalWS = half4(NormalizeNormalPerPixel(normalWS), 0.0);   // URP 14 存未编码的世界法线
#if defined(_WRITE_RENDERING_LAYERS)
    outRenderingLayers = float4(EncodeMeshRenderingLayer(GetMeshRenderingLayer()), 0, 0, 0);
#endif
}
#ifdef VERTEX
void MyDepthNormalsVert()
{
    gl_Position = TransformObjectToHClip(positionOS.xyz);
    uv = TransformTex(texcoord0.xy, _BaseMap_ST);
    VertexNormalInputs nrm = GetVertexNormalInputs(normalOS, tangentOS);
    normalWS = nrm.normalWS;
#if defined(_NORMALMAP)
    tangentWS = vec4(nrm.tangentWS, tangentOS.w * GetOddNegativeScale());
#endif
}
#endif

#ifdef FRAGMENT
vec4 MyDepthNormalsFrag()
{
    MyAlphaClip(uv);
#if defined(_NORMALMAP)
    // SSAO、贴花等读 _CameraNormalsTexture,带上法线贴图细节它们才对得上
    vec3 normalTS = UnpackNormalScale(texture(_BumpMap, uv), _BumpScale);
    vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz);
    vec3 n = TransformTangentToWorld(normalTS, mat3(tangentWS.xyz, bitangent, normalWS));
#else
    vec3 n = normalWS;
#endif
    return vec4(NormalizeNormalPerPixel(n), 0.0);   // URP 14 存未编码的世界法线
}
#endif

DepthOnly 用于深度预渲染(Depth Prepass)和生成 _CameraDepthTexture(当 URP 判断不能直接拷贝深度时);DepthNormals 用于生成 _CameraNormalsTexture,SSAO、屏幕空间贴花、某些描边效果都依赖它。如果一个不透明 Shader 缺少 DepthNormals Pass,它在法线图里就是一个“洞”,SSAO 会在它周围产生错误的遮蔽。注意 DepthNormals 里同样要采样法线贴图——SSAO 读到的法线应当和着色用的法线一致,否则砖缝等细节处的遮蔽会和光照对不上。URP 14 Lit 的 DepthNormals Pass 输出的是未编码的世界空间法线(八面体编码 _GBUFFER_NORMALS_OCT 只在 GBuffer Pass 里声明),我们保持一致。

图 11-13MyPBR 场景开启 URP 自带的 SSAO(来源设为 Depth Normals)。左:关闭;右:开启,墙角、方块之间、球与地面接触处出现遮蔽。SSAO 正确作用于全部 MyPBR 物体,说明 DepthNormals Pass 工作正常。

SSAO 的结果通过 _SCREEN_SPACE_OCCLUSION 关键字回到 Forward Pass:CreateAmbientOcclusionFactor 把屏幕空间 AO 和材质 AO 取最小值作为间接光遮蔽,再按 SSAO 设置中的 Direct Lighting Strength 得到直接光遮蔽(GetMainLight(inputData, …, aoFactor) 内部会乘到灯光颜色上)。

Assets/Cookbook/Ch11/MyPBR.shader(节选:pass_meta)
Pass
{
    Name "Meta"
    Tags { "LightMode"="Meta" }
    Cull Off

    HLSLPROGRAM
    #pragma target 2.0
    #pragma vertex MyMetaVert
    #pragma fragment MyMetaFrag
    #pragma shader_feature_local_fragment _MASKMAP
    #pragma shader_feature_local_fragment _EMISSION
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature EDITOR_VISUALIZATION
    #include "Assets/Cookbook/Ch11/MyPBR_MetaPass.hlsl"
    ENDHLSL
}
Pass
{
    Name "Meta"
    Tags { "LightMode"="Meta" }
    Cull Off

    GLSLPROGRAM
    #pragma shader_feature_local_fragment _MASKMAP
    #pragma shader_feature_local_fragment _EMISSION
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #include "Ch11/MyPBR_MetaPass.glsl"
    #ifdef VERTEX
    void main() { MyMetaVert(); }
    #endif
    #ifdef FRAGMENT
    void main() { fragColor = MyMetaFrag(); }
    #endif
    ENDGLSL
}
Assets/Cookbook/Ch11/MyPBR_MetaPass.hlsl(节选:meta)
MetaVaryings MyMetaVert(MetaAttributes input)
{
    MetaVaryings o = (MetaVaryings)0;
    // 烘焙时 unity_MetaVertexControl 指示用哪套 UV 当“屏幕位置”
    o.positionCS = UnityMetaVertexPosition(input.positionOS.xyz, input.uv1, input.uv2);
    o.uv = TRANSFORM_TEX(input.uv0, _BaseMap);
#if defined(EDITOR_VISUALIZATION)
    UnityEditorVizData(input.positionOS.xyz, input.uv0, input.uv1, input.uv2, o.VizUV, o.LightCoord);
#endif
    return o;
}

half4 MyMetaFrag(MetaVaryings i) : SV_Target
{
    MySurface s = MySampleSurface(i.uv);
    MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness);

    UnityMetaInput meta = (UnityMetaInput)0;
    // 反弹光的颜色:漫反射色为主;粗糙金属的镜面反射在烘焙器看来也像“漫反射”,
    // 按粗糙度加一部分 F0 进去(与 URP Lit 的经验做法相同)
    meta.Albedo = m.diffuseColor + m.f0 * (m.perceptualRoughness * m.perceptualRoughness) * 0.5;
    meta.Emission = s.emission;
#if defined(EDITOR_VISUALIZATION)
    meta.VizUV = i.VizUV;
    meta.LightCoord = i.LightCoord;
#endif
    return UnityMetaFragment(meta);
}
#ifdef VERTEX
// UnityMetaVertexPosition:用光照贴图 UV 当“屏幕位置”,把网格摊平到贴图空间
void MyMetaVert()
{
    vec2 p = unity_MetaVertexControl.y > 0.5 ? texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw
                                            : texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
    uv = TransformTex(texcoord0.xy, _BaseMap_ST);
}
#endif

#ifdef FRAGMENT
vec4 MyMetaFrag()
{
    MySurface s = MySampleSurface(uv);
    MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness);

    // 反弹光的颜色:漫反射色为主;粗糙金属的镜面反射在烘焙器看来也像“漫反射”,
    // 按粗糙度加一部分 F0 进去(与 URP Lit 的经验做法相同)
    vec3 albedo = m.diffuseColor + m.f0 * (m.perceptualRoughness * m.perceptualRoughness) * 0.5;
    vec3 emission = s.emission;
    // UnityMetaFragment:按烘焙器的请求输出反照率或自发光
    if (unity_MetaFragmentControl.x > 0.5) return vec4(saturate(albedo), 1.0);
    if (unity_MetaFragmentControl.y > 0.5) return vec4(emission, 1.0);
    return vec4(0.0);
}
#endif

Meta Pass 只在烘焙光照贴图(或在编辑器中显示某些可视化模式)时被调用。烘焙器把物体按 lightmap UV 摊开渲染,每个纹素读两样东西:反照率(决定光线在这里反弹时带走什么颜色)和自发光(作为面光源参与烘焙)。UnityMetaVertexPosition 根据 unity_MetaVertexControl 决定用第二套还是第三套 UV 作为“屏幕位置”,UnityMetaFragment 根据 unity_MetaFragmentControl 决定输出反照率还是自发光,并处理 Lighting 窗口里的 Albedo Boost 等设置。反照率我们用漫反射色加上一部分 F0:完全光滑的金属在烘焙器看来几乎不反弹漫射光,但粗糙金属的镜面波瓣很宽,表现得像漫反射体,按 α/2 混入是 URP Lit 也采用的经验做法。没有 Meta Pass 的物体在烘焙时被当作黑色,既不反弹光,自发光也不起作用。

关于 DeferredURP 的 Deferred 路径只为带 UniversalGBuffer Pass 的材质写 G-Buffer,其余不透明物体只有 LightMode 为 UniversalForwardOnly(或 SRPDefaultUnlit)的 Pass 会在延迟光照之后以前向方式绘制;UniversalForward 在 Deferred 下不会被绘制。MyPBR 的光照模型无法塞进 URP 固定格式的 G-Buffer(G-Buffer 只存材质参数,光照由 URP 的延迟光照 Shader 统一计算,用的是 URP 自己的 BRDF),所以如果项目用 Deferred,请把 ForwardLit Pass 的 LightMode 改成 UniversalForwardOnly——URP 的 Complex Lit 正是这样做的。