案例 11.9其余四个 Pass进阶
目标:给 MyPBR 补齐投射阴影、深度预渲染、深度法线与烘焙用的 Pass,让它在 URP 的所有功能里都是“一等公民”。
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 }
// 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):方向光阴影的正交投影只覆盖级联范围附近的一段深度,位于近平面之前的投射体如果被直接裁掉,就会在阴影里留下洞。
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 }
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 里声明),我们保持一致。

SSAO 的结果通过 _SCREEN_SPACE_OCCLUSION 关键字回到 Forward Pass:CreateAmbientOcclusionFactor 把屏幕空间 AO 和材质 AO 取最小值作为间接光遮蔽,再按 SSAO 设置中的 Direct Lighting Strength 得到直接光遮蔽(GetMainLight(inputData, …, aoFactor) 内部会乘到灯光颜色上)。
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 }
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:完全光滑的金属在烘焙器看来几乎不反弹漫射光,但粗糙金属的镜面波瓣很宽,表现得像漫反射体,按 混入是 URP Lit 也采用的经验做法。没有 Meta Pass 的物体在烘焙时被当作黑色,既不反弹光,自发光也不起作用。
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 正是这样做的。