Chapter 29: Reflection and Fresnel
这一章讨论一个具体画面:同一个 PBR 材质球在正视角下高光很弱,在边缘角度突然变亮,并且金属、塑料、玻璃、皮肤的边缘亮度变化并不相同。读完本章后,读者应能追踪一个 fragment shader 中的反射向量、Fresnel 项、F0、metallic、roughness、IBL 采样和最终像素颜色之间的关系。
反射在渲染管线里有两个层级。第一层是方向计算:给定表面法线和入射方向,shader 得到一个反射方向,用它查询环境贴图或计算镜面高光。第二层是能量分配:给定视角、材质和折射率,Fresnel 项决定多少能量进入镜面反射,多少能量留给漫反射、透射或次表面路径。
本章的贯穿材料是一颗材质球。它使用 baseColor、roughness、metallic 和可选 ior,在一个 fragment shader 中接收法线、视线方向、光照方向和预过滤环境贴图。我们会持续回到这颗材质球,解释为什么边缘更亮、为什么金属的反射带有颜色、为什么玻璃需要把反射和透射分开,以及为什么实时管线常用 Schlick 近似替代完整 Fresnel 方程。
工程上的结论可以先给出:反射方向回答“从哪里取光”,Fresnel 项回答“取多少镜面能量”。正视角主要由 F0 控制,掠射角会把反射推向更高权重;roughness 改变反射的清晰度和能量分布,metallic 改变 F0 的来源和漫反射分配。排查 Fresnel 相关问题时,应按法线空间、视线方向、F0 来源、Schlick 输入、IBL LUT、色彩空间和材质参数顺序检查。
29.1 反射机制的物理原理与数学公式
反射机制先从一个局部表面点开始。fragment shader 看到的输入通常包括世界空间位置 P、单位法线 N、观察方向 V 和光照方向 L。其中 N 定义局部切平面,V 指向相机,L 指向光源。只要这些方向的空间不一致,后续反射和 Fresnel 计算都会给出稳定但错误的结果,例如高光跟着模型旋转、边缘亮度出现在背面、环境反射方向反了。
理想镜面反射遵守入射角等于反射角。shader 常用的向量形式是 ,其中 I 是指向表面的入射向量,N 是归一化法线,R 是离开表面的反射向量。docs.gl 对 GLSL reflect 的定义也使用这一形式,并明确要求法线先归一化。这个公式只处理方向,它没有告诉我们反射能量是多少。
下面的 GLSL 片段展示反射方向的最小路径。示例采用世界空间,worldPos 是当前片元位置,cameraPos 是相机位置,环境贴图用反射方向采样。
vec3 N = normalize(worldNormal);
vec3 V = normalize(cameraPos - worldPos);
vec3 I = -V;
vec3 R = reflect(I, N);
vec3 reflectedRadiance = texture(environmentMap, R).rgb;
这段代码证明的点是:reflect 的输入方向采用“从相机射向表面”的方向 I,而常见 PBR 公式中的 V 采用“从表面指向相机”的方向。两者符号相反。把 V 直接传给 reflect 会把采样方向翻到另一侧,环境反射会出现左右翻转或背面取样。
反射方向还要和微表面模型区分。理想镜面把整个表面当作一张平滑镜子,只产生一个确定的 R。粗糙表面由大量微小面元组成,每个微面元都有自己的法线,最终形成一个反射方向分布。roughness 控制这个分布的宽度:低粗糙度让环境反射清晰,高粗糙度让反射展开到更大的立体角。
完整材质球的一次直接光镜面计算可以写成 D * G * F 的组合。D 描述微表面法线分布,G 描述遮蔽和 masking-shadowing,F 描述 Fresnel 能量分配。本章只展开 F,但需要记住 F 进入的是同一条 specular BRDF 路径;它和反射方向、粗糙度、法线分布一起决定最终高光形状。
Fresnel 的物理对象是两个介质的边界。光从空气进入塑料、玻璃、水或皮肤表面时,一部分能量被反射,一部分能量进入介质内部。对于介电体,折射率(Index of Refraction,IOR)决定正视角下的基础反射率。空气到折射率为 ior 的介质时,正入射反射率可近似写成 。例如 ior = 1.5 时, 约为 0.04。
这条公式解释了一个常见视觉事实:塑料、玻璃、水这类介电体在正视角下通常只有少量镜面反射,但在边缘角度会明显变亮。PBRT 第 4 版的 Dielectric BSDF 章节 把介电边界拆成反射和透射两条分量,并使用 Fresnel 反射率 R 与 1 - R 分配采样概率。实时 PBR 管线虽然常用更便宜的近似,核心关系仍然相同:Fresnel 决定反射与剩余能量的比例。
在贯穿材质球里,反射路径可以归纳为下面的管线。它从几何法线和材质参数开始,经过 shader 的方向计算和 Fresnel 评估,最后影响像素颜色。
图中最容易出错的节点是 N、V 和 F0。N 的空间错误会改变反射方向和角度项;V 的符号错误会让反射采样翻转;F0 的来源错误会让所有视角下的镜面能量偏高或偏低。一次可复查的调试路径应先输出 N、V、R 的 false color,再固定 F0 为 0.04,最后恢复材质贴图和 IBL。
29.2 Fresnel 在 PBR 管线中的作用与表达式
Fresnel 项在 PBR 中承担能量闸门的角色。它接收视角相关的角度输入和材质的基础反射率,输出一个 vec3 或 float 形式的镜面反射比例。这个比例通常乘到 specular BRDF 上,同时影响 diffuse 或 transmission 的剩余能量。
实时渲染最常用的是 Schlick 近似:。这里的 在微表面 BRDF 中常取 dot(V, H),H 是半程向量 normalize(V + L)。直接使用反射环境时,也会出现用 dot(N, V) 的近似形式。两种输入服务不同路径:直接光微表面高光使用 V·H,IBL 预积分和边缘观察常使用 N·V。
Filament 的材质文档 把 Schlick Fresnel 写成 f0 + (f90 - f0) * (1 - VoH)^5,其中 f0 是正入射反射率,f90 是掠射角反射率。常规实时 PBR 会把 f90 设为 1.0 或接近 1.0,于是边缘角度的反射趋近白色或高亮。这个模型成本低,适合 fragment shader 中大量执行。
F0 的来源决定材质类别。介电体的 F0 通常是接近灰色的小值,例如常见塑料和玻璃可使用约 0.04 作为默认值;水的正入射反射率约 0.02。金属的 F0 来自 baseColor 或测量的复折射率结果,通常是有颜色的高反射率。PBR 的 metallic 工作流利用这一差异,把 F0 从介电默认值和 baseColor 之间插值。
下面的代码展示金属度工作流中的最小 Fresnel 输入构造。它表达了三个判断:介电体使用灰色 F0,金属使用带颜色的 baseColor 作为镜面反射基色,漫反射能量随 metallic 增大而减少。
vec3 dielectricF0 = vec3(0.04);
vec3 F0 = mix(dielectricF0, baseColor.rgb, metallic);
vec3 diffuseColor = baseColor.rgb * (1.0 - metallic);
float VoH = clamp(dot(V, H), 0.0, 1.0);
vec3 F = F0 + (vec3(1.0) - F0) * pow(1.0 - VoH, 5.0);
这段代码的输出应按材质类别观察。metallic = 0 时,正视角高光接近灰白色,baseColor 主要进入漫反射;metallic = 1 时,漫反射消失,baseColor 进入镜面反射,金色、铜色、铝色的反射呈现不同颜色。metallic 位于中间值时,多数情况下代表材质混合、贴图过渡、污渍或资产压缩结果,需要结合美术管线解释。
Fresnel 还决定 grazing angle 的亮度。cosθ 越小,(1 - cosθ)^5 越接近 1,输出越接近掠射角反射率。材质球边缘正是 N·V 小的区域,所以即使塑料的 F0 只有 0.04,边缘也会反射更多环境光。这个现象在水面、清漆、玻璃外缘和低角度地面上明显。
Fresnel 与 roughness 的关系需要放在完整 BRDF 中看。Fresnel 控制角度相关的镜面能量比例,roughness 控制这部分能量分布到多宽的方向范围。低 roughness 会把增加的边缘反射表现为清晰的亮边或锐利环境反射,高 roughness 会把它扩散成宽而柔的边缘亮度。观察画面时,把“更亮”和“更清晰”分开判断,能定位是 Fresnel、roughness 还是环境贴图预过滤出现问题。
29.3 在 Shader 中高效评估 Fresnel 项
Fresnel 在 fragment shader 中执行频率高,优化目标是减少昂贵指令、减少分支、保持能量关系稳定,并让 IBL 与直接光使用一致的材质参数。一次全屏 PBR pass 可能为每个可见像素执行多次 Fresnel 评估,直接光、clear coat、sheen、IBL LUT 都会触发类似计算。
Schlick 近似的主要成本来自 pow。指数固定为 5 时,可以把 pow(1.0 - u, 5.0) 展开成乘法。这样能减少 shader 编译器对通用幂函数的依赖,也方便在移动 GPU 和较低精度路径中控制结果。
vec3 fresnelSchlick(float u, vec3 f0)
{
float x = clamp(1.0 - u, 0.0, 1.0);
float x2 = x * x;
float x5 = x2 * x2 * x;
return f0 + (vec3(1.0) - f0) * x5;
}
这段函数的输入 u 必须先明确来源。直接光 Cook-Torrance 路径通常传入 dot(V, H);屏幕调试 Fresnel 轮廓时常传入 dot(N, V);IBL 预积分 LUT 常把 N·V 和 roughness 作为二维坐标。把这些路径混用会形成表面边缘亮度和直接高光不匹配的画面症状。
IBL 中的 Fresnel 计算通常拆成两部分。预过滤环境贴图根据 roughness 存储不同模糊程度的反射结果;BRDF LUT 根据 N·V 和 roughness 存储积分后的系数;运行时再用 F0 重建镜面环境光。这种设计把每像素多方向积分转成一次 cubemap 采样和一次二维 LUT 采样,瓶颈从大量 ALU 与采样循环变成固定资源读取。
下面的片段展示典型 IBL specular 重建路径。它省略了环境贴图预过滤的生成过程,只保留运行时 shader 的关键数据流。
vec3 N = normalize(worldNormal);
vec3 V = normalize(cameraPos - worldPos);
vec3 R = reflect(-V, N);
float NoV = clamp(dot(N, V), 0.0, 1.0);
vec3 F0 = mix(vec3(0.04), baseColor.rgb, metallic);
vec3 prefiltered = textureLod(prefilteredEnvMap, R, roughnessToMip(roughness)).rgb;
vec2 brdf = texture(brdfLut, vec2(NoV, roughness)).rg;
vec3 specularIBL = prefiltered * (F0 * brdf.x + brdf.y);
这段代码对应的资源路径很清楚。prefilteredEnvMap 回答“这个粗糙度下从反射方向能取到多少环境光”;brdfLut 回答“在当前视角和粗糙度下,微表面 BRDF 的角度积分如何缩放 F0”;F0 回答“这个材质正视角下有多少基础镜面反射”。任何一个资源的颜色空间、mip 选择、坐标范围或绑定状态错误,都会表现为反射能量不稳定。
性能优化应围绕瓶颈类型做判断。直接光数量少时,Schlick 乘法展开的收益有限,清晰的数据路径更有价值;多灯、cluster lighting 或 forward+ 路径中,Fresnel 函数会在光循环里重复执行,乘法展开和分支合并更有意义;IBL 路径中,cubemap 和 LUT 采样通常比几次乘法更受带宽、cache 和纹理过滤影响。
材质参数输入也要稳定。F0 应在线性空间中参与计算,baseColor 如果来自 sRGB 贴图,应先解码到线性空间;roughness 也应保持和预过滤环境贴图生成时一致的参数化。许多“Fresnel 太强”的画面问题来自 baseColor 未解码、metallic mask 错误、normal map 切线空间错误或 LUT 与环境贴图来自不同预计算约定。
可复用的排查顺序如下。先固定环境光为简单灰色,检查 N·V 的灰度图是否在轮廓处变暗;再固定 F0 = vec3(0.04),确认塑料边缘自然变亮;接着关闭 metallic,排查金属贴图是否把非金属区域推高;最后恢复 IBL,检查反射方向、mip level、BRDF LUT 坐标和色彩空间。
29.4 不同材料视觉差异的 Fresnel 贡献
不同材料的 Fresnel 差异来自两个来源:正入射反射率的大小与颜色,以及反射之外的能量去向。塑料把大部分能量交给漫反射;金属几乎没有普通漫反射,能量主要留在带颜色的镜面反射中;玻璃同时存在反射和透射;皮肤表层具有介电反射,内部还有吸收和次表面散射。
| 材料 | F0 典型来源 | 正视角表现 | 掠射角表现 | 排查重点 |
|---|---|---|---|---|
| 塑料 | 灰色介电默认值,常用约 0.04 | 高光弱,颜色主要来自漫反射 | 边缘出现灰白反射 | F0、roughness、normal map |
| 金属 | 带颜色的 baseColor 或测量值 | 镜面反射带金属颜色,漫反射趋近零 | 边缘强反射,颜色受材质和环境共同影响 | metallic mask、线性空间、环境贴图 |
| 玻璃 | IOR 推导的介电 F0,同时有透射 | 正视角反射弱,可看到透射对象 | 边缘反射增强,透射贡献下降 | IOR、厚度、透明排序、折射路径 |
| 皮肤 | 表层介电反射,常在低 F0 范围 | 高光受油脂层和 roughness 影响 | 边缘有反射,同时受次表面散射影响 | roughness 分区、SSS、法线细节 |
塑料材质适合用来校准 Fresnel 的基础正确性。把 baseColor 设为中灰或红色,metallic = 0,F0 = 0.04,roughness 从 0.1 调到 0.8。正视角高光会随 roughness 展宽,边缘反射仍由 Fresnel 推高。若正视角已经像金属一样强烈,优先检查 F0 是否被贴图或常量设得过高。
金属材质的关键差异是 F0 带颜色。黄金的反射偏黄,铜偏红,铝偏中性灰白。因为金属没有普通介电体那样的漫反射主体,baseColor 在金属工作流中进入 specular F0。如果金属表面看起来像涂了颜色的塑料,常见原因是 metallic mask 没有生效,或 diffuseColor 没有按 1 - metallic 缩减。
玻璃材质把 Fresnel 的能量分配表现得更直观。正视角时,较多能量进入透射或折射路径,反射较弱;掠射角时,反射增强,透射贡献降低。实时透明渲染还受到排序、厚度、屏幕空间折射和后处理限制,所以玻璃画面问题要把 Fresnel 公式、透明 pass、深度写入和背景采样一起检查。
皮肤材质中的 Fresnel 主要来自表层油脂和角质层反射。皮肤同时包含粗糙度变化、微法线细节、血色吸收和次表面散射,因此边缘亮度不能全部归因于 Fresnel。稳定的做法是先单独观察 specular pass,再观察 SSS 或 diffuse profile,最后叠加 normal map。这样可以区分“表面反光过强”和“内部散射过亮”。
同一 Fresnel 公式在不同材料上的视觉结果不同,是因为 F0、roughness、diffuse、transmission 和 SSS 所在能量路径不同。PBR 管线的任务是让这些路径在同一套参数下闭合:塑料保留漫反射,金属把颜色放入镜面,玻璃在反射和透射之间分配,皮肤把表层反射和内部散射分层处理。
一个可迁移判断顺序可以覆盖多数材质问题。先把材质放在同一环境贴图和同一曝光下观察;再固定 roughness,比较 F0 的大小和颜色;随后固定 F0,比较 roughness 对清晰度的影响;最后恢复材质专属路径,例如金属的 colored specular、玻璃的 transmission、皮肤的 SSS。这个顺序把 Fresnel、粗糙度和材质分层拆开,便于定位错误来源。
本章最终建立的理解是:反射方向、Fresnel 项和材质参数处在同一条 shader 数据路径里。反射方向决定环境或光源从哪个方向进入镜面项,Fresnel 决定当前视角下镜面项取得多少能量,材质类型决定 F0 的数值、颜色和剩余能量去向。只要按这三层追踪,一帧中的亮边、金属偏色、玻璃边缘反射和皮肤油光都能回到可检查的输入、公式和资源状态。
最小自检任务
给定一个 PBR 材质球,shader 中有如下现象:塑料材质在正视角已经出现很强的白色镜面反射,材质球边缘亮度正常;金属材质的边缘亮度存在,但整体颜色像红色塑料;玻璃材质正视角几乎看不到反射,边缘反射明显。请基于本章内容设计一次排查,说明先检查哪些 shader 输入和材质参数,再判断每种材质的问题更可能出在哪条路径。
答案要点
排查应先统一空间和方向。输出 N、V、R 与 N·V 的调试图,确认法线已归一化,观察方向从表面指向相机,reflect(-V, N) 用于环境采样,N·V 在边缘区域变小。若这些图正确,边缘 Fresnel 正常就具备基础依据。
塑料正视角反射过强时,先固定 F0 = vec3(0.04) 并关闭 metallic 贴图。若强反射下降,问题集中在 F0 常量、specular 贴图、sRGB 解码或 metallic mask。若仍然过强,继续检查 roughness 是否过低、环境贴图曝光是否过高、BRDF LUT 是否绑定错误。
金属像红色塑料时,重点检查 metallic 是否真正进入 F0 = mix(vec3(0.04), baseColor, metallic),以及 diffuseColor = baseColor * (1.0 - metallic) 是否生效。金属应把颜色放入镜面反射,并压低普通漫反射;红色塑料感说明 diffuse 路径仍在输出主要颜色,或 metallic mask 没有覆盖目标区域。
玻璃现象与 Fresnel 关系基本一致。正视角反射弱、边缘反射明显,符合介电边界在 Fresnel 分配下的视觉行为。进一步检查应放在 transmission、折射、透明排序、深度和厚度路径上,而非优先改大 F0。把 F0 任意提高会让玻璃变成强反射塑料或镜面涂层。
本章知识点总结
- 反射方向:反射向量回答 shader 从哪个方向取得环境光或镜面贡献。
- 方向约定:PBR 中的
V通常从表面指向相机,传给reflect时常使用-V。 - 法线空间:
N、V、L必须处在同一空间,反射和 Fresnel 才有可解释结果。 - 能量分配:Fresnel 项决定视角相关的镜面反射比例,并影响剩余能量路径。
- 正视角值:
F0表示正入射反射率,介电体常用灰色小值,金属使用带颜色的高反射值。 - Schlick 近似:实时 PBR 常用 近似 Fresnel。
- 角度输入:直接光微表面高光常使用
V·H,IBL 与轮廓调试常使用N·V。 - 粗糙度作用:roughness 改变镜面能量的方向分布,Fresnel 改变镜面能量的角度权重。
- IBL 路径:预过滤环境贴图回答取多少环境光,BRDF LUT 回答角度积分如何缩放
F0。 - 塑料校准:塑料适合检查介电
F0、roughness、normal map 和环境曝光是否合理。 - 金属差异:金属把
baseColor放入镜面F0,普通漫反射随 metallic 增大而缩减。 - 玻璃分配:玻璃需要同时检查 Fresnel、透射、折射、透明排序和厚度相关路径。
- 皮肤分层:皮肤表层反射受 Fresnel 控制,内部亮度还受吸收和次表面散射影响。
- 排查顺序:先看法线和视角,再看
F0,再看 roughness,最后恢复 IBL 与材质专属路径。