
1. 项目概述为什么我们需要在Shader Graph里搞自定义光照如果你用Unity的URP管线做项目有一段时间了大概率会遇到一个瓶颈Shader Graph自带的那些光照节点比如PBR Master或者Simple Lit用起来是方便但想实现一些“特别”的光照效果时就感觉手脚被捆住了。比如你想做一个风格化的角色需要边缘光Rim Light的强度不仅受视角影响还和角色当前的生命值挂钩或者你想做一个科幻场景让物体的高光反射带有扫描线动画又或者你想精确控制场景中每个点光源对某个特定材质的衰减方式。这些需求用标准光照模型去调参数往往事倍功半甚至根本做不到。这就是“自定义光照”Custom Lighting的价值所在。它把光照计算的“黑盒”打开让你能亲手编写光照与材质表面交互的每一个步骤。而URP Shader Graph的Custom Lighting功能则是把这个高级能力用可视化的节点图方式呈现出来大大降低了门槛。你不用从头去写一个完整的HLSL着色器只需要在Shader Graph里像搭积木一样组合出你理想中的光照公式。最近社区里讨论热度很高的“体积光”和“高亮”Outline/Highlight效果其底层实现往往都绕不开对光照数据的精细操控。体积光需要处理光线在介质中的散射和衰减高亮效果则需要独立于主光照之外叠加一层特殊的边缘计算。这些都是标准PBR流程难以优雅实现的恰恰是Custom Lighting的用武之地。所以这篇指南的目标很明确我们不只教你打开Custom Function节点写几行代码而是要带你彻底理解URP Shader Graph自定义光照的整套工作流。从获取光源数据、编写光照模型函数到处理阴影、环境光遮蔽AO最后整合成一个专业、高效且可复用的Shader Graph。无论你是想实现独特的艺术风格还是解决特定的渲染技术难题这套方法都能给你提供坚实的支撑。2. 核心思路拆解Shader Graph自定义光照的底层逻辑在深入节点之前我们必须先搞清楚Shader Graph的Custom Lighting和手写Shader的Custom Lighting在思路上有何异同。理解了这一点你才能避免“照猫画虎”真正灵活运用。2.1 URP光照数据获取的“标准路径”在手写URP着色器中如Unity官方文档所示我们通过#include Lighting.hlsl然后调用GetMainLight()、GetAdditionalLight()等函数来获取光源数据。这些函数返回一个Light结构体里面包含了方向、颜色、距离衰减、阴影衰减等关键信息。在Shader Graph里我们无法直接#include头文件但Shader Graph的底层本身就是生成HLSL代码。因此自定义光照的核心就是通过一个“自定义函数”Custom Function节点把我们想用的HLSL代码“注入”到Shader Graph生成的代码中去。这个自定义函数节点就是连接Shader Graph可视化界面与底层HLSL代码世界的桥梁。你需要告诉它函数名是什么、输入参数有哪些、输出结果是什么以及最重要的——函数体即HLSL代码的内容。2.2 从“表面着色器”到“片元着色器”的思维转变使用Shader Graph的PBR Master节点时你是在一个“表面着色器”Surface Shader的简化抽象层里工作。你提供颜色、法线、金属度、光滑度等参数引擎帮你完成复杂的BRDF计算。而当你启用Custom Lighting时你实际上接管了最终颜色输出的计算。PBR Master节点会退居二线它提供的输入如Base Color, Normal依然可用但最终的光照混合、颜色输出将由你连接的Custom Lighting节点完全决定。这要求你的思维要从“配置参数”转变为“编写算法”。你需要思考对于当前这个像素片元所有光源对它贡献的总和应该如何计算漫反射怎么算高光怎么算环境光怎么加这些都需要你明确定义。2.3 模块化设计分离数据获取与光照计算一个健壮的自定义光照实现应该遵循模块化原则。我建议将其拆分为至少两个核心部分光照数据获取模块这是一个或多个Custom Function节点专门负责调用URP的GetMainLight、GetAdditionalLightsCount、GetAdditionalLight等函数将获取到的光源数据方向、颜色、衰减整理并输出给后续节点。这个模块通常比较固定可以做成一个可复用的子图Sub Graph。光照模型计算模块这是真正体现你创意的地方。它接收来自上一个模块的光源数据以及来自材质属性的表面信息颜色、法线、粗糙度等然后根据你定义的数学模型如Lambert, Phong, Blinn-Phong或更风格化的公式计算出最终的光照颜色。这个模块也可以做成子图方便在不同材质间切换不同的光照模型。这种分离使得你的Shader Graph结构清晰调试方便。当光照效果不对时你可以先检查数据获取模块输出的光源颜色和方向是否正确再排查光照模型的计算逻辑。3. 实战第一步构建可复用的光照数据获取子图理论说再多不如动手做。我们首先来创建一个最核心的部件一个能获取主光源和所有额外点光源数据的子图。3.1 创建Custom Function节点与定义接口在Shader Graph中新建一个子图Sub Graph命名为“URP_LightingData”。在空白处右键选择Create Node-Custom Function。选中这个Custom Function节点在Graph Inspector面板中进行关键配置Name: 可以命名为GetMainLightData。Type 选择String。这是最关键的一步意味着我们将直接输入HLSL代码。Source 在下方的大文本框中粘贴我们的HLSL代码。这里我们先实现获取主光源void GetMainLightData_float(float3 WorldPos, out float3 Direction, out float3 Color, out float DistanceAtten, out float ShadowAtten) { #ifdef SHADERGRAPH_PREVIEW // 在Shader Graph预览窗口或未烘焙光照时使用一个默认的顶光源 Direction float3(0, 1, 0); Color float3(1, 1, 1); DistanceAtten 1.0; ShadowAtten 1.0; #else // 包含URP光照库 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // 获取主光源 Light mainLight GetMainLight(); Direction mainLight.direction; Color mainLight.color; DistanceAtten mainLight.distanceAttenuation; ShadowAtten mainLight.shadowAttenuation; #endif }注意这里有一个极其重要的技巧——使用#ifdef SHADERGRAPH_PREVIEW。Shader Graph的预览窗口Preview通常没有完整的光照环境直接调用GetMainLight()可能会失败或返回错误数据导致你的节点图在预览中一片黑。这个预处理指令判断是否在预览中如果是则返回一套默认的、可视化的光照数据让你在编辑时就能看到大致效果。Inputs 添加一个输入端口类型Vector 3名称WorldPos。这是片元的世界空间坐标用于某些光照计算虽然GetMainLight不需要但为后续扩展预留。Outputs 添加四个输出端口Direction(Vector 3)Color(Vector 3)DistanceAtten(Float)ShadowAtten(Float)配置好后这个节点就有了一个输入和四个输出。将其输出端口连接到子图的输出节点上并命好名如Main Light Direction。现在你就有了一个能在任何Shader Graph中调用的、安全可靠的主光源数据获取器。3.2 扩展获取所有额外光源并循环计算只有一个主光源显然不够。我们需要处理场景中的多个点光源、聚光灯。这需要用到循环。在Shader Graph中实现循环需要一点技巧。我们再创建一个Custom Function节点命名为CalculateAdditionalLights。void CalculateAdditionalLights_float(float3 WorldPos, float3 WorldNormal, float3 BaseColor, out float3 TotalAddLight) { TotalAddLight float3(0, 0, 0); #ifdef SHADERGRAPH_PREVIEW // 预览模式下简单模拟一个点光源 float3 previewLightPos float3(0, 2, 0); float3 lightDir normalize(previewLightPos - WorldPos); float diff max(dot(WorldNormal, lightDir), 0.0); TotalAddLight BaseColor * diff * float3(0.5, 0.5, 0.8); // 给点蓝色调 #else #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl int pixelLightCount GetAdditionalLightsCount(); for (int i 0; i pixelLightCount; i) { Light light GetAdditionalLight(i, WorldPos); // 简单的兰伯特漫反射计算 float3 lightDir light.direction; float diff max(dot(WorldNormal, lightDir), 0.0); float3 diffuse light.color * diff * light.distanceAttenuation * light.shadowAttenuation; TotalAddLight diffuse; } #endif }这个函数接收世界坐标、法线、基础颜色遍历所有额外光源为每个光源计算一个简单的漫反射贡献并累加到TotalAddLight输出中。实操心得在Shader Graph的Custom Function里写循环是可行的但要注意性能。GetAdditionalLightsCount()返回的是影响当前像素的光源数量由URP的每像素光照Per-Pixel Lighting设置控制。在移动平台或性能敏感场景务必在URP Asset中限制“Per Object Light”的数量并避免在循环内进行过于复杂的计算。3.3 整合成完整的照明子图现在我们有了获取主光源和额外光源的两个函数。在一个主Shader Graph中你可以这样连接使用Position节点的World输出作为两个Custom Function的WorldPos输入。使用Normal节点的World输出作为CalculateAdditionalLights的WorldNormal输入。将GetMainLightData输出的Color、Direction、ShadowAtten与你想要的表面法线通常是Normal Vector节点输出一起输入到你自己的光照模型计算节点比如另一个Custom Function实现Phong模型。将CalculateAdditionalLights输出的TotalAddLight与主光源的计算结果相加。最后不要忘记加上环境光。可以使用Ambient节点或者通过Custom Function调用SampleSH9函数来获取球谐光照数据。这样一个支持多光源、包含阴影衰减的基础光照框架就搭建好了。你可以将这个复杂的连线网络再次封装成一个名为“StandardCustomLighting”的子图输入法线、颜色等直接输出最终光照颜色极大提升复用性。4. 实现专业级光照效果从兰伯特到风格化有了可靠的数据管道我们就可以专注于光照模型本身了。这才是体现“专业级”和“自定义”的地方。4.1 基础模型复现兰伯特与布林-冯首先我们实现一个经典的兰伯特漫反射模型作为验证。创建一个名为LambertLighting的Custom Function。void LambertLighting_float(float3 LightColor, float3 LightDir, float3 Normal, out float3 Out) { float NdotL max(dot(normalize(Normal), normalize(LightDir)), 0.0); Out LightColor * NdotL; }将主光源的Color、Direction和物体World Normal输入输出就是兰伯特漫反射结果。简单但足以验证整个链路是否通畅。接下来实现一个更常用的布林-冯高光模型。这需要视角方向。void BlinnPhongLighting_float(float3 LightColor, float3 LightDir, float3 Normal, float3 ViewDir, float SpecularPower, float SpecularIntensity, out float3 DiffuseOut, out float3 SpecularOut) { // 漫反射 (Lambert) float NdotL max(dot(normalize(Normal), normalize(LightDir)), 0.0); DiffuseOut LightColor * NdotL; // 高光 (Blinn-Phong) float3 halfVec normalize(normalize(LightDir) normalize(ViewDir)); float NdotH max(dot(normalize(Normal), halfVec), 0.0); SpecularOut LightColor * pow(NdotH, SpecularPower) * SpecularIntensity; }这里我们输出了漫反射和高光两个部分。在Shader Graph中你可以用View Direction节点获得视角方向。SpecularPower控制高光点的大小幂次越高高光点越小越锐利SpecularIntensity控制高光强度。4.2 进阶效果边缘光与菲涅尔效应边缘光Rim Light是增强物体轮廓、营造氛围的利器。其核心是菲涅尔效应视线与表面法线夹角越大即看边缘时效果越强。void RimLight_float(float3 Normal, float3 ViewDir, float RimPower, float RimIntensity, out float Rim) { float fresnel 1.0 - saturate(dot(normalize(Normal), normalize(ViewDir))); Rim pow(fresnel, RimPower) * RimIntensity; }将这个Rim因子乘以一个边缘光颜色然后加到最终光照结果上。你可以用它来做卡通渲染的描边或者科幻物体的能量外溢效果。4.3 结合热词实现“高亮”与“体积光”思路高亮Outline/Highlight一种常见的实现方式是基于法线或深度的屏幕后处理。但在自定义光照层面我们可以实现一种“内发光”式的高亮。例如在计算光照时额外判断表面某属性如一张遮罩贴图的R通道如果超过阈值则让该区域的自发光强度剧增或者使用一个完全独立于主光照的、饱和度高且不受阴影影响的二次光照计算。这需要你在光照计算逻辑中加入分支判断。体积光Volumetric Light这在Shader Graph中通常通过后处理或粒子系统实现但自定义光照可以辅助。例如你可以让物体在特定视角和光照方向下模拟光线穿过半透明介质的效果。这需要更复杂的光照模型可能涉及对光源方向与视角方向的点积进行重映射并叠加噪声纹理来模拟光线中的尘埃散射。你可以创建一个Custom Function输入WorldPos,LightDir,ViewDir输出一个Scatter因子用来调制最终颜色或透明度模拟光线在物体内部的散射。5. 阴影与环境光遮蔽的集成没有阴影的光照是没有灵魂的。在自定义光照中集成阴影是迈向专业级的关键一步。5.1 正确应用阴影衰减幸运的是我们在第一步获取光源数据时GetMainLight和GetAdditionalLight返回的Light结构体里已经包含了shadowAttenuation阴影衰减这个字段。它的值在0到1之间0表示完全在阴影中1表示完全被照亮。因此在你的光照计算中无论是漫反射还是高光在乘以光源颜色light.color之后一定要再乘以light.shadowAttenuation。对于主光源我们已经从GetMainLightData节点获取了这个值。对于额外光源在循环体内light.shadowAttenuation也是可用的。// 在额外光源循环内 float3 diffuse light.color * NdotL * light.distanceAttenuation * light.shadowAttenuation; // 注意这里乘了阴影衰减注意事项确保你的Shader Graph的Master节点设置中Shadow Caster Pass是启用的。否则物体无法投射阴影。同时接受阴影需要物体材质包含正确的渲染标签Tags和深度写入这些通常在创建URP Lit Shader Graph时已默认配置好但如果你是从头创建Unlit Graph则需要手动添加必要的Pass和设置。5.2 集成屏幕空间环境光遮蔽环境光遮蔽能极大地增强场景的立体感和真实感。URP提供了屏幕空间环境光遮蔽功能。要在自定义光照中使用它我们需要获取当前像素的SSAO数据。创建一个名为GetSSAO的Custom Function节点。void GetSSAO_float(float2 ScreenUV, out float SSAO) { SSAO 1.0; // 默认值 #ifndef SHADERGRAPH_PREVIEW #if defined(_SCREEN_SPACE_OCCLUSION) // 包含SSAO库 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOcclusionTexture.hlsl // 采样SSAO纹理 float2 uv ScreenUV; #if UNITY_UV_STARTS_AT_TOP uv.y 1 - uv.y; #endif SSAO SampleAmbientOcclusion(uv); #endif #endif }这个函数首先检查是否启用了_SCREEN_SPACE_OCCLUSION关键字在URP Renderer Asset中启用SSAO后会自动定义。然后它采样SSAO纹理。ScreenUV输入需要是归一化的屏幕空间坐标0到1可以通过Screen Position节点的Normalized输出获得。在最终光照计算中将漫反射和环境光部分乘以这个SSAO因子。通常高光部分不受AO影响。float3 finalColor (diffuse ambient) * ssaoFactor specular;6. 性能优化与常见问题排查自定义光照功能强大但滥用也会带来性能问题。以下是一些关键的优化点和排错技巧。6.1 性能优化要点光源数量管理这是最大的性能杀手。在URP Asset的Lighting设置中严格控制Per Object Lights的数量。在Custom Function的循环中GetAdditionalLightsCount()返回的就是这个值。对于移动端建议设置为2-4个。分支与预览宏充分利用#ifdef SHADERGRAPH_PREVIEW和#if defined(_SCREEN_SPACE_OCCLUSION)这类编译分支。预览代码要轻量只在编辑器下编译运行时功能只在对应功能启用时编译避免不必要的计算和纹理采样。避免复杂计算在循环内光照模型的计算应尽可能高效。如果模型复杂考虑将其部分计算结果如基于法线的某种映射提前到循环外计算。使用子图复用将光照数据获取、通用光照模型如兰伯特打包成子图。这不仅能保持主图整洁Shader Graph在生成代码时也可能对重复使用的子图进行更好的优化。6.2 常见问题排查表问题现象可能原因排查步骤Shader Graph预览一片黑1. 未处理SHADERGRAPH_PREVIEW宏。2. 光源数据获取失败Direction或Color为(0,0,0)。1. 检查Custom Function代码确保在#ifdef SHADERGRAPH_PREVIEW分支下提供了合理的默认值。2. 在预览中尝试输出Direction或Color到一个颜色端口看是否为非零值。物体不接收阴影1. Master节点未启用Shadow Caster Pass。2. 在光照计算中未乘以shadowAttenuation。3. 光源未开启投射阴影。1. 检查Graph Inspector中Master节点的“Shadow Caster”选项。2. 检查光照计算代码确保light.shadowAttenuation被乘入。3. 在Unity场景中检查光源的“Cast Shadows”属性。额外光源没效果1.GetAdditionalLightsCount()返回0。2. 光源不在物体的每对象光照限制内。3. 循环内计算错误结果未累加。1. 在函数中输出pixelLightCount到颜色看是否大于0。2. 检查URP Asset的Per Object Lights设置并确保点/聚光灯在物体附近且强度足够。3. 检查TotalAddLight变量是否在循环前正确初始化为0并在循环内正确。自定义高光异常闪烁或过亮1. 视角方向或半角向量未归一化。2. 高光幂次(SpecularPower)过低。3. 高光计算在阴影处未正确衰减。1. 确保在计算点积前对ViewDir、HalfVec等向量进行normalize。2. 适当提高SpecularPower如32、64、128。3. 确保高光计算也乘以了light.shadowAttenuation根据艺术需求决定。SSAO不起作用1. URP Renderer Asset中未启用SSAO。2. 屏幕UV坐标获取错误。3. 未定义_SCREEN_SPACE_OCCLUSION宏。1. 在URP Renderer Asset的Renderer Features中添加并配置“Screen Space Ambient Occlusion”。2. 使用Screen Position节点的Normalized输出并注意UV的Y方向用宏处理。3. 确保材质使用的Shader变体包含SSAO。有时需要手动在材质上启用一个开关或检查Shader的编译变体。6.3 调试技巧当效果不符合预期时不要盲目修改代码。学会使用Shader Graph的调试功能分步输出将复杂的Custom Function拆开。例如先单独输出Light.Color看颜色对不对再输出NdotL看漫反射因子范围是否在[0,1]最后再输出最终结果。使用颜色可视化将任何中间值向量或标量连接到Master节点的颜色输入上可以直观地看到它的空间分布。这是调试Shader最强大的手段之一。检查生成的代码在Shader Graph的Graph Inspector底部点击“Show Generated Code”可以查看最终生成的HLSL代码。检查你的Custom Function代码是否被正确插入宏定义是否生效。这对于排查复杂的编译问题非常有用。掌握URP Shader Graph的自定义光照相当于为你打开了图形渲染的一扇新大门。它不再是简单的参数调节而是真正的创作。从获取数据到设计模型从集成阴影到优化性能这套流程需要反复练习和调试。我个人的经验是从一个最简单的、只有主光源的兰伯特模型开始确保每一步都正确然后再逐步添加额外光源、高光、特效和优化。每成功实现一个效果你对光照和着色的理解就会加深一层。最终你将能够游刃有余地用可视化工具创造出独一无二的视觉风格解决那些标准着色器无法应对的渲染挑战。