图形学基础|PBR回顾
一、前言
在辐射度量学和渲染方程中简单介绍了辐射度量学以及提及了渲染方程。
实时渲染,其本质就是模拟光线与各类材质进行交互的过程,在有限的时间内尽可能准确地近似求解渲染方程。
这就不得不提到基于物理的渲染(Physically Based Rendering,PBR)。
在之前学习OpenGL的过程中,也曾学习过一些PBR相关的基础,大多来自Learn OpenGL PBR Theory
。
当时对上述的知识囫囵吞枣,没有进行比较深刻的比较理解,在实现方面也比较简单,有一些预计算的贴图都是从网络上截取的资源。
这些过程的简化,对自己的图形渲染水平的提高都是不利的。因而,找个机会对这些知识再一次温习总结并实践理解。
本文的主要内容为:
- 概率论相关的基础知识,这部分是预计算时进行的采样和积分计算的基础知识。
- 蒙特卡洛积分,结合不同的采样给出如何对定积分进行近似求解。
- 反射方程,结合UE4的ShadingModel介绍反射方程中的各个项。
- IBL,以公式推导结合代码的方式对环境光的实现进行介绍。
二、概率论基础
2.1 随机变量
随机试验(E)
- 可以在相同的条件下重复地进行
- 每次试验的可能结果不止一个,并且能事先明确试验的所有可能结果
- 进行一次试验之前不确定哪一个结果会出现
例子:抛一枚硬币,观察正面,反面出现的情况。
样本空间(S)
随机试验所有可以能的结果组成的集合。
样本点
样本空间的元素,即每个可能的结果。
随机事件
随机试验E的样本空间S的子集称为随机事件。
在随机现象中,有很多问题与数值有密切的关系,这时引入了随机变量的概念。
将定义在样本空间 S S S上的函数称之为随机变量。这种函数的取值是不可预先确定的,是随机的。
随机变量可以分为:离散型和非离散型。
其中非离散型主要以连续型随机变量为主。离散型研究其分布律,而连续型则研究其概率密度(某一个点的改变不影响总体,因此求积分时开区间和闭区间没有太大区别)。
离散型随机变量:随机变量可能取到的值时有限个数或可列无限多个。
连续型随机变量:随机变量可能取到的值时无限个数。
这里举一个离散型随机变量的例子:
抛一枚骰子,观察其正面向上的数值,则有1,2,3,4,5,6这些结果。
2.2 分布函数(CDF)和概率密度函数(PDF)
随机变量的分布函数定义如下:(设 X X X为随机变量,x为任意实数)
F ( x ) = P ( X ≤ x ) , x ∈ ( − ∞ , ∞ ) F(x)=P(X\le x),x\in (-\infty,\infty) F(x)=P(X≤x),x∈(−∞,∞)
2.2.1 离散型变量
对于离散型随机变量 X X X,可以取的值有 x 1 , . . . , x i , . . . , x n x1,...,xi, ..., xn x1,...,xi,...,xn ,对应的概率为 P ( x 1 ) , . . . , P ( x i ) , . . . , P ( x n ) P(x1),...,P(xi), ..., P(xn) P(x1),...,P(xi),...,P(xn)。
那么,其分布函数则为:
F ( x ) = P ( X ≤ x ) = ∑ x i < x P ( x i ) F(x)=P(X \le x)= \sum_{xi<x}P(x_i) F(x)=P(X≤x)=xi<x∑P(xi)
对满足条件的随机变量对应的概率进行累加。
其中有很多常用的离散型随机分布,例如:
- 伯努利分布、二值分布、泊松分布。
2.2.2 连续型变量
对于连续型随机变量 X X X, f ( x ) f(x) f(x)为x的概率密度函数(PDF),简称为概率密度。
概率密度表示:随机变量取到 x x x值的概率为 f ( x ) f(x) f(x)。
分布函数定义如下:
F ( x ) = ∫ − ∞ x f ( t ) d t F(x)=\int_{-\infty}^{x}f(t)dt F(x)=∫−∞xf(t)dt
概率密度函数的积分,即围成的面积,为随机变量落入某一区间的概率:
F ( x 1 ≤ x ≤ x 2 ) = ∫ x 1 x 2 f ( t ) d t F(x1 \le x \le x_2)=\int_{x_1}^{x_2}f(t)dt F(x1≤x≤x2)=∫x1x2f(t)dt
其中有很多常用的随机型随机分布,例如:
- 均匀分布、正态分布、指数分布
更多详细细节可以参考 随机变量及其分布
2.3 期望和方差
数学期望,简称期望,又称为均值。数学期望完全由随机变量的分布所确定,若 X X X服从某一分布,也称 E ( X ) E(X) E(X)是这一分布的数学期望。
离散型随机变量:
E ( X ) = ∑ k = 1 k = ∞ x k p k E(X)=\sum_{k=1}^{k=\infty}{x_kp_k} E(X)=k=1∑k=∞xkpk
连续型随机变量:
E ( X ) = ∫ − ∞ ∞ x f ( x ) d x E(X)=\int_{-\infty}^{\infty}{xf(x)dx} E(X)=∫−∞∞xf(x)dx
方差,表达了随机变量X的取值与其数学期望的偏离程度。
D ( X ) = V a r ( X ) = E [ ( X − E [ X ] ) 2 ] σ ( X ) = D ( X ) \begin{aligned} D(X) = & Var(X)=E[(X-E[X])^2]\\ \sigma(X) = & \sqrt{D(X)} \end{aligned} D(X)=σ(X)=Var(X)=E[(X−E[X])2]D(X)
其中,
由定义可知,方差实际上就是随机变量X的函数 g ( x ) g(x) g(x)的数学期望。
g ( X ) = ( X − E ( X ) ) 2 g(X)=(X-E(X))^2 g(X)=(X−E(X))2
离散型随机变量的方差:
D ( X ) = ∑ k = 1 ∞ [ x k − E ( X ) ] 2 p k D(X)=\sum_{k=1}^{\infty} [x_k-E(X)]^2 p_k D(X)=k=1∑∞[xk−E(X)]2pk
连续型随机变量的方差:
D ( X ) = ∫ − ∞ ∞ ( x − E ( X ) ) 2 f ( x ) d x D(X)=\int_{-\infty }^{\infty } (x-E(X))^2f(x)dx D(X)=∫−∞∞(x−E(X))2f(x)dx
2.4 二维随机变量
对于二维随机变量,而需要了解以下的概念。
设现在有二维连续型随机变量 ( X , Y ) (X,Y) (X,Y),二维随机变量的联合概率密度函数为 f ( x , y ) f(x,y) f(x,y),那么其联合分布函数为:
F ( x , y ) = P { X < x , Y < y } = ∫ − ∞ x ∫ − ∞ y f ( u , v ) d u d v F(x,y)=P\{X<x,Y<y\}=\int_{-\infty}^{x}\int_{-\infty}^{y}f(u,v)dudv F(x,y)=P{X<x,Y<y}=∫−∞x∫−∞yf(u,v)dudv
联合概率密度满足:
f ( x , y ) ≥ 0 , ∫ − ∞ ∞ ∫ − ∞ ∞ f ( x , y ) d x d y = 1 f(x,y) \ge 0,\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}f(x,y)dxdy=1 f(x,y)≥0,∫−∞∞∫−∞∞f(x,y)dxdy=1
X X X的边缘概率密度为:
f X ( x ) = ∫ − ∞ ∞ ( x , y ) d y f_X(x) = \int_{-\infty}^{\infty}(x,y)dy fX(x)=∫−∞∞(x,y)dy
在 X = x X=x X=x时, Y Y Y的条件概率密度为:
f Y ∣ X ( y ) = f ( x , y ) f X ( x ) f_{Y|X}(y) = \frac{f(x,y)}{f_X(x)} fY∣X(y)=fX(x)f(x,y)
2.5 概率分布变换
已知一个随机变量 X X X的概率密度为 f X ( x ) f_X(x) fX(x),现在我们令随机变量 Y = T ( X ) Y=T(X) Y=T(X)。
尝试求出 Y Y Y的概率密度函数 f Y ( y ) f_Y(y) fY(y)。为了计算方便,仅考虑T为单调递增的情况。
由分布函数定义已知:
P { Y < T ( x ) } = P { X < x } P\{Y<T(x)\} = P\{X<x\} P{Y<T(x)}=P{X<x}
因此有:
f Y ( y ) = f Y ( T ( x ) ) = f X ( x ) f_Y(y) = f_Y(T(x)) = f_X(x) fY(y)=fY(T(x))=fX(x)
对两边一起求导:
f y ( y ) d y d x = f X ( x ) f y ( y ) = ( d y d x ) − 1 f X ( x ) \begin{aligned} f_y(y)\frac{dy}{dx} = & f_X(x) \\ f_y(y) = & (\frac{dy}{dx})^{-1} f_X(x) \end{aligned} fy(y)dxdy=fy(y)=fX(x)(dxdy)−1fX(x)
这样,即可求得Y的概率密度函数。
而对于多维随机变量,设 X X X和 Y Y Y都是n维随机变量, X X X和 Y Y Y有以下转移关系: Y = M ( X ) Y=M(X) Y=M(X)。
有以下结论:
f Y ( y ) = f Y ( M ( x ) ) = f X ( x ) ∣ J T ( x ) ∣ f_Y(y) = f_Y(M(x)) = \frac{fX(x)}{|J_T(x)|} fY(y)=fY(M(x))=∣JT(x)∣fX(x)
其中, ∣ J T ( x ) ∣ |J_T(x)| ∣JT(x)∣表示M的雅克比矩阵的行列式的绝对值。M的雅克比矩阵为:
( ∂ M 1 / ∂ x 1 ⋯ ∂ M 1 / ∂ x n ⋮ ⋱ ⋮ ∂ M n / ∂ x 1 ⋯ ∂ M n / ∂ x n ) \begin{pmatrix} \partial M_1/ \partial x_1 & \cdots & \partial M_1/ \partial x_n\\ \vdots & \ddots & \vdots \\ \partial M_n/ \partial x_1 & \cdots & \partial M_n / \partial x_n \end{pmatrix} ⎝⎜⎛∂M1/∂x1⋮∂Mn/∂x1⋯⋱⋯∂M1/∂xn⋮∂Mn/∂xn⎠⎟⎞
在图形渲染中,使用得最多的有以下的情况:
球坐标系
单位球上的坐标 ( θ , ϕ ) (\theta,\phi) (θ,ϕ)和笛卡尔坐标系 ( x , y , z ) (x,y,z) (x,y,z)的转换如下:
x = c o s θ c o s ϕ y = s i n θ s i n ϕ z = c o s θ \begin{aligned} x = & cos \theta cos\phi \\ y = & sin \theta sin\phi \\ z = & cos \theta \end{aligned} x=y=z=cosθcosϕsinθsinϕcosθ
根据推导有,相应的概率密度为:
f ( θ , ϕ ) = s i n θ f ( x , y , z ) f( \theta , \phi ) = sin\theta f(x,y,z) f(θ,ϕ)=sinθf(x,y,z)
在球坐标系中,立体角的定义如下:
d w = s i n θ d θ d ϕ dw = sin\theta d\theta d\phi dw=sinθdθdϕ
立体角在某个 Ω \Omega Ω范围内的概率为:
P ( w ∈ Ω ) = ∫ Ω f ( w ) d w P(w \in \Omega) = \int_{\Omega} f(w) dw P(w∈Ω)=∫Ωf(w)dw
又 f ( θ , ϕ ) d θ d ϕ = f ( w ) d w f( \theta , \phi ) d\theta d\phi = f(w)dw f(θ,ϕ)dθdϕ=f(w)dw可以推导得到:
f ( θ , ϕ ) = s i n ( θ ) f ( w ) f( \theta , \phi ) = sin(\theta) f(w) f(θ,ϕ)=sin(θ)f(w)
注!这个概率密度公式在后续大有作为!
三、蒙特卡洛积分和重要性采样
蒙特卡洛方法是一种采用统计思路,用于求解难以直接求出具体值问题的方法。
在图形学中,蒙特卡洛方法最主要的应用就是帮助我们快速的求解积分。
什么是蒙特卡洛积分呢?
简而言之就是,在求积分时,如果找不到被积函数的原函数,那么利用经典积分方法是得不到积分结果的。
但是蒙特卡洛积分方法告诉我们,利用一个随机变量对被积函数进行采样,并将采样值进行一定的处理,那么当采样数量达到一定时,得到的结果可以很好的近似原积分的结果。这样一来,我们就不用去求原函数的形式,就能求得积分的近似结果。
而重要性采样和蒙特卡洛积分二者是两个不同的概念,仅仅是因为经常一起使用。
3.1 黎曼和
在介绍蒙特卡洛积分之前,让我们先来了解一下黎曼和(Riemann Sum)。
黎曼和是一种很简单的估计定积分的数值方法。
给定我们要求解的定积分: ∫ a b f ( x ) d x \int_{a}^{b}f(x)dx ∫abf(x)dx
如图所示,对于一个这样不规则的图形,很难直接求解出它的面积。
不过根据积分的第一中值定理,在积分区间 [ a , b ] [a,b] [a,b]一定可以找到存在一点 ε \varepsilon ε使得:
∫ a b f ( x ) d x = f ( ε ) ( b − a ) \int_{a}^{b}f(x)dx = f(\varepsilon)(b-a) ∫abf(x)dx=f(ε)(b−a)
即可以转换为一个长度为 b − a b-a b−a,高度为 f ( ε ) f(\varepsilon) f(ε)的矩形的面积。
那么,如何求解这个高度 f ( ε ) f(\varepsilon) f(ε)呢?
Riemann Sum是这样来求解的:
- 设定一个步进值step,从函数的定义域 [ a , b ] [a,b] [a,b]起点出发,一步一步的往终点去移动,计算每一个函数的值,然后将这些值求平均数(除以步进的步数)。
1 N ∑ i = 1 N f ( x i ) \frac{1}{N}\sum_{i=1}^{N}f(x_i) N1∑i=1Nf(xi)
则该定积分的估计为:
∫ a b f ( x ) d x ≈ b − a N ∑ i = 1 N f ( x i ) \int_{a}^{b}f(x)dx \approx \frac{b-a}{N}\sum_{i=1}^{N}f(x_i) ∫abf(x)dx≈Nb−ai=1∑Nf(xi)
3.2 蒙特卡洛积分
同样的,对于定积分 ∫ a b f ( x ) d x \int_{a}^{b}f(x)dx ∫abf(x)dx,蒙特卡洛积分的公式如下:
∫ a b f ( x ) d x ≈ 1 N ∑ i = 1 N f ( x i ) p d f ( x i ) \int_{a}^{b}f(x)dx \approx \frac{1}{N}\sum_{i=1}^{N} \frac{f(x_i)}{pdf(x_i)} ∫abf(x)dx≈N1i=1∑Npdf(xi)f(xi)
其中, p d f ( x ) pdf(x) pdf(x)表示概率密度分布函数。
我们对这个公式求期望E:
E [ 1 N ∑ i = 1 N f ( x i ) p ( x i ) ] = 1 N ∑ i = 1 N E [ f ( x i ) p ( x i ) ] = 1 N ∑ i = 1 N ∫ a b f ( x ) p ( x ) p ( x ) d x = 1 N ∑ i = 1 N ∫ a b f ( x ) d x = ∫ a b f ( x ) d x \begin{aligned} E[\frac{1}{N}\sum_{i=1}^{N} \frac{f(x_i)}{p(x_i)}] = & \frac{1}{N} \sum_{i=1}^{N} E[\frac{f(x_i)}{p(x_i)}]\\ = & \frac{1}{N} \sum_{i=1}^{N} \int_{a}^{b} \frac{f(x)}{p(x)}p(x)dx \\ = & \frac{1}{N} \sum_{i=1}^{N} \int_{a}^{b} f(x)dx \\ = & \int_{a}^{b} f(x)dx \end{aligned} E[N1i=1∑Np(xi)f(xi)]====N1i=1∑NE[p(xi)f(xi)]N1i=1∑N∫abp(x)f(x)p(x)dxN1i=1∑N∫abf(x)dx∫abf(x)dx
蒙特卡洛积分的数学期望与所要求的定积分是完全相同的。
pdf的准确度会大大的影响我们的收敛速度(也就是N的大小)。
当pdf较为准确时,那么我们只需少数的样本就对我们的积分值有一个较为准确的估计。
当pdf不够准确时,那么我们只需很多的样本才能对我们的积分值有一个较为准确的估计。
3.3 采样
在有了蒙特卡洛积分之后,我们就需要确定随机变量 X X X的概率分布了。
由于我们并不知道随机变量 X X X的概率密度函数 p ( X ) p(X) p(X),所以我们只能进行猜测。
其中的一种方案就是假设 X X X在样本空间是均分分布的,那么 p ( X ) p(X) p(X)即为一个常数。
3.3.1 均匀采样
均匀采样是图形学中常见的一个问题和方法。
这类问题的描述如下:
如何用[0,1]之间的均匀采样获得在XXX上的均匀采样?
其中,XXX可以是圆盘,球面,球体等。我们可以利用的只有[0,1]之间的均匀采样(可以通过程序的伪随机实现,后续再单独进行介绍)。
下面我们将对一些均匀采样的获得进行推导介绍。
单位半球面均匀采样
对于半球面的均匀采样而言,采样的是面积,半球面的面积可以用立体角 ω \omega ω来表示,即 p ( ω ) p(\omega) p(ω)为常数,根据半球面积可求得:
p ( w ) = 1 2 π p(w)=\frac{1}{2\pi} p(w)=2π1
再结合2.4(概率分布变换)中所得的结论,则可以得到球坐标下的概率密度:
p ( θ , ϕ ) = s i n θ f ( w ) = s i n θ 2 π p(\theta ,\phi ) = sin\theta f(w) = \frac{sin\theta }{2\pi } p(θ,ϕ)=sinθf(w)=2πsinθ
有了pdf,则可以求得边缘pdf以及条件pdf:
p
(
θ
)
=
∫
0
2
π
p
(
θ
,
ϕ
)
d
ϕ
=
s
i
n
θ
p(\theta) = \int_{0}^{2\pi}p(\theta ,\phi )d\phi = sin\theta
p(θ)=∫02πp(θ,ϕ)dϕ=sinθ
p
(
ϕ
∣
θ
)
=
p
(
θ
,
ϕ
)
p
(
θ
)
=
1
2
π
p(\phi|\theta) = \frac{p(\theta ,\phi )}{p(\theta) } = \frac{1}{2\pi}
p(ϕ∣θ)=p(θ)p(θ,ϕ)=2π1
接下来可以求解CDF(概率分布函数):
P
(
θ
)
=
∫
0
θ
s
i
n
θ
′
d
θ
′
=
1
−
c
o
s
θ
P(\theta) =\int_{0}^{\theta }sin\theta'd\theta' = 1-cos\theta
P(θ)=∫0θsinθ′dθ′=1−cosθ
P ( ϕ ) = ∫ 0 ϕ 1 2 π d ϕ ′ = ϕ 2 π P(\phi) = \int_{0}^{\phi}\frac{1}{2\pi} d\phi' = \frac{\phi}{2\pi} P(ϕ)=∫0ϕ2π1dϕ′=2πϕ
根据生成的[0,1]之间的均匀采样,则可以反求出对应的 ( θ , ϕ ) (\theta,\phi) (θ,ϕ):
θ
=
a
r
c
c
o
s
(
1
−
ξ
1
)
\theta = arccos(1-\xi_1 )
θ=arccos(1−ξ1)
ϕ
=
2
π
ξ
2
\phi = 2\pi \xi_2
ϕ=2πξ2
其中, ξ 1 \xi_1 ξ1和 ξ 2 \xi_2 ξ2则是通过[0,1]均匀分布生成的。
单位半球面均匀采样,是在图形渲染中经常使用到的一个采样方式。
单位半球面余弦权重采样
余弦采样指的是采样点在半球面分布的概率 p ( w ) p(w) p(w)与角度 θ \theta θ的余弦 c o s θ cos\theta cosθ正相关,概率密度的求解过程如下:
∫ Ω p = 1 ∫ 0 2 π ∫ 0 π / 2 c ∗ c o s θ s i n θ d θ d ϕ = 1 c = 1 π p ( θ , ϕ ) = 1 π c o s θ s i n θ \begin{aligned} \int_{\Omega}{p} = & 1 \\ \int_{0}^{2\pi}\int_{0}^{\pi/2}{c\ast cos\theta sin\theta d\theta d\phi } = & 1\\ c = & \frac{1}{\pi } \\ p(\theta ,\phi ) = & \frac{1}{\pi } cos\theta sin\theta \end{aligned} ∫Ωp=∫02π∫0π/2c∗cosθsinθdθdϕ=c=p(θ,ϕ)=11π1π1cosθsinθ
有了pdf,则可以求得边缘pdf以及条件pdf:
p ( θ ) = s i n 2 θ p ( ϕ ∣ θ ) = 1 2 π \begin{aligned} p(\theta) = & sin2\theta \\ p(\phi|\theta) = & \frac{1}{2\pi} \end{aligned} p(θ)=p(ϕ∣θ)=sin2θ2π1
再根据pdf求解CDF:
P ( θ ) = 1 − c o s 2 θ 2 = s i n 2 θ P ( ϕ ) = ϕ 2 π \begin{aligned} P(\theta) = & \frac{1-cos2\theta }{2} = sin^2\theta \\ P(\phi) = & \frac{\phi}{2\pi} \end{aligned} P(θ)=P(ϕ)=21−cos2θ=sin2θ2πϕ
根据生成的[0,1]之间的均匀采样,则可以反求出对应的 ( θ , ϕ ) (\theta,\phi) (θ,ϕ):
θ = 1 − ξ 1 2 ϕ = 2 π ξ 2 \begin{aligned} \theta = & \sqrt{1-\xi_1^2} \\ \phi = & 2\pi \xi_2 \end{aligned} θ=ϕ=1−ξ122πξ2
通过上述两个例子,可以小结一下求解思路:
- 用PDF来描述题目要求,求出所有变量的联合PDF;
- 求出第一个变量的边缘PDF
- 根据已知的边缘PDF或条件PDF,依次求其余变量的条件PDF
- 求所有变量的CDf,并令其分别等于 ξ i \xi_i ξi(来自于[0,1]均匀分布的采样值)
- 反求各个变量和 ξ i \xi_i ξi的关系
备注:这里我们来看一种均匀分布的2D随机采样。
他将十进制转换成二进制,再将二进制转换到[0,1]之间的小数,这一过程被称作 Radical Inverse。具体的方法如下图。
Hammersley采样点集合:
( x i , y i ) = ( i / N , ϕ ( i ) ) (x_i,y_i) = (i/N,\phi(i)) (xi,yi)=(i/N,ϕ(i))
其中,N为采样点总数, ϕ ( i ) \phi(i) ϕ(i)为RadicalInverse之后得到的小数。
代码实现如下:
float RadicalInverse( uint bits ){
//reverse bit
//高低16位换位置
bits = (bits << 16u) | (bits >> 16u);
//A是5的按位取反
bits = ((bits & 0x55555555) << 1u) | ((bits & 0xAAAAAAAA) >> 1u);
//C是3的按位取反
bits = ((bits & 0x33333333) << 2u) | ((bits & 0xCCCCCCCC) >> 2u);
bits = ((bits & 0x0F0F0F0F) << 4u) | ((bits & 0xF0F0F0F0) >> 4u);
bits = ((bits & 0x00FF00FF) << 8u) | ((bits & 0xFF00FF00) >> 8u);
return float(bits) * 2.3283064365386963e-10;
}
vec2 Hammersley(uint i,uint N){
return vec2(float(i) / float(N), RadicalInverse(i))
}
3.3.2 重要性采样
为什么要使用重要性采样?
比如:
- 下图表示一束光线沿着 w i w_i wi方向,遇到一个微平面的反射情况,反射的光线呈现一个椭圆。
- 如果我们想要计算所有反射光线总和,可以对每一个方向都进行计算然后累加即可。
- 但我们可以知道如果我们的采样的光线超过这个椭圆的范围,那么贡献为0,对最后的结果不会有任何的影响。
- 因此,我们需要尽量采样落在椭圆范围内的光线,使用少量的采样获得较好的增益。
因此,重要性采样的目的就是有针对性的进行采样计算,而不是完全的进行随机采样,从而减少一些无效值的计算。
重要性采样小结这篇文章给出了如何根据NDF得到相应的重要性采样方法。
例如通过GGX的NDF推导出了类似于我们上述均匀采样所得到的 ( θ , ϕ ) (\theta,\phi) (θ,ϕ)的计算公式。
四、反射方程
基于物理的渲染遵循的是反射方程(The Reflectance Equation),这是渲染方程特化版本。
其公式如下:
L o ( p , w o ) = ∫ Ω f r ( p , w i , w o ) ( w i ⋅ n ) d w i L_o(p,w_o) = \int_{\Omega} f_r(p,w_i,w_o)(w_i\cdot n)dw_i Lo(p,wo)=∫Ωfr(p,wi,wo)(wi⋅n)dwi
其中, f r f_r fr 为BRDF(双向反射分布函数)。
下面将介绍一下《Real Shading in Unreal Engine 4》中提及的虚幻的着色模型(Shading Model)。
f r = k d f l a m b e r t + k s f c o o k − t o r r a n c e f_r = k_d f_{lambert} + k_s f_{cook-torrance} fr=kdflambert+ksfcook−torrance
其中, k s k_s ks表示入射光被反射部分的比例, k d k_d kd表示被折射的比例。
4.1 Diffuse BRDF
Unreal在评估了Burley的漫反射模型,只观察到和与Lambertain模型相比轻微的差别。
除此之外,任何更复杂的漫反射模型很难高效的使用基于图像光照或球谐光照(IBL和SH)。
UE在Diffuse BRDF选择了Lambertain模型,公式如下:
f ( l , v ) = c d i f f π f(l,v) = \frac{c_{diff}}{\pi} f(l,v)=πcdiff
其中, c d i f f c_{diff} cdiff为材质的漫反射率(diffuse albedo)。
float3 Diffuse_Lambert( float3 DiffuseColor ) {
return DiffuseColor * (1 / PI);
}
4.2 Microfacet Specular BRDF
常规的Cook-Torrance微表面镜面反射着色模型如下:
f ( l , v ) = D ( h ) F ( v , h ) G ( l , v , h ) 4 ( n ⋅ l ) ( n ⋅ v ) f(l,v)=\frac{D(h)F(v,h)G(l,v,h)}{4(n\cdot l)(n\cdot v)} f(l,v)=4(n⋅l)(n⋅v)D(h)F(v,h)G(l,v,h)
其中,各个项都具有其特定的含义。
- D,法线分布函数 Normal Distribution Function(NDF)。
- G,几何函数。
- F,菲涅尔方程。
4.2.1 法线分布函数 D
D,法线分布函数 Normal Distribution Function(NDF),描述的是微表面法线的分布。
当越多的微表面法线与宏观表面法线相近的时候,高光区域会越亮,区域越小,能量越集中。
反之,高光区域会越暗,区域越大。
举例来说,假设给定一个向量v,如果我们的微平面中有35%与向量v取向一致,则法线分布函数(NDF)将会返回0.35。
UE这里选择了GGX法线分布函数,原因如下:
- 相比于Blinn-Phong的公式,GGX的额外消耗更小,并提供了更好的”长尾效果”,这个长尾效果的表现吸引了艺术家。
同时,其采用了迪士尼重新定义的参数 α = R o u g n e s s 2 \alpha = Rougness^2 α=Rougness2。
D项的公式如下:
D ( h ) = a 2 π ( ( n ⋅ h ) 2 ( a 2 − 1 ) + 1 ) 2 D(h)=\frac{a^2}{\pi((n\cdot h)^2(a^2-1)+1)^2} D(h)=π((n⋅h)2(a2−1)+1)2a2
该函数返回的是一个标量。
主流NDF函数高光长尾效果的对比,GGX拥有最好的长尾表现,效果如下:
UE4中着色器代码的D项实现:
float D_GGX( float Roughness, float NoH )
{
float a = Roughness * Roughness;
float a2 = a * a;
float d = ( NoH * a2 - NoH ) * NoH + 1; // 2 mad
return a2 / ( PI*d*d ); // 4 mul, 1 rcp
}
4.2.2 几何函数 G
G,几何函数,从统计学上近似地计算微平面间相互遮蔽的比率。这种相互遮蔽会损耗光线的能量。
几何函数采用一个材料的粗糙度参数作为输入参数,粗糙度较高的表面其微平面间相互遮蔽的概率就越高。
UE评估了几何衰减项的多种选择,最后选择使用Schlick模型。但 k = α 2 k=\frac{\alpha}{2} k=2α,以更好地拟合Smith模型GGX。
通过这种修改,Schlick模型与 α = 1 \alpha=1 α=1的Smith完全匹配,并且相当 [ 0 , 1 ] [0,1] [0,1]范围内的近似逼近。如下图所示。
公式如下:
k = ( r o u g h n e s s + 1 ) 2 8 G l ( v ) = n ⋅ v ( n ⋅ v ) ( 1 − k ) + k G ( l , v , h ) = G l ( l ) ∗ G l ( v ) \begin{aligned} k = & \frac{(roughness+1)^2}{8} \\ G_l(v) = & \frac{n\cdot v}{(n\cdot v)(1-k)+k}\\ G(l,v,h) = & G_l(l) \ast G_l(v) \end{aligned} k=Gl(v)=G(l,v,h)=8(roughness+1)2(n⋅v)(1−k)+kn⋅vGl(l)∗Gl(v)
几何函数(Geometry Function)是一个0到1之间的标量,描述了微平面自阴影的属性,表示了具有半矢量法线的微平面(microfacet)中,同时被入射方向和反射方向可见(没有被遮挡的)的比例,即未被遮挡的m= h微表面的百分比。
几何函数(Geometry Function)即是对能顺利完成对光线的入射和出射交互的微平面概率进行建模的函数。
- 观察方向,几何遮蔽(Geometry Obstruction)
- 光线入射方向,几何阴影(Geometry Shadowing)
之前的Schlick模型的实现代码如下,但目前UE中并没有使用这个。
// Tuned to match behavior of Vis_Smith
// [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"]
float Vis_Schlick( float a2, float NoV, float NoL )
{
float k = sqrt(a2) * 0.5;
float Vis_SchlickV = NoV * (1 - k) + k;
float Vis_SchlickL = NoL * (1 - k) + k;
return 0.25 / ( Vis_SchlickV * Vis_SchlickL );
}
当前UE使用的几何函数G为:
// Appoximation of joint Smith term for GGX
// [Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"]
float Vis_SmithJointApprox(float a2, float NoV, float NoL)
{
float a = sqrt(a2);
float Vis_SmithV = NoL * (NoV * (1 - a) + a);
float Vis_SmithL = NoV * (NoL * (1 - a) + a);
return 0.5f * 1.0f /(Vis_SmithV + Vis_SmithL);
}
4.2.3 菲涅尔方程 F
F,菲涅尔方程,表示的是入射光线被表面反射所占的比例。
这个比例会随着我们观察的角度的不同而不同。
经典的Schlick近似公式如下:
F ( h , v , F 0 ) = F 0 + ( 1 − F 0 ) ( 1 − h ⋅ v ) 5 F(h,v,F_0)=F_0+(1-F_0)(1-h\cdot v)^5 F(h,v,F0)=F0+(1−F0)(1−h⋅v)5
其中, F 0 F0 F0为表面的基本反射属性。
F项描述的是镜面反射部分的比例,那么漫反射部分的比例自然就是1-F。
对于金属材质而言,只有镜面反射,没有漫反射。则对于F项而言,需要区别对待金属材质和非金属材质。
因而,材质的属性输入存在一个Metallic
金属项。
前面所提到的 k s k_s ks其本质就是F项,因而可以求解出 k d k_d kd,代码如下:
vec3 ks = F;
vec3 kd = vec3(1.0f)-ks;
kd *= 1.0f - Metallic; // 对于金属而言,只有镜面反射(Metallic=1,kd=0)
虚幻采用对Schlick的近似作为F项,即使用了球面高斯Spherical Gaussian近似代替power计算,以此提高计算效率,并且差异微不可察,公式为:
F ( h , v , F 0 ) = F 0 + ( 1 − F 0 ) 2 ( − 5.55473 ( v ⋅ h ) − 6.98316 ) ( v ⋅ h ) F(h,v,F_0)=F_0+(1-F_0)2^{(-5.55473(v\cdot h)-6.98316)(v\cdot h)} F(h,v,F0)=F0+(1−F0)2(−5.55473(v⋅h)−6.98316)(v⋅h)
采用球面高斯的近似来代替Power的选择,主要是考虑在GPU上运算的效率来考虑(GPU指令减少了50%)。
然而,实际F项并没有使用球面高斯近似,而是采用一种更高效的方式:
Fc + (1-Fc) * SpecularColor
,SpecularColor本质就是 F 0 F_0 F0。
// [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"]
float3 F_Schlick( float3 SpecularColor, float VoH )
{
float Fc = Pow5( 1 - VoH ); // 1 sub, 3 mul
//return Fc + (1 - Fc) * SpecularColor; // 1 add, 3 mad
// Anything less than 2% is physically impossible and is instead considered to be shadowing
// 50的意义:F0小于0.02在物理上是没有意义的,小于0.02的值被当做阴影处理。
return saturate( 50.0 * SpecularColor.g ) * Fc + (1 - Fc) * SpecularColor;
}
至此,对于UE的PBR模型的各项都有了基本的了解,在直接光照中,可以依据材质的属性对各个项进行计算,最后获得光照结果。
五、IBL
PBR 其本质是一个光照模型,相比于经典的光照模型:
-
漫反射采用
Lambert
-
镜面反射采用
blinn-Phong
-
环境光采用
C*(1-AO)
在PBR中:
- 漫反射为:
Lambert/PI
- 镜面反射采用
Cook-Torrance
模型; - 环境光则通过
IBL(Image-Based Lighting)
实现。
对于直接光照,漫反射和镜面反射的计算在前面已经介绍过光照模型了。
对于一个着色点而言,其所受的环境光来自于四面八方(各个方向),因而无法实时地对每个着色点的环境光进行计算,需要采用一系列算法进行预计算,从而在实时中可以直接获取或经过简单的计算进行实现环境光效果。
一般而言,可以通过一张环境贴图(Environment Map)来保存一个物体周围的环境信息。然后通过某种处理,来实现丰富的环境光照效果。
漫反射环境光照部分,一般采用传统IBL中Irradiance Environment Mapping技术,也可以采用SH进行存储和重建(可参考另一篇博文球谐光照(Spherical Harmonics Lighting))。
镜面反射环境光照部分,一般则采用了UE提供的Split Sum Approximation分解求和近似。
下面将分别进行介绍和提供实现代码。
5.1 漫反射环境光照
在这里,我们介绍IrradianceMap。
首先,看一下反射方程的漫反射部分。
L d i f f = k d ∫ Ω f d L i ( p , w i ) ( n ⋅ w i ) d w i f d = c d i f f π \begin{aligned} L_{diff} = & k_d \int_{\Omega}{f_d Li(p,w_i)(n\cdot w_i)dw_i} \\ f_d = & \frac{c_{diff}}{\pi} \end{aligned} Ldiff=fd=kd∫ΩfdLi(p,wi)(n⋅wi)dwiπcdiff
其中, k d k_d kd表示环境光中漫反射的比例。又由于 f d = c d i f f π f_d=\frac{c_{diff}}{\pi} fd=πcdiff为常数。可以提取到积分符号之外,则有:
L d i f f = k d c d i f f π ∫ Ω L i ( p , w i ) ( n ⋅ w i ) d w i = k d c d i f f π ∫ 0 2 π ∫ 0 π 2 L i ( p , w i ) c o s θ s i n θ d θ d ϕ \begin{aligned} L_{diff} = & k_d \frac{c_{diff}}{\pi} \int_{\Omega}{Li(p,w_i)(n\cdot w_i)dw_i} \\ = & k_d \frac{c_{diff}}{\pi} \int_{0}^{2\pi} \int_{0}^{\frac{\pi}{2}} L_i(p,w_i)cos\theta sin\theta d\theta d\phi \end{aligned} Ldiff==kdπcdiff∫ΩLi(p,wi)(n⋅wi)dwikdπcdiff∫02π∫02πLi(p,wi)cosθsinθdθdϕ
现在,积分项只取决于法线 n n n,与观察方向(相机方向) w 0 w_0 w0无关, Ω \Omega Ω是指法线方向所在的上半球。
因而,积分项是可以提前进行计算的!在实时渲染时仅需用法线 n n n进行采样即可。
这个思路就是IrradianceMap。
要求解定积分的数值可以采用蒙特卡洛积分。但不同的采样方式获得求解公式亦不同。
5.1.1 均匀采样
这里的均匀采样指的是对角度 ( θ , ϕ ) (\theta,\phi) (θ,ϕ)进行均匀采样。
相当于将角度看作一个二维平面,进行均匀采样。
则概率密度函数为:
p ( θ i , ϕ j ) = 1 2 π ∗ 1 0.5 π p(\theta_i,\phi_j)=\frac{1}{2\pi}\ast \frac{1}{0.5\pi} p(θi,ϕj)=2π1∗0.5π1
则积分项可以进行如下近似:
∫ Ω L i ( p , w i ) ( n ⋅ w i ) d w i = ∫ 0 2 π ∫ 0 π 2 L i ( p , w i ) c o s θ s i n θ d θ d ϕ = 1 N 1 N 2 ∑ i N 1 ∑ j N 2 L i ( p , θ i , ϕ j ) c o s θ i s i n θ i p ( θ i , ϕ j ) = 1 N 1 N 2 ∑ i N 1 ∑ j N 2 L i ( p , θ i , ϕ j ) c o s θ i s i n θ i 1 2 π 1 0.5 π = π 2 N 1 N 2 ∑ i N 1 ∑ j N 2 L i ( p , θ i , ϕ j ) c o s θ i s i n θ i \begin{aligned} \int_{\Omega}{Li(p,w_i)(n\cdot w_i)dw_i} = & \int_{0}^{2\pi} \int_{0}^{\frac{\pi}{2}} L_i(p,w_i)cos\theta sin\theta d\theta d\phi \\ = & \frac{1}{N_1N_2} \sum_{i}^{N_1} \sum_{j}^{N_2} \frac{L_i(p,\theta_i,\phi_j) cos\theta_i sin\theta _i}{p(\theta_i,\phi_j)} \\ = & \frac{1}{N_1N_2} \sum_{i}^{N_1} \sum_{j}^{N_2} \frac{L_i(p,\theta_i,\phi_j) cos\theta_i sin\theta _i}{ \frac{1}{2\pi} \frac{1}{0.5\pi}} \\ = & \frac{\pi ^2}{N_1N_2} \sum_{i}^{N_1} \sum_{j}^{N_2} {L_i(p,\theta_i,\phi_j)cos\theta_i sin\theta _i}\\ \end{aligned} ∫ΩLi(p,wi)(n⋅wi)dwi====∫02π∫02πLi(p,wi)cosθsinθdθdϕN1N21i∑N1j∑N2p(θi,ϕj)Li(p,θi,ϕj)cosθisinθiN1N21i∑N1j∑N22π10.5π1Li(p,θi,ϕj)cosθisinθiN1N2π2i∑N1j∑N2Li(p,θi,ϕj)cosθisinθi
代码如下:
// NumSamplesPerDir 每个方向采样的数量
// sampleDelta 采样的角度间隔
float sampleDelta = 1.0 / NumSamplesPerDir;
float NumSamples = 0.0;
for (float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
// spherical to cartesian (in tangent space)
float sintheta = sin(theta);
float costheta = cos(theta);
float3 tangentSample = float3(sintheta * cos(phi), sintheta * sin(phi), costheta);
// tangent space to world
float3 sampleVec = TangentToWorld(tangentSample, Normal);
// 采样颜色
float3 sampleColor = CubeEnvironment.SampleLevel(LinearSampler, sampleVec, 0).rgb;
// 累加Radiance
Irradiance += sampleColor * costheta * sintheta;
// 采样数量++
NumSamples++;
}
}
// 除以采样数量
Irradiance = PI*Irradiance / NumSamples;
5.1.2 余弦权重的半球采样
将该概率密度函数应用到Irradiancemap求解的积分项中则有:
∫ Ω L i ( p , w i ) ( n ⋅ w i ) d w i = ∫ 0 2 π ∫ 0 π 2 L i ( p , w i ) c o s θ s i n θ d θ d ϕ = 1 N ∑ i N L i ( p , θ , ϕ ) c o s θ s i n θ p ( θ , ϕ ) = 1 N ∑ i N L i ( p , θ , ϕ ) c o s θ s i n θ 1 π c o s θ s i n θ = π N ∑ i N L i ( p , ω i ) \begin{aligned} \int_{\Omega}{Li(p,w_i)(n\cdot w_i)dw_i} = & \int_{0}^{2\pi} \int_{0}^{\frac{\pi}{2}} L_i(p,w_i)cos\theta sin\theta d\theta d\phi \\ = & \frac{1}{N} \sum_{i}^{N} \frac{L_i(p,\theta,\phi) cos\theta sin\theta }{p(\theta,\phi)} \\ = & \frac{1}{N} \sum_{i}^{N} \frac{L_i(p,\theta,\phi) cos\theta sin\theta }{\frac{1}{\pi}cos\theta sin\theta } \\ = & \frac{\pi }{N} \sum_{i}^{N} {L_i(p,\omega_i)} \\ \end{aligned} ∫ΩLi(p,wi)(n⋅wi)dwi====∫02π∫02πLi(p,wi)cosθsinθdθdϕN1i∑Np(θ,ϕ)Li(p,θ,ϕ)cosθsinθN1i∑Nπ1cosθsinθLi(p,θ,ϕ)cosθsinθNπi∑NLi(p,ωi)
代码如下:
float3 Irradiance = 0;
// 一个采样数量值
const uint NumSamples = 400;
for (uint i = 0; i < NumSamples; i++)
{
float2 E = Hammersley(i, NumSamples, Random);
float3 L = TangentToWorld(CosineSampleHemisphere(E).xyz, N);
float NoL = saturate(dot(N, L));
float Factor = NoL > 0 ? 1.0 : 0.0;
Irradiance += CubeEnvironment.SampleLevel(LinearSampler, L, 0).rgb * Factor;
}
Irradiance /= NumSamples;
return float4(Irradiance, 1.0);
至此,我们完成了Irradiancemap的预计算,从上述提供的代码当中可以看出,我们将系数 π 2 \pi^2 π2或 π \pi π都不包含在内,后续在使用的过程要注意这个问题。
5.1.3 One More Thing
在实践过程中发现,如果采用过程中不配合LOD进行的话,会出现噪点。例如对于下图,仅采样LOD0生成IrradianceMap。
而采用了LOD则有更好的效果,效果如下:
结合LOD采样的实现代码如下所示:
float sampleDelta = 1.0 / NumSamplesPerDir;
// 计算LOD级别
uint2 Dimension;
CubeEnvironment.GetDimensions(Dimension.x, Dimension.y);
float lod = max(log2(Dimension.x / float(NumSamplesPerDir)) + 1.0, 0.0);
float NumSamples = 0.0;
for (float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
// spherical to cartesian (in tangent space)
float sintheta = sin(theta);
float costheta = cos(theta);
float3 tangentSample = float3(sintheta * cos(phi), sintheta * sin(phi), costheta);
// tangent space to world
float3 sampleVec = TangentToWorld(tangentSample, Normal);
// 采样LOD级别的环境贴图
float3 sampleColor = CubeEnvironment.SampleLevel(LinearSampler, sampleVec, lod).rgb;
Irradiance += sampleColor * costheta * sintheta;
NumSamples++;
}
}
Irradiance = PI*Irradiance / NumSamples;
5.2 镜面反射环境光
让我们来看一下反射方程的镜面反射部分。
L S p e c u l a r = ∫ Ω f s L i ( w i ) ( n ⋅ w i ) d w i f s = D ( h ) F ( w o , w h ) G ( w i , w o , w h ) 4 ( n ⋅ w i ) ( n ⋅ w o ) \begin{aligned} L_{Specular} = & \int_{\Omega}{f_s Li(w_i)(n\cdot w_i)dw_i} \\ f_s = & \frac{D(h)F(w_o,w_h)G(w_i,w_o,w_h)}{4(n \cdot w_i)(n \cdot w_o)} \\ \end{aligned} LSpecular=fs=∫ΩfsLi(wi)(n⋅wi)dwi4(n⋅wi)(n⋅wo)D(h)F(wo,wh)G(wi,wo,wh)
镜面反射部分,取决于 n n n、 w o w_o wo、还有 BRDF 中的粗糙度Roughness、 F 0 F_0 F0(金属度和albedo决定),总共是9个维度。
方向向量用球面坐标表示的话就是 2 个维度。
因此,不能仅存储一张CubeMap就解决问题。
考虑暴力地实时求解方法,采用重要性采样,将上述渲染方程转换为重要性采样之后的样式:
p d f = p ( w i ) = D ⋅ ( n ⋅ h ) 4 ( v ⋅ h ) pdf=p(w_i)=\frac{D \cdot (n \cdot h)}{4(v \cdot h)} pdf=p(wi)=4(v⋅h)D⋅(n⋅h)
L S p e c u l a r = ∫ Ω f s L i ( w i ) ( n ⋅ w i ) d w i ≈ 1 N ∑ i = 1 N f s L i ( n ⋅ w i ) p ( w i ) L_{Specular} = \int_{\Omega}{f_s Li(w_i)(n\cdot w_i)dw_i} \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f_s L_i (n\cdot w_i)}{p(w_i)} LSpecular=∫ΩfsLi(wi)(n⋅wi)dwi≈N1i=1∑Np(wi)fsLi(n⋅wi)
其中,N为采样的总样本数量。 p ( w i ) p(w_i) p(wi)为重要采样的pdf(概率密度函数)。
这种方法虽然可以求解出来,但为了快速计算,不能进行过多的采样。
采样的数量少带来的误差就比较大,因而导致效果较差。
为了加速对该公式的计算,UE4将上述的公式划分为两个不同的求和部分!而这两个求和部分能够分别通过预计算得到结果。
推导的公式如下:
f s = D ( h ) F ( w o , w h ) G ( w i , w o , w h ) 4 ( n ⋅ w i ) ( n ⋅ w o ) F = F 0 + ( 1 − F 0 ) ( 1 − ( w o ⋅ w h ) 5 L S p e c u l a r = ∫ Ω f s L i ( w i ) ( n ⋅ w i ) d w i = ∫ Ω L i ( w i ) d w i ∗ ∫ Ω f s ( n ⋅ w i ) d w i \begin{aligned} f_s = & \frac{D(h)F(w_o,w_h)G(w_i,w_o,w_h)}{4(n \cdot w_i)(n \cdot w_o)} \\ F = & F_0 + (1-F_0)(1-(w_o \cdot w_h)^5 \\ L_{Specular} = & \int_{\Omega}{f_s Li(w_i)(n\cdot w_i)dw_i} \\ = & \int_{\Omega}{Li(w_i)dw_i} \ast \int _{\Omega} {f_s(n \cdot w_i)dw_i}\\ \end{aligned} fs=F=LSpecular==4(n⋅wi)(n⋅wo)D(h)F(wo,wh)G(wi,wo,wh)F0+(1−F0)(1−(wo⋅wh)5∫ΩfsLi(wi)(n⋅wi)dwi∫ΩLi(wi)dwi∗∫Ωfs(n⋅wi)dwi
可以看出:
- 第一个积分项,仅与环境贴图的纹理颜色有关;被称为pre-filter environment map。
5.2.1 BRDF-LUT
首先,来看第二个积分项!
∫ Ω f ( p , w i , w o ) ( w i ⋅ n ) d w i \int_{\Omega} f(p,w_i,w_o)(w_i\cdot n)dw_i ∫Ωf(p,wi,wo)(wi⋅n)dwi
公式中 f f f 包含了 F 0 F_0 F0,将其进行分离:
f ( p , w i , w 0 ) = D G F 4 ( w i ⋅ n ) ( w i ⋅ n ) f(p,w_i,w_0)= \frac{DGF}{4(w_i\cdot n)(w_i\cdot n)} f(p,wi,w0)=4(wi⋅n)(wi⋅n)DGF
F = F 0 + ( 1 − F 0 ) ( 1 − v ⋅ h ) 5 F=F_0+(1-F_0)(1-v \cdot h)^5 F=F0+(1−F0)(1−v⋅h)5
公式就变成以下:
∫ ( F 0 + ( 1 − F 0 ) ( 1 − w o ⋅ h ) 5 ) D G 4 ( w i ⋅ n ) ( w i ⋅ n ) ( w i ⋅ n ) d w i = F 0 ∗ ∫ ( 1 − ( 1 − w o ⋅ h ) 5 ) D G 4 ( w o ⋅ n ) d w i + ∫ ( 1 − v ⋅ h ) 5 D G 4 ( w o ⋅ n ) d w i \begin{aligned} \int (F_0+(1-F_0)(1-w_o \cdot h)^5) \frac{DG}{4(w_i\cdot n)(w_i\cdot n)}(w_i\cdot n)dw_i \\ = F_0 \ast \int (1-(1-w_o\cdot h)^5) \frac{DG}{4(w_o\cdot n)}dw_i + \int (1-v\cdot h)^5 \frac{DG}{4(w_o\cdot n)}dw_i \end{aligned} ∫(F0+(1−F0)(1−wo⋅h)5)4(wi⋅n)(wi⋅n)DG(wi⋅n)dwi=F0∗∫(1−(1−wo⋅h)5)4(wo⋅n)DGdwi+∫(1−v⋅h)54(wo⋅n)DGdwi
可以将公式理解成为:
F 0 ∗ S c a l e + B i a s F_0 \ast Scale + Bias F0∗Scale+Bias
从而将Scale和Bias进行预计算存储。
可以看出这Scale、Bias两项,和 n n n、 w o w_o wo、粗糙度Roughness三个变量有关!
不过可以注意到在粗糙度相同的情况下,对于任意的法线 n n n,只要观察方向和法线之间的夹角相同,则Scale和Bias就保持不变。
如此一来,我们仅需要关心法线 n n n和观察法向 w o w_o wo的夹角即可,这样的好处是根据 w o w_o wo和法线的点积(余弦值)作为一个自变量,同时完全可以根据这个余弦值还原出观察方向 w o w_o wo,此时法线为固定值 ( 0 , 0 , 1 ) (0,0,1) (0,0,1)。则 w o w_o wo在XZ平面上变化:
通过上图的关系,可以求得 w 0 w_0 w0, x = 1 − c o s 2 θ x=\sqrt{1-cos^2\theta} x=1−cos2θ, z = c o s θ z=cos\theta z=cosθ,则 w 0 w_0 w0为 ( 1 − c o s 2 θ , 0 , c o s θ ) (1-cos^2\theta,0,cos\theta) (1−cos2θ,0,cosθ)。
通过上述这种方式就把三个自变量降维简化为两个了!即可以预计算一张二维纹理了!
- 例如水平方向 N d o t V NdotV NdotV,竖直方向为 R o u g h n e s s Roughness Roughness。
/**
* ┌---→ NdotV
* |
* ↓
* Roughness
*/
计算则采用蒙特卡洛重要性采样对积分进行近似求解,选取 p d f = D ( w i ⋅ n ) 4 ( w o ⋅ w h ) pdf = \frac{D(w_i\cdot n)}{4(w_o\cdot w_h)} pdf=4(wo⋅wh)D(wi⋅n)。有:
∫ ( F 0 + ( 1 − F 0 ) ( 1 − w o ⋅ h ) 5 ) D G 4 ( w i ⋅ n ) ( w i ⋅ n ) ( w i ⋅ n ) d w i = F 0 ∗ ∫ ( 1 − ( 1 − w o ⋅ h ) 5 ) D G 4 ( w o ⋅ n ) d w i + ∫ ( 1 − v ⋅ h ) 5 D G 4 ( w o ⋅ n ) d w i ≈ F 0 ∗ 1 N ∑ i = 1 N ( 1 − ( 1 − w o ⋅ h ) 5 ) G ( w o ⋅ h ) ( w i ⋅ n ) ( w o ⋅ n ) + 1 N ∑ i = 1 N ( 1 − w o ⋅ h ) 5 G ( w o ⋅ h ) ( w i ⋅ n ) ( w o ⋅ n ) \begin{aligned} & \int (F_0+(1-F_0)(1-w_o \cdot h)^5) \frac{DG}{4(w_i\cdot n)(w_i\cdot n)}(w_i\cdot n)dw_i \\ = & F_0 \ast \int (1-(1-w_o\cdot h)^5) \frac{DG}{4(w_o\cdot n)}dw_i + \int (1-v\cdot h)^5 \frac{DG}{4(w_o\cdot n)}dw_i \\ \approx & F_0 \ast \frac{1}{N} \sum_{i=1}^{N} (1-(1-w_o\cdot h)^5) \frac{G(w_o\cdot h)}{(w_{i}\cdot n)(w_o\cdot n) } + \frac{1}{N} \sum_{i=1}^{N} (1-w_o\cdot h)^5 \frac{G(w_o\cdot h)}{(w_{i}\cdot n)(w_o\cdot n) } \end{aligned} =≈∫(F0+(1−F0)(1−wo⋅h)5)4(wi⋅n)(wi⋅n)DG(wi⋅n)dwiF0∗∫(1−(1−wo⋅h)5)4(wo⋅n)DGdwi+∫(1−v⋅h)54(wo⋅n)DGdwiF0∗N1i=1∑N(1−(1−wo⋅h)5)(wi⋅n)(wo⋅n)G(wo⋅h)+N1i=1∑N(1−wo⋅h)5(wi⋅n)(wo⋅n)G(wo⋅h)
可以将除了 F 0 F_0 F0外的两项分别进行预计算存储。
代码如下:
const float PI = 3.14159265358979323846264338327950288;
float RadicalInverse_VdC(unsigned int bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10;
}
Vector2f Hammersley(unsigned int i, unsigned int N)
{
return Vector2f(float(i) / float(N), RadicalInverse_VdC(i));
}
Vector3f ImportanceSampleGGX(Vector2f Xi, float roughness, Vector3f N)
{
float a = roughness * roughness;
float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
// 这里采样的球是一个切线空间的球
// from spherical coordinates to cartesian coordinates
Vector3f H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// from tangent-space vector to world-space sample vector
Vector3f up = abs(N.z) < 0.999 ? Vector3f(0.0, 0.0, 1.0) : Vector3f(1.0, 0.0, 0.0);
Vector3f tangent = Cross(up, N).Normalize();
Vector3f bitangent = Cross(N, tangent);
Vector3f sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
return sampleVec.Normalize();
}
float GeometrySchlickGGX(float NdotV, float roughness)
{
float a = roughness;
float k = (a * a) / 2.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(float roughness, float NoV, float NoL)
{
float ggx2 = GeometrySchlickGGX(NoV, roughness);
float ggx1 = GeometrySchlickGGX(NoL, roughness);
return ggx1 * ggx2;
}
Vector2f IntegrateBRDF(float NdotV, float roughness, unsigned int samples = 128)
{
Vector2f Eval;
// View
Vector3f V;
V.x = sqrt(1 - NdotV * NdotV); // sin
V.y = 0;
V.z = NdotV; // cos
float A = 0;
float B = 0;
// Normal
Vector3f N = Vector3f(0, 0, 1);
for (unsigned int i = 0; i < samples; ++i)
{
// Hammersley产生均匀分布的2D随机采样
Vector2f Xi = Hammersley(i, samples);
// GGX重要性采样,采样得到的是半程向量H
Vector3f H = ImportanceSampleGGX(Xi, roughness, N);
// H确定了,可以计算出L
Vector3f L = Vector3f(2.0f * V.Dot(H) * H - V).Normalize();
float NoL = std::max(L.z, 0.0f);
float NoH = std::max(H.z, 0.0f);
float VoH = std::max(V.Dot(H), 0.0f);
float NoV = std::max(N.Dot(V), 0.0f);
// 上半球
if (NoL > 0.0)
{
float G = GeometrySmith(roughness, NoV, NoL);
float G_Vis = (G * VoH) / (NoH * NoV);
float Fc = pow(1.0 - VoH, 5.0);
A += (1.0 - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
Eval.x = A / (1.0f * samples);
Eval.y = B / (1.0f * samples);
return Eval;
}
void GenerateBRDFLut()
{
// 分辨率
int N = 128;
DirectX::ScratchImage BRDFLut;
BRDFLut.Initialize2D(DXGI_FORMAT_R32G32_FLOAT, N, N, 1, 1);
const DirectX::Image img = BRDFLut.GetImages()[0];
float* dst = (float*)(img.pixels);
size_t rowPitch = 0;
size_t slicePitch = 0;
DirectX::ComputePitch(img.format, img.width, img.height, rowPitch, slicePitch);
/**
* ┌---→ NdotV
* |
* ↓
* Roughness
*/
for (int rowIndex = 0; rowIndex < N; ++rowIndex)
{
for (int colInddex = 0; colInddex < N; ++colInddex)
{
float roughness = (rowIndex + 0.5f) * (1.0f / N);
float NoV = (colInddex + 0.5f) * (1.0f / N);
Vector2f eval = IntegrateBRDF(NoV, roughness);
// write pixel value
float* dst = (float*)(img.pixels + rowPitch * (rowIndex));
dst[colInddex * 2 + 0] = eval.x;
dst[colInddex * 2 + 1] = eval.y;
}
}
// 可压缩成为R16G16_FLOAT
// BRDFLut.dds
HRESULT hr = DirectX::SaveToDDSFile(BRDFLut.GetImages(), BRDFLut.GetImageCount(), BRDFLut.GetMetadata(), DirectX::DDS_FLAGS_NONE, L"dx12_brdflut.dds");
BRDFLut.Release();
}
而虚幻中的LUT图,宽高比并不为1,其分辨率为128x32,其对粗糙度的精度要求不如NdotV,如下所示:
5.2.2 Prefilter EnvironmentMap
再来看第一个积分项:
∫ Ω L i ( w i ) d w i \int_{\Omega}{Li(w_i)dw_i} ∫ΩLi(wi)dwi
这个积分项看似和漫反射环境光的积分类似,可以通过一张CubeMap纹理存储即可。
但其真实的物理含义却告诉你,不可以如此处理。因为在镜面反射中,光照是与视角有关的( V ⋅ R V \cdot R V⋅R),并非像漫反射那样结果都一致( n ⋅ l n\cdot l n⋅l)。
如果,对于每个视角方向 w o w_o wo和每一个法线方向 n n n都进行预计算,那么需要N张纹理,完全不能接受。
这里的优化方式是假设:
- 视角方向 w o w_o wo = 法线方向 n n n = 反射方向 R R R
采样 L i L_i Li仅需要一个 l l l方向。
采用中程向量的计算办法也可以很轻松的计算出入射光方向。计算方法如下:
l = 2 ∗ ( H ⋅ w o ) H − w o l = 2*(H \cdot w_o) H - w_o l=2∗(H⋅wo)H−wo
其中,这里的 H H H和 w o w_o wo都是单位向量。中程向量就是我们需要进行随机生成了。
伪代码如下:
vec3 R = n;
vec3 V = w_o;
for(int i = 0; i < N; i++)
{
vec3 randNum = randFunction();
vec3 H = importanceSamlpe(randNum,n);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
color += texture(cubeMap,L).xyz;
}
color /= N;
再考虑加权平均,对于不同角度的光线,贡献应该不同。
当NdotL较小时,期待其对最终的结果影响较小,因而赋予较小的权重!
- 入射光线方向越接近法线方向,那么他的光照贡献应该更大!
伪代码如下:
vec3 R = n;
vec3 V = w_o;
for(int i = 0; i < N; i++)
{
vec3 randNum = randFunction();
vec3 H = importanceSamlpe(randNum,n);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
// dot(n,L)权重
color += texture(cubeMap,L).xyz * dot(n,L);
weight += dot(n,L);
}
color /= weight;
同时,当粗糙度不同的时候,CubeMap上影响的着色点光照范围也不同,重要性采样还要加入粗糙度的影响。
最终的伪代码如下:
vec3 R = n;
vec3 V = w_o;
for(int i = 0; i < N; i++)
{
vec3 randNum = randFunction();
// 重要性采样加入粗糙度影响 Roughness!
vec3 H = importanceSamlpe(randNum,n,roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
// dot(n,L)权重
color += texture(cubeMap,L).xyz * dot(n,L);
weight += dot(n,L);
}
color /= weight;
上述的描述可知,需要给每层Mipmap一个粗糙度值。
在虚幻中是这么计算的:
// Mip,当前CubeMap的Mip等级
// CubemapMaxMip,最大的MipMap等级
// 通过以上公式计算得到一个粗糙度值
float ComputeReflectionCaptureRoughnessFromMip(float Mip, half CubemapMaxMip)
{
float LevelFrom1x1 = CubemapMaxMip - 1 - Mip;
return exp2((1.0 - LevelFrom1x1) / 1.2);
}
那么就有对应的公式根据粗糙度计算当前的Mipmap等级:
half ComputeReflectionCaptureMipFromRoughness(float Roughness, half CubemapMaxMip)
{
half LevelFrom1x1 = 1.0 - 1.2 * log2(max(Roughness, 0.001));
return CubemapMaxMip - 1 - LevelFrom1x1;
}
虚幻中预过滤环境贴图的着色器代码可参考:ReflectionEnvironmentShaders.usf
void FilterPS(
FScreenVertexOutput Input,
out float4 OutColor : SV_Target0
)
{
float2 ScaledUVs = Input.UV * 2 - 1;
const int SelectedCubeFace = CubeFace;
float3 CubeCoordinates = GetCubemapVector(ScaledUVs, SelectedCubeFace);
float3 N = normalize(CubeCoordinates);
float3x3 TangentToWorld = GetTangentBasis( N );
// 计算粗糙度
float Roughness = ComputeReflectionCaptureRoughnessFromMip( MipIndex, NumMips - 1 );
if( Roughness < 0.01 )
{
OutColor = SourceCubemapTexture.SampleLevel(SourceCubemapSampler, CubeCoordinates, 0 );
return;
}
uint CubeSize = 1 << ( NumMips - 1 );
const float SolidAngleTexel = 4*PI / ( 6 * CubeSize * CubeSize ) * 2;
//const uint NumSamples = 1024;
// 采样数量
const uint NumSamples = Roughness < 0.1 ? 32 : 64;
float4 FilteredColor = 0;
BRANCH
if( Roughness > 0.99 )
{
// Roughness=1, GGX is constant. Use cosine distribution instead
// 当粗糙度为1时,GGX为常数,使用余弦分布代替
LOOP
for( uint i = 0; i < NumSamples; i++ )
{
float2 E = Hammersley( i, NumSamples, 0 );
float3 L = CosineSampleHemisphere( E ).xyz;
float NoL = L.z;
float PDF = NoL / PI;
float SolidAngleSample = 1.0 / ( NumSamples * PDF );
float Mip = 0.5 * log2( SolidAngleSample / SolidAngleTexel );
L = mul( L, TangentToWorld );
FilteredColor += SourceCubemapTexture.SampleLevel(SourceCubemapSampler, L, Mip );
}
OutColor = FilteredColor / NumSamples;
}
else
{
float Weight = 0;
LOOP
for( uint i = 0; i < NumSamples; i++ )
{
float2 E = Hammersley( i, NumSamples, 0 );
// 6x6 Offset rows. Forms uniform star pattern
//uint2 Index = uint2( i % 6, i / 6 );
//float2 E = ( Index + 0.5 ) / 5.8;
//E.x = frac( E.x + (Index.y & 1) * (0.5 / 6.0) );
E.y *= 0.995;
// GGX重要性采样
float3 H = ImportanceSampleGGX( E, Pow4(Roughness) ).xyz;
float3 L = 2 * H.z * H - float3(0,0,1);
float NoL = L.z;
float NoH = H.z;
if( NoL > 0 )
{
//float TexelWeight = CubeTexelWeight( L );
//float SolidAngleTexel = SolidAngleAvgTexel * TexelWeight;
//float PDF = D_GGX( Pow4(Roughness), NoH ) * NoH / (4 * VoH);
// 概率密度函数
float PDF = D_GGX( Pow4(Roughness), NoH ) * 0.25;
// mipmap等级
float SolidAngleSample = 1.0 / ( NumSamples * PDF );
float Mip = 0.5 * log2( SolidAngleSample / SolidAngleTexel );
// 切线空间到世界空间
L = mul( L, TangentToWorld );
// 采样并乘以NoL权重
FilteredColor += SourceCubemapTexture.SampleLevel(SourceCubemapSampler, L, Mip ) * NoL;
Weight += NoL;
}
}
OutColor = FilteredColor / Weight;
}
}