案例 24.2在 Graph 中读取主光与阴影进阶
目标:写一个 File 模式的 Custom Function,输入世界坐标,输出主光方向、颜色、距离衰减和阴影衰减,可以同时用于 Lit 和 Unlit 图。
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)。
阴影坐标在片元阶段逐像素计算: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_SCREEN | Boolean ×3 | 主光阴影(单级联 / 级联 / 屏幕空间) |
_SHADOWS_SOFT | Boolean,Stages = Fragment | 软阴影(每灯档位) |
_ADDITIONAL_LIGHTS | Boolean | 附加光逐像素 |
_ADDITIONAL_LIGHT_SHADOWS | Boolean,Stages = Fragment | 附加光阴影 |
_FORWARD_PLUS | Boolean | Forward+ 路径的分簇光照 |
用 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

SHADERGRAPH_PREVIEW 时的替身分支,固定光向、没有阴影,相当于你在节点预览里看到的;右:场景中的真实结果,主光从右后方来,光向和阴影都来自 URP预览分支给的值要“看起来合理”即可,它只影响编辑器里的预览球,不会进入打包后的 Shader。有些作者喜欢在预览分支里返回明显的颜色(如纯品红),提醒自己“这个节点在预览里是假的”,也是个好习惯。