SIGGRAPH 2020 Course: Samurai Shading in Ghost of Tsushima主要分享了《对马岛之魂》这款游戏中的一些图形学技术,包括
- 渲染强各向异性的材质
- 渲染强asperity scattering的材质
- 提升皮肤渲染的准确性
- 改进的detailed map
《对马岛之魂》的背景放在13世纪封建社会的日本,玩家操作一名日本武士,为了解放被蒙古入侵的对马岛的故事。整个游戏的渲染风格不是写实(photorealism),而是风格化的(stylized realism)。用到的渲染管线多数是延迟管线,少许前向管线,具体的划分如下。
- 延迟管线
- 兰伯特漫反射部分,包括透明材质和asperity scattering部分。
- 各向同性的GGX高光
- 前向管线
- 各向异性的GGX高光(SGGX)
- 各向异性的asperity scattering BRDF
- 次表面散射等
本文的思维导图如下。
各向异性的高光(Anisotropic Specular)
《对马岛之魂》的各向异性高光算法改进自2015年的一篇paper——The SGGX Microflake Distribution。在介绍SGGx以及《对马岛之魂》的改进之前,我们先看一下Microflake theory是怎么一回事。
Microflake theory
学习过PBR的同学应该对Microfacet theory比较熟悉,Microfacet theory讲的是对于一个粗糙的表面,可以视为是由无数个绝对光滑的微小表面(微表面)构成的。每个微表面都遵循镜面反射定律,而在宏观上,利用NDF来描述整体的特性。
类似地,Microflake theory描述的是体素的物理模型,常用于毛发、针织物之类物体的volume rendering。Microflake theory假设,在一个体素内充斥着各种椭球体的flakes,每个flakes都有一定的朝向性,也是绝对光滑的。光线在穿过这个体素的时候,会与这些flakes发生作用。但是不同于Microfacet theory,Microflake theory没有解决如何从宏观上描述flakes的形态的问题。
The SGGX Microflake Distribution
为了完善Microflake theory,SGGX应运而出。SGGX希望解决的是,如何在宏观上描述flakes的行为(类似于Microfacet theory用NDF描述微表面的朝向性),使得Microflake theory可以支持LoD。
SGGX模型的关键点有两个
- 提出了用投影面积描述flakes的宏观行为,类似于Microfacet theory中的roughness参数。
- 提出了用椭球体(ellipsoid)来编码投影面积的想法。
下面具体说说这两个关键点的含义。
光线在体素内部与flakes的相互作用有两个环节,一个是光线与flakes相交,另一个是光线被flakes反射。类似于ray tracing算法中追踪光线的两个步骤(求交与交互)。在第一个环节,光线与flakes发生碰撞的概率可以认为与flakes在光线方向上的投影面积成正比,投影面积越大,意味着越容易碰到光线。如下图所示。
而在第二个过程,我们要研究的是光线如何被flakes反射。这时,flakes在光线的tangent方向(即normal的垂直方向)上的投影面积越大,光线被反射地越分散。此时flakes在光线tangent方向上的投影面积,就类似于Microfacet theory中的roughness这个参数了。
需要注意的是,这第二点其实并没有严格的数学推导,只是基于直觉的假设。
将Microflake理论中flakes的(包括在光线方向以及光线的tangent方向的)投影面积等效于Microfacet理论中的roughness,这是SGGX模型的第一个洞见。接下来的问题是,如何宏观上表示这一堆零散的Microflake,将它们的朝向或者说投影面积用类似于Microfacet理论中的NDF函数表示呢?
这就引出了SGGX理论的第二个洞见——椭球体模型。如下图所示。
将不知到如何分布的一堆Microflake整合为一个完整的椭球体,这一步直觉上是可行的,但是同样缺乏严格的理论论证,只是SGGX理论给出的一个假设。这一假设也是SGGX模型的误差来源之一。
所以,我们就不经任何严格推理地假设,一块体素中的所有Microflake刚好可以构成一个椭球体,并且这个椭球体在各个方向上的投影刚好等于所有Microflake的投影的总和。那么,相对于Microfacet理论用两个粗糙度系数
α
x
\alpha_x
αx和
α
y
\alpha_y
αy,描述椭球体需要一个3x3的矩阵。
好在这个3x3的矩阵是个对称矩阵,只有6个独立的参数。
到目前为止,还有一个问题一直没有提到,那就是SGGX究竟为什么叫SGGX?
SGGX的全称是Symmetric GGX。GGX是Microfacet理论的一个法向分布模型,它是定义在半球面上的法向分布,这对描述表面属性的Microfacet理论是很好理解的,但是对于描述体素的Microflake理论而言,半球面分布的GGX很明显是不够的。而SGGX则是将半球面上分布的GGX对称到整个球面上,这也是symmetric S的含义。
改进的SGGX模型
SGGX模型用一个3x3的矩阵来描述椭球的形状,但是这个3x3的矩阵过于抽象,不太容易将这6个参数跟某个物理量(例如roughness之类的)对应起来,因此更实用的一个形式是将这个矩阵分解为特征向量与特征值相乘的形式:
方便起见,我们把这个矩阵称为
S
e
i
g
e
n
{\bf{S}}_{eigen}
Seigen形式。三个特征向量对应椭球的三个轴,三个特征值对应沿着相应轴的投影面积的平方。这样描述还有一个好处,GGX可以看做是三个特征向量分别为
t
⃗
\vec{t}
t、
b
⃗
\vec{b}
b、
n
⃗
\vec{n}
n的特殊情况,此时对应的特征值分别为
α
x
2
\alpha_x^2
αx2、
α
y
2
\alpha_y^2
αy2、
1
1
1,前两个分别为粗糙度在tangent方向、bitangent方向的分量的平方。
但是凭空出现的这三个椭球的轴太诡异了,还不是我们需要的物理量。因此我们想把它搞成我们熟悉的轴,比如像GGX那样其中一个轴是法向。我们将SGGX的三个轴
ω
^
i
\hat\omega_i
ω^i稍作旋转,写成这个形式:
我们把这个矩阵称为
S
n
o
r
m
{\bf{S}}_{norm}
Snorm形式。注意,这里的
t
⃗
\vec{t}
t、
b
⃗
\vec{b}
b、
n
⃗
\vec{n}
n向量并不是GGX里面用到的mesh的tangent空间,而是各向异性的tangent空间旋转对齐到normal后的tangent空间。各向异性的tangent空间不同于mesh的tangent空间的地方在于,后者由mesh定义,而前者可以由artist自行定义。这就使得各向异性不再受制于mesh走向,而可以自行决定偏置方向。而且,改写成这个形式以后,椭球体矩阵
S
{\bf{S}}
S只需要三个参数(
S
x
x
S_{xx}
Sxx、
S
x
y
S_{xy}
Sxy、
S
y
y
S_{yy}
Syy)外加法向量。
使用SGGX模型
《对马岛之魂》引入SGGX的初衷,在于希望能够自主控制各向异性参数的tangent方向。默认情况下,各向异性参数的tangent方向是跟随mesh的,这就阻碍了美术的表现力。如果能将tangent方向与各向异性参数一起保存起来,美术效果就可以做的更好。再加上SGGX对LoD的良好支持,完美~
为了支持SGGX模型,并且设计相关材质,《对马岛之魂》自定义了一个Substance Designer node给artist使用。Artist需要指定材质的
- Gloss UV,即在UV方向上的roughness系数
- direction,即椭球体的朝向
然后,encode过程如下。
- 根据artist指定的normal和2x2各向异性GGX矩阵恢复SGGX矩阵的
S
n
o
r
m
{\bf{S}}_{norm}
Snorm形式
- 这是获得的矩阵 S {\bf{S}} S是可以进行线性插值的,可以用于生成mipmap,获得新的差值后的矩阵 S ′ {\bf{S}}' S′
- 将
S
′
{\bf{S}}'
S′恢复成
S
n
o
r
m
{\bf{S}}_{norm}
Snorm形式,先迭代计算向量
n
′
{\bf{n}}'
n′,然后用
n
′
{\bf{n}}'
n′计算矩阵
M
n
′
{\bf{M}}_{{\bf{n}}'}
Mn′,最后计算
S
x
x
′
S_{xx}'
Sxx′,
S
x
y
′
S_{xy}'
Sxy′和
S
y
y
′
S_{yy}'
Syy′。
- 存储的时候将normal压缩存储在BC5的纹理中,2x2的各向异性矩阵的三个系数 S x x ′ S_{xx}' Sxx′, S x y ′ S_{xy}' Sxy′和 S y y ′ S_{yy}' Syy′以如下公式压缩存储在BC7纹理中。
[ S x x ′ , 1 2 S x y ′ S x x ′ S y y ′ + 1 2 , S y y ′ ] [\sqrt{S_{xx}'}, \frac{1}{2}\frac{S_{xy}'}{\sqrt{S_{xx}'S_{yy}'}} + \frac{1}{2}, \sqrt{S_{yy}'}] [Sxx′,21Sxx′Syy′Sxy′+21,Syy′]
这样一张额外的纹理就足以表达各向异性SGGX的参数了。相较于SGGX论文保存整个3x3矩阵的6个参数的方式节省了很多空间。
接下来是decode过程。假设各向异性纹理的三个通道分别为
T
x
T_x
Tx,
T
y
T_y
Ty,
T
z
T_z
Tz。
- 从纹理贴图中恢复 S x x ′ S_{xx}' Sxx′, S x y ′ S_{xy}' Sxy′和 S y y ′ S_{yy}' Syy′。
S x x ′ = T x 2 S x y ′ = T x × ( T y × 2 − 1 ) × T z S y y ′ = T z 2 \begin{aligned} S_{xx}' & = T_x^2 \\ S_{xy}' & = T_x \times (T_y \times 2 - 1) \times T_z \\ S_{yy}' & = T_z^2 \\ \end{aligned} Sxx′Sxy′Syy′=Tx2=Tx×(Ty×2−1)×Tz=Tz2
- 求解2x2矩阵
(
S
x
x
′
S
x
y
′
S
x
y
′
S
x
y
′
)
\begin{pmatrix} S_{xx}' & S_{xy}' \\ S_{xy}' & S_{xy}' \end{pmatrix}
(Sxx′Sxy′Sxy′Sxy′)
的特征值
α
t
2
\alpha_t^2
αt2和
α
b
2
\alpha_b^2
αb2,以及
α
t
α
b
\alpha_t\alpha_b
αtαb。
Δ = ( S x x ′ − S y y ′ ) 2 + 4 S x y ′ 2 α t 2 = 1 2 ( S x x ′ + S y y ′ + Δ ) α b 2 = 1 2 ( S x x ′ + S y y ′ − Δ ) α t α b = α t 2 α b 2 \begin{aligned} \Delta & = \sqrt{(S_{xx}' - S_{yy}')^2 + 4{S_{xy}'}^2} \\ \alpha_t^2 & = \frac{1}{2}(S_{xx}' + S_{yy}' + \Delta) \\ \alpha_b^2 & = \frac{1}{2}(S_{xx}' + S_{yy}' - \Delta) \\ \alpha_t\alpha_b & = \sqrt{\alpha_t^2\alpha_b^2} \end{aligned} Δαt2αb2αtαb=(Sxx′−Syy′)2+4Sxy′2=21(Sxx′+Syy′+Δ)=21(Sxx′+Syy′−Δ)=αt2αb2
- 然后计算mesh的tangent space到anisotropic的tangent space的旋转角度的 sin \sin sin和 cos \cos cos值
c = 1 2 × ( S x x ′ − S y y ′ Δ + 1 ) s = Sign ( S x y ′ ) × 1 − c 2 \begin{aligned} c & = \sqrt{\frac{1}{2} \times (\frac{S_{xx}' - S_{yy}'}{\Delta} + 1)} \\ s & = \text{Sign}(S_{xy}')\times \sqrt{1 - c^2} \end{aligned} cs=21×(ΔSxx′−Syy′+1)=Sign(Sxy′)×1−c2
- 最后根据旋转角度和mesh的tangent向量 t ⃗ m \vec{t}_m tm、bitangent向量 b ⃗ m \vec{b}_m bm计算各向异性的tangent向量 t ⃗ a \vec{t}_a ta、bitangent向量 b ⃗ a \vec{b}_a ba。
t ⃗ a = c × t ⃗ m + s × b ⃗ m b ⃗ a = − s × t ⃗ m + c × b ⃗ m \begin{aligned} \vec{t}_a & = c \times \vec{t}_m + s \times \vec{b}_m \\ \vec{b}_a & = -s \times \vec{t}_m + c \times \vec{b}_m \end{aligned} taba=c×tm+s×bm=−s×tm+c×bm
- 有了 α t 2 \alpha_t^2 αt2、 α b 2 \alpha_b^2 αb2、 α t α b \alpha_t\alpha_b αtαb以及 t ⃗ a \vec{t}_a ta、 b ⃗ a \vec{b}_a ba,可以计算各向异性的NDF了
D ( h ⃗ ) = ( α t α b ) 3 π ( ( t a ⃗ ⋅ h ⃗ ) 2 α b 2 + ( b a ⃗ ⋅ h ⃗ ) 2 α t 2 + ( n ⃗ ⋅ h ⃗ ) 2 ( α t α b ) 2 ) 2 D(\vec{h}) = \frac{(\alpha_t\alpha_b)^3}{\pi((\vec{t_a}\cdot\vec{h})^2\alpha_b^2+(\vec{b_a}\cdot\vec{h})^2\alpha_t^2+(\vec{n}\cdot\vec{h})^2(\alpha_t\alpha_b)^2)^2} D(h)=π((ta⋅h)2αb2+(ba⋅h)2αt2+(n⋅h)2(αtαb)2)2(αtαb)3
可以看出,《对马岛之魂》在SGGX实际使用的时候,并没有像论文中提到的那样对矩阵 S {\bf{S}} S进行直接mipmap差值,这个近似会导致一定的误差。另一个缺陷在于,当 α t \alpha_t αt和 α b \alpha_b αb一个很大一个很小的时候,效果并不理想,也会产生一定的artifact。
Fuzz shading
第二部分讲了一个用于渲染毛绒材质的BRDF模型。《对马岛之魂》用这个BRDF来渲染地面的苔藓和马匹的绒毛。通常搞一个新的BRDF的一般步骤都是测量,分析建模,最后弄一个曲线逼近。建模部分《对马岛之魂》参考了一篇2002年的论文:The Secret of Velvety Skin。
测量与建模
我们先看一下论文The Secret of Velvety Skin的测量和建模结果。
下图展示了实拍的黑色绒布圆柱体的照片(B),与之对比的是兰伯特圆柱体的理论效果(A)。可以看出绒布的视觉效果完全不遵循兰伯特反射,在A亮的地方B较暗,反之亦然。C图展示了B中各个位置的亮度。
进一步的测量展示出,入射光线和视角的不同会对BRDF的影响,如下图所示。左图展示的是入射光线和视角在同一个方向,同时随着
θ
\theta
θ变化时的BRDF值(坐标图的纵轴,下同)。这个图可以理解为展示了backscattering的效果,越垂直于法向,backscattering越强。中图展示了入射光线平行于法向时,BRDF强度随视角的变化,即normal incidence。相对而言BRDF变化没有那么剧烈(注意这个图的纵坐标被拉伸了,其值并不大)。右图则展示了入射光线和视角在法向的对称两侧(半向量等于法向)时,BRDF的强度随着夹角大小的变化。这个图反映的是完全镜面反射(specular reflection)的效果,可以看出越是接近掠射角(gazing angle),BRDF越强。
总结起来两个点
- strong back scattering lobe
- smaller forward scattering lobe
最后,The Secret of Velvety Skin对绒布的BRDF建模如下图所示。其中的虚线表示兰伯特反射的BRDF,这个模型满足了前面提到的几个观察:backscattering强,specular reflection较弱。当然,如果入射角接近normal,图中红色的曲线形状还会有变化。
曲线逼近
基于前面观测建模得到的BRDF,《对马岛之魂》搞了一个新的Diffuse BRDF公式:
L d ( v ) = c f u z z ⋅ p ( θ h , ϕ h ) ⋅ g s c a t t e r p ( θ h , ϕ h ) = r n o r m P S c h l i c k ( k S c h l i c k , g D o t F u z z ( θ h , ϕ h ) ) g D o t F u z z ( θ h , ϕ h ) = 1 − ( cos θ t i l t ⋅ cos θ h + sin θ t i l t ⋅ cos ϕ h ) 2 P S c h l i c k ( k , d ) = 1 − k 2 4 π ( 1 + k d ) 2 \begin{aligned} L_d({\bf{v}}) & ={\bf{c}}_{fuzz}\cdot p(\theta_h, \phi_h) \cdot g_{scatter} \\ p(\theta_h, \phi_h) & = r_{norm} P_{Schlick}(k_{Schlick}, g_{DotFuzz}(\theta_h, \phi_h)) \\ g_{DotFuzz}(\theta_h, \phi_h) & = \sqrt{1-(\cos{\theta_{tilt}} \cdot \cos{\theta_h} + \sin{\theta_{tilt} \cdot \cos{\phi_h}})^2} \\ P_{Schlick}(k, d) & = \frac{1-k^2}{4\pi(1+kd)^2} \\ \end{aligned} Ld(v)p(θh,ϕh)gDotFuzz(θh,ϕh)PSchlick(k,d)=cfuzz⋅p(θh,ϕh)⋅gscatter=rnormPSchlick(kSchlick,gDotFuzz(θh,ϕh))=1−(cosθtilt⋅cosθh+sinθtilt⋅cosϕh)2=4π(1+kd)21−k2
其中, c f u z z {\bf{c}}_{fuzz} cfuzz表示布料的diffuse颜色, θ h \theta_h θh表示半向量 h ⃗ \vec{h} h和法向的夹角, ϕ h \phi_h ϕh表示半向量 h ⃗ \vec{h} h和y轴的夹角。 θ t i l t \theta_{tilt} θtilt表示绒毛的朝向与法向的夹角。
这个BRDF公式主要是用于渲染绒布的漫反射的,因此它用于替代兰伯特反射模型 L l a m b e r t = c d i f f cos ( θ l ) π L_{lambert} = c_{diff}\frac{\cos(\theta_l)}{\pi} Llambert=cdiffπcos(θl)。 P S c h l i c k ( k , d ) P_{Schlick}(k, d) PSchlick(k,d)是作者搞出这个BRDF的核心,这个公式是Schlick对Henyey-Greenstein方程的近似实现,为了将视角、入射光线方向和绒布的毛绒方向融合进来,作者引入 g D o t F u z z ( θ , ϕ ) g_{DotFuzz}(\theta, \phi) gDotFuzz(θ,ϕ)函数,替代 P S c h l i c k ( k , d ) P_{Schlick}(k, d) PSchlick(k,d)中的 cos \cos cos项 d d d。 r n o r m r_{norm} rnorm项是为了将 P S c h l i c k ( k , d ) P_{Schlick}(k, d) PSchlick(k,d)归一化而引入的,由于 r n o r m r_{norm} rnorm不存在一个解析解,所以这个函数其实是一个经验函数,它的引入也会导致一定的能量损失。最后 g s c a t t e r g_{scatter} gscatter项是为了将函数曲线收拢到跟前面的建模曲线类似的情况而添加的,也可以理解为一个经验项。
完整的函数定义和图像可以参考作者给出的链接。
函数图像看起来跟模型还是有点相似的,误差主要出现在backscattering部分,但是总体而言已经很接近了。虽然实际使用起来不知道效果怎么样,感觉这么复杂的一个公式会使得计算量暴增。
改进
在《对马岛之魂》上线以后,为了解决这个BRDF的能量损失问题,作者又搞出了一个基于SGGX的版本,将前面的 p ( θ , ϕ ) p(\theta, \phi) p(θ,ϕ)替换为 p S G G X ( θ , ϕ ) p_{SGGX}(\theta, \phi) pSGGX(θ,ϕ):
L d ( v ) = c f u z z ⋅ p S G G X ( θ h , ϕ h ) ⋅ g s c a t t e r p S G G X ( θ h , ϕ h ) = D ( g H a l f D o t F u z z ( θ h , ϕ h ) ) 4 σ ( θ l , ϕ l ) g H a l f D o t F u z z ( θ h , ϕ h ) = cos θ t i l t ⋅ cos θ h + sin θ t i l t ⋅ cos ϕ h D ( d ) = α S G G X 3 π ( d 2 ( 1 − α S G G X 2 ) + α S G G X 2 ) 3 σ ( d ) = 1 + g u D o t F u z z ( θ l , ϕ l ) 2 ( α S G G X 2 − 1 ) g u D o t F u z z ( θ l , ϕ l ) = cos θ t i l t ⋅ cos θ l + sin θ t i l t ⋅ cos ϕ l \begin{aligned} L_d({\bf{v}}) & ={\bf{c}}_{fuzz}\cdot p_{SGGX}(\theta_h, \phi_h)\cdot g_{scatter} \\ p_{SGGX}(\theta_h, \phi_h) & = \frac{D(g_{HalfDotFuzz}(\theta_h, \phi_h))}{4\sigma(\theta_l, \phi_l)} \\ g_{HalfDotFuzz}(\theta_h, \phi_h) & = \cos{\theta_{tilt}} \cdot \cos{\theta_h} + \sin{\theta_{tilt} \cdot \cos{\phi_h}} \\ D(d) & = \frac{\alpha_{SGGX}^3}{\pi(d^2(1-\alpha_{SGGX}^2)+\alpha_{SGGX}^2)^3} \\ \sigma(d) & = \sqrt{1+g_{uDotFuzz}(\theta_l, \phi_l)^2(\alpha_{SGGX}^2 - 1)} \\ g_{uDotFuzz}(\theta_l, \phi_l) & = \cos{\theta_{tilt}} \cdot \cos{\theta_l} + \sin{\theta_{tilt} \cdot \cos{\phi_l}} \\ \end{aligned} Ld(v)pSGGX(θh,ϕh)gHalfDotFuzz(θh,ϕh)D(d)σ(d)guDotFuzz(θl,ϕl)=cfuzz⋅pSGGX(θh,ϕh)⋅gscatter=4σ(θl,ϕl)D(gHalfDotFuzz(θh,ϕh))=cosθtilt⋅cosθh+sinθtilt⋅cosϕh=π(d2(1−αSGGX2)+αSGGX2)3αSGGX3=1+guDotFuzz(θl,ϕl)2(αSGGX2−1)=cosθtilt⋅cosθl+sinθtilt⋅cosϕl
其中,
θ
l
\theta_l
θl表示光线方向和法向的夹角,
ϕ
l
\phi_l
ϕl表示光线方向和y轴的夹角。完整图像可以参考链接,具体的推导可以参考作者的PPT,不在赘述。
实现细节
公式中的几个参数是可以调整的,比如 g s c a t t e r g_{scatter} gscatter的控制系数density,斑驳的大小spread,绒毛朝向 θ t i l t \theta_{tilt} θtilt,以及绒毛的颜色 c f u z z {\bf{c}}_{fuzz} cfuzz。
在《对马岛之魂》的实现中,这些参数的取值为
- g s c a t t e r g_{scatter} gscatter的控制系数 d = 0.5 d = 0.5 d=0.5
- spread = 0.9,即 k S c h l i c k ≈ − 0.1 k_{Schlick} \approx -0.1 kSchlick≈−0.1, S G G X ≈ 0.95 _{SGGX}\approx0.95 SGGX≈0.95
- 绒毛的朝向 θ t i l t \theta_{tilt} θtilt和顶点的法向一致
- 绒毛的颜色 c f u z z = 5 ⋅ c L a m b e r t {\bf{c}}_{fuzz}=5\cdot{\bf{c}}_{Lambert} cfuzz=5⋅cLambert
同时,引入新的参数Fuzziness
用于混合Lambert BRDF和Fuzz BRDF,即:
L D i f f u s e = lerp ( L L a m b e r t , L F u z z , Fuzziness ) L_{Diffuse} = \text{lerp}(L_{Lambert}, L_{Fuzz}, \text{Fuzziness}) LDiffuse=lerp(LLambert,LFuzz,Fuzziness)
Skin shading
皮肤是一种有很强的的次表面散射(subsurface scattering)现象的材质,所谓次表面散射现象,指的是光线打在材质表面的时候,有一部分会进入材质内部,在材质内部多次弹射后再离开材质表面,如下图所示。
这个现象如果严格按照物理原理进行实现,应该是在光线出射点对周围一个小范围内的入射光线进行积分,并以一定的比例加到当前出射点的出射光线上。但是在实时渲染中这个方法很明显是不可能实现的。对此,有两个思路可以进行近似,一种是渲染高清的,subsurface scattering的方法,包括separable subsurface scattering、screen space subsurface scattering等,这个方法需要多个pass才能完成,本质上是利用图像卷积或者模糊来近似subsurface scattering效果。另一种方法是快速但是没那么精确的,pre-integrated skin shading方法,通常用在移动端,将散射值预先渲染好保存在纹理中,然后实时渲染时进行采样,单个pass即可解决。
顺便说一句,这两种对皮肤的渲染方法UE4都有支持,前者有subsurface shading model,后者有PreintegratedSkin shading model。
而《对马岛之魂》采用的是预积分(pre-integrated)的方法。在探讨具体的方法之前,我们先了解一下预积分方法背后的理论和原理。
基于预积分的皮肤渲染
接下来具体看一下预积分方法的思路。首先考虑一个问题,什么情况下皮肤会产生次表面散射现象产生?可以肯定的一点是,当固定的平行光照射在完全平整的表面上的时候,是看不出次表面散射的,因为相邻处产生的折射光线非常平均。只有在这几种情况会有明显的次表面散射现象:
- mesh的尺度上,曲率变化大的位置
- 小尺度上,normal map上有小的bump
- 阴影处
这三种情况分别处理,依次对应
- large-scale feature
- small surface detail
- shadow
Scattering and diffuse light
对于大尺度上的次表面散射现象,可以通过积分的预计算,将结果保存成LUT,然后在实时运算时直接读纹理。LUT的两个轴分别表示法向与光线方向的乘积( N ⋅ L N\cdot L N⋅L)和曲率。
还是从前面完全平整的表面这个假设开始讨论。如果我们把这个完全平整的表面稍稍弯曲,围成一个半径为
r
r
r的球(各个点的曲率均为
1
/
r
1/r
1/r),此时某个过球心的切面如下图所示。
假设我们要计算其中P点受到周围点的次表面散射的散射值的大小,其中L表示光线的方向,N表示点P的法向。以Q点为例,Q点受到的光照强度应该是
cos
(
θ
+
x
)
\cos(\theta + x)
cos(θ+x)。
Q点会对P点有一定的散射率,记为 q ( x ) q(x) q(x)。上图中的圆环上有无数个这样的Q点,把他们都算上,最终P点受到的散射值为
D ( θ , r ) = ∫ − π π cos ( θ + x ) q ( x ) d x D(\theta,r) = \int^{\pi}_{-\pi}\cos(\theta+x)q(x)dx D(θ,r)=∫−ππcos(θ+x)q(x)dx
有一点需要注意,上式中的 cos \cos cos项不能小于零,因为如果Q点没有光线照射到(即 cos ( θ + x ) < 0 \cos(\theta+x)<0 cos(θ+x)<0)时不会对P点有负的影响。另外这个积分是在圆环(ring)上做的,它并不能完全表示整个球面上所有的点对P的影响。但是[1]声称这个误差并不大,可以这样近似。
我们再看一下 q ( x ) q(x) q(x)的取值。考虑到 q ( x ) q(x) q(x)表示Q点对P点的散射率,同理,Q点会对球面上(近似的圆环上)所有点都有一个散射率,而根据能量守恒定律,这些比率加起来恒等于1,因此有:
∫ − π π q ( x ) d x = 1 \int^{\pi}_{-\pi}q(x)dx = 1 ∫−ππq(x)dx=1
接下来我们只要找一个满足上式的 q q q就可以了。这里最常用的是diffusion profile R ( d ) R(d) R(d),其中 d d d表示两点间的距离,在圆环的假设下, d d d可以写为半径和夹角的函数: d = 2 r sin ( x / 2 ) d=2r\sin(x/2) d=2rsin(x/2)。考虑到 R ( d ) R(d) R(d)在圆环上的积分不为1,我们可以令 q ( x ) = k R ( d ) q(x) = kR(d) q(x)=kR(d)。这样有:
∫ − π π k R ( d ) d x = 1 \int^{\pi}_{-\pi}kR(d)dx = 1 ∫−ππkR(d)dx=1
即:
k = 1 / ∫ − π π R ( d ) d x q ( x ) = R ( d ) / ∫ − π π R ( d ) d x \begin{aligned} k & = 1 / \int^{\pi}_{-\pi}R(d)dx \\ q(x) & = R(d) / \int^{\pi}_{-\pi}R(d)dx \end{aligned} kq(x)=1/∫−ππR(d)dx=R(d)/∫−ππR(d)dx
再将这个 q q q代入 D ( θ , r ) D(\theta,r) D(θ,r):
D ( θ , r ) = ∫ − π π cos ( θ + x ) q ( x ) d x = ∫ − π π cos ( θ + x ) R ( d ) ∫ − π π R ( d ) d x d x = ∫ − π π cos ( θ + x ) R ( d ) d x ∫ − π π R ( d ) d x = ∫ − π π cos ( θ + x ) R ( 2 r sin ( x / 2 ) ) d x ∫ − π π R ( 2 r sin ( x / 2 ) ) d x \begin{aligned} D(\theta,r) & = \int^{\pi}_{-\pi}\cos(\theta+x)q(x)dx \\ & = \int^{\pi}_{-\pi}\cos(\theta+x)\frac{R(d)}{\int^{\pi}_{-\pi}R(d)dx}dx \\ & = \frac{\int^{\pi}_{-\pi}\cos(\theta+x)R(d)dx}{\int^{\pi}_{-\pi}R(d)dx} \\ & = \frac{\int^{\pi}_{-\pi}\cos(\theta+x)R(2r\sin(x/2))dx}{\int^{\pi}_{-\pi}R(2r\sin(x/2))dx} \end{aligned} D(θ,r)=∫−ππcos(θ+x)q(x)dx=∫−ππcos(θ+x)∫−ππR(d)dxR(d)dx=∫−ππR(d)dx∫−ππcos(θ+x)R(d)dx=∫−ππR(2rsin(x/2))dx∫−ππcos(θ+x)R(2rsin(x/2))dx
这个最终形式也是我们可以预计算的积分。
在实际应用中,通常会用
N
⋅
L
N\cdot L
N⋅L表示
θ
\theta
θ,用
1
/
r
1/r
1/r代替
r
r
r预计算积分纹理以及在实时采样。
N
⋅
L
N\cdot L
N⋅L怎么取下一节再讨论,这里说一下
1
/
r
1/r
1/r怎么计算。如下图所示。
基本原理是,曲率越大,意味着单位
Δ
p
\Delta p
Δp对应的
Δ
N
\Delta N
ΔN越大,所以可以简单地利用
Δ
N
/
Δ
P
\Delta N/\Delta P
ΔN/ΔP来求曲率。当然在对纹理采样的时候,需要将
1
/
r
1/r
1/r归一化到
[
0
,
1
]
[0,1]
[0,1]之间。
float curve = saturate(length(fwidth(NormalDirection)) / length(fwidth(i.posWorld)) * _CurveFactor);
Scattering and normal maps
前面讨论的积分公式虽然很方便做预计算,但是有一个很致命的问题,那就是它假定了皮肤是一个球体,所有的点都有着相同的曲率。这很明显是不可能的。这个假设使得模型无法用在曲率变化很快的地方。对于这类情况,需要额外进行处理。
好在实际应用中,这类细节不是跟mesh绑定在一起的,mesh通常是粗粒度的,而细节信息则是用normal map来存储。这就使得我们可以通过对normal map做模糊操作,来近似次表面散射现象。由于次表面散射的程度与光线的波长有关,所以RGB三个通道需要有不同的模糊程度。如果对他们分别存储三张模糊过的normal map就太浪费存储了,通常的做法是用原始的normal map和模糊后的normal map做差值,差值系数即次表面散射程度。
用模糊后的normal作为diffuse light的normal,再计算 N ⋅ L N\cdot L N⋅L,这样就可以在计算diffuse light的时候考虑到细粒度的特征。
Shadow scattering
前两部分分别介绍了粗粒度和细粒度特征的处理方法,但是还有一种情况没有涵盖到,那就是阴影。阴影不受mesh和normal map的影响,可能出现在任何地方。在阴影区域,亮部会向暗部扩散次表面散射产生的散射值,将暗部稍稍提亮。基于这个基本的想法,一个最朴素的解决方案是,能不能改变一下阴影的过渡程度,稍微朝亮部的部分偏一下。如下图所示,我们找个映射将左边的伴影过渡转换为右侧,右侧多出来的一段亮部就是次表面散射现象。
这个问题的解决思路跟前面类似,也可以将预计算的结果保存在纹理中。纹理的两个轴分别是原阴影系数和宽度,采样结果是对应RGB通道的修正后的阴影系数。
改进
《对马岛之魂》主要做了这三个方面的改进:
- 对punctual light,采用linear scattering profile或cylindrical integration
- 对SH lighting LUT,在球面积分时采用radial profile
- 使用directional curvature代替mean curvature采样纹理。
其中比较明显的改进集中在第三点上。
前面的模型讨论的是平行光照,但是《对马岛之魂》认为,对所有的光照采用同样的计算方法是不合适的,同时提出,对平行光应该采用方向曲率而不是平均曲率,而AO之类的光源可以采用平均曲率计算。下图展示了不同曲率的计算方法在平行光下的区别。图中上面一行表示的是相应的曲率值。可以看出使用方向曲率可以减少不自然的次表面散射现象(比如鼻尖附近)。
方向曲率的使用方法如下。
- 首先我们需要在mesh的vertex信息中保存曲率张量和平均曲率。曲率张量的计算参考下式,具体的方法参考[3]
II = [ d 1 ⃗ , d 2 ⃗ ] [ κ 1 0 0 κ 2 ] [ d 1 ⃗ , d 2 ⃗ ] T \text{II} = \begin{bmatrix} \vec{d_1}, \vec{d_2} \end{bmatrix} \begin{bmatrix} \kappa_1 & 0 \\ 0 & \kappa_2 \\ \end{bmatrix} \begin{bmatrix} \vec{d_1}, \vec{d_2} \end{bmatrix}^T II=[d1,d2][κ100κ2][d1,d2]T
其中, d i ⃗ \vec{d_i} di表示主方向, κ i \kappa_i κi表示主方向上的曲率。由于 II \text{II} II是一个对称矩阵,所以保存它只需要一个vec3即可。
- 有了曲率张量,在实时运算是根据光线的方向计算该方向上的曲率:
κ I ⃗ = I ⃗ T II I ⃗ \kappa_{\vec{I}} = \vec{I}^T\text{II}\vec{I} κI=ITIII
推测这里的 I ⃗ \vec{I} I应该是光线在tangent平面的投影,是个二维向量。
- 然后利用这个方向曲率,采用LUT。
改进之后的skin shading,在鼻梁、鼻翼和嘴唇部分有一些区别,但是也说不出改善了多少。
Detail map
在detail map这部分,《对马岛之魂》采用的是合成纹理的方法,具体来说,是将目标纹理拆分为两部分:
关于纹理混合相关的背景,可以参考[4]。
《对马岛之魂》的改进点在于,改进了overlay混合方法的合成算子 ⨂ \bigotimes ⨂。原有的算子为:
f o v e r l a y ( s , d ) = { 2 s d , s < 0.5 1 − 2 ( 1 − s ) ( 1 − d ) , s ≥ 0.5 f_{overlay}(s,d) = \begin{cases} 2sd, & s < 0.5 \\ 1-2(1-s)(1-d), & s \geq 0.5 \end{cases} foverlay(s,d)={2sd,1−2(1−s)(1−d),s<0.5s≥0.5
改进后的是:
f o s ( s , d ) = { 2 s d , s < 0.5 1 − 2 ( 1 − s ) ( 1 − d ) , s ≥ 0.5 and d ≥ 0.5 lerp ( 2 s d , s , ( 2 s − 1 ) 2 ) , s ≥ 0.5 and d < 0.5 f_{os}(s,d) = \begin{cases} 2sd, & s < 0.5 \\ 1-2(1-s)(1-d), & s \geq 0.5 \text{ and } d \geq 0.5 \\ \text{lerp}(2sd, s, (2s-1)^2), &s \geq 0.5 \text{ and } d < 0.5 \end{cases} fos(s,d)=⎩⎪⎨⎪⎧2sd,1−2(1−s)(1−d),lerp(2sd,s,(2s−1)2),s<0.5s≥0.5 and d≥0.5s≥0.5 and d<0.5
如果用曲线把精度损失情况可视化出来,是这样的:
渲染效果对比:
数值上有一些提升,虽然看起来不是很明显。
这个改进的点严格来说算是TA的一个小创新,UE4中只要在Material Editor中加一个额外的判定就可以了,代码都不用改。
Reference
- SIGGRAPH 2020 Course
- Eric Heitz, Jonathan Dupuy, Cyril Crassin, and Carsten Dachsbacher. The SGGX microflake distribution. ACM Trans. Graph., 34(4), July 2015.
- Jan Koenderink and Sylvia Pont. The secret of velvety skin. Machine Vision and Applications, 14(4):260–268, 2003.
- GPU Pro 2
- https://zhuanlan.zhihu.com/p/56052015
- Estimating Curvature and Their Derivatives on Triangle Meshes
- Normal Blend方法总结