在 虚幻引擎之自定义着色模型(ShadingModel) 中,笔者简要地提到了Hair(毛发)光照模型,但是并没有做深入一点的介绍和源码的阅读。
同时与头发材质渲染经常一起提到的就是各向异性(Anisotropy),这又是什么样的概念呢?
在本篇博客中,将对各向异性和头发渲染进行介绍。
在渲染头发、丝绸等材质时,常要用到各向异性高光(Anisotropic highlighting)。
那么,什么是各向异性高光呢?先来看个直观的对比。
可以看到:
各向异性(Anisotropic)指的是在不同方向上表现出的光照效果会产生差异。
右侧的金属表面反射的高光效果和使用 BlinnPhong (或者类似算法)得到的高光效果有着明显的差异。
这可能是由于加工工艺导致的,表面并不是抛光的(一个完全被抛光的物体表面是光学平面),因此形成了大量细小沟槽。
这样类型的凹槽造成了各向异性的光照效果。由于凹槽没有小到可以被光线忽略,所以当光线到达物体表面的时候没有办法直接反射出去,而是射入了凹槽中,在凹槽内部经过多次反射,最终才反射出来。
现实生活中还有很多物体表面会产生各向异性的光照效果,比如丝绸织物, 头发,CD表面等。
在渲染中来实现各向异性光照时,我们需要在像素着色器中,根据法线扰动规则重新计算法线,这样虽然是一个平面,但它上面的像素却会因为法线扰动而形成一些纹理、凹槽的效果,从而展示出更多的细节表现,而且法线的扰动通常是有规律的,所以在不同的方向上表现出的效果可能会不一样,从而表现出所谓的光学各向异性。
将各向同性的shading model拓展到各向异性,需要解决两个问题:
对于问题1:
既然是各向异性,必须要分别定义各个方向上的差异属性,这里的属性一般是粗糙度(Roughness)。
方向一般在切平面上选取两个轴,即tangent方向和bitangent方向,他们与normal共同构成了一组正交基底(TBN坐标系)。
对于需要精细控制切平面方向的模型,比如头发,会单独提供切向贴图,指明切向方向,类似于法向贴图。其他情况一般由系统(shader)确定切向方向。
通过增加额外的一个参数,将原有的粗糙度替换成为tangent和bitangent方向的粗糙度,记作 α t , α b \alpha_{t},\alpha_{b} αt,αb或 α x , α y \alpha_{x},\alpha_{y} αx,αy。
在实际操作中,一般是增加一个anisotropy
各向异性参数,
然后用粗糙度 α \alpha α( α = r o u g h n e s s ∗ r o u g h n e s s \alpha = roughness * roughness α=roughness∗roughness)和anisotropy
计算Tangent和BiTangent方向的粗糙度。
具体的映射方法有很多:
Neubelt and Pettine
α x = α α y = l e r p ( 0 , α , 1 − a n i s o t r o p y ) \begin{aligned} \alpha_x = & \alpha \\ \alpha_y = & lerp(0,\alpha,1-anisotropy) \end{aligned} αx=αy=αlerp(0,α,1−anisotropy)
Burley
α x = α 1 − 0.9 × a n i s o t r o p y α y = α 1 − 0.9 × a n i s o t r o p y \begin{aligned} \alpha_x = & \frac{\alpha}{\sqrt{1-0.9\times anisotropy} } \\ \alpha_y = & \alpha \sqrt{1-0.9\times anisotropy} \end{aligned} αx=αy=1−0.9×anisotropyαα1−0.9×anisotropy
Kulla
α x = α × ( 1 + a n i s o t r o p y ) α y = α × ( 1 − a n i s o t r o p y ) \begin{aligned} \alpha_x = & \alpha \times (1+anisotropy) \\ \alpha_y = & \alpha \times (1-anisotropy) \\ \end{aligned} αx=αy=α×(1+anisotropy)α×(1−anisotropy)
对于问题2:
BRDF公式取决于三个部分:法线发布函数D,遮挡项G,和菲涅尔项F。
其中,F取决于材质的固有属性,是不会变的。各向异性是宏观的现象,不会影响微观表面的性质。
至于D和G,由于G是从D推导而来,因此先看一下D需要如何进行修改。
法线分布函数
D(m),法线分布函数,代表一个微平面表面法线的统计分布(概率分布)。
其输入是一个法线方向,返回的是一个概率值。
一般以h(半程向量)作为微表面的法线,只有 m = h m=h m=h时,才会将光线 l l l反射到 v v v方向。
对于D(m)在微平面上进行积分,可以得到微平面的面积 ∫ D ( m ) d m \int D(m)dm ∫D(m)dm。
投影到宏观表面平面上的微平面区域,等于宏观表面的面积,被约定为1。。
∫ D ( m ) ( n ⋅ m ) d m = 1 \int D(m) (n \cdot m) dm = 1 ∫D(m)(n⋅m)dm=1
更一般地,微观表面(microsurface)和宏观表面(macrosurface)在垂直于任何视图方向v的平面上的投影是相等的:
∫ D ( m ) ( v ⋅ m ) d m = v ⋅ n \int D(m) (v \cdot m) dm = v \cdot n ∫D(m)(v⋅m)dm=v⋅n
法线分布函数的形状不变性
法线分布函数D的形状不变性是指改变D的粗糙度,相当于对微表面进行拉伸,而不改变微表面的形状。
对于形状不变的NDF,缩放粗糙度参数相当于通过倒数拉伸微观几何,如下图所示:
若一个各向同性的NDF可以改写成以下形式,则这个NDF具有形状不变性(shape-invariant):
D ( m ) = 1 α 2 ( n ⋅ m ) 4 g ( 1 − ( n ⋅ m ) 2 α ( n ⋅ m ) ) D(m)=\frac{1}{{\alpha}^2(n\cdot m)^4} g(\frac{\sqrt[]{1-(n\cdot m)^2}}{\alpha (n\cdot m)} ) D(m)=α2(n⋅m)41g(α(n⋅m)1−(n⋅m)2)
其中, g ( ) g() g()代表一个表示了NDF形状的一维函数。
具有形状不变性(shape-invariant)的法线分布函数,可以用于推导该函数的归一化的各向异性版本,并且可以很方便地推导出对应的遮蔽阴影项G。
对于常见的法线分布函数的旋转不变性的分类:
下图显示了如何通过**拉伸表面(stretching the surface)**将各向同性的形状不变分布转换为各向异性分布。
相反,任何具有各向异性分布的配置都可以转换回具有各向同性分布的配置。
下图展示了:通过拉伸表面,可以将各向同性具备形状不变性的分布,转换为各向异性分布。
通过上述的方法,可以推导出Beckmann和GGX分布的各向异性形式。
若一个各向同性(isotropic)的NDF具备形状不变性(shape-invariant),则其可以用以下形式写出:
D ( m ) = 1 α 2 ( n ⋅ m ) 4 g ( 1 − ( n ⋅ m ) 2 α ( n ⋅ m ) ) D(m)=\frac{1}{{\alpha}^2(n\cdot m)^4} g(\frac{\sqrt[]{1-(n\cdot m)^2}}{\alpha (n\cdot m)} ) D(m)=α2(n⋅m)41g(α(n⋅m)1−(n⋅m)2)
那么,通过此形式可以可得到各向异性的(anisotropic)版本:
D ( m ) = 1 α x α y ( n ⋅ m ) 4 g ( ( t ⋅ m ) 2 α x 2 + ( b ⋅ m ) 2 α y 2 ( n ⋅ m ) ) D(m)=\frac{1}{{\alpha_x \alpha_y}(n\cdot m)^4} g(\frac{\sqrt[]{\frac{(t\cdot m)^2}{\alpha _x^2} +\frac{(b\cdot m)^2}{\alpha _y^2} }}{(n\cdot m)} ) D(m)=αxαy(n⋅m)41g((n⋅m)αx2(t⋅m)2+αy2(b⋅m)2)
其中,参数 α x \alpha_x αx和 α y \alpha_y αy分别表示沿切线(tangent)方向和副切线(bitangent)方向的粗糙度。
Anisotropic Beckmann Distribution
各项异性的Beckmann分布形式如下:
D B a n i s o ( m ) = 1 π α x α y ( n ⋅ m ) 4 e x p ( − ( t ⋅ m ) 2 α x 2 + ( b ⋅ m ) 2 α y 2 ( n ⋅ m ) 2 ) D_{Baniso}(m)= \frac{1}{\pi\alpha_x \alpha_y (n\cdot m)^4}exp(-\frac{\frac{(t \cdot m)^2}{\alpha_x^2} + \frac{(b \cdot m)^2}{\alpha_y^2}}{(n\cdot m)^2} ) DBaniso(m)=παxαy(n⋅m)41exp(−(n⋅m)2αx2(t⋅m)2+αy2(b⋅m)2)
以下为UE4中Anisotropic Beckmann分布的Shader实现代码:
// Anisotropic Beckmann
float D_Beckmann_aniso( float ax, float ay, float NoH, float3 H, float3 X, float3 Y )
{
float XoH = dot( X, H );
float YoH = dot( Y, H );
float d = - (XoH*XoH / (ax*ax) + YoH*YoH / (ay*ay)) / NoH*NoH;
return exp(d) / ( PI * ax*ay * NoH * NoH * NoH * NoH );
}
Anisotropic GGX Distribution
各项异性的GGX分布形式如下:
D G G X a n i s o ( m ) = 1 π α x α y 1 ( ( t ⋅ m ) 2 α x 2 + ( b ⋅ m ) 2 α y 2 + ( n ⋅ m ) 2 ) 2 D_{GGXaniso}(m)= \frac{1}{\pi\alpha_x \alpha_y}\frac{1}{(\frac{(t \cdot m)^2}{\alpha_x^2}+\frac{(b \cdot m)^2}{\alpha_y^2}+(n\cdot m)^2)^2} DGGXaniso(m)=παxαy1(αx2(t⋅m)2+αy2(b⋅m)2+(n⋅m)2)21
以下为UE4中Anisotropic GGX分布的Shader实现代码:
// Anisotropic GGX
// [Burley 2012, "Physically-Based Shading at Disney"]
float D_GGXaniso( float ax, float ay, float NoH, float3 H, float3 X, float3 Y )
{
float XoH = dot( X, H );
float YoH = dot( Y, H );
float d = XoH*XoH / (ax*ax) + YoH*YoH / (ay*ay) + NoH*NoH;
return 1 / ( PI * ax*ay * d*d );
}
头发渲染的光照模型,从简单到复杂顺序如下:
目前游戏实时渲染领域处于Scheuermann Model阶段。
这里我们仅介绍目前两种模型:Kajiya-Kay Model 和Marschner Model。
一般的光照模型可以简化为:
在头发光照模型中:
头发的各向异性高光会形成类似天使环,这有别于金属的椭圆形高光。
对于头发可以采用以下的方式strand based anisotropy进行建模,可以将这种微观的细丝看成直径非常小长度很长的圆柱。
摘录自 角色渲染技术——毛发及其他
那么,我们宏观看上看到的这类表面上某一点的光照实际上是一圈圆柱上的点的光照总和。
这一圈点有无数个方向不同的法线,如果要计算光照总体贡献,就需要对这一圈点的入射光和BRDF的乘积进行一个半圆形的积分,并且还要考虑到相应的可见性(遮挡)函数。类似于:
∫ 0 2 π p ( l , v , v ( θ ) ) ∗ L ∗ ( l ⋅ n ( θ ) ) ∗ V ( θ ) ∗ r ∗ d θ \int_{0}^{2\pi} p(l,v,v(\theta)) \ast L \ast (l\cdot n(\theta ))\ast V(\theta ) \ast r \ast d\theta ∫02πp(l,v,v(θ))∗L∗(l⋅n(θ))∗V(θ)∗r∗dθ
其中, p p p是BRDF, L L L是和法线方向无关的入射光强, V ( θ ) V(\theta) V(θ)是和角度相关的可见性。
而 ∫ 0 2 π ( l ⋅ n ( θ ) ) ∗ V ( θ ) ∗ r ∗ d θ \int_{0}^{2\pi}(l\cdot n(\theta ))\ast V(\theta ) \ast r \ast d\theta ∫02π(l⋅n(θ))∗V(θ)∗r∗dθ 很像角色渲染技术——皮肤中提到的bent normal的概念。
进一步,假设可以预积分出bent normal,则上述的公式可以近似为:
k ∗ p ( l , v , n b e n t ) ∗ L ∗ ( l ∗ n b e n t ) k \ast p(l,v,n_{bent}) \ast L \ast (l*n_{bent}) k∗p(l,v,nbent)∗L∗(l∗nbent)
这个 n b e n t n_{bent} nbent就是预积分出来的方向,那么这个方向到底是哪里呢?
由于整个半圆形都在一个法线平面上,如果假设光照方向向量投影在该平面的向量是 n ′ n' n′,那么整个半圆上所有的法线应该对称地分布在 n ′ n' n′两侧,所以 n ′ n' n′和 n b e n t n_{bent} nbent方向应该相同。
假定细丝延伸的方向为 t t t,那么 t 、 n b e n t 、 l t、n_{bent}、l t、nbent、l这三个方向应该共面!并且 n b e n t n_{bent} nbent和 l l l二者弧线垂直,如下图所示。
通过简单的平面三角函数有:
l ∗ n b e n t = c o s θ = s i n ( π 2 − θ ) = 1 − ( l ⋅ t ) 2 l*n_{bent} = cos\theta = sin(\frac{\pi}{2}-\theta)=\sqrt[]{1-(l\cdot t)^2} l∗nbent=cosθ=sin(2π−θ)=1−(l⋅t)2
类似有:
h ∗ n b e n t = 1 − ( h ⋅ t ) 2 h*n_{bent} = \sqrt[]{1-(h\cdot t)^2} h∗nbent=1−(h⋅t)2
因此,我们用细丝方向的延伸t,来代替法线方向 n b e n t n_{bent} nbent来进行各向异性高光的计算。
切线方向他是指头发丝的几何切线。它就是一个向量。和模型切空间的tangent不是相等关系。
切线是垂于法线的一条向量。 但由于有无数条,所以最终通过UV坐标的方向来确定。
可以通过让美术沿着发丝生长的方向去展开模型uv坐标的u或者v的值,使得这个方向恰好就是切线空间(tangent space)中的切线方向(因为切线的方向就是uv坐标中u或者v增长的方向)。
经过前文的推导,不同于常规的光照方程使用的法线进行这里的高光计算。
这里采用的是切线参与高光的光照计算。下图展示了所有需要用到的向量。
在每根头发的固定点处,T总是保持不变的,N是随视线V变化而变化的。
在一个被渲染的点处,其法线在各个视线(View)方向是不同的,这大概所谓就是的“各向异性“!
而我们之所以要有T就是为了计算出这个隐藏在背后的法线N。当然,我们不需要计算出这个N的确切值,其蕴含在T和H的正弦值中,V蕴含在H中。
卡吉雅模型是一种经验模型,对头发高光的一种拟合模型。
该模型解决了以下的问题:
Kajiya-Kay模型,目前大都采用Scheuermann_HairRendering的双层高光模型:
d i f f u s e = K d ∗ l e r p ( 0.25 , 1.0 , n ⋅ l ) diffuse = K_d \ast lerp(0.25,1.0,n \cdot l) diffuse=Kd∗lerp(0.25,1.0,n⋅l)
s p e c u l a r = K s 1 ∗ s m o o t h s t e p ( − 1 , 0 , d o t ( t 1 , h ) ) ∗ s i n ( t 1 , h ) p 1 + K s 2 ∗ s m o o t h s t e p ( − 1 , 0 , d o t ( t 2 , h ) ) ∗ s i n ( t 2 , h ) p 2 specular = K_{s1} * smoothstep(-1,0,dot(t1,h))*sin(t1,h)^{p1} + K_{s2} * smoothstep(-1,0,dot(t2,h))*sin(t2,h)^{p2} specular=Ks1∗smoothstep(−1,0,dot(t1,h))∗sin(t1,h)p1+Ks2∗smoothstep(−1,0,dot(t2,h))∗sin(t2,h)p2
这里是对原始模型的改进。
如下图所示,可以看出发白的一层是没有颜色的。另一层发黄的是有颜色的高光。
若使用相同的切线,两层高光必定重合,为了让双层高光错开,使用shiftedT(对切线在法线方向去偏移)。
T s h i f t e d = T + s h i f t ∗ N T_{shifted} = T + shift * N Tshifted=T+shift∗N
对于两层高光,shift值应该设置不同,但如果对于同一层高光来说。shift是一个固定的值,会导致头发光照区域边缘非常齐整,没有锯齿感。
很多漫画中角色头发上的高光区域的边缘就有很明显地不规则状锯齿,也是对现实世界的观察。
为了要设置为不同值,因此可以使用一张切线扰动图。
从而对 s h f i t shfit shfit值修改:
s h i f t = c o n s t S h i f t + ( t e x t u r e ( s h i f t T e x , u v ) − 0.5 ) shift = constShift + (texture(shiftTex,uv)-0.5) shift=constShift+(texture(shiftTex,uv)−0.5)
这里为什么要-0.5呢?
因为纹理采样出的值范围为[0,1],减去-0.5变为[-0.5,0.5],有正有负,可以让T向N或N的反方向随机偏移。因此实现W型高光。
两个主要的函数:
// 偏移切线
float3 ShiftTangent(float3 T, float3 N, float shift)
{
float3 shiftedT = T + (shift * N);
return normalize(shiftedT);
}
// 发丝高光计算函数
float StrandSpecular(float3 T, float3 V, float L, float exponent)
{
float3 H = normalize(L + V);
float dotTH = dot(T, H);
float sinTH = sqrt(1.0 - dotTH*dotTH);
float dirAtten = smoothstep(-1.0, 0.0, dot(T, H));
return dirAtten * pow(sinTH, exponent);
}
其中,对于方向衰减系数dirAtten的计算:
float dirAtten = smoothstep(-1., 0., dotTH);
对于为什么要这样计算,对各向异性高光的理解作出了解释:
方向衰减系数涉及到两个方向,一个是光源的方向L,一个是视线方向V,它们都蕴含在H中。
smoothstep
可以用来生成0到1的平滑过渡值,它也叫平滑阶梯函数。
smoothstep
的第一个参数为-1,表面dotTH小于或等于-1时候,dirAtten值为0。(dotTH最小为-1,不可能再小了)。
什么时候dotTH为-1呢?
smoothstep
的第一个参数为0,表明当dotTH大于或等于0时,dirAtten值为1。
注意,dotTH最大值为1,此时T和H的方向恰好相同,两者之间的夹角为0度。
所以,smoothstep(-1., 0., dotTH)
的作用:取和切线T角夹在0至180度之间的H。
当T和H之间的夹角不在这个范围内时,必然出现H和对应N的点积小于0,即此时高光为0 (此时光源照不到当前着色点或相机看不到当前着色点),此时dirAtten的值就应当为0(即没有高光)。
头发效果研究笔记 基于Kajiya-Kay Model通过不同贴图和参数调节调整了许多效果,可以进行查看学习。
使用Kajiya-Kay模型的头发渲染 中提到了除了使用Kajiya-Kay模型。
还提到,可以使用GGX各向异性法线分布函数和Ward各向异性法线分布函数。
float GGXAnisotropicNormalDistribution(float anisotropic, float roughness, float NdotH, float HdotX, float HdotY, float SpecularPower, float c)
{
float aspect = sqrt(1.0 - 0.9 * anisotropic);
float roughnessSqr = roughness * roughness;
float NdotHSqr = NdotH * NdotH;
float ax = roughnessSqr / aspect;
float ay = roughnessSqr * aspect;
float d = HdotX * HdotX / (ax * ax) + HdotY * HdotY / (ay * ay) + NdotHSqr;
return 1 / (3.14159 * ax * ay * d * d);
}
float sqr(float x) {
return x * x;
}
float WardAnisotropicNormalDistribution(float anisotropic, float NdotL, float NdotV, float NdotH, float HdotX, float HdotY) {
float aspect = sqrt(1.0h - anisotropic * 0.9h);
float roughnessSqr = (1 - 0.5);
roughnessSqr *= roughnessSqr;
float X = roughnessSqr / aspect;
float Y = roughnessSqr * aspect;
float exponent = -(sqr(HdotX / X) + sqr(HdotY / Y)) / sqr(NdotH);
float Distribution = 1.0 / (4.0 * 3.14159265 * X * Y * sqrt(NdotL * NdotV));
Distribution *= exp(exponent);
return Distribution;
}
记·天刀手游角色渲染分析中就拆解头发的渲染实现,采用GGX各向异性高光,分别以H、V、V的偏移三个参数分三次计算高光和漫反射并叠加。
笔者也截帧过天刀手游,发现确实如上述实现方式。
小结:
基于Kajiya-Kay模型的毛发渲染至今仍然在游戏中大量的使用。
它的优点是计算性能也比较好,缺点是整个模型仍然是一个经验公式,并不太符合近年来基于物理渲染的概念,也无法做到能量守恒。
效果展示
基于Kajiya Kay模型:
漫反射
float NdotL = dot(N,L);
float halfLambert = (NotL+1)/2;
halfLambert = lerp(0.25,1,halfLambert);
float3 diffuse = albedoTexture.rgb * BaseColor * halfLambert;
各向異性高光
各向异性高光的计算和采用的就是3.1.2中介绍的两层高光模型。
通过控制输入StrandSpecular
函数的Power值从而实现形状的控制。
头发上每个点都有一个PowerOffset,来控制形状,通过以下CalculatePower
映射成为一个Power值。
float CalculatePower(float PowerBegin,float PowerEnd,float PowerOffset,float PowerScale)
{
float newPower = PowerEnd + (PowerBegin-PowerEnd) * PowerOffset;
return PowerScale * newPower;
}
同理,对切线方向的偏移值shift也可以加入这种细节控制。
当然,还有Mask贴图控制高光出现的位置。
总的来说,Kajiya-Kay不是基于物理的,不是能量守恒的,仅仅是一个经验模型。
Marschner是基于物理的毛发渲染模型,是Stephen R. Marschner等人共同发表的论文《Light Scattering from Human Hair Fibers》内的方法。
其基于头发纤维的结构,进行了一个较为准确的物理分析得到的计算方法。
头发纤维从微观角度来看,实际上是一个多层的结构,它的最外层像鳞片一样(很像微表面理论中看到的粗糙的表面)。
光线在穿过层层头发纤维内部的过程中也会发生折射,反射等,因此我们看到的最终头发呈现的颜色实际上是多条光路综合作用的结果。
基于为头发纤维的物理结构,抽象出如下图所示的光照模型:
其中,作者考虑了光照贡献最大的三条光路:R,TT,TRT。(R表示反射,T表示透射)。
表皮鳞片的倾斜会让射线R(反)和射线TRT(透反透)稍微偏离理想镜面反射方向。二者稍稍错开,形成两个视觉上可区分的亮点。
R是由于表面高光反射,因此R为白色。而TRT是由穿过头发内进过吸收反射出的,因此有纤维的颜色。这也解释了为什么Kajiya-Kay的两个反射区及颜色。
TT(透透)是两次透射。
具体的过程请参考 虚幻4渲染编程(人物篇)【第二卷:Human Hair Shading】。
这里直接写一下推导结果。
由在三维空间处理头发光线的几何问题比较困难,将头发切成横向和纵向的。
根据光路分析,又结合光路可能即包含纵向散射,又包含横向散射,因此得到头发上某一点的BSDF:
S = ∑ p S p S=\sum_{p}^{}S_p S=p∑Sp
其中, S p = M p ∗ N p S_p = M_p \ast N_p Sp=Mp∗Np, p = R , T R T , T T p=R,TRT,TT p=R,TRT,TT。M为纵向散射函数,N为方位散射函数。
纵向散射函数M
在实际中,使用一个标准差为 β \beta β标准高斯函数。
模型参数集合如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0ZzABjW-1624366548642)(images/18-Model-M-param.png)]
g ( σ , x − μ ) = 1 2 π σ 2 e − ( x − μ ) 2 2 σ 2 g(\sigma ,x-\mu )=\frac{1}{2\pi \sigma ^2}e^{-\frac{(x-\mu)^2}{2\sigma^2} } g(σ,x−μ)=2πσ21e−2σ2(x−μ)2
可以得到:
M R ( θ h ) = g ( β R , θ h − α R ) M_R(\theta _h) = g(\beta_R,\theta_h-\alpha_R) MR(θh)=g(βR,θh−αR)
M T T ( θ h ) = g ( β T T , θ h − α T T ) M_{TT}(\theta _h) = g(\beta_{TT},\theta_h-\alpha_{TT}) MTT(θh)=g(βTT,θh−αTT)
M T R T ( θ h ) = g ( β T R T , θ h − α T R T ) M_{TRT}(\theta _h) = g(\beta_{TRT},\theta_h-\alpha_{TRT}) MTRT(θh)=g(βTRT,θh−αTRT)
方位散射函数N
RR和TRT方位角散射函数分别简化为 cos 2 ϕ \cos^2 \phi cos2ϕ,TT方位角散射函数满足 ϕ \phi ϕ符合高斯分布,公式分别如下:
N R = c o s 2 ϕ N_{R}=cos^2 \phi NR=cos2ϕ
N T T = g ( γ , π − ϕ ) N_{TT}=g(\gamma,\pi - \phi) NTT=g(γ,π−ϕ)
N T R T = c o s 2 ϕ N_{TRT}=cos^2 \phi NTRT=cos2ϕ
虚幻引擎中的Hair着色模型采用的正是Marschner Model。
在材质编辑器中将着色模型改为Hair。
Hair模式的材质输入节点与普通模式有所不同。
Metallic变成了Scatter,控制着散射强度。
Normal 变成了Tangent。这个切线是发丝的几何切线方向(指向发根)。
Custom Data 0 变成了 Backlit。这个输入本应该是控制透光度,但是实际上并没有用到。
Opacity Mask,因为选了 Masked混合模式,所以有这个选项。可以使用 DitherTemporalAA 节点将半透明的部分变成网点,配合 TemporalAA 产生半透明的效果。虽然效果比真正的半透明差一点,但是不会产生排序问题。
UE头发材质的着色器代码
ShadingModels.ush的HairBxDF函数:
FDirectLighting HairBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
const float3 BsdfValue = HairShading(GBuffer, L, V, N, Shadow.TransmissionShadow, Shadow.HairTransmittance, 1, 0, uint2(0, 0));
FDirectLighting Lighting;
Lighting.Diffuse = 0;
Lighting.Specular = 0;
Lighting.Transmission = AreaLight.FalloffColor * Falloff * BsdfValue;
return Lighting;
}
HairShading位于HairBsdf.ush文件。
// Hair BSDF
// Approximation to HairShadingRef using concepts from the following papers:
// [Marschner et al. 2003, "Light Scattering from Human Hair Fibers"]
// [Pekelis et al. 2015, "A Data-Driven Light Scattering Model for Hair"]
float3 HairShading( FGBufferData GBuffer, float3 L, float3 V, half3 N, float Shadow, FHairTransmittanceData HairTransmittance, float InBacklit, float Area, uint2 Random )
{
// ...省略源码
}
注:着色器中的N指的是实际为Tangent。// N is the vector parallel to hair pointing toward root
让我们具体的看下代码:
R、TT、TRT中的M项
公式中M项采用g,即标准高斯函数进行模拟。
期望中角度的偏移如下:
float Alpha[] =
{
-Shift * 2,
Shift,
Shift * 4,
};
其中,R使用的是Shift,TT使用的是Alpha[1],TRT使用的是Alpha[2]。
方差使用与粗糙度相关的值。
float ClampedRoughness = clamp(GBuffer.Roughness, 1/255.0f, 1.0f);
float B[] =
{
Area + Pow2(ClampedRoughness),
Area + Pow2(ClampedRoughness) / 2,
Area + Pow2(ClampedRoughness) * 2,
};
其中,R使用的是B[0],TT使用的是B[1],TRT使用的是B[2]。Area固定为0。roughness值越小则反射越集中强烈,值越大则越分散。
三个项中的M分别计算如下:
// 对于R:
float Mp = Hair_g(B[0] * BScale, SinThetaL + SinThetaV - Shift);
// 对于TT:
float Mp = Hair_g( B[1], SinThetaL + SinThetaV - Alpha[1] );
// 对于TRT:
float Mp = Hair_g( B[2], SinThetaL + SinThetaV - Alpha[2] );
其中, θ h \theta_h θh为SinThetaL + SinThetaV,该值和法平面和入射光线、摄像机方向的夹角有关。
R、TT、TRT中的N项
UE的头发对N项也是尽可能拟合。
R中的N:
float Np = 0.25 * CosHalfPhi;
CosHalfPhi,是方位角差的半角,计算如下:
const float CosPhi = dot(Lp,Vp) * rsqrt( dot(Lp,Lp) * dot(Vp,Vp) + 1e-4 );
const float CosHalfPhi = sqrt( saturate( 0.5 + 0.5 * CosPhi ) );
TT项的N:
float Np = exp( -3.65 * CosPhi - 3.98 );
TRT项的N:
float Np = exp( 17 * CosPhi - 16.78 );
R、TT、TRT中的其他部分
对于R:
// Mp:纵向散射M。
float Mp = Hair_g(B[0] * BScale, SinThetaL + SinThetaV - Shift);
// Np:方位角散射N。
float Np = 0.25 * CosHalfPhi;
// Fp: 菲涅尔项。
float Fp = Hair_F(sqrt(saturate(0.5 + 0.5 * VoL)));
// GBuffer.Specular实际含义是Scatter,控制着散射强度。
S += Mp * Np * Fp * (GBuffer.Specular * 2) * lerp(1, Backlit, saturate(-VoL));
对于TT:
S += Mp * Np * Fp * Tp * Backlit;
对于TRT:
S += Mp * Np * Fp * Tp;
头发渲染的前世今生:如何在游戏中拥有一头迷人秀发? 总结了一些制作技巧,对头发渲染的排序问题,Shader的优化。
下面进行一部分简单的笔记记录。
头发建模可分为两种:基于引导线、基于面片。
左图为基于引导线,右图为基于面片。
实时基于线的头发渲染是非常困难:
目前主流的hairwork也是将头发变成面片模型。
手游制作头发主要分为2步骤:
制作头发常用的贴图有以下几种:
在头发渲染中,深度排序(approximate depth sorting)是一个难题。
毛发这种边缘半透明,且双面的材质是不能使用简单的alphaTest来渲染的。
因此这样会使得毛发边缘看起来很粗糙。所以应该尽可能保证顺序正确使用alphaBlend(从后向前的顺序绘制才可以正确进行AlphaBlend)。
优化的头发渲染排序方案如题所示,共分为4个Pass:
Pass1: 绘制不透明的部分(写深度)
Pass2: 绘制不透明的部分(渲染前面的不透明部分)
Pass3:绘制半透明的背面部分
Pass4:绘制半透明的前面部分
Pass1和Pass2原是可以合并在一起做的。之所以拆分为先写深度,再写颜色。是希望利用硬件的Early-Z Culling特性。
除此之外,关闭Color buffer的写入只修改Z buffer,会比同时写入Color buffer和Z buffer快2-4倍。
这也是常说的使用Z-Prepass的前向渲染管线的原理。
着色模型简介和实现(上)
【基于物理的渲染(PBR)白皮书】(四)法线分布函数相关总结
角色渲染技术——毛发及其他
Scheuermann_HairRendering
对各向异性高光的理解
头发效果研究笔记
记·天刀手游角色渲染分析
剖析Unreal Engine超真实人类的渲染技术Part 3 - 毛发渲染及其它
头发渲染的前世今生:如何在游戏中拥有一头迷人秀发?
虚幻4渲染编程(人物篇)【第二卷:Human Hair Shading】
UE4 毛发渲染参考文章
UE4 Hair Shading