Games202 - 作业4

Kulla-Conty Appoximation

Posted Kongouuu's Blog on November 3, 2021

Games202 作业4 Kulla-Conty补正

简介

我们在考虑微表面pbr的时候,会考虑到材质的微表面的法线分布和微表面的几何遮挡。假设我们的材质较为粗糙,像上图往右的材质,我们的物品会越来越暗。假设我们材质粗糙,那表示很大部分的光会往外跑,并且也会有增加的自遮挡行为,所以表面就会显得很暗,但这又比较不合理。

假设我的反射的光被其他微表面挡住了,那光线也不应该直接被其他微表面吞噬,而是应该继续反射。上图之所以暗是因为我们当前考虑的是一个单次bounce的模型,也就是被遮挡的光都直接消失了。更实际的效果应该是被遮挡的继续弹射,而没有上图这般的能量损失。

当然,为了实时渲染的效率,我们没有办法说去对每个射出的光进行微表面弹射后继续计算第二次bounce的值,所以需要用Kulla-Conty去模拟粗糙表面的能量补偿。

Kulla Conty 理论

计算

1. 计算BRDF反射了多少能量

上面的方程用于计算我们brdf到底反射了多少能量。实际上右手边就是一个普通的渲染方程(对球面积积分 Li * Fr * cos 的),这里只是对渲染方程进行了以下几个修改

  1. 假设入射光Li的值为1
  2. 把球面的积分展开成两个积分。这个变换会把 [∫A dω] 变成 [∫∫sinθdθdΦ]
  3. 积分换元,我们在第一段积分内通过设置μ = sinθ ,把cosθsinθdθ 改成 μ dμ 并且积分域也跟着变动。

第一次看到公式的时候挺懵的,最后发现其实根本上就是入射光值为1的渲染方程罢了,实际从渲染方程到上面的形式的过程如下:

\[E(\mu _o) = \int_{A}^{} L_i * Fr * cos\theta _i\, d\omega _i \,\,\,\,A为半圆 \\ = \int_{0}^{2\pi} \int_{0}^{\pi/2} L_i * F_r * cos\theta _i * sin\theta _i\,\,\, d\theta _i \, d\phi \\ [球面积分就是∫∫sinθdθdΦ的形式,\theta 到\pi/2且\phi到2\pi就是半球的范围] \\ = \int_{0}^{2\pi} \int_{0}^{1} L_i * F_r * \mu\,\,\, d\mu \, d\phi \\ [\mu = sin\theta \,\,\,\,\, d\mu = cos\theta d\theta]\]

2. 计算补偿能量的BRDF

通过1.的计算,我们可以得出我们的渲染方程他到底反射了多少能量,换句话说我们也可以得出他损失了多少能量,也就是 [ 1-E(μo) ]。但为了得出这个E(μo),我们需要算出这个补偿的BRDF fmsoi,Φ) ,让我们可以通过积分来得到我们需要的能量损失。

既然我们需要计算[ 1-E(μo) ], 那我们完全可以把他放到积分的主体内部:

这里有以下几点要注意的:

  1. 除了[ 1-E(μo) ],我们还考虑了BRDF另一侧的[ 1-E(μi) ],因为BRDF是双向的方程,所以我们需要去考虑他
  2. 两个损失能量函数相乘结果可能低于我们所期待的,所以分母这里有一个归一化的常量

3. 上面的能量补偿意味着什么

一开始学的时候看到说能量损失是[ 1-E(μo) ],那为什么不直接把这一项补回去?为什么要做积分?其实也不是很难理解。

首先我们计算出的一个结果[ E(μo) ]指的是在整个半球范围内接受完全光照的情况下,针对反射方向ωo有多少的损失,但我们需要的不是这个,因为我们在渲染的时候并不会有在半球内接受完全光照的情况。为了计算能量补偿我们还需要考虑入射的角度,无脑的去补偿只会让材质闪瞎狗眼。

那我们要怎么算出特定入射角的整体补偿呢? 这时候我们考虑另一个渲染方程模型““补偿渲染方程”。这个模型里面的BRDF,也就是fms并不是反射了多少能量,而是补偿了多少能量。这个渲染方程就是上上图所描述的一个渲染方程。

我们知道在接受全角度完全接受光照的时候,我们使用正常的高光BRDF反射了E(μo)的能量,所有我们希望我们的“补偿渲染方程”在半圆面积接受完全光照的情况下可以补偿出对应的[ 1-E(μo) ] 。 积分只是一个推导的过程,最后需要用到的不过是推导出来的fms,我们可以通过这个方程去计算在特定的入射以及观察角度下,我们应该补偿多少能量。

作业怎么写

Part1: 完成E(μ)的计算

我们预计算实际再算什么

在计算之前要先了解一下我们计算的E(μ)他究竟是什么。 这里我们并不是把整个场景所有的静态物体拿来进行考虑来计算一个能量损失,我们仔细的去观察这个BRDF公式会发现这里并没有“立体角在球面面积”这方面的考量。 这里对结果有影响的只有我们选择的观察点V和法线N的夹角, 还有就是我们采样的光源入射点。

所以很简单的理解就是,比起对场景所有的物体操作,不如我们直接去算在一个2D的1/4圆内的所有观察点,我们的BRDF会有多少能量损失。 这里考虑的是2D的四分之一圆是因为我们本来考虑的就是一个3D半圆的物体,在这个范围内我们会反复计算 dot(V,N),而这个值,也就是观察点和法线的夹角cosθ,只会在0~1的区间出现。

意思就是,我们只需要计算在不同的粗糙度和不同角度的V(也就是不同的dot(V,N)的值),这样的二维输入存到一个可以采样的贴图就好了

那么怎么去计算

E(μ)的计算其实挺简单的,就是用蒙特卡洛去算一个入射光为1的渲染方程。

\[E(\mu _o) =\int_{A}^{} 1* F_r * cos\theta _i\, d\omega _i \,\,\,\, \\ = \int_{A}^{} \frac{F·D·G}{4(n·l)(n·v)} * cos\theta _i\, d\omega _i \\ 因为(n·l) = cos\theta_i \\ = \int_{A}^{} \frac{F·D·G}{4(n·v)} d\omega _i \\ 因为白炉测试考虑的是金属材质F_0 = 1 , 因此菲涅尔项F = 1 \\ E(\mu _o) = \int_{A}^{} \frac{D·G}{4(n·v)} d\omega _i \\\]

所以我们只需要使用给定的法线分布函数和几何遮蔽函数就可以轻易的做出这一部分的蒙特卡洛积分。作业里使用的是GGX光照模型的法线分布和几何遮蔽。

Part1 Bonus: 用重要性采样计算E(μ)

为什么要使用重要性采样?使用传统的GGX的模型生成的pdf他并不能很好的去形容我们整个法线分布的情况,所以上一步做出来的效果有比较大的噪音。似乎是Par1所使用的随机向量和pdf值计算的跟实际情况不符合的样子,这部分目前还不太了解,先写代码。

  1. 生成采样方向:采样点的这个hemmersy函数在这里是没有被介绍的,总之就是拿了两个值来套公式得到θ和Φ, 通过角度计算出方向向量。通过给予的normal计算出TBN转换矩阵,把得到的方向从切线空间转换到世界空间上。

  1. 计算渲染方程:在多个sample的背景下,我们平时如果用brdf计算出来的值要除以一个pdf,代表来自该入射光立体角的权重。在多重性采样的背景下公式变成了下面这个样子

    代码也变成简单粗暴的:

一轮写下来后感觉是传统GGX的方式的微表面法线和实际不符,加上并没有过多考虑各个微表面法线应该有的概率密度吧。

Part2: 完成Eavg(μ)的计算

Eavg这个变量主要是用于后面的补偿计算。这个值的本质意思就是在所有可能的角度下的BRDF能量反射的平均值。我们前面计算出来的E(μ)则是考虑了单一的观察方向

计算方式比较的无脑,用上一阶段在每个像素算出来的E,在每个sample方向上乘以cosθ [也就是 N·L]就可以了

\[E_{avg} = 2\int_{0}^{1}E(\mu)\mu_i\, d\mu_i \\ \mu = sin\theta\ \\d\mu = cos\theta\,d\theta \\ E_{avg} = 2\int_{0}^{\pi/2}E(\mu)cos\theta\, d\theta\]

Part3: 完善Shader

跟上面的介绍的一样,我们把提前预计算的所有的视角采样点和粗糙度对应的 E(μ)Eavg(μ) 都存在了各自的贴图里。我们在考虑补偿项的时候可以直接采样到入入射时的损失E(μi)和出射时的损失,套作业里面的公式直接得到补偿后的渲染结果…