实时渲染管线探析 · 第 四 篇

基于物理的渲染:从渲染方程到工业实践

2026-09-1024,420 字203 张配图本文由@溯光 出品,转载请注明出处

站点链接:技术博客页

题图:釉面上的镜面高光
题图釉面上的镜面高光:高光的形状由 roughness 决定,而反射强度由 F0 决定@ Wikimedia Commons · Windmemories(CC BY-SA 4.0)

系列文章前言

这个系列写到第四篇,终于要碰那个所有渲染管线都在用、但很少有人从头讲清的东西了。

前三篇的路线是这样的:第一篇拆延迟渲染,讲的是"几何信息怎么存进 G-Buffer、光照怎么被推迟到屏幕空间去算";第二篇拆阴影贴图,从 PCF 一路讲到 VSM,讲的是"怎么判断一个点有没有被挡住";第三篇拆后处理链,Bloom、色调映射、抗锯齿三件事,讲的是"算完之后怎么把它变成一张能看的图"。

这三篇里反复出现一个没被展开的东西:光照到底该怎么算。第一篇里 G-Buffer 存了 albedo、normal、roughness、metallic 这些通道,但没说过为什么是这几个通道;第二篇里阴影决定了"能不能收到光",但没说过收到之后那份光该怎么变成表面的亮度;第三篇里色调映射把高动态范围压进显示范围,但没说过为什么某些颜色压完就失真了。

这篇要补的就是这块。而补它的代价是,我们得先把那个从 1986 年一直用到今天的积分式写出来——因为它才是这一切的源头。

系列四篇的坐标:本篇在「几何→可见性→后处理」链条中补上「光如何与表面作用」这一环
图 4-1系列四篇的坐标:本篇在「几何→可见性→后处理」链条中补上「光如何与表面作用」这一环@ 本系列绘制

按照惯例,先把这篇的坐标说清楚。

这篇写给谁:写过 shader、见过 roughness、metallic 这些参数、但说不清它们背后物理依据的开发者;以及已经能跑通 PBR 流程、却在"为什么磨砂金属看起来偏暗""为什么掠射角会有一圈亮边"这类现象上卡住的人。

读完能得到什么:一条从渲染方程出发、经过 BRDF 与微平面理论、落到 IBL 预计算、最后收敛到工业参数约定的完整链条;每一步近似的失效边界;以及一张能对照 glTF 规范与三大引擎的参数表。

本系列前三篇给本篇提供了什么
图 4-2本系列前三篇给本篇提供了什么@ 本系列绘制

不涉及什么(这条边界要先声明,免得读到一半发现不是自己想要的)

本文的边界划分
图 4-3本文的边界划分@ 本系列绘制

:本文不深入路径追踪的完整实现——它只会作为"能量守恒的参照物"出现;不涉及大气散射与体积散射;次表面散射只在皮肤那里作为引子提一句;不讲各引擎材质编辑器的操作步骤;也不给具体项目的性能调优建议。原因很简单:这些每一项都够写一篇同体量的文章,塞进来会让主线失焦。

关键参数的取值范围也先摆在这里,后文会逐个展开:介质的正入射反射率(F0)几乎总是 0.04,金属的 base color 落在 0.66 到 1.0 之间,roughness 是 0 到 1 的连续量,而 metallic 现实中只有 0 和 1 两个取值。

读完本篇可以带走的三样东西
图 4-4读完本篇可以带走的三样东西@ 本系列绘制

目录总览

章 主题 解决的问题
零 背景铺垫 术语对齐、范围边界、贯穿全篇的问题
一 从渲染方程说起 光照的账怎么算,为什么这个式子不能直接解
二 BRDF:那个被积的核 四条性质,哪一条让预计算成为可能
三 从 Lambert 到 Oren-Nayar 粗糙漫反射为什么到 2024 年还在被修
四 微平面理论:D、G、F 三项各自的职责,以及 NDF 的归一化约束
五 几何项与菲涅尔 掠射角那点事,两套 $k$ 映射不能混用
六 单散射的缺口 磨砂金属为什么偏暗,怎么补
七 IBL 与 split-sum 环境光怎么进那个积分,$N=V=R$ 的代价
八 预滤波、LUT 与探针 落地细节:mip 链、R16G16、box projection
九 工业实践与参数取值 两套工作流、F0 取值带、编码约定、PBR Neutral
十 结语 九步近似的收口表、三个未解问题、选型建议
全篇目录总览表:十二章各自的主题与所解决的问题
图 4-5全篇目录总览表:十二章各自的主题与所解决的问题@ 本系列绘制

整篇的推进可以概括为一条主线:先立下渲染方程,再解开 BRDF 的四条性质,接着落到微平面 D/G/F 三项,补上多散射的能量缺口,最后经 IBL 与环境探针的落地细节,收敛到工业化可用的参数约定——前四步负责"把图像算对",最后一步负责"把它交得出去"。

现在可以往下读了。


背景铺垫

0.1 从一次"看起来不对"说起

先说一个具体的现象,因为它比任何定义都更容易让人意识到问题的存在。

假设你在引擎里建了三个球:一个纯金属材质、roughness 拉到 0.1;一个纯金属材质、roughness 拉到 0.9;还有一个非金属的塑料球,roughness 也是 0.9。三者用同一组光照参数、同一个环境贴图。

按理说,前两个球的区别只是"高光锐利还是模糊",第三个球应该看起来是"哑光的"。但实际跑出来,很多人会看到:roughness 0.9 的金属球偏暗,甚至比同 roughness 的塑料球还暗。

这不是 bug,也不是引擎实现有问题。它是"单次散射"这个近似在高粗糙度下的必然结果——光在微表面之间弹了很多次才离开表面,而我们的公式只算了弹一次的部分,弹两次以上的能量就凭空消失了。

同一材质在 roughness 从低到高时的能量去向:单散射只取一次反弹,多次反弹的能量被丢弃
图 4-6同一材质在 roughness 从低到高时的能量去向:单散射只取一次反弹,多次反弹的能量被丢弃@ 本系列绘制

先来看看,这个现象在本文第七章会被完整解释,并给出补偿方法。之所以放在开头说,是因为它恰好说明了这个领域的性质:渲染方程写出来只有一行,但从它到一个能跑的 shader,中间要做一连串近似,而每一步近似都在某个角落留下痕迹。

渲染方程的落地路径:一行积分式经七步近似变成可执行 shader,每步都标注代价
图 4-7渲染方程的落地路径:一行积分式经七步近似变成可执行 shader,每步都标注代价@ 本系列绘制

0.2 这件事的难度在哪

OK,言归正传。如果只看公式,PBR 并不复杂——一个积分式,加一个 BRDF 函数,加一个菲涅尔项。真正的难点在于三件事:

OK,言归正传。第一,那个积分式没法直接算。 渲染方程要求对半球面上所有入射方向积分。计算机做不到对连续函数求积分,只能采样,而采样的代价随质量上升得很快。整个实时渲染的 PBR 史,本质上是"怎么用尽可能少的采样逼近这个积分"的历史。

第二,物理正确和美术可调是冲突的。 严格按物理来,一个材质需要几十个参数才能描述清楚,美术没法用。Disney 在 2012 年做的事,就是把参数压缩到美术能理解的一小撮,同时保证物理上站得住。这个取舍至今还在演化。

参数数量与物理完备性的取舍曲线:Disney 2012 把几十个参数压到美术可用的规模
图 4-8参数数量与物理完备性的取舍曲线:Disney 2012 把几十个参数压到美术可用的规模@ 本系列绘制

第三,规范与引擎的实现细节并不一致。 同一个"金属球",在 glTF 里怎么描述、在 Unreal 里怎么落、在 Unity 里怎么落、在 Blender 里导出又变成什么,四者之间存在微妙的差异。这些差异在简单场景里看不出来,一旦做跨引擎资产迁移就会集中爆发。

同一材质在 glTF / Unreal / Unity / Blender 四端的参数落点差异
图 4-9同一材质在 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 规定的默认材质模型
术语之间的依赖关系
图 4-10术语之间的依赖关系@ 本系列绘制
核心术语中英对照表,固定本文译名用法
图 4-11核心术语中英对照表,固定本文译名用法@ 本系列绘制

0.4 一个贯穿全篇的问题

最后把这篇要回答的核心问题摆出来。后文每一章都在回答它的一个侧面:

如果渲染方程是对的,那么从它到一个美术能调、跨引擎能迁移、跑起来还不崩的材质管线,中间每一步做了什么近似,各自在什么时候会露馅?

全篇核心问题的分解:把「近似在哪失效」拆成七个可独立验证的子问题
图 4-12全篇核心问题的分解:把「近似在哪失效」拆成七个可独立验证的子问题@ 本系列绘制

带着这个问题往下读,比记住任何单个公式都有用。


一、光照的账怎么算:从渲染方程说起

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 $$

渲染方程各项的几何含义:自发光 + 半球积分(BRDF × 入射辐射度 × 余弦因子)
图 4-13渲染方程各项的几何含义:自发光 + 半球积分(BRDF × 入射辐射度 × 余弦因子)@ 本系列绘制

把它拆开逐项读一遍:

  • $L_o(x, \omega_o)$ 是从点 $x$ 沿方向 $\omega_o$ 出来的辐射度——也就是我们要算的那个值,最终会变成屏幕上一个像素的颜色
  • $L_e(x, \omega_o)$ 是该点自身发出的光。对绝大多数物体这个项是零,只有光源自己才不为零
  • 积分号是对整个半球面 $\Omega$ 做的,也就是"从上方所有方向进来的光"
积分域的几何含义:半球面上每个方向 i 都有自己的入射辐射度 Li 与余弦因子
图 4-14积分域的几何含义:半球面上每个方向 i 都有自己的入射辐射度 Li 与余弦因子@ 本系列绘制
  • $f_r$ 是 BRDF,它决定"从这个方向进来的光,有多少会反射到那个方向去"
  • $L_i$ 是从方向 $\omega_i$ 进来的入射辐射度
四个关键量的关系:Li 进来、BRDF 决定分配、Lo 出去
图 4-15四个关键量的关系:Li 进来、BRDF 决定分配、Lo 出去@ 本系列绘制
  • $(\omega_i \cdot n)$ 是余弦因子,它描述的是"斜着照进来的光,实际落到单位面积上的通量会变少"

需要补充一段来历。这个方程常被直接归给 Kajiya 1986 年的论文《The Rendering Equation》[1],但它在同年由 David Immel 等人与 James Kajiya 独立引入图形学,且形式与辐射传热领域早已成熟的理论一致 [2]。Cornell 的课程书目里对它的评价很直白:如今被视为表面渲染的理论基础 [3]。

1.2 为什么不能直接解

这个方程的麻烦之处在于,$L_i$ 本身也是未知的。

先来看看这个式子难在哪。从方向 $\omega_i$ 进来的光从哪来?可能直接来自光源,也可能来自另一个物体的反射——而那个物体的反射,又要用同一个方程去算。也就是说,方程的两边都含未知量,未知量之间还互相引用。

方程两边都是未知量的示意:Lo(A) 依赖 Li(A),Li(A) 又来自 Lo(B)
图 4-16方程两边都是未知量的示意:Lo(A) 依赖 Li(A),Li(A) 又来自 Lo(B)@ 本系列绘制
递归依赖:A 点的入射光可能来自 B 点,B 点的入射光又可能来自 A 点——方程两边都是未知量
图 4-17递归依赖:A 点的入射光可能来自 B 点,B 点的入射光又可能来自 A 点——方程两边都是未知量@ 本系列绘制

离线渲染之所以要跑几分钟甚至几小时,就是在处理这种递归依赖——用路径追踪反复采样,让光在场景里弹很多次,直到结果收敛。

实时渲染没有这个预算。所以工业界的做法是:把递归切断,只保留直接可见的那部分。 具体来说:

  • 进入 $L_i$ 的光,只算"来自光源的"和"来自环境贴图的"
  • 物体之间的多次相互反射,用其他手段近似(光照贴图、探针、屏幕空间反射),而不是解方程

这就是为什么实时渲染里"间接光"总是一个需要单独处理的模块,也是为什么本文后半部分要花两章讲 IBL——它本质上是在回答"那部分被切断的间接光,怎么补回来一点"。

解析光源与环境光两条路径
图 4-18解析光源与环境光两条路径@ 本系列绘制
离线路径追踪的多次反弹采样 vs 实时管线只保留直接可见的切断方式
图 4-19离线路径追踪的多次反弹采样 vs 实时管线只保留直接可见的切断方式@ 本系列绘制

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]。

余弦因子为什么不能省
图 4-20余弦因子为什么不能省@ 本系列绘制

实现:求和的形式决定了渲染循环的形状——外层遍历光源,内层计算 BRDF 与余弦因子。这个循环放在哪个阶段执行,就是延迟渲染与向前渲染的分野(详见本系列第一篇)。

效果:当 $N$ 足够大且 BRDF 正确时,结果在视觉上收敛到"物理正确";$N$ 不足时会出现噪点或能量偏差。

采样数 N 与收敛误差的关系:N 增大误差下降,但实时预算下 N 有硬上限
图 4-21采样数 N 与收敛误差的关系:N 增大误差下降,但实时预算下 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 两条路径的拆分做得相当清楚。

解析光源求和循环的四个关键步骤:方向计算 → 余弦因子截断 → NDF/F 的半程向量 → 累加
图 4-22解析光源求和循环的四个关键步骤:方向计算 → 余弦因子截断 → NDF/F 的半程向量 → 累加@ 本系列绘制

渲染方程是在一个半球上积分,积分域就是它:

半球积分域:渲染方程对上半球所有入射方向积分,$\cos\theta$ 加权后即式中的 $N$
图 4-23半球积分域:渲染方程对上半球所有入射方向积分,$\cos\theta$ 加权后即式中的 $N$@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

立体角是这一族量里最抽象的一个,它的几何含义是这样:

立体角:球面上的一块面积除以半径平方,单位是球面度
图 4-24立体角:球面上的一块面积除以半径平方,单位是球面度@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

渲染方程里被积的是辐射亮度,它的定义是单位面积、单位立体角上的能量:

辐射亮度(Radiance):单位投影面积、单位立体角上的辐射通量,是渲染的基本量
图 4-25辐射亮度(Radiance):单位投影面积、单位立体角上的辐射通量,是渲染的基本量@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

再往前一层是辐射强度——只对立体角积分、不涉及面积:

辐射强度(Radiant Intensity):单位立体角上的辐射通量,是辐射亮度去掉面积项
图 4-26辐射强度(Radiant Intensity):单位立体角上的辐射通量,是辐射亮度去掉面积项@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

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)} $$

下面我们开始。读作"出射辐射度的微分增量,除以入射辐照度的微分增量"。

BRDF 作为「每单位入射光」的响应函数:入射翻倍则出射翻倍,比值不变
图 4-27BRDF 作为「每单位入射光」的响应函数:入射翻倍则出射翻倍,比值不变@ 本系列绘制
BRDF 的几何定义:入射方向 ωi、出射方向 ωo、法线 n,以及定义它的微分比值
图 4-28BRDF 的几何定义:入射方向 ωi、出射方向 ωo、法线 n,以及定义它的微分比值@ 本系列绘制

理解这个定义的关键在于:它是一个"每单位入射光"的响应函数,而不是"反射了多少光"的绝对值。入射光变强一倍,出射也变强一倍,比值不变——这个性质叫线性,是第四条(常被省略的)性质。

UC Berkeley 的 CS184 课件把 BRDF 的性质列得很清楚 [6],下面三条是物理可信的必要条件。

2.2 三条约束

第一条,非负(positivity):

$$ f_r(\omega_i, \omega_o) \ge 0 $$

表面不能把光反射成负的。这一条看着理所当然,但它是判断"某个经验模型能不能用"的第一道筛子——比如某些早期模型在高光边缘会算出负值,那就不合法。

非负性的筛除效果:早期经验模型在高光边缘算出负值的情形
图 4-29非负性的筛除效果:早期经验模型在高光边缘算出负值的情形@ 本系列绘制

第二条,互易(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 $$

反射出去的总能量,不能超过入射进来的总能量。这条是最重要、也最容易被违反的——本文第七章整章都在讲它被违反之后会发生什么、以及怎么补偿。

第四条,线性:多个光源同时照时,结果等于各自单独照的结果之和。这条让"遍历光源求和"成为合法操作,是整条渲染循环成立的前提。

四条性质各自被违反时的症状
图 4-30四条性质各自被违反时的症状@ 本系列绘制
BRDF 四条性质:非负、互易、能量守恒、线性——前三条是物理可信条件,第四条让求和合法
图 4-31BRDF 四条性质:非负、互易、能量守恒、线性——前三条是物理可信条件,第四条让求和合法@ 本系列绘制

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$ 无关 让多光源求和与预计算成为可能
BRDF 四条性质的数学形式与工程用途对照
图 4-32BRDF 四条性质的数学形式与工程用途对照@ 本系列绘制

其中线性的作用最容易被忽略。它之所以重要,是因为 IBL 的整个预计算方案都建立在它之上:既然 BRDF 与入射光无关,那就可以把"积分里与光无关的部分"提前算好存起来,运行时只做查表。如果没有线性,第八、九章讲的那套 split-sum 根本不可能成立。

2.4 公式与实现:一个合法的 BRDF 长什么样

最简的合法 BRDF 是 Lambert 漫反射:

BRDF、BTDF 与 BSDF 的关系
图 4-33BRDF、BTDF 与 BSDF 的关系@ 本系列绘制

$$ 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$ 次方,都可以用同样的思路去验算。

BRDF 里各常数对应的归一化来源:π、4π、1/8、1/21 各自的出处
图 4-34BRDF 里各常数对应的归一化来源:π、4π、1/8、1/21 各自的出处@ 本系列绘制

代码形态上,Lambert 漫反射的实现就是一行:

// 【Lambert 漫反射】反照率除以 π —— 那个 π 是能量守恒要求的归一化因子
float3 DiffuseLambert(float3 albedo) {
    return albedo / PI;                    // 【1】PI 不可省略,省略则能量超发
}

上面这一行特别说明:分母的 $\pi$ 省略掉,画面会变亮大约 3.14 倍,而且在高光区域会明显过曝。很多"PBR 看起来发灰/发白"的排查,最后都指向这类归一化因子的缺失。

完整的 BRDF 实现清单与对照可以参考 pbrt 的反射模型章节 [8],它对每个模型都给了归一化推导。

常用 BRDF 的合法性对照:Lambert / Oren-Nayar / Blinn-Phong / GGX 是否满足四条性质
图 4-35常用 BRDF 的合法性对照:Lambert / Oren-Nayar / Blinn-Phong / GGX 是否满足四条性质@ 本系列绘制

BRDF 的求值频率在引擎里是可选的——逐像素还是逐顶点:

着色模式对照:逐像素(per-pixel)与逐顶点(per-vertex)在球面高光上的差别
图 4-36着色模式对照:逐像素(per-pixel)与逐顶点(per-vertex)在球面高光上的差别@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

一个合法的 BRDF 描述的是光与表面相互作用的三种去向:

光与表面的相互作用:入射光被反射、折射或吸收,BRDF 只管其中能返回观察方向的那部分
图 4-37光与表面的相互作用:入射光被反射、折射或吸收,BRDF 只管其中能返回观察方向的那部分@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

2.5 这一章留下的问题

Lambert 这个模型有个明显的问题:它太简单了。 真实的粗糙表面,比如未施釉的陶器、混凝土、粗糙布料,在掠射角附近会比 Lambert 预测的更亮。这个问题在 1994 年被专门建模过,而且直到 2024 年还在被修正。下一章就来讲这段"看似简单、其实不简单"的历史。

Lambert 与 Oren-Nayar 在明暗交界处的亮度差异:真实粗糙表面在掠射角更亮
图 4-38Lambert 与 Oren-Nayar 在明暗交界处的亮度差异:真实粗糙表面在掠射角更亮@ 本系列绘制

三、漫反射不简单:从 Lambert 到 Oren-Nayar

3.1 Lambert 的漏洞

上一章结尾说到 Lambert 太简单。具体哪里简单?

Lambert 假设表面把入射光均匀地散射到整个半球,反射强度只与入射角有关,与出射方向无关——所以严格来说它是一个"常数 BRDF"。对很多材质这个假设够用:石膏、纸张、粗糙塑料,视觉上都接近这个行为。

但有一类表面它预测得不对:未施釉的陶器、月球表面、粗毛毡、混凝土。这类表面有个共同特征——微观上是凹凸不平的粉末状或纤维状结构,而不是光滑连续的面。

Lambert 隐含的三个假设
图 4-39Lambert 隐含的三个假设@ 本系列绘制
光滑表面与粗糙颗粒表面的微观结构差异:后者在明暗交界处会出现明显的亮度回升
图 4-40光滑表面与粗糙颗粒表面的微观结构差异:后者在明暗交界处会出现明显的亮度回升@ 本系列绘制

先来看看,拿月球的照片对比一下就很直观。按 Lambert 计算,月球在明暗交界处应该平滑地暗下去;但实际观测里,那一圈会反常地亮一点。这不是测量误差,是这类表面固有的反射行为。

Lambert 与实测粗糙表面的亮度曲线对照:明暗交界处出现反常回升
图 4-41Lambert 与实测粗糙表面的亮度曲线对照:明暗交界处出现反常回升@ 本系列绘制

3.2 Oren-Nayar:把表面当成 V 形凹槽

1994 年,Oren 和 Nayar 给出了一个专门描述这类表面的模型。它的思路很巧妙:不去直接建模那些复杂的颗粒,而是把表面简化为一组 V 形的凹槽,然后用球面高斯分布描述这些凹槽朝向的分散程度。

Oren-Nayar 的 V 形凹槽模型:微面之间互相遮挡,在掠射角形成亮度回升
图 4-42Oren-Nayar 的 V 形凹槽模型:微面之间互相遮挡,在掠射角形成亮度回升@ 本系列绘制

这个几何简化抓住了关键机制:互相遮挡。当一个 V 形凹槽的入射角变大时,凹槽的一个面被另一个面挡住,遮挡比例上升;但同时,可见的那个面里,法线更朝向观察者的比例也在上升。两个效应的净结果,是明暗交界处出现亮度回升。

pbrt 采用的是 Oren-Nayar 的拟合近似形式,含两个参数 A 和 B [8]。UCSD 的课程讲义里有一页专门并排对照 Lambert 与 Oren-Nayar 的形式差异 [9]。

Oren-Nayar 的两个拟合参数 A、B 随 σ 变化的曲线
图 4-43Oren-Nayar 的两个拟合参数 A、B 随 σ 变化的曲线@ 本系列绘制

3.3 参数逐项释义:粗糙度在这里的另一个含义

Oren-Nayar 有一个参数 $\sigma$,表示凹槽朝向的分散程度。它的作用与后文微平面理论里的 roughness 类似,但两者不是同一个量——这是容易混淆的地方:

参数 所属模型 描述对象 典型取值
$\sigma$ Oren-Nayar 漫反射微面的朝向分散 0(= Lambert)到 $\pi/2$
roughness $\alpha$ 微平面理论 镜面微面的朝向分散 0(镜面)到 1(全粗糙)
Oren-Nayar 的 σ 与微平面理论的 roughness 对照:两者描述不同层的微面分散,不可互换
图 4-44Oren-Nayar 的 σ 与微平面理论的 roughness 对照:两者描述不同层的微面分散,不可互换@ 本系列绘制

工程上的经验是:$\sigma$ 很少单独暴露给美术,通常由粗糙度间接映射。原因很简单——两个参数都往美术界面上摆,没人知道该怎么调。

粗糙漫反射在引擎里的位置
图 4-45粗糙漫反射在引擎里的位置@ 本系列绘制

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(把物体放进均匀白色环境)时才看得出来。

粗糙漫反射的修正时间线:Lambert → Oren-Nayar 1994 → EON 2024
图 4-46粗糙漫反射的修正时间线:Lambert → Oren-Nayar 1994 → EON 2024@ 本系列绘制
EON 对 Oren-Nayar 的能量修正:粗糙度接近上限时的能量偏差与归一化后的收敛
图 4-47EON 对 Oren-Nayar 的能量修正:粗糙度接近上限时的能量偏差与归一化后的收敛@ 本系列绘制

3.6 这一章留下的问题

漫反射项讲完了。但真正让 PBR 区别于传统光照模型的,是高光那一半——也就是"镜面反射怎么算"。而它需要一个至今仍在被讨论的理论框架。下一章进入微平面理论。


四、微平面理论:D、G、F 与法线分布

4.1 核心思想:宏观的粗糙来自微观的分散

下面我们开始。微平面理论的出发点是这么一个观察:看起来粗糙的表面,在微观上是由大量朝向各异的小平面组成的,而每个小平面本身是光滑的。

宏观粗糙表面由微观光滑小面构成:微法线的分散程度决定了宏观的粗糙观感
图 4-48宏观粗糙表面由微观光滑小面构成:微法线的分散程度决定了宏观的粗糙观感@ 本系列绘制

这个想法把"粗糙"从一个形容词变成了一个可计算的量——微面法线的分布。

微面法线的分布密度:集中对应锐利高光,分散对应宽高光
图 4-49微面法线的分布密度:集中对应锐利高光,分散对应宽高光@ 本系列绘制

Berkeley 的课程讲义里把它讲得很清楚:微面法线越集中(都指向宏观法线附近),高光越锐利;越分散,高光越宽 [11]。UCSD 的 BSDF 讲义也强调同一个点——NDF 本质上是微法线方向的概率密度函数 [12]。

“宏观粗糙来自微观分散”这句话的几何图景是这样:

微平面模型:宏观表面由大量朝向各异的微小镜面组成,粗糙度描述这些微法线的分散程度
图 4-50微平面模型:宏观表面由大量朝向各异的微小镜面组成,粗糙度描述这些微法线的分散程度@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

同一块表面的光线去向,逐条看是这样的:

微平面上的光线:每条光线各按其所在微法线的方向反射,只有对齐半程向量的那些才进入高光
图 4-51微平面上的光线:每条光线各按其所在微法线的方向反射,只有对齐半程向量的那些才进入高光@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/Theory

4.2 Cook-Torrance 的三项分解

1982 年,Cook 和 Torrance 把这个框架整理成一个可用的反射模型,把镜面项拆成三个因子相乘 [13]:

从 Cook-Torrance 到 Disney BRDF
图 4-52从 Cook-Torrance 到 Disney BRDF@ 本系列绘制

$$ 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)}$ 归一化分母 抵消方向投影带来的偏差 必须
Cook-Torrance 的三项分解:D 决定高光形状、F 决定反射强度、G 决定遮挡损失
图 4-53Cook-Torrance 的三项分解:D 决定高光形状、F 决定反射强度、G 决定遮挡损失@ 本系列绘制

先来看看,为什么要除那个分母。 这一项看起来最没道理,但它恰恰是微平面理论"成立"的关键。微平面模型描述的是微面上的物理量,而我们观测的是宏观表面上的量——两者之间差了一个方向投影的换算。分母里的两个余弦因子就是这个换算,系数 4 是立体角与面积的联合换算结果。

微面量与宏观量的换算:方向投影与系数 4 的来源
图 4-54微面量与宏观量的换算:方向投影与系数 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 归一化约束的几何含义:半球上按投影加权的微法线总量恒为 1
图 4-55NDF 归一化约束的几何含义:半球上按投影加权的微法线总量恒为 1@ 本系列绘制

好了,这条约束的实用价值在于它是一个筛子:

NDF 是否满足归一化 后果
Phong / Blinn-Phong 不满足 非能量守恒,随粗糙度变化会出现能量漂移
Beckmann 满足 可用
GGX / Trowbridge-Reitz 满足 可用

这解释了为什么 Blinn-Phong 虽然是实时渲染里最经典的高光模型,却在新标准里被弃用——不是因为它不好看,而是因为它不合法。

Blinn-Phong 的能量漂移:随指数变化反射总能量不守恒
图 4-56Blinn-Phong 的能量漂移:随指数变化反射总能量不守恒@ 本系列绘制

法线分布函数的形状,可以看这张对照:

法线分布函数(NDF):微面朝向随粗糙度的分布形状
图 4-57法线分布函数(NDF):微面朝向随粗糙度的分布形状@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/Theory

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 形式在图形学界重新流行 实时标准
NDF 演化时间线:1923 Berry → 1963 Beckmann → 1973 Phong → 1975 Trowbridge-Reitz → 1977 Blinn-Phong → 2007 GGX 命名
图 4-58NDF 演化时间线:1923 Berry → 1963 Beckmann → 1973 Phong → 1975 Trowbridge-Reitz → 1977 Blinn-Phong → 2007 GGX 命名@ 本系列绘制

依然是一处容易被忽略的地方:有一点值得注意: GGX 不是 2007 年提出的新分布,而是 1975 年 Trowbridge-Reitz 分布的重命名。 中文社区的 NDF 谱系整理里这一点讲得比我更细 [15]。之所以值得强调,是因为它说明图形学里的"新方法"常常是旧方法在合适的时候被重新发现。

GGX 公式与其尾部形状:α 增大时尾部变重、高光展宽
图 4-59GGX 公式与其尾部形状:α 增大时尾部变重、高光展宽@ 本系列绘制

4.5 Beckmann 与 GGX 的差异:尾部

两个分布的关键差别在尾部。

用粗糙度的直观解释:Beckmann 的分布尾部衰减快,意思是"偏离宏观法线很远的微面"很少;GGX 的尾部更重,这类微面更多。

Beckmann 与 GGX 的分布尾部对比:GGX 尾部更重,离轴高光能量更多,高光边缘更自然
图 4-60Beckmann 与 GGX 的分布尾部对比:GGX 尾部更重,离轴高光能量更多,高光边缘更自然@ 本系列绘制

这个差异在画面上的表现:

  • 用 Beckmann,高光会有一个明显的"边界",边缘外突然变暗
  • 用 GGX,高光边缘拖着一条渐弱的尾巴,看起来更像真实材质
Beckmann 与 GGX 的工程对照:尾部行为、离轴能量、高光边缘观感
图 4-61Beckmann 与 GGX 的工程对照:尾部行为、离轴能量、高光边缘观感@ 本系列绘制

cstopics 的百科条目给了一张对比表,指出 Beckmann 尾部似高斯、离轴高光能量少;GGX 尾部更重、高光更自然,而 Blinn-Phong 则完全不满足能量守恒 [16]。这也是 GGX 成为事实标准的原因——它不是最精确的,但是它在这个权衡点上最合适。

三个 NDF 候选的适用场景
图 4-62三个 NDF 候选的适用场景@ 本系列绘制

4.6 源码链接

完整的 NDF 实现与验证可以参考两个地方:Filament 的材质文档给出了它选用的分布与参数 [17];Walter 等人的原始论文给了三类分布的完整数学形式与推导 [14]。

4.7 这一章留下的问题

$D$ 和 $F$ 都清楚了,但 $G$ 只说了"必须有"。它其实有两种近似,而这两种近似在掠射角的表现差别明显——而且混用会导致错误。下一章讲它,以及菲涅尔项里那个"两套 $k$ 映射不能混用"的坑。


五、几何项与菲涅尔:掠射角那点事

5.1 几何项在解决什么

先来看看,几何项 $G$ 描述的是微面之间的互相遮挡。当视线或光线接近掠射角时,微面之间会互相挡住,能有效参与反射的比例下降。

它要处理两类现象,名字很形象:

  • 遮蔽(masking):出射方向被别的微面挡住
  • 阴影(shadowing):入射方向被别的微面挡住
几何项要处理的两类遮挡:遮蔽(挡住出射)与阴影(挡住入射),以及凹槽内的多次互反射
图 4-63几何项要处理的两类遮挡:遮蔽(挡住出射)与阴影(挡住入射),以及凹槽内的多次互反射@ 本系列绘制

还有第三类——互反射(interreflection),也就是光在微面间弹多次才离开。它在第七章会成为一个主角,因为单散射模型正是把它当作不存在。

三类几何事件的几何形态:遮蔽、阴影、互反射
图 4-64三类几何事件的几何形态:遮蔽、阴影、互反射@ 本系列绘制

几何项在解决的遮挡关系,可以直接看到:

几何项:微面之间互相遮挡(masking)的几何关系
图 4-65几何项:微面之间互相遮挡(masking)的几何关系@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/Theory

5.2 Smith 几何项的两种近似

工程上用的 $G$ 几乎都是 Smith 形式,而 Smith 形式有两种实现方式:

形式 思路 代价 掠射角表现
非相关式(separable) 把遮蔽与阴影各自独立算再相乘 低 掠射角偏暗
相关式(correlated) 假设遮蔽与阴影的高度相关,一并处理 略高 更接近参考解
Smith 几何项的非相关式与相关式在掠射角的表现差异:非相关式会偏暗
图 4-66Smith 几何项的非相关式与相关式在掠射角的表现差异:非相关式会偏暗@ 本系列绘制

两种近似的差别,体现在遮蔽与阴影的合成方式上:

几何阴影(masking–shadowing):两种 Smith 近似的对比
图 4-67几何阴影(masking–shadowing):两种 Smith 近似的对比@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/Theory

相关式的一个常见实现是(社区推导里给了可直接对照的代码 [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,言归正传。拿水面举例:站在岸边俯视水面,能看到水下的石头(透射为主);贴着水面平视,水面像镜子一样反光(反射为主)。这就是菲涅尔效应。

菲涅尔对观感的影响区间
图 4-68菲涅尔对观感的影响区间@ 本系列绘制
Fresnel 反射率随入射角变化:正入射时约 4%,接近 90° 时迅速逼近 100%
图 4-69Fresnel 反射率随入射角变化:正入射时约 4%,接近 90° 时迅速逼近 100%@ 本系列绘制

真实的菲涅尔方程(Fresnel equations)依赖偏振与折射率,计算偏重。图形学里普遍用 Schlick 近似:

$$ F = F_0 + (1 - F_0)(1 - \cos\theta)^5 $$

其中 $F_0$ 是正入射时的反射率。那个五次方不是拟合出来的花招——它是 Schlick 观察真实曲线后选的一个"计算便宜且形状接近"的幂次。

Schlick 近似的计算代价
图 4-70Schlick 近似的计算代价@ 本系列绘制

这里用上了阶段 2 找回来的那张素材。 下图是 Wikimedia Commons 上维基百科条目所用的反射率曲线,横轴是入射角,纵轴是反射率:

Fresnel 反射率随入射角变化:从正入射的约 4% 迅速升至掠射角附近的 100%
图 4-71Fresnel 反射率随入射角变化:从正入射的约 4% 迅速升至掠射角附近的 100%@ Wikimedia Commons · Cepheiden(CC BY-SA 3.0)

菲涅尔在实拍里最好认——金属球的边缘明显更亮:

实拍金属球反射:掠射角(边缘)反射率显著升高,正是菲涅尔效应
图 4-72实拍金属球反射:掠射角(边缘)反射率显著升高,正是菲涅尔效应@ 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

值得盯一眼曲线的形状:从 0° 到 60° 左右,反射率几乎平着走;60° 之后开始加速;到 85° 以上几乎是垂直上升。这个"前段平、后段陡"的形状解释了画面上的现象——掠射角出现的那一圈亮边不是渲染错误,是真实的物理行为。

Schlick 五次方近似与真实 Fresnel 方程的曲线对照:在 60°–90° 区间偏差最大
图 4-73Schlick 五次方近似与真实 Fresnel 方程的曲线对照:在 60°–90° 区间偏差最大@ 本系列绘制

菲涅尔的落地还牵涉高光模式的选择:

高光模式(specular mode):不同模式对掠射角高光的处理不同
图 4-74高光模式(specular mode):不同模式对掠射角高光的处理不同@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

掠射角为什么变亮,这张图给出了几何解释:

掠射角(grazing angles):视线越接近平行于表面,菲涅尔反射率越高
图 4-75掠射角(grazing angles):视线越接近平行于表面,菲涅尔反射率越高@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

忽略粗糙度时,菲涅尔曲线是这样的:

菲涅尔曲线(不含粗糙度):理想镜面下的反射率随入射角变化
图 4-76菲涅尔曲线(不含粗糙度):理想镜面下的反射率随入射角变化@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/Theory

菲涅尔最直观的实例是水面——视线越接近掠射,反射越强:

掠射反射(湖面):视线接近水平时反射率急剧上升,远处水面近乎成为镜子
图 4-77掠射反射(湖面):视线接近水平时反射率急剧上升,远处水面近乎成为镜子@ Wikimedia Commons · Trauenbaum(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3ALandscape%20mirror%20reflection%20Lough%20Leane%20Killarney.jpg

同样的现象在低日照下更明显:

掠射反射(低日照峡湾):光源与视线都在低角度,镜面反射把整片水面点亮
图 4-78掠射反射(低日照峡湾):光源与视线都在低角度,镜面反射把整片水面点亮@ Wikimedia Commons · 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

掠射角下的反射还会带上环境的颜色:

掠射反射(彩色倒影):反射率升高后,环境中的颜色被如实带进反射里
图 4-79掠射反射(彩色倒影):反射率升高后,环境中的颜色被如实带进反射里@ Wikimedia Commons · Alias 0591 from the Netherlands(CC BY 2.0)· https://commons.wikimedia.org/wiki/File%3AColored%20reflection%20%288393387310%29.jpg

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 着色,故带颜色
常见材质的 F0 取值表:介质集中在 0.02–0.05,金属则落在 0.66–1.0
图 4-80常见材质的 F0 取值表:介质集中在 0.02–0.05,金属则落在 0.66–1.0@ 本系列绘制

这些数字在第九章还会用到,届时它们会变成"两套工作流怎么取舍"的判据。$F_0$ 的参考值在 learnopengl 的 PBR 教程里有线性与 sRGB 两列对照 [19];100x 计算机图形学指南第 29 章给了同样的表并补充了常见错误的症状特征 [20]。

F0 取值带:介质集中在 0.02–0.05 的窄带,金属落在 0.66–1.0 的宽带
图 4-81F0 取值带:介质集中在 0.02–0.05 的窄带,金属落在 0.66–1.0 的宽带@ 本系列绘制

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$
直接光与 IBL 两套 k 映射曲线对照:混用会让掠射角明显过暗
图 4-82直接光与 IBL 两套 k 映射曲线对照:混用会让掠射角明显过暗@ 本系列绘制

learnopengl 在教程里明确点出了这个区别 [19]。但更值得注意的是 Karis 在 UE4 论文里的另一句提醒:GGX 的 $k = (\text{Roughness}+1)^2/8$ 重映射是给解析光源用的,用在 IBL 上会导致掠射角过暗 [4]。

这句话在中文教程里出现得很少。而它的后果很具体:IBL 环境下物体的边缘会有一圈不自然的暗边。 如果你遇到过"环境光下物体轮廓发暗"的现象,八成就是这里混用了。

k 映射混用的画面后果:物体轮廓出现一圈不自然暗边
图 4-83k 映射混用的画面后果:物体轮廓出现一圈不自然暗边@ 本系列绘制

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]。

三种几何项实现的精度与成本对照:非相关式 / 相关式 / 解析近似
图 4-84三种几何项实现的精度与成本对照:非相关式 / 相关式 / 解析近似@ 本系列绘制

5.7 这一章留下的问题

到这里,单散射的 BRDF 已经完整了:漫反射项(Lambert 或 Oren-Nayar)+ 镜面项(D·G·F)。它能画出非常漂亮的材质球。

但它有一个系统性的错误——在高粗糙度下会丢失能量。而这个错误的成因,恰好就是 5.1 节提到但没有展开的第三类现象:互反射。下一章专门讲它。


六、单散射的缺口:多散射补偿

6.1 先把能量去哪了说清楚

好了,第五章结尾留下了一个悬念:单散射模型在高粗糙度下会丢能量。这一章就来算这笔账。

检验方法叫 furnace test(白炉测试):把一个物体放进一个颜色完全均匀、各方向亮度一致的白炉里,让它的材质设定为"完全漫反射白色"或"金属白"。如果渲染器是正确的,物体应该完全融入背景,看不出轮廓——因为反射的能量恰好等于收到的能量。

furnace test 的原理:均匀白色环境中的物体应与背景无缝;出现暗轮廓即说明丢了能量
图 4-85furnace test 的原理:均匀白色环境中的物体应与背景无缝;出现暗轮廓即说明丢了能量@ 本系列绘制

实际跑下来会发生什么?金属材质在粗糙度低时几乎无缝;粗糙度升到 0.8 以上,球体开始显出暗色的轮廓,越粗糙越暗。

白炉测试的三个判读
图 4-86白炉测试的三个判读@ 本系列绘制
单散射模型的能量损失曲线:roughness 从 0 升到 1 时,保留的能量单调下降,介质的损失比金属更快
图 4-87单散射模型的能量损失曲线:roughness 从 0 升到 1 时,保留的能量单调下降,介质的损失比金属更快@ 本系列绘制

说白了,为什么?回到微平面理论:微面之间会互相遮挡。被遮挡的那部分微面,单散射模型认为"它们不参与反射,直接丢弃"。但物理上,光碰到一个微面被挡住之后,会弹到相邻的微面上去,可能弹两三次才离开表面。

单散射只算了"弹一次就离开"的那部分,弹两次以上的一律没算。粗糙度越高,微面越倾斜,需要多次反弹的比例越大——所以损失也越大。

单次、两次、三次反弹各占的能量份额随粗糙度的变化
图 4-88单次、两次、三次反弹各占的能量份额随粗糙度的变化@ 本系列绘制
单散射与多次散射的路径对比:单散射只计一次反弹,多次散射包含微面间的能量传递
图 4-89单散射与多次散射的路径对比:单散射只计一次反弹,多次散射包含微面间的能量传递@ 本系列绘制

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) 补偿量小,因为大部分能量本就以漫反射形式出来了
三类物体的多散射补偿对照:镜面反射体损失最大、介质最小
图 4-90三类物体的多散射补偿对照:镜面反射体损失最大、介质最小@ 本系列绘制

下面我们开始。用一句话概括这篇论文的贡献:它给了一个"用单散射算两次"的廉价补偿法——一次算常规的单散射结果,一次算"假设表面是纯白、Fresnel 为 1"的能量上限,两者之比就是丢失的比例。

三类物体的补偿量随粗糙度变化:镜面反射体补偿最多、介质最少
图 4-91三类物体的补偿量随粗糙度变化:镜面反射体补偿最多、介质最少@ 本系列绘制

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 完全无缝。如果你在项目里看到"极粗糙金属仍有轻微偏暗",这是预期的,不是实现错误。

多散射补偿的取舍清单
图 4-92多散射补偿的取舍清单@ 本系列绘制
补偿前后 furnace test 对照:暗轮廓显著减弱但仍非完全无缝
图 4-93补偿前后 furnace test 对照:暗轮廓显著减弱但仍非完全无缝@ 本系列绘制

6.7 这一章留下的问题

到这里,直接光照这条路已经完整了——漫反射、镜面反射、几何项、菲涅尔、多散射补偿。

OK,言归正传。但直接光照只解决了"光源直接照到"的情况。现实世界里,物体受到的光大部分来自环境:天空、地面、周围的墙面。这些光没有明确的方向,不能当解析光源处理,数量上也不可能逐个遍历。

下一章讲业界怎么把"整个环境"当成一个光源来算。


七、IBL:把环境光变成可预计算的卷积

7.1 基本思路:把环境贴图当成一张光的方向图

下面我们开始。IBL(Image-Based Lighting)的核心想法只有一句话:既然环境光从四面八方来,那就用一张环境贴图(通常是等距柱状投影的全景图或 cubemap)记录下每个方向的亮度,把它当成光源。

等距柱状投影的环境贴图:每个像素对应一个入射方向,整张图就是一张「光的方向分布图」
图 4-94等距柱状投影的环境贴图:每个像素对应一个入射方向,整张图就是一张「光的方向分布图」@ 本系列绘制

下面两张是阶段 2 找来的真实全景图,它们就是 IBL 的输入长什么样:

室外等距柱状全景:天空与地面的亮度差直接决定了环境光的方向性
图 4-95室外等距柱状全景:天空与地面的亮度差直接决定了环境光的方向性@ Wikimedia Commons · DerMische(CC BY-SA 4.0)

可作为 IBL 光源的等距柱状实拍全景:

等距柱状实拍全景:可直接作为 IBL 的环境光源
图 4-96等距柱状实拍全景:可直接作为 IBL 的环境光源@ Alexandre Duret-Lutz from Paris, France(CC BY-SA 2.0)· https://commons.wikimedia.org/wiki/File:Dent_de_Vaulion_-_360_degree_panorama.jpg
室内等距柱状全景
图 4-97室内等距柱状全景@ Wikimedia Commons · Diliff(CC BY-SA 3.0)

环境贴图在内存里是这样展开的:

等距柱状投影(equirectangular):环境贴图从球面到平面
图 4-98等距柱状投影(equirectangular):环境贴图从球面到平面@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Diffuse-irradiance

它映射回场景后成为光照的来源:

环境贴图映射回场景:整张全景成为场景的光照来源
图 4-99环境贴图映射回场景:整张全景成为场景的光照来源@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Diffuse-irradiance

再多一张不同光照条件的环境贴图:

另一张环境全景:不同光照条件下的 HDRI 光源
图 4-100另一张环境全景:不同光照条件下的 HDRI 光源@ DerMische(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Nebel_Schienen_Panorama.jpg

把两张图对比着看,能立刻理解为什么环境光不能用一个常数表示:室外图里上方(天空)明显亮于下方(地面);室内图里窗户位置有几个极亮的斑点。如果用同一个常数当环境光,这两个场景会渲染成一模一样的漫反射底色。

但问题也立刻来了:渲染方程要求对整个半球积分,现在半球上每个方向都有一个值,这个积分怎么算?

环境贴图与半球方向的映射关系:每个像素对应一个入射方向
图 4-101环境贴图与半球方向的映射关系:每个像素对应一个入射方向@ 本系列绘制

如果每个像素实时采样——比如采 1024 个方向、每个方向查一次贴图——成本是每像素上千次纹理采样。完全没有可接受性。

IBL 的第一步是把环境贴图当成一张“光从哪个方向来”的方向图,等距柱状投影的全景图就是这个形式:

等距柱状全景(ESO 台址):经度方向均匀、纬度方向按 $\sin$ 压缩,是环境贴图的常见存储格式
图 4-102等距柱状全景(ESO 台址):经度方向均匀、纬度方向按 $\sin$ 压缩,是环境贴图的常见存储格式@ Wikimedia Commons · ESO(CC BY 4.0)· https://commons.wikimedia.org/wiki/File%3A360-degree%20panorama%20of%20the%20ESO%20Headquarters%20%28hqe-pano2%29.jpg

换一个场地看,同一格式下的光照分布差异很明显:

等距柱状全景(ALMA):空旷高地的环境以天空为主,与室内场景的辐照度分布完全不同
图 4-103等距柱状全景(ALMA):空旷高地的环境以天空为主,与室内场景的辐照度分布完全不同@ Wikimedia Commons · ESO/ S. Brunier(CC BY 4.0)· https://commons.wikimedia.org/wiki/File%3AALMA%20360%20degree%20panorama%20%28pano360-alma%29.jpg

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 的强度。两者相乘近似原来的积分。

split-sum 的两项分解:① 按粗糙度预滤波的环境贴图(与视角相关)× ② 与环境无关的 BRDF LUT
图 4-104split-sum 的两项分解:① 按粗糙度预滤波的环境贴图(与视角相关)× ② 与环境无关的 BRDF LUT@ 本系列绘制

这个近似的精度,Karis 自己在论文里给了评价:对常量入射光是精确的,对常见环境相当准确 [4]。

split-sum 的适用边界
图 4-105split-sum 的适用边界@ 本系列绘制

为什么它可以被预计算——这就要回到第二章提到的 BRDF 线性性质。因为 $f_r$ 与入射光无关,所以两个积分里与光无关的部分都可以提前算好:

  • 第一项只依赖粗糙度与视角,可以按粗糙度分级预滤波,存进 cubemap 的 mip 层级
  • 第二项只依赖粗糙度与 $\cos\theta_v$,可以做成一张二维查找表

运行时每像素只做几次纹理采样,成本从"上千次"降到了"几次"。预滤波与 BRDF LUT 的完整推导过程另有两份同源材料可对照:课程笔记镜像与演讲 slides [107] [108]。

成本对比:每像素实时采样上千次 vs 预计算后两次查表
图 4-106成本对比:每像素实时采样上千次 vs 预计算后两次查表@ 本系列绘制

7.3 $N=V=R$:比 split-sum 更大的误差源

这一节是本文最想强调的地方之一。

下面进入。为了让第一项可预计算,Karis 引入了一个假设:$N = V = R$,也就是"法线、视线、反射方向三者重合"。物理上这意味着"垂直观察一个平坦表面",此时反射方向恰好等于视线方向。

N=V=R 假设的代价:掠射角处本应出现的细长反射被近似掉了
图 4-107N=V=R 假设的代价:掠射角处本应出现的细长反射被近似掉了@ 本系列绘制

代价是什么?论文里的原话是:掠射角处得不到细长反射(don't get lengthy reflections at grazing angles) [4]。

而更重要的是论文紧接着的那句评价——它明确说明 $N=V=R$ 的各向同性假设"是 IBL 方案中比 split-sum 更大的误差源" [4]。

这句话的含义值得展开:业界普遍担心的是 split-sum 这个数学近似的精度,但论文作者自己认为,真正的误差大头在 $N=V=R$ 这个几何假设上。

IBL 两个误差源的对照:split-sum 数学近似 vs N=V=R 几何假设,后者被论文作者判为更大的误差源
图 4-108IBL 两个误差源的对照:split-sum 数学近似 vs N=V=R 几何假设,后者被论文作者判为更大的误差源@ 本系列绘制

好了,这条信息在中文文章里几乎看不到。 我逐条核验了 Karis 的原始 PDF 才确认这句话的存在。它的实际影响是:如果你在调一个"反射看起来不够长、不够拉丝"的金属材质,问题不在 split-sum,而在 $N=V=R$。 补偿手段是加屏幕空间反射(SSR)做补充,或者用抛物面/视差校正的探针,而不是去提高预滤波的分辨率。

同一金属材质在 N=V=R 与含 SSR 补偿下的反射形态对照:后者出现细长拉丝
图 4-109同一金属材质在 N=V=R 与含 SSR 补偿下的反射形态对照:后者出现细长拉丝@ 本系列绘制

7.4 一个容易被忽略的细节:$\cos\theta_l$ 加权

预滤波采样时,Karis 是按 $\cos\theta_l$ 加权的 [4]。这个加权不在简化的公式里,是工程实现时补上的。

是否做 cosθl 加权对预滤波结果的影响:不加权会在高粗糙度下出现亮度偏差
图 4-110是否做 cosθl 加权对预滤波结果的影响:不加权会在高粗糙度下出现亮度偏差@ 本系列绘制

为什么要加?因为渲染方程里本来就带着 $(\omega_i \cdot n)$ 这个余弦因子,而预滤波阶段如果用 GGX 重要性采样直接平均,会把斜向的样本与正对的样本等权看待——这与原方程的加权方式不一致。补上 $\cos\theta_l$ 之后,预滤波的结果才与"按渲染方程积分"接近。

这类细节的特点是:不看原始论文就不知道,而不知道也不会报错。

cosθl 加权前后的预滤波结果差异:高粗糙度下的亮度偏差
图 4-111cosθl 加权前后的预滤波结果差异:高粗糙度下的亮度偏差@ 本系列绘制

7.5 漫反射那一路:辐照度卷积与球谐

下面我们开始。上面讲的都是镜面反射。漫反射那一路相对简单,但也有自己的选择。

IBL 两条路径的分工
图 4-112IBL 两条路径的分工@ 本系列绘制

方案一:辐照度卷积(irradiance convolution)。 把环境贴图对每个方向做半球积分,得到一张低频的"辐照度图"。运行时只需一次采样。learnopengl 的教程把这一步讲得比较清楚 [26]。

代价是:卷积需要预计算,且辐照度图是全局的——它不知道"这个点被墙挡住了"。

方案二:球谐函数(Spherical Harmonics)表示。 2001 年,Ramamoorthi 和 Hanrahan 的论文给出了一个结论:辐照度环境图用球谐表示时,只需要 3 个频带(9 个系数)就能达到平均误差低于 3% [27]。

这意味着什么?把整个环境的光照信息压缩成 9 个浮点数,就能重建出漫反射光照,而且误差不到 3%。这是 IBL 之所以能在移动端和游戏主机上大规模使用的基础——内存开销从"一张 cubemap"降到了"36 字节"。

内存开销对照:完整 cubemap 与 9 系数 SH 的数量级差异
图 4-113内存开销对照:完整 cubemap 与 9 系数 SH 的数量级差异@ 本系列绘制
球谐九系数重建环境光:0–2 阶共 9 个系数即可逼近漫反射辐照度,平均误差低于 3%
图 4-114球谐九系数重建环境光:0–2 阶共 9 个系数即可逼近漫反射辐照度,平均误差低于 3%@ 本系列绘制

引擎里环境光的有无,对照非常明显:

引擎中的环境光(天空 IBL)对照:有无环境光的场景差异
图 4-115引擎中的环境光(天空 IBL)对照:有无环境光的场景差异@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html

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]。

SH 各阶系数对辐照度的贡献占比:0–2 阶已覆盖绝大部分能量
图 4-116SH 各阶系数对辐照度的贡献占比:0–2 阶已覆盖绝大部分能量@ 本系列绘制

辐照度卷积的结果是一张低频图:

辐照度图(irradiance map):环境光经半球卷积后的低频结果
图 4-117辐照度图(irradiance map):环境光经半球卷积后的低频结果@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Diffuse-irradiance

辐照度卷积要算的是半球上所有方向的加权和,采样方式是这样的:

半球采样:对上半球各方向按 $\cos\theta$ 加权重采样,得到该法线下的辐照度
图 4-118半球采样:对上半球各方向按 $\cos\theta$ 加权重采样,得到该法线下的辐照度@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Diffuse-irradiance

换成另一个法线方向,积分域随之旋转:

半球采样(带法线):积分域随法线旋转,这正是辐照度需要按法线方向查表的原因
图 4-119半球采样(带法线):积分域随法线旋转,这正是辐照度需要按法线方向查表的原因@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Diffuse-irradiance

把这件事写成积分,就是一个在球面上的卷积:

球面积分:辐照度等于环境贴图与 $\cos\theta$ 核在半球上的卷积
图 4-120球面积分:辐照度等于环境贴图与 $\cos\theta$ 核在半球上的卷积@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Diffuse-irradiance

7.6 这一章留下的问题

原理讲完了:split-sum 拆成两项,一项预滤波、一项查表;漫反射那一路可以用 SH 压到 9 个系数。

但工程上还有一堆具体问题:预滤波的 mip 链怎么组织?BRDF LUT 的两个输出怎么存、用什么精度?环境是一个立方体、物体在中间移动时视差怎么办?探针之间怎么混合?

下一章就是这些落地问题。


八、预滤波、BRDF LUT 与探针

8.1 预滤波环境贴图:mip 链与粗糙度的对应

下面进入。第一项积分的实现方式是把环境贴图按粗糙度分级预滤波,每一级对应一个 mip 层。

预滤波 mip 数量的取舍
图 4-121预滤波 mip 数量的取舍@ 本系列绘制
预滤波 mip 链:mip 0 对应 roughness 0(清晰镜像反射),随 mip 层级上升粗糙度增加、反射逐渐模糊
图 4-122预滤波 mip 链:mip 0 对应 roughness 0(清晰镜像反射),随 mip 层级上升粗糙度增加、反射逐渐模糊@ 本系列绘制

具体做法:

  • mip 0 存原始的、最高频的环境贴图,对应 roughness 0——清晰的镜像反射
  • mip 1、2、3…… 依次存储用不同粗糙度卷积后的结果
  • 运行时按材质的 roughness 选择 mip 层级,mip 之间做三线性插值

为什么用 GGX 分布做卷积而不是简单地降低分辨率——这是关键区别。简单降分辨率得到的是"高斯模糊",而 GGX 重要性采样得到的是"物理上正确的粗糙反射"。两者的差异在高光边缘处看得出来:高斯模糊的高光边缘是均匀渐晕,GGX 则拖着一条符合菲涅尔规律的尾巴。

高斯模糊与 GGX 卷积的高光边缘对照:前者均匀渐晕、后者带菲涅尔尾巴
图 4-123高斯模糊与 GGX 卷积的高光边缘对照:前者均匀渐晕、后者带菲涅尔尾巴@ 本系列绘制

预滤波的结果是一条 mip 链,粗糙度逐级递增:

预滤波环境贴图(prefilter map):mip 层级与粗糙度一一对应
图 4-124预滤波环境贴图(prefilter map):mip 层级与粗糙度一一对应@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

粗糙度对反射的影响,引擎里可以直接对照:

粗糙度(roughness):数值越低反射越锐利,直接决定预滤波 mip 的取用层级
图 4-125粗糙度(roughness):数值越低反射越锐利,直接决定预滤波 mip 的取用层级@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

预滤波要处理的正是这个镜面波瓣:

镜面波瓣(specular lobe):粗糙度决定波瓣的宽窄,预滤波即对波瓣积分
图 4-126镜面波瓣(specular lobe):粗糙度决定波瓣的宽窄,预滤波即对波瓣积分@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

预滤波实现不当会在 mip 之间留下接缝:

预滤波接缝(seams):mip 层级之间采样不一致导致的可见缝
图 4-127预滤波接缝(seams):mip 层级之间采样不一致导致的可见缝@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

learnopengl 的镜面 IBL 教程详细讲了预滤波的实现步骤 [31];有一篇 2025 年的笔记(时效较新)把 GGX 重要性采样与蒙特卡洛的关系讲得更清楚 [32];另有一篇把"按 roughness 存 mip、$N=V=R$ 假设"的来龙去脉梳理了一遍 [33]。

预滤波要靠重要性采样把样本集中到高贡献方向,PDF 就是它的分布依据:

概率密度函数:预滤波按 NDF 的分布采样,样本才会落在真正贡献大的方向附近
图 4-128概率密度函数:预滤波按 NDF 的分布采样,样本才会落在真正贡献大的方向附近@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

按这个分布采出来的样本落点是这样, roughness 越大分布越散:

预滤波采样:随粗糙度增大,样本分布从高度集中变为大范围散开,对应 mip 链上的不同层级
图 4-129预滤波采样:随粗糙度增大,样本分布从高度集中变为大范围散开,对应 mip 链上的不同层级@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

8.2 BRDF LUT:两个输出与 R16G16 精度

第二项积分的实现是一张二维查找表。

下面进入。它的输入输出关系值得记牢:

内容
输入 1 roughness
输入 2 $\cos\theta_v$
输出 1 scale(对 $F_0$ 的缩放系数,常记为 A)
输出 2 bias(常数偏移,常记为 B)
BRDF LUT 的二维参数空间:横轴 cosθv、纵轴 roughness,两个输出通道分别是 scale 与 bias
图 4-130BRDF LUT 的二维参数空间:横轴 cosθv、纵轴 roughness,两个输出通道分别是 scale 与 bias@ 本系列绘制

为什么只有两个输出。 因为 $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 区域变化剧烈,低精度格式会在这里产生可见的条带。

LUT 分辨率与精度的权衡
图 4-131LUT 分辨率与精度的权衡@ 本系列绘制
BRDF LUT 两个通道的取值分布热力图:A 在小 roughness 区变化剧烈
图 4-132BRDF LUT 两个通道的取值分布热力图:A 在小 roughness 区变化剧烈@ 本系列绘制
BRDF LUT 的存储格式选择:作者强调精度重要、采用 R16G16,低精度会在小 roughness 区域产生条带
图 4-133BRDF LUT 的存储格式选择:作者强调精度重要、采用 R16G16,低精度会在小 roughness 区域产生条带@ 本系列绘制

split-sum 的第二项就是这张预先算好的查找表:

BRDF LUT:split-sum 中预计算的积分查找表
图 4-134BRDF LUT:split-sum 中预计算的积分查找表@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

BRDF LUT 的积分依赖低差异序列采样:

低差异序列(low-discrepancy sequence):用更均匀的采样点降低 LUT 的方差
图 4-135低差异序列(low-discrepancy sequence):用更均匀的采样点降低 LUT 的方差@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

生成这张表的代码(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$ 是带颜色的。

镜面 IBL 运行时的数据流:反射方向 → 预滤波 mip → BRDF LUT → scale/bias 组合
图 4-136镜面 IBL 运行时的数据流:反射方向 → 预滤波 mip → BRDF LUT → scale/bias 组合@ 本系列绘制

两项合起来的结果是这样:

IBL 合成结果:环境光照射下的金属与粗糙材质
图 4-137IBL 合成结果:环境光照射下的金属与粗糙材质@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

换一套环境贴图,同一材质的观感随之改变:

不同环境贴图下的同一材质:IBL 让材质真正“进入”环境
图 4-138不同环境贴图下的同一材质:IBL 让材质真正“进入”环境@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/IBL/Specular-IBL

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 按包围盒与优先级
三大引擎反射探针机制对照:捕获形状、视差校正手段、混合策略与关键参数
图 4-139三大引擎反射探针机制对照:捕获形状、视差校正手段、混合策略与关键参数@ 本系列绘制

数据来源分别是 Unreal 的 Reflections Captures 官方文档 [35]、Unity 的 Reflection Probe 手册 [36] 与 Godot 4.0 的文档 [37]。

box projection 的原理值得单独说一下,因为它是这三家里最容易被误解的机制。

核心问题是:cubemap 是在某个位置捕获的,如果被观察的物体移动了,反射就"错位"了——本该映出墙面的方向可能映出天空。

box projection 的做法是:把反射方向与一个包围盒求交,用交点作为采样方向。

box projection 视差校正原理:反射射线与包围盒求交,交点方向代替原始反射方向作为采样方向
图 4-140box projection 视差校正原理:反射射线与包围盒求交,交点方向代替原始反射方向作为采样方向@ 本系列绘制

这样就得到了近似正确的视差效果——物体靠近盒子边缘时,反射内容会相应地"滑动"。

有无 box projection 的反射对照:无校正时反射内容错位
图 4-141有无 box projection 的反射对照:无校正时反射内容错位@ 本系列绘制

引擎里承接 IBL 的实体是反射探针:

引擎中的反射探针(Reflection Probe):局部环境反射的落地形态
图 4-142引擎中的反射探针(Reflection Probe):局部环境反射的落地形态@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/reflection_probes.html

与探针互补的是屏幕空间反射:

引擎中的屏幕空间反射(SSR):用已有画面推断反射
图 4-143引擎中的屏幕空间反射(SSR):用已有画面推断反射@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/reflection_probes.html

社区有一个改进实现值得参考:它指出经典 box projection 在盒子边角处的不足,并引入 contact hardening(接触硬化)改善掠射角附近的反射,同时给出了 Unity 默认实现与 Godot 近似做法的对比代码,称其"比经典方法有大的改进,且很便宜" [38]。

Unity 的 HDRP 里另有独立的反射探针参数体系 [39];要一份从"环境贴图当无限远光源"讲起的综述,可以参考一篇 DX12 的实现笔记 [40]。

室内场景是探针最容易露馅的地方——墙与地面把环境切成几个区域,box projection 就是为这种情形准备的:

室内等距柱状全景:空间被墙地切开,探针若只用一个球面近似,转折处会明显失真
图 4-144室内等距柱状全景:空间被墙地切开,探针若只用一个球面近似,转折处会明显失真@ Wikimedia Commons · 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

8.5 一个实现细节:探针混合的代价

好了,探针数量上去之后,混合本身会变成开销。

Unreal 的处理方式能说明问题:它不追求"所有探针都参与混合",而是让小 capture 覆盖大 capture,同时给粗糙表面设了明确的混合阈值——roughness 0.3 以上完全用 lightmap 的间接高光,0.1 以下完全用反射探针,中间过渡 [35]。

这个阈值设计背后有一个物理直觉:粗糙度高的表面,反射本身就很模糊,用哪一个探针的结果差别不大,用便宜的那个就行。所以把"贵的那条路"限制在低粗糙度区间,是性价比最高的分法。

这段的实用价值:如果你在做探针系统,不要一开始就追求"无缝混合所有探针"。先看粗糙度分布——如果场景里大部分是高粗糙材质,那么优化重点不在探针混合,而在环境贴图的 mip 质量。

探针混合权重随粗糙度的分布:0.1 以下用探针、0.3 以上用 lightmap
图 4-145探针混合权重随粗糙度的分布:0.1 以下用探针、0.3 以上用 lightmap@ 本系列绘制

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 本身就是颜色
直观性 参数少,但"金属"是派生量 更直接,但易出现物理上不可能的取值
出错方式 金属/非金属边界难画 可以画出能量超标的材质
MR 与 SG 两套工作流的参数对照:参数集合、金属表现方式、直观性与典型出错方式
图 4-146MR 与 SG 两套工作流的参数对照:参数集合、金属表现方式、直观性与典型出错方式@ 本系列绘制

两套工作流的转换关系(社区整理得比较清楚 [52]):按 specular 的亮度判定像素是不是金属——specular 亮而 diffuse 暗的是金属,反之为非金属;金属的 base color 取 specular map,非金属取 diffuse map;roughness 与 glossiness 的关系是:

MR 与 SG 两套工作流的通道对应关系与转换路径
图 4-147MR 与 SG 两套工作流的通道对应关系与转换路径@ 本系列绘制

$$ \text{roughness} = 1 - \text{glossiness} $$

引擎里的着色模式切到带贴图的一档,两套工作流的差异会更清楚:

引擎着色模式(带贴图):同一模型切换工作流后,粗糙度与金属度通道的解释随之改变
图 4-148引擎着色模式(带贴图):同一模型切换工作流后,粗糙度与金属度通道的解释随之改变@ Godot 官方文档(CC BY 4.0) · Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

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 想表达的东西。

glTF 材质扩展的收敛路径:SG 扩展进入 Archived,现行路径是 MR + KHR_materials_specular + KHR_materials_ior
图 4-149glTF 材质扩展的收敛路径:SG 扩展进入 Archived,现行路径是 MR + KHR_materials_specular + KHR_materials_ior@ 本系列绘制

为什么值得单独强调:很多教程仍把 MR / SG 描述成"两个可选工作流,看团队习惯"。但规范层面的选择已经做了——MR 是默认,SG 是历史。

glTF 材质扩展的演进时间线:SG 扩展从提出到移入 Archived
图 4-150glTF 材质扩展的演进时间线:SG 扩展从提出到移入 Archived@ 本系列绘制

这件事在实践中的体现是:如果你现在从零开始做一套材质管线,选 SG 意味着走一条正在被淘汰的路。已有的 SG 资产需要转换,而转换是有损的(金属掩码的判定、glossiness到roughness的映射都会引入偏差)。社区里有具体的迁移案例记录了这些偏差 [87]。

9.3 参数逐项释义:F0 取值带与 metallic 的二值性

这一节是本章的核心,也是阶段 2 找到的那两张通道图真正发挥作用的地方。

先看 metallic 通道。这一张来自一份金属走道(steel walkway)材质的 PBR 数据——

图 metallic 通道:只有纯黑与纯白的区域,没有中间灰
图 4-151metallic 通道:只有纯黑与纯白的区域,没有中间灰@ Wikimedia Commons · ambientCG / Lennart Demes(CC0) · 数据通道图,非彩色

再看同一份材质的 roughness 通道:

图 同一材质的 roughness 通道:连续灰阶,粗糙区明显更粗糙
图 4-152同一材质的 roughness 通道:连续灰阶,粗糙区明显更粗糙@ Wikimedia Commons · ambientCG.com / Lennart Demes(CC0) · 数据通道图,非彩色

粗糙度贴图则是一张灰度图,逐像素控制高光收敛程度:

粗糙度贴图(roughness):灰度逐像素控制高光扩散
图 4-153粗糙度贴图(roughness):灰度逐像素控制高光扩散@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Roughness.png

两张图放在一起,metallic 该怎么处理就一目了然了:现实材料要么是金属,要么不是,不存在 0.5 的金属。所以 metallic 应当作 0/1 处理,中间值只会出现在"材质过渡区"这种需要混合的少数位置。

把整个参数空间摊开来看更直观——下面这张来自 Blender 官方手册:上排是 roughness 从 0 到 1 的连续变化,下排是 metallic 从 0 到 1 的变化。同一 base color、同一光照条件下,两个参数各自独立地改变着反射形态:

metallic 与 roughness 的参数对照
图 4-154参数对照:上排 roughness 由光滑到粗糙(高光从锐利收窄到弥散铺开),下排 metallic 由介质到金属(反射率抬升、漫反射逐渐消失)@ Blender Foundation · Blender Manual(CC BY-SA 4.0)· https://docs.blender.org/manual/en/latest/render/shader_nodes/shader/principled.html

贴图集里的基础色通道,实际是一张灰度无关的彩色图:

基础色贴图(base color):只含反照率,不含光照信息
图 4-155基础色贴图(base color):只含反照率,不含光照信息@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Base.png

Filament 的材质文档把这条写成了明确的范围约定 [17]:

参数 取值范围 说明
金属 base color [0.66, 1.0](sRGB 约 170–255) 低于这个范围说明值不对
介质 base color [0.04, 0.94] 上限 0.94 对应"最亮的非金属"
metallic 0 或 1 应作二值处理
Filament 给出的 base color 取值带:金属 0.66–1.0、介质 0.04–0.94,超出范围说明参数有问题
图 4-156Filament 给出的 base color 取值带:金属 0.66–1.0、介质 0.04–0.94,超出范围说明参数有问题@ 本系列绘制

工业界的 PBR 贴图集长这样:

PBR 贴图集:albedo / normal / roughness / metallic / AO 各司其职
图 4-157PBR 贴图集:albedo / normal / roughness / metallic / AO 各司其职@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/Theory

再看实录材质能对上多少:

风化木材:非金属的 base color 落在 0.04–0.94 区间内,长年侵蚀让表面纹理层层显露
图 4-158风化木材:非金属的 base color 落在 0.04–0.94 区间内,长年侵蚀让表面纹理层层显露@ Wikimedia Commons · Kelsey Todd(CC BY-SA 4.0)
上漆木材:base color 与左图接近,但表面多了一层涂层——同样的 base color 出现两种反射行为
图 4-159上漆木材:base color 与左图接近,但表面多了一层涂层——同样的 base color 出现两种反射行为@ Wikimedia Commons · Shadowmeld Photography(CC BY-SA 4.0)
涂装木板:颜色来自涂层而不是材质本身——base color 描述的是表面当前状态
图 4-160涂装木板:颜色来自涂层而不是材质本身——base color 描述的是表面当前状态@ Wikimedia Commons · Nikodem Nijaki(CC BY-SA 3.0)

这三张图说明的是同一件事的两面:base color 的取值区间很宽(0.04–0.94),但决定"看起来像什么"的不只是 base color——第二张图与第一张 base color 接近,观感差异全在涂层带来的低粗糙度高光上。这也是为什么调材质时不能只盯 base color 一个参数。

金属那边则是另一种规律:

铜镀层:金属的 F0 是带颜色的——铜吸收蓝光,所以反射偏暖
图 4-161铜镀层:金属的 F0 是带颜色的——铜吸收蓝光,所以反射偏暖@ Wikimedia Commons · Michalgocki(CC BY 4.0)
金属废料:多种金属混杂,各自反射颜色不同——金属的 F0 带色源于自身吸收特性
图 4-162金属废料:多种金属混杂,各自反射颜色不同——金属的 F0 带色源于自身吸收特性@ Wikimedia Commons · Syced(CC0)
锈蚀钢筋:锈层把同一种金属的粗糙度推到极端,镜面高光几乎完全消失
图 4-163锈蚀钢筋:锈层把同一种金属的粗糙度推到极端,镜面高光几乎完全消失@ Wikimedia Commons · W.carter(CC BY-SA 4.0)

真实材质的粗糙与锈蚀,是粗糙度贴图要还原的对象:

锈蚀金属实拍:粗糙度与金属度的真实分布
图 4-164锈蚀金属实拍:粗糙度与金属度的真实分布@ C Avery(CC BY 3.0)· https://commons.wikimedia.org/wiki/File:Free_Texture_Rusty_Metal_(22819579).jpeg

铜那张解释了 SG 工作流为什么容易出错:在 SG 里,反射颜色是直接给的值,美术可以把一个"金属"的反射调成任意颜色;但在物理上,金属的反射颜色就是它的 base color,铜之所以反射偏暖,是因为它吸收了蓝光——这不是一个可以随便调的艺术参数。

介质那边同样有对应的实拍:

白釉瓷器:釉面是低 roughness 的介质,高光窄而锐
图 4-165白釉瓷器:釉面是低 roughness 的介质,高光窄而锐@ Wikimedia Commons · Windmemories(CC BY-SA 4.0)
元代釉瓷枕的釉面:与上图同类,但釉色不同——同样说明 base color 变化不影响高光形状
图 4-166元代釉瓷枕的釉面:与上图同类,但釉色不同——同样说明 base color 变化不影响高光形状@ Wikimedia Commons · Gary Todd(CC0)
皮肤特写:base color 的高散射一端,也是次表面散射出场的地方(本文不展开)
图 4-167皮肤特写:base color 的高散射一端,也是次表面散射出场的地方(本文不展开)@ Wikimedia Commons · Clump(CC0)

把陶瓷两张与木材三张对照着看,可以得到一条实用判断:同一个 base color 值配不同 roughness,观感差异可能大于不同 base color 配同一 roughness。所以材质制作里 roughness 的精度往往比 base color 更值得投入。

三个参数的实际覆盖面
图 4-168三个参数的实际覆盖面@ 本系列绘制

一个常被忽略的材质层——清漆:

清漆层(clearcoat)对照:在基础材质上再叠一层镜面反射
图 4-169清漆层(clearcoat)对照:在基础材质上再叠一层镜面反射@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

同一套贴图里的金属度通道,是灰度图:

金属度贴图(metallic):灰度逐像素区分金属与非金属,取值应趋近二值
图 4-170金属度贴图(metallic):灰度逐像素区分金属与非金属,取值应趋近二值@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Metallic.png

法线通道同样来自同一套扫描贴图:

法线贴图(切线空间):RGB 编码逐像素法线方向,是细节的主要来源
图 4-171法线贴图(切线空间):RGB 编码逐像素法线方向,是细节的主要来源@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Normal_(Tangent_Space).png

ORM 打包是工业管线的常见做法,引擎里是这样组织的:

ORM 打包:把遮蔽(O)、粗糙度(R)、金属度(M)压进同一张纹理的 R/G/B 通道
图 4-172ORM 打包:把遮蔽(O)、粗糙度(R)、金属度(M)压进同一张纹理的 R/G/B 通道@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

金属材质的一整套通道贴图摊开来看是这样:

金属贴图集总览:Color、Roughness、Normal、AO 等通道并排,金属材质的 Color 通道即反射色
图 4-173金属贴图集总览:Color、Roughness、Normal、AO 等通道并排,金属材质的 Color 通道即反射色@ Wikimedia Commons · ambientCG.com / Lennart Demes(CC0)· https://commons.wikimedia.org/wiki/File%3ADiamondPlate005A%20PREVIEW.jpg

其中最说明问题的是金属度通道本身——它整张是纯白的:

金属度贴图(纯白):全表面 metallic = 1,贴图退化为常量。这正是 metallic 二值性的直接体现——真实材质要么是金属要么不是,中间的过渡值只在边缘与污渍处出现
图 4-174金属度贴图(纯白):全表面 metallic = 1,贴图退化为常量。这正是 metallic 二值性的直接体现——真实材质要么是金属要么不是,中间的过渡值只在边缘与污渍处出现@ Wikimedia Commons · ambientCG.com / Lennart Demes(CC0)· https://commons.wikimedia.org/wiki/File%3ADiamondPlate005A%208K%20Metalness.png

9.4 线性与 sRGB:那张最容易搞错的约定

好了,这一节的内容看着基础,但它是实际项目里出错最多的环节。

glTF 规范对纹理编码有明确要求 [43]:

数据 编码 原因
baseColorTexture 的 RGB sRGB 它是颜色,人眼感知是非线性的
baseColorFactor 线性 它是乘子,不是颜色
roughness / metallic 线性(non-color data) 它们是物理量,不是颜色
normal / AO 线性 同上
glTF 的纹理编码约定:baseColor 走 sRGB,而 roughness/metallic/normal 属物理数据必须线性
图 4-175glTF 的纹理编码约定:baseColor 走 sRGB,而 roughness/metallic/normal 属物理数据必须线性@ 本系列绘制

最容易漏的是中间两行。 很多教程讲"记得把 albedo 设成 sRGB"就结束了,但没有提"roughness 和 metallic 必须设成 linear/non-color"。如果导入时把 roughness 贴图也按 sRGB 采样,结果会明显偏——因为 sRGB 解码会把中间灰值整体压低。

同一 roughness 贴图按 sRGB 与按线性采样的对照:前者中间调被整体压低
图 4-176同一 roughness 贴图按 sRGB 与按线性采样的对照:前者中间调被整体压低@ 本系列绘制

这条有个专门的术语:non-color data(非颜色数据)。一份少见的中文整理明确把这个边界讲了出来 [51]。

如果你遇到"同一个材质在 Blender 里好好的,导到 Unity 就偏了",先查的就是这里。社区里有一份跨端实测对照表,把 Blender / Unity / Three.js 三端的金属度输出逐档列了出来,还包含 KHR_texture_transform 坐标翻转与法线 Y 通道反转的排查 [53]。

Blender / Unity / Three.js 三端金属度输出逐档对照
图 4-177Blender / Unity / Three.js 三端金属度输出逐档对照@ 本系列绘制

线性、非线性与 HDR 的关系,一张图就能说清:

线性与非线性(sRGB)以及 HDR 的区别:同一场景在两种色彩空间下的表现
图 4-178线性与非线性(sRGB)以及 HDR 的区别:同一场景在两种色彩空间下的表现@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/PBR/Theory

同一套通道在色彩空间上却是分家的:Color 是 sRGB,其余通道都是线性数据:

藤编贴图集总览:Color 通道按 sRGB 存储,Roughness/Normal/AO 均为线性数据,读取时不可混用
图 4-179藤编贴图集总览:Color 通道按 sRGB 存储,Roughness/Normal/AO 均为线性数据,读取时不可混用@ Wikimedia Commons · ambientCG.com / Lennart Demes(CC0)· https://commons.wikimedia.org/wiki/File%3AWicker013%20PREVIEW.png

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)、可逆。

PBR Neutral 的保真区间:0.08–0.8 范围内的输入被精确复现,超出部分才被压缩
图 4-180PBR Neutral 的保真区间:0.08–0.8 范围内的输入被精确复现,超出部分才被压缩@ 本系列绘制

注意它明确不做 gamut mapping [45],并假设输入与光照都是 Rec.709(与 glTF 一致)。这说明了它的定位:不是通用色调映射器,而是电商/建筑/CAD 场景下"材质颜色不能变"的专用方案。 商业落地的报道提到它已被主流 3D 工具与引擎广泛采用 [46]。

这条和本系列第三篇的接口在这里:第三篇讲的色调映射决定了下游材质能否保真;如果你做的是商品展示类应用,选错了色调映射,前面八章把材质算得再准也没用——它最后会在色调映射那一步被改掉。

常见色调映射曲线族对照:ACES 有风格化偏移、PBR Neutral 在 0.08–0.8 区间保真
图 4-181常见色调映射曲线族对照:ACES 有风格化偏移、PBR Neutral 在 0.08–0.8 区间保真@ 本系列绘制

9.6 HDRI 采集与材质扫描:一手规范长什么样

下面进入。最后讲素材从哪来。这一节的资料里,Poly Haven 的技术标准是最值得看的一份,因为它是少见的公开写明的采集规范 [57]:

要求 值 理由
是否接受渲染生成的 HDRI 不接受,仅收真实照片 保证是"真实世界的光"
最小水平分辨率 16K 低于此在光滑材质上会看出模糊
是否允许裁剪 必须 unclipped 太阳等高亮度源被裁掉就丢了能量
色卡 需同曝光包围单独拍 Macbeth 色卡 用于色彩标定
后期 禁止 tonemap / 曲线 / 锐化,全线性流程 任何非线性操作都会破坏线性性
内容 需去除三脚架与底部阴影 否则会出现不存在的暗斑
Poly Haven 的 HDRI 技术标准:仅收真实照片、最小 16K、必须 unclipped、全线性、禁 tonemap
图 4-182Poly Haven 的 HDRI 技术标准:仅收真实照片、最小 16K、必须 unclipped、全线性、禁 tonemap@ 本系列绘制

细节来自法线贴图——它把高模的法线烘到低模上:

法线贴图(normal map):用 RGB 编码逐像素法线方向
图 4-183法线贴图(normal map):用 RGB 编码逐像素法线方向@ Julian Herzog (Website)(CC BY 4.0)· https://commons.wikimedia.org/wiki/File:Normal_map_example_-_Map.png

应用之后,平坦表面立刻显出凹凸:

应用法线贴图后的着色结果:平面呈现出立体细节
图 4-184应用法线贴图后的着色结果:平面呈现出立体细节@ Julian Herzog (Website)(CC BY 4.0)· https://commons.wikimedia.org/wiki/File:Normal_map_example_-_Result.png

扫描材质库里的样例,能同时看到多种贴图的合成效果:

扫描材质样例(Poly Haven):颜色、粗糙与法线共同作用的结果
图 4-185扫描材质样例(Poly Haven):颜色、粗糙与法线共同作用的结果@ Amal Kumar(CC0)· https://commons.wikimedia.org/wiki/File:Clay_-_rusty_metal_grid_(Amal_Kumar_via_Poly_Haven).webp

"必须 unclipped"这一条最容易被忽略。裁剪过的 HDRI 看起来正常(画面里那个白斑本来也看不出层次),但它丢掉的是光照能量——用这种 HDRI 照亮的场景会整体偏暗,而且高光滚降会不自然。

裁剪与未裁剪 HDRI 照亮的同一场景对照:前者整体偏暗
图 4-186裁剪与未裁剪 HDRI 照亮的同一场景对照:前者整体偏暗@ 本系列绘制

工业管线里的基础色贴图,原始规格是这样的:

8K 基础色(base color)贴图:ambientCG 的 CC0 材质库规格
图 4-1878K 基础色(base color)贴图:ambientCG 的 CC0 材质库规格@ ambientCG.com / Lennart Demes(CC0)· https://commons.wikimedia.org/wiki/File:Bricks091_8K-PNG_Color.png

还有一张常被忽略的高度图:

高度贴图(height):记录表面起伏,用于视差与置换
图 4-188高度贴图(height):记录表面起伏,用于视差与置换@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Height.png

同一材质的成品渲染,是这些通道合成的结果:

材质成品渲染(lit):各通道合成后在光照下的最终外观
图 4-189材质成品渲染(lit):各通道合成后在光照下的最终外观@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Lit.png

另一套 CC0 材质库的基础色贴图:

8K 基础色贴图(另一套):工业材质库的产出规格
图 4-1908K 基础色贴图(另一套):工业材质库的产出规格@ ambientCG.com / Lennart Demes(CC0)· https://commons.wikimedia.org/wiki/File:Bricks093_8K-PNG_Color.png

法线贴图在引擎材质里的接入方式:

法线贴图:在不增加几何的前提下补充表面细节
图 4-191法线贴图:在不增加几何的前提下补充表面细节@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

高度/置换在引擎里需要显式开启:

高度贴图(height):开启后由射线步进模拟置换,代价不低
图 4-192高度贴图(height):开启后由射线步进模拟置换,代价不低@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

需要更多可用的环境贴图时,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 在中高亮度区偏移
材质管线的交付检查流程
图 4-193材质管线的交付检查流程@ 本系列绘制
PBR 材质管线落地检查清单:八项可执行检查与各自的不合格表现
图 4-194PBR 材质管线落地检查清单:八项可执行检查与各自的不合格表现@ 本系列绘制

环境光遮蔽贴图常被误当成阴影,其实它是遮蔽信息:

环境光遮蔽贴图(AO):记录缝隙处的遮挡程度,不含方向
图 4-195环境光遮蔽贴图(AO):记录缝隙处的遮挡程度,不含方向@ JasonFunderberker(CC0)· https://commons.wikimedia.org/wiki/File:Chainsaw_Ambient_Occlusion.png

落地检查清单里必须核对的一项——AO:

环境光遮蔽贴图(AO):烘焙的遮蔽信息,与实时阴影叠加使用
图 4-196环境光遮蔽贴图(AO):烘焙的遮蔽信息,与实时阴影叠加使用@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

还有一个进阶通道——弯曲法线:

弯曲法线(bent normal):记录遮蔽主方向,比普通法线多一层方向信息
图 4-197弯曲法线(bent normal):记录遮蔽主方向,比普通法线多一层方向信息@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html

这份清单可以直接当验收标准用——八项里前五项是数据约定、后三项是渲染管线配置,覆盖了实践中最常见的失效模式。


十、结语:材质保真,以及三个未解的问题

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 + 一条取值带 美术能调、跨引擎能迁 编码约定搞错会让全部前功尽弃
全篇九步近似的收口表:每步换来了什么、又在哪里露馅
图 4-198全篇九步近似的收口表:每步换来了什么、又在哪里露馅@ 本系列绘制

这张表是本文最想留下的东西。 每一行都对应一个"看起来能用、但知道边界才能用好"的近似。把它记住,比记住任何一个公式都有用。

10.2 三个未解的问题

OK,言归正传。按系列惯例,结语要说清"哪些是已经解决的、哪些还没有"。

第一,$N=V=R$ 没有便宜的替代方案。 它被论文作者自己判为比 split-sum 更大的误差源,但至今没有一个"同样便宜且准确"的方案。SSR 能补充屏幕内的反射,屏幕外依然要退回到这个假设。

第二,多散射补偿仍是近似。 2019 年的方案很实用,但它补不满。真正的多次反弹需要更贵的模型(多层材质、光线追踪),实时预算下还没有普遍可行的做法。

第三,材质制作的效率问题没解决。 参数收敛到了三个(base color / roughness / metallic),听起来够简单,但实际上美术仍要画贴图、要理解取值带、要处理编码约定。9.7 节那张清单里的八项,每一项都有人在踩。

这三个问题的共同点:都不是"算得不够快",而是"近似还没找到更好的平衡点"。

三个未解问题的共同结构:都不是性能问题,而是近似的平衡点问题
图 4-199三个未解问题的共同结构:都不是性能问题,而是近似的平衡点问题@ 本系列绘制

10.3 选型建议

下面进入。如果要在项目里做取舍,按场景分三类:

  • 移动端 / VR:SH 探针(9 系数)+ 单张预滤波 cubemap,跳过 $N=V=R$ 之外的复杂探针;多散射补偿建议开,它几乎不花钱
  • 3A / PC:完整 IBL + 反射探针 + box projection + SSR 补充,多散射补偿必开
  • 商品展示 / 建筑可视化:优先选 PBR Neutral 这类保真色调映射,材质的颜色准确性比渲染的真实感更重要

一句话总结这三类的差别:游戏里"看起来对"就够了,商品展示里"测得准"才是要求。这两者对近似的容忍度完全不同——前者可以接受 $N=V=R$,后者不能接受色调映射带来的偏色。

三类场景的技术栈取舍
图 4-200三类场景的技术栈取舍@ 本系列绘制
三类场景的选型决策树:移动端 / 3A / 商品展示各自的技术栈取舍
图 4-201三类场景的选型决策树:移动端 / 3A / 商品展示各自的技术栈取舍@ 本系列绘制

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,正是为这件事专门做的规格。

与前四篇的关系:前三篇加本篇回答的是"画成什么样",下一篇回答的是"画什么"。两者合起来才是一条完整管线的全貌。

系列五篇全景:几何→可见性→后处理→材质光照→剔除,两条主线在此合拢
图 4-202系列五篇全景:几何→可见性→后处理→材质光照→剔除,两条主线在此合拢@ 本系列绘制

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.