PBR 理论、微表面 BRDF 与能量分配
PBR 理论、微表面 BRDF 与能量分配:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能说出微表面模型的核心假设和能量守恒约束:表面由无数微小镜面组成,出去的光不超过进来的光()
- 能推导并手写 Cook-Torrance BRDF 的反射率方程:说清 (法线分布)、(几何遮蔽)、(菲涅尔)三项各自在算什么物理量,以及分母 的归一化意义
- 能回答:一块粗糙度 0.8 的红色塑料球和一块粗糙度 0.2 的金球,谁有漫反射、谁的 F0 是 albedo 颜色?两者的 NDF 峰形有什么区别?
为什么同一个球换种材料,看起来就完全不同
用手机拍一张塑料杯和一张不锈钢杯。塑料杯身上有一大片柔和的白色亮斑,杯面颜色是杯子本身的颜色;不锈钢杯上高光又小又亮,杯面反射的是周围环境的颜色而不是金属自己的颜色。两种杯子被同一束光照着,高光形状和颜色来源却完全不同。
上一篇的冯氏/布林-冯氏光照只有一个「反光度」参数,金属和塑料的区别全靠手调,经验多了才大概像。工业界早就发现:真实材质的反光行为是可以用几条物理规律写成公式的——不用猜参数,只要告诉公式「它多粗糙、是不是金属」,高光形状和颜色就能自动算对。
这一章要做的就是:把那几条物理规律拆开、每一项弄清楚它在算什么。读完后你就有了一把「物理尺子」,能度量任何材质的反光行为,而不再靠调反光度蒙。
微表面模型:每块表面都是无数小镜子
所有 PBR 的起点是一条假设:任何表面,无论看起来多平整,放大到微观层面都是由无数朝向各异的微小镜面组成的。每一面微镜只做一件事——把打到它身上的光按镜面反射定律弹出去。这就是↡PBR 的核心几何假设:任何表面在微观层面由无数朝向各异的微小完美镜面(microfacet)组成。每个微面只做镜面反射。宏观看到的漫反射/模糊高光,是无数微面反射的统计叠加结果。。
那宏观上为什么有的表面高光清晰、有的模糊?关键是这些微镜朝向的一致性。光滑表面的微面几乎都朝同一个方向——入射光被统一弹到同一侧,你看到一个小而亮的高光斑;粗糙表面的微面朝向杂乱——光被散射到四面八方,高光变得又大又淡。↡描述微表面朝向有多混乱的参数(0~1)。0 = 完美光滑,所有微面对齐,镜面反射集中;1 = 极粗糙,微面朝向完全随机,反射分散到各方向。(roughness)就是度量这种「混乱程度」的参数:
能量守恒:出去的光不能比进来的多
微表面模型还隐含一条硬约束:↡一个物理定律在渲染中的约束:一个表面反射出去的光能量总和不超过打进来的光能量。在 PBR 中体现为镜面反射系数 k_s 与漫反射系数 k_d 之和等于 1(k_s + k_d = 1)。反射越强,漫反射必须越弱,反之亦然。——一束光打到表面,反射出去的 + 折射进去的(被吸收或散射成漫反射)= 打进来的,绝不能凭空多出光来。
把这条约束翻译成系数:镜面反射占比 、漫反射占比 ,则 。高光越强,漫反射就越弱——这就是你看到的「金属表面几乎全是反射、看不到自身颜色」的原因( 很大→ 很小→漫反射几乎没有)。
反射率方程:把半球上所有来光加起来
现在要量化「一个点 朝你的眼睛方向 发出多少光」。直觉上:从表面上方半球 的每个方向 都可能有光射过来,每一份入射光经过表面的 ↡双向反射分布函数(Bidirectional Reflective Distribution Function)。给定入射方向 omega_i 和出射方向 omega_o,BRDF 返回一个比例值——表示 omega_i 方向的入射光有多少份量沿 omega_o 方向反射出去。它是 PBR 的核心函数,Cook-Torrance BRDF 是其中一种具体实现。 反射后,有一部分沿 方向出去。把所有方向的贡献积分起来,就是↡出射辐射度 Lo 的计算公式:对表面上方半球 omega 的所有入射方向 omega_i 求积分,每条入射光的贡献 = BRDF fr 乘以入射辐射度 Li 乘以余弦项 (n . omega_i)。描述了一个表面点最终朝观察方向发出多少光。:
- :点 朝 出射的↡辐射度(Radiance):沿某方向单位立体角、单位投影面积上的辐射功率。通俗说就是「这个方向上光有多亮」。(就是「有多亮」)
- :BRDF——衡量 方向的光有多少沿 方向弹出去
- :从 方向射来的入射辐射度
- :余弦项——光线越正对表面(夹角小),有效照射面积越大
Cook-Torrance BRDF:漫反射 + 镜面反射
现在给 一个具体的实现。Cook-Torrance 把 BRDF 拆成两部分相加——一份漫反射、一份镜面反射:
- 左半 :↡最简单的漫反射 BRDF——假设光打进表面后被均匀散射到所有方向,反射率等于反照率颜色 c 除以 pi。除以 pi 是为了让半球积分归一化(各方向散射总量等于 c)。。 是反照率颜色(albedo),除以 是归一化(保证半球积分 = )
- 右半 :Cook-Torrance 镜面项。、、 三个函数分别描述微表面的三种物理效应。分母 是归一化校正
下面三节逐一拆开 、、。
单步确认:BRDF 每一项各管什么
公式很长时,先按因果顺序检查它,而不是把 D、G、F 当成三个可以随意调的高光旋钮。
先让镜面与漫反射不抢同一份能量
对不透明 PBR 工作流,先由 Fresnel 得到 kS,再把剩余能量给非金属的漫反射。金属的 metallic=1 会使 kD 归零。
法线分布函数 D:有多少微面对齐了半程向量
(↡Normal Distribution Function:给定半程向量 h 和粗糙度 alpha,返回微表面中法线恰好对齐 h 方向的那些微面所占的统计密度。D 越大 = 越多微面能把光反射到你眼里 = 高光越亮。常用实现是 GGX/Trowbridge-Reitz。)回答一个问题:在所有微面中,法线恰好朝向半程向量 的有多大比例? 只有那些法线对齐 的微面才能把入射光精确反射到你的眼睛方向—— 越大,这种微面越多,高光越亮。
猜一猜:下面三张图分别对应粗糙度 0.1、0.5、1.0。你觉得哪一张的 NDF 曲线最「尖」(集中在正中间)、哪一张最「平」(分散到两侧)?尖的对应什么样的高光?
粗糙度低 → 微面朝向集中 → NDF 是窄而高的尖峰 → 高光小而亮。粗糙度高 → 微面朝向分散 → NDF 是宽而矮的丘 → 高光大而暗。
几何函数 G:有多少微面被邻居挡住了
光打到微面上反射出去,但这份反射光未必能到你眼里——因为邻近的微面可能挡在中间。↡Geometry Function:估算微表面中有多少比例的微面既没被邻近微面遮住观察方向(masking),也没被遮住光线方向(shadowing)。G 越小 = 被遮挡越严重 = 高光越暗。Smith 方法把观察方向和光线方向的遮蔽分开算再相乘。 就是估算这种自遮蔽有多严重。遮蔽来自两个方向:
- 遮蔽(Masking):反射光朝你飞来,但被旁边凸起的微面挡住——你看不到
- 阴影(Shadowing):入射光朝这块微面飞来,但被旁边凸起的微面挡住——光照不到
Smith 的做法很直觉:两个方向各算一遍遮蔽比例,再乘起来——。表面越粗糙,微面凸起越多,遮蔽越严重, 越小。
菲涅尔方程 F:角度越斜反射越强
最后一项 描述的是一个日常也能观察到的现象:你从越斜的角度看一个表面,它反射越强。站在湖边正下方看水,水是透明的(反射弱);远眺湖面,水面像镜子一样反射天空(反射强)。这就是↡菲涅尔效应:光线以越大的入射角(越掠射)打到表面,反射率越高。在 0 度(正看)时反射率为 F0(基础反射率),在 90 度(掠射)时所有材质的反射率都趋近 1(全反射)。Schlick 近似公式:F = F0 + (1-F0)(1-(h.v))^5。。
是材质在正看(0°)时的基础反射率。电介质(非金属)的 普遍很低(约 0.04),金属的 很高且有颜色(等于 albedo)。但不管 多低,掠射角时都会飙升到接近 1:
金属与非金属:一个 metallic 参数的两条路
理论上 PBR 已经完整了,但实际使用时有个关键简化:绝大多数材质要么是金属、要么是非金属,很少有「半金属」。于是引入一个 ↡metallic 工作流参数(0~1)。0 = 电介质(非金属):F0 固定约 0.04,有着色漫反射,镜面接近白色。1 = 金属:F0 等于 albedo 颜色(高且有色),漫反射为零(折射光全被吸收),镜面反射带颜色。中间值仅在过渡边缘短暂出现。 参数在两条路之间切换:
- metallic = 0(非金属): 固定 ≈ 0.04,漫反射 = albedo 颜色(你看到球本身的红色/蓝色),镜面几乎白色
- metallic = 1(金属): = albedo 颜色,漫反射 = 0(折射光全被金属内部的自由电子吸收),镜面反射带颜色(金色金属反射金色光)
数学推导:从方程到每一项
符号表
| 符号 | 含义 |
|---|---|
| 表面上被着色的点 | |
| 表面法线(单位向量) | |
| 入射方向(单位向量,指向光源) | |
| 出射方向(单位向量,指向观察者) | |
| 半程向量 | |
| 从 方向射来的入射辐射度 | |
| 朝 方向出射的辐射度 | |
| BRDF(双向反射分布函数) | |
| 反照率颜色 (albedo) | |
| 粗糙度 (roughness) | |
| 几何项的粗糙度重映射系数 | |
| 0° 入射时的基础反射率 |
反射率方程
出发点:点 朝 的出射辐射度等于上半球所有入射方向的贡献之和:
人话翻译:把半球每个方向飞来的光 乘以「BRDF 说这个方向的光有多少会沿 出去」再乘以「光线越正对表面有效面积越大」的余弦权重,全部加起来。
Cook-Torrance BRDF
把 拆成漫反射 + 镜面反射两部分:
人话翻译:左半是 Lambertian 漫反射——光均匀散射到所有方向,亮度只取决于颜色 ;右半是 Cook-Torrance 镜面—— 说有多少微面对齐了 , 说当前角度反射占多大比例, 说有多少微面没被挡住,分母做归一化。
GGX 法线分布函数 D
人话翻译: 是法线和半程向量的贴合度(1=完美对齐)。 小时分母的平方项极大地压低了偏离对齐处的值→尖峰; 大时分母变得平缓→宽丘。分子的 保证积分归一化。
Schlick-GGX 几何函数 G
先定义单方向的 :
人话翻译: 是表面法线与方向 的余弦。 越大(粗糙度越高),分母比分子增长得更快→ 越小→遮蔽越严重。当 (完美光滑),(无遮蔽)。
Smith 方法把观察方向和光线方向各算一遍再乘:
其中 的取法随场景不同:直接光照用 ,IBL 用 。
Fresnel-Schlick 近似
人话翻译: 是半程向量与观察方向的余弦——正看时 ,,反射率 ≈ (基础值);掠射时 ,,反射率 → (全反射)。五次方决定了「从 到 1 的爬升集中在最后那段掠射角」。
合起来:代入积分
把上面各项代回反射率方程的积分里:
人话翻译:这就是最终要在片段着色器里算的东西。对于直接光源(点光/方向光),积分退化成有限次求和——每盏灯一份贡献,循环累加即可。下一章(PBR 光照)就是把这个公式写成 GLSL。
亲手拖一拖:D / G / F 三条曲线
猜一猜:把粗糙度从 0.1 拉到 0.8,哪条曲线变化最剧烈?切换到金属模式,Fresnel 曲线的起点会跳到多高?
BRDF 曲线加载中…
上面三条曲线正是 Cook-Torrance BRDF 里的三个核心函数。拖动粗糙度滑块,观察 D 曲线(蓝色)从窄尖峰变成宽矮丘——这就是高光从「小而亮」到「大而暗」的数学原因。G 曲线(绿色)在粗糙度升高时整体下降,说明遮蔽更严重。F 曲线(橙色)切换到金属后起点从 0.04 跳到 0.7,解释了金属为什么正看也有强反射。
代码对照:D / G / F 的 GLSL 实现
下面是 Cook-Torrance 三个子函数的 GLSL 片段。下一章「PBR 光照」会把它们组合起来在真实场景中跑,这里先看每个函数怎么把公式翻译成代码:
// ======== D:GGX / Trowbridge-Reitz 法线分布函数 ========
// 输入:法线 N、半程向量 H、粗糙度 roughness
// 输出:D 值(微面对齐 H 的统计密度)
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness; // alpha = roughness^2(业界约定先平方一次,手感更线性)
float a2 = a * a; // alpha^2
float NdotH = max(dot(N, H), 0.0); // N . H,截到 0 防负
float NdotH2 = NdotH * NdotH;
// 公式分母:pi * ((NdotH^2)*(a2-1)+1)^2
float denom = NdotH2 * (a2 - 1.0) + 1.0;
denom = 3.14159265 * denom * denom;
return a2 / denom; // alpha^2 / denom
}
// ======== G:Schlick-GGX 几何遮蔽(单方向) ========
// 输入:N . 方向 的余弦 NdotV、重映射系数 k
// 输出:G_sub 值(该方向未被遮挡的比例)
float GeometrySchlickGGX(float NdotV, float k) {
return NdotV / (NdotV * (1.0 - k) + k);
}
// Smith 方法:观察方向 + 光线方向各算一遍再相乘
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0; // 直接光照的 k 重映射
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
return GeometrySchlickGGX(NdotV, k) * GeometrySchlickGGX(NdotL, k);
}
// ======== F:Fresnel-Schlick 近似 ========
// 输入:半程向量与观察方向余弦 cosTheta、基础反射率 F0
// 输出:当前角度的反射率(vec3,金属有色所以是三通道)
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}逐函数对照:
DistributionGGX直接翻译 。注意roughness先平方一次变成 (业界约定),再平方得GeometrySchlickGGX翻译 。直接光照的fresnelSchlick翻译 。输入是cosTheta = h . v;返回vec3因为金属的 有颜色
常见误区
小结与练习
这一章从微表面模型出发,经过能量守恒约束,推导出了完整的 Cook-Torrance BRDF 反射率方程。三个核心函数 D/G/F 分别量化了「多少微面对齐半程向量」「多少微面没被遮挡」「当前角度反射占多大比例」。加上 metallic 工作流区分金属/非金属,就构成了 PBR 的完整理论基础。下一章将把这些公式写进 GLSL,在真实场景里点灯看效果。
练习
推导题
- 从 Fresnel-Schlick 公式 出发,验证:当 (正看)时 ;当 (掠射)时 。中间角度呢? 时 约等于多少(取 )?
- GGX NDF 公式 ,当 (微面法线完美对齐 )时 等于多少?写出化简过程,说明它只取决于 。
概念题
- 一块粗糙度 0.8 的红色塑料球(metallic=0)和一块粗糙度 0.2 的金球(metallic=1),分别回答:(a) 谁有漫反射?(b) 谁的 F0 是 albedo 颜色?(c) 谁的 NDF 更「尖」?
- 为什么 Cook-Torrance 分母里有 ?如果去掉会怎样?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 微表面模型
PBR 的核心几何假设:任何表面在微观层面由无数朝向各异的微小完美镜面(microfacet)组成。宏观看到的漫反射/模糊高光是无数微面反射的统计叠加。
- 粗糙度
roughness(0~1),描述微表面朝向有多混乱。0 = 完美光滑(微面对齐,反射集中),1 = 极粗糙(微面随机,反射分散)。
- 能量守恒
一个表面反射出去的光总量不超过照进来的光。PBR 中体现为 k_s + k_d = 1,反射越强漫反射越弱。
- BRDF
双向反射分布函数(Bidirectional Reflective Distribution Function)。给定入射方向和出射方向,返回入射光沿出射方向反射出去的比例。Cook-Torrance 是一种具体实现。
- 反射率方程
Lo(p, omega_o) = 对半球所有入射方向积分 fr . Li . cos(theta)。描述一个表面点最终朝观察方向发出多少光。
- 辐射度
Radiance:沿某方向单位立体角、单位投影面积上的辐射功率。通俗说就是「这个方向上光有多亮」。
- NDF(法线分布函数)
Normal Distribution Function:给定半程向量 h 和粗糙度 alpha,返回微面中法线对齐 h 方向的统计密度。常用实现是 GGX/Trowbridge-Reitz。
- 几何函数
Geometry Function:估算微面中有多少比例既没被遮住观察方向也没被遮住光线方向。Smith 方法把两个方向分开算再相乘。
- 菲涅尔效应
Fresnel Effect:光线以越大的入射角打到表面,反射率越高。正看时反射率为 F0,掠射时趋近 1。Schlick 近似:F = F0 + (1-F0)(1-(h.v))^5。
- Lambertian 漫反射
最简单的漫反射模型——假设光打进表面后被均匀散射到所有方向,f_lambert = c / pi。
- metallic
金属度参数(0~1)。0 = 电介质(低 F0,有色漫反射,白色镜面);1 = 金属(高有色 F0,无漫反射,有色镜面)。
版本、来源与运行边界
本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。
正式概念与状态责任
- microfacet:在“PBR 理论、微表面 BRDF 与能量分配”中由BRDF 求值合同与材质参数负责解释其输入、受控状态和可观察结果;运行时以材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查定位它的第一处变化。
- energy conservation:在“PBR 理论、微表面 BRDF 与能量分配”中由BRDF 求值合同与材质参数负责解释其输入、受控状态和可观察结果;运行时以材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查定位它的第一处变化。
- brdf:在“PBR 理论、微表面 BRDF 与能量分配”中由BRDF 求值合同与材质参数负责解释其输入、受控状态和可观察结果;运行时以材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查定位它的第一处变化。
- cook-torrance:在“PBR 理论、微表面 BRDF 与能量分配”中由BRDF 求值合同与材质参数负责解释其输入、受控状态和可观察结果;运行时以材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“由材质参数求 D/G/F,分配 diffuse/specular 能量并计算 BRDF”发生后,BRDF 求值合同与材质参数应怎样改变normal/view/light/half vectors、roughness、metallic、F0、D/G/F、kD/kS 和分母;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
PBR 理论、微表面 BRDF 与能量分配:状态合同
用微表面 D/G/F 与能量守恒解释 Cook-Torrance BRDF 中每一项的物理责任
验证场景
官方教程正式概念
logl-39 · 基线帧
microfacet:固定 context、资源内容与输入事件,执行“由材质参数求 D/G/F,分配 diffuse/specular 能量并计算 BRDF”
冻结输入:microfacet
BRDF 求值合同与材质参数记录normal/view/light/half vectors、roughness、metallic、F0、D/G/F、kD/kS 和分母
结果:得到可重复的初始 GL 状态与资源身份
观测:材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查中的初始快照
预期:BRDF 求值合同与材质参数得到可复查结果,并持续满足“kD+kS 不凭空增能;金属的 diffuse 贡献归零;分母避免零除”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“由材质参数求 D/G/F,分配 diffuse/specular 能量并计算 BRDF”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“kD+kS 不凭空增能;金属的 diffuse 贡献归零;分母避免零除”。
CPU command · GL state · GPU result
PBR 理论、微表面 BRDF 与能量分配:五段轨迹
当前观测:材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查中的初始快照
不变量:kD+kS 不凭空增能;金属的 diffuse 贡献归零;分母避免零除
实验三:单故障与同输入恢复
注入“金属材质仍保留 Lambert diffuse,同时又计算有色镜面,反射能量重复”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查一起恢复才算修复。
Single fault · first divergence · replay
PBR 理论、微表面 BRDF 与能量分配:反例与恢复
故障:金属材质仍保留 Lambert diffuse,同时又计算有色镜面,反射能量重复
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“金属材质仍保留 Lambert diffuse,同时又计算有色镜面,反射能量重复”
kD+kS 不凭空增能;金属的 diffuse 贡献归零;分母避免零除
材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查
最小可重放检查
unit: logl-39
owner: BRDF 求值合同与材质参数
state_or_resource: normal/view/light/half vectors、roughness、metallic、F0、D/G/F、kD/kS 和分母
command: 由材质参数求 D/G/F,分配 diffuse/specular 能量并计算 BRDF
pass_invariant: kD+kS 不凭空增能;金属的 diffuse 贡献归零;分母避免零除
single_fault: 金属材质仍保留 Lambert diffuse,同时又计算有色镜面,反射能量重复
required_evidence: 材质输入、Ndot*、D/G/F、kD/kS、BRDF 分量与极端参数检查复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。