Image-Based Lighting · From Theory to Shader

基于图像的
照明Image-Based Lighting

Irradiance Map · Split Sum · Multi-scatter Energy Compensation
PBR Diffuse IBL Specular IBL Irradiance Convolution GGX Cook-Torrance Schlick Fresnel Karis 2013 Fdez-Agüera 2019
01 引言:IBL 的两条管线

基于图像的照明(Image-Based Lighting, IBL)把一张 HDR 环境贴图当作来自整个球面的入射光源,让物体能"实时"地反映出周围环境的照明信息——天光、太阳、室内灯具、地面反弹色,全部一气呵成。它是现代 PBR 管线在环境光(ambient)这一项上的终极形态。

而要在 fragment shader 里 地求解半球积分,IBL 必须分两条管线烘焙、两条管线运行时重组:

🌫 漫反射 IBL · Diffuse

对环境贴图做余弦加权卷积,得到 Irradiance Map。运行时按法线 N 单次纹理采样即可。一张极低分辨率(32² × 6 面)就够了,因为余弦核是非常平滑的低通滤波器。

✨ 镜面 IBL · Specular

核心是 Karis 2013 的 Split Sum Approximation:把高维积分撕成两份独立预计算——Prefiltered Cubemap(按粗糙度分层模糊)+ BRDF LUT(2D 查找表)。运行时两次纹理采样 + 一次乘加。

⚡ 多重散射补偿 · Multi-scatter

朴素 Split Sum 仅算单次微表面散射,高粗糙度下能量损失 30–35%。Fdez-Agüera 2019 给出了无需额外贴图的补偿公式,从 BRDF LUT 现有通道直接推导。现代引擎标配。

本笔记结构

从渲染方程出发 → 分别讲漫反射通道(Irradiance Map)→ 镜面通道(Split Sum:拆解、预滤波、LUT)→ 着色器重组 → 能量补偿。共 8 个交互演示,包括浏览器现场跑蒙特卡洛烘焙 BRDF LUT、per-pixel JS 渲染的 PBR 球、能量损失曲线等。约 40 分钟。

02 物理基础:渲染方程的两支

对不发光不透明表面,出射 Radiance 是入射 Radiance 在上半球 的加权积分:

反射方程 (Reflection Equation)

PBR 把 BRDF 拆成漫反射 + 镜面两支独立处理:

能量分配的两通道

是漫反射颜色(金属为 0,电介质为 albedo×(1-F))。两支独立积分,独立预计算,运行时分别采样后求和——这是 IBL 双管线设计的根本理由。

分量 物理意义 常用模型
法线分布函数(NDF):法线对齐半向量 的微表面比例 GGX / Trowbridge–Reitz
菲涅尔项:随视角变化的反射比例 Schlick 近似
几何遮蔽-阴影项:微表面互相挡住的概率 Smith Joint G
分母 从 half-vector 域换元到 light-vector 域的 Jacobian

Schlick 菲涅尔近似

真正的菲涅尔方程很复杂;Schlick 给出了一个5 次幂的工程近似,在大多数视角下误差 < 1%:

正面入射时的反射率:电介质常数约 0.04(黑色玻璃),金属直接取 albedo(铜约 (0.95, 0.64, 0.54))。

▸ 交互:Schlick 菲涅尔曲线 — F₀ 决定基线,5 次幂决定边缘亮度

注意当 趋近于 0(掠射角)时, 急剧抬升,所有材质的反射率都趋近 100%。这就是为什么车漆、湖水边缘看起来更亮——Fresnel Effect

水/玻璃 F₀ ≈ 0.04
塑料   F₀ ≈ 0.05
    F₀ ≈ 0.56
    F₀ ≈ 0.95
    F₀ ≈ 0.97
为什么实时渲染做不动?

朴素地求 需要在着色器里对环境贴图 蒙特卡洛多次采样,每像素 256+ 样本才能去噪——4K 屏即 8 亿次采样/帧,且 BRDF 含 GGX 重要性采样代码,绝无法实时。IBL 的所有招数都是为了把这个 拍扁成

03 漫反射 IBL · Irradiance Map

漫反射通道的处理优雅至极。对 Lambert BRDF (一个常数),出射 Radiance 简化为:

漫反射分支

关键观察: 只依赖于 (法线方向),与视角 无关。这意味着我们可以把它预先卷积成一张以法线方向为索引的低分辨率立方贴图——Irradiance Map(辐照度贴图)

烘焙:余弦加权卷积

对每一个方向 ,在以它为顶点的半球面上做余弦加权积分。用余弦重要性采样(Malley's method)可以高效完成:

余弦加权采样的 PDF 与积分中的 项正好相消,留下纯净的 求平均,再乘 。这是图形学采样里最优雅的化简之一。

资源形态

属性 典型值 说明
分辨率 32² × 6 面 (cube) 或 9 SH 系数 余弦核极平滑,低频信号即可
采样数 每像素 1024–4096 烘焙时;运行时 1 次
显存 约 24 KB(cube)/ 9 × 12 字节(SH) SH 法异常省
运行时开销 1 次 cube 采样

替代方案:球谐函数 (Spherical Harmonics)

因为辐照度信号极低频,直接用 9 个 SH 系数(前两阶,)拟合就够用了。Frostbite、CryEngine 都用这条路;显存仅 9 个 RGB float = 108 字节,比一张 cubemap 还便宜两个数量级

是球谐基函数。Ramamoorthi & Hanrahan 2001 证明:对 Lambert BRDF,9 项 SH 拟合误差 < 1%。这条路省到极致。

▸ 交互:环境贴图 vs 辐照度贴图(1D 切片可视化)

上方是程序化"天空+太阳" 1D 切片,高频(太阳是细尖峰);下方是对它做余弦加权卷积后的辐照度,极平滑,太阳被"涂抹"成宽缓的鼓包。这就是为什么辐照度贴图只需 32² 分辨率:低频信号根本采不出锯齿。

Environment(原始) ·  Irradiance(卷积后) ·  余弦核宽度 ≈ 整个半球( 处非零)
运行时着色器

三行代码搞定漫反射 IBL:
float3 irradiance = SAMPLE_TEXTURECUBE(_IrradianceMap, sampler, N).rgb;
float3 kD = (1 - F) * (1 - metallic);
float3 diffuseIBL = kD * albedo * irradiance;
其中 是漫反射能量比(金属为 0,电介质为 ,确保能量守恒)。

04 镜面 IBL:Split Sum 的核心数学

我们想求的镜面反射积分形如:

Karis 注意到一个事实:当我们用蒙特卡洛重要性采样估计这个积分时,离散和形如 。如果沿 BRDF 形状采样(即 ),并假设入射光 在采样波瓣范围内变化平缓,则可以做以下近似:

Split Sum 核心近似

两个独立的求和——这就是名字 Split Sum(拆分)的由来。

等价地,写成连续积分形式:

近似的物理直觉

第一项是"按 BRDF 重要性给环境贴图做一次模糊"——粗糙度越大、卷积核越宽、结果越像低 mip。第二项是"假设环境是纯白时 BRDF 自身能积出多少能量"——只跟视角和粗糙度有关。两项独立、维度低,都可以离线烘焙

近似何时失效?

这个近似假设 在 BRDF 采样波瓣覆盖的角度范围内"基本不变"。换言之,波瓣越窄(粗糙度越低)越准;环境光越平滑越准。失效场景:

▸ 交互:GGX 法线分布波瓣 — 粗糙度如何影响"模糊范围"

这里径向绘出 GGX 分布 。粗糙度()越大,波瓣越胖;这正是预滤波 Cubemap 用的卷积核形状。

α = roughness² = 0.090
峰值 D(0) = 39.2
半角宽度12°
05 预计算 Part I:Prefiltered Environment Map

第一项 物理上是"用 GGX NDF 形状对环境贴图做一次卷积"。我们对每一个粗糙度 都计算一份模糊后的 Cubemap,再把它们存到同一张立方贴图的不同 Mip 层级里。

烘焙流程

1
为每个 Mip 选择 Roughness
通常 mipLevel = roughness × maxMip;mip 0 → 完美镜面,mip N → 粗糙散射。

典型映射:mipLevel = roughness * (numMips - 1)。UE4 选 maxMip = 8,对应 256² → 1² 的 cube faces。
注意:低分辨率 mip 需要更高样本数(采样波瓣变宽,方差大),通常 mip 0 用 1 样本(直接采原贴图),mip N 用 1024 样本。

2
用 GGX 重要性采样生成方向
通过 Hammersley + GGX 反 CDF 把 [0,1)² 均匀点映射到 GGX 分布的半向量。

GGX 重要性采样的反 CDF 公式(半向量在切空间):

其中 是 Hammersley 点。然后 得到入射方向。

3
假设 N = V — Karis 的关键妥协
真正的卷积依赖于 V,但每个像素 V 不同。Karis 直接令 N = V = R,避免拉伸高光。

这是 Split Sum 的第二个近似:因为我们不知道每个 texel 将来会被哪个视角采样,索性假设视线沿法线方向。代价是掠射角的高光被错误地变成圆形(实际应该被拉成各向异性条纹)。Karis 在 UE4 talk 中坦承这是"明显但可接受的失真"。

4
采样 + 加权累积 + 写入 mip
用 (n·l) 做 Lambert 加权(不是 PDF!),求平均,写入对应 mipLevel。

关键代码片段(Compute Shader 化伪码):

分母是权重归一化,不是 PDF。这一步等价于做"按余弦加权的 GGX 卷积",是 Karis 的另一处实战优化。

▲ 点击每一步展开实现细节

▸ 交互:GGX 重要性采样在半球上的分布

蓝色点是 Hammersley 序列经 GGX 反 CDF 映射后的入射方向。低粗糙度(α 小)时点紧紧聚拢在镜面方向(红轴);粗糙度增大时点散布到整个半球。这就是"卷积核宽度随 mip 变化"的视觉来源。

入射方向 L
镜面方向 R
法线 N

▸ 交互:模拟预滤波后的 Mip 链

这里把一张程序化的 HDR "天空盒"(亮日轮 + 蓝天 + 暗地)按不同粗糙度做卷积模糊,模拟 Cubemap 各 mip 的内容。Mip 越高 → roughness 越大 → 越接近平均色。注意亮区如何被"摊开"

Mip 0 (镜面) → Mip 7 (粗糙) · 每条带表示一个粗糙度级别的 1D 环形环境

06 预计算 Part II:Environment BRDF LUT

第二项 看似还是个高维积分,但通过把 Schlick 菲涅尔的 提到积分外,可以把它线性分解为只依赖于 的两个标量。

F₀ 的线性提取

代入 BRDF 积分。令 ,则 ,于是:

F₀ 提取后的分解

把 BRDF 中的 拿掉只留下 ,与 完全无关。

剩下的两个积分只依赖于两个标量参数

📊 输入维度

U 轴(视角余弦),
V 轴:粗糙度

🎨 输出通道

R 通道:Scale, 的乘数项。
G 通道:Bias,与 无关的常数项。

💾 资产形态

一张 256×256 的 RG16F 二维纹理,约 256 KB,全场景共享一份。

▸ 交互:浏览器内现场用蒙特卡洛烘焙这张 BRDF LUT

按下"重新生成",会用 Hammersley + GGX 重要性采样在 JS 里算每个像素。低样本数能看到明显噪点;高样本数收敛到平滑曲面。这就是 UE4 那张著名 LUT 的本质

→ NdotV  ↑ Roughness  R/G = Scale/Bias
就绪
拾取像素 (移动鼠标到图上)
N·V = 0.50  α = 0.50
Scale =   Bias =
读懂这张图

左下角(粗糙 = 0、视角正面):Scale 接近 1,Bias 接近 0——意味着此时反射量约等于 ,符合直觉。右上角(粗糙 = 1、掠射角):Scale 减小、Bias 增大——掠射处即便 很低(如塑料)也能看到明亮反光,因为 Bias 把 Schlick 边缘项的能量"独立"贡献了进来

07 完整运行时:漫反射 + 镜面合一

把前面三份预计算资源(Irradiance Map、Prefiltered Cubemap、BRDF LUT)合在一起,IBL 的整条 fragment shader 就长这样:

// ── 完整 IBL 着色(HLSL,URP 风格) ──
float3 ImageBasedLighting(float3 N, float3 V, float roughness,
                          float3 albedo, float metallic)
{
    float3 R     = reflect(-V, N);
    float  NdotV = saturate(dot(N, V));

    // 解算 F0:金属取 albedo,电介质取 0.04
    float3 F0 = lerp(0.04, albedo, metallic);

    // ── 镜面通道 (Section 04-06) ──
    float  mip              = roughness * MAX_REFLECTION_LOD;
    float3 prefilteredColor = SAMPLE_TEXTURECUBE_LOD(_PrefilterMap,
                                          sampler_PrefilterMap,
                                          R, mip).rgb;
    float2 envBRDF = SAMPLE_TEXTURE2D(_BRDFLut, sampler_BRDFLut,
                                float2(NdotV, roughness)).rg;
    float3 specular = prefilteredColor * (F0 * envBRDF.x + envBRDF.y);

    // ── 漫反射通道 (Section 03) ──
    float3 irradiance = SAMPLE_TEXTURECUBE(_IrradianceMap, sampler_IrrMap, N).rgb;
    float3 kS = F0 + (1 - F0) * pow(1 - NdotV, 5);   // Schlick
    float3 kD = (1 - kS) * (1 - metallic);
    float3 diffuse = kD * albedo * irradiance;

    return diffuse + specular;
}

性能账本

方法 每像素纹理采样 每像素 ALU 实时?
朴素蒙特卡洛 IBL 256+ × Cube ~2k 次三角函数
完整 IBL(漫反射+镜面) 3 次(IrrMap + Prefilter + LUT) ~15 次乘加

▸ 交互:纯 Canvas 实现的 PBR 球 — 漫反射 + 镜面 IBL 完整重组

这个球是纯 JavaScript per-pixel 渲染的:每像素的色彩 = (kD × albedo × irradianceMap.sample(N)) + (prefilteredEnv(R, roughness) × (F₀ × scale + bias))。Irradiance Map 是页面加载时算好的;Scale/Bias 来自上方刚生成的 LUT;这就是真实 IBL Shader 在做的事

实时重组: 0.0ms

试试:选"银"预设,把 Roughness 拖到最大 (1.0),再切换"单次/多重散射"——这就是 Section 08 要解决的问题,肉眼可见的亮度差就是损失的 30% 能量

08 能量补偿:多重散射的还魂术

Cook-Torrance / GGX 微表面模型只算了单次反射——光线撞上一个 microfacet → 反弹一次 → 出场。但真实粗糙表面是沟壑纵横的:光线可能在多个 microfacet 之间反弹 N 次才能离开表面(Multiple Scattering),而单次散射模型把这部分能量直接丢弃了。

损失了多少?— 白炉测试 (Furnace Test)

设想一个理想"白色炉膛":四面八方都是 的纯白光,材质 (完美反射金属)。理论上反射光也应当 (能量守恒)。但 Split Sum 的反射量等于:

单次散射能量(白炉测试值)

就是 Section 06 的 BRDF LUT 两通道之和——一个 LUT 已经免费提供了能量诊断信息!理想为 1,实际…

▸ 交互:能量损失曲线 — 白炉测试一目了然

下图横轴是粗糙度,纵轴是白炉单次散射能量 (已对半球做余弦加权平均,等价于 控制的整体反射比例)。理想守恒线是 ,三条曲线分别对应三种 阴影区域就是被丢弃的能量——把这块加回来正是补偿的目标。

水/电介质 (F₀=0.04)
铁 (F₀=0.56)
金 (F₀=0.95)
能量守恒线 y=1

最大损失: ~ 35% @ α=1

补偿理论:Fdez-Agüera 2019 紧凑解

修复多重散射有许多方法。最早的 Kulla–Conty (2017) 需要额外烘焙 表。Fdez-Agüera 2019(被 Khronos glTF / Filament 采用)证明,仅用现有 LUT 的 Scale 和 Bias 两个值就能闭式推出补偿项,无需任何额外贴图:

Fdez-Agüera 多重散射补偿

是 Schlick 在整个半球上的解析平均(积分 的解析解 = 1/21)。整套公式新增 4 行 ALU,无新贴图。

漫反射也要补偿

能量被镜面通道补回了一部分,意味着漫反射通道的"剩余能量"也要相应调整。完整守恒形式:

注意 用的是带补偿后的总镜面比例。原本 Karis 标准 Split Sum 用的是 ,会留下"灰色阴霾"——补偿后场景对比度提升、暗部更干净。

HLSL 实现

// ── Fdez-Agüera 2019 多重散射补偿(增量 4 行) ──
float2 envBRDF = SAMPLE_TEXTURE2D(_BRDFLut, sampler_BRDFLut, float2(NdotV, roughness)).rg;

float3 Fss_Ess = F0 * envBRDF.x + envBRDF.y;          // 单次散射
float  Ess_W   = envBRDF.x + envBRDF.y;                // 白炉能量 (F0=1)
float3 F_avg   = F0 + (1 - F0) / 21.0;                // 平均 Fresnel
float3 Fms_Ems = (Fss_Ess * F_avg * (1 - Ess_W)) /
                  (1 - F_avg * (1 - Ess_W));            // 多次散射补偿

float3 specular = (Fss_Ess + Fms_Ems) * prefilteredColor;
float3 kD       = (1 - Fss_Ess - Fms_Ems) * (1 - metallic);
float3 diffuse  = kD * albedo * irradiance;
视觉效果

能量补偿对低 F₀ 材质几乎不可见(塑料、皮革、布料),因为初始反射本就少;对高 F₀ 金属在中-高粗糙度下显著提亮(粗糙金、磨砂铜、刷漆铝)。回到 Section 07 的 PBR 球,选"银 + roughness=1"切换看效果——多 30% 的亮度,颜色不再发灰,符合金属的"哪怕磨毛也很亮"的视觉直觉。

家族其他方案

Kulla–Conty 2017:经典多重散射补偿,烘焙 1D LUT。理论严谨但要额外贴图。
Turquin 2019:解析逼近,把 BRDF LUT 升级到 RGBA 把 塞进 Blue 通道。
Hill–Heitz 2020:随机化版本,路径追踪适配。
Fdez-Agüera 是实时管线下的最优解——零额外资源、4 行 ALU。

09 拓展与总结

更进一步

头发、拉丝金属、CD 表面都需要各向异性 BRDF(沿切线方向 vs 副法线方向粗糙度不同)。常用扩展:

  • A双 LUT 法:分别存 两个方向的 LUT,运行时按切线/副法线投影插值。
  • B修改 R 向量:把镜面反射方向 R 沿主拉伸轴弯曲,用各向同性 LUT,简单但有失真。UE5 默认采用。

当硬件支持光线追踪后(RTX、Lumen),高质量镜面反射改用反射射线 + ReSTIR / SVGF 降噪,但 IBL 依然作为fallback:远处反射、low-roughness 远景、超出 BVH 的物体仍走 Split Sum。
典型组合:粗糙度 < 0.3 用 RT,> 0.3 用 IBL;中间用距离场+插值过渡。这就是 UE5 Lumen Reflections 的核心策略。

车漆、清漆木料、皮革表面有 双层结构:上层光滑透明的清漆 + 下层有色基底。Filament/UE5 的解法:

  • 1对清漆层做独立的 Split Sum(roughness 通常 ≤ 0.1,使用 mip 0-2)。
  • 2对基底层应用 衰减后再做标准 IBL。
  • 3两层求和。需要 1 次额外 prefilter 采样 + 1 次额外 LUT 采样。

对漫反射,9 系数 SH 投影是工业首选:

烘焙时把环境贴图按球谐基函数积分,得到 9 个 RGB 系数;运行时按法线方向求 9 项加权和即可——0 次纹理采样,纯 ALU。Frostbite、Unreal Light Probes 全用此法。缺点是只能表达低频信号(不能表达"墙边阴影锐利变化"),需要配合 SDF / Lightmap 处理高频。

结语:工程妥协的美学

IBL 这套"两通道 + 三贴图 + 三次纹理采样"的体系,是图形学史上"明知有近似但仍然惊艳"的范例。它把不可解的运行时高维积分,通过精心设计的近似(Lambert 余弦核、Karis 拆分、N=V 简化、Fdez-Agüera 闭式补偿)和合理的预计算(Irradiance Map、Prefiltered Cubemap、BRDF LUT)压缩到 着色器开销。

它的成功不在于数学严谨,而在于误差落在了感知不敏感的方向——人眼对高频细节有限的容忍度,正好被工程师抓住作为"省算力的余地"。从 2013 Karis 在 SIGGRAPH 演示 UE4 那只镀金茶壶起,到 2026 年的 Lumen / Strand 头发 / Clearcoat 车漆,整套 IBL 框架都在这个底座上自然演化

如果你正在写自己的渲染管线,把这三份资产手撸一遍——亲手用 Hammersley 采 GGX、亲手做余弦加权卷积、亲手把 Schlick 拆成 Scale + Bias、亲手算 Fdez-Agüera 补偿——比读 100 篇论文都更能让你"看见"PBR 的物理含义。

核心参考文献

主题 文献 年份
Split Sum 原始 Karis, Real Shading in Unreal Engine 4 (SIGGRAPH Course) 2013
SH 漫反射 IBL Ramamoorthi & Hanrahan, An Efficient Representation for Irradiance Environment Maps 2001
能量补偿(经典) Kulla & Conty, Revisiting Physically Based Shading at Imageworks 2017
能量补偿(紧凑) Fdez-Agüera, A Multiple-Scattering Microfacet Model for Real-Time IB (JCGT)L 2019
工业实现 Filament Documentation (Google) · Khronos glTF 2.0 持续更新
各向异性扩展 Heitz, Understanding the Masking-Shadowing Function (JCGT) 2014