站点链接:技术博客页
系列文章前言
这个系列写到第四篇,终于要碰那个所有渲染管线都在用、但很少有人从头讲清的东西了。
前三篇的路线是这样的:第一篇拆延迟渲染,讲的是"几何信息怎么存进 G-Buffer、光照怎么被推迟到屏幕空间去算";第二篇拆阴影贴图,从 PCF 一路讲到 VSM,讲的是"怎么判断一个点有没有被挡住";第三篇拆后处理链,Bloom、色调映射、抗锯齿三件事,讲的是"算完之后怎么把它变成一张能看的图"。
这三篇里反复出现一个没被展开的东西:光照到底该怎么算。第一篇里 G-Buffer 存了 albedo、normal、roughness、metallic 这些通道,但没说过为什么是这几个通道;第二篇里阴影决定了"能不能收到光",但没说过收到之后那份光该怎么变成表面的亮度;第三篇里色调映射把高动态范围压进显示范围,但没说过为什么某些颜色压完就失真了。
这篇要补的就是这块。而补它的代价是,我们得先把那个从 1986 年一直用到今天的积分式写出来——因为它才是这一切的源头。
按照惯例,先把这篇的坐标说清楚。
这篇写给谁:写过 shader、见过 roughness、metallic 这些参数、但说不清它们背后物理依据的开发者;以及已经能跑通 PBR 流程、却在"为什么磨砂金属看起来偏暗""为什么掠射角会有一圈亮边"这类现象上卡住的人。
读完能得到什么:一条从渲染方程出发、经过 BRDF 与微平面理论、落到 IBL 预计算、最后收敛到工业参数约定的完整链条;每一步近似的失效边界;以及一张能对照 glTF 规范与三大引擎的参数表。
不涉及什么(这条边界要先声明,免得读到一半发现不是自己想要的)
:本文不深入路径追踪的完整实现——它只会作为"能量守恒的参照物"出现;不涉及大气散射与体积散射;次表面散射只在皮肤那里作为引子提一句;不讲各引擎材质编辑器的操作步骤;也不给具体项目的性能调优建议。原因很简单:这些每一项都够写一篇同体量的文章,塞进来会让主线失焦。
关键参数的取值范围也先摆在这里,后文会逐个展开:介质的正入射反射率(F0)几乎总是 0.04,金属的 base color 落在 0.66 到 1.0 之间,roughness 是 0 到 1 的连续量,而 metallic 现实中只有 0 和 1 两个取值。
目录总览
| 章 | 主题 | 解决的问题 |
|---|---|---|
| 零 | 背景铺垫 | 术语对齐、范围边界、贯穿全篇的问题 |
| 一 | 从渲染方程说起 | 光照的账怎么算,为什么这个式子不能直接解 |
| 二 | BRDF:那个被积的核 | 四条性质,哪一条让预计算成为可能 |
| 三 | 从 Lambert 到 Oren-Nayar | 粗糙漫反射为什么到 2024 年还在被修 |
| 四 | 微平面理论:D、G、F | 三项各自的职责,以及 NDF 的归一化约束 |
| 五 | 几何项与菲涅尔 | 掠射角那点事,两套 $k$ 映射不能混用 |
| 六 | 单散射的缺口 | 磨砂金属为什么偏暗,怎么补 |
| 七 | IBL 与 split-sum | 环境光怎么进那个积分,$N=V=R$ 的代价 |
| 八 | 预滤波、LUT 与探针 | 落地细节:mip 链、R16G16、box projection |
| 九 | 工业实践与参数取值 | 两套工作流、F0 取值带、编码约定、PBR Neutral |
| 十 | 结语 | 九步近似的收口表、三个未解问题、选型建议 |
整篇的推进可以概括为一条主线:先立下渲染方程,再解开 BRDF 的四条性质,接着落到微平面 D/G/F 三项,补上多散射的能量缺口,最后经 IBL 与环境探针的落地细节,收敛到工业化可用的参数约定——前四步负责"把图像算对",最后一步负责"把它交得出去"。
现在可以往下读了。
背景铺垫
0.1 从一次"看起来不对"说起
先说一个具体的现象,因为它比任何定义都更容易让人意识到问题的存在。
假设你在引擎里建了三个球:一个纯金属材质、roughness 拉到 0.1;一个纯金属材质、roughness 拉到 0.9;还有一个非金属的塑料球,roughness 也是 0.9。三者用同一组光照参数、同一个环境贴图。
按理说,前两个球的区别只是"高光锐利还是模糊",第三个球应该看起来是"哑光的"。但实际跑出来,很多人会看到:roughness 0.9 的金属球偏暗,甚至比同 roughness 的塑料球还暗。
这不是 bug,也不是引擎实现有问题。它是"单次散射"这个近似在高粗糙度下的必然结果——光在微表面之间弹了很多次才离开表面,而我们的公式只算了弹一次的部分,弹两次以上的能量就凭空消失了。
先来看看,这个现象在本文第七章会被完整解释,并给出补偿方法。之所以放在开头说,是因为它恰好说明了这个领域的性质:渲染方程写出来只有一行,但从它到一个能跑的 shader,中间要做一连串近似,而每一步近似都在某个角落留下痕迹。
0.2 这件事的难度在哪
OK,言归正传。如果只看公式,PBR 并不复杂——一个积分式,加一个 BRDF 函数,加一个菲涅尔项。真正的难点在于三件事:
OK,言归正传。第一,那个积分式没法直接算。 渲染方程要求对半球面上所有入射方向积分。计算机做不到对连续函数求积分,只能采样,而采样的代价随质量上升得很快。整个实时渲染的 PBR 史,本质上是"怎么用尽可能少的采样逼近这个积分"的历史。
第二,物理正确和美术可调是冲突的。 严格按物理来,一个材质需要几十个参数才能描述清楚,美术没法用。Disney 在 2012 年做的事,就是把参数压缩到美术能理解的一小撮,同时保证物理上站得住。这个取舍至今还在演化。
第三,规范与引擎的实现细节并不一致。 同一个"金属球",在 glTF 里怎么描述、在 Unreal 里怎么落、在 Unity 里怎么落、在 Blender 里导出又变成什么,四者之间存在微妙的差异。这些差异在简单场景里看不出来,一旦做跨引擎资产迁移就会集中爆发。
0.3 术语先对齐
中文社区在这个领域的译名不太统一,先把本文的用法固定下来,免得后文歧义。括号里是本文会同时使用的英文原名,第一次出现时双语并写。
| 中文 | 英文 | 本文定义 |
|---|---|---|
| 辐射度 / 辐射亮度 | radiance | 单位面积、单位立体角上的辐射通量——渲染方程要解的量 |
| 辐照度 | irradiance | 单位面积上收到的辐射通量(对方向积分后) |
| 双向反射分布函数 | BRDF (Bidirectional Reflectance Distribution Function) | 把入射辐照度映射到出射辐射度的比例函数 |
| 微平面理论 | Microfacet Theory | 用微观小面集合描述宏观表面的理论框架 |
| 法线分布函数 | NDF / D (Normal Distribution Function) | 微面法线的概率密度 |
| 几何遮蔽项 | G (Geometry / Visibility term) | 微面之间互相遮挡的比例 |
| 菲涅尔项 | F (Fresnel term) | 反射率随入射角变化的关系 |
| 基于图像的光照 | IBL (Image-Based Lighting) | 用环境贴图当光源的近似方法 |
| 分割求和近似 | split-sum approximation | Karis 2013 提出的 IBL 预计算方案 |
| 金属度-粗糙度工作流 | metallic-roughness workflow | glTF 规定的默认材质模型 |
0.4 一个贯穿全篇的问题
最后把这篇要回答的核心问题摆出来。后文每一章都在回答它的一个侧面:
如果渲染方程是对的,那么从它到一个美术能调、跨引擎能迁移、跑起来还不崩的材质管线,中间每一步做了什么近似,各自在什么时候会露馅?
带着这个问题往下读,比记住任何单个公式都有用。
一、光照的账怎么算:从渲染方程说起
1.1 一个只能写出来、不能解出来的式子
渲染方程(rendering equation)通常写作:
$$ L_o(x, \omega_o) = L_e(x, \omega_o) + \int_{\Omega} f_r(x, \omega_i, \omega_o)\, L_i(x, \omega_i)\, (\omega_i \cdot n)\, d\omega_i $$
把它拆开逐项读一遍:
- $L_o(x, \omega_o)$ 是从点 $x$ 沿方向 $\omega_o$ 出来的辐射度——也就是我们要算的那个值,最终会变成屏幕上一个像素的颜色
- $L_e(x, \omega_o)$ 是该点自身发出的光。对绝大多数物体这个项是零,只有光源自己才不为零
- 积分号是对整个半球面 $\Omega$ 做的,也就是"从上方所有方向进来的光"
- $f_r$ 是 BRDF,它决定"从这个方向进来的光,有多少会反射到那个方向去"
- $L_i$ 是从方向 $\omega_i$ 进来的入射辐射度
- $(\omega_i \cdot n)$ 是余弦因子,它描述的是"斜着照进来的光,实际落到单位面积上的通量会变少"
需要补充一段来历。这个方程常被直接归给 Kajiya 1986 年的论文《The Rendering Equation》[1],但它在同年由 David Immel 等人与 James Kajiya 独立引入图形学,且形式与辐射传热领域早已成熟的理论一致 [2]。Cornell 的课程书目里对它的评价很直白:如今被视为表面渲染的理论基础 [3]。
1.2 为什么不能直接解
这个方程的麻烦之处在于,$L_i$ 本身也是未知的。
先来看看这个式子难在哪。从方向 $\omega_i$ 进来的光从哪来?可能直接来自光源,也可能来自另一个物体的反射——而那个物体的反射,又要用同一个方程去算。也就是说,方程的两边都含未知量,未知量之间还互相引用。
离线渲染之所以要跑几分钟甚至几小时,就是在处理这种递归依赖——用路径追踪反复采样,让光在场景里弹很多次,直到结果收敛。
实时渲染没有这个预算。所以工业界的做法是:把递归切断,只保留直接可见的那部分。 具体来说:
- 进入 $L_i$ 的光,只算"来自光源的"和"来自环境贴图的"
- 物体之间的多次相互反射,用其他手段近似(光照贴图、探针、屏幕空间反射),而不是解方程
这就是为什么实时渲染里"间接光"总是一个需要单独处理的模块,也是为什么本文后半部分要花两章讲 IBL——它本质上是在回答"那部分被切断的间接光,怎么补回来一点"。
1.3 六步闭环:这一章的方法与参数
按照系列惯例,每章技术内容走"概念 → 原理 → 公式 → 参数 → 实现 → 效果"六步。本章的落点如下:
概念:渲染方程把"一个点的颜色"定义为"它发出的光加上它反射的光"。
原理:反射的那部分是对半球入射光的加权积分,权重由 BRDF 与余弦因子共同决定。
公式:见 1.1 节。离散化之后,实际实现在算的是:
$$ L_o(x, \omega_o) \approx L_e(x, \omega_o) + \sum_{k=1}^{N} f_r(x, \omega_{i,k}, \omega_o)\, L_i(x, \omega_{i,k})\, (\omega_{i,k} \cdot n)\, \Delta\omega $$
参数:$N$ 是采样数。OK,言归正传。解析光源场景下 $N$ 等于光源数(通常几个到几十个);环境光照场景下 $N$ 是预计算的采样数——Karis 在 IBL 的预滤波里用的是 1024 个样本 [4]。
实现:求和的形式决定了渲染循环的形状——外层遍历光源,内层计算 BRDF 与余弦因子。这个循环放在哪个阶段执行,就是延迟渲染与向前渲染的分野(详见本系列第一篇)。
效果:当 $N$ 足够大且 BRDF 正确时,结果在视觉上收敛到"物理正确";$N$ 不足时会出现噪点或能量偏差。
1.4 参数逐项释义:$N$ 的代价与选择
$N$ 这个参数值得单独说,因为它直接决定性能预算。
对解析光源,$N$ 等于"影响该像素的光源数量"。做法是筛选——只处理贡献最大的若干个,其余用近似合并。这也是前向渲染里"每像素最多 N 个光源"这个限制的来源。
对环境光,$N$ 不能是实时采样的。如果每个像素都去半球上采 1024 个方向、每个方向再查一次 cubemap,代价是每像素上千次纹理采样——完全不可接受。所以业界把这一步搬到离线做:预先把环境贴图卷积成"已经积分好的形式",运行时只做一次查表。这个思路就是第八章要展开的 IBL。
代码形态上,解析光源的求和大致长这样:
// 【直接光照】对每个影响本像素的解析光源求和
float3 total = 0;
for (uint i = 0; i < lightCount; ++i) { // 【1】遍历光源:N 由光源数决定
float3 L = normalize(lights[i].position - pos); // 【2】入射方向
float NoL = saturate(dot(N, L)); // 【3】余弦因子,负值截断
if (NoL <= 0) continue; // 【4】背面的光直接跳过
float3 H = normalize(L + V); // 【5】半程向量,供 NDF/F 使用
total += BRDF(L, V, N, H) * lights[i].color * NoL;
}
上面这段里特别说明三点。
【1】处遍历的是光源数组,$N$ 就是这里的 lightCount——这个循环的形状决定了整个着色阶段的成本结构。
【4】处的提前退出看着像优化,其实有物理含义:$(\omega_i \cdot n) \le 0$ 表示光从背面照来,这一项本就该是零,跳过它不损失精度。
【5】处的半程向量 $H$ 是为微平面项准备的——NDF 与菲涅尔项都以它为中心计算,这一点会在第四章展开。
完整的实现路径可以参考 Filament 的着色器源码与文档 [5],它对直接光与 IBL 两条路径的拆分做得相当清楚。
渲染方程是在一个半球上积分,积分域就是它:
立体角是这一族量里最抽象的一个,它的几何含义是这样:
渲染方程里被积的是辐射亮度,它的定义是单位面积、单位立体角上的能量:
再往前一层是辐射强度——只对立体角积分、不涉及面积:
1.5 这一章留下的问题
渲染方程只是一个框架。真正决定"这个表面看起来像什么"的,是积分号里面那个 $f_r$——BRDF。而它不是一个随便什么函数都行:物理上合法的 BRDF 必须满足三条约束。下一章就来把这三条讲清楚。
二、BRDF:那个被积的核
2.1 定义:一个比值
BRDF 的定义是一个比值:
$$ f_r(\omega_i, \omega_o) = \frac{dL_o(\omega_o)}{dE_i(\omega_i)} $$
下面我们开始。读作"出射辐射度的微分增量,除以入射辐照度的微分增量"。
理解这个定义的关键在于:它是一个"每单位入射光"的响应函数,而不是"反射了多少光"的绝对值。入射光变强一倍,出射也变强一倍,比值不变——这个性质叫线性,是第四条(常被省略的)性质。
UC Berkeley 的 CS184 课件把 BRDF 的性质列得很清楚 [6],下面三条是物理可信的必要条件。
2.2 三条约束
第一条,非负(positivity):
$$ f_r(\omega_i, \omega_o) \ge 0 $$
表面不能把光反射成负的。这一条看着理所当然,但它是判断"某个经验模型能不能用"的第一道筛子——比如某些早期模型在高光边缘会算出负值,那就不合法。
第二条,互易(reciprocity):
$$ f_r(\omega_i, \omega_o) = f_r(\omega_o, \omega_i) $$
交换入射与出射方向,结果不变。这个性质也叫 Helmholtz 互易性 [7]。它的实用价值在于:如果实现里出现了不对称的现象(某个角度亮、反过来的角度不亮),那说明公式或代码写错了——它可以直接当调试手段用。
第三条,能量守恒:
$$ \int_{\Omega} f_r(\omega_i, \omega_o)\, (\omega_o \cdot n)\, d\omega_o \le 1 $$
反射出去的总能量,不能超过入射进来的总能量。这条是最重要、也最容易被违反的——本文第七章整章都在讲它被违反之后会发生什么、以及怎么补偿。
第四条,线性:多个光源同时照时,结果等于各自单独照的结果之和。这条让"遍历光源求和"成为合法操作,是整条渲染循环成立的前提。
2.3 参数逐项释义:为什么是这四个性质
把这四条放在一起看,会发现它们各自对应一个工程上的用处:
| 性质 | 数学形式 | 工程用途 |
|---|---|---|
| 非负 | $f_r \ge 0$ | 筛掉不合法的经验模型 |
| 互易 | $f_r(\omega_i,\omega_o) = f_r(\omega_o,\omega_i)$ | 调试手段:不对称即错误 |
| 能量守恒 | $\int f_r \cos\theta \, d\omega \le 1$ | 判断近似是否丢失能量 |
| 线性 | $f_r$ 与 $L_i$ 无关 | 让多光源求和与预计算成为可能 |
其中线性的作用最容易被忽略。它之所以重要,是因为 IBL 的整个预计算方案都建立在它之上:既然 BRDF 与入射光无关,那就可以把"积分里与光无关的部分"提前算好存起来,运行时只做查表。如果没有线性,第八、九章讲的那套 split-sum 根本不可能成立。
2.4 公式与实现:一个合法的 BRDF 长什么样
最简的合法 BRDF 是 Lambert 漫反射:
$$ f_d = \frac{\rho}{\pi} $$
其中 $\rho$ 是反照率(albedo),取值范围 0 到 1。分母那个 $\pi$ 不是凑出来的——它是为了满足能量守恒而必须出现的归一化因子。这一点值得算一遍:把常数 BRDF 代进能量守恒的积分式,
$$ \int_{\Omega} \frac{\rho}{\pi} \cos\theta \, d\omega = \frac{\rho}{\pi} \cdot \pi = \rho \le 1 $$
$\cos\theta$ 在半球上的积分是 $\pi$,正好把分母约掉,剩下 $\rho$ 本身。这就是"反照率不能超过 1"这个约束的来源。
这个推导的意义:它说明 BRDF 里那些看着奇怪的常数($\pi$、$4\pi$、$1/8$)都不是随便写的,每一个都对应一次归一化。后文遇到 GGX 的 $k=(\alpha+1)^2/8$、菲涅尔的 $1/5$ 次方,都可以用同样的思路去验算。
代码形态上,Lambert 漫反射的实现就是一行:
// 【Lambert 漫反射】反照率除以 π —— 那个 π 是能量守恒要求的归一化因子
float3 DiffuseLambert(float3 albedo) {
return albedo / PI; // 【1】PI 不可省略,省略则能量超发
}
上面这一行特别说明:分母的 $\pi$ 省略掉,画面会变亮大约 3.14 倍,而且在高光区域会明显过曝。很多"PBR 看起来发灰/发白"的排查,最后都指向这类归一化因子的缺失。
完整的 BRDF 实现清单与对照可以参考 pbrt 的反射模型章节 [8],它对每个模型都给了归一化推导。
BRDF 的求值频率在引擎里是可选的——逐像素还是逐顶点:
一个合法的 BRDF 描述的是光与表面相互作用的三种去向:
2.5 这一章留下的问题
Lambert 这个模型有个明显的问题:它太简单了。 真实的粗糙表面,比如未施釉的陶器、混凝土、粗糙布料,在掠射角附近会比 Lambert 预测的更亮。这个问题在 1994 年被专门建模过,而且直到 2024 年还在被修正。下一章就来讲这段"看似简单、其实不简单"的历史。
三、漫反射不简单:从 Lambert 到 Oren-Nayar
3.1 Lambert 的漏洞
上一章结尾说到 Lambert 太简单。具体哪里简单?
Lambert 假设表面把入射光均匀地散射到整个半球,反射强度只与入射角有关,与出射方向无关——所以严格来说它是一个"常数 BRDF"。对很多材质这个假设够用:石膏、纸张、粗糙塑料,视觉上都接近这个行为。
但有一类表面它预测得不对:未施釉的陶器、月球表面、粗毛毡、混凝土。这类表面有个共同特征——微观上是凹凸不平的粉末状或纤维状结构,而不是光滑连续的面。
先来看看,拿月球的照片对比一下就很直观。按 Lambert 计算,月球在明暗交界处应该平滑地暗下去;但实际观测里,那一圈会反常地亮一点。这不是测量误差,是这类表面固有的反射行为。
3.2 Oren-Nayar:把表面当成 V 形凹槽
1994 年,Oren 和 Nayar 给出了一个专门描述这类表面的模型。它的思路很巧妙:不去直接建模那些复杂的颗粒,而是把表面简化为一组 V 形的凹槽,然后用球面高斯分布描述这些凹槽朝向的分散程度。
这个几何简化抓住了关键机制:互相遮挡。当一个 V 形凹槽的入射角变大时,凹槽的一个面被另一个面挡住,遮挡比例上升;但同时,可见的那个面里,法线更朝向观察者的比例也在上升。两个效应的净结果,是明暗交界处出现亮度回升。
pbrt 采用的是 Oren-Nayar 的拟合近似形式,含两个参数 A 和 B [8]。UCSD 的课程讲义里有一页专门并排对照 Lambert 与 Oren-Nayar 的形式差异 [9]。
3.3 参数逐项释义:粗糙度在这里的另一个含义
Oren-Nayar 有一个参数 $\sigma$,表示凹槽朝向的分散程度。它的作用与后文微平面理论里的 roughness 类似,但两者不是同一个量——这是容易混淆的地方:
| 参数 | 所属模型 | 描述对象 | 典型取值 |
|---|---|---|---|
| $\sigma$ | Oren-Nayar | 漫反射微面的朝向分散 | 0(= Lambert)到 $\pi/2$ |
| roughness $\alpha$ | 微平面理论 | 镜面微面的朝向分散 | 0(镜面)到 1(全粗糙) |
工程上的经验是:$\sigma$ 很少单独暴露给美术,通常由粗糙度间接映射。原因很简单——两个参数都往美术界面上摆,没人知道该怎么调。
3.4 公式与实现
Oren-Nayar 的一种常见实现形式:
// 【Oren-Nayar 粗糙漫反射】σ 为微面朝向的分散程度
float3 DiffuseOrenNayar(float3 albedo, float3 N, float3 V, float3 L, float sigma) {
float s2 = sigma * sigma;
float A = 1.0 - 0.5 * s2 / (s2 + 0.33); // 【1】两项拟合参数
float B = 0.45 * s2 / (s2 + 0.09);
float NoL = saturate(dot(N, L));
float NoV = saturate(dot(N, V));
float3 Lp = normalize(L - N * dot(N, L)); // 【2】把 L、V 投影到切平面
float3 Vp = normalize(V - N * dot(N, V));
float cosD = saturate(dot(Lp, Vp)); // 【3】投影后的夹角余弦
float t1 = max(0.0, cosD) * sin(max(acos(NoL), acos(NoV)));
float t2 = min(1.0, min(NoL, NoV));
return albedo / PI * (A + B * t1 * t2); // 【4】σ=0 时退化为 Lambert
}
上面这段特别说明四处。
【1】处两个拟合参数在 $\sigma = 0$ 时分别给出 $A = 1$、$B = 0$,整个式子退化成 Lambert——这是验证实现是否正确的最快方法:把粗糙度调到零,结果应当与 Lambert 完全一致。
【2】处把入射与出射方向都投影到切平面再比较夹角,这是 Oren-Nayar 与 Lambert 的分野所在——Lambert 根本不关心出射方向,而这里出射方向的方位角会影响结果。
【3】处那个投影后夹角的余弦,是把三维问题降到二维的关键一步:V 形凹槽的遮挡只发生在"沿着凹槽走向"的平面内,投影之后就能用平面几何处理。
【4】处同样带着 $1/\pi$ 的归一化因子。这一步不能省——省掉之后亮度会凭空涨三倍多,而且会与镜面项之间的能量分配失衡。
3.5 一个到 2024 年还在修的问题
到这里,故事本来可以结束了。但 2024 年 JCGT 上又出现了一篇论文,专门修正 Oren-Nayar 不满足能量守恒的问题 [10]。
说白了,问题出在哪?Oren-Nayar 的推导目标是"形状上正确",它没有强制要求总反射能量不超过入射能量。在粗糙度不高时偏差很小;但在粗糙度接近上限、掠射角很大时,它可能既可能少反射、也可能多反射。
修正方案叫 EON(Energy-preserving Oren-Nayar),核心是给模型加一个与角度相关的归一化因子,把积分结果压回 1 以内。
这说明什么:粗糙漫反射这个看起来最没有技术含量的项,历史比想象的更长——1760 年 Lambert 在《光度学》里给出余弦定律,1994 年 Oren 与 Nayar 补上粗糙表面的修正,直到 2024 年 EON 才把能量守恒补齐 [10]。也就是说,这个看起来最没有技术含量的项,被修了两百多年。原因是它同时被两个目标拉扯——形状要像,能量还要守。而工业界对这两者有不同的容忍度:形状不对,美术会立刻指出"高光位置怪";能量不守恒,只有在做 furnace test(把物体放进均匀白色环境)时才看得出来。
3.6 这一章留下的问题
漫反射项讲完了。但真正让 PBR 区别于传统光照模型的,是高光那一半——也就是"镜面反射怎么算"。而它需要一个至今仍在被讨论的理论框架。下一章进入微平面理论。
四、微平面理论:D、G、F 与法线分布
4.1 核心思想:宏观的粗糙来自微观的分散
下面我们开始。微平面理论的出发点是这么一个观察:看起来粗糙的表面,在微观上是由大量朝向各异的小平面组成的,而每个小平面本身是光滑的。
这个想法把"粗糙"从一个形容词变成了一个可计算的量——微面法线的分布。
Berkeley 的课程讲义里把它讲得很清楚:微面法线越集中(都指向宏观法线附近),高光越锐利;越分散,高光越宽 [11]。UCSD 的 BSDF 讲义也强调同一个点——NDF 本质上是微法线方向的概率密度函数 [12]。
“宏观粗糙来自微观分散”这句话的几何图景是这样:
同一块表面的光线去向,逐条看是这样的:
4.2 Cook-Torrance 的三项分解
1982 年,Cook 和 Torrance 把这个框架整理成一个可用的反射模型,把镜面项拆成三个因子相乘 [13]:
$$ f_{spec} = \frac{D(h)\, F(\omega_o, h)\, G(\omega_i, \omega_o, h)}{4\, (\omega_i \cdot n)\, (\omega_o \cdot n)} $$
三相各自的职责:
| 项 | 名称 | 作用 | 是否可选 |
|---|---|---|---|
| $D$ | 法线分布函数 NDF | 有多少微面的法线正好能把光反射到观察方向 | 必须 |
| $F$ | 菲涅尔项 | 单个微面按入射角反射多少比例 | 必须 |
| $G$ | 几何遮蔽项 | 有多少微面因互相遮挡而失效 | 必须 |
| $\frac{1}{4(\omega_i\cdot n)(\omega_o\cdot n)}$ | 归一化分母 | 抵消方向投影带来的偏差 | 必须 |
先来看看,为什么要除那个分母。 这一项看起来最没道理,但它恰恰是微平面理论"成立"的关键。微平面模型描述的是微面上的物理量,而我们观测的是宏观表面上的量——两者之间差了一个方向投影的换算。分母里的两个余弦因子就是这个换算,系数 4 是立体角与面积的联合换算结果。
Filament 的官方文档给出了工业级的推导 [5]:它引入一个可见性函数 $V$ 替代 $G$,令 $V = G/(4|N\cdot L||N\cdot V|)$,于是镜面项简化为 $D \cdot V \cdot F$ 的形式,分母被吸收掉了。这是引擎实现里更常见的写法。
4.3 NDF 的归一化约束
这是本文要强调的第一个"中文文章普遍不写"的点。
一个合法的 NDF 必须满足:
$$ \int_{\Omega} D(\omega_h)\, (\omega_h \cdot n)\, d\omega_h = 1 $$
直觉解释:$D$ 是"微法线的概率密度",那么把整个半球上的微法线都数一遍、按投影加权,总量必然是 1。
好了,这条约束的实用价值在于它是一个筛子:
| NDF | 是否满足归一化 | 后果 |
|---|---|---|
| Phong / Blinn-Phong | 不满足 | 非能量守恒,随粗糙度变化会出现能量漂移 |
| Beckmann | 满足 | 可用 |
| GGX / Trowbridge-Reitz | 满足 | 可用 |
这解释了为什么 Blinn-Phong 虽然是实时渲染里最经典的高光模型,却在新标准里被弃用——不是因为它不好看,而是因为它不合法。
法线分布函数的形状,可以看这张对照:
pbrt 在讲 Beckmann 分布时明确给出了这条归一化约束 [8];Walter 等人的原始论文系统对比了 Beckmann、Phong、GGX 三类分布 [14]。
4.4 法线分布的历史谱系
NDF 这个概念的演化过程,本身就是一段值得单独讲的脉络:
| 年份 | 提出者 / 名称 | 贡献 | 现状 |
|---|---|---|---|
| 1923 | Berry | 最早的微面统计描述 | 历史 |
| 1963 | Beckmann | 高斯型粗糙假设,光学界沿用 | 可用,尾部偏窄 |
| 1973 | Phong | 经验式高光 | 不合法(不归一化) |
| 1975 | Trowbridge-Reitz | 提出后来被称为 GGX 的形式 | 实时标准 |
| 1977 | Blinn-Phong | 半程向量简化 | 不合法(不归一化) |
| 1989 | ABC | 修正 Beckmann 的尾部 | 少用 |
| 2007 | GGX 命名 | Trowbridge-Reitz 形式在图形学界重新流行 | 实时标准 |
依然是一处容易被忽略的地方:有一点值得注意: GGX 不是 2007 年提出的新分布,而是 1975 年 Trowbridge-Reitz 分布的重命名。 中文社区的 NDF 谱系整理里这一点讲得比我更细 [15]。之所以值得强调,是因为它说明图形学里的"新方法"常常是旧方法在合适的时候被重新发现。
4.5 Beckmann 与 GGX 的差异:尾部
两个分布的关键差别在尾部。
用粗糙度的直观解释:Beckmann 的分布尾部衰减快,意思是"偏离宏观法线很远的微面"很少;GGX 的尾部更重,这类微面更多。
这个差异在画面上的表现:
- 用 Beckmann,高光会有一个明显的"边界",边缘外突然变暗
- 用 GGX,高光边缘拖着一条渐弱的尾巴,看起来更像真实材质
cstopics 的百科条目给了一张对比表,指出 Beckmann 尾部似高斯、离轴高光能量少;GGX 尾部更重、高光更自然,而 Blinn-Phong 则完全不满足能量守恒 [16]。这也是 GGX 成为事实标准的原因——它不是最精确的,但是它在这个权衡点上最合适。
4.6 源码链接
完整的 NDF 实现与验证可以参考两个地方:Filament 的材质文档给出了它选用的分布与参数 [17];Walter 等人的原始论文给了三类分布的完整数学形式与推导 [14]。
4.7 这一章留下的问题
$D$ 和 $F$ 都清楚了,但 $G$ 只说了"必须有"。它其实有两种近似,而这两种近似在掠射角的表现差别明显——而且混用会导致错误。下一章讲它,以及菲涅尔项里那个"两套 $k$ 映射不能混用"的坑。
五、几何项与菲涅尔:掠射角那点事
5.1 几何项在解决什么
先来看看,几何项 $G$ 描述的是微面之间的互相遮挡。当视线或光线接近掠射角时,微面之间会互相挡住,能有效参与反射的比例下降。
它要处理两类现象,名字很形象:
- 遮蔽(masking):出射方向被别的微面挡住
- 阴影(shadowing):入射方向被别的微面挡住
还有第三类——互反射(interreflection),也就是光在微面间弹多次才离开。它在第七章会成为一个主角,因为单散射模型正是把它当作不存在。
几何项在解决的遮挡关系,可以直接看到:
5.2 Smith 几何项的两种近似
工程上用的 $G$ 几乎都是 Smith 形式,而 Smith 形式有两种实现方式:
| 形式 | 思路 | 代价 | 掠射角表现 |
|---|---|---|---|
| 非相关式(separable) | 把遮蔽与阴影各自独立算再相乘 | 低 | 掠射角偏暗 |
| 相关式(correlated) | 假设遮蔽与阴影的高度相关,一并处理 | 略高 | 更接近参考解 |
两种近似的差别,体现在遮蔽与阴影的合成方式上:
相关式的一个常见实现是(社区推导里给了可直接对照的代码 [18]):
// 【Smith 几何项 · 相关式】用 lerp 在 α 上插值,比非相关式更接近参考解
float G2_GGX_corr(float NoL, float NoV, float alpha) {
float a = lerp(2.0 * NoL * NoV, NoL + NoV, alpha); // 【1】α 控制"相关程度"
return 0.5 / max(a, 1e-5); // 【2】已含 1/4 归一化的一部分
}
上面这段特别说明两处。
【1】处那个 lerp 是相关式的核心:$\alpha = 0$ 时退化为 $2 N\cdot L\, N\cdot V$(接近一条经验曲线),$\alpha = 1$ 时退化为 $N\cdot L + N\cdot V$。它把"完全相关"到"完全独立"用一个插值串起来了,这是相关式的工程价值。
【2】处那个 0.5 与 max 是两个细节:前者是归一化的一部分(不要在这里再乘一个 4);后者的下限保护不能省——掠射角下 a 会趋近零,不保护会出现无穷大,表现为屏幕上的白点噪点。
Filament 的做法稍有不同,它直接引入可见性函数 $V$ 并给出完整推导 [5];Patrick Bell 的推导文则把相关式与非相关式的对比代码都列了出来 [18]。两者对照着看,能很快建立直觉。
5.3 菲涅尔:为什么掠射角会亮
菲涅尔项 $F$ 描述"单个微面按入射角反射多少"。它的物理事实是:垂直看一个表面时反射很弱,斜着看时反射迅速增强。
OK,言归正传。拿水面举例:站在岸边俯视水面,能看到水下的石头(透射为主);贴着水面平视,水面像镜子一样反光(反射为主)。这就是菲涅尔效应。
真实的菲涅尔方程(Fresnel equations)依赖偏振与折射率,计算偏重。图形学里普遍用 Schlick 近似:
$$ F = F_0 + (1 - F_0)(1 - \cos\theta)^5 $$
其中 $F_0$ 是正入射时的反射率。那个五次方不是拟合出来的花招——它是 Schlick 观察真实曲线后选的一个"计算便宜且形状接近"的幂次。
这里用上了阶段 2 找回来的那张素材。 下图是 Wikimedia Commons 上维基百科条目所用的反射率曲线,横轴是入射角,纵轴是反射率:
菲涅尔在实拍里最好认——金属球的边缘明显更亮:
值得盯一眼曲线的形状:从 0° 到 60° 左右,反射率几乎平着走;60° 之后开始加速;到 85° 以上几乎是垂直上升。这个"前段平、后段陡"的形状解释了画面上的现象——掠射角出现的那一圈亮边不是渲染错误,是真实的物理行为。
菲涅尔的落地还牵涉高光模式的选择:
掠射角为什么变亮,这张图给出了几何解释:
忽略粗糙度时,菲涅尔曲线是这样的:
菲涅尔最直观的实例是水面——视线越接近掠射,反射越强:
同样的现象在低日照下更明显:
掠射角下的反射还会带上环境的颜色:
5.4 参数逐项释义:$F_0$ 的取值
先来看看,$F_0$ 是这一章最需要记住的参数,因为它是工业参数的锚点。
| 材质 | $F_0$(线性) | 说明 |
|---|---|---|
| 水 | 0.02 | |
| 塑料、玻璃(低反射) | 0.03 | |
| 塑料、玻璃(高反射) | 0.05 | |
| 多数介质(默认) | 0.04 | IoR≈1.5,MR 工作流里硬编码进 shader |
| 钻石 | 0.17 | 高折射率介质的代表 |
| 金属 | 0.66 – 1.0 | 用 base color 着色,故带颜色 |
这些数字在第九章还会用到,届时它们会变成"两套工作流怎么取舍"的判据。$F_0$ 的参考值在 learnopengl 的 PBR 教程里有线性与 sRGB 两列对照 [19];100x 计算机图形学指南第 29 章给了同样的表并补充了常见错误的症状特征 [20]。
5.5 一个具体的坑:两套 $k$ 映射不能混用
Schlick-GGX 的几何项里有一个 $k$ 参数,它的映射方式有两套:
$$ k_{direct} = \frac{(\alpha+1)^2}{8}, \qquad k_{IBL} = \frac{\alpha^2}{2} $$
- 直接光用 $k = (\alpha+1)^2/8$
- IBL 用 $k = \alpha^2/2$
learnopengl 在教程里明确点出了这个区别 [19]。但更值得注意的是 Karis 在 UE4 论文里的另一句提醒:GGX 的 $k = (\text{Roughness}+1)^2/8$ 重映射是给解析光源用的,用在 IBL 上会导致掠射角过暗 [4]。
这句话在中文教程里出现得很少。而它的后果很具体:IBL 环境下物体的边缘会有一圈不自然的暗边。 如果你遇到过"环境光下物体轮廓发暗"的现象,八成就是这里混用了。
5.6 六步闭环:本章的实现与效果
概念:几何项处理微面遮挡,菲涅尔项处理角度依赖的反射强度。
原理:Smith 形式把遮蔽与阴影建模为高度相关的统计问题;Schlick 用幂函数逼近菲涅尔曲线。
公式:见 5.2 与 5.3。完整的镜面项实现是三者相乘再乘归一化因子。
参数:$F_0$(介质 0.04、金属由 base color 给出)、$\alpha$(由 roughness 映射)、$k$ 的映射方式(区分直接光与 IBL)。
实现:
// 【镜面项:D · F · G 的完整形态】三项相乘,除以方向投影的归一化分母
float3 SpecularBRDF(float3 N, float3 V, float3 L, float3 F0, float alpha) {
float3 H = normalize(V + L); // 【1】半程向量
float NoL = saturate(dot(N, L));
float NoV = saturate(dot(N, V));
float NoH = saturate(dot(N, H));
float VoH = saturate(dot(V, H));
float D = GGX_NDF(NoH, alpha); // 【2】法线分布
float G = G2_GGX_corr(NoL, NoV, alpha); // 【3】几何项(相关式)
float3 F = SchlickFresnel(VoH, F0); // 【4】菲涅尔
return D * G * F / max(4.0 * NoL * NoV, 1e-4); // 【5】归一化分母 + 下限保护
}
特别说明三处。
【1】处所有三项都围绕半程向量 $H$ 计算——这是微平面理论"每个微面都是镜面"的直接推论:只有法线正好等于 $H$ 的那些微面,才能把 $L$ 方向的光反射到 $V$ 方向。
【3】处用的是相关式。如果换成非相关式,画面不会报错,只会在掠射角偏暗——这类错误最难查,因为它不崩溃、不闪烁,只是"看起来不太对"。
【5】处分母里的 max 下限保护同样不可省,理由与 5.2 节第【2】处相同。
效果:三种 $G$ 实现的精度-性能权衡,社区实现里有一份对照清单 [21]。
5.7 这一章留下的问题
到这里,单散射的 BRDF 已经完整了:漫反射项(Lambert 或 Oren-Nayar)+ 镜面项(D·G·F)。它能画出非常漂亮的材质球。
但它有一个系统性的错误——在高粗糙度下会丢失能量。而这个错误的成因,恰好就是 5.1 节提到但没有展开的第三类现象:互反射。下一章专门讲它。
六、单散射的缺口:多散射补偿
6.1 先把能量去哪了说清楚
好了,第五章结尾留下了一个悬念:单散射模型在高粗糙度下会丢能量。这一章就来算这笔账。
检验方法叫 furnace test(白炉测试):把一个物体放进一个颜色完全均匀、各方向亮度一致的白炉里,让它的材质设定为"完全漫反射白色"或"金属白"。如果渲染器是正确的,物体应该完全融入背景,看不出轮廓——因为反射的能量恰好等于收到的能量。
实际跑下来会发生什么?金属材质在粗糙度低时几乎无缝;粗糙度升到 0.8 以上,球体开始显出暗色的轮廓,越粗糙越暗。
说白了,为什么?回到微平面理论:微面之间会互相遮挡。被遮挡的那部分微面,单散射模型认为"它们不参与反射,直接丢弃"。但物理上,光碰到一个微面被挡住之后,会弹到相邻的微面上去,可能弹两三次才离开表面。
单散射只算了"弹一次就离开"的那部分,弹两次以上的一律没算。粗糙度越高,微面越倾斜,需要多次反弹的比例越大——所以损失也越大。
6.2 一个被量化的直觉
举个例子。有个现成的例子可以帮人建立直觉:同一块材质,抛光之后会变亮。
modelviewer.dev 的色调映射对照页里提到这个现象:深色哑光球看起来会比预期更暗——因为粗糙度高,微小腔体的遮蔽更强,能量损失更多;把同一个材质抛光(降低 roughness),它会变亮 [22]。
这句话解释了美术日常的一个困惑:"我什么都没改,只是把粗糙度调低了,怎么颜色变了?" 物理上颜色确实不该变(base color 是同一个值),但单散射模型的能量损失随粗糙度变化,所以观感变了。
6.3 Fdez-Agüera 2019:三类近似的补偿方案
2019 年,Fdez-Agüera 在 JCGT 上发表了一篇论文,专门解决实时渲染里 IBL 场景下的多散射能量补偿 [23]。这篇文章是本文的第二个"中文文章普遍不写"的点。
它的思路很实用:不去真正模拟多次反弹(那太贵),而是用单散射模型的预计算积分结果,反推出"丢了多少",再按一个系数补回来。
论文按物体类型分三类处理,因为三类的 Fresnel 行为不同:
| 类型 | Fresnel 行为 | 补偿思路 |
|---|---|---|
| 镜面反射体(specular conductor) | $F$ 接近 1,几乎全反射 | 补偿量最大,能量损失也最明显 |
| 通用金属 | 用 base color 着色的 $F$ | 按金属的着色特性补偿 |
| 介质(dielectric) | $F_0$ 很小(约 0.04) | 补偿量小,因为大部分能量本就以漫反射形式出来了 |
下面我们开始。用一句话概括这篇论文的贡献:它给了一个"用单散射算两次"的廉价补偿法——一次算常规的单散射结果,一次算"假设表面是纯白、Fresnel 为 1"的能量上限,两者之比就是丢失的比例。
6.4 参数逐项释义
多散射补偿引入的实际参数很少,但有一个概念性的参数需要理解:能量补偿系数的取值范围与它随粗糙度的变化。
补偿系数在粗糙度趋近 0 时趋近 1(不需要补偿,因为高光锐利、遮蔽少),在粗糙度趋近 1 时降到最低(补偿最多)。这个单调性可以用来验证实现是否正确:如果补偿后能量反而超标(furnace test 里物体比背景亮),说明系数算错了方向。
社区实现里对补偿后的效果有实测点评 [24],也用 furnace test 作为验证手段;另有一份推导把导体与介质的不同补偿思路分别展开了 [25]。
6.5 实现:能量补偿的代码形态
多散射补偿的代码通常长这样——注意它是乘在单散射结果上的一个补偿因子,不是另算一遍完整 BRDF:
// 【多散射能量补偿】用单散射的预计算积分反推丢失比例,再补回来
float3 EnergyCompensation(float3 F0, float NoV, float NoL, float alpha) {
// 【1】单散射下"已经收回来"的反射量:F0 提供的部分 + 常数项
float2 ab = BRDF_LUT_Sample(NoV, alpha); // 复用 BRDF LUT 的 A/B
float E = ab.x * F0.r + ab.y; // 【2】F0 与白色环境的插值
// 【3】Fdez-Agüera 的补偿因子:E 越接近 1,说明单散射越完整,补偿越少
float Ems = 1.0 - E; // 丢失比例
float Favg = F0.r + (1.0 - F0.r) / 21.0; // 【4】Fresnel 的平均值(含 1/21 常数)
return F0 * (1.0 + Ems * Favg / max(E, 1e-4)); // 【5】补偿后乘回 F0
}
这段特别说明四处。
【1】处复用 BRDF LUT 是关键——多散射补偿不需要额外的预计算资源,直接借用第九章要讲的 BRDF LUT 的 A/B 两个通道。这也是它能被广泛采用的原因:零额外显存。
【2】处把 $F_0$ 与白色环境做线性插值,得到"当前 $F_0$ 下已收回的能量占比"。$F_0 = 0$(纯介质)时只剩 bias 项,$F_0 = 1$(理想导体)时是 scale 加 bias。
【4】处那个 1/21 是个有来历的常数。它是 Schlick 菲涅尔在半球上的积分平均值:对 $(1-\cos\theta)^5$ 做半球平均,结果是 $1/21$。所以 $F_{avg} = F_0 + (1-F_0)/21$ 表示"平均反射率"。这个数不能随手写成 1/20 或 1/22。
【5】处的结构值得注意:补偿是乘性的,作用在 $F_0$ 上,而不是加在最终颜色上。这样能保证补偿后的能量仍然随材质颜色变化,不会凭空变灰。
6.6 六步闭环:本章的效果
概念:单散射丢失的微面间多次反弹能量,在高粗糙度下显著。
原理:用单散射的预计算积分反推丢失比例,按物体类型的 Fresnel 行为补偿。
公式:见 6.5 的代码,核心是 $1 + E_{ms} F_{avg}/E$ 这个补偿因子。
参数:$F_{avg}$ 中的 $1/21$(Schlick 的半球平均)、补偿系数随粗糙度的单调性。
实现:见上,零额外显存开销。
效果:补偿后 furnace test 里粗糙金属球的暗轮廓消失;实测点评见 [24]。
一处诚实的说明:多散射补偿是近似,它不能恢复百分之百的能量。论文里给出的偏差在可接受范围内,但它不会让 furnace test 完全无缝。如果你在项目里看到"极粗糙金属仍有轻微偏暗",这是预期的,不是实现错误。
6.7 这一章留下的问题
到这里,直接光照这条路已经完整了——漫反射、镜面反射、几何项、菲涅尔、多散射补偿。
OK,言归正传。但直接光照只解决了"光源直接照到"的情况。现实世界里,物体受到的光大部分来自环境:天空、地面、周围的墙面。这些光没有明确的方向,不能当解析光源处理,数量上也不可能逐个遍历。
下一章讲业界怎么把"整个环境"当成一个光源来算。
七、IBL:把环境光变成可预计算的卷积
7.1 基本思路:把环境贴图当成一张光的方向图
下面我们开始。IBL(Image-Based Lighting)的核心想法只有一句话:既然环境光从四面八方来,那就用一张环境贴图(通常是等距柱状投影的全景图或 cubemap)记录下每个方向的亮度,把它当成光源。
下面两张是阶段 2 找来的真实全景图,它们就是 IBL 的输入长什么样:
可作为 IBL 光源的等距柱状实拍全景:
环境贴图在内存里是这样展开的:
它映射回场景后成为光照的来源:
再多一张不同光照条件的环境贴图:
把两张图对比着看,能立刻理解为什么环境光不能用一个常数表示:室外图里上方(天空)明显亮于下方(地面);室内图里窗户位置有几个极亮的斑点。如果用同一个常数当环境光,这两个场景会渲染成一模一样的漫反射底色。
但问题也立刻来了:渲染方程要求对整个半球积分,现在半球上每个方向都有一个值,这个积分怎么算?
如果每个像素实时采样——比如采 1024 个方向、每个方向查一次贴图——成本是每像素上千次纹理采样。完全没有可接受性。
IBL 的第一步是把环境贴图当成一张“光从哪个方向来”的方向图,等距柱状投影的全景图就是这个形式:
换一个场地看,同一格式下的光照分布差异很明显:
7.2 split-sum:Karis 2013 的两项分解
2013 年,Brian Karis 在 SIGGRAPH 的 PBS 课程里给出了一个方案,标题是《Real Shading in Unreal Engine 4》[4]。这份笔记是本文第三个"中文文章普遍不写"的点的来源——因为它自己在论文里说清了误差来源的排序,而中文文章基本只转了公式。
它的做法叫 split-sum approximation(分割求和近似):把镜面积分的复杂度从"实时光照下不可算"降到"两次查表"。
原始形式(对某一点的出射辐射度):
$$ L_o = \int_{\Omega} f_r(l, v)\, L_i(l)\, (n \cdot l)\, dl $$
Karis 把它拆成两个积分的乘积:
$$ L_o \approx \underbrace{\int_{\Omega} f_r(l, v)\, (n \cdot l)\, dl}_{① 预滤波环境贴图} \times \underbrace{\int_{\Omega} f_r(l, v)\, (n \cdot l)\, dl \Big|_{L_i = 1}}_{② BRDF LUT} $$
说人话:第一个积分假设入射光是常量,只算 BRDF 的形状;第二个积分假设环境是纯白的,只算 BRDF 的强度。两者相乘近似原来的积分。
这个近似的精度,Karis 自己在论文里给了评价:对常量入射光是精确的,对常见环境相当准确 [4]。
为什么它可以被预计算——这就要回到第二章提到的 BRDF 线性性质。因为 $f_r$ 与入射光无关,所以两个积分里与光无关的部分都可以提前算好:
- 第一项只依赖粗糙度与视角,可以按粗糙度分级预滤波,存进 cubemap 的 mip 层级
- 第二项只依赖粗糙度与 $\cos\theta_v$,可以做成一张二维查找表
运行时每像素只做几次纹理采样,成本从"上千次"降到了"几次"。预滤波与 BRDF LUT 的完整推导过程另有两份同源材料可对照:课程笔记镜像与演讲 slides [107] [108]。
7.3 $N=V=R$:比 split-sum 更大的误差源
这一节是本文最想强调的地方之一。
下面进入。为了让第一项可预计算,Karis 引入了一个假设:$N = V = R$,也就是"法线、视线、反射方向三者重合"。物理上这意味着"垂直观察一个平坦表面",此时反射方向恰好等于视线方向。
代价是什么?论文里的原话是:掠射角处得不到细长反射(don't get lengthy reflections at grazing angles) [4]。
而更重要的是论文紧接着的那句评价——它明确说明 $N=V=R$ 的各向同性假设"是 IBL 方案中比 split-sum 更大的误差源" [4]。
这句话的含义值得展开:业界普遍担心的是 split-sum 这个数学近似的精度,但论文作者自己认为,真正的误差大头在 $N=V=R$ 这个几何假设上。
好了,这条信息在中文文章里几乎看不到。 我逐条核验了 Karis 的原始 PDF 才确认这句话的存在。它的实际影响是:如果你在调一个"反射看起来不够长、不够拉丝"的金属材质,问题不在 split-sum,而在 $N=V=R$。 补偿手段是加屏幕空间反射(SSR)做补充,或者用抛物面/视差校正的探针,而不是去提高预滤波的分辨率。
7.4 一个容易被忽略的细节:$\cos\theta_l$ 加权
预滤波采样时,Karis 是按 $\cos\theta_l$ 加权的 [4]。这个加权不在简化的公式里,是工程实现时补上的。
为什么要加?因为渲染方程里本来就带着 $(\omega_i \cdot n)$ 这个余弦因子,而预滤波阶段如果用 GGX 重要性采样直接平均,会把斜向的样本与正对的样本等权看待——这与原方程的加权方式不一致。补上 $\cos\theta_l$ 之后,预滤波的结果才与"按渲染方程积分"接近。
这类细节的特点是:不看原始论文就不知道,而不知道也不会报错。
7.5 漫反射那一路:辐照度卷积与球谐
下面我们开始。上面讲的都是镜面反射。漫反射那一路相对简单,但也有自己的选择。
方案一:辐照度卷积(irradiance convolution)。 把环境贴图对每个方向做半球积分,得到一张低频的"辐照度图"。运行时只需一次采样。learnopengl 的教程把这一步讲得比较清楚 [26]。
代价是:卷积需要预计算,且辐照度图是全局的——它不知道"这个点被墙挡住了"。
方案二:球谐函数(Spherical Harmonics)表示。 2001 年,Ramamoorthi 和 Hanrahan 的论文给出了一个结论:辐照度环境图用球谐表示时,只需要 3 个频带(9 个系数)就能达到平均误差低于 3% [27]。
这意味着什么?把整个环境的光照信息压缩成 9 个浮点数,就能重建出漫反射光照,而且误差不到 3%。这是 IBL 之所以能在移动端和游戏主机上大规模使用的基础——内存开销从"一张 cubemap"降到了"36 字节"。
引擎里环境光的有无,对照非常明显:
Unity 的光照探针(Light Probes)就是这个技术的直接落地,其技术文档明确说明 SH 重建采用的是 Peter-Pike Sloan 的《Stupid Spherical Harmonics (SH) Tricks》记号,早期基于 Ramamoorthi/Hanrahan 的工作 [28]。WebXR 的光照估计规范也把 SH 作为工程标准:最多 9 个 SH 系数 × RGB = 27 个浮点数 [29]。
一份 GDC 2003 的课程讲义(Ramamoorthi 参与)里,对频域方法与 SH 卷积有系统综述 [30]。
辐照度卷积的结果是一张低频图:
辐照度卷积要算的是半球上所有方向的加权和,采样方式是这样的:
换成另一个法线方向,积分域随之旋转:
把这件事写成积分,就是一个在球面上的卷积:
7.6 这一章留下的问题
原理讲完了:split-sum 拆成两项,一项预滤波、一项查表;漫反射那一路可以用 SH 压到 9 个系数。
但工程上还有一堆具体问题:预滤波的 mip 链怎么组织?BRDF LUT 的两个输出怎么存、用什么精度?环境是一个立方体、物体在中间移动时视差怎么办?探针之间怎么混合?
下一章就是这些落地问题。
八、预滤波、BRDF LUT 与探针
8.1 预滤波环境贴图:mip 链与粗糙度的对应
下面进入。第一项积分的实现方式是把环境贴图按粗糙度分级预滤波,每一级对应一个 mip 层。
具体做法:
- mip 0 存原始的、最高频的环境贴图,对应 roughness 0——清晰的镜像反射
- mip 1、2、3…… 依次存储用不同粗糙度卷积后的结果
- 运行时按材质的 roughness 选择 mip 层级,mip 之间做三线性插值
为什么用 GGX 分布做卷积而不是简单地降低分辨率——这是关键区别。简单降分辨率得到的是"高斯模糊",而 GGX 重要性采样得到的是"物理上正确的粗糙反射"。两者的差异在高光边缘处看得出来:高斯模糊的高光边缘是均匀渐晕,GGX 则拖着一条符合菲涅尔规律的尾巴。
预滤波的结果是一条 mip 链,粗糙度逐级递增:
粗糙度对反射的影响,引擎里可以直接对照:
预滤波要处理的正是这个镜面波瓣:
预滤波实现不当会在 mip 之间留下接缝:
learnopengl 的镜面 IBL 教程详细讲了预滤波的实现步骤 [31];有一篇 2025 年的笔记(时效较新)把 GGX 重要性采样与蒙特卡洛的关系讲得更清楚 [32];另有一篇把"按 roughness 存 mip、$N=V=R$ 假设"的来龙去脉梳理了一遍 [33]。
预滤波要靠重要性采样把样本集中到高贡献方向,PDF 就是它的分布依据:
按这个分布采出来的样本落点是这样, roughness 越大分布越散:
8.2 BRDF LUT:两个输出与 R16G16 精度
第二项积分的实现是一张二维查找表。
下面进入。它的输入输出关系值得记牢:
| 内容 | |
|---|---|
| 输入 1 | roughness |
| 输入 2 | $\cos\theta_v$ |
| 输出 1 | scale(对 $F_0$ 的缩放系数,常记为 A) |
| 输出 2 | bias(常数偏移,常记为 B) |
为什么只有两个输出。 因为 $F_0$ 可以被提到积分外面。回到 Schlick 近似 $F = F_0 + (1-F_0)(1-\cos\theta)^5$,把它代进第二项积分后会变成 $F_0$ 的线性函数:
$$ \int f_r (n\cdot l)\, dl \Big|_{L_i=1} = F_0 \cdot A + B $$
于是那张表只需要存 $A$ 和 $B$ 两个值。这就是它叫"Environment BRDF"或"BRDF LUT"的原因。
Karis 在论文里特别强调了一点:这张表的精度很重要,他用的格式是 R16G16 [4]。原因也不难理解——$A$ 与 $B$ 的取值范围不同,且在小 roughness 区域变化剧烈,低精度格式会在这里产生可见的条带。
split-sum 的第二项就是这张预先算好的查找表:
BRDF LUT 的积分依赖低差异序列采样:
生成这张表的代码(IntegrateBRDF)在 Karis 的论文附录里给了完整 HLSL,社区也有把这段代码整理出来的实现 [34] [109]。
8.3 实现:运行时如何组合
把两项合起来,运行时的镜面 IBL 代码大致是这样:
// 【镜面 IBL 运行时】两次查表相乘,再乘上环境贴图的贡献
float3 SpecularIBL(float3 N, float3 V, float3 F0, float roughness, float2 ab) {
float3 R = reflect(-V, N); // 【1】按 N=V=R 假设取反射方向
float3 pre = PrefilteredEnvMap.SampleLevel(sampler, R, roughness * MAX_MIP); // 【2】查预滤波 mip
float2 lut = ab; // 【3】BRDF LUT:A 与 B
float3 spec = pre * (F0 * lut.x + lut.y); // 【4】scale / bias 组合
return spec;
}
特别说明三处。
【1】处那个 reflect(-V, N) 正是 $N=V=R$ 假设的直接体现——代码里连视线方向都没用上,只用了法线。这是性能上的巨大优势(一次反射计算 + 两次采样),但代价就是 7.3 节说的"掠射角得不到细长反射"。
【2】处用 roughness * MAX_MIP 选 mip 层,然后靠硬件的三线性插值在相邻 mip 之间过渡。这个映射不是线性的——如果直接线性映射,低粗糙度区域会出现明显的层级跳变,通常需要一个非线性重映射。
【4】处 F0 * lut.x + lut.y 就是 $F_0 \cdot A + B$。这三个量都是 float3(RGB 分通道),因为金属的 $F_0$ 是带颜色的。
两项合起来的结果是这样:
换一套环境贴图,同一材质的观感随之改变:
8.4 探针、视差与 box projection
OK,言归正传。到目前为止的讨论都假设"环境在无限远处",所以反射方向只看法线、不看位置。但游戏场景里的反射常常来自一面近处的墙或一个房间。
这就引入了反射探针(reflection probe)与视差校正。
三大引擎的做法对照:
| 引擎 | 探针机制 | 视差校正 | 混合策略 |
|---|---|---|---|
| Unreal | Reflection Capture(Sphere / Box)+ SkyLight | 把捕获的 cubemap 重投影到简单形状获得近似视差 | 小 capture 覆盖大 capture;粗糙表面用 lightmap 间接高光混合(roughness 0.3 全混合 → 0.1 以下不混合) |
| Unity | Reflection Probe(Baked / Custom / Realtime) | Box Projection + Box Size + Box Offset | Importance + Blending 权重混合;Cubemap 支持 HDR(OpenEXR / PNG) |
| Godot | Reflection probes | Box Projection 开关 + Extents / Origin Offset | 按包围盒与优先级 |
数据来源分别是 Unreal 的 Reflections Captures 官方文档 [35]、Unity 的 Reflection Probe 手册 [36] 与 Godot 4.0 的文档 [37]。
box projection 的原理值得单独说一下,因为它是这三家里最容易被误解的机制。
核心问题是:cubemap 是在某个位置捕获的,如果被观察的物体移动了,反射就"错位"了——本该映出墙面的方向可能映出天空。
box projection 的做法是:把反射方向与一个包围盒求交,用交点作为采样方向。
这样就得到了近似正确的视差效果——物体靠近盒子边缘时,反射内容会相应地"滑动"。
引擎里承接 IBL 的实体是反射探针:
与探针互补的是屏幕空间反射:
社区有一个改进实现值得参考:它指出经典 box projection 在盒子边角处的不足,并引入 contact hardening(接触硬化)改善掠射角附近的反射,同时给出了 Unity 默认实现与 Godot 近似做法的对比代码,称其"比经典方法有大的改进,且很便宜" [38]。
Unity 的 HDRP 里另有独立的反射探针参数体系 [39];要一份从"环境贴图当无限远光源"讲起的综述,可以参考一篇 DX12 的实现笔记 [40]。
室内场景是探针最容易露馅的地方——墙与地面把环境切成几个区域,box projection 就是为这种情形准备的:
8.5 一个实现细节:探针混合的代价
好了,探针数量上去之后,混合本身会变成开销。
Unreal 的处理方式能说明问题:它不追求"所有探针都参与混合",而是让小 capture 覆盖大 capture,同时给粗糙表面设了明确的混合阈值——roughness 0.3 以上完全用 lightmap 的间接高光,0.1 以下完全用反射探针,中间过渡 [35]。
这个阈值设计背后有一个物理直觉:粗糙度高的表面,反射本身就很模糊,用哪一个探针的结果差别不大,用便宜的那个就行。所以把"贵的那条路"限制在低粗糙度区间,是性价比最高的分法。
这段的实用价值:如果你在做探针系统,不要一开始就追求"无缝混合所有探针"。先看粗糙度分布——如果场景里大部分是高粗糙材质,那么优化重点不在探针混合,而在环境贴图的 mip 质量。
8.6 这一章留下的问题
到这里,从渲染方程到 IBL 的整条技术链走完了:BRDF 三项、多散射补偿、split-sum 两项、SH 压缩、探针视差。
但还有最后一块没碰——这些物理量到了美术手里,变成什么参数? 一个材质在 glTF 里怎么描述、在 Substance 里怎么画、在 Unity 和 Unreal 里怎么落?这些约定之间的差异,是实际项目里出错最多的地方。下一章讲这块。
九、工业实践:两套工作流与参数取值
9.1 两套工作流的分野
下面我们开始。前面八章讲的都是物理。这一章讲约定——这些物理量到了美术手里,被组织成什么参数。
历史上存在两套并行的工作流:
| 金属度-粗糙度(MR) | 镜面-光泽度(SG) | |
|---|---|---|
| 核心参数 | baseColor / metallic / roughness | diffuse / specular / glossiness |
| 金属表现 | 由 baseColor 与 metallic 共同决定 | 直接由 specular 给出 |
| 反射颜色 | metallic 混合 baseColor 得到 | specular 本身就是颜色 |
| 直观性 | 参数少,但"金属"是派生量 | 更直接,但易出现物理上不可能的取值 |
| 出错方式 | 金属/非金属边界难画 | 可以画出能量超标的材质 |
两套工作流的转换关系(社区整理得比较清楚 [52]):按 specular 的亮度判定像素是不是金属——specular 亮而 diffuse 暗的是金属,反之为非金属;金属的 base color 取 specular map,非金属取 diffuse map;roughness 与 glossiness 的关系是:
$$ \text{roughness} = 1 - \text{glossiness} $$
引擎里的着色模式切到带贴图的一档,两套工作流的差异会更清楚:
9.2 glTF 2.0 的收敛:SG 已被归档
先来看看,这里有一条多数中文文章没更新的信息。
glTF 2.0 于 2017 年发布时就把 MR 定为默认材质模型;随后几年里 SG 扩展被大量使用,但KHR_materials_pbrSpecularGlossiness 扩展如今已经被移入 Archived/ 目录,官方不建议新资产使用 [41]。官方给出的替代路径是 KHR_materials_specular 配合 KHR_materials_ior [42]——用一种更受约束的方式表达旧 SG 想表达的东西。
为什么值得单独强调:很多教程仍把 MR / SG 描述成"两个可选工作流,看团队习惯"。但规范层面的选择已经做了——MR 是默认,SG 是历史。
这件事在实践中的体现是:如果你现在从零开始做一套材质管线,选 SG 意味着走一条正在被淘汰的路。已有的 SG 资产需要转换,而转换是有损的(金属掩码的判定、glossiness到roughness的映射都会引入偏差)。社区里有具体的迁移案例记录了这些偏差 [87]。
9.3 参数逐项释义:F0 取值带与 metallic 的二值性
这一节是本章的核心,也是阶段 2 找到的那两张通道图真正发挥作用的地方。
先看 metallic 通道。这一张来自一份金属走道(steel walkway)材质的 PBR 数据——
再看同一份材质的 roughness 通道:
粗糙度贴图则是一张灰度图,逐像素控制高光收敛程度:
两张图放在一起,metallic 该怎么处理就一目了然了:现实材料要么是金属,要么不是,不存在 0.5 的金属。所以 metallic 应当作 0/1 处理,中间值只会出现在"材质过渡区"这种需要混合的少数位置。
把整个参数空间摊开来看更直观——下面这张来自 Blender 官方手册:上排是 roughness 从 0 到 1 的连续变化,下排是 metallic 从 0 到 1 的变化。同一 base color、同一光照条件下,两个参数各自独立地改变着反射形态:
贴图集里的基础色通道,实际是一张灰度无关的彩色图:
Filament 的材质文档把这条写成了明确的范围约定 [17]:
| 参数 | 取值范围 | 说明 |
|---|---|---|
| 金属 base color | [0.66, 1.0](sRGB 约 170–255) | 低于这个范围说明值不对 |
| 介质 base color | [0.04, 0.94] | 上限 0.94 对应"最亮的非金属" |
| metallic | 0 或 1 | 应作二值处理 |
工业界的 PBR 贴图集长这样:
再看实录材质能对上多少:
这三张图说明的是同一件事的两面:base color 的取值区间很宽(0.04–0.94),但决定"看起来像什么"的不只是 base color——第二张图与第一张 base color 接近,观感差异全在涂层带来的低粗糙度高光上。这也是为什么调材质时不能只盯 base color 一个参数。
金属那边则是另一种规律:
真实材质的粗糙与锈蚀,是粗糙度贴图要还原的对象:
铜那张解释了 SG 工作流为什么容易出错:在 SG 里,反射颜色是直接给的值,美术可以把一个"金属"的反射调成任意颜色;但在物理上,金属的反射颜色就是它的 base color,铜之所以反射偏暖,是因为它吸收了蓝光——这不是一个可以随便调的艺术参数。
介质那边同样有对应的实拍:
把陶瓷两张与木材三张对照着看,可以得到一条实用判断:同一个 base color 值配不同 roughness,观感差异可能大于不同 base color 配同一 roughness。所以材质制作里 roughness 的精度往往比 base color 更值得投入。
一个常被忽略的材质层——清漆:
同一套贴图里的金属度通道,是灰度图:
法线通道同样来自同一套扫描贴图:
ORM 打包是工业管线的常见做法,引擎里是这样组织的:
金属材质的一整套通道贴图摊开来看是这样:
其中最说明问题的是金属度通道本身——它整张是纯白的:
9.4 线性与 sRGB:那张最容易搞错的约定
好了,这一节的内容看着基础,但它是实际项目里出错最多的环节。
glTF 规范对纹理编码有明确要求 [43]:
| 数据 | 编码 | 原因 |
|---|---|---|
| baseColorTexture 的 RGB | sRGB | 它是颜色,人眼感知是非线性的 |
| baseColorFactor | 线性 | 它是乘子,不是颜色 |
| roughness / metallic | 线性(non-color data) | 它们是物理量,不是颜色 |
| normal / AO | 线性 | 同上 |
最容易漏的是中间两行。 很多教程讲"记得把 albedo 设成 sRGB"就结束了,但没有提"roughness 和 metallic 必须设成 linear/non-color"。如果导入时把 roughness 贴图也按 sRGB 采样,结果会明显偏——因为 sRGB 解码会把中间灰值整体压低。
这条有个专门的术语:non-color data(非颜色数据)。一份少见的中文整理明确把这个边界讲了出来 [51]。
如果你遇到"同一个材质在 Blender 里好好的,导到 Unity 就偏了",先查的就是这里。社区里有一份跨端实测对照表,把 Blender / Unity / Three.js 三端的金属度输出逐档列了出来,还包含 KHR_texture_transform 坐标翻转与法线 Y 通道反转的排查 [53]。
线性、非线性与 HDR 的关系,一张图就能说清:
同一套通道在色彩空间上却是分家的:Color 是 sRGB,其余通道都是线性数据:
9.5 PBR Neutral:为材质保真设计的色调映射
第九章写到这儿插一个和本系列第三篇直接呼应的话题。
OK,言归正传。第三篇里讲过色调映射(tone mapping)——它的任务是把高动态范围压进显示范围。当时讲的是"怎么压得好看"。但 2024 年 Khronos 提出的 PBR Neutral,目标不一样:它要压完之后颜色还对。
规格里给了几个关键常数 [45]:
| 常数 | 值 | 含义 |
|---|---|---|
| $F_{90}$ | 0.04 | IoR=1.5 的介质正入射菲涅尔,也就是第九章 9.3 节那个 0.04 |
| $K_s$ | $0.8 - F_{90}$ | 高光开始压缩的起点 |
| $K_d$ | 0.15 | 去饱和速度 |
核心承诺是这一条:对 $0.08 \le R,G,B \le 0.8$ 的输入,
$$ \text{cout} = \text{cin} - F_{90} $$
也就是成品的 base color 被精确复现(减去一个固定偏移)。而且它没有色相偏移(no hue shift)、可逆。
注意它明确不做 gamut mapping [45],并假设输入与光照都是 Rec.709(与 glTF 一致)。这说明了它的定位:不是通用色调映射器,而是电商/建筑/CAD 场景下"材质颜色不能变"的专用方案。 商业落地的报道提到它已被主流 3D 工具与引擎广泛采用 [46]。
这条和本系列第三篇的接口在这里:第三篇讲的色调映射决定了下游材质能否保真;如果你做的是商品展示类应用,选错了色调映射,前面八章把材质算得再准也没用——它最后会在色调映射那一步被改掉。
9.6 HDRI 采集与材质扫描:一手规范长什么样
下面进入。最后讲素材从哪来。这一节的资料里,Poly Haven 的技术标准是最值得看的一份,因为它是少见的公开写明的采集规范 [57]:
| 要求 | 值 | 理由 |
|---|---|---|
| 是否接受渲染生成的 HDRI | 不接受,仅收真实照片 | 保证是"真实世界的光" |
| 最小水平分辨率 | 16K | 低于此在光滑材质上会看出模糊 |
| 是否允许裁剪 | 必须 unclipped | 太阳等高亮度源被裁掉就丢了能量 |
| 色卡 | 需同曝光包围单独拍 Macbeth 色卡 | 用于色彩标定 |
| 后期 | 禁止 tonemap / 曲线 / 锐化,全线性流程 | 任何非线性操作都会破坏线性性 |
| 内容 | 需去除三脚架与底部阴影 | 否则会出现不存在的暗斑 |
细节来自法线贴图——它把高模的法线烘到低模上:
应用之后,平坦表面立刻显出凹凸:
扫描材质库里的样例,能同时看到多种贴图的合成效果:
"必须 unclipped"这一条最容易被忽略。裁剪过的 HDRI 看起来正常(画面里那个白斑本来也看不出层次),但它丢掉的是光照能量——用这种 HDRI 照亮的场景会整体偏暗,而且高光滚降会不自然。
工业管线里的基础色贴图,原始规格是这样的:
还有一张常被忽略的高度图:
同一材质的成品渲染,是这些通道合成的结果:
另一套 CC0 材质库的基础色贴图:
法线贴图在引擎材质里的接入方式:
高度/置换在引擎里需要显式开启:
需要更多可用的环境贴图时,Commons 上有三个成规模的分类可以直接取用,均已按许可标注 [104] [105] [106];另有几张单张全景可作为备选,例如天空占比大的山地全景与两张教堂室内 [110] [111] [112]。
材质扫描(photoscanned texture)是另一条路:RAW 显影与色彩标定 → 摄影测量重建 → 生成 roughness / normal / displacement / AO [59] [60]。ambientCG 把资产按来源分三类:Approximated(仅色图近似)、Photogrammetry(位移贴图来自摄影测量)、Procedural(全程序化)[62]。
许可方面要分清:Poly Haven 全部资产是 CC0,含商用、可无署名,且明确许可用于 AI 模型训练 [61];ambientCG 的素材同样是 CC0(本次用到的通道贴图就来自它)。这与 Commons 上的 CC BY-SA 系列不同——CC BY-SA 要求相同方式共享,用在商业产品里需要谨慎评估 [63]。
顺便说明本文素材的许可分层:题图与 9.3 节的通道图来自 ambientCG(CC0);木材、陶瓷、皮肤、金属实拍来自 Commons,分别是 CC0 / CC BY / CC BY-SA;两张全景图是 CC BY-SA 3.0 / 4.0。所有外部素材的原始页面与许可以 §图片版权 那张表为准。
9.7 落地检查清单
把这一章的要点收成一张可执行的清单:
| # | 检查项 | 不合格的表现 |
|---|---|---|
| 1 | metallic 是否作 0/1 处理 | 大面积出现"半金属"灰值 |
| 2 | 金属 base color 是否落在 0.66–1.0 | 金属看起来发灰、不像金属 |
| 3 | 介质 base color 是否落在 0.04–0.94 | 出现"纯黑非金属"(物理上不存在) |
| 4 | roughness / metallic 贴图是否设为 non-color | 整体偏暗,中间调被压低 |
| 5 | base color 是否按 sRGB 采样 | 整体偏亮、对比度异常 |
| 6 | IBL 的 $k$ 映射是否与直接光分开 | 环境光下物体轮廓出现暗边 |
| 7 | 是否做了多散射能量补偿 | 高粗糙度金属偏暗 |
| 8 | 色调映射是否保真(商品展示场景) | base color 在中高亮度区偏移 |
环境光遮蔽贴图常被误当成阴影,其实它是遮蔽信息:
落地检查清单里必须核对的一项——AO:
还有一个进阶通道——弯曲法线:
这份清单可以直接当验收标准用——八项里前五项是数据约定、后三项是渲染管线配置,覆盖了实践中最常见的失效模式。
十、结语:材质保真,以及三个未解的问题
10.1 主线的收口
好了,回到开头那个问题:从渲染方程到一个可交付的材质管线,中间每一步做了什么近似,各自在什么时候会露馅?
把十章的答案串起来:
| 章 | 那一步近似 | 换来了什么 | 在哪里露馅 |
|---|---|---|---|
| 一 | 切断间接光的递归,只算直接可见 | 实时可算 | 缺少真实的间接光,需要探针/SSR 补 |
| 二 | BRDF 只用四条性质约束,不追求物理完备 | 参数可管理 | 违反能量守恒的模型混进来(Phong) |
| 三 | 粗糙漫反射用 V 形凹槽简化 | 一个好用的解析式 | 到 2024 年才修好能量守恒 |
| 四 | NDF 选 GGX 而非最精确的分布 | 高光边缘自然 | 尾部重导致某些掠射角略偏 |
| 五 | Smith 用相关式近似、菲涅尔用 Schlick 五次方 | 便宜 | 混用两套 $k$ 会让掠射角过暗 |
| 六 | 多散射用"算两次"补偿 | 零额外显存 | 补不满,极粗糙仍有轻微偏暗 |
| 七 | split-sum 拆两项 + $N=V=R$ | 每像素两次查表 | 掠射角丢细长反射(比 split-sum 更大的误差源) |
| 八 | 环境分 mip、SH 压到 9 系数、box projection | 内存与性能可控 | 视差只是近似,边角处会错位 |
| 九 | 参数收敛成 MR + 一条取值带 | 美术能调、跨引擎能迁 | 编码约定搞错会让全部前功尽弃 |
这张表是本文最想留下的东西。 每一行都对应一个"看起来能用、但知道边界才能用好"的近似。把它记住,比记住任何一个公式都有用。
10.2 三个未解的问题
OK,言归正传。按系列惯例,结语要说清"哪些是已经解决的、哪些还没有"。
第一,$N=V=R$ 没有便宜的替代方案。 它被论文作者自己判为比 split-sum 更大的误差源,但至今没有一个"同样便宜且准确"的方案。SSR 能补充屏幕内的反射,屏幕外依然要退回到这个假设。
第二,多散射补偿仍是近似。 2019 年的方案很实用,但它补不满。真正的多次反弹需要更贵的模型(多层材质、光线追踪),实时预算下还没有普遍可行的做法。
第三,材质制作的效率问题没解决。 参数收敛到了三个(base color / roughness / metallic),听起来够简单,但实际上美术仍要画贴图、要理解取值带、要处理编码约定。9.7 节那张清单里的八项,每一项都有人在踩。
这三个问题的共同点:都不是"算得不够快",而是"近似还没找到更好的平衡点"。
10.3 选型建议
下面进入。如果要在项目里做取舍,按场景分三类:
- 移动端 / VR:SH 探针(9 系数)+ 单张预滤波 cubemap,跳过 $N=V=R$ 之外的复杂探针;多散射补偿建议开,它几乎不花钱
- 3A / PC:完整 IBL + 反射探针 + box projection + SSR 补充,多散射补偿必开
- 商品展示 / 建筑可视化:优先选 PBR Neutral 这类保真色调映射,材质的颜色准确性比渲染的真实感更重要
一句话总结这三类的差别:游戏里"看起来对"就够了,商品展示里"测得准"才是要求。这两者对近似的容忍度完全不同——前者可以接受 $N=V=R$,后者不能接受色调映射带来的偏色。
10.4 资源清单
一手文献:Karis 的 UE4 笔记 [4](本篇的 IBL 主线来源,也是"$N=V=R$ 是更大误差源"那句的出处);Fdez-Agüera 的多散射论文 [23];Ramamoorthi & Hanrahan 的辐照度环境图论文 [27];Walter 等人的微平面论文 [14];Burley 的 Disney BSDF 续作 [96]。
引擎与规范文档:Filament 的着色文档与材质指南 [5] [17];glTF 的材质规范与扩展 [41] [42] [43];Khronos 的 PBR Neutral 规格 [45];Unreal / Unity / Godot 的反射探针文档 [35] [36] [37]。
教程与综述:learnopengl 的 PBR 与 IBL 三篇 [19] [26] [31];pbrt 的反射模型章节 [8];一份 GDC 2003 的球谐综述 [30];社区整理的 PBR 资料索引 [74] [75]。
素材规范:Poly Haven 的 HDRI 技术标准与材质扫描流程 [57] [59] [60];ambientCG 的资产来源分类 [62]。
竞品与中文资料:需要横向对照的话,浅墨的 PBR 白皮书是中文圈体系最完整的一套 [65];以 SIGGRAPH 课程史为线索的 PBR 进入实时渲染的过程见 [66];对 Karis 论文的中文解读见 [67];IBL 的中文整理见 [68] [70]。本文与它们的差异集中在两处:渲染方程到 BRDF 的数学链条、以及 IBL 的误差来源排序。
延伸阅读:本文未逐条引用、但值得一读的还有:pbrt 的参考文献页与在线全文 [99] [100];Unreal 官方的 Physically Based Materials [101];一组"误解"专题(多散射、多层材质、斜率空间)[102];Godot 的特性总览(Panorama Sky 与 radiance map 的用法)[103];Games101 的材质与外观讲义 [97];以及一篇把渲染方程一路推到 Schlick-GGX 的实现笔记 [98]。
10.5 下篇预告
下一篇是本系列的收尾:《可见性与剔除:从视锥到 GPU-Driven》。
它会回到第一篇开头那个问题——场景里有几百万个物体,GPU 怎么知道该画哪些?从最朴素的视锥剔除,到遮挡剔除、层次 Z 缓冲、再到把剔除整个搬到 GPU 的 GPU-Driven 管线。
与前三篇的接口:第一篇的 G-Buffer 提供了本篇 BRDF 所需的材质参数;第二篇的阴影贴图与本篇共享"半球积分不可解、只能采样"这个困境;第三篇的色调映射决定了下游材质能否保真——而本篇第九章讲的 PBR Neutral,正是为这件事专门做的规格。
与前四篇的关系:前三篇加本篇回答的是"画成什么样",下一篇回答的是"画什么"。两者合起来才是一条完整管线的全貌。
10.6 落款
本文由@溯光 出品,转载请注明出处。 文中所有外部素材的原始页面与许可见下一节的图片版权表;数据与结论均给出可追溯来源,公式与代码为本文自行整理。 若发现事实性错误,欢迎指出——本文优先保证的是"说得准",其次才是"说得好读"。
参考文献
[1] Kajiya, The Rendering Equation, SIGGRAPH 1986 — https://dl.acm.org/doi/pdf/10.1145/15886.15902
[2] Rendering equation — https://handwiki.org/wiki/Rendering_equation
[3] Cornell CS6630 课程参考书目 — https://www.cs.cornell.edu/courses/cs6630/2012sp/biblio.stm
[4] Karis, Real Shading in Unreal Engine 4, SIGGRAPH 2013 PBS Course Notes — https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf
[5] Google Filament, Physically Based Rendering — https://google.github.io/filament/main/filament.html
[6] UC Berkeley CS184, BRDFs and Materials — https://cs184.eecs.berkeley.edu/public/su20/lectures/lec-13-brdfs-and-materials/lec-13-brdfs-and-materials.pdf
[7] Zamboni / FER 硕士论文, Physically based rendering — https://www.zemris.fer.hr/predmeti/ra/Magisterij/17_Ratkovic/Final_0036473349_56.pdf
[8] pbrt, Microfacet Models — https://pbr-book.org/3ed-2018/Reflection_Models/Microfacet_Models
[9] UCSD CSE168 Lecture 10 — https://visualcomp.ucsd.edu/classes/cse168/sp24/lectures/168-lecture10.pdf
[10] EON: A Practical Energy-Preserving Rough Diffuse BRDF, JCGT 2024 — https://www.jcgt.org/published/0014/01/06/paper.pdf
[11] UC Berkeley CS184 sp25, Introduction to Material Modeling — https://cs184.eecs.berkeley.edu/sp25/assets/lectures/14-material.pdf
[12] UCSD CSE272, BSDF — https://cseweb.ucsd.edu/~tzli/cse272/wi2026/lectures/03_bsdf.pdf
[13] Cook & Torrance 1982 文献著录 — https://www.cs.rit.edu/~jmg/gi/papers-new.html
[14] Walter et al., Microfacet Models for Refraction through Rough Surfaces — https://www.graphics.cornell.edu/~bjw/microfacetbsdf.pdf
[15] 【PBR 白皮书】法线分布函数总结 — https://blog.csdn.net/Kaitiren/article/details/118854923
[16] GGX (Trowbridge-Reitz) NDF 词条 — https://cstopics.com/encyclopedia/graphics/shading-and-lighting/shading-models/ggx-trowbridge-reitz-ndf
[17] Google Filament, Materials Guide — https://google.github.io/filament/main/materials.html
[18] Patrick Bell, Derivation of Physically Based Rendering — https://patricklbell.xyz/article/pbr-physics
[19] learnopengl, PBR Theory — https://learnopengl.com/PBR/Theory
[20] 100x Computer Graphics Guide, Ch.29 Physically-Based Materials — https://github.com/cemergin/100x-computer-graphics-guide/blob/main/part-5-advanced-rendering/29-pbr-materials.md
[21] Shader 光照模型参考 — https://github.com/x-cmd/skill/blob/main/data/minimax/shader-dev/reference/lighting-model.md
[22] modelviewer.dev, Tone Mapping Considerations for PBR — https://modelviewer.dev/examples/tone-mapping
[23] Fdez-Agüera, A Multiple-Scattering Microfacet Model for Real-Time IBL, JCGT 2019 — https://www.jcgt.org/published/0008/01/03/
[24] Graphics Programming Weekly #70 — https://www.jendrikillner.com/post/graphics-programming-weekly-issue-70/
[25] Multi-Scattering BRDF 推导 — https://jooh3444.tistory.com/34
[26] learnopengl, Diffuse Irradiance — https://learnopengl.com/PBR/IBL/Diffuse-irradiance
[27] Ramamoorthi & Hanrahan, An Efficient Representation for Irradiance Environment Maps, SIGGRAPH 2001 — https://cseweb.ucsd.edu/~ravir/papers/envmap/envmap.pdf
[28] Unity, Light Probes Technical Information — https://docs.unity3d.com/kr/2023.2/Manual/LightProbes-TechnicalInformation.html
[29] WebXR Lighting Estimation Explainer — https://github.com/immersive-web/lighting-estimation/blob/main/lighting-estimation-explainer.md
[30] Renderwonk / GDC 2003, Advanced Real-Time Illumination Techniques — https://renderwonk.com/publications/gdc-2003/gdc2003.pdf
[31] learnopengl, Specular IBL — https://learnopengl.com/PBR/IBL/Specular-IBL
[32] Image Based Lighting: Part 2 (Specular IBL),2025 — https://dk1242.github.io/2025/06/15/Image-Based-Lighting-2-SpecularIBL.html
[33] IBL — http://lonelywaiting.github.io/IBL/
[34] IntegrateBRDF 实现整理 — https://qiita.com/emadurandal/items/b2ae09c5cc1b3da821c8
[35] Unreal Engine, Reflections Captures — https://dev.epicgames.com/documentation/en-us/unreal-engine/reflections-captures-in-unreal-engine
[36] Unity, Reflection Probe — https://docs.unity3d.com/Manual/class-ReflectionProbe.html
[37] Godot 4.0, Reflection probes — https://docs.godotengine.org/en/4.0/tutorials/3d/global_illumination/reflection_probes.html
[38] Improved Box-Projected Reflections — https://github.com/frostbone25/Unity-Improved-Box-Projected-Reflections
[39] Unity HDRP, Reflection Probe — https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@13.1/manual/Reflection-Probe.html
[40] CosmicLearn, DirectX 12: Image-Based Lighting — https://www.cosmiclearn.com/dx12/ibl.php
[41] glTF KHR_materials_pbrSpecularGlossiness — https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness/README.md
[42] glTF KHR_materials_specular — https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_specular/README.md
[43] glTF material.pbrMetallicRoughness schema — https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/schema/material.pbrMetallicRoughness.schema.json
[44] glTF KHR_texture_basisu — https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_basisu/README.md
[45] Khronos, PBR Neutral Tone Mapper 规格 — https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md
[46] Khronos PBR Neutral 的商业落地报道 — https://displaydaily.com/khronos-pbr-neutral-color-consistency-in-e-commerce/
[47] Adobe, The PBR Guide Part 1 — https://www.adobe.com/learn/substance-3d-designer/web/the-pbr-guide-part-1
[48] Adobe, The PBR Guide Part 2 — https://www.adobe.com/us/learn/substance-3d-designer/web/the-pbr-guide-part-2
[49] Unity, Metallic and specular workflows — https://docs.unity3d.com/6000.2/Documentation/Manual/StandardShaderMetallicVsSpecular.html
[50] Unity, Smoothness — https://docs.unity3d.com/530/Documentation/Manual/StandardShaderMaterialParameterSmoothness.html
[51] Lexique PBR — https://www.n-3ds.com/texturing-pbr-lexique-diffuse-normal-roughness/
[52] Metallic-Roughness vs Specular-Glossiness — https://polyforge.xyz/learn/pbr-materials-metallic-roughness-specular-glossiness
[53] PBR 材质异常排查表 — https://wenku.csdn.net/column/2vv9drz614ti
[54] PBR 材质参数 — https://blog.csdn.net/haozi2008/article/details/112312157
[55] PBR 材质通用套用技巧 — https://jishuzhan.net/article/2058051753606221826
[56] Learn How to Master PBR Textures — https://resource.tcdc.or.th/article/11670/30110100566946/30110100566946_14.pdf
[57] Poly Haven, HDRI Technical Standards — https://docs.polyhaven.com/en/technical-standards/hdris
[58] Poly Haven, How to create high quality HDRI — https://blog.polyhaven.com/how-to-create-high-quality-hdri/
[59] Poly Haven, Photoscanned texture creation process — https://blog.polyhaven.com/photoscanned-texture-creation-process/
[60] Poly Haven, Photogrammetry 课程 — https://dev.polyhaven.com/learn/photogrammetry
[61] Poly Haven FAQ — https://docs.polyhaven.com/ko/faq
[62] ambientCG, Creation Methods — https://docs.ambientcg.com/creation-methods/
[63] 免费 3D 资产许可边界对照 — https://akamarupro.com/free-3d-asset-polyhaven-sketchfab-guide/
[64] Unity, High Dynamic Range Imaging (HDRI) 采集指南 — https://docs.unity3d.com/2018.1/Documentation/uploads/ExpertGuides/High_Dynamic_Range_Imaging_HDRI.pdf
[65] 浅墨《基于物理的渲染(PBR)白皮书》 — https://github.com/QianMo/PBR-White-Paper/blob/master/content/part%203/README.md
[66] 理解 PBR:从原理到实现 — https://neil3d.blog.csdn.net/article/details/83783638
[67] Real Shading in UE4 中文解读 — https://blog.csdn.net/zju_fish1996/article/details/88760583
[68] PBR、IBL 与其简单实现 — https://dhbloo.github.io/2019/09/12/slide/PBR-IBL/
[69] 渲染算法学习(四)Environment Lighting — https://blog.csdn.net/jiamada/article/details/120793839
[70] 球谐函数光照渲染 — https://www.cnblogs.com/BlueTOberry/p/19062470
[71] Sebastian Lagarde, Moving Frostbite to PBR — https://seblagarde.wordpress.com/tag/pbr/
[72] SIGGRAPH 2014, Physically Based Shading in Theory and Practice — https://dl.acm.org/doi/pdf/10.1145/2614028.2615431
[73] SIGGRAPH 2020 PBS Course — https://blog.selfshadow.com/publications/s2020-shading-course/hillaire/s2020_pbs_hillaire_slides.pdf
[74] awesome-pbr — https://github.com/Elli-Ossan/awesome-pbr
[75] awesome-pbr — https://github.com/neil3d/awesome-pbr
[76] Unity / URP 反射探针实操 — https://blog.csdn.net/Phantom1516/article/details/128684309
[77] PBR 理论基础 2:光照、材质与微面元理论 — https://blog.csdn.net/Jaihk662/article/details/115271085
[78] 物理模型中的 BRDF 是什么 — https://www.cnblogs.com/SmalBox/p/19113565
[79] PBR: Theory and Practice — https://mysimulator.uk/content/articles/pbr-shading.html
[80] learnopengl 中文版《镜面 IBL》 — https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/
[81] 【PBR 白皮书】开篇:核心知识体系 — https://blog.csdn.net/Kaitiren/article/details/117414906
[82] PBR 工作流中的贴图实现与优化 — https://blog.csdn.net/Kaitiren/article/details/129046991
[83] LearnOpenGL 笔记 7.4 PBR-Specular IBL — https://blog.csdn.net/u013617851/article/details/122465258
[84] Physically-Based Rendering: Real-Time Implementation — https://davidpeicho.github.io/teaching/rendering/pbr.pdf
[85] Mathematics of PBR and BRDF — https://vectree.io/pdf/c/mathematics-of-physically-based-rendering-pbr-and-bidirectional-reflectance-distribution-functions-brdf
[86] PBR in Substance Painter — https://www.lazi-akademie.de/wiki/software-tools/substance/substance-painter-pbr/
[87] Three.js 中 KHR_materials_pbrSpecularGlossiness 的迁移实践 — https://lain-lab.com/posts/javascript-threejs-khr-materials-pbrspecularglossiness/
[88] Illinois CS418, Simplified PBR — https://courses.grainger.illinois.edu/CS418/fa2025/text/ue4-pbr.html
[89] Vulkan Rust Tutorial, Specular lighting — https://vulkan-rust-tutorial.gitlab.io/v1/en/02_pbr/01_specular_lighting.html
[90] Cook-Torrance PBR 实现 PR — https://github.com/itsakeyfut/renderer/pull/122/files
[91] 基于图像的照明:镜面反射 — https://blog.csdn.net/ZJU_fish1996/article/details/88791858
[92] PBR 白皮书环境光照:IBL 实现详解 — https://blog.csdn.net/gitblog_00431/article/details/142507465
[93] Wikimedia Commons 用户指南 — https://upload.wikimedia.org/wikipedia/commons/e/ea/WMAU_Commons_Guide_(2018_version).pdf
[94] Wikimedia Foundation Commons 官方说明 — https://wikimediafoundation.org/what-we-do/wikimedia-projects/commons/
[95] Stanford, An Efficient Representation for Irradiance Environment Maps 论文与视频页 — https://vis.stanford.edu/videos/envmap/
[96] Burley, Extending the Disney BRDF to a BSDF — https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf
[97] Games101 L17 Materials and Appearances 笔记 — https://blog.csdn.net/weixin_70073176/article/details/140253750
[98] 基于物理光照渲染学习与实现 — https://blog.csdn.net/qq_41940565/article/details/128881526
[99] pbrt 4th edition References — https://pbr-book.org/4ed/References
[100] pbrt 在线全文首页 — https://pbr-book.org/
[101] Unreal Engine, Physically Based Materials — https://dev.epicgames.com/documentation/en-us/unreal-engine/physically-based-materials
[102] 误解专题:Multiscattering / Multilayering / Slope Space — https://www.gamedevpensieve.com/graphics/3d/3d_lighting/3d_lighting_physically-based-rendering
[103] Godot, List of features — https://docs.godotengine.org/pt-br/latest/about/list_of_features.html
[104] Wikimedia Commons, 360° panoramas with equirectangular projection 分类 — https://commons.wikimedia.org/wiki/Category:360%C2%B0_panoramas_with_equirectangular_projection
[105] Wikimedia Commons, Equirectangular projection 分类 — https://commons.wikimedia.org/wiki/Category:Equirectangular_projection
[106] Wikimedia Commons, Spherical panoramas 分类 — https://commons.wikimedia.org/wiki/Category:Spherical_panoramas
[107] Karis 2013 SIGGRAPH 课程笔记(镜像 PDF) — https://sunocean.life/blog/source/2013SiggraphPresentationsNotes-26915738.pdf
[108] Karis 2013 Real Shading in UE4 演讲 slides — https://neil3d.github.io/reading/assets/slides/s2013_pbs_epic_slides.pdf
[109] LearnOpenGL-CN 镜面 IBL(源码页) — https://github.com/LearnOpenGL-CN/learnopengl-cn.github.io/blob/master/07%20PBR/03%20IBL/02%20Specular%20IBL/index.html
[110] Fraueninsel Chiemsee Panorama(备选室外环境贴图) — https://commons.wikimedia.org/wiki/File:Fraueninsel_Chiemsee_Panorama.jpg
[111] Laon Cathedral Interior 360x180(备选室内环境贴图) — https://commons.wikimedia.org/wiki/File:Laon_Cathedral_Interior_360x180,Picardy,_France-_Diliff.jpg
[112] Ehrwald Panorama(天空占比大,适合环境光方向性示意) — https://commons.wikimedia.org/wiki/File:Ehrwald_Panorama.jpg
备选素材源
阶段 2 候选池中未采用,但许可清晰、可复用:
[113] 备选室外环境贴图(同作者系列) — https://commons.wikimedia.org/wiki/File:Chiemsee_bei_Seebruck_Luftbild.jpg [114] 备选室外环境贴图(工业场景) — https://commons.wikimedia.org/wiki/File:Erdfunkstelle_Raisting.jpg [115] 备选室内环境贴图(会议空间) — https://commons.wikimedia.org/wiki/File:35C3_Panorama.jpg [116] 备选自然光环境贴图(海滩) — https://commons.wikimedia.org/wiki/File:Battery_Point_Beach,Crecent_City,_California_May_2023.jpg [117] 备选自然光环境贴图(森林) — https://commons.wikimedia.org/wiki/File:Hiouchi,_California_May_2023.jpg [118] 备选教堂室内全景(Wells) — https://commons.wikimedia.org/wiki/File:Cmglee_Wells_Cathedral_chapter_house_photosphere.jpg [119] 备选教堂室内全景(Glasgow) — https://commons.wikimedia.org/wiki/File:Inside_Glasgow_Cathedral.jpg [120] 备选金属度通道图(Metal048B) — https://commons.wikimedia.org/wiki/File:Metal048B_8K-PNG_Metalness.png [121] 备选金属度通道图(Metal048C) — https://commons.wikimedia.org/wiki/File:Metal048C_8K-PNG_Metalness.png [122] 备选粗糙度通道图(同组高分辨率) — https://commons.wikimedia.org/wiki/File:MetalWalkway014_8K-PNG_Roughness.png [123] 备选金色金属表面纹理 — https://commons.wikimedia.org/wiki/File:Gold_metallic_finish_clean_smooth_seamless_metal_surface_sheet_texture.jpg [124] 备选塑料表面纹理(介质) — https://commons.wikimedia.org/wiki/File:Pale_blue_synthetic_plastic_seamless_surface_texture.jpg [125] 备选塑料表面纹理(粗糙) — https://commons.wikimedia.org/wiki/File:Grey_rough_coarse_synthetic_plastic_seamless_surface_texture.jpg [126] 备选陶瓷对照(未施釉,高 roughness) — https://commons.wikimedia.org/wiki/File:Two_Post-Medieval_Ceramic_Pottery_Vessel_Sherds(FindID_428672).jpg [127] 备选做旧木板(同系列) — https://commons.wikimedia.org/wiki/File:Old_painted_planks_texture_02.jpg [128] 备选染色木地板(base color 对照) — https://commons.wikimedia.org/wiki/File:Stained_Wood_Deck_Texture_DTXR-WD-GR-1.jpg
图片版权
本文全部外部素材均来自 Wikimedia Commons、ambientCG(经 Commons 收录)、Blender 官方手册、LearnOpenGL 与 Godot 官方文档。所有图注中的 @ 后内容即署名信息。
| # | 文件名 | 原始作品 | 作者 | 许可 | 原始页面 |
|---|---|---|---|---|---|
| 1 | src_metalness.png | MetalWalkway001 4K Metalness.png | ambientCG / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File:MetalWalkway001_4K_Metalness.png |
| 2 | src_roughness.png | MetalWalkway001 4K Roughness.png | ambientCG.com / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File:MetalWalkway001_4K_Roughness.png |
| 3 | src_wood_weathered.jpg | Weathered wood texture.jpg | Kelsey Todd | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File:Weathered_wood_texture.jpg |
| 4 | src_wood_planks.jpg | Old painted planks texture 01.jpg | Nikodem Nijaki | CC BY-SA 3.0 | https://commons.wikimedia.org/wiki/File:Old_painted_planks_texture_01.jpg |
| 5 | src_porcelain_song.jpg | 20240817 White Glazed Porcelain Pillow, Song.jpg | Windmemories | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File:20240817_White_Glazed_Porcelain_Pillow,_Song.jpg |
| 6 | src_porcelain_yuan.jpg | Yuan Glazed Porcelain Pillow.jpg | Gary Todd | CC0 | https://commons.wikimedia.org/wiki/File:Yuan_Glazed_Porcelain_Pillow.jpg |
| 7 | src_skin.jpg | Hand skin closeup.jpg | Clump | CC0 | https://commons.wikimedia.org/wiki/File:Hand_skin_closeup.jpg |
| 8 | src_copper.jpg | Copper coating.tif | Michalgocki | CC BY 4.0 | https://commons.wikimedia.org/wiki/File:Copper_coating.tif |
| 9 | src_metal_scrap.jpg | Metal scrap in Aomi.jpg | Syced | CC0 | https://commons.wikimedia.org/wiki/File:Metal_scrap_in_Aomi.jpg |
| 10 | src_rusty_rebar.jpg | Rusty rebar nets.jpg | W.carter | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File:Rusty_rebar_nets.jpg |
| 11 | src_wood_painted.jpg | Painted Wood Grain Texture DTXR-WD-GR-3.jpg | Shadowmeld Photography | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File:Painted_Wood_Grain_Texture_DTXR-WD-GR-3.jpg |
| 12 | src_pano_outdoor.jpg | Landschaftspark-Duisburg-Nord Hochofen Panorama.jpg | DerMische | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File:Landschaftspark-Duisburg-Nord_Hochofen_Panorama.jpg |
| 13 | src_pano_indoor.jpg | Soissons Cathedral Interior 360x180, Picardy, France - Diliff.jpg | Diliff | CC BY-SA 3.0 | https://commons.wikimedia.org/wiki/File:Soissons_Cathedral_Interior_360x180,Picardy,_France-_Diliff.jpg |
| 14 | src_fresnel_curve.png | Fresnel equations - reflectance.svg | Fresnel_reflection_coefficients_(DE).svg: Cepheiden derivative work: Cepheiden (talk) | CC BY-SA 3.0 | https://commons.wikimedia.org/wiki/File:Fresnel_equations_-_reflectance.svg |
| 15 | src_bsdf.png | Bidirectional scattering distribution function.svg | Twisp | Public domain | https://commons.wikimedia.org/wiki/File:Bidirectional_scattering_distribution_function.svg |
| 16 | src_pbr_params.png | Principled BSDF 的 roughness / metallic 球体序列(上下拼接) | Blender Documentation Team | CC BY-SA 4.0 | https://docs.blender.org/manual/en/latest/render/shader_nodes/shader/principled.html |
| 17 | src_lo_hemisphere.png | hemisphere.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 18 | src_lo_solid_angle.png | solid_angle.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 19 | src_lo_radiance.png | radiance.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 20 | src_lo_radiant_intensity.png | radiant_intensity.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 21 | src_lo_surface_reaction.png | surface_reaction.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 22 | src_lo_microfacets.png | microfacets.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 23 | src_lo_microfacets_rays.png | microfacets_light_rays.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory |
| 24 | src_grazing_lake.jpg | 1920px-Landscape_mirror_reflection_Lough_Leane_Killarney.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | Trauenbaum | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File%3ALandscape%20mirror%20reflection%20Lough%20Leane%20Killarney.jpg |
| 25 | src_grazing_fjord.jpg | 1920px-Low_sunlight_at_Øksfjorden_in_Loppa,_Troms_og_Finnmark,_Norway,_2022_August.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | Ximonic (Simo Räsänen) | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File%3ALow%20sunlight%20at%20%C3%98ksfjorden%20in%20Loppa%2C%20Troms%20og%20Finnmark%2C%20Norway%2C%202022%20August.jpg |
| 26 | src_grazing_colored.jpg | 1920px-Colored_reflection_(8393387310).jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | Alias 0591 from the Netherlands | CC BY 2.0 | https://commons.wikimedia.org/wiki/File%3AColored%20reflection%20%288393387310%29.jpg |
| 27 | src_pano_eso.jpg | 1920px-360-degree_panorama_of_the_ESO_Headquarters_(hqe-pano2).jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | ESO | CC BY 4.0 | https://commons.wikimedia.org/wiki/File%3A360-degree%20panorama%20of%20the%20ESO%20Headquarters%20%28hqe-pano2%29.jpg |
| 28 | src_pano_alma.jpg | 1920px-ALMA_360_degree_panorama_(pano360-alma).jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | ESO/ S. Brunier | CC BY 4.0 | https://commons.wikimedia.org/wiki/File%3AALMA%20360%20degree%20panorama%20%28pano360-alma%29.jpg |
| 29 | src_lo_ibl_hemi_sample.png | ibl_hemisphere_sample.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Diffuse-irradiance |
| 30 | src_lo_ibl_hemi_normal.png | ibl_hemisphere_sample_normal.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Diffuse-irradiance |
| 31 | src_lo_ibl_integrate.png | ibl_spherical_integrate.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Diffuse-irradiance |
| 32 | src_lo_ibl_pdf.png | ibl_pdf.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL |
| 33 | src_lo_prefilter_sample.png | ibl_prefilter_map_sample.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL |
| 34 | src_pano_galerie.jpg | 1920px-Alte_Nationalgalerie,_Berlin-Mitte,_360_Grad_Panorama,_160101,_ako.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | Ansgar Koreng | CC BY-SA 3.0 de | https://commons.wikimedia.org/wiki/File%3AAlte%20Nationalgalerie%2C%20Berlin-Mitte%2C%20360%20Grad%20Panorama%2C%20160101%2C%20ako.jpg |
| 35 | src_godot_shading_textured.png | standard_material_shading_modes_textured.webp | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html |
| 36 | src_tex_plate_preview.jpg | DiamondPlate005A_PREVIEW.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled | ambientCG.com / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File%3ADiamondPlate005A%20PREVIEW.jpg |
| 37 | src_tex_plate_metal.png | 1920px-DiamondPlate005A_8K_Metalness.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail | ambientCG.com / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File%3ADiamondPlate005A%208K%20Metalness.png |
| 38 | src_tex_wicker_preview.png | Wicker013_PREVIEW.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled | ambientCG.com / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File%3AWicker013%20PREVIEW.png |
| src_lo_ndf.png | ndf.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory | |
| src_lo_geometry.png | geometry.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory | |
| src_lo_geo_shadowing.png | geometry_shadowing.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory | |
| src_lo_linear_vs_hdr.png | lighting_linear_vs_non_linear_and_hdr.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory | |
| src_lo_pbr_textures.png | textures.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory | |
| src_lo_brdf_lut.png | ibl_brdf_lut.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | |
| src_lo_irradiance.png | ibl_irradiance.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Diffuse-irradiance | |
| src_godot_ambient.png | environment_ambient_comparison.webp | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html | |
| src_lo_equirect.png | ibl_equirectangular_projection.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Diffuse-irradiance | |
| src_lo_env_mapped.png | ibl_hdr_environment_mapped.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Diffuse-irradiance | |
| src_lo_prefilter.png | ibl_prefilter_map.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | |
| src_lo_ibl_result.png | ibl_specular_result_textured.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | |
| src_lo_ibl_envs.png | ibl_specular_result_different_environments.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | |
| src_godot_probe.png | reflection_probes_reflection_probe.webp | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/reflection_probes.html | |
| src_godot_ssr.png | reflection_probes_ssr.webp | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/reflection_probes.html | |
| src_godot_clearcoat.png | clearcoat_comparison.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | |
| src_tex_bricks.jpg | Bricks091 8K-PNG Color.png | ambientCG.com / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File:Bricks091_8K-PNG_Color.png | |
| src_tex_basecolor.jpg | Chainsaw Base.png | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Base.png | |
| src_tex_roughness.jpg | Chainsaw Roughness.png | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Roughness.png | |
| src_tex_ao.jpg | Chainsaw Ambient Occlusion.png | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Ambient_Occlusion.png | |
| src_normal_map.png | Normal map example - Map.png | Julian Herzog (Website) | CC BY 4.0 | https://commons.wikimedia.org/wiki/File:Normal_map_example_-_Map.png | |
| src_normal_result.png | Normal map example - Result.png | Julian Herzog (Website) | CC BY 4.0 | https://commons.wikimedia.org/wiki/File:Normal_map_example_-_Result.png | |
| src_mat_polyhaven.jpg | Clay - rusty metal grid (Amal Kumar via Poly Haven).webp | Amal Kumar | CC0 | https://commons.wikimedia.org/wiki/File:Clay_-rusty_metal_grid(Amal_Kumar_via_Poly_Haven).webp | |
| src_hdr_pano_snow.jpg | Dent de Vaulion - 360 degree panorama.jpg | Alexandre Duret-Lutz from Paris, France | CC BY-SA 2.0 | https://commons.wikimedia.org/wiki/File:Dent_de_Vaulion_-_360_degree_panorama.jpg | |
| src_hdr_pano_rail.jpg | Nebel Schienen Panorama.jpg | DerMische | CC BY-SA 4.0 | https://commons.wikimedia.org/wiki/File:Nebel_Schienen_Panorama.jpg | |
| src_metal_sphere_gold.jpg | Gold-coloured sphere, reflecting the photographer, in shop window of goldsmith in Munich, Germany (2023).jpg | Pittigrilli | CC BY 4.0 | https://commons.wikimedia.org/wiki/File:Gold-coloured_sphere,reflecting_the_photographer,_in_shop_window_of_goldsmith_in_Munich,_Germany(2023).jpg | |
| src_tex_rusty_metal.jpg | Free Texture Rusty Metal (22819579).jpeg | C Avery | CC BY 3.0 | https://commons.wikimedia.org/wiki/File:Free_Texture_Rusty_Metal_(22819579).jpeg | |
| src_tex_metallic.jpg | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Metallic.png | ||
| src_tex_normal_tangent.jpg | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Normal_(Tangent_Space).png | ||
| src_tex_height.jpg | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Height.png | ||
| src_tex_lit.jpg | JasonFunderberker | CC0 | https://commons.wikimedia.org/wiki/File:Chainsaw_Lit.png | ||
| src_tex_bricks2.jpg | ambientCG.com / Lennart Demes | CC0 | https://commons.wikimedia.org/wiki/File:Bricks093_8K-PNG_Color.png | ||
| src_godot_orm.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_roughness.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_normal_map.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_ao_map.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_height_map.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_shading_modes.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_specular_mode.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_godot_bentnormal.png | Godot Engine documentation | CC BY 4.0 | https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html | ||
| src_lo_specular_lobe.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | ||
| src_lo_grazing.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | ||
| src_lo_prefilter_seams.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL | ||
| src_lo_fresnel_no_roughness.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/Theory | ||
| src_lo_lds.png | Joey de Vries | CC BY 4.0 | https://learnopengl.com/PBR/IBL/Specular-IBL |
自绘示意图的图注统一标注"本系列绘制",不在此表内。许可方面:CC0 与 Public domain 无需署名,CC BY 需署名,CC BY-SA 另需以相同方式共享。
相关链接 - 上一篇:【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿 - 下一篇(预告):【实时渲染管线探析】(五) 可见性与剔除:从视锥到 GPU-Driven - 本文全部参考文献(128 条)见上一节,图片版权见「图片版权」表
以上。 如果这篇帮你把 PBR 那条链子从头到尾串起来了,或者指出了你项目里某个查了很久的偏色来源,那它就达到了目的。感谢大家读到这里。
With Best Wishes.