在实时渲染领域,光照计算一直是性能开销的大头。如果一个场景里有几十盏灯,每盏灯都对每个像素做完整的光照运算,帧率很快就会崩掉。光照贴图(Lightmap)的思路很简单:对于场景中不动的物体,把它们接受光照后的结果提前算好,存成一张贴图,运行时直接采样这张贴图就行,等于把复杂的光照计算转移到了离线阶段。本文将围绕光照贴图API的烘焙流程与优化手段展开,给出可直接使用的代码示例和排查思路。

光照贴图的烘焙原理与核心流程
光照贴图的烘焙本质上是一个离线的全局光照计算过程。引擎会把场景中的静态几何体收集起来,为它们分配一套专用的Lightmap UV,然后从光源出发模拟光线的弹射(直接光照加上若干次间接弹射),把最终落在每个UV纹素上的光照强度记录下来,输出成一张或多张贴图。运行时,GPU只需要用这套UV去采样Lightmap,乘以物体的反照率贴图,就能得到接近路径追踪效果的画面。
整个流程通常分为三步:第一步是标记静态物体并生成Lightmap UV;第二步是调用烘焙API执行光照模拟;第三步是把烘焙结果与材质绑定。以一个典型的C++风格接口为例,烘焙调用的核心逻辑大致如下:
// 准备烘焙参数
BakeSettings settings;
settings.quality = BakeQuality::High; // 烘焙质量档位
settings.bounces = 3; // 间接光弹射次数
settings.texelsPerUnit = 10.0f; // 每世界单位对应的纹素密度
settings.lightmapSize = 2048; // 单张Lightmap的最大边长
// 收集所有标记为静态的渲染器
std::vector<MeshRenderer*> staticMeshes = scene.GetStaticRenderers();
// 执行烘焙,返回Lightmap资源句柄
LightmapHandle handle = lightmapAPI.Bake(staticMeshes, settings);
// 将结果绑定回材质
for (auto* mesh : staticMeshes) {
mesh->GetMaterial()->SetTexture("_BakedLightmap", handle.GetTexture());
mesh->GetMaterial()->SetVector("_LightmapScaleOffset", handle.GetUVTransform(mesh));
}
这里有一个容易被忽视的细节:Lightmap UV和模型自带的纹理UV是两套东西。纹理UV可以重叠、可以镜像,而Lightmap UV必须保证不重叠,否则两块几何体的光照信息会互相污染,出现明显的漏光或错误亮斑。几乎所有引擎的烘焙API都会提供自动UV展开功能,但自动展开的质量参差不齐,接缝处理不好会在烘焙结果上留下肉眼可见的色块边界。
UV布局与烘焙参数的实操要点
烘焙质量的上限往往在UV布局阶段就已经决定了。第一个要点是控制纹素密度的一致性。如果地面用每米10个纹素,而旁边的墙用每米50个纹素,两者接收到的光照细节精度不一致,拼在一起时接缝会非常突兀。建议对同类材质、相邻表面的物体使用统一的密度设置,大多数API都支持按世界单位设定纹素密度,而不是简单给每个物体分配固定分辨率。
第二个要点是留出足够的边缘填充(Padding)。由于运行时通常采用双线性采样,采样点会取相邻纹素的加权平均,如果两个不同物体的UV块贴得太近,采样时会串到对方的光照数据,形成渗色。一般建议padding至少为4到8个纹素,具体的设置方式如下:
UVLayoutSettings uvSettings; uvSettings.padding = 8; // 纹素级别的边距 uvSettings.maxAngleError = 8.0f; // 展开时的角度误差容限 uvSettings.maxAreaError = 15.0f; // 面积误差容限 uvSettings.packStrategy = PackStrategy::BestAreaFit; // 紧凑排布策略 lightmapAPI.GenerateLightmapUV(staticMeshes, uvSettings);
第三个要点是烘焙参数的选择。弹射次数(bounces)决定间接光的丰富程度,室内场景一般2到4次就够了,次数再往上增加,画面提升有限但烘焙时间会成倍增长。采样数控制噪点水平,采样不足时Lightmap上会出现彩色噪点,尤其体现在阴影边缘。如果烘焙时间紧张,可以先低参数快速迭代,确认光照方向和强度没问题后,再用高参数出最终结果。
运行时的性能与画质优化
烘焙完成只是第一步,如何在运行时高效使用Lightmap同样重要。最直接的优化是贴图分辨率分配。一张4096的Lightmap占用的显存和带宽是1024的16倍,而场景中并非所有物体都需要那么高的精度。近处、玩家经常停留的区域应该用高密度纹素,远处的装饰性几何体完全可以压到很低。可以按物体在画面中的重要性分档,把Lightmap组织成图集,或者干脆用低分辨率贴图配合锐化滤波。
压缩格式的选择也很有讲究。Lightmap存储的是光照能量,动态范围比普通颜色贴图大,用标准的DXT1压缩容易在暗部出现色带。更合适的做法是使用HDR压缩格式,或者把光照数据拆成RGBM、Half精度浮点等编码方式,在shader里解码:
// RGBM解码,a通道存共享的编码系数
vec3 DecodeLightmap(vec4 rgba)
{
float range = 8.0; // 编码时设定的最大动态范围
return rgbatexture.rgb * rgba.a * range;
}
// 在片元着色器中采样Lightmap
vec3 baked = DecodeLightmap(texture2D(_BakedLightmap, lightmapUV));
vec3 finalColor = albedo * (baked + realtimeLightContribution);
另一个关键优化是静态与动态物体的分离处理。Lightmap只对静态物体有效,动态角色需要用光照探针(Light Probe)来近似环境光。API通常会同时输出一套探针球谐系数,动态物体根据位置在探针之间插值:
// 采样距离角色最近的四个探针,按权重混合
ProbeBlendResult blended = probeAPI.SampleBlended(player->GetPosition());
shader->SetSHCoefficients("_SHAr", blended.shR);
shader->SetSHCoefficients("_SHAg", blended.shG);
这样做的好处是动态物体也能染上环境的间接光颜色,和静态场景的光照色调保持统一,避免角色像贴图一样浮在场景上。
常见烘焙问题与排查思路
漏光是最常见的烘焙瑕疵,表现为阴影区域透出不该有的亮光,通常由两种原因引起:一是几何体太薄,光线穿透了单面墙,解决办法是加厚几何体或在烘焙时启用双面阴影投射;二是Lightmap分辨率不足,薄墙两侧的纹素距离太近,光照信息混叠,提高纹素密度即可缓解。
接缝问题则多与UV展开相关。自动展开在曲率变化大的地方切开模型,两侧纹素的光照值不一致,渲染时就会出现亮线。可以在烘焙后对接缝两侧的纹素做一次平均值滤波,或者在建模阶段手动调整UV,让接缝落在光照变化平缓的区域、背光面或被遮挡的位置。
最后是噪点问题。噪点源于采样不足,除了提高采样数,还可以从光源角度入手:减少高频细节极强的自发光面、避免小面积高强度光源,必要时开启烘焙API自带的降噪滤波器,但要注意过度降噪会把阴影边缘糊成一团,失去方向感。综合运用这些手段,基本可以在烘焙时间和画面质量之间找到适合自己的平衡点。
光照贴图Lightmap烘焙3D渲染优化修改时间:2026-09-13 01:18:36