目錄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官方團隊面前,虛擬數字人探索的腳步會一直向前邁進,爲實時渲染領域拿下一個又一個里程碑。
本系列文章完!