案例 6.9ClassicLit:完整的 Blinn-Phong + 多光源 + 阴影 Shader高阶
目标:把本章所有内容组装成一个可以直接用于项目的 Shader:归一化 Blinn-Phong、球谐环境光、主光源级联软阴影、Forward / Forward+ 附加光循环、附加光阴影、逐顶点附加光、Light Layers、光照 Cookie、SSAO、雾效、Alpha 裁剪、GPU Instancing,以及完整的 ShadowCaster / DepthOnly / DepthNormals Pass,兼容 SRP Batcher,并能在 Forward、Forward+、Deferred 三种路径下工作。
文件组织仿照 URP 自己的 Lit:
| 文件 | 内容 |
|---|---|
ClassicLit.shader | 属性、渲染状态、各 Pass 的关键字声明 |
ClassicLitInput.hlsl | 材质 CBUFFER 与贴图声明,被所有 Pass 共享 |
ClassicLitForwardPass.hlsl | 前向光照的顶点/片元函数 |
ShadowCasterPass.hlsl 等 | 案例 6.4、6.8 写好的通用 Pass |
Shader 主文件
// 案例 6.9 ClassicLit:完整的 Blinn-Phong + 多光源 + 阴影 Shader(后续章节会以它为基础扩展) Shader "Cookbook/Ch06/ClassicLit" { Properties { [MainTexture] _BaseMap("Base Map", 2D) = "white" {} [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1) _SpecColor("Specular Color", Color) = (0.2, 0.2, 0.2, 1) _Smoothness("Smoothness", Range(0, 1)) = 0.5 [HDR] _EmissionColor("Emission", Color) = (0, 0, 0, 1) [Toggle(_ALPHATEST_ON)] _AlphaClip("Alpha Clip", Float) = 0 _Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5 [Toggle(_RECEIVE_SHADOWS_OFF)] _ReceiveShadowsOff("Receive Shadows Off", Float) = 0 [Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", Float) = 2 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" } HLSLINCLUDE #include "Assets/Cookbook/Ch06/ClassicLitInput.hlsl" ENDHLSL Pass { Name "ForwardLit" // 没有 GBuffer Pass 的自定义光照 Shader 要标成 UniversalForwardOnly: // Forward / Forward+ 下它和 UniversalForward 一样被绘制;Deferred 下它会进入“Forward Only”通道, // 而只标 UniversalForward 的物体在 Deferred 里会直接消失 Tags { "LightMode"="UniversalForwardOnly" } Cull [_Cull] HLSLPROGRAM #pragma target 2.0 #pragma vertex ClassicLitVertex #pragma fragment ClassicLitFragment // 材质关键字 #pragma shader_feature_local _RECEIVE_SHADOWS_OFF #pragma shader_feature_local_fragment _ALPHATEST_ON // URP 关键字(与 URP 14 Lit.shader 的 ForwardLit 保持一致的子集) #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_fragment _ _ADDITIONAL_LIGHT_SHADOWS #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 #pragma multi_compile_fog #pragma multi_compile_instancing #include "Assets/Cookbook/Ch06/ClassicLitForwardPass.hlsl" ENDHLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull [_Cull] HLSLPROGRAM #pragma target 2.0 #pragma vertex ShadowCasterVertex #pragma fragment ShadowCasterFragment #pragma shader_feature_local_fragment _ALPHATEST_ON #pragma multi_compile_instancing // 区分方向光与点光/聚光:两者计算“指向光源方向”的方式不同 #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW #include "Assets/Cookbook/Ch06/ShadowCasterPass.hlsl" ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R Cull [_Cull] HLSLPROGRAM #pragma target 2.0 #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #pragma shader_feature_local_fragment _ALPHATEST_ON #pragma multi_compile_instancing #include "Assets/Cookbook/Ch06/DepthOnlyPass.hlsl" ENDHLSL } Pass { Name "DepthNormalsOnly" // 与 UniversalForwardOnly 配套:Deferred 的深度法线预渲染只收 DepthNormalsOnly; // Forward 下 DepthNormals 与 DepthNormalsOnly 都会被绘制,所以两者只能二选一 Tags { "LightMode"="DepthNormalsOnly" } ZWrite On Cull [_Cull] HLSLPROGRAM #pragma target 2.0 #pragma vertex DepthNormalsVertex #pragma fragment DepthNormalsFragment #pragma shader_feature_local_fragment _ALPHATEST_ON #pragma multi_compile_instancing #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/RenderingLayers.hlsl" #include "Assets/Cookbook/Ch06/DepthNormalsPass.hlsl" ENDHLSL } } }
// 案例 6.9 ClassicLit:完整的 Blinn-Phong + 多光源 + 阴影 Shader(后续章节会以它为基础扩展)(GLSL 版,对应 Ch06/ClassicLit.shader) Shader "Cookbook/Ch06/ClassicLit" { SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" } GLSLINCLUDE #include "Ch06/ClassicLitInput.glsl" ENDGLSL Pass { Name "ForwardLit" // 没有 GBuffer Pass 的自定义光照 Shader 要标成 UniversalForwardOnly: // Forward / Forward+ 下它和 UniversalForward 一样被绘制;Deferred 下它会进入“Forward Only”通道, // 而只标 UniversalForward 的物体在 Deferred 里会直接消失 Tags { "LightMode"="UniversalForwardOnly" } Cull [_Cull] GLSLPROGRAM // 材质关键字 #pragma shader_feature_local _RECEIVE_SHADOWS_OFF #pragma shader_feature_local_fragment _ALPHATEST_ON // URP 关键字:主光阴影、附加光(逐像素)、附加光阴影、软阴影由网页版引擎始终定义; // _FORWARD_PLUS、_LIGHT_LAYERS 在网页版里分别由引擎内部 / uniform _LightLayers 处理; // _SCREEN_SPACE_OCCLUSION 由 _AmbientOcclusionParam.x 判断;雾由 unity_FogMode 判断 #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #include "Ch06/ClassicLitForwardPass.glsl" ENDGLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull [_Cull] GLSLPROGRAM #pragma shader_feature_local_fragment _ALPHATEST_ON // 区分方向光与点光/聚光:两者计算“指向光源方向”的方式不同(GLSL 版里是 uniform _CastingPunctualLightShadow) #include "Ch06/ShadowCasterPass.glsl" ENDGLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R Cull [_Cull] GLSLPROGRAM #pragma shader_feature_local_fragment _ALPHATEST_ON #include "Ch06/DepthOnlyPass.glsl" ENDGLSL } Pass { Name "DepthNormalsOnly" // 与 UniversalForwardOnly 配套:Deferred 的深度法线预渲染只收 DepthNormalsOnly; // Forward 下 DepthNormals 与 DepthNormalsOnly 都会被绘制,所以两者只能二选一 Tags { "LightMode"="DepthNormalsOnly" } ZWrite On Cull [_Cull] GLSLPROGRAM #pragma shader_feature_local_fragment _ALPHATEST_ON #include "Ch06/DepthNormalsPass.glsl" ENDGLSL } } }
前向 Pass 的关键字是 URP 14 Lit.shader 中 ForwardLit 关键字的一个子集:去掉了光照贴图、反射探针混合、贴花缓冲、调试显示等第 8 章以后才涉及的部分。如果你的项目用到这些功能,回到 Lit.shader 按需补上。
材质输入
// 案例 6.9 ClassicLit 的材质输入:所有 Pass 共享同一个 UnityPerMaterial,保证 SRP Batcher 兼容 #ifndef COOKBOOK_CH06_CLASSIC_LIT_INPUT_INCLUDED #define COOKBOOK_CH06_CLASSIC_LIT_INPUT_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half4 _SpecColor; half _Smoothness; half4 _EmissionColor; half _Cutoff; CBUFFER_END TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); #endif
// 案例 6.9 ClassicLit 的材质输入:所有 Pass 共享同一组材质参数(GLSL 版,对应 Ch06/ClassicLitInput.hlsl) // HLSL 版把它们放进 CBUFFER_START(UnityPerMaterial) 以兼容 SRP Batcher;GLSL 里就是普通 uniform。 uniform vec4 _BaseMap_ST; uniform vec4 _BaseColor; uniform vec4 _SpecColor; uniform float _Smoothness; uniform vec4 _EmissionColor; uniform float _Cutoff; uniform sampler2D _BaseMap;
CBUFFER 被放在单独的文件里,通过 SubShader 级别的 HLSLINCLUDE 引入,四个 Pass 拿到的是同一份声明。_Cutoff 只有在开启 Alpha 裁剪时才被用到,但它必须始终存在于 CBUFFER 中:SRP Batcher 要求 CBUFFER 的布局不能随关键字变化。
顶点着色器
Varyings ClassicLitVertex(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); half3 normalWS = TransformObjectToWorldNormal(input.normalOS); output.positionCS = vertexInput.positionCS; output.positionWS = vertexInput.positionWS; output.normalWS = normalWS; output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); half fogFactor = ComputeFogFactor(vertexInput.positionCS.z); half3 vertexLight = VertexLighting(vertexInput.positionWS, normalWS); // 只有 _ADDITIONAL_LIGHTS_VERTEX 时非零 output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) output.shadowCoord = GetShadowCoord(vertexInput); #endif return output; }
#ifdef VERTEX // URP 的 VertexLighting():只有 _ADDITIONAL_LIGHTS_VERTEX(逐顶点附加光)时才非零 vec3 VertexLighting(vec3 positionWS, vec3 normalWS) { vec3 vertexLightColor = vec3(0.0); #if defined(_ADDITIONAL_LIGHTS_VERTEX) for (int i = 0; i < GetAdditionalLightsCount(); i++) { Light light = GetAdditionalPerObjectLight(i, positionWS); vertexLightColor += LightingLambert(light.color * light.distanceAttenuation, light.direction, normalWS); } #endif return vertexLightColor; } void main() { VertexPositionInputs vertexInput = GetVertexPositionInputs(positionOS.xyz); vec3 nWS = TransformObjectToWorldNormal(normalOS); gl_Position = vertexInput.positionCS; positionWS = vertexInput.positionWS; normalWS = nWS; uv = TransformTex(texcoord0.xy, _BaseMap_ST); float fogFactor = ComputeFogFactor(vertexInput.positionCS.z); vec3 vertexLight = VertexLighting(vertexInput.positionWS, nWS); // 只有 _ADDITIONAL_LIGHTS_VERTEX 时非零 fogFactorAndVertexLight = vec4(fogFactor, vertexLight); } #endif
顶点阶段做三件“能提前算就提前算”的事:雾因子(URP 14 实际在片元里重新计算,见下)、逐顶点附加光 VertexLighting()(只有 _ADDITIONAL_LIGHTS_VERTEX 时才有内容,其余情况返回 0 并被编译器优化掉),以及在单级联时提前算好阴影坐标。
组装 InputData
InputData BuildInputData(Varyings input, half3 normalWS) { InputData inputData = (InputData)0; inputData.positionWS = input.positionWS; inputData.positionCS = input.positionCS; inputData.normalWS = normalWS; inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(input.positionWS); #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) inputData.shadowCoord = input.shadowCoord; #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS) inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS); // 级联:逐像素选级联 #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif // URP 14 强制逐像素算雾(ShaderVariablesFunctions.hlsl 里 #define _FOG_FRAGMENT 1),顶点雾因子只作后备 inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactorAndVertexLight.x); inputData.vertexLighting = input.fogFactorAndVertexLight.yzw; inputData.bakedGI = SampleSH(normalWS); // 本章只用球谐环境光,光照贴图见第 8 章 inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS); inputData.shadowMask = half4(1, 1, 1, 1); // 没有烘焙阴影时为 1 return inputData; }
InputData BuildInputData(vec3 normalWS) { InputData inputData = InitInputData(); inputData.positionWS = positionWS; inputData.positionCS = gl_FragCoord; inputData.normalWS = normalWS; inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(positionWS); #if defined(_RECEIVE_SHADOWS_OFF) inputData.shadowCoord = vec4(0, 0, 0, 0); // 不接收阴影:URP 不定义 MAIN_LIGHT_CALCULATE_SHADOWS #else inputData.shadowCoord = TransformWorldToShadowCoord(positionWS); // 级联:逐像素选级联 #endif // URP 14 强制逐像素算雾(_FOG_FRAGMENT),这里按像素位置重新求雾因子,顶点雾因子只作后备 inputData.fogCoord = ComputeFogFactor(TransformWorldToHClip(positionWS).z); inputData.vertexLighting = fogFactorAndVertexLight.yzw; inputData.bakedGI = SampleSH(normalWS); // 本章只用球谐环境光,光照贴图见第 8 章 inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(gl_FragCoord); inputData.shadowMask = vec4(1, 1, 1, 1); // 没有烘焙阴影时为 1 return inputData; }
这个函数和 URP 的 InitializeInputData(LitForwardPass.hlsl)结构相同。之所以要把各种数据装进 InputData,除了 LIGHT_LOOP_BEGIN 的硬性要求,还因为 URP 的很多函数——GetMainLight(inputData, ...)、GetAdditionalLight(i, inputData, ...)、CalculateShadowMask——都以它为参数。注意雾:URP 14 在 ShaderVariablesFunctions.hlsl 里写死了 #define _FOG_FRAGMENT 1,雾一律按像素深度计算,所以要调用 InitializeInputDataFog 而不是直接使用顶点里的雾因子。
一盏灯的光照
// 一盏灯的贡献:归一化 Blinn-Phong(URP 约定,漫反射不除 π,高光系数为 (n+8)/8) half3 ClassicBlinnPhong(Light light, half3 normalWS, half3 viewDirWS, half3 albedo, half3 specColor, float shininess) { half NdotL = saturate(dot(normalWS, light.direction)); half3 radiance = light.color * (light.distanceAttenuation * light.shadowAttenuation * NdotL); float3 H = SafeNormalize(float3(light.direction) + float3(viewDirWS)); float NdotH = saturate(dot(float3(normalWS), H)); float spec = (shininess + 8.0) * 0.125 * pow(NdotH, shininess); return (albedo + specColor * spec) * radiance; }
// 一盏灯的贡献:归一化 Blinn-Phong(URP 约定,漫反射不除 π,高光系数为 (n+8)/8) vec3 ClassicBlinnPhong(Light light, vec3 normalWS, vec3 viewDirWS, vec3 albedo, vec3 specColor, float shininess) { float NdotL = saturate(dot(normalWS, light.direction)); vec3 radiance = light.color * (light.distanceAttenuation * light.shadowAttenuation * NdotL); vec3 H = SafeNormalize(light.direction + viewDirWS); float NdotH = saturate(dot(normalWS, H)); float spec = (shininess + 8.0) * 0.125 * pow(NdotH, shininess); return (albedo + specColor * spec) * radiance; }
这就是第 5 章案例 5.6 的归一化 Blinn-Phong,只是把光的颜色、距离衰减、阴影衰减和 N·L 合成了一个 radiance。所有光源——主光源、附加方向光、点光、聚光——都调用它。
片元着色器
half4 ClassicLitFragment(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); half4 baseSample = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; #if defined(_ALPHATEST_ON) clip(baseSample.a - _Cutoff); #endif half3 albedo = baseSample.rgb; half3 specColor = _SpecColor.rgb; float shininess = exp2(10.0 * _Smoothness + 1.0); // 与 URP SimpleLit 相同的映射 half3 normalWS = NormalizeNormalPerPixel(input.normalWS); InputData inputData = BuildInputData(input, normalWS); // 名字必须叫 inputData,LIGHT_LOOP_BEGIN 会用到 half4 shadowMask = CalculateShadowMask(inputData); AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData.normalizedScreenSpaceUV, 1.0); uint meshRenderingLayers = GetMeshRenderingLayer(); // ---- 环境光(SSAO 只压暗间接光,直接光按 Direct Lighting Strength 衰减)---- half3 color = inputData.bakedGI * albedo * aoFactor.indirectAmbientOcclusion; // ---- 主光源:带阴影、阴影淡出和 Cookie ---- Light mainLight = GetMainLight(inputData, shadowMask, aoFactor); #if defined(_LIGHT_LAYERS) if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers)) #endif color += ClassicBlinnPhong(mainLight, normalWS, inputData.viewDirectionWS, albedo, specColor, shininess); // ---- 附加光源 ---- #if defined(_ADDITIONAL_LIGHTS) uint pixelLightCount = GetAdditionalLightsCount(); #if USE_FORWARD_PLUS for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++) { FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor); #if defined(_LIGHT_LAYERS) if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers)) #endif color += ClassicBlinnPhong(light, normalWS, inputData.viewDirectionWS, albedo, specColor, shininess); } #endif LIGHT_LOOP_BEGIN(pixelLightCount) // 这个重载会顺带算出附加光阴影(_ADDITIONAL_LIGHT_SHADOWS)与 Cookie Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor); #if defined(_LIGHT_LAYERS) if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers)) #endif color += ClassicBlinnPhong(light, normalWS, inputData.viewDirectionWS, albedo, specColor, shininess); LIGHT_LOOP_END #endif #if defined(_ADDITIONAL_LIGHTS_VERTEX) color += inputData.vertexLighting * albedo; // 逐顶点附加光:只有漫反射 #endif color += _EmissionColor.rgb; color = MixFog(color, inputData.fogCoord); return half4(color, baseSample.a); }
void main() { vec4 baseSample = texture(_BaseMap, uv) * _BaseColor; #if defined(_ALPHATEST_ON) if (baseSample.a - _Cutoff < 0.0) discard; #endif vec3 albedo = baseSample.rgb; vec3 specColor = _SpecColor.rgb; float shininess = exp2(10.0 * _Smoothness + 1.0); // 与 URP SimpleLit 相同的映射 vec3 N = NormalizeNormalPerPixel(normalWS); InputData inputData = BuildInputData(N); vec4 shadowMask = inputData.shadowMask; // URP:CalculateShadowMask(inputData) // URP:CreateAmbientOcclusionFactor(uv, 1.0)。没开 SSAO 时两项都是 1 AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV); uint meshRenderingLayers = GetMeshRenderingLayer(); // ---- 环境光(SSAO 只压暗间接光,直接光按 Direct Lighting Strength 衰减)---- vec3 color = inputData.bakedGI * albedo * aoFactor.indirectAmbientOcclusion; // ---- 主光源:带阴影、阴影淡出(URP 的 GetMainLight(inputData, shadowMask, aoFactor))---- Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask); mainLight.color *= aoFactor.directAmbientOcclusion; // HLSL 版的 #if defined(_LIGHT_LAYERS):GLSL 版里是 uniform _LightLayers(URP Asset 打开 Light Layers 时为 1) if (_LightLayers < 0.5 || IsMatchingLightLayer(GetMainLightLayerMask(), meshRenderingLayers)) color += ClassicBlinnPhong(mainLight, N, inputData.viewDirectionWS, albedo, specColor, shininess); // ---- 附加光源 ---- #if defined(_ADDITIONAL_LIGHTS) // HLSL 版:Forward+ 先单独循环附加方向光,再用 LIGHT_LOOP_BEGIN 遍历分簇结构; // 网页版统一成普通 for 循环(Forward 下是本物体的灯,Forward+ 下是全部可见附加光,方向光在最前面) int pixelLightCount = GetAdditionalLightsCount(); for (int lightIndex = 0; lightIndex < pixelLightCount; lightIndex++) { // 这个重载会顺带算出附加光阴影(_ADDITIONAL_LIGHT_SHADOWS) Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask); #if defined(_RECEIVE_SHADOWS_OFF) light.shadowAttenuation = 1.0; #endif light.color *= aoFactor.directAmbientOcclusion; if (_LightLayers < 0.5 || IsMatchingLightLayer(GetAdditionalLightLayerMask(lightIndex), meshRenderingLayers)) color += ClassicBlinnPhong(light, N, inputData.viewDirectionWS, albedo, specColor, shininess); } #endif #if defined(_ADDITIONAL_LIGHTS_VERTEX) color += inputData.vertexLighting * albedo; // 逐顶点附加光:只有漫反射 #endif color += _EmissionColor.rgb; color = MixFog(color, inputData.fogCoord); fragColor = vec4(color, baseSample.a); }
- 阴影遮罩与 AO。
CalculateShadowMask在没有光照贴图时返回unity_ProbesOcclusion;CreateAmbientOcclusionFactor在开启 SSAO 时采样_ScreenSpaceOcclusionTexture,得到间接光遮蔽和直接光遮蔽两个值,没开启时都是 1。 - 主光源。
GetMainLight(inputData, shadowMask, aoFactor)一次完成:阴影采样、阴影淡出、烘焙阴影混合、Cookie、SSAO 对直接光的衰减。 - 附加光。结构与案例 6.1 相同,只是换成了带阴影和 AO 的
GetAdditionalLight重载,并加上 Light Layers 判断。宏FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK只在“Forward+ + 光照贴图 + Subtractive 模式”时展开为一个continue,用来跳过已经烘焙进光照贴图的灯,保留它是为了和 URP 的行为一致。 - 自发光与雾。自发光在雾之前加入,所以远处的灯泡会被雾吞没;如果希望自发光穿透雾,可以把它移到
MixFog之后。
效果



与 Simple Lit 对比,除了归一化,还有两处差异值得注意:Simple Lit 的高光项没有乘 N·L(见 CalculateBlinnPhong 与 LightingSpecular),光从侧后方掠射时,高光可能越过交界线出现在背光面上;Simple Lit 的光滑度在 _SPECULAR_COLOR 模式下实际取自 _SpecColor.a(材质面板会把 Smoothness 滑块写进这里),用脚本修改 _Smoothness 是无效的。
如何扩展
ClassicLit 是后续章节的“底座”:第 7 章会给它加上法线贴图(在 Varyings 里加切线、构建 TBN);第 8 章会接入光照贴图、反射探针和 Meta Pass;第 14 章的卡通渲染会把 ClassicBlinnPhong 换成色阶化的光照函数;第 17 章的顶点动画需要在 Forward、ShadowCaster、DepthOnly、DepthNormals 四个 Pass 里做相同的顶点偏移。由于光照函数、InputData 构建和各个 Pass 已经拆开,这些扩展都只需要改动很小的范围。