ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

URP光照贴图与GPU Instancing共存方案:静态与动态物体批量渲染优化实战

URP光照贴图与GPU Instancing共存方案:静态与动态物体批量渲染优化实战 1. 项目概述1.1 核心需求解析先说说这个标题的来历。我接了项目组一个比较头疼的活儿美术那边在场景里摆了大量重复的植被、路灯、石块还有一些会动的装饰物比如风扇叶片、巡逻的小车、飘动的旗帜。这些物体数量一多Draw Call直接就爆了帧率掉到没法看。当时项目用的渲染管线是URP场景里既有光照贴图烘焙的需求又需要做批量渲染优化两边一叠加问题就变成了一道组合题——怎么在URP管线里让光照贴图和GPU Instancing同时生效而且能同时处理好静态物体和动态物体。先说结论光照贴图和GPU Instancing本身不冲突真正让它们打架的是使用方式。光照贴图要配合实例化渲染关键在三个点——烘焙参数、材质球上的Lightmap属性传递、以及物体是静态还是动态带来的不同处理策略。这篇文章会把每个环节拆开来讲最后给出可以直接抄作业的配置方案。1.2 适合什么项目和人群如果你在做开放世界、城市街道、数字孪生、工业厂房这一类需要大量重复物体又希望效果好一点的场景这篇内容对你会很有帮助。具体来说适合下面几类人刚把项目从内置管线迁到URP发现批量渲染和光照方案都要改一遍的Unity开发者。用GPU Instancing做了大批量物体渲染但发现静态物体和动态物体的亮暗颜色对不上的朋友。烘焙了光照贴图之后开启Instancing导致部分物体丢失光照或者亮度异常正在排查问题的人。想搞清楚URP底下SRP Batcher、GPU Instancing、Static Batching到底怎么选怎么搭配的技术美术和图形程序员。无论你是Unity初学者还是有一定经验的开发者只要在实际项目里碰过类似问题这篇文章应该能帮你省下至少两天的排查时间。2. 光照贴图与GPU Instancing的设计思路2.1 为什么静态物体优先考虑光照贴图场景里的静态物体也就是完全不动的那些——地面、建筑外墙、大石头、固定路灯它们最适合用光照贴图。光照贴图的核心逻辑很简单把光照信息预先计算好烘焙到一张纹理上运行时直接采样完全不参与实时光照计算。这样做的好处显而易见——省掉了实时光源的逐像素计算省掉了阴影贴图的采样和滤波性能开销几乎可以忽略。URP管线下烘焙光照贴图的操作入口在Window Rendering Lighting切换到Scene选项卡勾选Baked Global Illumination然后设置Lightmapper推荐用GPU Lightmapper速度实测比CPU快好几倍再点Generate Lighting。整个烘焙过程会为场景生成Lightmap纹理和Light Probe数据。静态物体的Mesh Renderer采样光照贴图的过程也很透明Unity会在后台为每个静态物体生成Lightmap Index和Lightmap Scale Offset这两个值会写进Mesh Renderer的属性里运行时Shader通过这两个值去Lightmap纹理上取对应的区域。2.2 GPU Instancing解决的是绘制调用开销GPU Instancing的原理简单说就是一次Draw Call渲染多个相同物体的网格GPU自己循环处理每个实例的差异。它最适合大量重复网格加同一材质的场景——比如一千棵树、五百个路灯、两百根柱子。这些物体的顶点数据是一样的只是位置、旋转、缩放、颜色这些属性不同Unity把这些差异打包成Per-instance数据传给GPUShader里访问unity_InstanceID拿到每个实例自己的参数。但这里有一个关键点实例化要求所有实例在同一个Pass渲染使用同一个材质球实例。这意味着如果每个物体的材质球是独立的或者MaterialPropertyBlock差异太大实例化效果就会大打折扣。3. URP下的核心配置与实操步骤3.1 URP管线与光照贴图配置URP是Scriptable Render Pipeline的一种实现它对光照贴图的支持和内置管线有些细微差异。首先确认一下你用的URP版本我实测推荐用12.x以上的版本光照和实例化的兼容性稳定很多。项目里要做的第一件事是确认Asset里有没有正确创建URP Pipeline Asset。新建项目时选择URP模板会自动生成老项目迁移的话要手动在Project窗口右键Create Rendering URP Asset。然后把Graphics Settings里的Scriptable Render Pipeline Settings和Quality Settings里的Render Pipeline Asset都指向它。接着是光照贴图相关参数的设置。打开Lighting窗口在Scene选项卡里把Lightmapper选为GPU采样数建议从默认值先提高到128或256。光照贴图分辨率一般根据物体大小来定地墙面这类大面积用40-80的Texels Per Unit植物小物体可以降一点。还有一个容易被忽略的参数——Max Lightmap Size如果单张光照贴图的物体太多UV会被压得很小可以分多张贴图或者适当调大这个值。烘焙完成之后可以在Scene视图右上角切到Baked Lightmap模式预览效果。如果发现漏光或者阴影过渡不自然优先检查场景物体的Lightmap Static勾选状态——注意不是Mesh Renderer上的那个Static而是每个GameObject右上角那个Static下拉菜单里要勾选Contribute GI。3.2 GPU Instancing开启方式与条件开启GPU Instancing的方式很直接选中材质球在Surface Options面板下方找到GPU Instancing复选框打上勾就完事。但打勾只代表允许这个材质被实例化实际能不能命中实例化还要满足一系列条件多个物体的Mesh必须一致不然只能分批次绘制。一定要使用同一个材质球资源不能每个物体有自己的Material实例。如果要用MaterialPropertyBlock传个性化参数Block里的数据要符合Shader的Per-instable属性定义。物体不能同时被Static Batching合并StaticBatchingUtility.Combine处理过的大Mesh会破坏实例化判定。URP管线下还有一个特殊的点如果管线开启了SRP Batcher那么Shader要兼容SRP Batcher才能生效而GPU Instancing是不依赖SRP Batcher的。两者实际上可以共存Unity会优先走SRP Batcher的合批路径对不能合并的大批量实例物体再走实例化路径。实际项目中建议两个都开着各管一摊。4. 静态物体的光照贴图加实例化实战4.1 配置静态物体的Lightmap采样静态物体走光照贴图加实例化核心障碍在于每个实例的Lightmap UV不一样实例化Shader里要能正确读取每个实例的lightmapIndex和lightmapScaleOffset。Unity内置的规则是同一材质球下如果物体都是静态的且参与GI烘焙URP会在实例化路径里自动处理Lightmap属性。这一点在内置管线时代就有URP也保留了。但有个坑如果你的Shader是自定义的没有加入Lightmap采样相关的代码那么物体即使烘焙了光照贴图也不会显示正确光照。标准做法是使用URP自带的Lit Shader或者基于Lit做派生修改这样Lightmap支持是现成的。如果用了自定义Shader需要检查是否包含以下输入#ifdef LIGHTMAP_ON DECLARE_LIGHTMAP_OR_SH( staticLightmapUV, vertexSH, staticLightmapUV); #endif以及片元阶段对Lightmap纹理采样和乘加运算的代码。URP的Shader库中UnityInput.hlsl、Lighting.hlsl里都有现成实现不建议新手自己造轮子。4.2 静态实例的MaterialPropertyBlock处理静态物体多的情况下经常遇到需要给个别物体调色、调亮度的需求。如果美术希望同一堆石块里有些颜色偏深那么就需要用MaterialPropertyBlock来传Per-instance数据。这里要特别小心如果传了SetColor或SetFloat之类的不带Per-instance标记的属性线程会判定为不能实例化于是退回普通渲染性能直接打回原形。正确的做法是确保Shader里对应的属性用UnityPerMaterial声明并且在实例化输入结构体里添加UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor) UNITY_INSTANCING_BUFFER_END(Props)然后用UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor)去访问。这样在C#侧用MaterialPropertyBlock传这个属性实例化依然成立。另外要提醒一点MaterialPropertyBlock会缓存每个Renderer的修改如果场景切换或运行时大量修改属性但不清理Block会积攒很多脏数据导致内存上涨。建议用完的就clear。4.3 静态物体实例化后光照异常排查我遇到过的一个典型问题是大量静态石块开了GPU Instancing之后个别石块表面的光照呈现出细碎噪点或偏色。查了半天发现是烘焙光照贴图时这些石块跑到了不同的Lightmap Atlas里采样精度不一致导致颜色有偏差。解决方法是打开Lighting窗口的Lightmap Settings把Atlas大小调大或者让相关物体归入同一张Atlas。还有一个更省事的方式把物体拖得更近一些再重新烘焙让Unity把它们排到同一张贴图里。这个操作我建议在项目初期就规划好场景大规模铺开之后再处理就很费劲了。另外光照贴图的压缩格式也可能引发问题。URP下如果Lightmap用了高压缩格式远处的静态物体表面会出现明显的色块和渐变断层。推荐在URP Asset的Lightmaps选项卡里把Format设为RGBA16或RGBA8保障采样精度。5. 动态物体的光照贴图与实例化处理5.1 动态物体为什么不能直接用静态Lightmap动态物体——会动的人、车、风扇、飘带——它们的核心问题是没有固定的UV位置没法像静态物体那样直接烘焙Lightmap。如果强行给动态物体加Lightmap Static那么物体移动后光照信息不更新看起来就像“贴着一张错误的光照图在跑”穿帮非常明显。所以动态物体在URP里的常规方案是走Light Probe。在场景中放置Light Probe Group把探针摆放到关键位置烘焙的时候Unity会记录这些位置的光照信息。运行时动态物体通过空间插值获取所在位置的光照状态。5.2 动态物体实例化时Light Probe的Bug动态物体的实例化和Light Probe之间有个坑必须提前说当大量动态物体用GPU Instancing渲染时默认的Light Probe采样是按顶点/物体中心做的可是在实例化路径里Light Probes的Index和Blend权重是作为Per-instance属性传入的。URP的实现实际上支持这个操作但前提是材质球开启了GPU Instancing且Shader里包含LightProbe相关代码。我实际测试过URP自带Lit Shader可以支持实例化加Light Probe但如果你用的是Simple Lit或者自定义Shader很可能没有完整实现。这种情况下动态物体就会出现“直接黑或亮得不自然”的现象。解决思路有两个升级Shader为完整的Lit确认Input里包含LIGHTMAP_ON、DYNAMICLIGHTMAP_ON等关键字处理。使用Unity 2023.2以上版本的URP它对光照探针实例化的支持更完善。5.3 动态光照贴图方案运行时更新如果项目里动态物体也想有类似光照贴图的效果——不是Light Probe那种粗粒度的探针插值而是精细的纹理光照——也可以用“动态光照贴图”的思路。这个方案在数字孪生和展览展示项目中很常用。做法把烘焙出来的Lightmap纹理在运行时更新到一个RenderTexture上动态物体的MaterialPropertyBlock里动态设置lightmapIndex和lightmapScaleOffset让Shader把动态物体当成静态物体采样光照贴图。这么做的问题是更新频繁会带来GPU开销所以一般只在物体不频繁变形的场景用比如转台展示、缓慢移动的展车。实操时要注意动态物体必须有一个第三套UV用于Lightmap采样这个UV可以在建模时展好也可以在导入设置里通过Generate Lightmap UVs自动生成。然后在Shader里加入float2 dynamicLightmapUV input.staticLightmapUV.xy * dynamicLightmap_ST.xy dynamicLightmap_ST.zw;但说实话这个方案的工程量并不小如果不是硬性需求我更推荐直接用Light Probes加实时光照的混合方案。6. 常见问题与排查技巧实录6.1 实例化后物体光照变暗或者丢失这个问题出现频率很高。现象是开启GPU Instancing后部分物体的光照亮度明显下降或者完全变成黑色。排查步骤第一步检查材质球Surface Options里GPU Instancing是否勾选。第二步确认Shader的关键字。第三步检查有没有用MaterialPropertyBlock传过非实例化属性比如SetFloat(_Glossiness, value)而这个属性在Shader里不是Per-instance。第四步检查Lightmap的采样纹理是否被正确绑定到GPU上如果烘焙后场景没保存Lightmap数据或者Lighting场景文件被改名也会出现类似问题。6.2 动态物体实例化后Light Probe插值失效这个问题多发于Unity 2021到2022的URP版本。表现是动态物体在场景中移动时亮度变化很生硬或者始终使用同一组光照值。原因在于URP的Lit Shader在处理实例化与Light Probe时顶点着色器里根据unity_ProbeVolumeParams做了分支但实例化路径下这个分支的代码没有正确执行。解决办法是把URP升级到12.1以上版本或者手动在Shader里把LightProbe相关的实例化宏补上。你要排查的话可以在Frame Debugger里查看Draw Call的Shader Properties确认有没有PerInstance的unity_SHAr、unity_SHAHB之类的数据上传。没有的话就是Shader层面的兼容问题。6.3 Lightmap Atlas过于拥挤导致精度不足当场景物体特别多时Unity会把不同物体的UV排布在少数几张Atlas上每张图的大小固定。物体太多太密时每个物体分到的纹理区域很小光照贴图看起来糊成一团。处理方案在Lighting窗口把Lightmap Resolution调高或者在Lightmap Parameters资源里调整Texel Count。这里注意一个平衡——提高分辨率会让烘焙时间暴涨最坏的情况下我试过一张4096的图烘了四十分钟。所以建议先小图验证调好效果再开全分辨率。另一个技巧是给核心物体单独分配一张Lightmap Atlas在Lightmap Parameters里设置Scale In Lightmap的值。比如英雄级道具设成2普通石块设成0.5让Unity自动调节分配比例。6.4 URP下SRP Batcher与GPU Instancing的兼容性陷阱有些朋友开了SRP Batcher后发现GPU Instancing好像不生效了就慌乱地关掉SRP Batcher。实际上这两个机制并不冲突。SRP Batcher处理的是不同材质之间Shader状态的切换开销GPU Instancing处理的是同材质多个物体的合批。开启SRP Batcher后只有那些真正能实例化的大批量物体会走Instancing路径别的物体走SRP Batcher的合批。两者并存收益最大。不过要注意如果Shader里使用了#pragma multi_compile_instancing但缺少合适的声明可能出现实例化物体被SRP Batcher“吞掉”导致渲染错误。排查方法是看Frame Debugger里Draw Call的类型Instanced的Draw Call会被标记为“Instanced”如果显示为“SRP Batch”说明没有命中实例化。6.5 常见问题速查表现象可能原因解决动作开启Instancing后物体变暗Shader缺少Lightmap采样关键字换用URP Lit或补齐宏动态物体亮度更新生硬Light Probe插值未走实例化路径升级URP或修改Shader代码个别物体光照精度差Lightmap Atlas分配太密调Scale In Lightmap或增大Atlas大量同网格物体Draw Call仍很高未勾选GPU Instancing或材质球不统一统一材质球并勾选Instancing静态物体烘焙后没效果物体没有标记Contribute GI在Static下拉菜单中勾选GI7. 项目实测案例复盘7.1 数据对比与优化收益我们测试场景做的是一个社区公园的局部区域——五十棵行道树、十六个路灯、围栏、长椅、凉亭外加移动的游客角色和巡逻小车。初始状态是纯实时光照加RawImage烘焙Draw Call大概是2300多帧率在低端设备上只有22帧。优化过程第一步把所有非移动物体标记Static烘焙光照贴图关闭大部分Realtime Light只保留一个Realtime Directional Light用于动态物体照明。这时Draw Call降到900多主要原因是大面积静态物体不再占实时光照资源。第二步给重复的树、路灯、长椅材质开启GPU InstancingDraw Call直接降到400多。第三步给动态的游客、巡逻车放置Light Probe Group确保动态物体能够和静态环境融合。动态物体材质确认GPU Instancing开启。最终结果Draw Call约430低端设备帧率稳定跑到48帧中高端设备跑满60帧没有问题。需要说明的是这套方案并不是把所有问题都解决了动态物体还是占了一些实时光照开销但整体已经足够平滑。7.2 动态物体的实例化实现细节动态物体的实例化实现上有一点心得不要把动态物体的移动逻辑写在Update里直接改transform这会打断合批。更好的做法是使用ECS或者至少用BatchRendererGroup不过ECS对很多项目团队来说改动成本偏高。折中做法是把动态物体挂在一个空的父节点下通过移动父节点来实现整体运动子物体的Transform保持不变。这样可以和静态物体一样保持Transform的不变从而更好地命中实例化。这个技巧在表演场景中特别有用——比如一排路灯整体升起或者一组花车同时出发。还要注意动画效果和实例化是天生不友好的。如果物体骨骼动画非常复杂Unity的SkinMeshRenderer本身不支持GPU Instancing除非你走GPU Skinning方案。实际项目中遇到这种情况我会建议要么拆分静态与动态部分比如飘带是动态的单独处理要么用Shader动画代替骨骼动画——比如做飘动的旗帜用顶点动画加时间偏移完全可以模拟。7.3 这个方案能不能扩展到更大的场景有人问过我这套光照贴图加实例化的组合能不能直接扩展到整个开放世界地图。经验是不建议直接套用。开放世界地图面临流式加载、多区块边缘拼接、光照贴图张数管理等一系列问题。GPU Instancing在大场景中依然是利器但光照贴图要考虑的是“动态加载和卸载”而不是简单的一次性烘焙完毕。更合理的做法是分区块烘焙每个区块独立管理Lightmap和Light Probe并在运行时通过Addressables或Streaming加载。对动态物体来说开放世界的Light Probe摆放也要精细很多。探针太稀疏移动时光照跳变明显探针太密集烘焙时间和内存都会上升。建议结合场景复杂度分层放置探针关键通道加密开阔平坦区域稀疏一些。8. 小结与避坑心得8.1 配置优先级建议对一般项目我建议按下面的优先级来配置和优化静态物体优先。先把所有不会动的物体合理标记Static烘焙一张好的光照贴图这一步收益最大。重复物体用GPU Instancing。数量多且网格顶点简单的物体最适合。注意材质球统一。动态物体用Light Probe加实例化。保证动态物体不会被光照突变“闪瞎”。自定义Shader要特别谨慎。要保证Lightmap、Light Probe、Instancing三者的Shader宏都齐全。8.2 最后一个实用的心得最后分享一个排查顺序的窍门遇到光照和实例化结合的问题先用Frame Debugger观察Draw Call的类型和数量把问题缩窄到“是根本没合批”还是“合批了但光照不对”。前者是批处理条件没满足后者是Shader或数据传递问题。这个顺序能帮你少走很多弯路。我见过太多人一看到光照不对就疯狂调Shader结果发现只是静态标记没勾选或者材质球数量没有统一。反过来也有人一看到Draw Call高无脑开GPU Instancing结果没注意Shader根本不支持白忙活。排查顺序对了很多问题十分钟内就能定位。
返回列表