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使用的压缩格式通常是BC6H(HDR)或BC1(LDR),具体取决于颜色格式。
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 { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } 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 Debugger:Unity的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模板,并做好性能预算。只有这样,全局光照才能从一个令人头疼的技术难题,真正转变为提升项目视觉品质和沉浸感的强大工具。