目录html
毛发渲染一直是实时图形学的难题,由于其光照复杂,数量众多,物理效果很差抽象等。在早期,只能经过若干面片代替,后来随着硬件及渲染技术的提高,慢慢发展出了经验模型的Kajiya-Kay和基于物理的Marschner毛发渲染模型。Mike采用的是Marschner毛发渲染模型。git
真实世界的毛发主要由纤维构造,也可分红多层结构,有中心的发髓(Medulla)、内部的皮质(Cortex)和表皮的角质层(Cuticle)构成。(下图)api
毛发剖面图app
其中角质层放大后,可见坑坑洼洼的微表面(下图),它是形成高光和反射的介质。此外,光线照射毛发表皮以后,还会发生透射和次反射。dom
毛发放大数千倍后的微表面wordpress
毛发微表面的坑洼具备较统一的指向性,由根部指向尾部,在图形学可用切线及各向异性属性来衡量这一现象。函数
简化后的毛发模型性能
Marschner是基于物理的毛发渲染模型,是Stephen R. Marschner等人共同发表的论文《Light Scattering from Human Hair Fibers》内的方法。学习
该方法研究分析了真实世界的毛发构成及特性,抽象出以下图所示的光照模型:ui
毛发对应的横截面光照模型图:
该模型将光照在毛发的做用分红3部位:
基于以上光照模型,论文又进一步根据几何光学分析了光线在某一个光路上的行为,并把这个行为具体的分红了两类,即纵向散射(longitudinal scattering)和方位角散射(azimuthal scattering)。
差角度计算以下:
\(\theta_d = (\theta_r - \theta_i) /2\)
\(\phi = (\phi_r - \phi_i)\)
半角度计算以下:
\(\theta_h = (\theta_r - \theta_i) /2\)
\(\phi_h = (\phi_r - \phi_i) /2\)
\(R\),\(TT\),\(TRT\)三种散射纵向散射函数\(M\)都知足\(\theta_h\)符合高斯分布。公式以下:
\(M_R = g(\beta_R, \alpha_R, \theta_h)\)
\(M_{TT} = g(\beta_{TT}, \alpha_{TT}, \theta_h)\)
\(M_{TRT} = g(\beta_{TRT}, \alpha_{TRT}, \theta_h)\)
\(R\)和\(TRT\)散射方位角散射函数\(N\)分别简化为$\cos^2 \phi \(,\)TT\(散射方位角散射函数\)N\(知足\)\phi$ 符合高斯分布。公式以下:
\(N_R= \cos^2\phi\)
\(N_{TT} = g(\gamma_{TT}, 0.0, \pi - \phi)\)
\(N_{TRT} = \cos^2\phi\)
最终散射公式以下:
\(S = S_R + S_{TT} + S_{TRT}\)
\(S_P = M_P \cdot N_P, \ \ for \ P = R, TT, TRT\)
利用以上渲染技术能够渲染出Mike的直接光照部分:
不一样灯光角度下的Mike毛发渲染效果
毛发除了上一小节描述的直接光照外,还须要增长非直接光照,以模拟环境光或漫反射。
出于性能的考虑,UE4默认给头发加了一个相似于diffuse的fake scattering (非物理真实的散射)的散射的间接光照。渲染结果以下图:
增长了非物理真实的间接光照的效果
UE4采用的是Dual Scattering(双向散射)的多散射近似光照模型,论文出处:Dual Scattering Approximation for Fast Multiple Scattering in Hair。和离线光线跟踪毛发间接采样方法相比,双向散射会节省大量时间,质量几乎接近。
双向散射主要用于估计毛发的多散射函数,这个函数有两个部分组成:
这两种贡献的总和称为双向多散射。这种计算模型不受光源数量和类型的限制。
如上图所示,可得到以下的抽象公式:
\[ \Psi(x,\omega_d,\omega_i) = \Psi^G(x,\omega_d,\omega_i)(1 + \Psi^L(x,\omega_d,\omega_i)) \]
毛发光照(包含直接光照和间接光照)实现的伪代码:
更具体的推导和实现过程请参看参考论文,也可参考这篇技术文章:Real-Time Hair Simulation and Rendering。
UE实现毛发的shader代码主要在:
Light Scattering from Human Hair Fibers论文给出了下面一组测量的标准值,后面的源码中大量涉及这些常量或计算公式:
下面着手分析毛发的光照着色源码:
// 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, float Backlit, float Area, uint2 Random ) { // to prevent NaN with decals // OR-18489 HERO: IGGY: RMB on E ability causes blinding hair effect // OR-17578 HERO: HAMMER: E causes blinding light on heroes with hair float ClampedRoughness = clamp(GBuffer.Roughness, 1/255.0f, 1.0f); //const float3 DiffuseN = OctahedronToUnitVector( GBuffer.CustomData.xy * 2 - 1 ); //const float Backlit = GBuffer.CustomData.z; #if HAIR_REFERENCE // todo: ClampedRoughness is missing for this code path float3 S = HairShadingRef( GBuffer, L, V, N, Random ); //float3 S = HairShadingMarschner( GBuffer, L, V, N ); #else // N is the vector parallel to hair pointing toward root const float VoL = dot(V,L); const float SinThetaL = dot(N,L); const float SinThetaV = dot(N,V); float CosThetaD = cos( 0.5 * abs( asinFast( SinThetaV ) - asinFast( SinThetaL ) ) ); //CosThetaD = abs( CosThetaD ) < 0.01 ? 0.01 : CosThetaD; const float3 Lp = L - SinThetaL * N; const float3 Vp = V - SinThetaV * N; 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 ) ); //const float Phi = acosFast( CosPhi ); // 下面不少初始化的值都是基于上面给出的表格得到 float n = 1.55; // 毛发的折射率 //float n_prime = sqrt( n*n - 1 + Pow2( CosThetaD ) ) / CosThetaD; float n_prime = 1.19 / CosThetaD + 0.36 * CosThetaD; // 对应R、TT、TRT的longitudinal shift float Shift = 0.035; float Alpha[] = { -Shift * 2, Shift, Shift * 4, }; // 对应R、TT、TRT的longitudinal width float B[] = { Area + Pow2( ClampedRoughness ), Area + Pow2( ClampedRoughness ) / 2, Area + Pow2( ClampedRoughness ) * 2, }; float3 S = 0; // 下面各份量中的Mp是纵向散射函数,Np是方位角散射函数,Fp是菲涅尔函数,Tp是吸取函数 // 反射(R)份量 if(1) { const float sa = sin( Alpha[0] ); const float ca = cos( Alpha[0] ); float Shift = 2*sa* ( ca * CosHalfPhi * sqrt( 1 - SinThetaV * SinThetaV ) + sa * SinThetaV ); float Mp = Hair_g( B[0] * sqrt(2.0) * CosHalfPhi, SinThetaL + SinThetaV - Shift ); float Np = 0.25 * CosHalfPhi; float Fp = Hair_F( sqrt( saturate( 0.5 + 0.5 * VoL ) ) ); S += Mp * Np * Fp * ( GBuffer.Specular * 2 ) * lerp( 1, Backlit, saturate(-VoL) ); } // 透射(TT)份量 if(1) { float Mp = Hair_g( B[1], SinThetaL + SinThetaV - Alpha[1] ); float a = 1 / n_prime; //float h = CosHalfPhi * rsqrt( 1 + a*a - 2*a * sqrt( 0.5 - 0.5 * CosPhi ) ); //float h = CosHalfPhi * ( ( 1 - Pow2( CosHalfPhi ) ) * a + 1 ); float h = CosHalfPhi * ( 1 + a * ( 0.6 - 0.8 * CosPhi ) ); //float h = 0.4; //float yi = asinFast(h); //float yt = asinFast(h / n_prime); float f = Hair_F( CosThetaD * sqrt( saturate( 1 - h*h ) ) ); float Fp = Pow2(1 - f); //float3 Tp = pow( GBuffer.BaseColor, 0.5 * ( 1 + cos(2*yt) ) / CosThetaD ); //float3 Tp = pow( GBuffer.BaseColor, 0.5 * cos(yt) / CosThetaD ); float3 Tp = pow( GBuffer.BaseColor, 0.5 * sqrt( 1 - Pow2(h * a) ) / CosThetaD ); //float t = asin( 1 / n_prime ); //float d = ( sqrt(2) - t ) / ( 1 - t ); //float s = -0.5 * PI * (1 - 1 / n_prime) * log( 2*d - 1 - 2 * sqrt( d * (d - 1) ) ); //float s = 0.35; //float Np = exp( (Phi - PI) / s ) / ( s * Pow2( 1 + exp( (Phi - PI) / s ) ) ); //float Np = 0.71 * exp( -1.65 * Pow2(Phi - PI) ); float Np = exp( -3.65 * CosPhi - 3.98 ); // Backlit是背光度,由材质提供。 S += Mp * Np * Fp * Tp * Backlit; } // 次反射(TRT)份量 if(1) { float Mp = Hair_g( B[2], SinThetaL + SinThetaV - Alpha[2] ); //float h = 0.75; float f = Hair_F( CosThetaD * 0.5 ); float Fp = Pow2(1 - f) * f; //float3 Tp = pow( GBuffer.BaseColor, 1.6 / CosThetaD ); float3 Tp = pow( GBuffer.BaseColor, 0.8 / CosThetaD ); //float s = 0.15; //float Np = 0.75 * exp( Phi / s ) / ( s * Pow2( 1 + exp( Phi / s ) ) ); float Np = exp( 17 * CosPhi - 16.78 ); S += Mp * Np * Fp * Tp; } #endif if(1) { // Use soft Kajiya Kay diffuse attenuation float KajiyaDiffuse = 1 - abs( dot(N,L) ); float3 FakeNormal = normalize( V - N * dot(V,N) ); //N = normalize( DiffuseN + FakeNormal * 2 ); N = FakeNormal; // Hack approximation for multiple scattering. float Wrap = 1; float NoL = saturate( ( dot(N, L) + Wrap ) / Square( 1 + Wrap ) ); float DiffuseScatter = (1 / PI) * lerp( NoL, KajiyaDiffuse, 0.33 ) * GBuffer.Metallic; float Luma = Luminance( GBuffer.BaseColor ); float3 ScatterTint = pow( GBuffer.BaseColor / Luma, 1 - Shadow ); S += sqrt( GBuffer.BaseColor ) * DiffuseScatter * ScatterTint; } S = -min(-S, 0.0); return S; }
从上面可知,先算出R、TT、TRT的各个份量的函数系数,将它们的光照贡献量相加,最后采用Kajiya Kay漫反射模型和多散射近似法模拟漫反射部分。
本节将剖析Mike用到的毛发材质,它们的材质有个共同点:都是用了Hair的着色模型(下图)。
下图是头发(M_Hair)的总览图。
基础色(Base Color)
首先是下图模拟了头发中心偏亮、边缘渐变变暗的效果。(下图)
模拟的头发渐变效果以下图。
下图所示的Scalp Variation部分是提取靠近头皮(即头发根部)的UV纹理,而后去采样噪点纹理,生成一张有随机变化的遮罩图:
Hair Albedo部分主要是模拟了发根到发伟的颜色渐变,其中发根处利用颜色遮罩hair_color_mask
更好地将发根颜色融入头皮。
颜色混合最后阶段,将加入边沿色和环境遮挡色,使得头发颜色最终呈现出逼真的效果。
须要注意的是,头发的顶点色大部分是黄色,小部分是白色(下图)。
散射(Scatter)
对于Hair着色模型,才有此属性,以模拟头发的漫反射颜色及强度。实现方法就是将头发边缘色乘以一个缩放因子。(下图)
粗糙度(Roughness)
粗糙度的计算也不复杂,将基础色涉及的Scalp Variation部分输出的结果做为线性插值Alpha,在最大和最小值之间过渡,再通过一个缩放因子,便可获得最终结果。
切线(tangent)
利用基础色涉及的Scalp Variation部分的结果和采样噪点图,生成纹理V方向上有随机变化纹路的切线数据,以模拟头发的微平面。
背光度(Backlit)
背光度主要是控制头发着色过程透射(TT)部分(参见4.2 毛发的底层实现)的缩放。
由UV集合2控制的贴图经由反向和阴影缩放,便可获得数据。
此外,还有顶点坐标偏移、AO等数据,这些将忽略其分析,有兴趣的读者可自行查看材质。
头发模糊材质主要是在头发根部加入模糊效果,而且添加像素深度偏移,使得头发更好地“植入”头皮,过渡更天然。(下图)
其实现的核心是采样像素周边16个场景颜色的点,作平均计算,模拟高斯模糊的结果。(下图)
眉毛和睫毛的材质跟头发的材质很是接近,可参看上一小节。
绒毛是很容易被忽略的渲染细节,只有在镜头很近时才能发现。但实际上Mike的整个身体被绒毛所包围,这能够提高人物皮肤的细节和渲染真实度:
黄色区域所示即是绒毛,可见绒毛在Mike身上遍地开花
来一张近处特写:
它的材质采用透明混合、无光照着色模式。
颜色计算跟以前的毛发有点相似,先对周边场景颜色进行模糊,通过明暗度调整、边缘亮度调整,得到最终颜色。此外,也采用了位置偏移。(下图)
除了皮肤、眼睛、头发等重要部位的渲染,Mike的其它部分的渲染也一样注重细节。
舌头也采用了次表面散射着色模型。
对于颜色,在一张漫反射和亮度反射图中作插值,通过饱和度调整和颜色亮度调整,得到最终颜色和自发光颜色。
对于法线,在一张基础贴图之上,混合了微观细节法线。
对于牙齿,为了反映其相似玉石的散射效果(下图),也一样采用了次表面散射着色模型。
它的材质总览图以下:
对于颜色,在牙齿基础色和模糊后的柔色之间插值混合,结果若干次亮度、饱和度及色调(TeethTint)变换,获得中间色,再加入菲涅尔效应的边缘色,得到最终色。
对于高光,利用法线和视线向量求得一个与视角相关的因子,以便调整高光度,使得与反射向量越接近的像素高光越强。
对于粗糙度和次表面散射强度,利用AO遮罩图通过数次调整后得到。
对于法线,跟舌头相似,在一张基础贴图之上,混合了微观细节法线。
衣服启用了Masked
混合模式和Cloth
着色模型,采用了多层材质,背景层是衣服自己的材质,第二层是纽扣材质(下图)。
对于衣服自己的材质,颜色利用一张灰度图乘以指定色,再通过一系列调整得到,这种变色也是游戏领域常采用的变色方案。优势是可控制材质的明暗度和颜色,缺点是只能有单一的色相,不能有多种色相。衣服的法线也是采用两层贴图混合而成。此外,还设置了次表面散射颜色(SubsurfaceColor)、清漆(ClearCoat)、AO等属性。
对于纽扣材质,很是简单,此处忽略。
首先分析场景的布灯。人物左前方斜45度角是主灯,提供了摄影界经常使用的伦勃朗式的光照和阴影;角色正前方提供了一个补光灯,下降面部的阴影浓度;角色右边有一个侧灯,提供脸部和身体的侧面轮廓,提升质感;角色后方有两个背景灯,用以照亮背景和头发,使头发更具层次感,也能体现头发和耳朵的次表面散射和透射效果。(下图)
其中,主灯由蓝图动态建立而成,相似若干个聚光灯组成的灯阵,模拟很大的柔光灯,提供角色的主要光源以及眼神光。(下图)
上:由若干盏聚光灯组成的灯阵;下:眼神高光反馈的灯阵形状。
此外,场景提供了体积雾,而且配以一个点光源,模拟天然过渡的背景效果。(下图)
本系列文章牢牢围绕着Unreal的官方数字人类《Meet Mike》的角色进行渲染技术的剖析,它们涉及的技术点以下:
能达到如此逼真的渲染效果,总结起来,主要有如下缘由:
基于物理的光照模型
基于真人扫描的模型
基于物理和摄影艺术的场景灯光
高度定制的材质
就Mike而言,虽然渲染效果已经逼近真实,但也存在一些问题:
毛发没有物理效果。
材质非全部场景的灯光都能适应。在某些场景,渲染出来的角色效果存在失真现象。
驱动效果不够流畅(从发布的视频得出结论)。
固然,在后续的Siren项目中,以上有些问题获得解决或缓解。
相信在强大的UE官方团队面前,虚拟数字人探索的脚步会一直向前迈进,为实时渲染领域拿下一个又一个里程碑。
本系列文章完!