URP 渲染实战 / 第 10 章 逐行读懂 URP Lit / 案例 10.1
案例 10.1最小 Lit:自己喂数据给 UniversalFragmentPBR进阶
目标:写一个只有一百多行的 Lit Shader:自己填 SurfaceData 和 InputData,直接调用 URP 的 UniversalFragmentPBR,光照结果与 URP Lit 一致;同时提供 ShadowCaster、DepthOnly、DepthNormals Pass,并保持 SRP Batcher 兼容。
思路:三个以上的 Pass 要共享同一份 UnityPerMaterial,最稳妥的做法是把 CBUFFER 写进一个 hlsl,所有 Pass 都 include 它。阴影和深度 Pass 不必手写:URP 的 ShadowCasterPass.hlsl、DepthOnlyPass.hlsl 只依赖 _BaseMap、_BaseColor、_Cutoff 这几个名字(_ALPHATEST_ON 时才用到),先 include 我们的输入文件再 include 它们即可。
Assets/Cookbook/Ch10/MinimalLitInput.hlsl
#ifndef COOKBOOK_CH10_MINIMAL_LIT_INPUT_INCLUDED #define COOKBOOK_CH10_MINIMAL_LIT_INPUT_INCLUDED // “最小 Lit”的材质输入。三个 Pass(ForwardLit / ShadowCaster / DepthOnly)共用这一份, // 保证 UnityPerMaterial 布局完全一致 —— 这是 SRP Batcher 兼容的前提。 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // SurfaceInput.hlsl 声明了 _BaseMap / _BumpMap / _EmissionMap 以及 SampleAlbedoAlpha、SampleNormal、Alpha 等辅助函数, // URP 的 ShadowCasterPass.hlsl、DepthOnlyPass.hlsl 也依赖这些名字。 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half4 _EmissionColor; half _Cutoff; // ShadowCasterPass / DepthOnlyPass 在 _ALPHATEST_ON 时会用到 half _Metallic; half _Smoothness; half _BumpScale; CBUFFER_END #endif
#ifndef COOKBOOK_CH10_MINIMAL_LIT_INPUT_INCLUDED #define COOKBOOK_CH10_MINIMAL_LIT_INPUT_INCLUDED // “最小 Lit”的材质输入。(GLSL 版,对应 Ch10/MinimalLitInput.hlsl) // 三个 Pass(ForwardLit / ShadowCaster / DepthOnly)共用这一份, // 保证 UnityPerMaterial 布局完全一致 —— 这是 SRP Batcher 兼容的前提。 // HLSL 版 include 了 SurfaceInput.hlsl(声明 _BaseMap / _BumpMap / _EmissionMap 以及 SampleAlbedoAlpha、SampleNormal、Alpha); // GLSL 没有 TEXTURE2D_ARGS 这种“贴图 + 采样器”参数宏,下面直接写出这几个函数,采样固定的那张贴图。 uniform vec4 _BaseMap_ST; uniform vec4 _BaseColor; uniform vec4 _EmissionColor; uniform float _Cutoff; // ShadowCasterPass / DepthOnlyPass 在 _ALPHATEST_ON 时会用到 uniform float _Metallic; uniform float _Smoothness; uniform float _BumpScale; // ---- SurfaceInput.hlsl ---- uniform sampler2D _BaseMap; uniform sampler2D _BumpMap; uniform sampler2D _EmissionMap; vec4 SampleAlbedoAlpha(vec2 uv) { return texture(_BaseMap, uv); } float Alpha(float albedoAlpha, vec4 color, float cutoff) { float alpha = albedoAlpha * color.a; #ifdef _ALPHATEST_ON if (alpha - cutoff < 0.0) discard; #endif return alpha; } vec3 SampleNormal(vec2 uv, float scale) { #ifdef _NORMALMAP return UnpackNormalScale(texture(_BumpMap, uv), scale); #else return vec3(0.0, 0.0, 1.0); #endif } #endif
ForwardLit Pass 的关键字从 Lit 里挑出实时光照真正需要的部分(光照贴图、Light Cookie、Rendering Layers、DBuffer、Debug Display 等省略):
Assets/Cookbook/Ch10/MinimalLit.shader(节选:keywords)
// 材质关键字:有法线贴图才开(脚本创建材质时要自己 EnableKeyword) #pragma shader_feature_local _NORMALMAP // 管线关键字:与 URP 14 Lit.shader 保持同名,管线会按设置自动打开 #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING #pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION #pragma multi_compile _ _FORWARD_PLUS #pragma multi_compile_fog #pragma multi_compile_instancing
核心是两个“填表”函数。SurfaceData 按金属度工作流填写,specular 留 0:
Assets/Cookbook/Ch10/MinimalLit.shader(节选:surface)
SurfaceData BuildSurfaceData(float2 uv) { SurfaceData s = (SurfaceData)0; half4 albedoAlpha = SampleAlbedoAlpha(uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)); s.albedo = albedoAlpha.rgb * _BaseColor.rgb; s.alpha = 1.0; // 不透明 s.metallic = _Metallic; // 金属度工作流:specular 留 0 s.specular = 0; s.smoothness = _Smoothness; s.normalTS = SampleNormal(uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap), _BumpScale); s.occlusion = 1.0; s.emission = _EmissionColor.rgb; s.clearCoatMask = 0; s.clearCoatSmoothness = 0; return s; }
SurfaceData BuildSurfaceData(vec2 uv) { SurfaceData s = InitSurfaceData(); vec4 albedoAlpha = SampleAlbedoAlpha(uv); s.albedo = albedoAlpha.rgb * _BaseColor.rgb; s.alpha = 1.0; // 不透明 s.metallic = _Metallic; // 金属度工作流:specular 留 0 s.specular = vec3(0.0); s.smoothness = _Smoothness; s.normalTS = SampleNormal(uv, _BumpScale); s.occlusion = 1.0; s.emission = _EmissionColor.rgb; s.clearCoatMask = 0.0; s.clearCoatSmoothness = 0.0; return s; }
InputData 的写法与 InitializeInputData 一致,有三处简化:阴影坐标总是逐像素计算(省掉一个插值器,代价是每像素一次矩阵乘法);环境漫反射直接逐像素 SampleSH;ShadowMask 设为全 1(没有光照贴图):
Assets/Cookbook/Ch10/MinimalLit.shader(节选:inputdata)
InputData BuildInputData(Varyings i, half3 normalTS) { InputData d = (InputData)0; d.positionWS = i.positionWS; d.positionCS = i.positionCS; #if defined(_NORMALMAP) float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz); d.tangentToWorld = half3x3(i.tangentWS.xyz, bitangent, i.normalWS); d.normalWS = TransformTangentToWorld(normalTS, d.tangentToWorld); #else d.normalWS = i.normalWS; #endif d.normalWS = NormalizeNormalPerPixel(d.normalWS); d.viewDirectionWS = GetWorldSpaceNormalizeViewDir(i.positionWS); d.shadowCoord = TransformWorldToShadowCoord(i.positionWS); // 逐像素算级联,省一个插值器 d.fogCoord = i.fogFactor; d.vertexLighting = 0; d.bakedGI = SampleSH(d.normalWS); // 无光照贴图:逐像素 SH d.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(i.positionCS); d.shadowMask = half4(1, 1, 1, 1); return d; }
InputData BuildInputData(vec3 normalTS) { InputData d = InitInputData(); d.positionWS = positionWS; d.positionCS = gl_FragCoord; #if defined(_NORMALMAP) vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz); d.tangentToWorld = mat3(tangentWS.xyz, bitangent, normalWS); // GLSL 的 mat3 按列构造:列 = T、B、N d.normalWS = TransformTangentToWorld(normalTS, d.tangentToWorld); #else d.normalWS = normalWS; #endif d.normalWS = NormalizeNormalPerPixel(d.normalWS); d.viewDirectionWS = GetWorldSpaceNormalizeViewDir(positionWS); d.shadowCoord = TransformWorldToShadowCoord(positionWS); // 逐像素算级联,省一个插值器 d.fogCoord = fogFactor; d.vertexLighting = vec3(0.0); d.bakedGI = SampleSH(d.normalWS); // 无光照贴图:逐像素 SH d.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(gl_FragCoord); d.shadowMask = vec4(1.0); return d; }
Assets/Cookbook/Ch10/MinimalLit.shader(节选:frag)
half4 frag(Varyings i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); SurfaceData surfaceData = BuildSurfaceData(i.uv); InputData inputData = BuildInputData(i, surfaceData.normalTS); half4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; }
注意变量名必须叫 inputData——原因见上一节 Forward+ 的 LIGHT_LOOP_BEGIN。最后是三个辅助 Pass:
Assets/Cookbook/Ch10/MinimalLit.shader(节选:shadowpass)
Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma target 2.0 #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #pragma multi_compile_instancing #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW // 先 include 自己的输入(CBUFFER 与 ForwardLit 相同),再直接复用 URP 的阴影投射 Pass #include "Assets/Cookbook/Ch10/MinimalLitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl" ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R HLSLPROGRAM #pragma target 2.0 #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #pragma multi_compile_instancing #include "Assets/Cookbook/Ch10/MinimalLitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl" ENDHLSL } Pass { // 开了 SSAO(Depth Normals 模式)或其他需要 _CameraNormalsTexture 的功能时会用到 Name "DepthNormals" Tags { "LightMode"="DepthNormals" } ZWrite On HLSLPROGRAM #pragma target 2.0 #pragma vertex DepthNormalsVertex #pragma fragment DepthNormalsFragment #pragma shader_feature_local _NORMALMAP #pragma multi_compile_instancing #include "Assets/Cookbook/Ch10/MinimalLitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/LitDepthNormalsPass.hlsl" ENDHLSL }
Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 GLSLPROGRAM // 先 include 自己的输入(uniform 与 ForwardLit 相同),再复用 URP 的阴影投射 Pass(ShadowCasterPass.hlsl 的 GLSL 写法) #include "Ch10/MinimalLitInput.glsl" VARYING vec2 uv; #ifdef VERTEX void main() // ShadowPassVertex { uv = TransformTex(texcoord0.xy, _BaseMap_ST); gl_Position = GetShadowPositionHClip(positionOS.xyz, normalOS); } #endif #ifdef FRAGMENT void main() // ShadowPassFragment { Alpha(SampleAlbedoAlpha(uv).a, _BaseColor, _Cutoff); fragColor = vec4(0.0); } #endif ENDGLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R GLSLPROGRAM #include "Ch10/MinimalLitInput.glsl" VARYING vec2 uv; #ifdef VERTEX void main() // DepthOnlyVertex { uv = TransformTex(texcoord0.xy, _BaseMap_ST); gl_Position = TransformObjectToHClip(positionOS.xyz); } #endif #ifdef FRAGMENT void main() // DepthOnlyFragment { Alpha(SampleAlbedoAlpha(uv).a, _BaseColor, _Cutoff); fragColor = vec4(gl_FragCoord.z); } #endif ENDGLSL } Pass { // 开了 SSAO(Depth Normals 模式)或其他需要 _CameraNormalsTexture 的功能时会用到 Name "DepthNormals" Tags { "LightMode"="DepthNormals" } ZWrite On GLSLPROGRAM #pragma shader_feature_local _NORMALMAP #include "Ch10/MinimalLitInput.glsl" VARYING vec2 uv; VARYING vec3 normalWS; VARYING vec4 tangentWS; #ifdef VERTEX void main() // DepthNormalsVertex(LitDepthNormalsPass.hlsl) { uv = TransformTex(texcoord0.xy, _BaseMap_ST); VertexNormalInputs n = GetVertexNormalInputs(normalOS, tangentOS); normalWS = n.normalWS; tangentWS = vec4(n.tangentWS, tangentOS.w * GetOddNegativeScale()); gl_Position = TransformObjectToHClip(positionOS.xyz); } #endif #ifdef FRAGMENT void main() // DepthNormalsFragment { Alpha(SampleAlbedoAlpha(uv).a, _BaseColor, _Cutoff); #ifdef _NORMALMAP vec3 nTS = SampleNormal(uv, _BumpScale); vec3 b = tangentWS.w * cross(normalWS, tangentWS.xyz); vec3 n = normalize(mat3(tangentWS.xyz, b, normalWS) * nTS); #else vec3 n = normalize(normalWS); #endif fragColor = vec4(n, 0.0); } #endif ENDGLSL }

扩展方向想加 Mask Map,就在 BuildSurfaceData 里采样一张贴图,把 R、G、A 分别填进 metallic、occlusion、smoothness;想支持光照贴图,就照 Lit 加上
LIGHTMAP_ON 等关键字、DECLARE_LIGHTMAP_OR_SH 插值器和 SAMPLE_GI,再补一个 Meta Pass。Shader Graph 的 Lit Target 生成的代码,本质上也是这个结构。常见坑① 不要用
UsePass "Universal Render Pipeline/Lit/ShadowCaster" 偷懒:被引用 Pass 的 CBUFFER 是 Lit 的布局,与你的 ForwardLit 不同,整个 Shader 会失去 SRP Batcher 兼容性(材质 Inspector 顶部会提示原因)。② 漏掉 _FORWARD_PLUS,Forward+ 渲染器下附加光不亮。③ 漏掉 _MAIN_LIGHT_SHADOWS_SCREEN 不会报错,但开启屏幕空间阴影(Screen Space Shadows Renderer Feature)后会选到“无阴影”变体——本案例为简洁省略了它,需要时照 Lit 补上,并像 GetShadowCoord 那样为它计算屏幕坐标。