目錄
四、毛髮渲染
4.1 毛髮的構造及渲染技術
毛髮渲染一直是實時圖形學的難題,因為其光照複雜,數量眾多,物理效果不好抽象等。在早期,只能通過若干面片代替,後來隨著硬體及渲染技術的提升,慢慢發展出了經驗模型的Kajiya-Kay和基於物理的Marschner毛髮渲染模型。Mike採用的是Marschner毛髮渲染模型。
4.1.1 毛髮的構造
真實世界的毛髮主要由纖維構造,也可分成多層結構,有中心的發髓(Medulla)、內部的皮質(Cortex)和表皮的角質層(Cuticle)構成。(下圖)
毛髮剖面圖
其中角質層放大後,可見坑坑窪窪的微表面(下圖),它是造成高光和反射的介質。此外,光線照射毛髮表皮之後,還會發生透射和次反射。
毛髮放大數千倍後的微表面
毛髮微表面的坑窪具有較統一的指向性,由根部指向尾部,在圖形學可用切線及各向異性屬性來衡量這一現象。
簡化後的毛髮模型
4.1.2 Marschner毛髮渲染模型
Marschner是基於物理的毛髮渲染模型,是Stephen R. Marschner等人共同發表的論文《Light Scattering from Human Hair Fibers》內的方法。
該方法研究分析了真實世界的毛髮構成及特性,抽象出如下圖所示的光照模型:
毛髮對應的橫截面光照模型圖:
該模型將光照在毛髮的作用分成3部位:
- 反射(R):表面的反射,產生主高光,受毛髮切線和各向異性影響。
- 傳輸-傳輸(TT):傳輸-傳輸路線,光線照射並穿透毛囊,然後從另一邊照射出去。這是光線在一定髮量中的散射過程。
- 傳輸-反射-傳輸(TRT):光線進入毛囊,從內表面邊界反射出來,然後再照射出來。產生的是次高光。
基於以上光照模型,論文又進一步根據幾何光學分析了光線在某一個光路上的行為,並把這個行為具體的分成了兩類,即縱向散射(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毛髮渲染效果
4.1.3 毛髮的間接光照
毛髮除了上一小節描述的直接光照外,還需要增加非直接光照,以模擬環境光或漫反射。
出於效能的考慮,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。
4.2 毛髮的底層實現
UE實現毛髮的shader程式碼主要在:
- \Engine\Shaders\Private\ShadingModels.ush。
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漫反射模型和多散射近似法模擬漫反射部分。
4.3 毛髮的材質解析
本節將剖析Mike用到的毛髮材質,它們的材質有個共同點:都是用了Hair的著色模型(下圖)。
4.3.1 頭髮(M_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等資料,這些將忽略其分析,有興趣的讀者可自行檢視材質。
4.3.2 頭髮模糊(M_HairBlur)
頭髮模糊材質主要是在頭髮根部加入模糊效果,並且新增畫素深度偏移,使得頭髮更好地“植入”頭皮,過渡更自然。(下圖)
其實現的核心是取樣畫素周邊16個場景顏色的點,做平均計算,模擬高斯模糊的結果。(下圖)
4.3.3 眉毛和睫毛(M_Lashes、M_Brows)
眉毛和睫毛的材質跟頭髮的材質非常接近,可參看上一小節。
4.3.4 絨毛(M_Fuzz)
絨毛是很容易被忽略的渲染細節,只有在鏡頭很近時才能發現。但實際上Mike的整個身體被絨毛所包圍,這可以提升人物皮膚的細節和渲染真實度:
黃色區域所示便是絨毛,可見絨毛在Mike身上遍地開花
來一張近處特寫:
它的材質採用透明混合、無光照著色模式。
顏色計算跟之前的毛髮有點類似,先對周邊場景顏色進行模糊,經過明暗度調整、邊緣亮度調整,獲得最終顏色。此外,也採用了位置偏移。(下圖)
五、其它部位
除了皮膚、眼睛、頭髮等重要部位的渲染,Mike的其它部分的渲染也同樣注重細節。
5.1 舌頭
舌頭也採用了次表面散射著色模型。
對於顏色,在一張漫反射和亮度反射圖中做插值,經過飽和度調整和顏色亮度調整,獲得最終顏色和自發光顏色。
對於法線,在一張基礎貼圖之上,混合了微觀細節法線。
5.2 牙齒
對於牙齒,為了反映其類似玉石的散射效果(下圖),也同樣採用了次表面散射著色模型。
它的材質總覽圖如下:
對於顏色,在牙齒基礎色和模糊後的柔色之間插值混合,結果若干次亮度、飽和度及色調(TeethTint)變換,得到中間色,再加入菲涅爾效應的邊緣色,獲得最終色。
對於高光,利用法線和視線向量求得一個與視角相關的因子,以便調整高光度,使得與反射向量越接近的畫素高光越強。
對於粗糙度和次表面散射強度,利用AO遮罩圖經過數次調整後獲得。
對於法線,跟舌頭類似,在一張基礎貼圖之上,混合了微觀細節法線。
5.3 衣服
衣服啟用了Masked
混合模式和Cloth
著色模型,採用了多層材質,背景層是衣服本身的材質,第二層是鈕釦材質(下圖)。
對於衣服本身的材質,顏色利用一張灰度圖乘以指定色,再經過一系列調整獲得,這種變色也是遊戲領域常採用的變色方案。優點是可控制材質的明暗度和顏色,缺點是隻能有單一的色相,不能有多種色相。衣服的法線也是採用兩層貼圖混合而成。此外,還設定了次表面散射顏色(SubsurfaceColor)、清漆(ClearCoat)、AO等屬性。
對於鈕釦材質,非常簡單,此處忽略。
5.4 燈光
首先分析場景的布燈。人物左前方斜45度角是主燈,提供了攝影界常用的倫勃朗式的光照和陰影;角色正前方提供了一個補光燈,降低面部的陰影濃度;角色右邊有一個側燈,提供臉部和身體的側面輪廓,提高質感;角色後方有兩個背景燈,用以照亮背景和頭髮,使頭髮更具層次感,也能體現頭髮和耳朵的次表面散射和透射效果。(下圖)
其中,主燈由藍圖動態建立而成,類似若干個聚光燈組成的燈陣,模擬很大的柔光燈,提供角色的主要光源以及眼神光。(下圖)
上:由若干盞聚光燈組成的燈陣;下:眼神高光反饋的燈陣形狀。
此外,場景提供了體積霧,並且配以一個點光源,模擬自然過渡的背景效果。(下圖)
六、總結和展望
6.1 渲染技術總結
本系列文章緊緊圍繞著Unreal的官方數字人類《Meet Mike》的角色進行渲染技術的剖析,它們涉及的技術點如下:
- 皮膚
- 基於物理的渲染(PBR)
- 雙向反射分佈函式(BRDF)
- 次表面散射(SSS)
- 高斯函式
- 偶極子(Dipole)
- 多偶極子(Multi Dipole)
- 多個高斯函式模擬皮膚次表面散射
- 雙向次散射反射模型(BSSRDF)
- 可分離的次表面散射(SSSS)
- 奇異值分解(SVD)
- 紋理空間模糊
- 螢幕空間模糊
- 預卷積核權重
- 眼睛
- 基於物理的反射
- 鏡面反射
- 折射
- 自反射(預烘焙)
- 參合多介質渲染(participating media rendering)
- 其它細節:
- 溼潤度(法線擾動)
- 血色
- 接觸陰影
- 淚腺體
- 遮蔽模糊體
- 眼角混合物
- 基於物理的反射
- 頭髮
- Marschner毛髮渲染
- 反射(R)
- 透射(TT)
- 次反射(TRT)
- 雙層UV
- 高精度模型
- XGen生成
- Marschner毛髮渲染
6.2 能達到實時逼真的原因
能達到如此逼真的渲染效果,總結起來,主要有以下原因:
基於物理的光照模型
- PBR
- BSSRDF
SSSS
基於真人掃描的模型
- 超高精度模型(70w頂點,60w三角面)
- 超高解析度貼圖(4K+)
- 功能眾多的貼圖
- 基礎色、高光、粗糙、次表面散射、清漆、法線、AO等貼圖
- 掃描直出、轉置、二次製作
- 眾多細節
- 皮膚細節:毛孔、雀斑、血絲、絨毛、雙層高光、皺紋......
- 眼球細節:反射、折射、自陰影、側面光、材質過渡、法線擾動......
基於物理和攝影藝術的場景燈光
- 聚光燈陣
- 補光燈
- 側燈
- 背面輪廓燈
- 背景過渡燈
高度定製的材質
- 皮膚材質
- 眼睛材質
- 毛髮材質
- 衣服材質
6.3 不足
就Mike而言,雖然渲染效果已經逼近真實,但也存在一些問題:
毛髮沒有物理效果。
材質非所有場景的燈光都能適應。在某些場景,渲染出來的角色效果存在失真現象。
- SSSS渲染出現的皮膚條紋。
驅動效果不夠流暢(從釋出的視訊得出結論)。
當然,在後續的Siren專案中,以上有些問題得到解決或緩解。
相信在強大的UE官方團隊面前,虛擬數字人探索的腳步會一直向前邁進,為實時渲染領域拿下一個又一個里程碑。
本系列文章完!
本系列文章其它部分
特別說明
- 感謝參考文獻的所有作者們!
- 未經允許,禁止轉載!
參考文獻
- Next-Generation-Character-Rendering (ACM Transactions on Graphics, Vol. 29(5), SIGGRAPH Asia 2010)
- Separable Subsurface Scattering
- Real-Time Realistic Skin Translucency
- Skin Microstructure Deformation with Displacement Map Convolution
- 《由淺入深學習PBR的原理和實現》
- 數字人類
- 照片級角色
- 角色渲染技術——毛髮及其他
- Light Scattering from Human Hair Fibers
- Dual Scattering Approximation for Fast Multiple Scattering in Hair
- A Data-Driven Light Scattering Model for Hair
- GPU Gem 2: Chapter 23. Hair Animation and Rendering in the Nalu Demo
- Real-Time Hair Simulation and Rendering
- Digital Mike頭髮製作及渲染的深度揭祕
- 細緻到毛孔 ! 深度揭祕超真實皮膚的實時渲染技術(上篇)