1️⃣首先,确认LTC
技术的目标:解决多边形面光源的光照积分问题,特别是高光部分的求解。多边形面光源的积分和我们之前考虑的问题都不一样:
SpotLight
等):简单,积分域就是一个点。IBL
技术类,进行简化。我们考虑更加物理的光源:多边形光源。这个时候,积分域 S S S 就很复杂了,求解更加复杂:
I = ∫ S ( L ( w l ) ⋅ f ( p , w i , w o ) ⋅ cos ( θ p ) ) d w i I=\int_S(L(w_l)\cdot f(p,w_i,w_o)\cdot \cos(\theta_p))dw_i I=∫S(L(wl)⋅f(p,wi,wo)⋅cos(θp))dwi
注意:我们考虑的是多边形光源,这意味着这个光源的所有顶点都是在一个平面上,不能有凹凸!
最朴素的思想就是:使用蒙特卡罗方法。但这个思路在性能上是无法接受的。
2️⃣作者就发现了一种线性变换球面分布(Linearly Transformed Spherical Distributions
)的思想。这种思想就是对于任意一个球面分布函数,一定可以通过一个线性变换矩阵将其变化到另外一个球面分布函数。这个时候希望来了,由于 c o s + ( θ i ) cos^+(\theta_i) cos+(θi)是一个球面分布函数(余弦分布函数), f ( p , w i , w o ) f(p,w_i,w_o) f(p,wi,wo) 也是一个球面分布函数。
那么具体这个算法到底是什么呢?
c o s + cos^+ cos+是clamp余弦
3️⃣根据论文,我们拿来进行简化计算的球面分布函数,实际上是 c o s + ( θ i ) ⋅ 1 π cos^+(\theta_i) \cdot \frac{1}{\pi} cos+(θi)⋅π1。而原来的复杂的球面分布函数,实际上是 f ( p , w i , w o ) ⋅ cos θ i f(p,w_i,w_o)\cdot \cos{\theta_i} f(p,wi,wo)⋅cosθi
1️⃣根据第一节,我们有两个认识:
LTCS
思想(数学思想,所以我不懂,只是给出结论):存在一个变换矩阵 M M M,可以把简单的球面分布函数 c o s + ( θ i ) ⋅ 1 π cos^+(\theta_i)\cdot \frac{1}{\pi} cos+(θi)⋅π1,变换成复杂的球面分布函数 f ( p , w i , w o ) ⋅ cos θ i f(p,w_i,w_o)\cdot \cos{\theta_i} f(p,wi,wo)⋅cosθi。*
不是普通的矩阵乘法,这里的线性变换是指把我们的入射向量乘以矩阵M。2️⃣但是,还是看不出来有什么用,所以我们需要知道LTCS
的另外一个性质:
∫ S D ( w ) d w = ∫ S t D t ( w t ) d w t \int_SD(w)dw=\int_{S_t}D_t(w_t)dw_t ∫SD(w)dw=∫StDt(wt)dwt
所以,我们会有:
∫ S f ( p , w i , w o ) ⋅ cos θ i d w i = ∫ S t c o s + ( θ i ) π d w t \int_S f(p,w_i,w_o)\cdot \cos{\theta_i} dw_i=\int_{S_t}\frac{cos^+(\theta_i)}{\pi} dw_t ∫Sf(p,wi,wo)⋅cosθidwi=∫Stπcos+(θi)dwt
而积分域: S = M ∗ S t S=M*S_t S=M∗St。积分域进行变换,实际上就是对这个多边形域的每个顶点,应用这个矩阵 M M M。
3️⃣对 c o s cos cos 进行积分是很简单的,哪怕积分域 S t S_t St 比原积分域 S S S 复杂很多,也不会有太大影响——实际上,积分域 S t S_t St 也不会比 S S S 复杂,毕竟,我们并没有增加删除顶点,而只是对顶点的位置进行变换(不考虑裁剪: S S S 是五边形, S t S_t St 也会是五边形)。而且似乎对 c o s cos cos 进行积分有近似方法,这个后续进行分析。
4️⃣所以我们的最终的目标就是:放弃在多边形积分域 S S S中,使用复杂的 D ( w ) = f ( p , w i , w o ) ⋅ cos θ i D(w)=f(p,w_i,w_o)\cdot \cos{\theta_i} D(w)=f(p,wi,wo)⋅cosθi进行光照积分,而是使用简单的 D t ( w t ) = c o s + ( θ i ) π D_t(w_t)=\frac{cos^+(\theta_i)}{\pi} Dt(wt)=πcos+(θi)进行积分计算。
I = ∫ S t ( L ( w t ) ⋅ D t ( w t ) ) d w t I=\int_{S_t}(L(w_t)\cdot D_t(w_t))dw_t I=∫St(L(wt)⋅Dt(wt))dwt
但目前还是有两个问题需要解决:
我们依次来解决!
1️⃣根据原论文,我们可以知道,变换前后的球面分布函数有如下关系:
D ( w ) = D t ( w t ) δ w t δ w = D o ( M − 1 w ∣ ∣ M − 1 w ∣ ∣ ) ∣ M − 1 ∣ ∣ ∣ M − 1 w ∣ ∣ 3 D(w)=D_t(w_t)\frac{\delta w_t}{\delta w}=D_o(\frac{M^{-1}w}{||M^{-1}w||})\frac{|M^{-1}|}{||M^{-1}w||^3} D(w)=Dt(wt)δwδwt=Do(∣∣M−1w∣∣M−1w)∣∣M−1w∣∣3∣M−1∣
其中, D ( w ) = f ( p , w , w o ) ⋅ cos θ i D(w)=f(p,w,w_o)\cdot \cos{\theta_i} D(w)=f(p,w,wo)⋅cosθi, D o ( w t ) = cos θ t π D_o(w_t)=\frac{\cos{\theta_t}}{\pi} Do(wt)=πcosθt, w t = M ∗ w w_t=M*w wt=M∗w。 M M M 就是我们需要的转换矩阵。根据原论文,这个矩阵 M M M 实际上有如下固定形式:
2️⃣所以,我们的问题从求得一个矩阵,变成了求得 ( a , b , c , d ) (a,b,c,d) (a,b,c,d)的值,那么,怎么求呢?
和IBL
方法类似,我们在BRDF
中不考虑菲涅尔项,而且假设入射方向等于法线方向,那么 f ( p , w i , w o ) ⋅ cos θ i f(p,w_i,w_o)\cdot \cos{\theta_i} f(p,wi,wo)⋅cosθi 项就只依赖于视线方向和粗糙度。那么我们就可以计算得到一个张类似于BRDF LUT
的二维纹理,其横纵坐标是视线法线夹角和粗糙度。
3️⃣咦?我们似乎还是不会求 M M M?根据大佬的博客:对于任意一个 f ( p , w i , w o ) ⋅ cos θ i f(p,w_i,w_o)\cdot \cos{\theta_i} f(p,wi,wo)⋅cosθi 一定找的到一个M变换矩阵把他变换到余弦分布,因此其实只需要遍历所有的矩阵M总能找到一个误差足够小的矩阵 M M M,不过穷举法不可能,我们采用一种单纯形法用于寻找 M M M。单纯形法的具体过程有点类似梯度下降的思路,我们随机初始化一个矩阵 M M M,然后经过计算,我们会得到一个“梯度”也就是我们该往哪个方向去修正我们的矩阵 M M M,循环往复,直到 M M M 的误差在接受范围内。
具体过程来说:
LUT
的每个坐标,也就是一对具体的视线法线夹角和粗糙度,我们可以随意取一个入射方向 w w w,求得 N 0 = f ( p , w , w o ) ⋅ cos θ i N_0=f(p,w,w_o) \cdot \cos{\theta_i} N0=f(p,w,wo)⋅cosθi。L3
范数,作为误差 e e e 。4️⃣我们进行预计算,最终得到一张横纵坐标是视线法线夹角和粗糙度,存储了转换矩阵的逆 M − 1 M^{-1} M−1的LUT
。
M矩阵预计算code
1️⃣最简单的情况就是:多边形光源的辐照度是恒定的,那么这个时候,入射光 L L L就可以直接提出来。
I = L ⋅ ∫ S t 1 ∣ ∣ s − p ∣ ∣ 2 ⋅ cos θ t π d w t I=L\cdot\int_{S_t}\frac{1}{||s-p||^2} \cdot \frac{\cos{\theta_t}}{\pi} dw_t I=L⋅∫St∣∣s−p∣∣21⋅πcosθtdwt
其中, s s s 是渲染点, p p p是多边形光源上的点(变化后的,位于 S t S_t St上)。而且,根据Daniel的方法,上诉积分具有解析解:
∫ S t 1 ∣ ∣ s − p ∣ ∣ 2 ⋅ cos θ t π d w t = 1 2 π ∑ j = 1 e n p ⋅ Γ j ∣ ∣ Γ j ∣ ∣ Υ j = I D − − − − − − − − − − − − − − − − − − − − Γ j = Y j × Y j + 1 \int_{S_t}\frac{1}{||s-p||^2} \cdot \frac{\cos{\theta_t}}{\pi} dw_t=\frac{1}{2\pi}\sum_{j=1}^{e}{n_p\cdot\frac{\Gamma_j}{||\Gamma_j||}}\Upsilon_j=I_D \\ --------------------\\ \Gamma_j=Y_j \times Y_{j+1} ∫St∣∣s−p∣∣21⋅πcosθtdwt=2π1j=1∑enp⋅∣∣Γj∣∣ΓjΥj=ID−−−−−−−−−−−−−−−−−−−−Γj=Yj×Yj+1
其中, e e e 是多边形光源的边数, n p n_p np 为渲染点 p p p 法线, Y j Y_j Yj 是着色点到多边形顶点的向量(这些顶点都是经过变换的,位于 S t S_t St), Υ j \Upsilon_j Υj为向量之间的夹角( Y j Y_j Yj 和 Y j + 1 Y_{j+1} Yj+1之间的夹角 )。
1 ∣ ∣ s − p ∣ ∣ 2 \frac{1}{||s-p||^2} ∣∣s−p∣∣21来自于光线衰减。
2️⃣所以,我们最终只需进行如下计算即可:
L ( p , w o ) = L ⋅ I D L(p,w_o)=L\cdot I_D L(p,wo)=L⋅ID
1️⃣纹理多边形光源的辐照度明显是不均匀的,所以我们不能直接将入射光提取出来,但我们可以利用下面这个常用技巧:
I = ∫ S ( L ( w l ) D ( w l ) ) d w l ≈ I D ⋅ I L − − − − − − − I D = ∫ S D ( w l ) d w l − − − − − − − I L = ∫ S ( L ( w l ) D ( w l ) ) d w l ∫ S D ( w l ) d w l I=\int_S{(L(w_l)D(w_l))}dw_l\approx I_D\cdot I_L \\ -------\\ I_D=\int_SD(w_l)dw_l \\ -------\\ I_L=\frac{\int_S{(L(w_l)D(w_l))}dw_l}{\int_SD(w_l)dw_l} I=∫S(L(wl)D(wl))dwl≈ID⋅IL−−−−−−−ID=∫SD(wl)dwl−−−−−−−IL=∫SD(wl)dwl∫S(L(wl)D(wl))dwl
我们把积分拆成两部分,第一部分 I D I_D ID很简单,就是我们所需的完美形式,可以直接使用Daniel的方法求得解析解:
∫ S t 1 ∣ ∣ s − p ∣ ∣ 2 ⋅ cos θ t π d w t = 1 2 π ∑ j = 1 e n p ⋅ Γ j ∣ ∣ Γ j ∣ ∣ Υ j = I D − − − − − − − − − − − − − − − − − − − − Γ j = Y j × Y j + 1 \int_{S_t}\frac{1}{||s-p||^2} \cdot \frac{\cos{\theta_t}}{\pi} dw_t=\frac{1}{2\pi}\sum_{j=1}^{e}{n_p\cdot\frac{\Gamma_j}{||\Gamma_j||}}\Upsilon_j=I_D \\ --------------------\\ \Gamma_j=Y_j \times Y_{j+1} ∫St∣∣s−p∣∣21⋅πcosθtdwt=2π1j=1∑enp⋅∣∣Γj∣∣ΓjΥj=ID−−−−−−−−−−−−−−−−−−−−Γj=Yj×Yj+1
2️⃣第二部分 I L I_L IL 怎么办呢?作者的方法是,可以看做纹理进行滤波后的值。在预过滤的步骤中,作者使用高斯滤波器,采取不同的过滤核半径进行过滤,对应不同的LOD。
根据论文,这个过滤还有个需要注意的地方:预过滤纹理的值必须定义在纹理空间的每个地方,甚至在纹理之外——作者在纹理周围引入一个边缘,在这个区域,增加了滤镜的半径,使其与纹理相交。
3️⃣对于 I L I_L IL ,我们还有最后一个需要解决的问题,那就是用来读取这个纹理的UV
和LOD Level
。
LOD Level
,论文里面是通过一个一维函数——纹理平面的平方距离 r 2 r^2 r2与多边形的面积 a a a之比: r 2 a \frac{r^2}{a} ar2。此外,最终结果需要进行混合,例如,我们得到的LOD Level
是3.4
,那么我们需要读取Level 3
和Level 4
的纹理值,然后进行混合(权重各自是0.6
和0.4
)。UV
。我们在使用Daniel的方法进行求解的时候,会得到一个指向面光源的向量,这个向量所指面光源上的点是对光照贡献最大的点,记作Q
点。我们需要做到就是得到这个Q
点,并算出它在这个纹理平面上对应的UV
坐标。4️⃣以下读取纹理贴图的源码来自Monica大佬的Github:
// vPolygonalLightVertexPos: 多边形光源的顶点
// vLooupVector:Daniel方法进行求解得到的向量
vec3 fecthFilteredLightTexture(vec3 vPolygonalLightVertexPos[4], vec3 vLooupVector)
{
vec3 V1 = vPolygonalLightVertexPos[1] - vPolygonalLightVertexPos[0];
vec3 V2 = vPolygonalLightVertexPos[3] - vPolygonalLightVertexPos[0];
vec3 PlaneOrtho = cross(V1, V2);
float PlaneAreaSquared = dot(PlaneOrtho, PlaneOrtho);
SRay Ray;
Ray.m_Origin = vec3(0);
Ray.m_Dir = vLooupVector;
vec4 Plane = vec4(PlaneOrtho, -dot(PlaneOrtho, vPolygonalLightVertexPos[0]));
float Distance2Plane;
rayPlaneIntersect(Ray, Plane, Distance2Plane);
vec3 P = Distance2Plane * Ray.m_Dir - vPolygonalLightVertexPos[0];
float Dot_V1_V2 = dot(V1, V2);
float Inv_Dot_V1_V1 = 1.0 / dot(V1, V1);
vec3 V2_ = V2 - V1 * Dot_V1_V2 * Inv_Dot_V1_V1;
vec2 UV;
UV.y = dot(V2_, P) / dot(V2_, V2_);
UV.x = dot(V1, P) * Inv_Dot_V1_V1 - Dot_V1_V2 * Inv_Dot_V1_V1 * UV.y;
UV = vec2(1 - UV.y, 1 - UV.x);
float Distance = abs(Distance2Plane) / pow(PlaneAreaSquared, 0.25);
float Lod = log(2048.0 * Distance) / log(3.0);
float LodA = floor(Lod);
float LodB = ceil(Lod);
float t = Lod - LodA;
vec3 ColorA = texture(u_FilteredLightTexture, vec3(UV, LodA)).rgb;
vec3 ColorB = texture(u_FilteredLightTexture, vec3(UV, LodB)).rgb;
return mix(ColorA, ColorB, t);
}
5️⃣最终,我们得到了此时的结果:
I = I L ⋅ I D I=I_L\cdot I_D I=IL⋅ID
不知道对不对
1️⃣之前在计算过程中一直忽略了菲涅耳项,因此我们还需要计算菲涅耳项用于对上面计算的光照结果进行矫正。这里的思路也是类似IBL
的方法,所以我们还需要使用第二张LUT
,也就是大名鼎鼎的BRDF LUT
:
然后,实时运行时,直接使用:
// F 就是 F0, 或者说 Specular Color
vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);
2️⃣所以说,最终结果是:
I S p e c = ( F 0 ∗ L U T . x + L U T . y ) ⋅ I D ⋅ I L I_{Spec}=(F_0*LUT.x+LUT.y)\cdot I_D \cdot I_L ISpec=(F0∗LUT.x+LUT.y)⋅ID⋅IL
1️⃣以上一切都是关于计算高光的,和漫反射无关。
2️⃣而实际上,漫反射的计算要简单的多,因为它没有lob
,是均匀的,所以实际上它根本不需要考虑变换矩阵 M M M,在代码上,则是如下:
vec3 LTC_Evaluate(vec3 N, vec3 V, vec3 P, mat3 Minv, vec3 points[4], bool twoSided);
...
vec3 spec = LTC_Evaluate(N, V, pos, Minv, points, twoSided);
spec *= texture2D(ltc_mag, uv).w;
// 矩阵M是单位矩阵,不需要任何转换
vec3 diff = LTC_Evaluate(N, V, pos, mat3(1), points, twoSided);
col = lcol*(scol*spec + dcol*diff);
col /= 2.0*pi;
[1]Monica的小甜甜的博客
[2]官方源码
[3][Real-Time Polygonal-Light Shading with Linearly Transformed Cosines]