# Kulla-Conty 多次散射补偿

# 单次反射的问题

经典的 BRDF 模型只描述单次表面反射。但真实微面之间会互相遮挡、反弹。
对于粗糙度较高的材质,由于模型忽略了光线在微表面间的多次弹射,所以会导致渲染结果比真实情况偏暗,且能量不守恒。

# 反射补偿

在游戏开发中,Kulla-Conty 近似方案是解决微表面模型能量流失的标准方法。Kulla-Conty 提供了一个简单且高效的经验公式来补偿这些丢失的多次散射能量。

在下文中,我们约定:

  • nn 为宏观表面法线
  • vv 为观察方向
  • ll 为光照方向
  • μo=nv\mu_o = n \cdot v 观察方向与法线的夹角余弦
  • μi=nl\mu_i = n \cdot l 光照方向与法线的夹角余弦
  • fssf_{ss} 单次散射 BRDF 传统的微表面项
  • 如果把法线 nn 看作球体的北极
    • θ\theta 是极角,代表纬度,决定了向量离法线有多远
    • ϕ\phi 是方位角,代表经度,决定了向量绕着法线转到了哪个方向。
    • ϕo\phi_o 指的是出射方向(即视线 vv)在表面切平面上的旋转角度

第一步:计算方向反照率 E(μ)E(\mu)

E(μ)=02π01fss(μ,μo,ϕ)μodμodϕoE(\mu) = \int_{0}^{2\pi} \int_{0}^{1} f_{ss}(\mu, \mu_o, \phi) \mu_o d\mu_o d\phi_o

对于一个入射方向 μi\mu_i,单次散射模型反射出的总能量为 E(μi)E(\mu_i)。那么丢失的能量就是 1E(μi)1 - E(\mu_i)

第二步:计算平均反照率 EavgE_{avg} 这是对所有入射角度的 E(μ)E(\mu) 进行二次加权平均

Eavg=201E(μ)μdμE_{avg} = 2 \int_{0}^{1} E(\mu) \mu d\mu

第三步:构造多次散射补偿项 fmsf_{ms}
为了补全能量,Kulla-Conty 提出了一个对称的补偿项。对于纯白物体,补偿项与单次反射项相加后,总的反照率接近 1

fms(μi,μo)=(1E(μi))(1E(μo))π(1Eavg)f_{ms}(\mu_i, \mu_o) = \frac{(1 - E(\mu_i))(1 - E(\mu_o))}{\pi (1 - E_{avg})}

(1E(μo))(1E(μi))(1 - E(\mu_o))(1 - E(\mu_i)) 确保了根据观察角和入射角成比例地补充能量
π(1Eavg)\pi (1 - E_{avg}) 是归一化因子,保证积分后的总能量恰好等于丢失的总量

第四步,考虑菲涅尔效应
上面的公式假设反射率是 100%100\%。对于带颜色的金属或非金属,需要引入平均菲涅尔项 FavgF_{avg} 对补偿项进行修正。
为了得到 FavgF_{avg},我们需要对这个函数在半球上进行加权积分:

Favg=201(F0+(1F0)(1μ)5)μdμF_{avg} = 2 \int_{0}^{1} (F_0 + (1 - F_0)(1 - \mu)^5) \mu d\mu

经过积分演算,可以得到一个非常简洁的线性代数公式:

Favg=F0+(1F0)(1/21)F_{avg} = F_0 + (1 - F_0)(1 / 21)

Favg=2021F0+121F_{avg} = \frac{20}{21} F_0 + \frac{1}{21}

最终结果

ffinal=fss+fmsFavg1Favg(1Eavg)f_{final} = f_{ss} + \frac{f_{ms} \cdot F_{avg}}{1 - F_{avg}(1 - E_{avg})}

在游戏开发中的实现
在 Shader 中实时计算积分是很耗费性能的,所以通常的做法是将积分预计算并存储在一张 2D 贴图(LUT)中。在渲染管线中,直接读取这张图得到积分值,其他部分仍然按照上述公式计算即可。

# BRDF + 反射补偿 与 Albedo 的计算

计算方式如下:

specular=ffinalspecular = f_{final}

kd=(1F)(1metallic)kd = (1 - F)(1 - metallic)

diffuse=kdalbedoπdiffuse = \dfrac{kd \cdot albedo}{\pi}

brdf=(diffuse+specular)(nl)brdf = (diffuse + specular)(n \cdot l)

在物理模型中,如果镜面反射因为多次散射而变强了,那么理论上留给漫反射的能量应该减少。
但是为了性能、计算复杂度、视觉效果之间的平衡,通常不会因为加入了 fmsf_{ms} 而去扣除漫反射的能量,所以 kdkd 中仍然使用菲涅尔项 FF,不做更改。