1. 介绍
物体在不同光照下的表现不同,PRT
(Precomputed Radiance Transfer)是一 个计算物体在不同光照下表现的方法。光线在一个环境中,会经历反射,折射, 散射,甚至还会在物体的内部进行散射。为了模拟具有真实感的渲染结果,传统的 Path Tracing
方法需要考虑来自各个方向的光线、所有可能的传播形式,并且收敛速度极慢。PRT
使用一种预计算方法,该方法在离线渲染的 Path Tracing 工具链中预计算 lighting
以及 light transport
, 并将它们用球谐函数拟合后储存,这样就将时间开销转移到了离线中。最后通过使用这些预计算好的数据,我们可以轻松达到实时渲染严苛的时间要求,同时渲染结果可以呈现出全局光照的效果。
预计算部分是PRT
算法的核心,也是其局限性的根源。因为在预计算 light transfer
时包含了 visibility
以及cos
项,这代表着实时渲染使用的这些几何信息已经完全固定了下来。所以 PRT
方法存在的限制包括:
- 不能计算随机动态场景的全局光照
- 场景中物体不可变动。
以上实际上来自Games202作业2,下面给出我自己的技术理解
个人觉得,Light Map
、Irradiance Map
、Precomputed Radiance Transfer
应该是一脉相承的,都是使用预计算,来解决渲染质量和性能的权衡问题:
-
Light Map
。此技术实际上只是把常数项( f d f_d fd)提取出来,然后没有采用split sum
,而是基于实际的对象,没有做任何近似,直接使用路径追踪等复杂离线方法来计算。此技术不仅绑定了光源(静态)、还绑定了对象(静态)。一般引擎中的烘焙过程就是使用了此技术。此技术的限制极大,物体和光源必须都是静态的,一旦变换其一,就需要重新烘焙,来计算Light Map
。实际渲染过程中,由于引擎都是以低分辨率计算Light Map
,然后将多个物体、多个表面的Light Map
存在一张纹理上,所以需要使用特定的LightUV
来读取Light Map
,然后再乘上 f d f_d fd(或albedo
)。
L p r e = ∫ L ⋅ V ⋅ d o t ( n ⋅ l ) d w L_{pre}=\int {L\cdot V\cdot dot(n\cdot l)} dw Lpre=∫L⋅V⋅dot(n⋅l)dw
-
Irradiance Map
。此技术详情见我的另外一篇博客。这里简单讲下:为了避免Light Map
过于严苛的限制,让运动物体可以收益于预计算。Irradiance Map
提出了一个重大假设:那就是环境光源无限远,物体上的每一个点,相对于环境光,都是一致的,并且忽略掉自反射和自阴影。这样的话,我们就只绑定了光源,物体在此光源的影响范围内,可以任意的旋转和移动。在预计算公式上,其实也和Light Map
没有区别(去掉了V
),只是考虑原理不一样了。实际渲染过程中,以法线作为索引,来读取这张环境贴图,然后乘上 f d f_d fd 即可。
L p r e = ∫ L ⋅ d o t ( n ⋅ l ) d w L_{pre}=\int {L\cdot dot(n\cdot l)} dw Lpre=∫L⋅dot(n⋅l)dw -
Efficient Irradiance Map
。此技术详情见我的另外一篇博客。个人目前对于RTR4
以及原论文中的关于此技术起源的解释,还是不能理解。所以这里给出我的解释:Irradiance Map
低频的特性(相当于低通滤波器),意味着它的入射光照分布可以用光滑曲线去近似,而依然可以取得不错的渲染结果。那么直接给出答案吧(以下都以三阶SH举例数字):我们可以将光照分布投影到少数几个(27个float
)SH系数上,实际使用时重建,这样可以极大压缩内存需求。而这个时候,为了投影到SH上,我们需要使用把原本的积分方程拆成两部分:光照部分L
和余弦部分cos
,分别对其进行SH投影。这里产生了一个问题:法线n
和 L ( w ) L(w) L(w)不同,它独立于w
,相当于第二个参数,我们需要对每个可能的方向(总共 w i d t h ∗ h e i g h t width* height width∗height 个方向)做SH投影,那么就会产生 27 ∗ w i d t h ∗ h e i g h t 27 * width* height 27∗width∗height个SH系数,这相当于好几张纹理了,这根本不省内存!幸运的是,受益于SH旋转不变的特性,可以简化为基于法线的缩放系数 A l A_l Al,来可以克服这个问题,最终的渲染过程也正如我们的期待:计算得到最终的法线 N ( θ , ϕ ) N(\theta,\phi) N(θ,ϕ)后,根据法线的球坐标,得到各个球谐基 Y l . m ( θ , ϕ ) Y_{l.m}(\theta,\phi) Yl.m(θ,ϕ)的值,然后代入重建公式:
E ( θ , ϕ ) = ∑ l , m A l L l m Y l m ( θ , ϕ ) E(\theta,\phi)=\sum_{l,m}{A_l}L_{lm}Y_{lm}(\theta,\phi) E(θ,ϕ)=l,m∑AlLlmYlm(θ,ϕ) -
Precomputed Radiance Transfer
。PRT
又是另外一个技术路线了,它不关注 ∫ ( L ) d w \int (L) dw ∫(L)dw部分(可以直接按照Efficient Irradiance Map
进行SH投影),而是重新捡起了 V V V项,并且还考虑了自反射,也就是说,我们在这关注的是 ∫ ( V ⋅ c o s ) d w \int(V\cdot cos)dw ∫(V⋅cos)dw部分,这部分也被称为传输项。这里只是起个头,具体的还是看后文。
2. Diffuse Transfer
正如前文所说,PRT
技术的核心在于怎么求解转移方程,来解释物体如何对自身投射阴影和散射光线。而对于Diffuse
物体来说,应用PRT
技术是简单的。
对于光照 L p ( s ) L_p(s) Lp(s)(整个半球域的光照),我们直接进行SH投影,得到 n 2 n^2 n2 个SH参数 ( L p ) i (L_p)_i (Lp)i 。然后对应的,我们对传输项进行SH投影,也得到 n 2 n^2 n2 个SH参数 ( M p ) i (M_p)_i (Mp)i 。
正如下文所示,随着考虑的要素变多,传输项变得复杂
SH基的正交性提供了一个有用的性质:即给定任意两个球上的函数
a
a
a 和
b
b
b ,它们的投影满足
∫
a
(
s
)
b
(
s
)
d
s
=
∑
i
=
1
n
2
a
i
b
i
\int{a(s)b(s)ds} = \sum_{i=1}^{n^2}{a_ib_i}
∫a(s)b(s)ds=∑i=1n2aibi。换句话说,对带限函数的乘积进行积分,可将其简化为投影系数的点积和。所以:
L
o
=
ρ
π
∫
(
L
p
(
s
)
⋅
M
p
(
s
)
)
d
s
=
ρ
π
∑
i
=
1
n
2
(
M
p
)
i
(
L
p
)
i
L_o=\frac{\rho}{\pi}\int{(L_{p}(s)\cdot M_{p}(s))}ds=\frac{\rho}{\pi}\sum_{i=1}^{n^2}(M_p)_i(L_p)_i
Lo=πρ∫(Lp(s)⋅Mp(s))ds=πρi=1∑n2(Mp)i(Lp)i
怎么求SH参数?这里简单提个醒:
f l m = ∫ f ( s ) y l m ( s ) d s = ∑ i = 1 i = n f ( s i ) y l m ( s i ) Δ s i f_l^m=\int{f(s)y_l^m(s)ds}=\sum_{i=1}^{i=n}f(s_i)y_l^m(s_i)\Delta s_i flm=∫f(s)ylm(s)ds=i=1∑i=nf(si)ylm(si)Δsi
知道high level
的想法后,现在留下的问题就是:传输项是什么?通过考虑阴影、自反射等因素,我们会有如下三个可选的传输项。
2.1 unshadowed diffuse transfer
这是最简单的传输方程,我们不要考虑阴影和自反射。我们假设
p
p
p 处的法线为
N
p
N_p
Np,那么传输项实际就是钳制的余弦项:
M
N
p
(
s
)
=
max
(
N
p
⋅
s
,
0
)
M_{N_p}(s)=\max{(N_p \cdot s, 0)}
MNp(s)=max(Np⋅s,0)
下面是作业中的代码:
for (int i = 0; i < mesh->getVertexCount(); i++)
{
const Point3f &v = mesh->getVertexPositions().col(i);
const Normal3f &n = mesh->getVertexNormals().col(i);
auto shFunc = [&](double phi, double theta) -> double {
Eigen::Array3d d = sh::ToVector(phi, theta);
const auto wi = Vector3f(d.x(), d.y(), d.z());
double cos_ = wi.dot(n);
//double cos_ = 0.0;
return max(cos_, 0);
};
// SHOrder : 使用的SH阶数
// shFunc : 要投影的函数,这里是转移函数
// m_SampleCount : 采样点数
auto shCoeff = sh::ProjectFunction(SHOrder, shFunc, m_SampleCount);
for (int j = 0; j < shCoeff->size(); j++)
{
m_TransportSHCoeffs.col(i).coeffRef(j) = (*shCoeff)[j];
}
}
2.2 shadowed diffuse transfer
这里我们就要考虑阴影了,其实就是在余弦项的基础上加上可见性
V
V
V。也就是说,传输项变成了:
M
N
p
+
S
h
a
d
o
w
(
s
)
=
max
(
N
p
⋅
s
,
0
)
∗
V
p
(
s
)
M_{N_p+Shadow}(s)=\max{(N_p \cdot s, 0)} *V_p(s)
MNp+Shadow(s)=max(Np⋅s,0)∗Vp(s)
可见项怎么计算?实际上,一个很简单的思路就是:在当前渲染点的法线半球,进行采样得到方向
s
s
s 的同时,沿着这个方向
s
s
s,进行击中判定。如果击中了,则
V
=
0
V=0
V=0;否则
V
=
1
V=1
V=1。
下面是作业中的代码:
for (int i = 0; i < mesh->getVertexCount(); i++)
{
const Point3f &v = mesh->getVertexPositions().col(i);
const Normal3f &n = mesh->getVertexNormals().col(i);
auto shFunc = [&](double phi, double theta) -> double {
Eigen::Array3d d = sh::ToVector(phi, theta);
const auto wi = Vector3f(d.x(), d.y(), d.z());
double cos_ = wi.dot(n);
//double cos_ = 0.0;
Ray3f r(v, wi);
return max(cos_, 0) * (1.0 - scene->rayIntersect(r));
};
// SHOrder : 使用的SH阶数
// shFunc : 要投影的函数,这里是转移函数
// m_SampleCount : 采样点数
auto shCoeff = sh::ProjectFunction(SHOrder, shFunc, m_SampleCount);
for (int j = 0; j < shCoeff->size(); j++)
{
m_TransportSHCoeffs.col(i).coeffRef(j) = (*shCoeff)[j];
}
}
2.3 interreflected diffuse transfer
这里进一步考虑了自反射,在考虑传输项之前,我们还需要重写光照方程:
L
o
=
ρ
p
π
∫
(
L
p
(
s
)
⋅
M
N
p
+
S
h
a
d
o
w
(
s
)
)
d
s
L
o
1
=
L
o
+
ρ
p
π
∫
L
p
0
‾
(
s
)
M
N
p
(
1
−
V
p
(
s
)
)
d
s
L
o
2
=
L
o
1
+
ρ
p
π
∫
L
p
1
‾
(
s
)
M
N
p
(
1
−
V
p
(
s
)
)
d
s
⋯
L_o=\frac{\rho_p}{\pi}\int{(L_{p}(s)\cdot M_{N_p+Shadow}(s))}ds \\ L_o^1=L_o+\frac{\rho_p}{\pi}\int{\overline{L_p^0}(s)M_{N_p}(1-V_p(s))ds} \\ L_o^2=L_o^1+\frac{\rho_p}{\pi}\int{\overline{L_p^1}(s)M_{N_p}(1-V_p(s))ds} \\ \cdots
Lo=πρp∫(Lp(s)⋅MNp+Shadow(s))dsLo1=Lo+πρp∫Lp0(s)MNp(1−Vp(s))dsLo2=Lo1+πρp∫Lp1(s)MNp(1−Vp(s))ds⋯
实际上,上诉过程就是物体本身多次互bounce
的过程,我们迭代计算多少次,就相当于bounce
越多。其中,
L
‾
p
(
s
)
\overline{L}_p(s)
Lp(s)是来自物体
O
O
O 自身的、方向
s
s
s 的辐射度(radiance
)。困难在于,除非入射光来自无限远的光源,否则我们实际上不知道
L
‾
p
(
s
)
\overline{L}_p(s)
Lp(s)。
但真的困难吗?
L
o
L_o
Lo可以直接计算(虽然这里说是直接,但请注意,我们通篇都是讲的GI
,算的是间接光照),而自反射导致的迭代依赖积分似乎很复杂?我们如果把这个过程拆成多个Pass
,迭代计算自反射不就成了,反正我们不需要过多考虑性能——这些都是离线计算的。但我们必须注意到一个问题:不管是公式上直接看,还是理论理解,我们在计算传输方程的时候,都需要考虑 BRDF了(对于diffuse
,也就是
ρ
π
\frac{\rho}{\pi}
πρ)!
其实之前的计算也可以考虑 ρ π \frac{\rho}{\pi} πρ,反正是个常数。但考虑自反射,情况就发生了变化,因为我们的转移函数需要考虑来自物体其他点的辐射度了!此时,考虑 ρ π \frac{\rho}{\pi} πρ 是必不可少的!
M
N
p
(
s
)
=
ρ
p
π
max
(
N
p
⋅
s
,
0
)
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
M
N
p
+
S
h
a
d
o
w
(
s
)
=
ρ
p
π
max
(
N
p
⋅
s
,
0
)
∗
V
p
(
s
)
M_{N_p}(s)=\frac{\rho_p}{\pi}\max{(N_p \cdot s, 0)} \\ \\ ------------------- \\ M_{N_p+Shadow}(s)=\frac{\rho_p}{\pi}\max{(N_p \cdot s, 0)} *V_p(s)
MNp(s)=πρpmax(Np⋅s,0)−−−−−−−−−−−−−−−−−−−MNp+Shadow(s)=πρpmax(Np⋅s,0)∗Vp(s)
以上就是自反射情况下,对之前两个传输方程的修正。那么,让我们正式开始描述把!
Pass 1
。按照shadowed diffuse transfer
描述的那样,计算得到模型上每一个点的传输参数—— ( M p ) i 0 (M_p)_i^0 (Mp)i0 。Pass 2
。也是和Pass 1
一样,遍历模型的每一个顶点。以 p p p 点为例,在其法线半球域,进行采样。对于采样得到的方向 s d s_d sd,我们只考虑和物体本身相交的,也就是说,传输方程参数的计算如下:
( M p ) i 1 = ∑ s d ( 1 − V p ( s d ) ) ⋅ ( M q ) i 0 ⋅ M N p ( s d ) (M_p)^1_i = \sum_{s_d}(1-V_p(s_d))\cdot (M_q)_i^0\cdot M_{N_p}(s_d) (Mp)i1=sd∑(1−Vp(sd))⋅(Mq)i0⋅MNp(sd)
其中, ( M p ) i 1 (M_p)^1_i (Mp)i1 的下标代表球谐参数的索引(第几个SH参数),其上标代表迭代次数。 q q q 代表物体上和 s d s_d sd相交的点。上述方程比较好理解: 1 − V p ( s d ) 1-V_p(s_d) 1−Vp(sd) 是由于我们只考虑和物体相交的方向; ( M q ) i 0 (M_q)_i^0 (Mq)i0则是需要考虑 q q q 点本身的光线传输情况。
( M q ) i 0 (M_q)_i^0 (Mq)i0 实际上并不是 q q q 的传输参数,或者说, q q q 点身可能并不是模型的一个顶点,而是相交的三角形图元上的一个点。所以 ( M q ) i 0 (M_q)_i^0 (Mq)i0 实际上是这个相交图元的三个顶点的传输参数的插值。
Games202
里面是说的是重心坐标插值。
Pass 3
,就是迭代,直接贴公式了:
( M p ) i 2 = ∑ s d ρ p π ⋅ ( 1 − V p ( s d ) ) ⋅ ( M q ) i 1 ⋅ M N p ( s d ) (M_p)^2_i = \sum_{s_d}\frac{\rho_p}{\pi}\cdot (1-V_p(s_d))\cdot (M_q)_i^1\cdot M_{N_p}(s_d) (Mp)i2=sd∑πρp⋅(1−Vp(sd))⋅(Mq)i1⋅MNp(sd)- 可能继续的
Pass4
、Pass 5
。只要你不嫌麻烦。 Pass final
,组合直接传输和多次传输(再说一遍:我们通篇都是讲的GI
,算的是间接光照):
( M p ) i = ( M p ) i 0 + ( M p ) i 1 + ( M p ) i 2 + ⋯ + ( M p ) i n (M_p)_i=(M_p)_i^0+(M_p)_i^1+(M_p)_i^2+\cdots+(M_p)_i^n (Mp)i=(Mp)i0+(Mp)i1+(Mp)i2+⋯+(Mp)in
这里就暂时不贴代码了,嘛哈哈哈,当时太菜了,写错了。
2.4 实时计算
这个就很简单了,以三阶SH投影为例:
light
。对于光照的投影(例如:来自环境贴图),我们得到 9 R + 9 G + 9 B = 27 9_R+9_G+9_B=27 9R+9G+9B=27 个SH参数,然后实际上可以以uniform mat3
传入顶点着色器。由于是27
个参数,我们应该传入 M a t R , M a t G , M a t B Mat_R,Mat_G,Mat_B MatR,MatG,MatB 三个mat3
矩阵。Transform
。对于转移函数的投影,则是有 N u m b e r 顶 点 数 ∗ 9 Number_{顶点数} * 9 Number顶点数∗9 个SH参数。所以我们应该把这个转移SH参数,设置成顶点数据结构体的成员,类似于顶点法线、顶点位置等。只不过,这个成员是mat3
类型。
2.5 旋转光源
如果我们想要旋转光源,那就破坏了预计算系方法的静态光源要求,除非我们重新预计算旋转后光源的分布情况。但更好的方法还是在SH系数上花功夫。本节介绍这个方法,它主要利用了球谐函数的两个性质:
-
球谐函数具有旋转不变性,通俗的讲就是:假设有一个旋转 R R R,对原函数 f ( x ) f(x) f(x) 的旋转 R ( f ( x ) ) R(f(x)) R(f(x)) 与直接旋转 f ( x ) f(x) f(x) 的自变量是一样的,即 R 1 ( f ( x ) ) = f ( R 2 ( x ) ) R_1(f(x))=f(R_2(x)) R1(f(x))=f(R2(x))。
-
对每层
band
上的SH coefficient
,可以分别在上面进行旋转,并且这个旋转是线性变化的。 也就是说,SH系数可以看成是向量,并且可以拆分。
⭐️ 我们对环境贴图(举个例)进行SH投影,得到9
个SH系数,可以把这个看出一个
v
e
c
9
vec9
vec9向量,旋转环境贴图实际上就是旋转这个vec9
向量:
L
i
g
h
t
_
S
H
_
v
e
c
9
=
[
L
0
,
0
,
⋯
,
L
2
,
2
]
=
[
∑
L
n
p
⋅
Y
0
,
0
(
n
p
)
⋅
Δ
A
r
e
a
,
⋯
,
∑
L
n
p
⋅
Y
2
,
2
(
n
p
)
⋅
Δ
A
r
e
a
]
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
−
R
(
L
i
g
h
t
_
S
H
_
v
e
c
9
)
=
R
(
[
L
0
,
0
,
⋯
,
L
2
,
2
]
)
=
R
(
∑
L
n
p
⋅
Δ
A
r
e
a
⋅
[
Y
0
,
0
(
n
p
)
,
⋯
,
Y
2
,
2
(
n
p
)
]
)
=
∑
L
R
−
(
n
p
)
⋅
Δ
A
r
e
a
⋅
R
(
[
Y
0
,
0
(
n
p
)
,
⋯
,
Y
2
,
2
(
n
p
)
]
)
Light\_SH\_vec9=[L_{0,0},\cdots,L_{2,2}]=[\sum{L_{n_p}\cdot Y_{0,0}(n_p)\cdot \Delta{Area}},\cdots,\sum{L_{n_p}\cdot Y_{2,2}(n_p)\cdot \Delta{Area}}] \\----------------------- \\ R(Light\_SH\_vec9)=R( [L_{0,0},\cdots,L_{2,2}])=R(\sum{L_{n_p}\cdot \Delta{Area}}\cdot [Y_{0,0}(n_p),\cdots, Y_{2,2}(n_p)]) \\ =\sum{L_{R^-(n_p)}\cdot \Delta{Area}} \cdot R([Y_{0,0}(n_p),\cdots, Y_{2,2}(n_p)])
Light_SH_vec9=[L0,0,⋯,L2,2]=[∑Lnp⋅Y0,0(np)⋅ΔArea,⋯,∑Lnp⋅Y2,2(np)⋅ΔArea]−−−−−−−−−−−−−−−−−−−−−−−R(Light_SH_vec9)=R([L0,0,⋯,L2,2])=R(∑Lnp⋅ΔArea⋅[Y0,0(np),⋯,Y2,2(np)])=∑LR−(np)⋅ΔArea⋅R([Y0,0(np),⋯,Y2,2(np)])
通过如上公式推导,我们可以知道:
R
(
L
i
g
h
t
_
S
H
_
v
e
c
9
)
R(Light\_SH\_vec9)
R(Light_SH_vec9)的求解可以看作
R
(
[
Y
0
,
0
(
n
p
)
,
⋯
,
Y
2
,
2
(
n
p
)
]
)
R([Y_{0,0}(n_p),\cdots, Y_{2,2}(n_p)])
R([Y0,0(np),⋯,Y2,2(np)])的求解(至于
R
+
(
L
n
p
)
=
L
R
−
(
n
p
)
R^+(L_{n_p})=L_{R^-(n_p)}
R+(Lnp)=LR−(np),就是换个采样的反射度值,是一个未知且无关的变换)。而
R
(
⋅
)
R(\cdot)
R(⋅)实际上就是一个
m
a
t
9
mat9
mat9矩阵(拿来旋转
v
e
c
9
vec9
vec9 啊!)。此外,
[
Y
0
,
0
(
n
p
)
,
⋯
,
Y
2
,
2
(
n
p
)
]
[Y_{0,0}(n_p),\cdots, Y_{2,2}(n_p)]
[Y0,0(np),⋯,Y2,2(np)] 实际上就是法线
n
p
n_p
np的SH投影,或者说
P
(
n
p
)
=
[
Y
0
,
0
(
n
p
)
,
⋯
,
Y
2
,
2
(
n
p
)
]
P(n_p)=[Y_{0,0}(n_p),\cdots, Y_{2,2}(n_p)]
P(np)=[Y0,0(np),⋯,Y2,2(np)]。最终,我们需要考虑的就是
R
(
P
(
n
p
)
)
R(P(n_p))
R(P(np))的求解!
R
1
(
P
(
n
p
)
)
=
P
(
R
2
(
n
p
)
)
R_1(P(n_p))=P(R_2(n_p))
R1(P(np))=P(R2(np))
1️⃣首先,以三阶SH为例,来确定符号。根据上面的分析,投影函数 P ( n i ) P(n_i) P(ni) 是一个类球谐函数(多个球谐函数组成的向量,从下面的代码也可以看出来),返回SH系数向量,返回类型暂定为 v e c 9 vec9 vec9。它的伪代码大致如下:
/*float getSH(int l, int m)
{
float sh;
for(int i = 0; i < width; ++i)
{
for(int j = 0; j < height; ++j)
{
vec3 n =GetNormal(i, j)
sh += envMap(i, j) * Y(l, m, n) * getArea(i, j);
}
}
return sh;
}*/
vec9 P(vec3 n)
{
vec9 sh;
for(int l = 0; l < 3; ++l)
{
for(int m = -l; m <= l; ++m )
{
sh[getID(l, m)] = Y(l, m, n);
}
}
}
所以
P
(
n
i
)
P(n_i)
P(ni)具备旋转不变性,所以有如下关系:
M
m
a
t
9
⋅
P
(
n
i
)
=
P
(
R
(
n
i
)
)
M_{mat9} \cdot P(n_i) = P(R(n_i))
Mmat9⋅P(ni)=P(R(ni))
我们知道
P
(
n
i
)
P(n_i)
P(ni) 以及
P
(
R
(
n
i
)
)
P(R(n_i))
P(R(ni))的值(后者就是对法线进行旋转),似乎不好求解得到
M
m
a
t
9
M_{mat9}
Mmat9。但如果我们采样9
个法线,并把他们组合成矩阵
A
=
[
P
(
n
0
)
,
P
(
n
1
)
,
⋯
,
P
(
n
9
)
]
A=[P(n_0),P(n_1),\cdots,P(n_9)]
A=[P(n0),P(n1),⋯,P(n9)],根据数字知识,此时满足:
M
m
a
t
9
⋅
A
=
[
P
(
R
(
n
0
)
)
,
P
(
R
(
n
1
)
)
,
⋯
,
P
(
R
(
n
9
)
)
]
M_{mat9} \cdot A = [P(R(n_0)),P(R(n_1)),\cdots, P(R(n_9))]
Mmat9⋅A=[P(R(n0)),P(R(n1)),⋯,P(R(n9))]
这个时候就好办了,A
是矩阵,我们就可以利用矩阵的逆,来进行操作了:
M
m
a
t
9
=
[
P
(
R
(
n
0
)
)
,
P
(
R
(
n
1
)
)
,
⋯
,
P
(
R
(
n
9
)
)
]
⋅
A
−
1
M_{mat9}= [P(R(n_0)),P(R(n_1)),\cdots, P(R(n_9))]\cdot A^{-1}
Mmat9=[P(R(n0)),P(R(n1)),⋯,P(R(n9))]⋅A−1
2️⃣接下来可以讨论具体算法的实现了:
- 首先,选取9个法线 [ n 0 , n 1 , ⋯ , n 9 ] [n_0,n_1,\cdots,n_9] [n0,n1,⋯,n9],对其中的法线 n i n_i ni,在球谐上投影,得到 P ( n i ) P(n_i) P(ni)。9`个 v e c 9 vec9 vec9 向量 P ( n i ) P(n_i) P(ni)构成了我们需要的矩阵 A A A,并求出 A − 1 A^{−1} A−1。
- 其次,给定旋转
R
R
R,对所有
n
i
n_i
ni 依次做旋转
R
(
n
i
)
R(n_i)
R(ni)、SH投影取系数
P
(
R
(
n
i
)
)
P(R(n_i))
P(R(ni))。 此时我们应该得到
9
个 v e c 9 vec9 vec9 向量 P ( R ( n i ) ) P(R(n_i)) P(R(ni)),这些向量构成了我们需要的矩阵 S S S。 - 接着,求出所需的矩阵
M
=
S
A
−
1
M = SA^{−1}
M=SA−1,用
M
M
M 乘以
Light SH coefficient vector
就可以得到旋转后的SH系数了。
3️⃣Games202
里面给出了优化思路:mat9
太大了,且计算耗时,而且0阶那个系数根本不需要考虑旋转。在此基础上,我们可以把剩余的8
个系数,按照所属阶拆成两部分:1阶的vec3
,2阶的vec5
。具体流程和上面的类似,可以看看如下截图:
4️⃣不管实现方法如何,我们最终可以发现,这个用来旋转光源SH Vector的函数(一个,或多个矩阵),实际上是独立于环境光的。也就是说,我们完全可以预计算几个常用的旋转角度,通过上诉方法求出旋转矩阵,然后实际运行的时候就可以直接用了。其次,这个旋转算法不是是适用于这个PRT
,它依赖的仅仅是球谐函数的特性,所以其他使用SH基进行压缩的GI
算法,也可以使用这个方法来旋转光源。
3. Specular Transfer
对于Specular
物体来说,应用PRT
技术则有点复杂。我目前也没有完全弄明白,这里先给出我们需要提前知道的设定:
- BRDF方程不依赖法线,而依赖于反射向量 R R R: f r = G ( s , R , r ) = B R D F ( s , R , r o u g h n e s s ) f_r=G(s,R,r)=BRDF(s,R,roughness) fr=G(s,R,r)=BRDF(s,R,roughness)。
- BRDF不是各向异性的,所以它的
lobe
是以 R R R为轴,圆对称的。
3.1 shadowed specular transfer
我们首先给出渲染方程:
L
G
S
(
L
p
,
R
,
r
)
=
∫
L
p
(
s
)
G
(
s
,
R
,
r
)
V
p
(
s
)
d
s
L_{GS}(L_p,R,r)=\int{L_p(s)G(s,R,r)V_p(s)ds}
LGS(Lp,R,r)=∫Lp(s)G(s,R,r)Vp(s)ds
如果依然按照之前的思路,会遇到困难:
G
G
G不仅是入射光方向
s
s
s的函数,不能被简化成SH系数向量。
作者的思路是:
- 首先,去除 R R R的影响,让: G r ∗ ( z ) = G ( s , ( 0 , 0 , 1 ) , r ) G_r^*(z)=G(s,(0,0,1),r) Gr∗(z)=G(s,(0,0,1),r)
- 然后,将积分看作是: G r ∗ G_r^* Gr∗和光照进行卷积,然后投影到SH基上,最后代入实际的 R R R,来获得 L G S ( L p , R , r ) L_{GS}(L_p,R,r) LGS(Lp,R,r)。
- 重建公式: L G S = ∑ α l 0 ⋅ G l 0 ⋅ L l m ⋅ Y l m ( R ) L_{GS}=\sum{\alpha_l^0 \cdot G_l^0 \cdot L^m_l \cdot Y_l^m(R)} LGS=∑αl0⋅Gl0⋅Llm⋅Ylm(R)
SH Convolution一个圆对称核函数 h ( z ) h(z) h(z),与函数 f f f 的卷积表示为: h ∗ f h*f h∗f。卷积的投影满足:
( h ∗ f ) l m = 4 π 2 l + 1 h l 0 f l m = a l 0 ⋅ h l 0 ⋅ f l m (h*f)^m_l=\sqrt{\frac{4\pi}{2l+1}}h^0_lf^m_l=a^0_l \cdot h^0_l \cdot f^m_l (h∗f)lm=2l+14πhl0flm=al0⋅hl0⋅flm
1️⃣综上所诉,我们首先可以把
G
(
s
,
(
0
,
0
,
1
)
,
r
)
G(s,(0,0,1),r)
G(s,(0,0,1),r) 投影到SH基上。方法如下:
G
l
m
=
∫
G
(
s
,
(
0
,
0
,
1
)
,
r
)
y
l
m
(
s
)
d
s
G_l^m=\int{G(s,(0,0,1),r)y_l^m(s)\mathrm{d}s}
Glm=∫G(s,(0,0,1),r)ylm(s)ds
2️⃣
ToDo