LearnOpenGL 系列 · 最终章
第0030课:IBL(基于图像的光照)
Image Based Lighting — 用环境贴图照亮世界
一、概述
IBL(Image Based Lighting,基于图像的光照)是 PBR 光照体系中的关键环节。它将环境贴图作为光源,为场景提供全局照明。与上一课中的直接点光源不同,IBL 能从四面八方照亮物体,产生极其逼真的环境反射效果。
本课对应的源文件:
src/6.pbr/2.1.1.ibl_irradiance_conversion/— HDR 等矩形贴图转 Cubemapsrc/6.pbr/2.1.2.ibl_irradiance/— 漫反射 IBL(辐照度图)src/6.pbr/2.2.1.ibl_specular/— 高光 IBL(预滤波 + BRDF LUT)src/6.pbr/2.2.2.ibl_specular_textured/— 纹理化 PBR + IBL 综合
二、IBL 的核心思想
传统光照用离散的点光源/方向光模拟照明,而 IBL 的思路是:将一张全景 HDR 环境贴图视为无穷远的光源,通过积分计算表面上每一点接收到的环境光能量。
IBL 将一个复杂的渲染方程拆解为两部分分别处理:
- 漫反射 IBL — 低频环境照明,产生柔和的环境色
- 高光 IBL — 高频环境反射,产生镜面倒影
三、HDR 环境贴图 → Cubemap 转换
第一步是将 HDR 等矩形贴图转换为六面体 Cubemap,以便后续采样:
// 加载 HDR 等矩形贴图(stbi_loadf 加载浮点纹理)
float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0);
// 创建 512x512 RGBA16F 的 Cubemap
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr);
// 将等矩形贴图渲染到 Cubemap 的 6 个面
// 使用 90° FOV、朝向 6 个方向的摄像机矩阵
for (unsigned int i = 0; i < 6; ++i) {
equirectangularToCubemapShader.setMat4("view", captureViews[i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0);
renderCube();
}
为什么用 Cubemap? Cubemap 的 6 个面可以像普通纹理一样采样,且适合进行卷积计算。
四、漫反射 IBL:辐照度图 (Irradiance Map)
4.1 原理
漫反射 IBL 通过对环境贴图进行 半球积分卷积 来计算每个法线方向接收到的总漫反射光。具体做法是对 Cubemap 的每个纹素,采样其周围半球方向上的所有环境光并求平均。
// 辐照度卷积着色器核心代码
vec3 irradiance = vec3(0.0);
for (int i = 0; i < sampleCount; i++) {
vec2 xi = hammersley(i, sampleCount); // 重要性采样
vec3 direction = importanceSampleGGX(xi, normal, roughness);
irradiance += texture(environmentMap, direction).rgb * cos(theta) * sin(theta);
}
irradiance = PI * irradiance / sampleCount;
4.2 实现
创建一个 32x32 的低分辨率 Cubemap,从已转换的环境 Cubemap 中卷积计算:
// 创建 32x32 的辐照度图
for (unsigned int i = 0; i < 6; ++i)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, ...);
// 使用辐照度卷积着色器渲染 6 个面
irradianceShader.use();
irradianceShader.setInt("environmentMap", 0);
// ... 对每个面进行卷积 ...
为什么是 32x32? 漫反射光照是低频信息,低分辨率即可存储,且 GPU 缓存友好。
五、高光 IBL:预滤波环境贴图 + BRDF LUT
高光 IBL 比漫反射复杂得多,因为高光反射依赖于视角方向和粗糙度。解决方案是预计算并拆分为两项:
5.1 预滤波环境贴图 (Pre-filtered Environment Map)
对不同粗糙度级别生成不同模糊程度的 Cubemap Mipmap 层:
// 生成 5 层 Mipmap,每层对应不同粗糙度
for (unsigned int mip = 0; mip < 5; ++mip) {
float roughness = (float)mip / 4.0f;
// 缩小 FBO 尺寸并渲染
prefilterShader.setFloat("roughness", roughness);
// ... 渲染 Cubemap 的 6 个面 ...
}
运行时根据材质粗糙度选择对应的 Mip 级别采样,粗糙的物体读取更模糊的环境贴图。
5.2 BRDF LUT (Look-Up Texture)
BRDF 积分与场景无关,可预计算为一张 512x512 的 2D 查找纹理:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0);
// 使用专门 BRDF 着色器渲染全屏四边形
brdfShader.use();
renderQuad();
LUT 纹理的 x 通道存储菲涅尔系数 F₀ 的缩放,y 通道存储偏移,运行时只需一次纹理采样即获得 BRDF 积分结果。
六、IBL 与 PBR 的结合
在最终的 PBR 着色器中,IBL 贡献被加到直接光照之上:
// 漫反射 IBL
vec3 diffuseIBL = texture(irradianceMap, N).rgb * albedo * ao;
// 高光 IBL
vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * 4.0).rgb;
vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specularIBL = prefilteredColor * (F * brdf.x + brdf.y);
// 最终颜色
vec3 color = directLighting + diffuseIBL + specularIBL;
在 ibl_specular_textured.cpp 中,5 种不同材质(锈铁、金、草、塑料、墙)在同一环境贴图下展示了 IBL 如何让不同材质呈现截然不同的视觉效果。
七、引擎连接:Unity 的 Reflection Probe 与 Light Probe
- Reflection Probe(反射探针) — Unity 中的 Cubemap 捕获器,与预滤波环境贴图功能完全相同。在场景中放置 Reflection Probe → 捕获 → 生成辐照度 + 预滤波贴图 → 作用到周围的 PBR 材质上。
- Light Probe Group(光照探针组) — 对动态物体在场景中进行间接光照的插值采样,本质上是对辐照度图的多点采样方案。
在 Unreal 中:Reflection Capture Actor 负责捕获环境并生成 IBL 数据,其重要性等同于本课实现的完整 IBL 管线。
八、练习
INFO
练习 1:不同环境贴图对比 将 HDR 环境贴图替换为其他场景(如室内、森林、夜晚街道),观察同一组 PBR 球体在不同环境光照下的视觉差异。
INFO
练习 2:辐照度图分辨率测试 将辐照度图从 32x32 改为 64x64 和 16x16,观察性能与画质的变化——会发现漫反射对分辨率不敏感。
INFO
练习 3:预滤波 Mip 层级观察 在渲染循环中禁用预滤波贴图的 Mipmap,使用同一层采样,观察粗糙材质高光变形的现象。 ← 上一课:PBR 光照下一课:调试与文字渲染 →