技术博客

LightmapGI 烘焙流程与存储格式 —— 烘焙数据进了哪个文件

第 143 / 149 篇 · 2026-01-21 · godot · 渲染管线全局光照烘焙

烘焙(Bake)按钮按下去,进度条走完,场景里的间接光"啪"地一下对了——但那个 .res 文件里到底存了什么、烘焙时 GPU 在跑什么,多数人没看过。这篇拆 Godot 4 的 LightmapGI:烘焙管线的五趟 pass、UV2 的分配规则,以及 LightmapGIData 资源的二进制布局。

烘焙管线:一次 bake 的五趟 pass

servers/rendering/renderer_rd/lightmapper_rd.cpp 的 LightmapperRD 是纯 GPU 烘焙器(3.x 时代是 CPU 光追,4.0 重写为 compute + 光栅化混合),一趟 bake 的 pass 序列:

1. rasterize   —— 把每个面片光栅化进 lightmap uv2 空间,记录 albedo/emission/depth
2. direct      —— 从光源直接照射(光栅化 shadow map 或 compute ray)
3. bake        —— 间接光累积:对 texel 发射 hemisphere ray,在 SDF 上 marching
4. lightprobes —— 把结果降采样成 irradiance 探针(动态物体采样用)
5. postprocess —— denoise + 边缘填充(未命中几何的 texel 用邻域填)

关键是第 3 趟的每 texel 独立发射 ray:一个 128² 的 atlas 面片就是 16384 个 texel,每个对上半球发 N 条 ray(quality 决定 N),ray 在场景 SDF(复用 SDFGI 那套体素化代码)里 sphere-tracing。这一步纯 GPU 并行,所以 4.x 烘焙比 3.x 快一个数量级——但 ray 数是 $O(\text{texel 数} \times N)$,texel 密度调高成本线性涨。

uv2:烘焙的寻址空间

Lightmap 不走 mesh 自身的 uv,而是引擎在烘焙时给每个 mesh 动态分配 uv2 空间:Mesh::lightmap_unwrap()(xatlas 库)把场景所有几何的展开图打包进 atlas,texel 密度由 texel_size 参数决定(默认 0.2,即 1 世界单位 5 个 texel)。没做 uv2 的 mesh 烘焙时会现场 unwrap——这步是 CPU 的,大场景卡进度条多半卡在这里。

运行时 shader 里用 uv2 * lightmap_scale + lightmap_offset(每 instance 一组)从全局 atlas 采样,这就是为什么 lightmap 必须 per-instance 数据而不能纯全局纹理。

LightmapGIData 里存了什么

烘焙结果不是一张 EXR 图,而是 LightmapGIData 资源(通常跟着场景存成 .res/二进制嵌入 .tscn),内部两个数组:

// scene/3d/lightmap_gi.h(4.x,节选)
class LightmapGIData : public Resource {
    Vector<Ref<Image>> light;      // RGBE9995 压缩的 2D 纹理数组
    Vector<Ref<Image>> occlusion;  // 可选:方向性遮蔽(双 paraboloid 两张)
    Vector<Vector<uint8_t>> probe_data; // irradiance 探针的 octahedral 数据
    ...
};

选 RGBE9995 而不是 RGBA16F 是体积权衡:HDR 光照要存 0~N 的动态范围,半浮点每通道 16 位共 64 位/texel,RGBE9995 用共享指数压到 32 位/texel——一张 4096² atlas 从 128MB 降到 64MB,代价是 mantissa 只有 9 位,极暗区有色带(烘焙器会在 denoise pass 里加抖动缓解)。

occlusion 那组是双抛物面(dual paraboloid)方向性遮蔽,让动态物体受光时有方向感;probe_data 是 LightmapProbe 节点位置处的 irradiance,供动态物体通过 sample_lightprobe 插值采样。

一图流

LightmapGI 烘焙与数据流 uv2 unwrap direct 直接光 间接光累积 denoise + 填边 LightmapGIData 运行时寻址:uv2 × lightmap_scale + offset(每 instance 一组) 存储:RGBE9995(32 位/texel,共享指数 9 位尾数)比 RGBA16F 省一半显存 动态物体:probe_data 的 octahedral irradiance 按位置三线性插值

实战推论

  • texel_size 是烘焙时间的总开关,它和 atlas 尺寸、ray 数三者相乘决定 bake 时长。先用 0.3 粗烘验证 uv2 布局,再提密度出正式版。
  • 烘焙慢先分 profiling:进度条前半不动是 CPU unwrap(xatlas),后半不动是 GPU ray 累积。两者优化手段完全不同。
  • LightmapGIData 里的纹理是 Image 不是 Texture2D,加载时按层创建 TextureLayered——直接把 .res 当贴图外部引用会失败,这是迁移 3.x 工程常见的坑。

本文基于 Godot 4.x 源码:scene/3d/lightmap_gi.h/.cpp(LightmapGI/LightmapGIData)、servers/rendering/renderer_rd/lightmapper_rd.h/.cpp(GPU 烘焙器)、servers/rendering/renderer_rd/shaders/lightmapper_*.glsl。