案例 11.3Forward Pass:顶点、InputData 与片元进阶
目标:写出 ForwardLit Pass 的顶点、片元函数,并理解为什么我们要填 URP 的 InputData 结构体。
Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } ZWrite On Cull Back HLSLPROGRAM #pragma target 2.0 #pragma vertex MyPBRVert #pragma fragment MyPBRFrag // ---- 材质关键字(只编译用到的组合)---- #pragma shader_feature_local _NORMALMAP #pragma shader_feature_local_fragment _MASKMAP #pragma shader_feature_local_fragment _EMISSION #pragma shader_feature_local_fragment _DFG_LUT #pragma shader_feature_local_fragment _ALPHATEST_ON // ---- URP 管线关键字(抄自 URP 14 Lit.shader 的 ForwardLit Pass)---- #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile _ EVALUATE_SH_MIXED EVALUATE_SH_VERTEX #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING #pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION #pragma multi_compile_fragment _ _LIGHT_COOKIES #pragma multi_compile _ _LIGHT_LAYERS #pragma multi_compile _ _FORWARD_PLUS #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/RenderingLayers.hlsl" // ---- Unity 内置关键字:光照贴图、雾、调试 ---- #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING #pragma multi_compile _ SHADOWS_SHADOWMASK #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog #pragma multi_compile_fragment _ DEBUG_DISPLAY #pragma multi_compile_instancing #include "Assets/Cookbook/Ch11/MyPBR_ForwardPass.hlsl" ENDHLSL }
Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } ZWrite On Cull Back GLSLPROGRAM // ---- 材质关键字(只编译用到的组合)---- #pragma shader_feature_local _NORMALMAP #pragma shader_feature_local_fragment _MASKMAP #pragma shader_feature_local_fragment _EMISSION #pragma shader_feature_local_fragment _DFG_LUT #pragma shader_feature_local_fragment _ALPHATEST_ON // ---- URP 管线关键字:_MAIN_LIGHT_SHADOWS、_ADDITIONAL_LIGHTS、_SHADOWS_SOFT 等网页版引擎始终打开 ---- // ---- 调试:网页版模拟的 Rendering Debugger ---- #pragma multi_compile_fragment _ DEBUG_DISPLAY #include "Ch11/MyPBR_ForwardPass.glsl" #include "Ch11/DebugDisplay.glsl" #ifdef VERTEX void main() { MyPBRVert(); } #endif #ifdef FRAGMENT void main() { #if defined(DEBUG_DISPLAY) // 让 Rendering Debugger 的材质视图 / 材质校验也能作用于 MyPBR: // 把我们的表面参数交给调试函数(HLSL 版是翻译成 URP 的 SurfaceData 再交给 CanDebugOverrideOutputColor) MySurface s = MySampleSurface(uv); InputData id = MyInitInputData(s.normalTS); MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness); vec4 debugColor; if (CanDebugOverrideOutputColor(s.albedo, s.alpha, m.f0, s.metallic, 1.0 - s.perceptualRoughness, s.occlusion, s.emission, id.normalWS, debugColor)) { fragColor = debugColor; return; } #endif fragColor = MyPBRFrag(); } #endif ENDGLSL }
关键字列表对照 URP 14 的 Shaders/Lit.shader 的 ForwardLit Pass 抄写,然后删掉我们不支持的部分(视差、细节贴图、透明混合、清漆、DOTS Instancing、实时 GI 的 DYNAMICLIGHTMAP_ON)。每一行 multi_compile 都对应 URP Asset 或场景里的一个开关:主光阴影三选一、附加光逐像素/逐顶点、软阴影档位、SSAO、反射探针混合与盒体投影、Light Cookie、Light Layers、Forward+。少声明一个,对应功能在这个 Shader 上就“静默失效”;多声明一个,变体数翻倍。c29 会讲如何统计和剥离变体。
struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 staticLightmapUV : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; half3 normalWS : TEXCOORD2; #if defined(_NORMALMAP) half4 tangentWS : TEXCOORD3; // xyz 切线,w 副切线符号 #endif half fogFactor : TEXCOORD4; #if defined(_ADDITIONAL_LIGHTS_VERTEX) half3 vertexLight : TEXCOORD5; #endif DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 6); // 有光照贴图时是 lightmap UV,否则是逐顶点 SH UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO };
// WebGL 不允许结构体做 varying:HLSL 的 Varyings 拆成单独的变量(顶点输入 Attributes 由引擎固定命名:positionOS、normalOS……) VARYING vec2 uv; VARYING vec3 positionWS; VARYING vec3 normalWS; #if defined(_NORMALMAP) VARYING vec4 tangentWS; // xyz 切线,w 副切线符号 #endif VARYING float fogFactor; #if defined(_ADDITIONAL_LIGHTS_VERTEX) VARYING vec3 vertexLight; #endif // 光照贴图 UV / 逐顶点 SH:网页版没有光照贴图,SH 在片元里逐像素求
Varyings MyPBRVert(Attributes input) { Varyings o = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); VertexPositionInputs pos = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs nrm = GetVertexNormalInputs(input.normalOS, input.tangentOS); o.positionCS = pos.positionCS; o.positionWS = pos.positionWS; o.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); o.normalWS = nrm.normalWS; #if defined(_NORMALMAP) // 镜像缩放(负缩放)会翻转副切线方向,GetOddNegativeScale 负责修正 o.tangentWS = half4(nrm.tangentWS, input.tangentOS.w * GetOddNegativeScale()); #endif o.fogFactor = ComputeFogFactor(pos.positionCS.z); #if defined(_ADDITIONAL_LIGHTS_VERTEX) // 逐顶点附加光:只累加 color * atten * NoL(漫反射的 E/π),镜面无法逐顶点 half3 vl = 0; uint n = GetAdditionalLightsCount(); for (uint i = 0u; i < n; ++i) { Light l = GetAdditionalLight(i, pos.positionWS); vl += l.color * l.distanceAttenuation * saturate(dot(nrm.normalWS, l.direction)); } o.vertexLight = vl; #endif OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, o.staticLightmapUV); OUTPUT_SH(o.normalWS, o.vertexSH); return o; }
#ifdef VERTEX void MyPBRVert() { VertexPositionInputs pos = GetVertexPositionInputs(positionOS.xyz); VertexNormalInputs nrm = GetVertexNormalInputs(normalOS, tangentOS); gl_Position = pos.positionCS; positionWS = pos.positionWS; uv = TransformTex(texcoord0.xy, _BaseMap_ST); normalWS = nrm.normalWS; #if defined(_NORMALMAP) // 镜像缩放(负缩放)会翻转副切线方向,GetOddNegativeScale 负责修正 tangentWS = vec4(nrm.tangentWS, tangentOS.w * GetOddNegativeScale()); #endif fogFactor = ComputeFogFactor(pos.positionCS.z); #if defined(_ADDITIONAL_LIGHTS_VERTEX) // 逐顶点附加光:只累加 color * atten * NoL(漫反射的 E/π),镜面无法逐顶点 vec3 vl = vec3(0.0); int n = GetAdditionalLightsCount(); for (int i = 0; i < n; ++i) { Light l = GetAdditionalLight(i, pos.positionWS); vl += l.color * l.distanceAttenuation * saturate(dot(nrm.normalWS, l.direction)); } vertexLight = vl; #endif } #endif
顶点函数用 GetVertexPositionInputs / GetVertexNormalInputs 一次拿到各个空间的位置和 TBN。DECLARE_LIGHTMAP_OR_SH 是 URP 的宏:开了 LIGHTMAP_ON 时声明一个 lightmap UV,否则声明一个逐顶点 SH 插值器;配套的 OUTPUT_LIGHTMAP_UV / OUTPUT_SH 负责填值,在“SH 逐像素”模式下 OUTPUT_SH 什么都不做。逐顶点附加光(URP Asset 中 Additional Lights 设为 Per Vertex 时)我们自己写了一个 Lambert 循环——它只累加 color × 衰减 × NoL,到片元里再乘漫反射色,π 恰好与 1/π 抵消。
// 填 URP 的 InputData:它只是“像素的几何与环境数据”的容器, // 填好它,阴影、SSAO、Forward+ 的分簇循环、Rendering Debugger 都能直接用 InputData MyInitInputData(Varyings i, half3 normalTS) { InputData d = (InputData)0; d.positionWS = i.positionWS; d.positionCS = i.positionCS; #if defined(_NORMALMAP) float sgn = i.tangentWS.w; float3 bitangent = sgn * cross(i.normalWS, i.tangentWS.xyz); half3x3 tbn = half3x3(i.tangentWS.xyz, bitangent, i.normalWS); d.tangentToWorld = tbn; d.normalWS = TransformTangentToWorld(normalTS, tbn); #else d.normalWS = i.normalWS; #endif d.normalWS = NormalizeNormalPerPixel(d.normalWS); // 插值后长度不为 1,必须重新归一化 d.viewDirectionWS = GetWorldSpaceNormalizeViewDir(i.positionWS); d.shadowCoord = TransformWorldToShadowCoord(i.positionWS); // 逐像素算级联索引,级联/屏幕空间阴影都适用 d.fogCoord = InitializeInputDataFog(float4(i.positionWS, 1.0), i.fogFactor); #if defined(_ADDITIONAL_LIGHTS_VERTEX) d.vertexLighting = i.vertexLight; #endif d.bakedGI = SAMPLE_GI(i.staticLightmapUV, i.vertexSH, d.normalWS); // 光照贴图或 SH d.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(i.positionCS); d.shadowMask = SAMPLE_SHADOWMASK(i.staticLightmapUV); #if defined(DEBUG_DISPLAY) #if defined(LIGHTMAP_ON) d.staticLightmapUV = i.staticLightmapUV; #else d.vertexSH = i.vertexSH; #endif #endif return d; }
// 填 URP 的 InputData:它只是“像素的几何与环境数据”的容器, // 填好它,阴影、SSAO、附加光循环都能直接用 InputData MyInitInputData(vec3 normalTS) { InputData d = InitInputData(); d.positionWS = positionWS; d.positionCS = gl_FragCoord; #if defined(_NORMALMAP) float sgn = tangentWS.w; vec3 bitangent = sgn * cross(normalWS, tangentWS.xyz); mat3 tbn = mat3(tangentWS.xyz, bitangent, normalWS); // GLSL 的 mat3 按列构造:列就是 T/B/N d.tangentToWorld = tbn; d.normalWS = TransformTangentToWorld(normalTS, tbn); #else d.normalWS = normalWS; #endif d.normalWS = NormalizeNormalPerPixel(d.normalWS); // 插值后长度不为 1,必须重新归一化 d.viewDirectionWS = GetWorldSpaceNormalizeViewDir(positionWS); d.shadowCoord = TransformWorldToShadowCoord(positionWS); d.fogCoord = fogFactor; #if defined(_ADDITIONAL_LIGHTS_VERTEX) d.vertexLighting = vertexLight; #endif d.bakedGI = SampleSH(d.normalWS); // 网页版没有光照贴图:逐像素 SH d.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(gl_FragCoord); d.shadowMask = vec4(1.0); return d; }
InputData 是 URP 定义的“像素几何与环境数据”的容器,里面没有任何 BRDF 内容。填好它有三个直接收益:GetMainLight(inputData, shadowMask, aoFactor) 会自动处理阴影、Light Cookie 和 SSAO 的直接光部分;Forward+ 的 LIGHT_LOOP_BEGIN 宏内部会读 inputData.normalizedScreenSpaceUV 和 inputData.positionWS;Rendering Debugger 的调试函数也以它为输入。
阴影坐标我们逐像素调用 TransformWorldToShadowCoord。URP Lit 在“单级联”时会在顶点里算好再插值以省一点 ALU,但级联阴影必须逐像素选级联,屏幕空间阴影则需要屏幕坐标——TransformWorldToShadowCoord 内部已经按 _MAIN_LIGHT_SHADOWS_CASCADE、_MAIN_LIGHT_SHADOWS_SCREEN 分好了支,逐像素调用在所有模式下都正确,代价是一次矩阵乘法。
half4 MyPBRFrag(Varyings i #if defined(_WRITE_RENDERING_LAYERS) , out float4 outRenderingLayers : SV_Target1 #endif ) : SV_Target0 { UNITY_SETUP_INSTANCE_ID(i); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); MySurface s = MySampleSurface(i.uv); InputData id = MyInitInputData(i, s.normalTS); MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness); #if defined(DEBUG_DISPLAY) // 让 Rendering Debugger 的材质视图 / 材质校验也能作用于 MyPBR: // 把我们的表面参数翻译成 URP 的 SurfaceData,交给它的调试函数 SurfaceData sd = (SurfaceData)0; sd.albedo = s.albedo; sd.metallic = s.metallic; sd.specular = m.f0; sd.smoothness = 1.0h - s.perceptualRoughness; sd.normalTS = s.normalTS; sd.emission = s.emission; sd.occlusion = s.occlusion; sd.alpha = s.alpha; id.brdfDiffuse = m.diffuseColor; id.brdfSpecular = m.f0; half4 debugColor; if (CanDebugOverrideOutputColor(id, sd, debugColor)) return debugColor; #endif MyLightingTerms t = MyComputeLighting(s, m, id); float3 color = t.directDiffuse + t.directSpecular + t.indirectDiffuse + t.indirectSpecular + t.emission; color = MixFog(color, id.fogCoord); #if defined(_WRITE_RENDERING_LAYERS) outRenderingLayers = float4(EncodeMeshRenderingLayer(GetMeshRenderingLayer()), 0, 0, 0); #endif return half4(color, 1.0); }
vec4 MyPBRFrag() { MySurface s = MySampleSurface(uv); InputData id = MyInitInputData(s.normalTS); MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness); // HLSL 版在 DEBUG_DISPLAY 下把表面参数交给 Rendering Debugger;网页版的调试视图见 Ch11/MyPBRDebug.glsl MyLightingTerms t = MyComputeLighting(s, m, id); vec3 color = t.directDiffuse + t.directSpecular + t.indirectDiffuse + t.indirectSpecular + t.emission; color = MixFog(color, id.fogCoord); return vec4(color, 1.0); }
片元函数的主干只有四行:读表面 → 填 InputData → 推导 BRDF 参数 → 计算光照。DEBUG_DISPLAY 分支把我们的 MySurface 翻译成 URP 的 SurfaceData,交给 CanDebugOverrideOutputColor(摘自 URP 14 ShaderLibrary/Debug/Debugging3D.hlsl,我们用的是不需要 BRDFData 的三参数版本)。有了这几行,Window › Analysis › Rendering Debugger 里的 Albedo、Smoothness、法线等材质视图和材质校验对 MyPBR 同样有效——c13 会大量用到这个功能。_WRITE_RENDERING_LAYERS 来自 RenderingLayers.hlsl 的 pragma,开启 Decal Layers / Light Layers 的某些组合时,URP 要求不透明物体额外输出一个 Rendering Layer 目标。

DEBUG_DISPLAY 分支,中、右两格里的 MyPBR 物体会照常着色。unity_check 和 Inspector 的编译按钮默认只编译当前关键字组合,光照贴图、调试视图、逐顶点光这类“平时不开”的分支即使有语法错误也发现不了。配套工程的 Editor/MyPBRVariantCheck.cs(菜单 )用六组典型关键字组合强制编译每个 Pass,并把编译错误打印出来;本章配图时它报告的错误数为 0。