GameTech/URP 渲染实战

案例 24.2在 Graph 中读取主光与阴影进阶

目标:写一个 File 模式的 Custom Function,输入世界坐标,输出主光方向、颜色、距离衰减和阴影衰减,可以同时用于 Lit 和 Unlit 图。

Assets/Cookbook/Ch24/CustomLighting.hlsl(节选:mainlight)
void MainLight_float(float3 WorldPos, out float3 Direction, out float3 Color,
                     out float DistanceAtten, out float ShadowAtten)
{
#if defined(SHADERGRAPH_PREVIEW)
    // 预览球:给一个固定的斜上方光,不算阴影
    Direction = normalize(float3(0.5, 0.5, -0.3));
    Color = float3(1, 1, 1);
    DistanceAtten = 1;
    ShadowAtten = 1;
#else
    // 在片元阶段逐像素求阴影坐标:同时兼容无级联、级联和屏幕空间阴影三种关键字
    float4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
    // 带 positionWS 的重载会顺带做“阴影距离淡出”,与 URP Lit 行为一致
    Light light = GetMainLight(shadowCoord, WorldPos, half4(1, 1, 1, 1));
    Direction = light.direction;
    Color = light.color;
    DistanceAtten = light.distanceAttenuation;
    ShadowAtten = light.shadowAttenuation;
#endif
}

void MainLight_half(half3 WorldPos, out half3 Direction, out half3 Color,
                    out half DistanceAtten, out half ShadowAtten)
{
    // half 版本直接转调 float 版本。注意:世界坐标一旦以 half 传进来就已经丢了精度(离原点远时阴影会抖),
    // 所以实际项目里建议把这个节点的 Precision 设成 Single,让 Shader Graph 调用 _float 版本
    float3 d, c; float da, sa;
    MainLight_float(WorldPos, d, c, da, sa);
    Direction = d; Color = c; DistanceAtten = da; ShadowAtten = sa;
}
void MainLight_float(vec3 WorldPos, out vec3 Direction, out vec3 Color,
                     out float DistanceAtten, out float ShadowAtten)
{
#if defined(SHADERGRAPH_PREVIEW)
    // 预览球:给一个固定的斜上方光,不算阴影
    Direction = normalize(vec3(0.5, 0.5, -0.3));
    Color = vec3(1, 1, 1);
    DistanceAtten = 1.0;
    ShadowAtten = 1.0;
#else
    // 在片元阶段逐像素求阴影坐标:同时兼容无级联、级联和屏幕空间阴影三种关键字
    vec4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
    // 带 positionWS 的重载会顺带做“阴影距离淡出”,与 URP Lit 行为一致
    Light light = GetMainLight(shadowCoord, WorldPos, vec4(1, 1, 1, 1));
    Direction = light.direction;
    Color = light.color;
    DistanceAtten = light.distanceAttenuation;
    ShadowAtten = light.shadowAttenuation;
#endif
}

void MainLight_half(vec3 WorldPos, out vec3 Direction, out vec3 Color,
                    out float DistanceAtten, out float ShadowAtten)
{
    // half 版本直接转调 float 版本。注意:世界坐标一旦以 half 传进来就已经丢了精度(离原点远时阴影会抖),
    // 所以实际项目里建议把这个节点的 Precision 设成 Single,让 Shader Graph 调用 _float 版本
    vec3 d, c; float da, sa;
    MainLight_float(WorldPos, d, c, da, sa);
    Direction = d; Color = c; DistanceAtten = da; ShadowAtten = sa;
}

节点设置:Type = File,Source 选这个 hlsl,Name = MainLight;Inputs 一个 WorldPos(Vector3);Outputs 依次是 Direction(Vector3)、Color(Vector3)、DistanceAtten(Float)、ShadowAtten(Float)。WorldPos 接 Position 节点(Space = World)。

PositionSpace: World Out Custom Function: MainLight WorldPosDirectionColorDistanceAttenShadowAtten Dot ProductA: Normal(World)B: Direction MultiplyColor × Shadow× Saturate(N·L) Base Color
图 24-4最简单的自定义 Lambert:MainLight 节点的输出与 Normal Vector 做点积,再乘灯光颜色和阴影

阴影坐标在片元阶段逐像素计算:Custom Function 默认被调用在 Surface Description 函数里,也就是片元着色器中。这样无论级联与否、是否屏幕空间阴影都能得到正确结果(23.4 节解释过为什么不能在顶点阶段插值级联阴影坐标)。

Unlit 图要手动加关键字。Lit Target 的 Forward Pass 已经声明了全部光照关键字;Unlit Target 的 Forward Pass 则只有光照贴图、SSAO、DBuffer 等少数几个(见 UniversalUnlitSubTarget.cs 的 UnlitKeywords.Forward)。在 Unlit 图里用上面的函数,_MAIN_LIGHT_SHADOWS 之类的宏全都没定义,MainLightRealtimeShadow 会直接返回 1——不报错,只是没阴影。解决办法是在 Blackboard 里添加以下 Keyword(全部 Multi Compile、Global、不 Exposed):

Keyword(Reference)类型作用
_MAIN_LIGHT_SHADOWS、_MAIN_LIGHT_SHADOWS_CASCADE、_MAIN_LIGHT_SHADOWS_SCREENBoolean ×3主光阴影(单级联 / 级联 / 屏幕空间)
_SHADOWS_SOFTBoolean,Stages = Fragment软阴影(每灯档位)
_ADDITIONAL_LIGHTSBoolean附加光逐像素
_ADDITIONAL_LIGHT_SHADOWSBoolean,Stages = Fragment附加光阴影
_FORWARD_PLUSBooleanForward+ 路径的分簇光照

用 Boolean 而不是一个 Enum 声明三个阴影关键字会多出一些永远用不到的组合(2³ 而不是 4),变体数更多;追求精简可以照 URP Target 的做法用 Enum,但 Blackboard 上的 Enum Reference 不能留空,需要接受一个类似 _MAIN_LIGHT_SHADOWS_OFF 的占位条目。本章等价 Shader 的这几行就是 Blackboard 上这些 Keyword 生成出来的东西:

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
#pragma multi_compile _ _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile _ _FORWARD_PLUS
图 24-5同一个卡通 Shader 的两个分支(由等价手写 Shader 渲染)。左:打开 SHADERGRAPH_PREVIEW 时的替身分支,固定光向、没有阴影,相当于你在节点预览里看到的;右:场景中的真实结果,主光从右后方来,光向和阴影都来自 URP

预览分支给的值要“看起来合理”即可,它只影响编辑器里的预览球,不会进入打包后的 Shader。有些作者喜欢在预览分支里返回明显的颜色(如纯品红),提醒自己“这个节点在预览里是假的”,也是个好习惯。