Unity URP全局光照(GI)完全指南:Lightmap采样与实时GI的Shader集成 1. 项目概述为什么URP全局光照集成是项目成败的关键如果你在Unity URP项目里做过稍微复杂一点的场景尤其是那种室内外结合、有大量静态物件和动态角色的那你一定遇到过这个经典难题为什么我的角色在场景里跑动时身上的光影和周围环境格格不入像是P上去的或者为什么烘焙好的场景在移动端上跑起来帧率直接掉到没法看这些问题十有八九都出在全局光照的集成上更具体地说是Lightmap光照贴图和实时GI光照探针、反射探针在Shader里的采样与混合没做对。这个标题“Unity URP 全局光照 (GI) 完全指南 Lightmap 采样与实时 GI光照探针、反射探针的 Shader 集成”点出的正是Unity URP渲染管线里从美术效果到程序实现最核心、也最容易出岔子的环节。它不是一个简单的功能开关而是一套从场景光照设置、到烘焙流程、再到Shader代码编写的完整工作流。很多团队的美术和程序在这里是脱节的美术抱怨程序写的Shader不支持高级光照效果程序则头疼于美术烘焙出来的Lightmap又大又慢效果还不对。最终结果就是项目在视觉效果和运行性能之间反复拉扯上线日期一拖再拖。我自己带过好几个从Built-in管线迁移到URP或者直接用URP开发的中大型项目踩遍了这里面的坑。今天我就以一个一线开发者的视角把这套流程掰开揉碎了讲清楚。我们不仅要搞明白URP里全局光照的几种模式Baked, Mixed, Realtime到底该怎么选更要深入到Shader层面手把手教你如何正确地编写Shader来采样Lightmap、解码光照探针数据、处理反射探针的插值最终让静态场景的光影和动态物体的光照无缝融合。无论你是Shader新手还是想优化现有项目光照系统的老手这篇指南都能给你一套可直接落地的解决方案和避坑地图。2. URP全局光照核心概念与配置实战在动手写Shader之前我们必须把URP里全局光照的几个核心概念和它们在编辑器里的配置逻辑彻底理清。很多问题不是出在代码上而是出在最开始的设置上。2.1 Baked、Mixed、Realtime三种光照模式的本质区别与选型策略URP延续了Unity的全局光照分类但具体实现和性能开销上有其特点。这三种模式不是并列的选项而是针对不同物件和场景需求的组合策略。Baked GI烘焙全局光照这是性能最优解也是静态场景光照的基石。它的原理是把场景中静态物体Static接受的光照包括直接光、间接光、阴影预先计算好保存成一张或多张纹理也就是Lightmap光照贴图。运行时Shader只需要像读取普通贴图一样去采样这张Lightmap就能获得完整的光照信息GPU计算开销极低。在URP的Lighting窗口Window - Rendering - Lighting的Scene页签下勾选“Baked Global Illumination”就启用了此模式。它的代价是Lightmap会占用显存和内存只能影响标记为Static的物体场景光照如太阳角度一旦变化Lightmap就失效了需要重新烘焙。Realtime GI实时全局光照这是效果最动态的解但性能开销巨大在URP的移动端或高性能要求的项目中基本已被弃用。它试图实时计算光线在场景中的反弹。在URP中通常不直接使用传统的Enlighten Realtime GI而是通过光照探针Light Probes和反射探针Reflection Probes来模拟动态物体的间接光照和反射配合屏幕空间全局光照SSGI或光线追踪如果硬件支持等后处理技术来补充。Mixed Lighting混合光照这是URP项目中最常用、也最复杂的模式它试图在Baked和Realtime之间取得平衡。在Lighting窗口的Mixed Lighting部分你需要勾选“Baked Global Illumination”并且选择一种混合模式。URP主要支持以下两种子模式Baked Indirect这是最推荐的模式。静态物体之间的间接光照光线反弹被烘焙到Lightmap中而直接光照来自方向光等光源和阴影仍然是实时计算的。这意味着你可以实时旋转太阳方向改变光照颜色静态物体也能正确接收实时的直接光和投射实时阴影同时拥有高质量的烘焙间接光。这对于有昼夜循环的场景至关重要。Shadowmask一种更高级的模式它把阴影也分成了两部分。静态物体投射到静态物体上的阴影静态阴影被烘焙到一张叫Shadowmask的贴图里而静态物体投射到动态物体上以及动态物体投射的所有阴影仍然是实时的。这能进一步降低实时阴影的计算量但需要Shader支持并且管理更复杂。实操心得对于绝大多数URP项目我的建议是将主要静态环境地形、建筑、大型摆设设为Static使用Baked Indirect混合光照模式。这样既能保证静态场景拥有丰富的间接光照效果又能让动态角色和光源互动。除非有极致的静态场景性能需求且光照不变否则不要使用纯Baked模式。2.2 Lighting窗口关键参数详解与烘焙优化理解了模式我们来看看URP Lighting窗口里那些让人眼花缭乱的参数到底该怎么设置。错误的设置会导致烘焙时间巨长、Lightmap尺寸爆炸或者效果失真。Lightmapper选择烘焙器。Progressive GPU如果硬件支持是目前最快的选择它利用GPU进行烘焙交互预览非常方便。Progressive CPU作为备选。Enlighten已经过时不推荐使用。Lightmap Resolution光照贴图分辨率单位Texels per unit。这是最重要的质量/性能权衡参数。值越高Lightmap越清晰细节越多但贴图尺寸也越大。通常对于大型地形可能用5-10对于室内墙面和重要道具可以用30-60对于微小细节可以更高。切忌全局使用一个很高的分辨率。应该利用物体的“Scale In Lightmap”属性在Mesh Renderer组件中进行微调对重要物体提高缩放值对不重要物体降低缩放值甚至设为0来不让它产生Lightmap。Lightmap Padding光照贴图间距。防止不同物体的Lightmap像素在压缩时 bleeding渗色。通常保持默认即可如果发现Lightmap上物体边缘有光晕可以适当增大此值。Lightmap Size定义生成的单张Lightmap纹理的最大尺寸如1024, 2048, 4096。Unity会尝试将多个物体的光照信息打包到若干张这个尺寸的图集中。较小的尺寸意味着更多的图集可能会增加Draw Call过大的尺寸可能造成浪费。2048是一个比较通用的起点。Compressed是否压缩Lightmap。务必勾选。压缩能极大减少显存占用虽然会带来轻微的质量损失但在移动平台上收益巨大。Unity使用的压缩格式通常是BC6HHDR或BC1LDR具体取决于颜色格式。Ambient Occlusion环境光遮蔽。在烘焙设置中启用可以让物体缝隙、角落产生更自然的阴影增强体积感。这里的参数控制AO的强度和半径建议根据场景尺度调整。Directional Mode方向模式。这是Lightmap的“灵魂”设置之一。Non-Directional无方向。Lightmap只存储光照的颜色和强度RGB。这是最省内存的格式但物体表面会失去高光细节看起来比较“平”。Directional定向。Lightmap会额外存储一张“方向图”Directional Map记录主要入射光的方向编码在RGB通道中。这使得Shader在采样Lightmap时能够结合法线贴图Normal Map计算出正确的高光Specular让表面细节如砖墙、木板纹理的凹凸感在光照下更真实。如果你的场景使用了法线贴图强烈建议使用Directional模式。代价是Lightmap所需内存翻倍因为多了一张方向图。踩坑记录曾经在一个项目中美术抱怨所有带法线贴图的材质在烘焙后都变“平”了失去了质感。排查了半天最后发现就是Lightmapper的Directional Mode被错误地设成了Non-Directional。改成Directional后重新烘焙所有表面细节立刻“活”了过来。这个参数对最终视觉效果影响巨大但很容易被忽略。3. Shader集成核心解码与采样光照数据配置好了场景并完成了烘焙接下来就是最关键的环节在自定义的URP Shader中如何获取并使用这些光照数据。URP提供了一套相对Built-in管线更简洁、但需要手动集成的函数库。3.1 采样烘焙光照贴图Lightmap的标准流程要在Shader中使用Lightmap首先需要在Shader属性中声明它并在顶点/片元着色器中传递UV坐标。第一步声明属性和变量在Shader的Properties块和CGPROGRAM中的变量声明部分通常不需要手动声明Lightmap纹理。因为Unity会在烘焙后自动将Lightmap纹理绑定到特定的纹理单元如unity_Lightmap并通过内置变量提供UV变换数据。但是我们需要在顶点着色器的输入结构体和输出结构体中准备好Lightmap的UV坐标。// 在 appdata 结构体中顶点着色器输入 struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 texcoord : TEXCOORD0; float2 lightmapUV : TEXCOORD1; // 第二套UV用于Lightmap }; // 在 v2f 结构体中顶点着色器输出片元着色器输入 struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 positionWS : TEXCOORD2; DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 3); // URP宏声明光照贴图或球谐光照数据 };这里的关键是DECLARE_LIGHTMAP_OR_SH这个URP内置宏。它会根据情况在lightmapUV中存储Lightmap的UV坐标或者在vertexSH中存储顶点球谐光照数据当物体没有Lightmap但处于光照探针影响下时。第二步在顶点着色器中转换UV在顶点着色器中我们需要使用另一个URP内置宏OUTPUT_LIGHTMAP_UV来处理Lightmap UV的变换。Unity为了将多个物体的Lightmap打包到图集会对原始UV进行缩放和偏移这个宏帮我们完成了这个计算。Varyings vert(Attributes input) { Varyings output; // ... 计算 positionCS, normalWS, positionWS 等 ... output.uv TRANSFORM_TEX(input.texcoord, _BaseMap); // 主纹理UV // 处理光照贴图UV OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); // 如果还需要处理光照探针的球谐数据可以调用 OUTPUT_SH OUTPUT_SH(output.normalWS.xyz, output.vertexSH); return output; }第三步在片元着色器中采样和解码这是核心步骤。我们需要采样Lightmap纹理并将其从HDR编码解码成实际的光照颜色。half4 frag(Varyings input) : SV_Target { // ... 采样法线贴图、获取表面颜色等基础操作 ... // 1. 采样光照贴图 half4 bakedLightmapUV SAMPLE_TEXTURE2D(unity_Lightmap, samplerunity_Lightmap, input.lightmapUV.xy); // 2. 解码光照贴图颜色从HDR编码解码 half3 bakedGI DecodeLightmap(bakedLightmapUV); // 如果使用了Directional模式还需要解码方向信息来计算高光 #ifdef DIRLIGHTMAP_COMBINED // 采样方向图 half4 bakedDirTex SAMPLE_TEXTURE2D(unity_LightmapInd, samplerunity_Lightmap, input.lightmapUV.xy); // 解码出光照方向归一化向量 float3 bakedDir DecodeDirectionalLightmap(bakedDirTex.rgb, input.normalWS); // 使用bakedDir参与高光计算例如替换Blinn-Phong中的lightDir #else // 非Directional模式高光计算可能使用主光源方向或其他方式 float3 bakedDir input.normalWS; // 简单处理 #endif // 将bakedGI间接光与实时直接光计算结果混合 half3 mainLightColor ... // 计算主光源颜色和衰减 half3 finalColor (mainLightColor bakedGI) * surfaceColor; return half4(finalColor, 1.0); }DecodeLightmap和DecodeDirectionalLightmap是URP Shader Library中提供的函数它们正确处理了Lightmap的HDR编码和方向解码务必使用它们而不是直接使用采样的颜色值。3.2 集成实时全局光照光照探针与反射探针动态物体非Static无法使用预烘焙的Lightmap但它们需要融入烘焙好的光照环境。这时就要依靠光照探针Light Probes和反射探针Reflection Probes。光照探针Light Probes在场景空间中放置一系列采样点探针组烘焙时Unity会计算这些点上的光照信息编码为低阶球谐函数系数。运行时动态物体根据其位置在附近的几个探针间插值获取其所在位置的间接光照。在Shader中集成光照探针相对简单因为URP的UniversalFragmentPBR或UniversalFragmentBlinnPhong等核心光照函数已经内置了支持。对于自定义光照模型你需要获取插值后的球谐光照数据// 在片元着色器中 float3 positionWS input.positionWS; float3 normalWS normalize(input.normalWS); // 采样光照探针的球谐光照数据 half3 bakedGI SampleSH(normalWS); // 这是环境漫反射 // 或者更精确地使用包含位置信息的版本如果探针组启用了“Use Proxy Volume” // half3 bakedGI SampleSHPosition(positionWS, normalWS);SampleSH函数返回的bakedGI就是动态物体从光照探针获取的间接漫反射光。你需要将它和你计算的直接光结果相加。反射探针Reflection Probes用于为物体提供基于环境的镜面反射。和光照探针类似它也在场景中放置并烘焙动态物体会采样最近的反射探针的立方体贴图Cubemap。在URP Shader中集成反射探针确保材质球上启用了“Reflection Probes”选项。在片元着色器中使用GlossyEnvironmentReflection函数或相关宏。// 获取反射向量 float3 viewDirWS normalize(_WorldSpaceCameraPos - positionWS); float3 reflectVector reflect(-viewDirWS, normalWS); // 采样反射探针 half3 reflectionColor GlossyEnvironmentReflection(reflectVector, positionWS, _Smoothness, 1.0); // 将reflectionColor根据材质的金属度等参数混合到最终颜色中URP的PBR光照函数内部已经处理了反射探针的采样与混合。如果你写的是自定义的非PBR Shader可能需要手动实现这部分逻辑。注意事项光照探针和反射探针的密度和放置位置至关重要。在走廊、门口、室内外交界等光照变化剧烈的区域必须放置足够密集的探针否则动态物体移动时其身上的光照会发生突兀的“跳跃”。一个实用的技巧是先自动生成一层均匀的探针然后在关键区域手动添加或移动探针确保覆盖所有动态物体的活动路径。4. 混合光照模式下的Shader适配与性能陷阱当场景使用Baked Indirect或Shadowmask混合光照模式时Shader需要做额外的处理来正确响应实时光源和阴影。4.1 适配Baked Indirect混合光照在Baked Indirect模式下Shader需要区分直接光和间接光。间接光来自Lightmap或光照探针bakedGI而直接光需要实时计算。URP的主光源计算函数如GetMainLight会返回正确的直接光颜色和方向。你的Shader逻辑应该是// 计算实时直接光照包括阴影 Light mainLight GetMainLight(shadowCoord); // shadowCoord需要通过屏幕空间阴影映射或Shadowmask获得 half3 directLighting mainLight.color * mainLight.distanceAttenuation * mainLight.shadowAttenuation; // 间接光照来自烘焙GI half3 indirectLighting bakedGI; // 合并光照 half3 totalLighting (directLighting indirectLighting) * surfaceAlbedo;关键点在于GetMainLight函数中传入的shadowCoord。在混合光照下静态物体接收来自静态物体的阴影可能是烘焙的这需要通过SHADOWMAP_TYPE宏和相关的阴影采样函数来正确处理。URP的着色器变体会根据场景的光照设置自动处理这些细节但如果你写的是非常自定义的Shader可能需要查阅Shadow.hlsl库来手动实现。4.2 理解与集成Shadowmask模式Shadowmask模式更进一步它将静态阴影也烘焙到了一张额外的贴图Shadowmask中。这需要Shader支持额外的纹理采样和判断逻辑。启用Shadowmask在Project Settings - Quality - Shadowmask中将模式从“Distance Shadowmask”改为“Shadowmask”。在Lighting窗口中为参与烘焙的静态光源选择“Shadowmask”模式而不是“Baked Indirect”。Shader中的Shadowmask采样URP内置的Lit Shader已经支持。在自定义Shader中你需要声明并采样unity_ShadowMask纹理。在光照计算中判断当前片段是否应该使用烘焙的阴影Shadowmask还是实时阴影。这通常通过比较片段的世界坐标与光源的烘焙范围来实现或者使用URP提供的宏SampleShadowmask。// 这是一个简化示例实际应使用URP内置宏 half4 shadowMask SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, input.lightmapUV.xy); // 根据shadowMask.r和光源的shadowmask通道选择衰减值 half shadowAttenuation ... // 混合实时阴影和烘焙阴影Shadowmask能显著提升性能因为它减少了需要计算的实时阴影但增加了Lightmap的复杂度多了一张Shadowmask贴图和Shader的复杂度。除非你的场景有极大量的静态物体和复杂阴影且性能瓶颈明确在阴影计算上否则Baked Indirect模式通常是更简单、更可控的选择。4.3 性能优化关键点与常见陷阱全局光照集成不当是性能杀手。以下是一些必须关注的优化点Lightmap尺寸与数量这是内存占用的大头。定期在Lighting窗口的“Lightmaps”页签查看生成的Lightmap图集数量和尺寸。目标是使用尽可能少、尺寸尽可能合理的图集覆盖所有静态物体。通过精心调整每个物体的“Scale In Lightmap”属性可以大幅优化打包效率。光照探针数量每个光照探针都会增加烘焙时间和运行时插值计算量。避免在整个场景均匀放置高密度探针。使用探针组Light Probe Group的编辑工具在光照变化平缓的区域稀疏放置在变化剧烈的区域集中放置。反射探针分辨率与数量反射立方体贴图分辨率如128, 256, 512对内存和带宽影响很大。背景环境可以使用低分辨率128而近处的重要反射物体可能需要256。同样减少不必要的探针数量并合理设置其影响范围Box Size避免重叠覆盖。Shader变体爆炸如果你的Shader使用了大量的#ifdef来支持不同的光照模式如DIRLIGHTMAP_COMBINED,SHADOWMASK_ON,LIGHTMAP_ON等会导致生成的Shader变体数量成倍增长增加编译时间和包体大小。务必在Shader的编辑界面如VSCode中的ShaderLab部分使用#pragma multi_compile或#pragma shader_feature来精确控制需要的变体剔除项目用不到的模式。实时光源数量即使在混合光照下实时光源尤其是Pixel Light的数量依然需要严格控制。URP的渲染器设置中可以配置最大每对象光源数。过多的实时光源会迫使Shader进入逐像素光照路径并大幅增加Draw Call。5. 实战从零构建一个支持完整GI的URP自定义Shader理论说得再多不如动手写一个。下面我将带领你一步步创建一个简化的、但支持Lightmap、光照探针和反射探针的自定义URP Unlit Shader为了简化我们先不涉及复杂的PBR直接光计算。这个Shader将展示数据流的核心过程。5.1 Shader框架与属性定义首先我们创建一个新的Shader文件并搭建基础框架。注意URP Shader需要包含特定的HLSL文件和使用UniversalRenderPipeline标签。Shader Custom/MyGIUnlitShader { Properties { _BaseMap (Base Texture, 2D) white {} _BaseColor (Base Color, Color) (1, 1, 1, 1) _Smoothness (Smoothness, Range(0, 1)) 0.5 [Toggle(_LIGHTMAP_ON)] _LightmapToggle (Enable Lightmap, Float) 1.0 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { Name ForwardLit Tags { LightModeUniversalForward } HLSLPROGRAM #pragma vertex vert #pragma fragment frag // 关键声明我们需要的着色器变体 #pragma multi_compile _ _LIGHTMAP_ON // 控制光照贴图开关 #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE // 主光源阴影 #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS // 额外光源阴影 #pragma multi_compile _ _SHADOWS_SOFT // 软阴影 #pragma multi_compile_fog // 雾效 // 包含URP核心库 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl // 定义属性变量 TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST; // 纹理的缩放和偏移 half4 _BaseColor; half _Smoothness; // 顶点着色器输入输出结构体使用之前定义的Attributes和Varyings struct Attributes { ... }; // 如前文定义包含positionOS, normalOS, texcoord, lightmapUV struct Varyings { ... }; // 如前文定义包含positionCS, uv, normalWS, positionWS, lightmapUV, vertexSH // 顶点着色器 Varyings vert(Attributes input) { ... } // 如前文实现计算空间变换和UV // 片元着色器 half4 frag(Varyings input) : SV_Target { // 1. 采样基础纹理和颜色 half4 baseColor SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; half3 surfaceAlbedo baseColor.rgb; // 2. 初始化光照贡献 half3 totalLighting half3(0, 0, 0); half3 indirectDiffuse half3(0, 0, 0); // 3. 采样间接光照烘焙GI #ifdef _LIGHTMAP_ON // 有光照贴图采样并解码Lightmap half4 bakedLightmapUV SAMPLE_TEXTURE2D(unity_Lightmap, samplerunity_Lightmap, input.lightmapUV); indirectDiffuse DecodeLightmap(bakedLightmapUV); #else // 无光照贴图采样光照探针的球谐光照 indirectDiffuse SampleSH(input.normalWS); #endif // 4. 计算直接光照这里简化只考虑主光源 // 获取主光源数据包含阴影 Light mainLight GetMainLight(); // 计算简单的兰伯特漫反射 half NdotL saturate(dot(input.normalWS, mainLight.direction)); half3 directDiffuse mainLight.color * NdotL * mainLight.distanceAttenuation * mainLight.shadowAttenuation; // 5. 组合光照直接光 间接光 totalLighting (directDiffuse indirectDiffuse) * surfaceAlbedo; // 6. 可选添加基于反射探针的镜面反射简化版 // 这需要更复杂的环境BRDF计算此处仅示意 // float3 viewDir GetWorldSpaceNormalizeViewDir(input.positionWS); // float3 reflectVector reflect(-viewDir, input.normalWS); // half3 reflection GlossyEnvironmentReflection(reflectVector, input.positionWS, _Smoothness, 1.0); // totalLighting reflection * surfaceAlbedo; // 简化混合 // 7. 应用雾效 half fogFactor ComputeFogFactor(input.positionCS.z); totalLighting MixFog(totalLighting, fogFactor); return half4(totalLighting, baseColor.a); } ENDHLSL } // 可以在此添加ShadowCaster Pass等 } FallBack Universal Render Pipeline/Lit }这个Shader是一个高度简化的示例但它清晰地展示了流程通过变体编译指令#pragma multi_compile _ _LIGHTMAP_ON让Shader能同时兼容有Lightmap和没有Lightmap依赖光照探针的物体。在片元着色器中通过#ifdef _LIGHTMAP_ON分支分别用DecodeLightmap和SampleSH来获取间接光照。使用URP的GetMainLight()函数获取经过阴影处理的主光源数据。将直接光、间接光与表面颜色结合。5.2 调试与验证确保数据流正确写完Shader后如何验证它是否正确采样了全局光照数据呢这里有几个实用的调试技巧可视化Lightmap UV在Shader的片元着色器中直接返回input.lightmapUV.xy作为颜色。如果UV正确你应该看到物体表面显示出一张非重复的、被拉伸的“棋盘格”图案因为Lightmap UV是唯一且展开的。如果全黑或全白说明UV没有正确传递或采样。可视化Baked GI直接返回indirectDiffuse解码后的Lightmap或SH颜色。关闭场景中的所有实时光源静态物体应该仍然被照亮且光照效果和烘焙预览窗口一致。动态物体则显示为从光照探针插值得到的颜色。分离直接光和间接光在编辑器中你可以通过临时修改Shader将directDiffuse和indirectDiffuse分别输出到颜色通道如R和G来直观地看到两者的贡献比例检查混合是否正确。使用Frame DebuggerUnity的Frame Debugger是神器。你可以逐帧、逐个Draw Call查看渲染状态。检查你的材质球在渲染时是否正确绑定了unity_Lightmap纹理以及相关的Shader变体如LIGHTMAP_ON是否被启用。5.3 进阶与URP Lit Shader的对比与自定义扩展我们上面写的是一个简化版的Unlit Shader。URP内置的LitShader要复杂得多它完整实现了基于物理的渲染PBR并集成了所有光照特性。当你需要更复杂的效果时最好的起点往往是复制并修改URP的Lit Shader源码位于Packages/com.unity.render-pipelines.universal/Shaders/Lit.shader。通过阅读Lit Shader你可以学习到URP团队是如何更严谨地处理以下问题的光照模型完整的直接光PBR计算漫反射BRDF 镜面反射BRDF。阴影处理对Shadowmask、屏幕空间阴影等模式的完整支持通过GetShadowCoord和MainLightRealtimeShadow等函数。反射探针集成在GlobalIllumination函数中如何将反射探针采样与BRDF的镜面反射部分结合。细节层级LOD如何为不同性能平台生成不同的Shader变体。自定义扩展时我的建议是“渐进式修改”。不要试图从头重写一个完整的PBR Shader。而是基于Lit Shader通过添加或修改特定的#pragma指令、在关键函数如UniversalFragmentPBR前后插入你的自定义计算来逐步实现你想要的效果。例如如果你想增加一个自定义的视差效果可以在采样基础纹理之前在SurfaceData结构体填充阶段修改UV坐标。6. 项目迁移与平台适配中的疑难杂症很多团队是在项目中期才决定升级URP或优化光照的这时会遇到从Built-in管线迁移以及多平台尤其是移动端适配的特定问题。6.1 从Built-in管线迁移GI相关Shader如果你有旧的Built-in管线Shader需要迁移到URP在全局光照部分会遇到几个主要变化内置变量名改变这是最常见的错误。unity_Lightmap和unity_LightmapInd在URP中仍然存在但采样方式建议使用SAMPLE_TEXTURE2D宏和对应的sampler state。DecodeLightmap函数仍然可用但需要确保包含了正确的HLSL头文件Lighting.hlsl。ShadeSH9(用于球谐光照) 在URP中被SampleSH或相关函数替代。UNITY_LIGHTMODEL_AMBIENT不再推荐使用。环境光应来自Skybox或Volume系统并通过SampleSH获取。光照计算函数重构Built-in管线中你可能直接访问_LightColor0等变量在URP中必须通过GetMainLight()、GetAdditionalLights()等函数来获取光源数据这些函数封装了阴影和衰减计算。Shadowmap采样方式Built-in的SHADOW_ATTENUATION宏在URP中不再适用。需要使用GetMainLight返回的shadowAttenuation或者使用SampleShadowmap相关函数。迁移步骤建议第一步使用Unity官方的“Render Pipeline Converter”工具进行基础属性转换。第二步手动重写光照计算部分。参考URP提供的简单示例Shader如Simple Lit或本文的示例替换掉旧的光照变量和函数调用。第三步重点测试Lightmap和光照探针的效果。确保静态物体在烘焙后显示正确动态物体在探针间移动时光照平滑过渡。6.2 移动端Android/iOS性能优化专项移动平台对内存和带宽极其敏感URP全局光照的配置需要更加小心。Lightmap压缩与格式确保Lightmap启用压缩Compressed。对于不支持ASTC的旧设备ETC2是安全的格式但需要考虑Alpha通道Directional模式需要。可以考虑将Directional Lightmap的Directional信息存储在一张单独的、更低精度的贴图中。Lightmap分辨率砍半这是最直接有效的手段。将场景中大部分物体的Lightmap Resolution降低到原来的一半视觉损失可能很小但内存占用和纹理采样带宽直接降为1/4。减少光照探针数量移动端场景的探针数量要严格控制。可以适当增大探针的间距尤其是在开阔地带。避免使用“Use Proxy Volume”除非绝对必要因为它需要更多探针来定义体积。简化反射探针移动端尽量少用反射探针如果必须用将分辨率设为最低如64或128并禁用“Box Projection”除非有强烈的变形需求。考虑使用天空盒的反射作为全局替代。Shader变体剥离使用#pragma skip_variants或在Graphics Settings中设置Shader Stripping彻底移除移动端用不到的光照模式变体例如可以完全移除SHADOWMASK_ON和DIRLIGHTMAP_COMBINED的变体如果项目确定不用。慎用实时阴影在移动端即使是Baked Indirect模式下的实时直接光阴影开销也很大。考虑对次要光源禁用阴影或者使用更廉价的阴影技术如预计算的静态物体阴影贴图非Lightmap。6.3 常见问题排查清单当你遇到光照问题时可以按以下清单逐一排查问题现象可能原因排查步骤静态物体全黑/无光照1. 物体未标记为Static。2. Lightmap未烘焙或烘焙失败。3. Shader未启用LIGHTMAP_ON变体或未正确采样。1. 检查Inspector右上角Static复选框。2. 打开Lighting窗口查看Lightmaps页签是否有贴图点击“Generate Lighting”重新烘焙。3. 使用Frame Debugger检查该Draw Call使用的Shader变体并调试Shader输出Lightmap颜色。动态物体与场景光照不融合1. 场景中没有放置光照探针。2. 光照探针密度不足或位置不佳。3. Shader中未调用SampleSH或相关函数。1. 创建Light Probe Group并放置探针然后烘焙。2. 在动态物体活动路径上增加探针密度。3. 调试Shader输出SampleSH的结果。物体表面高光异常过亮/过暗1. 使用Non-Directional Lightmap但材质有法线贴图。2. Directional Lightmap方向信息解码错误。1. 将Lightmapper的Directional Mode改为Directional并重新烘焙。2. 检查Shader中DecodeDirectionalLightmap函数的输入法线是否正确。烘焙后阴影边缘有光晕Lightmap Padding值太小纹理过滤导致颜色渗色。在Lighting设置中增大Lightmap Padding值如从2调到4或8重新烘焙。移动设备上帧率低下1. Lightmap尺寸过大、数量过多。2. 光照/反射探针过多。3. 实时光源或阴影过多。1. 使用Asset Bundle Analyzer分析纹理内存优化Lightmap分辨率。2. 减少探针数量降低反射探针分辨率。3. 在URP Asset中限制每对象最大光源数减少使用实时阴影的光源。角色在特定区域光照“跳变”该区域光照探针覆盖不连续插值突变。在问题区域手动添加或调整光照探针的位置确保动态物体移动路径被平滑的探针网络覆盖。光照系统的调试往往需要美术和程序协同。养成使用Frame Debugger、RenderDoc等图形调试工具的习惯能让你快速定位问题是出在数据Lightmap、探针上还是出在Shader计算逻辑上。最后我想分享一个深刻的体会在URP中构建一个健壮、高效且美观的全局光照系统其核心不在于掌握多少炫酷的Shader技巧而在于对工作流和数据流的深刻理解。从场景物体的Static标记策略到Lighting窗口里每一个参数的含义再到Shader中那一行行采样和解码代码这是一个环环相扣的链条。任何一个环节的疏忽都会导致最终结果的偏差。我的建议是在项目早期就建立一套规范美术同学需要了解Lightmap分辨率、探针放置的原则程序同学则需要提供稳定、可调试的Shader模板并做好性能预算。只有这样全局光照才能从一个令人头疼的技术难题真正转变为提升项目视觉品质和沉浸感的强大工具。