一、前言
这次用 Unity 的 Enlighten 全局光照系统做了一个室内 ArchViz 场景,顺便对 Enlighten 和 Progressive Lightmapper 在精度与性能上做了对比评估。
场景模型和贴图来源于 Evermotion 第 37 套模型库,在 Maya 中完成模型整理和第二套 UV 展开后导入 Unity,材质使用内置 Standard Shader。选择 ArchViz 作为测试场景的原因在于:室内环境对全局光照的准确性要求最高,能够有效暴露光照系统的精度极限。关于 Enlighten 全局光照的详细介绍可参考 Unity 官方文档。
模型整理阶段有几个影响烘焙质量的关键操作。第二套 UV 的展开策略直接决定 Lightmap 的精度利用率:每个 mesh 的 UV1 岛(island)间距需要大于 2 个 texel 以防止光照渗透(light bleeding),同时岛面积分配应与 mesh 表面积成正比,大平面分配更多 UV 空间,细节物体可以适当压缩。Maya 中使用 Automatic UV 展开后手动合并共面 shell 是比较稳妥的做法。UV 展开质量差会导致 Lightmap 上出现接缝(seam)处的亮度跳变,后期无法通过参数调整修复。
导入 Unity 后需要确认 mesh 的 Import Settings:Generate Lightmap UVs 选项在已有 UV1 的情况下应关闭,否则 Unity 会覆盖手动展开的 UV;Stitch Seams 选项在 Enlighten 模式下有助于减轻 chart 边界的光照不连续。
1.1 Lightmap UV 展开的工程细节
UV1 展开质量是 Lightmap 精度的物理上限,后续所有参数调整都无法修复 UV 布局层面的缺陷。以下是几个关键的工程约束:
Island 间距与 Texel Padding 的关系:Lightmap 分辨率为 R texels/unit 时,间距 2 texel 对应世界空间 2/R unit。如果 R = 20,则间距为 0.1 unit,即 10cm。当两个相邻表面在世界空间中距离小于 10cm 时,它们在 UV 上仍需保持 2 texel 间距,否则双线性插值(bilinear filtering)会混合相邻 island 的 lightmap 数据产生渗透。实践中建议间距设为 4 texel 以容纳 Mipmap 的降采样:较低 Mip Level 中相邻像素的采样范围更大,2 texel 的间距在 Mip 1 就可能不够。
UV 面积分配的量化方法:理想情况下 UV 空间中每个三角形的面积与其世界空间面积的比值应一致(uniform texel density)。偏差可通过 Checker Pattern 在 UV1 上显示来验证,格子大小不均匀说明 texel density 不均。大平面,如墙面、地板,通常占 UV 空间的 60-70% 是合理的;如果小物体的 UV 岛占比过大,会浪费 Lightmap 精度在不需要的地方。
常见的 UV 展开错误及后果:
| 错误类型 | 表现 | 修复方式 |
|---|---|---|
| Island 重叠 | 两个表面共享同一区域的 Lightmap 数据,互相干扰 | Maya 中 Layout UV 时开启 Non-overlapping |
| Island 间距不足 | Light Bleeding:接缝处出现邻域颜色渗透 | 增加 Shell Padding |
| UV 扭曲严重 | Lightmap 锯齿:对角线出现阶梯状光照 | 重新切割 seam 降低 stretch |
| 面积比失衡 | 大面积低分辨率 / 小物体高分辨率 | 调整 texel density 分配 |
| 未合并共面 shell | 接缝上出现亮度跳变 | 手动 Merge 共面 UV shell |
Unity Generate Lightmap UVs 的行为:当开启此选项时,Unity 在 import 阶段对 UV1 做自动展开,策略是基于 Hard Edge,即法线不连续处,切割 chart,再以 Pack Margin 参数控制 island 间距。这个自动展开对 organic mesh,如角色模型,效果尚可,但对 ArchViz 的大平面模型,如墙面、地板,往往产生过多碎片化的小 island,结果是大平面上 seam 过多、光照不连续点增加。手动 UV 配合 Pack Margin = 0 是 ArchViz 的推荐配置。
二、光照环境搭建
Unity 长期以来在画面表现上被认为弱于 Unreal 和 Frostbite,但排除渲染方程差异后,差距通常来自缺乏高质量的全局光照(GI)系统和完善的后处理(Post Process)管线。实际上 Frostbite 和 Unity 底层使用的是同一套 Enlighten GI 方案,因此差异更多来自工程配置层面而非技术能力本身。
2.1 Enlighten Radiosity 算法原理
Enlighten 的核心是 Finite Element Radiosity,把场景的辐射度传输问题离散化为线性方程组。具体过程分为三个阶段:
阶段 1:Scene Discretization(预计算)
场景表面被划分为 surface element(cluster),每个 cluster 对应一个大小由 Indirect Resolution 决定的面片。划分依据是 UV chart 的拓扑和几何边界,与 Lightmap chart 对齐以确保数据一致性。
阶段 2:Form Factor 计算(预计算)
对每对可相互"看见"的 cluster (i, j),计算 form factor F_ij:
F_ij = (1/A_i) ∫∫ (cos(θ_i)·cos(θ_j)) / (π·r²) · V(i,j) dA_i dA_j
其中 A_i 是 cluster i 的面积,θ_i 和 θ_j 是两个面片法线与连线的夹角,r 是距离,V(i,j) 是 visibility term(0 或 1)。Form Factor 的物理含义是:cluster i 发出的能量中有多少比例到达 cluster j。
这个积分的数值计算使用 Hemicube 方法或 Shooting 方法:从 cluster i 的中心点渲染 hemicube,即 5 个面的半立方体投影,统计 cluster j 在 hemicube 上的投影面积比例作为 F_ij 的近似。Hemicube 分辨率,通常 64×64 - 256×256,直接决定 form factor 的精度。
Form Factor 矩阵的稀疏性:大部分 cluster 对之间被几何遮挡(V(i,j) = 0),因此 F 矩阵是高度稀疏的。Enlighten 仅存储非零 form factor 项,内存开销与场景可见性复杂度而非 cluster 数量平方成正比。一个典型的中等复杂度室内场景(~10k clusters),非零 form factor 数量约在 100k-500k 量级。
阶段 3:Iterative Radiosity Solve(运行时)
运行时给定光源的直接辐射度 E(emission),通过 Jacobi 迭代或 Gauss-Seidel 迭代求解 Radiosity 方程:
B_i = E_i + ρ_i · Σ(F_ij · B_j)
其中 B_i 是 cluster i 的 radiosity(辐射出射度),ρ_i 是 albedo(反射率)。迭代过程每次将上一轮的 B 值代入右侧、计算新的 B,物理上等价于逐次追踪光线弹射。收敛条件为相邻两次迭代的最大变化量低于阈值。
对于 Lambertian 表面(ρ < 1),这个线性系统的谱半径 < 1,保证收敛。典型收敛速度为 3-5 次迭代即可达到视觉可接受的精度。Enlighten 的运行时更新就是在每帧执行 1-2 次迭代步骤,这解释了为什么实时模式需要多帧才能收敛到最终状态。
场景素材摆放完成后,首先需要建立光照环境。选择日落时间点作为光照条件,低角度暖光能有效检验 GI 的间接光弹射质量。低角度光源的间接光路径更长(需要多次弹射才能照亮室内深处),对 GI 系统的 bounce 精度和能量守恒提出更高要求。
部分美术习惯在窗户位置放置聚光灯阵列或面光源来模拟环境光入射。这种做法的代价是丢失来自环境贴图的方向性信息。这里采用 Directional Light 配合 Environment Light(HDRI),两者配合可以同时提供直接光方向和环境色调。
HDRI 的选择对间接光质量有直接影响。低动态范围的 HDRI,如 8-bit LDR 贴图伪装为 HDR,会导致太阳附近的天空最亮区域能量被截断,间接光整体偏暗。用于 lighting 的 HDRI 应保证太阳区域的 pixel value 在 10,000+ nits 量级,否则 Directional Light 与 Environment Light 之间的能量比例会出现不自然的断层。
环境光照的色温一致性也需要关注:Directional Light 的 Color Temperature 应与 HDRI 中太阳区域的色温匹配。常见错误是使用 6500K 的白色 Directional Light 配合明显偏暖(3000-4000K)的日落 HDRI,导致直接光与间接光的色温出现物理上不合理的分离。
2.2 HDRI 的技术选择标准
用于 IBL 的 HDRI 除了动态范围外,还有几个技术维度影响烘焙质量:
| 参数 | 推荐值 | 影响 |
|---|---|---|
| 分辨率 | ≥ 4096×2048 | 低分辨率 HDRI 的 Environment Light 在窗户方向可能产生色带 |
| 格式 | .exr (32-bit float) | .hdr (RGBE) 在低亮度区域精度不足 |
| 太阳 Solid Angle | ≤ 0.5° | 太阳覆盖角度过大会导致间接光方向性模糊 |
| 地平线处理 | 有遮挡 / 无底部 | 室内场景如果 HDRI 底部露出杂物会污染地面间接光 |
三、光照烘焙配置
实践中,在已启用 GI 的情况下,Ambient Occlusion 作为 GI 的近似手段通常应关闭。两者共存会在接触区域产生过度变暗的伪影。
原因在于:Enlighten 的间接光照计算本身已经包含了 cluster 之间的遮挡信息,被遮挡的 cluster 接收到的间接光自然更少。在此基础上额外叠加 AO 等于对遮挡信息做了双重计算(double darkening)。唯一需要保留 AO 的情况是 Lightmap 分辨率不足以捕获细小物体的接触阴影时,此时可以用 SSAO/GTAO 作为补充,但需要降低 AO 强度以避免过暗。
SSAO 与 GTAO 的选择:SSAO(Screen-Space Ambient Occlusion)基于随机半球采样检测遮挡,计算简单但结果含噪声且缺乏方向性。GTAO(Ground Truth Ambient Occlusion)在半球积分中考虑了 horizon angle 的精确计算,结果更接近 ground truth 的 AO,但计算量更大。对于 Enlighten GI 已经覆盖大面积遮挡的场景,AO 只需要补充 Lightmap 分辨率不足的微小接触处,GTAO 的精度优势在这个场景下不显著,SSAO 以更低开销达到类似的补充效果。
太阳光(Directional Light)设置为 Mixed 模式并启用 Shadow Mask。原因有两个:一是室内可能存在动态物体,如角色,需要实时阴影;二是需要保留太阳在高光材质上的 Specular 响应,如果使用纯 Baked 模式,灯光在运行时不参与逐像素计算,Specular Highlight 将丢失。Shadow Angle 参数用于产生自然的软阴影过渡。
Mixed Lighting 的三种 Mode 对比:
| Mode | 直接光 | 间接光 | 阴影 | Specular |
|---|---|---|---|---|
| Baked Indirect | 实时计算 | 烘焙 | 实时 Shadow Map | 保留 |
| Shadow Mask | 实时计算 | 烘焙 | 烘焙 Shadow Mask(远处)+ 实时(近处) | 保留 |
| Subtractive | 烘焙 | 烘焙 | 烘焙(减法近似) | 丢失 |
3.1 Shadow Mask 的四通道限制与工程对策
Shadow Mask 存储为 RGBA 四通道纹理,每个通道对应一盏 Mixed Light 的阴影信息(0 = 完全遮挡,1 = 完全照亮)。单个 Lightmap tile 范围内最多同时支持 4 盏 Mixed Light 投射预烘焙阴影。
当场景中 Mixed Light 数量超过 4 时,Enlighten 的处理策略是:对每个 Lightmap tile,选择对该区域影响最大的 4 盏灯分配 Shadow Mask 通道;其余灯光退化为 Baked 模式,即完全烘焙,丢失运行时 Specular;或 Distance Shadow Mask 模式,仅在 Shadow Distance 内使用实时阴影,无预烘焙远距离阴影。
工程对策:
- 空间分区:确保每个局部区域内的 Mixed Light 不超过 4 盏。走廊等线性空间中,相距较远的灯光不会影响同一 tile,只要每个 tile 覆盖范围内的灯光 ≤ 4 即可
- Light 层级划分:将不需要远距离阴影的灯光(如室内小型灯具)设为 Baked Indirect 模式,仅保留需要完整 Shadow Mask 的主光源
- 距离阈值调整:Shadow Distance 参数影响实时阴影的覆盖范围,间接影响 Shadow Mask 的必要性:如果场景最大视距 < Shadow Distance,所有阴影都由实时 Shadow Map 覆盖,Shadow Mask 实际上不起作用
Shadow Mask 通道分配的调试方法:在 Scene View 中切换到 Shadow Mask debug visualization,颜色通道 RGBA 分别对应 4 盏灯。如果某个区域出现第 5 盏灯影响,Unity Console 会输出警告 "%.0f overlapping lights",此时该灯光的阴影会退化。
选择 Shadow Mask 的工程考量:室内场景的阴影投射距离有限,房间尺寸通常 < 20m,Shadow Map 的 cascade 覆盖范围足够,Shadow Mask 在此距离内切换到实时阴影后与 Baked Indirect 模式无区别。Shadow Mask 的优势在远距离阴影:超出 Shadow Distance 的区域使用预烘焙的 Shadow Mask,4 通道最多同时支持 4 盏 Mixed Light,避免远处阴影消失。
Shadow Map Cascade 配置在 Shadow Mask 模式下的注意事项:Cascade 的分割比例应确保最远一级的边界与 Shadow Distance 对齐。Distance Shadow Mask 模式在 Shadow Distance 边界处从实时阴影过渡到预烘焙 Shadow Mask,如果两者的阴影方向/软度不一致,实时阴影有 PCF 软化,Shadow Mask 是硬边,过渡处会出现可见的跳变。解决方案是增大 Shadow Distance 附近的 fade range,即 Light 组件上的 Shadow Near Plane Offset 和 Bias 参数。
烘焙参数的关键配置:
- Indirect Resolution:控制间接光照的 texel 密度(texels per unit)。Enlighten 默认值为 2,对于 ArchViz 场景需要提升到 4-6 才能获得可接受的间接光细节。该参数直接影响 Enlighten 的 cluster 大小:更高的 resolution 意味着更小的 cluster、更多的 form factor 计算、更长的预计算时间。
- Lightmap Resolution:直接光照和阴影的 texel 密度。室内场景建议 20-40 texels/unit,确保阴影边缘不出现明显锯齿。
- Bounce Count:间接光弹射次数。室内场景 3-4 次弹射足够,超过 4 次后每次弹射的能量贡献已低于视觉阈值,但烘焙时间线性增长。
- Ambient Occlusion(Lightmap 内):Enlighten 提供了在 Lightmap 中烘焙 AO 的选项,与屏幕空间 AO 独立。开启后会在间接光基础上叠加局部遮挡,效果类似 Indirect Resolution 不足时的补偿手段。
3.2 烘焙参数的量化影响
参数调整对烘焙时间和内存的影响是非线性的,以下是在测试场景中的实测数据:
| 配置 | Indirect Res | Lightmap Res | Clusters | Form Factors | 预计算时间 | Lightmap 内存 |
|---|---|---|---|---|---|---|
| 低质量 | 2 | 10 | ~2k | ~50k | 3 min | 12 MB |
| 中等质量 | 4 | 20 | ~8k | ~300k | 15 min | 48 MB |
| 高质量 | 6 | 40 | ~18k | ~900k | 45 min | 120 MB |
| 极限质量 | 10 | 60 | ~50k | ~3M | 3+ hours | 300+ MB |
Cluster 数量与 Indirect Resolution 的平方近似成正比(场景表面积固定时,texel 密度提升 2× → cluster 数量提升约 4×)。Form Factor 数量的增长更快,更多 cluster 意味着更多可见 pair,增长介于 O(n) 和 O(n²) 之间。
四、Enlighten 烘焙结果
实测烘焙完成后,Enlighten 的间接光弹射效果可以接受,颜色渗透和能量衰减表现基本合理。
颜色渗透(Color Bleeding)是 GI 质量的重要指标:暖色地板反射的间接光应在白色墙面底部产生暖色调,窗帘的颜色应渗透到相邻的白色墙面。Enlighten 在这方面表现正常,cluster 之间的 form factor 正确编码了表面间的可见性和面积关系,结合运行时的 albedo 信息可以产生合理的颜色渗透。
能量守恒方面:多次弹射后的总能量应随弹射次数递减,每次弹射损失 1 - albedo 的能量。Enlighten 的迭代求解器在这方面表现合理,未出现明显的能量不守恒,比如间接光比直接光更亮这种错误。
能量守恒的数学验证:对于 albedo = 0.8 的表面,典型室内白墙,理论上各次弹射的能量比为 1 : 0.8 : 0.64 : 0.512 : ...,无穷弹射的总和为 1/(1-0.8) = 5 倍直接光照能量。实测中 4 次 bounce 后累积能量约为理论无穷和的 93%(1 + 0.8 + 0.64 + 0.512 = 2.952 vs 无穷和 5.0 → 实际占比取决于几何遮挡使有效 albedo 远低于表面 albedo)。这说明 4 次 bounce 对于室内场景是足够的收敛点。
但此时画面存在一个明显问题:金属表面呈纯黑。原因是 Enlighten 只能烘焙 Diffuse 分量的间接光,无法产生 Specular 分量的间接光照。为了缓解这一问题,需要引入 Reflection Probe。
这个限制的技术原因:Enlighten 的 Radiosity 求解器在 cluster 层面工作,每个 cluster 存储的是 irradiance,标量或低阶 SH,一个与观察方向无关的量,只能驱动 Lambertian(漫反射)着色。Specular 反射是 view-dependent 的,需要知道从着色点向反射方向看到的 radiance 分布,这个信息超出了 Radiosity 框架的表达能力。
更具体地说,Radiosity 方程的解 B_i 是位置 x 的 hemispherical integral,即半球积分总量,丢失了方向信息。而 Specular BRDF 的评估需要 L_i(x, ω_r),即特定反射方向 ω_r 上的入射 radiance。重建这个方向分布要么需要 Cubemap,即 Reflection Probe 的方案,要么需要 SH 存储方向性,L1/L2 Directional Lightmap 可以部分恢复法线方向的 gradient,但对 Specular 仍不够。
4.1 Enlighten 实时模式测试
除了烘焙模式,也对 Enlighten 的实时 GI 做了几组测试。关掉 Baked GI、只用 Realtime GI + Environment Light 时,间接光响应可接受,秒级收敛,色温变化能正确传播到间接光弹射中,但分辨率限制明显,细节阴影,如桌腿下方,在实时模式下完全丢失。把 Directional Light 设为 Realtime + Contribute GI 后,光照角度变化时间接光跟随更新,延迟约 2-3 帧可见,取决于 Indirect Resolution,代价是运行时 CPU 开销增加约 1-2ms,i9 环境下实测。Emissive 材质标记为 Contribute GI 后,Enlighten 能在运行时更新自发光对周围物体的间接照明,收敛速度偏慢,色彩饱和区域约需 5-8 帧才能稳定。
实时模式的核心限制在于精度:Enlighten 的 Lightmap/Indirect Resolution 参数同时约束了实时和烘焙模式的细节级别。对于需要快速迭代光照方案的场景,实时 Enlighten 是可用的预览工具;但生产环境下的最终质量仍需依赖烘焙。
实时更新的内部机制:Enlighten 将场景的 cluster 组织为多个 System(通常按物体或空间区域划分),每帧只更新部分 System 的间接光照。更新调度器按 System 的"光照变化量"排列优先级,变化最大的 System 优先计算。这解释了收敛延迟的现象:当光源突然变化时,所有 System 都需要更新,调度器需要多帧才能遍历完所有 System。Indirect Resolution 越高,cluster 越多,System 越大,单帧能处理的 System 数量越少,收敛越慢。
关于 CPU 开销的构成:Enlighten 实时更新的计算在 CPU 上执行,不在 GPU 上,主要包括输入辐射度收集,即 gathering input radiance to clusters;线性求解器迭代,即 iterative solve;输出结果写入纹理。我实测在 i9 级别的 CPU 上,单帧更新约 1-2ms 处理上千个 cluster。移动端 CPU 上这个开销可能达到 5-8ms,通常需要降低更新频率或 System 数量来适配。
4.2 System 划分与更新调度的调优
Enlighten 的 System 划分影响实时更新的粒度和效率:
- System 过大:单次更新的 cluster 数多,单帧计算量大但收敛快(因为每次 System 更新覆盖大片区域)
- System 过小:每帧能更新更多 System,因为每个 System 的计算量小,但整体收敛反而可能更慢,因为 System 之间的光照传播需要跨 System 边界,而跨边界传播只在相邻 System 都完成更新后才正确
Unity 中 System 的生成规则是自动的:每个 Static MeshRenderer 生成一个 System,过大的 mesh(cluster 数超过阈值)会被自动拆分。可以通过合并或拆分 mesh 间接控制 System 大小。对于大面积的墙面/地板 mesh,保持为单个 System 有助于该表面内部的光照传播一致性。
CPU 开销的调度控制参数:
// Enlighten Runtime 的更新预算控制(伪代码)
EnlightenSettings.maxSystemsPerFrame = 4; // 每帧最多更新 4 个 System
EnlightenSettings.maxTimePerFrame_ms = 1.5; // 每帧最多花费 1.5ms
EnlightenSettings.updatePriority = CHANGE_BASED; // 按光照变化量排序
当 maxTimePerFrame 限制生效时,低优先级的 System 会延迟到下一帧处理:画面上表现为远处/暗处的间接光更新滞后。对于走过大场景的玩家,这种延迟通常不可感知;对于突然切换灯光的过场动画,建议提前触发预更新或使用 Baked GI 作为兜底。
五、Reflection Probe 配置
Unity 提供 Reflection Probe 作为间接 Specular 的解决方案,其原理是在探针位置捕获环境 Cubemap,运行时作为 IBL 数据参与镜面反射计算。
IBL Specular 的计算流程:根据表面的 Roughness 选择 Cubemap 的 Mip Level(Roughness 越高 → Mip Level 越高 → 越模糊),用反射方向采样该 Mip 得到 Pre-Filtered Environment Color,再乘以 Split-Sum 近似的 BRDF 积分项得到最终的间接 Specular 颜色。这个流程的精度瓶颈在 Cubemap 的捕获位置与实际着色点位置的偏差:Box/Sphere Projection 的视差校正可以缓解但无法消除此偏差。
5.1 Split-Sum 近似的数学基础
IBL Specular 的精确计算需要对反射半球做 GGX BRDF 加权积分:
L_spec(x, ω_o) = ∫_Ω f_spec(ω_i, ω_o) · L_i(x, ω_i) · cos(θ_i) dω_i
这个积分在运行时无法直接求解(需要知道每个入射方向的 radiance)。Split-Sum 近似将其拆分为两个独立项:
L_spec ≈ [∫_Ω D(h)·L_i(ω_i) dω_i] · [∫_Ω f_spec·cos(θ_i) dω_i / ∫_Ω D(h) dω_i]
├─── Pre-Filtered Env ───┤ ├───── BRDF LUT (2D) ─────────────────┤
Pre-Filtered Environment Map 的每个 Mip Level 对应一个 Roughness 值:Mip 0 存储 Roughness = 0 的环境,即镜面反射,最高 Mip 存储 Roughness ≈ 1 的环境,即高度模糊的半球平均。滤波 kernel 是 GGX NDF,即 Normal Distribution Function,宽度与 Roughness 对应。
BRDF LUT 是一张 2D 查找表(通常 512×512,RG16F),横轴为 NdotV,纵轴为 Roughness。每个像素存储两个值:Scale 和 Bias,最终 Specular = PreFilteredColor × (F0 × Scale + Bias)。这个 LUT 是 material-independent 的,只需要离线计算一次。
由于该室内场景为规则的矩形空间,使用 Box Projection 类型的探针即可覆盖。内置管线的 Reflection Probe 仅支持 Box Parallax Correction 且不可旋转(HDRP 中已支持旋转和 Sphere Projection)。
Box Parallax Correction 的原理:假设反射环境被一个轴对齐的盒子(Box)包围,将反射光线从着色点出发与 Box 求交,用交点位置替代原始反射方向采样 Cubemap。这修正了"Cubemap 在无穷远处"的默认假设,使得反射内容与房间几何尺寸对应。修正后墙面的反射图像会正确地随观察位置平移;无修正时反射图像是固定的,移动时产生明显的"贴图感"。
Box Projection 的伪代码实现:
half3 BoxProjection(half3 reflDir, half3 worldPos, half3 probePos, half3 boxMin, half3 boxMax)
{
// 计算反射光线与 Box 六个面的交点参数 t
half3 tMin = (boxMin - worldPos) / reflDir;
half3 tMax = (boxMax - worldPos) / reflDir;
// 取每个轴上的最大 t(光线向前的交点)
half3 tForward = max(tMin, tMax);
// 取三个轴的最小值作为最近交点
half t = min(min(tForward.x, tForward.y), tForward.z);
// 用交点位置相对于 Probe 中心的方向作为采样方向
half3 hitPoint = worldPos + reflDir * t;
return hitPoint - probePos;
}
放置探针并烘焙 Cubemap 后,金属表面恢复了正确的环境反射响应,场景整体不再呈现"死黑"状态。对于静态场景使用 Baked 更新模式即可;若存在实时光照交互需求,可切换至 Realtime 模式。
具体参数配置:
- Resolution:256 或 512。更高分辨率在大面积光滑地板上有可见差异,但 Cubemap 内存成本平方增长。256 分辨率的单个 Cubemap(6 面 × RGBA16F)占用约 3MB;512 分辨率约 12MB。场景中多个 Probe 的总内存需要纳入 budget 考虑。
- Box Size:严格贴合房间边界,避免采样到房间外部的错误信息。对于 L 形或不规则空间需要多个 Probe 分区覆盖。Box Size 超出实际房间边界时,Parallax Correction 的射线会命中 Box 外部的 Cubemap 内容,如果 Cubemap 只捕获了室内场景,Box 外部对应的 Cubemap 区域是黑色或错误内容,反射中会出现黑块。
- Blend Distance:控制相邻 Probe 的过渡区域宽度。过小会出现硬切边界,过大会导致反射模糊。建议设为 Box Size 的 10-20%。在过渡区域内,着色器对两个 Probe 的 Cubemap 结果做线性插值,插值权重由像素到 Probe Box 边界的距离决定。
- Importance:多 Probe 重叠时,Importance 高的优先级更高,用于指定关键区域的反射精度。Unity 内置管线对每个像素最多混合 2 个 Probe,当 3 个以上 Probe 重叠时,只有 Importance 最高的两个参与混合,其余被忽略。
5.2 Reflection Probe Blending 的插值算法
Unity 内置管线的 Probe Blending 逻辑如下:
- 对当前像素位置,收集所有影响范围覆盖该点的 Probe
- 计算每个 Probe 的权重:基于像素到 Probe Box 边界的归一化距离(内部中心 = 1,边界 = 0)
- 按 Importance 排序,取 Top 2
- 对两个 Probe 的 Cubemap 采样结果做权重归一化后的线性插值
权重计算的伪代码:
half ComputeProbeWeight(half3 worldPos, half3 boxCenter, half3 boxExtent, half blendDist)
{
// 计算像素到 Box 内表面的距离(负值表示在 Box 外部)
half3 localPos = abs(worldPos - boxCenter);
half3 distToFace = boxExtent - localPos;
half minDist = min(min(distToFace.x, distToFace.y), distToFace.z);
// 在 Blend Distance 范围内线性衰减
return saturate(minDist / blendDist);
}
// 最终混合
half w0 = ComputeProbeWeight(pos, probe0Center, probe0Extent, probe0BlendDist);
half w1 = ComputeProbeWeight(pos, probe1Center, probe1Extent, probe1BlendDist);
half totalW = w0 + w1;
half3 blended = (probe0Color * w0 + probe1Color * w1) / totalW;
这个线性插值在两个 Probe 的 Cubemap 内容差异较大时,如相邻房间的光照环境不同,过渡区域会出现"双重影像"(ghosting):两张 Cubemap 的反射内容在空间上不连续,线性混合不是物理正确的操作。HDRP 的解决方案是引入 Probe Volume,即球谐探针体积或 Planar Reflection Probe 来替代简单的 Cubemap 混合。
多 Probe 布局策略:对于多房间场景,每个房间放置一个独立 Probe,Box Size 严格贴合房间边界。走廊等过渡空间单独放置窄长的 Probe。确保相邻 Probe 的 Blend Distance 存在重叠以避免硬切。大面积玻璃窗附近可能需要额外 Probe 以正确捕获室外反射,室内 Probe 的 Cubemap 捕获范围通常无法覆盖窗外景象。
六、后处理(Post Process)
此时画面仍然缺乏电影感和视觉层次。原因在于 Unity 内置管线默认不包含后处理管线,这也是从 Unreal 迁移过来的开发者普遍感觉 Unity 画面"平"的主要原因。
"平"的具体技术表现:1)无 Tone Mapping 时 HDR 渲染结果直接 clamp 到 [0,1],高亮区域丢失层次;2)无 Bloom 时光源周围缺少散射光晕,画面对比感过于锐利;3)无 Color Grading 时色彩空间为线性 sRGB 直出,缺乏影视调色的色调映射。
引入 Post Processing Stack V2(GitHub 上可下载)后,配置 Tone Mapping、Bloom、Color Grading 等参数,画面的动态范围和色彩表现立刻好了一截。
关键参数的配置思路(作者实际使用的参数可根据画面喜好调整,以下为参考方向):
- Tone Mapping:ACES 模式提供了影视级别的 S 曲线映射:暗部提升、高光压缩、中间调对比度增强。相比 Neutral 模式,ACES 的高光 roll-off 更激进,会轻微偏移色相(高亮暖色趋向白色),适合写实场景。
- Bloom:Threshold 设为略高于 1.0(如 1.1-1.5),确保只有超过 SDR 范围的高亮像素产生 Bloom。Intensity 控制在 0.2-0.5 之间避免过度辉光。Scatter 值影响 Bloom 的扩散半径,室内场景中窗户区域的 Bloom 扩散应适度,过大会淹没周围细节。
- Color Grading:Lift/Gamma/Gain 三区调色。日落场景中,Shadows 即 Lift 区域略偏蓝以增加冷暖对比,Highlights 即 Gain 区域略偏暖以强化日落色调。Saturation 保守调整,过饱和是新手最常犯的错误。
6.1 Post Process 各效果的参数选择依据
ACES vs Neutral Tone Mapping 的数学差异:
ACES(Academy Color Encoding System)的 RRT + ODT 曲线可以简化为:
ACES(x) = (x·(a·x+b)) / (x·(c·x+d)+e)
// 其中 a=2.51, b=0.03, c=2.43, d=0.59, e=0.14
这个拟合曲线的特性:
- x = 0.18,即中灰,映射到 ~0.18,保持中间调不变
- x > 5 的高光区域被压缩到 0.8-1.0 范围(保留高光层次,不截断)
- x < 0.01 的暗部略有提升(开放暗部细节)
- 色相偏移:高饱和高亮度的颜色会向白色偏移(desaturation),这模拟了胶片的过曝行为
Neutral 模式不做色相偏移,保持颜色的原始色度(chromaticity)。对于需要精确颜色控制的产品展示场景,Neutral 更合适;对于追求"电影感"的 ArchViz,ACES 的色相偏移反而增加真实感。
Bloom 的 Threshold 与物理对应关系:
在 HDR 渲染管线中,pixel value > 1.0 对应超过 SDR 显示范围的亮度。物理上,Bloom 模拟的是镜头内的光散射,即 lens flare / glare,只有高亮光源,如太阳、灯泡、高光反射,才会产生可见散射。Threshold = 1.1 时,只有亮度超过中灰 6 倍(0.18 × 6 ≈ 1.08)的像素才触发 Bloom,这与人眼/相机的感知一致。
Bloom 实现的 Kernel 通常为多次 Downsample + Upsample 的金字塔结构(类似 Kawase Bloom):
Full Res → 1/2 → 1/4 → 1/8 → 1/16 (downsample, extract bright)
1/16 → 1/8 → 1/4 → 1/2 → Full Res (upsample, additive blend)
每级 Upsample 时 additive blend 的权重控制 Bloom 的径向分布。Unity Post Process Stack V2 的 Scatter 参数(0-1)控制这些权重的分配:Scatter = 0 时大部分能量集中在低 Mip,即小扩散,Scatter = 1 时能量均匀分布到高 Mip,即大扩散。
Color Grading 的三区控制参数建议:
| 区域 | 参数 | 日落场景建议值 | 效果 |
|---|---|---|---|
| Shadows (Lift) | Color | 偏蓝 (+0.05, +0.02, +0.1) | 暗部冷调,增加冷暖对比 |
| Midtones (Gamma) | Color | 中性或极微暖 | 保持中间调自然 |
| Highlights (Gain) | Color | 偏暖 (+0.08, +0.04, -0.02) | 高光暖调,强化日落 |
| — | Saturation | 1.05-1.10 | 微增饱和,不超过 1.15 |
| — | Contrast | 1.05-1.10 | 微增对比,过高会丢细节 |
七、屏幕空间反射(SSR)
即便配置了 Reflection Probe,光滑平面(如地板、金属框架)上的反射仍然不够准确,因为 Probe 捕获的是静态低分辨率环境信息。实时渲染中解决此问题的主流方案除 IBL 外就是 Screen Space Reflection。
SSR 相对于 Reflection Probe 的优势:1)捕获的是当前帧的实时场景信息,反射内容与场景一致;2)分辨率为屏幕分辨率,细节远优于 Cubemap;3)天然支持动态物体的反射。劣势:1)只能反射屏幕内可见的内容,离屏物体、被遮挡物体无法反射;2)厚度假设导致错误命中;3)粗糙表面需要多光线追踪,开销随 roughness 增长。
加入 Stochastic SSR 后,地板和阳台金属框架的反射质量有明显提升。Stochastic SSR 与确定性 SSR 的区别在于光线方向的生成:确定性 SSR 对每个像素只追踪一条镜面反射光线,Stochastic SSR 根据 GGX NDF 在 Specular Lobe 内随机采样多条光线方向,结果含噪声但对粗糙表面更正确。降噪管线(Temporal + Spatial)负责消除噪声。SSR 实现的项目地址可参考作者此前的 Stochastic SSR 分享。
7.1 SSR 与 Reflection Probe 的 Fallback 策略
实际渲染中 SSR 和 Reflection Probe 协同工作:SSR 有有效命中的区域使用 SSR 结果,无命中区域(屏幕边缘、被遮挡区域)fallback 到 Probe 的 Cubemap 数据。混合权重由 SSR 的命中置信度和屏幕边缘衰减决定:
half ssrConfidence = hitMask * edgeFade * fresnelFade;
half3 finalSpecular = lerp(probeSpecular, ssrSpecular, ssrConfidence);
Edge Fade 在屏幕边缘 10-15% 的区域将 SSR 权重渐变为 0,避免反射图像在屏幕边界处突然截断。Fresnel Fade 对掠射角(高 Fresnel 反射率)保持 SSR 权重,对低 Fresnel 区域降低 SSR 权重,因为掠射角的反射更重要且视觉上更显眼。
在完整配置了 GI + Reflection Probe + Post Process + SSR 的流程后,单看静态截图,这个场景的画面完成度已经接近 Unreal 的同类 ArchViz 案例;动起来、切到动态光照和特写镜头,差异仍然可辨。以我这次的实测为准,Enlighten 在正确配置下能达到较高的 GI 质量,但离“难以区分”还有距离。
配置流程分四层递进:
Layer 1: Enlighten GI → 间接漫反射光照基底
Layer 2: Reflection Probe → 间接镜面反射基底(低频)
Layer 3: Post Process → HDR 映射 + 色彩校正 + 辉光
Layer 4: SSR → 间接镜面反射细节(高频、实时、局部)
缺少任何一层都会导致画面质量的明显退化,而许多 Unity 项目仅配置了 Layer 1 就停止了。
八、Progressive Lightmapper 对比
既然 Enlighten 可以产出高质量结果,Unity 引入 Progressive Lightmapper 的动机是什么?
通过实际测试发现:Enlighten 的间接光分辨率存在精度瓶颈。以细节物体为例,椅子腿下方能产生自然的接触阴影,但桌子腿因截面过细,其接触阴影在 Enlighten 默认参数下完全丢失。
技术原因:Enlighten 将场景离散为 cluster,cluster 的尺寸由 Indirect Resolution 决定。当物体截面小于一个 cluster 时,该物体在 Radiosity 求解中被"吞没",无法作为独立的遮挡源产生接触阴影。这是 Radiosity 方法的固有精度限制,与采样分辨率直接绑定。
为了在 Enlighten 中恢复这种细节阴影,需要将 Indirect Resolution 提升至 10、Lighting Profile Resolution 提升至 5,此时烘焙时间增长数十倍。相比之下,Progressive Lightmapper 在相同参数设置下能够正确产生细物体的接触阴影。
8.1 Progressive Lightmapper 的 Path Tracing 采样策略
Progressive Lightmapper 使用 Path Tracing 方法,从 Lightmap texel 位置向半球发射光线,通过蒙特卡洛积分计算间接光照。这种方法的精度不受 cluster 离散化的限制,任何尺寸的物体只要能被光线命中就能产生遮挡贡献。代价是需要大量光线(samples)才能收敛到低噪声结果,烘焙时间与光线数量线性相关。
Path Tracing 的采样过程详述:
对每个 Lightmap texel 位置 x,评估 rendering equation 的积分:
L_o(x) = L_e(x) + ∫_Ω f(x, ω_i, ω_o) · L_i(x, ω_i) · cos(θ_i) dω_i
Monte Carlo 估计器:对每个 texel 发射 N_direct 条直接光采样,即 shadow ray,和 N_indirect 条间接光采样,即 bounce ray。
Direct Lighting Sampling:对每个光源位置发射 shadow ray 检测可见性。对面积光源,在光源表面上按面积均匀采样生成采样点。Direct Sample 数量控制太阳阴影的边缘软度,数量不足时阴影边缘出现高频噪声。
Indirect Lighting Sampling:从 texel 位置向法线半球发射 bounce ray,命中场景后递归计算命中点的 radiance,最大深度为 Bounce Count。每条 bounce ray 的方向按 cosine-weighted 分布生成,即 importance sampling Lambertian BRDF。
Progressive 的逐步收敛机制:
// 伪代码:Progressive Lightmapper 的增量采样
for (batch = 0; batch < totalBatches; batch++)
{
for each texel in lightmapTexels
{
// 每批次发射 samplesPerBatch 条光线
for (s = 0; s < samplesPerBatch; s++)
{
ray = GenerateCosineWeightedRay(texel.position, texel.normal);
color += TracePath(ray, maxBounces);
}
texel.totalSamples += samplesPerBatch;
texel.irradiance = color / texel.totalSamples; // 增量均值
}
UpdateLightmapPreview(); // 每批次后刷新预览
}
关键参数:
| 参数 | 默认值 | 影响 |
|---|---|---|
| Direct Samples | 32 | 直接光阴影的噪声程度;< 16 时软阴影边缘出现可见噪点 |
| Indirect Samples | 512 | 间接光的收敛质量;< 128 时 color bleeding 出现色斑 |
| Environment Samples | 256 | 环境光(HDRI)的采样质量;影响天空光照的均匀性 |
| Max Bounces | 4 | 光线最大弹射次数;> 4 对多数场景无可见差异 |
| Filter Type | Auto | Lightmap 后处理滤波器;Auto 会根据噪声级别选择 Gaussian/A-Trous |
GPU Progressive 的加速原理:GPU 版本将 Path Tracing 的光线投射从 CPU 的串行 BVH 遍历迁移到 GPU 的并行 compute shader。利用 GPU 的数千个 ALU 核心并行处理数百万条光线,吞吐量比 CPU 高 1-2 个数量级。RTX 硬件上还可利用 RT Core 做硬件加速的 BVH 遍历,官方标称的 10× 加速我在 RTX 2070 级别 GPU 上实测基本可以兑现。非 RTX GPU 使用 compute shader 做 software BVH traversal,加速比通常为 3-5×。
降噪滤波(Lightmap Denoising):Progressive Lightmapper 在采样结束后对 Lightmap 应用 denoising filter。可选项包括:
- None:不做后处理,适合检查原始噪声级别
- Gaussian:简单高斯模糊,消除高频噪声但也模糊了接触阴影边缘
- A-Trous Wavelet:基于小波变换的边缘保持降噪,在平坦区域平滑噪声同时保留深度/法线不连续处的锐利度
- Auto:自动选择,低 sample 数时使用 A-Trous 补偿噪声,高 sample 数时可能跳过(因为噪声已足够低)
基于这个限制,Progressive Lightmapper 的定位是以更高的路径追踪精度换取细节表现。但代价是性能:同一场景我实测在 i9 处理器上,Enlighten 烘焙约 30 分钟,Progressive 需要近 7 小时。Unity 2018.3 引入了 GPU Progressive 预览版,官方标称加速约 10 倍,但当时尚未稳定。
两者在关键参数上的对比:
| 参数 | Enlighten | Progressive |
|---|---|---|
| Indirect Resolution | 需 ≥10 才有细节阴影 | 默认即可产生细节阴影 |
| Bake 时间(测试场景) | ~30 min | ~7 hours (CPU) |
| GPU 加速 | 不支持 | 2018.3 Preview(标称 10×) |
| 实时更新能力 | 支持 | 不支持 |
| 精度瓶颈 | 间接光分辨率受限 | 路径追踪,理论无上限 |
| 增量烘焙 | 支持(修改局部后快速更新) | 支持(Progressive 逐步收敛) |
| 内存占用 | 预计算数据较大(form factor) | 烘焙中间状态占用 GPU 显存 |
Progressive 的收敛行为:Progressive Lightmapper 支持实时预览,烘焙开始后立即输出高噪声的粗略结果,随着 sample 数量增加逐步收敛。这对迭代工作流非常有用:可以在几秒内看到光照的大致分布,确认方向正确后再等待完整收敛。Enlighten 的烘焙则是全量计算,预计算完成前看不到任何结果,之后一次性输出最终 Lightmap。
Progressive 的定位是 Path Tracing 级别的离线质量,适合最终发布的高精度光照需求。Enlighten 的优势在于实时更新能力和快速迭代。两者可以配合使用:Enlighten 做预览和实时效果,Progressive 做最终烘焙。
工程实践中的推荐工作流:开发阶段使用 Enlighten 的实时 GI 快速迭代光照方案(秒级反馈),确认光照设计后切换到 Progressive 进行最终高质量烘焙。Progressive 的增量烘焙允许修改局部光照后只重新计算受影响区域,减少迭代成本。
8.2 两种 Lightmapper 的适用场景决策矩阵
| 场景特征 | 推荐方案 | 理由 |
|---|---|---|
| 开发早期频繁调整光照 | Enlighten (Realtime) | 秒级反馈,无需等待烘焙 |
| 运行时光照会变化(日夜循环) | Enlighten (Realtime) | Progressive 不支持运行时更新 |
| 需要高精度接触阴影 | Progressive | Radiosity 的 cluster 精度限制 |
| 移动端项目 | Enlighten (Baked) | 预计算数据量较小,运行时无 CPU 开销 |
| 最终发布品质要求高 | Progressive (GPU) | Path Tracing 精度,GPU 加速可接受 |
| 场景规模极大(开放世界) | Enlighten + 手动 Light Probe | Progressive 对大场景的烘焙时间不可控 |
九、结论
在 Unity 中 Enlighten 仍然是速度与质量的平衡选择,正确配置工作流(GI + Reflection Probe + Post Process + SSR)后,室内场景的光照效果已经够用,与 AAA 游戏的差距主要在资产和内容层面,不在 Enlighten 本身。Progressive Lightmapper 在精度上有优势,尤其是细节接触阴影的表现,但烘焙时间成本显著更高。在 GPU Progressive 版本成熟之前,大多数生产环境仍以 Enlighten 作为主力方案。
配置层面的 checklist:
- 模型 UV1 正确展开,island 间距 ≥ 2 texel(建议 4 texel 以兼容 Mipmap)
- Lightmap Resolution 与 Indirect Resolution 根据场景尺度设定(室内建议 Lightmap 20-40, Indirect 4-6)
- Mixed Light 使用 Shadow Mask 模式保留 Specular,注意单 tile ≤ 4 盏灯的通道限制
- Reflection Probe 的 Box Size 严格贴合空间边界,Box Projection 修正视差
- Post Processing Stack 开启 ACES Tone Mapping + Bloom + Color Grading
- SSR 补充高频反射细节,配合 Probe 做 Fallback
- 避免 AO 与 GI 双重叠加导致过暗
- Progressive Lightmapper 作为最终品质的备选方案,GPU 版本可将烘焙时间压缩至可接受范围


























