GameTech/URP 渲染实战

案例 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 主文件

Assets/Cookbook/Ch06/ClassicLit.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 按需补上。

材质输入

Assets/Cookbook/Ch06/ClassicLitInput.hlsl
// 案例 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 的布局不能随关键字变化。

顶点着色器

Assets/Cookbook/Ch06/ClassicLitForwardPass.hlsl(节选:vert)
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

Assets/Cookbook/Ch06/ClassicLitForwardPass.hlsl(节选: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 而不是直接使用顶点里的雾因子。

一盏灯的光照

Assets/Cookbook/Ch06/ClassicLitForwardPass.hlsl(节选:blinnphong)
// 一盏灯的贡献:归一化 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。所有光源——主光源、附加方向光、点光、聚光——都调用它。

片元着色器

Assets/Cookbook/Ch06/ClassicLitForwardPass.hlsl(节选:frag)
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);
}
  1. 阴影遮罩与 AO。CalculateShadowMask 在没有光照贴图时返回 unity_ProbesOcclusion;CreateAmbientOcclusionFactor 在开启 SSAO 时采样 _ScreenSpaceOcclusionTexture,得到间接光遮蔽和直接光遮蔽两个值,没开启时都是 1。
  2. 主光源。GetMainLight(inputData, shadowMask, aoFactor) 一次完成:阴影采样、阴影淡出、烘焙阴影混合、Cookie、SSAO 对直接光的衰减。
  3. 附加光。结构与案例 6.1 相同,只是换成了带阴影和 AO 的 GetAdditionalLight 重载,并加上 Light Layers 判断。宏 FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK 只在“Forward+ + 光照贴图 + Subtractive 模式”时展开为一个 continue,用来跳过已经烘焙进光照贴图的灯,保留它是为了和 URP 的行为一致。
  4. 自发光与雾。自发光在雾之前加入,所以远处的灯泡会被雾吞没;如果希望自发光穿透雾,可以把它移到 MixFog 之后。

效果

图 6-21场景中所有物体(包括地面)都使用 ClassicLit:红色纽结与蓝球是高光滑度材质,砖块立方体使用公共贴图 bricks_albedo,地上的“灯泡”是自发光材质,里面藏着一盏投射软阴影的暖色点光;右侧还有一盏冷色点光和一盏投影的聚光灯,主光源为微弱的逆光
图 6-22同一场景在三种渲染路径下的结果。左:Forward;中:Forward+;右:Deferred(ClassicLit 作为 Forward Only 材质绘制)。三者一致,说明附加光循环与 Pass 标签都写对了
图 6-23光滑度从左到右为 0.1、0.3、0.5、0.7、0.9,高光颜色同为 0.2。上排:URP Simple Lit;下排:ClassicLit。两者的指数映射相同,但 Simple Lit 的高光没有归一化,越光滑越暗;ClassicLit 越光滑越小、越亮

与 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 已经拆开,这些扩展都只需要改动很小的范围。

变体数量前向 Pass 的关键字组合在理论上有数万种。URP 在构建时会根据工程里所有 URP Asset 实际启用的功能(是否有附加光阴影、是否用 Forward+、是否开 SSAO 等)剥离不可能用到的变体,实际进包的数量要少得多。如果构建时间或包体过大,第 29 章会介绍如何进一步剥离。