接手《暗黑王朝》的光影时,我做的第一件事不是调参,而是把之前那版“看着还行”的实时光照全部关掉重来。这是一款哥特暗黑风格的动作游戏,整个视觉基调要求冷峻、压抑、高对比,单纯靠拉低曝光根本做不出层次。暗黑场景里,画面一旦没有可靠的全局光照,墙面和地面就会变成一团死黑,角色站在里面就像是贴图上去的。我们最终定下的方案很明确:以光影烘焙作为全局光照的主体,配合动态阴影和反射探针做局部补充。整套流程从资产预处理到烘焙参数调整,再到漏光排查,前后迭代了三个多月。这篇就把我在这个项目里真正跑通的技术实践完整拆开讲,每个环节都会解释为什么这么选,而不是只丢一堆参数。
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.FindObjectsByType<MeshFilter>(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 Samples | 64 | 直接光采样,动态光源少,64足够 |
| Indirect Samples | 256 | 间接光采样,这个值决定暗部噪点 |
| Bounces | 2 | 反弹次数,封闭空间2次足够 |
| Filtering Mode | Advanced | 高斯滤波,半径0.5 |
| Lightmap Resolution | 4 texels/unit起 | 根据物件体积浮动 |
| Lightmap Padding | 4 | 防止UV岛间渗色 |
| Ambient Occlusion | 开启 | 强度0.8,半径1.5米 |
| Compression | HDR | 保留暗部层次 |
有个反常识的点:暗黑风格场景的反弹次数不需要设很高。反而是在只有两次反弹的情况下,光线在角落和柱基附近会产生清晰的衰减,这种衰减恰好形成了暗黑质感。如果把Bounces拉满到4次,暗部会被填得平平整整,整个场景看起来就像被磨皮了一样,灰蒙蒙的,一点压迫感都没有。
3.2 暗黑场景为什么一烘焙就变“灰”
我们第一版完整烘焙出来,美术总监看了一眼直接说“像阴天拍的电影,没有暗黑感”。问题出在环境光照强度上。默认情况下,Unity的环境光来自天空盒,暗黑场景即便用了深色天空盒,烘焙时还是会有一定强度的天光进入封闭空间,间接光把本该黑的角落都抬亮了。
我在Lighting环境里做了两处调整:Environment Lighting Source选择Color,用一个接近深蓝黑色的环境色,Intensity压到0.2;Environment 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。暗黑风格的光影烘焙没有魔法参数,无非是把资产管干净、把选型想清楚,然后把采样、分辨率和反弹次数当作品控工具慢慢磨。希望这篇实践能让你少走几趟弯路。