
接手《暗黑王朝》的光影时我做的第一件事不是调参而是把之前那版“看着还行”的实时光照全部关掉重来。这是一款哥特暗黑风格的动作游戏整个视觉基调要求冷峻、压抑、高对比单纯靠拉低曝光根本做不出层次。暗黑场景里画面一旦没有可靠的全局光照墙面和地面就会变成一团死黑角色站在里面就像是贴图上去的。我们最终定下的方案很明确以光影烘焙作为全局光照的主体配合动态阴影和反射探针做局部补充。整套流程从资产预处理到烘焙参数调整再到漏光排查前后迭代了三个多月。这篇就把我在这个项目里真正跑通的技术实践完整拆开讲每个环节都会解释为什么这么选而不是只丢一堆参数。1. 为什么要用烘焙而不是实时全局光照选型背后的成本账1.1 实时GI在暗黑题材下的短板很多团队现在一提全局光照就下意识想上实时方案觉得烘焙是老路子、不灵活。但《暗黑王朝》里我们最核心的场景是教堂、地下墓穴、城堡走廊这些封闭空间的可见范围有限玩家移动路径相对固定反而是烘焙最有发挥空间的地方。实时GI最大的问题是两点一是性能预算被大量光追采样和反射更新吃掉而同屏还要叠加角色、技能特效、粒子移动端和主机平台的帧时间根本扛不住二是暗黑风格下美术主观控制光的强度、颜色和衰减范围非常关键实时GI会产生大量不可控的反弹光今天调好的暗部换个天气或者加个特效整体氛围就飘了。美术团队对“可控性”的要求最终压过了对“全动态”的执念。1.2 三种全局光照方案的对比结论我们把当时可选的三条路拉了一张表反复比过方案画面效果静态场景消耗动态物体匹配迭代效率适用阶段实时GI 屏幕空间GI好但暗部表现难稳定高好中场景探索Demo全烘焙Lightmap稳定暗部层次可精调极低需要探针辅助中每次改光要重烤正式关卡烘焙烘焙 实时阴影混合兼顾质感与动态低较好中最终项目落地我们选的是第三项主光源保持实时方向光只投射动态阴影静态场景的间接光、反弹和AO全部烘焙进Lightmap。这样角色和敌人可以随时加入实时阴影而场景的明暗关系完全由烘焙控制不会被特效和动态光打乱。1.3 跨平台发布的统一考量游戏要同时跑PC和主机帧率目标还不一样如果依赖实时GI每个平台都要单独调一遍性能参数。烘焙方案拷过去之后光影表现是像素级一致的差别只体现在Lightmap贴图分辨率和压缩格式上。这点在项目后期尤其重要因为光效、场景、关卡同步联调时减少一个不可控变量就少一排Bug。2. 烘焙前的资产预处理光照UV和场景静态标记才是成败关键2.1 光照UV不能依赖自动生成《暗黑王朝》的地编最初从建模那边接到的模型很多只有一层纹理UV他们习惯性在Unity里勾选“Generate Lightmap UVs”就让烘焙跑了。结果第一次全场景烘焙出来墙面上是一道道斜向的接缝黑线地板大面积偏色。问题根源是自动生成的光照UV把网格三角形投影到二维平面上时对角折叠和拉伸严重相邻三角形在Lightmap上隔着很远插值后自然漏缝。正确的做法是在DCC工具里手展第二套UV或者至少在导入设置里把自动生成的权重调对。我们用Blender统一加了第二套UV遵循几条硬规则每个模型的光照UV岛不能横跨模型的主要几何转折面所有UV岛的缩放尽量一致防止同一面墙上出现密度不均匀的纹理颗粒感支撑结构、柱子和墙面装饰单独给岛不要为了省空间硬塞在一张团里UV岛之间保留至少4像素的空白边距防止相邻岛渗色。如果你有大量老模型来不及回炉可以在Unity导入设置里打开Generate Lightmap UV并调整这几项Angle Distortion控制UV岛贴合表面法线的程度我们设20。Area Distortion控制面积拉伸容忍度我们设15。Hard Angle超过88度会被强制切断成新岛避免大弧度墙面被强行拉平。Pack Margin海岛间距设置4宁可多占一点贴图分辨率也不要渗色。2.2 用Editor脚本批量修正次级UV项目中期美术管线稳定后我写了一个批量检查脚本放在Tools菜单下每次从版本库拉新模型后先跑一遍using UnityEditor; using UnityEngine; public static class LightmapUVFixer { [MenuItem(Tools/Lighting/Validate Secondary UV)] static void ValidateSecondaryUV() { MeshFilter[] filters Object.FindObjectsByTypeMeshFilter(FindObjectsSortMode.None); foreach (var filter in filters) { Mesh mesh filter.sharedMesh; if (mesh null) continue; if (mesh.uv2 null || mesh.uv2.Length ! mesh.vertexCount) { Debug.LogWarning($模型缺少uv2: {filter.name}, filter); } } } }别小看这种检查它至少帮我们把一半的烘焙异常在进Lighting窗口之前就拦住了。另一段脚本用于批量设置导入参数让新模型默认开启次级UV并采用统一打包参数AssetImporter importer AssetImporter.GetAtPath(path); if (importer is ModelImporter mi) { mi.generateSecondaryUV true; mi.secondaryUVAngleDistortion 20f; mi.secondaryUVAreaDistortion 15f; mi.secondaryUVHardAngle 88f; mi.secondaryUVPackMargin 4f; importer.SaveAndReimport(); }这种脚本的价值不在于炫技而在于把“每个人手动调的参数”统一成“管线默认值”。否则三个人导同一套模型烘焙结果都不一样。2.3 静态标记和物体拆分规则场景里所有参与烘焙的物体必须勾选Contribute GI但这只是开始。我们遇到过一个很奇怪的现象一个大殿地面中心出现一块圆形的亮斑排查了半天发现是地板下面藏着一张不可见的碰撞体网格被误设成了静态且勾选了Contribute GI还把一张白色贴图给了Emissive通道。从那以后我们规定只参与碰撞的碰撞体一律不加MeshRenderer纯装饰性不可见物体直接移出烘焙层场景中所有参与烘焙的静态物体单独放在一个Layer方便统一排除和调试超过十万个顶点的巨型Mesh要拆件否则Lightmap分辨率会被一张大图稀释光细节全丢。3. 烘焙参数怎么调才不出“暗黑灰”采样、反弹和分辨率的选择3.1 一句话讲清Progressive Lightmapper的参数逻辑烘焙本质上是在场景里发射大量光线记录它们落到物体表面后的能量分布。Lightmapper里的采样数就是每个像素上发射光线的数量。采样越高噪点越少但烘焙时间按倍数增加。我们最终在《暗黑王朝》里采用的参数组合如下参数数值说明Direct Samples64直接光采样动态光源少64足够Indirect Samples256间接光采样这个值决定暗部噪点Bounces2反弹次数封闭空间2次足够Filtering ModeAdvanced高斯滤波半径0.5Lightmap Resolution4 texels/unit起根据物件体积浮动Lightmap Padding4防止UV岛间渗色Ambient Occlusion开启强度0.8半径1.5米CompressionHDR保留暗部层次有个反常识的点暗黑风格场景的反弹次数不需要设很高。反而是在只有两次反弹的情况下光线在角落和柱基附近会产生清晰的衰减这种衰减恰好形成了暗黑质感。如果把Bounces拉满到4次暗部会被填得平平整整整个场景看起来就像被磨皮了一样灰蒙蒙的一点压迫感都没有。3.2 暗黑场景为什么一烘焙就变“灰”我们第一版完整烘焙出来美术总监看了一眼直接说“像阴天拍的电影没有暗黑感”。问题出在环境光照强度上。默认情况下Unity的环境光来自天空盒暗黑场景即便用了深色天空盒烘焙时还是会有一定强度的天光进入封闭空间间接光把本该黑的角落都抬亮了。我在Lighting环境里做了两处调整Environment Lighting Source选择Color用一个接近深蓝黑色的环境色Intensity压到0.2Environment Reflections关闭或者把Reflection Intensity压到0.3反射探针单独控制。这样墙面在无直接光区域会自然黑下去只有靠近火把、吊灯、符文光源的地方才有温暖的光晕扩散对比一下子出来了。3.3 贴图分辨率密度管理满场景一个分辨率会出问题大头像只用4 texels/unit导致糊小物件用8又浪费。我们按物件角色分了三档主角交互物、可破坏物、门框8 texels/unit中景结构、柱子、雕像4 texels/unit远景、大型基座、山体2 texels/unit。用Unity的Lightmap Viewer工具可以实时查看密度颜色图绿色代表合适红蓝代表过密或过疏。实际操作中我会定期把所有Prefab的密度拉一张表检查防止新加模型继承了错误的默认值。4. 让静态烘焙“活”起来反射探针和光照探针的配合方案4.1 反射探针才是金属质感的灵魂暗黑场景里大量使用锈蚀的盔甲、金属武器、潮湿石板这些材质本身的BaseColor很暗如果没有反射它们看起来就是黑色剪影。烘焙Lightmap只记录漫反射金属的高光和镜面反射需要靠反射探针补上。我们在主战场、走廊交汇处、BOSS房各放了一颗Baked反射探针Cubemap分辨率设为128遇到大镜面大厅会单独再加256分辨率的探针。反射探针的摆放要避开一个坑不要放在场景正中间然后指望它覆盖整个房间。因为烘焙探针在单一采样点记录环境放在房间中央会把四面的窗户光全部混合进去金属表面反射出来的形状和实际结构对不上。我们尽量把探针放在玩家视角最常停留的位置比如入口处向内看的方向这样金属高光反射出的墙面轮廓与真实场景更接近。4.2 光照探针的明暗插值控制场景中动态物件比如敌人、碎尸、可拾取武器不会接收烘焙Lightmap它们的漫反射光照来自Light Probe Group。探针摆放密度决定了角色移动时的明暗变化是否自然。我们最初以3米间距铺满整个房间结果角色走过一段连续被柱子遮挡的区域时探针之间的光线插值产生了严重闪烁。后来总结出的经验是在明暗变化剧烈的区域比如门框两侧、火把周围、柱子阴影边界探针间距要加密到1米而在平坦开阔的区域5米间距完全够。关键是不要把探针放进静态物体内部否则角色一靠近探针会直接采样到墙内的黑暗角色瞬间被压黑。我用代码批量剔除过穿模探针RaycastHit hit; if (Physics.Raycast(probePosition, Vector3.down, out hit, 0.3f)) { Debug.LogWarning($探针与地面相交: {hit.collider.name}); }这个检查在每次布点后跑一遍能及时发现探针被地面或墙体吞掉的情况。4.3 自定义反射颜色修正暗黑氛围烘焙完成后的反射探针默认会忠实反射场景颜色但暗黑场景整体很黑反射出来也全是黑的金属就变成死黑。我们后期给反射探针加了颜色修正把Reflection Intensity调到0.35同时在高光反射最强的武器材质上叠加一层深蓝灰色的环境反射贴图用来模拟夜晚天光。这样金属表面会有一个微弱的冷色高光点既不会抢眼又能让玩家看清武器的轮廓。5. 动态角色和静态烘焙怎么“握手”混合光照模式下的衔接细节5.1 Shadowmask模式解决静态阴影与动态阴影冲突如果整个场景只用烘焙Lightmap角色身上没有实时阴影敌人走过来就像踩在幻影上。我们采用Distance Shadowmask模式远近光影由烘焙决定近距离阴影由实时光源投影。这个模式下主平行光保持实时投射但同时把Shadow Distance控制在25米25米以外的阴影自动过渡到烘焙好的Shadowmask视觉上无缝衔接性能压力也大幅下降。混合模式最容易出现的问题是漏阴影角色站在烘焙阴影区域中时如果实时阴影没跟上会看到角色半边身体是亮的。解决思路是把烘焙Shadowmask的暗部范围做宽一点也就是第3节提到的AO强度开到0.8人为加深静态场景的接触阴影动态角色踏入这些区域时更容易被整体压暗实时阴影的误差就不明显了。5.2 主角光照之外的接触阴影补充我强烈建议在成品阶段开启Contact Shadows这对暗黑风格是质变级的效果。角色踩在地板上时脚面和地面之间会有一圈细碎的接触阴影墙角会有一条精确的黑色分界线这种细节让物体看起来真正“坐”在场景里而不是悬浮在Lightmap上。代价是GPU开销所以只对主摄像机的主方向光启用范围控制在2米阴影贴图分辨率保持2048。5.3 动态光源的烘焙匹配实验《暗黑王朝》里有很多破坏场景后掉落的燃烧物它们本身带有动态点光源。我们做过一轮测试想用动态光去重新照亮静态场景但结论是动态光扫过的墙面和柱子与烘焙结果之间存在明显色差动态点光偏暖而烘焙暗部偏冷两种光源一叠加画面立刻脏了。最后统一方案是动态火把只负责照亮角色场景的明暗永远交给烘焙视觉一致性比局部真实感更重要。6. 烘焙实战中踩过的坑从漏光到噪点的完整排查链路6.1 墙面与地面交界处的黑线问题这个Bug曾经困扰了我们一周。墙面和地面交界处总会渲染出一条细细的黑线无论把Lightmap分辨率调多高都消不掉。后来我把材质换成Debug视图才看到问题出在Secondary UV上墙根底部的UV岛和地面UV岛在Lightmap纹理上挨得太近只有2像素的Margin采样时边界像素被周围三角形插值污染形成了一条黑色接缝。修复方案是把Pack Margin从2提高到4同时手动调整墙根UV岛位置让它们离地面UV至少隔开8像素。这里也解释了一个现象越小的UV岛越容易出接缝因为同样的Margin在小岛上占比更大。6.2 大厅地面莫名其妙的亮斑前文提过那个圆形亮斑完整排查链路是这样的第一步怀疑Lightmap分辨率不足调到最大仍有第二步怀疑反射探针关闭后依然存在第三步打开场景中所有隐藏网格发现地板下的碰撞体被勾选了Contribute GI并带了Emissive材质。这个碰撞体是地编在搭建时为了防止角色掉出场景加的但它被连带标记成了烘焙物体。清理静态标记后亮斑消失。这也说明了一个原则排查烘焙异常永远先从“哪些物体被意外加入了烘焙”开始而不是先调参数。烘焙是一个全局求解过程任何一颗隐藏的发光体都会以奇特的方式污染整个场景。6.3 烘焙噪点与过度平滑的取舍我们第一次以256间接采样烘焙时暗部充满了彩色噪点像老式电视的雪花。解决方案是增加采样数到256并开启NVIDIA AI Denoiser。但AI降噪有个副作用它会把暗部的细节一并抹掉让石板缝隙的立体感消失。到后期我们刻意把Filtering改成Advanced半径调到0.5让降噪只去除高频噪点保留一部分物理噪点的自然颗粒感。暗黑风格其实需要那么一点点“不干净”的质感磨得太光反而假。6.4 不要迷信“重新烘焙”按钮场景改一块石头也要重新烘焙吗我们这个项目前期太频繁全量重烤一次要40分钟迭代效率极低。后来改成区域烘焙策略把场景按房间拆成多个独立Lighting Scenario每次只烘焙改动所在的房间再把结果合并进Lightmap Atlas。虽然工作流复杂了一点但迭代时间从40分钟缩短到8分钟这8分钟对美术调整光源色温和强度来说才是可接受的节奏。是我在实际项目里最受益的一个调整。最后再分享一个小习惯每次正式烘焙前我都会先手动清理一遍Lighting数据把旧的LightingDataAsset和临时Lightmap缓存清空再重新生成。虽然Unity在技术上会自动覆盖但项目文件多了以后旧数据残片偶尔会导致编辑器里出现怎么刷新都消不掉的偏色。这个动作不花时间却让我少排查了无数个压根不存在的Bug。暗黑风格的光影烘焙没有魔法参数无非是把资产管干净、把选型想清楚然后把采样、分辨率和反弹次数当作品控工具慢慢磨。希望这篇实践能让你少走几趟弯路。