LearnOpenGL 系列 · 最终章

第0030课:IBL(基于图像的光照)

Image Based Lighting — 用环境贴图照亮世界

一、概述

IBL(Image Based Lighting,基于图像的光照)是 PBR 光照体系中的关键环节。它将环境贴图作为光源,为场景提供全局照明。与上一课中的直接点光源不同,IBL 能从四面八方照亮物体,产生极其逼真的环境反射效果。

本课对应的源文件:

  • src/6.pbr/2.1.1.ibl_irradiance_conversion/ — HDR 等矩形贴图转 Cubemap
  • src/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 光照下一课:调试与文字渲染 →