4、BRDF的几何解释:入射方向与出射方向的关系,半角向量与差向量,微表面理论中的几何衰减项

好,咱们进入第四章。这一章我打算聊聊BRDF背后那些几何关系。说实话,刚入行那会儿,我觉得BRDF就是个数学公式,套进去算就完了。后来被一个诡异的渲染结果折磨了两天,才发现——嗯,几何没搞懂,代码写得再漂亮也白搭。

这一章的核心就三件事:入射和出射到底怎么转的、半角向量和差向量是干嘛用的、以及微表面里那个“遮挡”怎么算。咱们一个一个来。

4.1 入射方向与出射方向:坐标系里的那点事

先看最基础的。BRDF是个四维函数,f(ωi, ωo)。两个方向,每个方向两个自由度(仰角θ和方位角φ)。但实际用的时候,我们很少直接拿原始角度算。

我个人习惯把入射方向ωi和出射方向ωo都转换到局部坐标系里。什么叫局部坐标系?就是以表面法线n为z轴,切线和副法线为x、y轴的那个坐标系。为什么这么做?因为BRDF假设表面是局部平坦的,所有计算都围绕法线展开。

关键点:BRDF的几何关系只依赖于方向之间的相对角度,不依赖绝对世界坐标。所以局部坐标系是天然的选择。

举个例子,你写一个Cook-Torrance BRDF,输入的是世界空间的ωi和ωo。第一件事就是把它们转到切线空间。我见过不少新手直接拿世界坐标算半角向量,结果法线贴图一上,全乱套了。

这里有个常用的转换关系:

方向 局部坐标表示 说明
ωi (sinθicosφi, sinθisinφi, cosθi) 入射方向,θi是入射天顶角
ωo (sinθocosφo, sinθosinφo, cosθo) 出射方向,θo是出射天顶角
n (0, 0, 1) 法线,就是z轴

你看,法线永远是(0,0,1)。这样很多计算就简化了。比如点积n·ωi直接等于cosθi。

我的小技巧:在shader里,我习惯把法线贴图采样结果从[0,1]映射到[-1,1],然后构建TBN矩阵。这样转换方向时只需要一个矩阵乘法,干净利落。

4.2 半角向量h:BRDF里的“中间人”

接下来是半角向量。这东西在微表面模型里太重要了。

半角向量h的定义很简单:

h = normalize(ωi + ωo)

它表示入射方向和出射方向的角平分线方向。为什么叫“半角”?因为h和ωi的夹角,等于h和ωo的夹角,各占一半。

你想想看,如果表面是一个完美的镜面,那么入射光反射出去,法线n正好就是半角向量h。换句话说,h就是微表面中那个“完美反射”的微平面法线。

这个理解太关键了。在微表面理论里,我们假设每个微平面都是完美镜面。那么对于一个给定的(ωi, ωo)对,只有法线方向恰好等于h的那些微平面,才能把光从ωi反射到ωo。其他朝向的微平面?它们反射的光去了别处。

核心结论:半角向量h直接决定了哪些微平面对当前光照方向有贡献。法线分布函数D(h)就是用来统计这些微平面的数量的。

我在项目中遇到过一个问题:各向异性材质(比如拉丝金属)的渲染。这时候半角向量不能直接用,得考虑一个修正项,把h投影到各向异性的切线和副法线方向上。嗯,这个后面讲材质参数时会细说。

4.3 差向量:另一种视角

除了半角向量,还有一个概念叫差向量。它不像半角向量那么常用,但在某些BRDF推导里会见到。

差向量通常定义为:

g = ωo - ωi

或者归一化版本。它表示从入射方向到出射方向的变化量。你可以把它理解为“光的方向改变了多少”。

差向量和半角向量之间有个简单关系:

  • 半角向量h = normalize(ωi + ωo)
  • 差向量g = ωo - ωi
  • 两者正交:h · g = 0

为什么会这样?因为h是ωi和ωo的和方向,g是差方向。和与差天然垂直。这个性质在推导BRDF的雅可比变换时很有用。

我个人很少直接用差向量做渲染,但在做重要性采样时,差向量能帮我理解采样分布的形状。比如GGX分布,它的采样方向围绕半角向量h分布,而差向量g决定了这个分布的“宽度”。

4.4 微表面理论中的几何衰减项G

好,重头戏来了。几何衰减项G,也叫阴影-遮挡函数。它解决的是微表面之间的相互遮挡问题。

你想想看,微表面是有起伏的。一个微平面可能被旁边的微平面挡住,导致它接收不到光照(阴影),或者它反射的光被挡住出不去(遮挡)。G项就是用来修正这个的。

G项通常分解为两个子项:

子项 含义 公式(Smith模型)
G1(ωi) 入射方向上的阴影概率 Λ(ωi) = ...
G1(ωo) 出射方向上的遮挡概率 Λ(ωo) = ...
G(ωi, ωo) 联合阴影-遮挡 G = G1(ωi) · G1(ωo)

最常用的G项模型是Smith模型。它假设微表面的高度分布和法线分布是相关的,然后通过一个Λ函数来计算遮挡概率。

对于GGX分布,Smith G项有解析解:

// GGX Smith G项
float SmithG_GGX(float NdotV, float alpha)
{
    float a = alpha * alpha;
    float b = NdotV * NdotV;
    return 2.0 * NdotV / (NdotV + sqrt(a + b - a * b));
}

// 联合G项
float G = SmithG_GGX(NdotL, roughness) * SmithG_GGX(NdotV, roughness);

这里NdotL是n·ωi,NdotV是n·ωo,alpha是粗糙度参数。

我曾经踩过的坑:一开始我直接用roughness作为alpha传进去,结果渲染出来的金属边缘发黑。后来才发现,GGX的alpha是roughness的平方。也就是说,alpha = roughness * roughness。这个细节在Disney原则的BRDF里特别容易搞错。

G项还有一个重要性质:当粗糙度趋近于0时,G趋近于1。因为表面越光滑,微平面越平坦,遮挡越少。反之,粗糙度越大,G越小,遮挡越严重。

另外,G项在掠射角(grazing angle)时会急剧下降。你想想看,当光线几乎平行于表面入射时,微平面之间的阴影效应非常明显。这就是为什么粗糙金属在边缘处会变暗——不是因为它不反射了,而是因为微表面互相挡住了。

4.5 知识结构图:BRDF几何关系一览

下面这张图把本章的核心概念串起来了。你可以看到入射方向、出射方向、半角向量、差向量以及几何衰减项之间的关系。

BRDF几何关系知识结构 入射方向 ωi 出射方向 ωo 局部坐标系 (TBN) 半角向量 h h = normalize(ωi + ωo) 差向量 g g = ωo - ωi h · g = 0(正交) 微表面理论 几何衰减项 G(ωi, ωo) 阴影 G1(ωi) 遮挡 G1(ωo) 核心逻辑:方向 → 半角/差向量 → 微表面 → 几何衰减

4.6 实战中的几何衰减项选择

最后聊点实际的。市面上常见的G项有好几种,我列个对比表:

G项模型 特点 适用场景 我的评价
Smith GGX 解析解,与NDF一致 大多数PBR材质 首选,稳定可靠
Smith Schlick-GGX 近似解,性能更好 移动端、实时渲染 精度损失可接受
Cook-Torrance 简单,不考虑高度相关 早期模型 已过时,不推荐
Kelemen 与Smith类似,更平滑 艺术风格化渲染 小众,但效果不错

我个人在项目中90%的情况都用Smith GGX。它和GGX法线分布函数是“天生一对”,物理一致性最好。只有在性能吃紧的时候,我才换成Schlick近似。

避坑指南:我曾经在实现Smith G项时,忘了对NdotV做clamp处理。结果在掠射角附近,NdotV接近0,G项分母出现极小值,导致整个BRDF爆炸。后来我养成了习惯:所有点积结果先clamp到[0.001, 1.0]再参与计算。