IBL 漫反射辐照、环境卷积与法线采样
IBL 漫反射辐照、环境卷积与法线采样:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能实现并改出 PBR 球的环境光照方式:把硬编码的
vec3(0.03)常量替换成基于环境贴图的漫反射 IBL,让球从环境中接收真实的漫反射光 - 能说出 IBL 漫反射的完整预计算管线:加载 HDR → 等距柱状转立方体贴图 → 半球卷积得到辐照度贴图 → 着色器用法线采样
- 能回答:为什么辐照度贴图只需要 32×32 这么低的分辨率就够用?卷积过程中 cos(θ)×sin(θ) 权重分别在补偿什么?
为什么暗面不该漆黑
上一章把 PBR 球渲出来了——但拨开那四盏点光源,球的暗面是一片死黑,只靠 vec3(0.03) * albedo 这个硬编码常量打点底光。现实里的暗面从不是死黑的:墙上、地板、天空都在往那里补光。
没有环境光的 PBR,就像把球放进一个漆黑的房间里只用手电筒照。手电直射的那面很亮,背光的那面漆黑。你知道这不对——房间的墙和地面会把光弹来弹去,把暗面也照亮。
这一章要做的就是:把一张环境照片「包」在场景外面当全方向光源,让球从四面八方收到来自环境的真实漫反射光。
IBL:环境贴图作为光源
↡IBL(Image-Based Lighting,基于图像的光照):用一张(或多张)图像代替离散光源,把图像上每个像素都当作一个方向的入射光,从而捕捉真实世界的复杂光照环境。分漫反射 IBL(辐照度贴图)和镜面 IBL(预滤波环境贴图 + BRDF LUT)两部分。 的核心思想很简单:不用 4 盏灯,而是用一张全景照片。照片上的每个像素都代表一个方向的光——蓝天的像素是蓝色入射光,草地的像素是绿棕色入射光,灯光区域的像素亮度极高。
这样一张照片要能「包住」整个场景,就需要一种能表示所有方向的投影格式。
HDR 与等距柱状投影
真实世界的光照动态范围极大——太阳直射可以是阴影区亮度的几千倍。普通 8bit/通道的 JPG 把这一切压成 0~1,高光全爆白。↡HDR(High Dynamic Range)环境贴图:使用浮点数(如 RGBE 或 float16)存储颜色值,允许超过 1.0 的亮度,真实保留高光与阴影的亮度比例。文件格式通常是 .hdr(RGBE)或 .exr(OpenEXR)。用浮点数存储,让太阳和阴影的亮度差真实保留下来,这样卷积出的环境光才能正确反映明暗对比。
常用格式是↡等距柱状投影(Equirectangular Projection):把球面展开成宽高比 2:1 的矩形。横轴是经度(0°~360°,方位角),纵轴是纬度(-90°~90°,仰角)。优点是简单直观;缺点是极点区域严重拉伸。(.hdr 文件宽高比 2:1)——把球面展开成一张矩形,左右两端是接缝,上下是南北极。
这张贴图不能直接拿来用——GPU 更擅长采样↡立方体贴图(Cubemap):由 6 张正方形纹理(+X/-X/+Y/-Y/+Z/-Z)拼成的立方体表面贴图。用一个 3D 方向向量采样,GPU 自动计算命中哪个面的哪个点。支持 GPU 硬件滤波,全方向采样效率高。(Cubemap)。所以第一步是把等距柱状 HDR 转成 Cubemap:对立方体六个面各渲一遍,逐片段用方向向量调用 SampleSphericalMap 从 HDR 贴图采样。
辐照度卷积
拿到立方体环境贴图之后,漫反射 IBL 需要预计算每个法线方向的↡辐照度(Irradiance):单位面积上从所有方向接收到的辐射通量总和(单位 W/m²)。漫反射 BRDF 只依赖法线方向的辐照度,而非入射方向细节,因此可以预计算成辐照度贴图。——即对以法线 N 为轴的半球,把所有方向的环境光加权积分起来。
这个积分对每个法线方向都做一遍,结果存进一张 32×32 的辐照度贴图。32×32 就够,因为漫反射 BRDF 对高频细节不敏感——不同法线方向的辐照度变化很平滑,高分辨率是浪费。
单步确认:从 HDR 环境到漫反射 IBL
把 HDR 全景转成 cubemap
等距柱状图适合存储,但运行时的方向查询更适合 cubemap:用六个 90 度捕获视图把 HDR 环境重投影到六个面,并保持浮点亮度。
动手:开关 IBL,看环境光怎么点亮暗面
猜一猜:把「IBL 环境光」滑块拖到 0(关掉),球的暗面会变成什么颜色?再拖到 1(开启),暗面的光从哪里来?拖一拖粗糙度,观察 IBL 与粗糙度如何相互影响。
实时演示加载中…
拖完之后你应该看到:
- IBL=1(开启):背景变成蓝天→地面渐变,球的暗面收到来自天空和地面颜色的环境光——上方暗面偏蓝,下方暗面偏棕
- IBL=0(关闭):背景变暗色,暗面退回死黑的
0.03常量,高光孤立在漆黑球上,明显不自然 - 粗糙度拉高:IBL 环境光在粗糙球上散得更匀,暗面更「饱满」;光滑球的 IBL 影响集中在法线朝上的区域
- 金属度拉高:
kD_ibl被压到零,漫反射 IBL 消失——金属表面没有漫反射
代码逐段拆解
这一节把 ShaderDemo 里 IBL 相关的代码从整体着色器中抽出来逐段讲。直接光照部分(D/G/F + 逐光源循环)与上一章完全相同,这里重点放在四个 IBL 新增部分。
a) SampleSphericalMap:等距柱状 UV 转换
在实际项目里,把 HDR 等距柱状投影转成立方体贴图,需要对立方体六个面各渲一遍,每个片段调用:
// 把归一化方向向量转成等距柱状贴图 UV
// 输入: v = normalize(localPos)(立方体片段的本地空间方向)
// 输出: UV ∈ [0,1]
vec2 SampleSphericalMap(vec3 v) {
// atan(z,x): 方位角 (-π, π),除以 2π → (-0.5, 0.5)
// asin(y): 仰角 (-π/2, π/2),除以 π → (-0.5, 0.5)
// + 0.5 → (0, 1)
const vec2 invAtan = vec2(0.1591, 0.3183); // 1/2π, 1/π
vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
uv *= invAtan;
uv += 0.5;
return uv;
}两个反三角函数分别对应经度和纬度:atan(z, x) 算方位角(球面水平位置),asin(y) 算仰角(球面垂直位置)。乘以 invAtan 把范围从 (-π, π)/(−π/2, π/2) 归一化到 (-0.5, 0.5),再 + 0.5 移到 (0, 1)。
b) 辐照度卷积着色器:半球采样
这是预计算步骤(离线渲染辐照度贴图),不在每帧执行:
// irradiance convolution fragment shader
// 对当前面片法线方向 N 的上半球离散采样,加权求和
void main() {
vec3 N = normalize(localPos); // 当前立方体面上的法线
// 构建切线空间(以 N 为 up 的局部坐标系)
vec3 up = abs(N.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
vec3 right = normalize(cross(up, N));
up = normalize(cross(N, right));
// 双重循环:经度 φ ∈ [0, 2π],纬度 θ ∈ [0, π/2]
vec3 irradiance = vec3(0.0);
float sampleDelta = 0.025;
float nrSamples = 0.0;
for (float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) {
for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) {
// 球坐标转笛卡尔(切线空间)
vec3 tangentSample = vec3(sin(theta)*cos(phi), sin(theta)*sin(phi), cos(theta));
// 切线空间 → 世界空间
vec3 sampleVec = tangentSample.x*right + tangentSample.y*up + tangentSample.z*N;
// cos(θ): 朗伯余弦律;sin(θ): 球面雅可比(面积权重)
irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
nrSamples++;
}
}
irradiance = PI * irradiance / nrSamples;
FragColor = vec4(irradiance, 1.0);
}双重循环遍历半球上所有方向:外层 φ 是方位角(绕 N 轴转一圈),内层 θ 是仰角(从顶部到赤道)。cos(theta) 是朗伯余弦律——正对法线的方向贡献最大;sin(theta) 是球面面积的雅可比补偿——越靠近赤道,同样角度增量对应的球面面积越大。最后乘以 π 是半球积分公式的归一化常量。
c) fresnelSchlickRoughness:IBL 专用 Fresnel
// IBL 版 Fresnel:max(vec3(1-roughness), F0) 替代 vec3(1.0)
// 粗糙面在掠射角不应达到全反射;用 roughness 压低上限
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
return F0 + (max(vec3(1.0 - roughness), F0) - F0)
* pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}与直接光照的 fresnelSchlick 相比,只有一处不同:(1.0 - F0) 变成了 (max(vec3(1.0 - roughness), F0) - F0)。当粗糙度为 0,退化成普通菲涅尔;当粗糙度为 1,反射率上限被压到 F0,掠射角不再趋近全反射——符合粗糙表面的物理行为。
IBL 里用这个变体而非普通 fresnelSchlick,是因为 IBL 输入的 cosTheta 是 dot(N, V)(而不是 dot(H, V)),直接看法线时 cos 值最大,普通 Fresnel 在此处返回 F0,不会出问题;但掠射角 dot(N,V)≈0 时普通 Fresnel 会把粗糙面的 kS 推得很高,导致漫反射 kD 被压得过低,画面偏暗且不物理正确。
d) 漫反射 IBL 合并:替换 0.03 常量
// IBL 漫反射:用粗糙度修正的 Fresnel 计算 kD_ibl
vec3 kS_ibl = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec3 kD_ibl = (1.0 - kS_ibl) * (1.0 - metallic);
// 从辐照度贴图采样(此处用程序化辐照度模拟)
vec3 irradiance = proceduralIrradiance(N);
vec3 diffuseIBL = irradiance * albedo;
// 最终环境光 = kD_ibl × 辐照度 × albedo
vec3 ambient = kD_ibl * diffuseIBL;
// (上一章:vec3 ambient = vec3(0.03) * albedo;)
vec3 color = ambient + Lo;关键变化只有两行:
irradiance = texture(irradianceMap, N).rgb从辐照度贴图采样(Demo 里用程序化模拟)——输入是法线方向,返回该法线方向接收到的漫反射光总量ambient = kD_ibl * irradiance * albedo——比上一章多了kD_ibl能量守恒因子,防止反射率(kS)和漫反射率(kD)之和超过 1
vec3(0.03) * albedo 被完全取代,球的暗面不再是死黑,而是从环境中接收到各方向的漫反射光。
常见误区
小结与练习
这一章把上一章的 vec3(0.03) * albedo 常量环境光替换成了真实的漫反射 IBL:加载 HDR 等距柱状贴图 → 用 SampleSphericalMap 转成 Cubemap → 半球卷积积分出 32×32 辐照度贴图 → PBR 着色器用法线采样辐照度 × kD_ibl 得到真实漫反射环境光。球的暗面不再是死黑,而是从各方向收到来自环境的漫反射光。
- 预计算一次辐照度贴图,运行时只需一次贴图采样
fresnelSchlickRoughness是 IBL 版专用 Fresnel,用 roughness 压低掠射角上限- 辐照度只需 32×32,因为漫反射是低频信号
- 下一章将处理镜面 IBL(预滤波环境贴图 + BRDF LUT),让高光也从环境中取光
练习
实现题
- 在 Demo 的
proceduralIrradiance函数里,把天空颜色从蓝色vec3(0.4, 0.6, 1.0)改成红橙色日落vec3(1.0, 0.4, 0.2),地面颜色改成深绿色vec3(0.1, 0.3, 0.1)。观察球的暗面颜色如何随法线方向变化——朝上的区域和朝下的区域分别收到什么颜色的 IBL?
- 把 IBL 那行
vec3 kD_ibl = (1.0 - kS_ibl) * (1.0 - metallic)简化成vec3 kD_ibl = vec3(1.0 - metallic)(去掉(1.0 - kS_ibl)能量守恒因子),观察非金属球面的高光边缘有什么变化。
概念题
- 辐照度卷积的双重循环(φ 方位角 × θ 仰角)使用均匀步长采样。球面上越靠近赤道,每个角度增量对应的球面积越大,越靠近极点越小。不加
sin(theta)权重的话,极点附近的采样会被过度计数。解释为什么sin(theta)能修正这个偏差:当 θ 趋近 90°(赤道方向),sin 值接近 1(权重大,覆盖面积大);当 θ 趋近 0°(极点方向),sin 值接近 0(权重小,覆盖面积小)。这个补偿因子在数学上的来源是什么?
- 为什么辐照度贴图可以低至 32×32 分辨率?如果把辐照度直接从原始 2K HDR 环境贴图逐像素计算(不预计算),每帧的开销是多少?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- IBL
Image-Based Lighting(基于图像的光照):用一张(或多张)图像代替离散光源,把图像上每个像素都当作一个方向的入射光,从而捕捉真实世界的复杂光照环境。分漫反射 IBL(辐照度贴图)和镜面 IBL(预滤波环境贴图 + BRDF LUT)两部分。
- HDR 环境贴图
High Dynamic Range 环境贴图:使用浮点数(如 RGBE 或 float16)存储颜色值,允许超过 1.0 的亮度,真实保留高光与阴影的亮度比例。文件格式通常是 .hdr(RGBE)或 .exr(OpenEXR)。须用 GL_RGB16F 格式上传 GPU。
- 等距柱状投影
Equirectangular Projection:把球面展开成宽高比 2:1 的矩形。横轴是经度(0°~360°,方位角),纵轴是纬度(-90°~90°,仰角)。常见于 .hdr 全景贴图格式。
- 立方体贴图
Cubemap:由 6 张正方形纹理(+X/-X/+Y/-Y/+Z/-Z)拼成的立方体表面贴图。用一个 3D 方向向量采样,GPU 自动计算命中哪个面。支持 GPU 硬件滤波,全方向采样效率高。
- 辐照度
Irradiance:单位面积上从所有方向接收到的辐射通量总和(单位 W/m²)。漫反射 BRDF 只依赖法线方向的辐照度,而非入射方向细节,因此可以预计算成低分辨率(32×32)辐照度贴图。
- 辐照度卷积
Irradiance Convolution:对每个法线方向,在其朝向的上半球对环境光做加权积分,权重为 cos(θ)×sin(θ)。结果存成辐照度贴图,在 PBR 着色器里用法线采样替代常量环境光。
- fresnelSchlickRoughness
IBL 专用 Fresnel 变体:把普通 Fresnel-Schlick 的反射率上限 vec3(1.0) 替换成 max(vec3(1-roughness), F0),用粗糙度压低掠射角时的最大反射率,防止粗糙面边缘 kS 趋近 1 导致漫反射 IBL 消失。
版本、来源与运行边界
本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。
正式概念与状态责任
- image based lighting:在“IBL 漫反射辐照、环境卷积与法线采样”中由capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素定位它的第一处变化。
- irradiance map:在“IBL 漫反射辐照、环境卷积与法线采样”中由capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素定位它的第一处变化。
- convolution:在“IBL 漫反射辐照、环境卷积与法线采样”中由capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素定位它的第一处变化。
- environment map:在“IBL 漫反射辐照、环境卷积与法线采样”中由capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“捕获六面环境,按每个方向积分半球辐照度,再与 albedo/kD 组合”发生后,capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler应怎样改变HDR 投影、六面 capture matrices、环境 texel、半球样本、卷积和 normal lookup;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
IBL 漫反射辐照、环境卷积与法线采样:状态合同
把 equirectangular HDR 转 cubemap,再卷积为按法线查询的 diffuse irradiance
验证场景
官方教程正式概念
logl-41 · 基线帧
image based lighting:固定 context、资源内容与输入事件,执行“捕获六面环境,按每个方向积分半球辐照度,再与 albedo/kD 组合”
冻结输入:image based lighting
capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler记录HDR 投影、六面 capture matrices、环境 texel、半球样本、卷积和 normal lookup
结果:得到可重复的初始 GL 状态与资源身份
观测:六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素中的初始快照
预期:capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler得到可复查结果,并持续满足“六面 view/projection 朝向一致;卷积权重包含 cosθ·sinθ 与采样步长”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“捕获六面环境,按每个方向积分半球辐照度,再与 albedo/kD 组合”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“六面 view/projection 朝向一致;卷积权重包含 cosθ·sinθ 与采样步长”。
CPU command · GL state · GPU result
IBL 漫反射辐照、环境卷积与法线采样:五段轨迹
当前观测:六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素中的初始快照
不变量:六面 view/projection 朝向一致;卷积权重包含 cosθ·sinθ 与采样步长
实验三:单故障与同输入恢复
注入“正 Y/负 Y capture view 的 up 向量写反,cubemap 顶底出现接缝和翻转”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素一起恢复才算修复。
Single fault · first divergence · replay
IBL 漫反射辐照、环境卷积与法线采样:反例与恢复
故障:正 Y/负 Y capture view 的 up 向量写反,cubemap 顶底出现接缝和翻转
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“正 Y/负 Y capture view 的 up 向量写反,cubemap 顶底出现接缝和翻转”
六面 view/projection 朝向一致;卷积权重包含 cosθ·sinθ 与采样步长
六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素
最小可重放检查
unit: logl-41
owner: capture FBO、environment cubemap、irradiance cubemap 与 PBR sampler
state_or_resource: HDR 投影、六面 capture matrices、环境 texel、半球样本、卷积和 normal lookup
command: 捕获六面环境,按每个方向积分半球辐照度,再与 albedo/kD 组合
pass_invariant: 六面 view/projection 朝向一致;卷积权重包含 cosθ·sinθ 与采样步长
single_fault: 正 Y/负 Y capture view 的 up 向量写反,cubemap 顶底出现接缝和翻转
required_evidence: 六面矩阵、HDR/cubemap 对照、样本权重、irradiance texel 与暗面像素复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。