PBR在Shader中的核心实现原理

13 人参与

当你第一次在引擎里把材质从“Standard”换成“Standard (Specular setup)”或者“Standard (Metallic workflow)”时,可能只觉得参数变多了,贴图槽也多了几张。但当你真正点开那个Shader文件,看到那一串串关于D、G、F的计算时,才会意识到,PBR绝不仅仅是一套新参数,而是一整套物理世界的编码规则。它的核心,是把现实世界的光与物质交互,用几行精炼的GLSL或HLSL代码,在GPU上实时地演绎出来。

微表面:所有魔法开始的地方

传统光照模型,比如Blinn-Phong,把表面当作一个完美的几何平面来处理光线。这显然不对。现实中,哪怕一块打磨过的金属,在显微镜下也是坑坑洼洼的。PBR的基石,正是这个“微表面”理论。它假设物体表面由无数个朝向随机的、微小的镜面构成。宏观上看到的粗糙或光滑,本质上就是这些微小镜面朝向的集中程度。

这个理论直接催生了PBR Shader中最核心的三个函数:法线分布函数几何遮蔽函数菲涅尔方程。业内常说的“Cook-Torrance BRDF”模型,就是这三者的乘积。在Shader里,它看起来可能像这样:

float D = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 specularBRDF = (D * G * F) / (4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001);

这几行代码,就是物理的浓缩。每一个变量——表面法线N、视线方向V、光线方向L、半角向量H——都在参与一场严格的几何与能量守恒审判。

能量守恒:不是建议,是铁律

这是PBR在实现上与旧模型最决裂的一点。在Shader里,能量守恒不是靠感觉调参数,而是靠数学公式锁死的。反射出去的光能量,加上折射/吸收的部分,必须等于入射光能量,一点都不能多。

这直接体现在对漫反射高光反射的处理上。在PBR Shader中,漫反射项通常会乘以(1.0 - F),这里的F就是菲涅尔项。物理意义很明确:被反射掉(F)的那部分光,就不能再参与漫反射了。对于金属,这个规则更极端——纯金属的漫反射系数被强制设为零,因为所有进入表面的光都被自由电子吸收了,根本不会发生次表面散射。这个简单的if (isMetallic) diffuse = vec3(0.0);判断,是区分物理与非物理渲染的一道鸿沟。

两张关键贴图:粗糙度与金属度

如果说理论是骨架,那贴图就是血肉。PBR工作流要求至少提供两张单通道贴图:粗糙度贴图金属度贴图。它们在Shader里可不是简单的乘数。

粗糙度值,会直接喂给法线分布函数D和几何函数G,控制高光的集中与扩散。一个常见的误区是把“光滑”理解为高亮度,但在PBR里,光滑意味着高光范围小而亮,粗糙则是范围大而暗淡——这一切都由数学驱动,美术师只需调整0到1之间的一个数值。

金属度贴图的作用更微妙。它像一个开关,决定了菲涅尔反射的基准值F0。对于非金属(电介质),F0是一个低值常量(比如0.04);对于金属,F0则来自反照率贴图的颜色值。因为金属的反射是带有颜色的(想想金子的淡黄色反光)。在Shader里,这通常是一句线性插值:vec3 F0 = mix(vec3(0.04), albedo, metallic);。你看,金属度贴图像一个混音推子,在电介质的灰白反射和金属的彩色反射之间平滑过渡。

线性空间与HDR:被忽略的“基础设施”

很多人在实现PBR Shader时卡在最后一步:为什么我的效果就是不如引擎官方的好看?问题往往出在渲染管线的“基础设施”上。

PBR的整个光照计算都假设在线性颜色空间中进行。这意味着你从纹理里采样出来的颜色值,必须先进行Gamma校正解码(pow(color, 2.2)近似),参与完所有线性计算后,最终输出到屏幕前再进行一次Gamma编码(pow(color, 1.0/2.2))。如果跳过这一步,光照叠加会出错,暗部细节会丢失,物理正确性从源头就崩塌了。

更进阶的是对HDR色调映射的支持。现实世界的光照强度范围极大,而屏幕能显示的亮度范围有限。PBR Shader计算出的颜色值很可能超过1.0。你不能简单把它钳制在[0,1],那会丢失所有高光细节。正确的做法是将HDR颜色值传递给后处理管线,用ACESFilmicReinhard这样的色调映射算子,优雅地将宽广的动态范围压缩到屏幕能显示的范围里,同时保留“过曝”区域的光晕感。这行代码往往不在表面Shader里,但它决定了PBR渲染的最终气质。

所以,下次当你调出一个质感惊人的材质时,不妨想想,那不只是几张漂亮的贴图,更是微表面上的亿万次光路追踪,被一行行冷静的代码,在1/60秒内暴力求解出来的结果。

参与讨论

13 条评论
  • 自恋的土豆

    微表面理论这块终于讲明白了,之前一直懵懵的👍

  • 影子人

    这解释比文档清晰多了,D/G/F三个函数终于对上号了

  • 猎豹特工

    线性空间那块是不是得强制framebuffer用linear space?

  • 寒江墨士

    我之前调材质老觉得发灰,原来是gamma校正漏了…

  • 幽光

    金属度插值那里,F0用albedo是不是会导致非金属偏色?

  • 冰魄寒

    前几天刚重写了一遍GGX,性能还行但移动端得省着用

  • 醉卧江湖

    说白了就是用统计学模拟微观结构,对吧?

  • 骆驼坚韧

    HDR处理不用tonemap直接clamp会怎样?画面是不是就死了?

  • 机械飞鸟

    绝了,原来菲涅尔不只是边缘亮这么简单

  • 抠门精

    漫反射乘(1-F)这步好多自定义shader都忽略了,难怪过曝

  • 暗黑骑士

    粗糙度影响高光形状这点,美术同学得好好科普

  • 软绵绵球

    这个系列能出个完整shader代码示例吗?想动手试试

  • 黯夜独行

    gamma校正用sRGB纹理采样是不是更高效?