數字人類(Digital Human)是利用計算機模擬真實人類的一種綜合性的渲染技術。也被稱爲虛擬人類、超真實人類、照片級人類。git
它是一種技術和藝術相結合的綜合性模擬渲染,涵蓋計算機圖形渲染、模型掃描、3D建模、肢體驅動、AI算法等領域。算法
數字人類概念圖api
隨着計算機渲染技術的發展,數字人類在電影領域早有應用。在上世紀80年代的《星球大戰》系列、《異形》系列等電影,到後來的《終結者》系列、《黑客帝國》系列、《指環王》系列等,再到近期的漫威、DC動畫電影,都存在着虛擬角色的身影,他們或是天賦異稟的人類,或是奇形怪狀的怪物。session
《星球大戰I》中的虛擬角色:尤達大師(Master Yoda)app
《黑客帝國》的主角不少鏡頭是採用計算機渲染而成的虛擬數字人less
電影《戰鬥天使》的畫面。主角阿麗塔也是虛擬角色。編輯器
因爲近些年計算機硬件性能和渲染技術的提高,除了在離線渲染領域的電影和動漫的普遍應用以外,實時領域的應用也獲得長足的進步。例如,次世代遊戲、3A大做、VR遊戲以及泛娛樂領域的直播領域。ide
《孤島驚魂5》中的虛擬遊戲角色函數
R&D和暴雪在GDC2013展現的次世代虛擬角色
Unreal Engine在GDC2018展現的虛擬角色Siren,可由演員實時驅動動做、表情、肢體等信息。
數字人類的步驟多,工序繁瑣。但總結起來,一般有如下幾個步驟:
模型掃描。一般藉助光學掃描儀或由單反相機組成的360度的攝影包圍盒,對掃描對象進行全方位的掃描,從而得到原始的模型數據。
上圖展現了模型掃描儀,由不少攝影和燈光設備組成的球形矩陣。
模型調整。由掃描階段獲取的初始模型一般有瑕疵,沒法直接投入渲染。須要美術人員利用3D建模工具(如Maya、3DMax等)進行調整、優化、從新拓撲,最終調整成合適的可用模型。
左:掃描的初始模型;中:調整後的中間模型;右:優化了細節的可用模型。
製做貼圖。在此階段,用建模軟件或材質製做軟件(如Substance)採納高精度模型烘焙或製做出漫反射、法線、粗糙度、AO、散射、高光等等貼圖,爲最後的渲染作準備。這些貼圖的原始尺寸一般都很是大,4K、8K甚至16K,目的是高精度還原虛擬人類的細節。
漫反射貼圖
法線貼圖
導入引擎。在此階段,將以前製做的模型和貼圖導入到渲染引擎(如UE四、Unity等),加入光照、材質、場景等元素,結合角色的綜合性PBR渲染技術,得到最終成像。
Unreal Engine渲染出的虛擬角色
Unreal Engine做爲商業渲染引擎的巨頭,在實時領域渲染數字人類作了不少嘗試,關鍵節點有:
2015年:《A Boy and His Kite》。展現了當時的開放世界概念和天然的角色動畫風格與憑藉第一人稱射擊遊戲成名的Epic之前作過的任何項目都大不相同。
《A Boy and His Kite》的畫面
2016年:《地獄之刃:塞娜的獻祭》。這是Unreal將數字人引入實時遊戲的一次嘗試,從畫質表現上,已經達到了異常逼真的程度。
《地獄之刃:塞娜的獻祭》中的遊戲角色畫面
2017年:《Meet Mike》。在Siggraph 2017中,Epic Game憑藉此項目爲世人展現了數字人科技的最新研究:利用最早進的畫面捕捉技術、體感控制技術以及畫面渲染技術在計算機中塑造人類的化身。其中數字人Mike是著名電影特效大師以及Fx Guide網站創始人Mike Seymour的化身。
Unreal Engine官方團隊製做的Mike虛擬角色
2018年:《Siren》。Siren是Epic Game、3Lateral、Cubic Motion、Vicon以及騰訊的NEXT工做室等多家跨國公司傾力合做,花費半年多打造的頂級實時渲染的虛擬角色。從畫質效果上看,已經與數碼照片無異。
《Siren》虛擬角色的細節,與數碼相機攝製的照片一模一樣
筆者本想以《Siren》的虛擬角色爲依託進行研究,奈何官方並未將此項目開源。
因此本文只能用《Meet Mike》項目的角色做爲研究對象。
《Meet Mike》項目的資源和源碼能夠從Unreal Engine的Epic Games Launcher中下載得到。
《Meet Mike》資源和源碼下載具體步驟
若成功下載了Mike工程,打開項目的DigitalHuman.uproject文件,能夠看到下面的畫面:
點擊右上角World Outliner面板的」final_mike「,能夠查看Mike模型及其全部材質的細節。
若是要研究某個部分的材質(好比皮膚),雙擊對應的材質,便可打開材質節點。下圖是雙擊M_Head皮膚材質後的界面:
打材質編輯器後,即可以進行後續的研究。後面章節將着重研究數字人的皮膚、眼球、毛髮以及身體其它部位的渲染技術。
Mike的一些數據:
57萬個三角形,69萬個頂點。其中大量三角形集中在臉部,特別是頭髮,約佔75%。
每根頭髮都是單獨三角形,大約有2萬多根頭髮。
臉部骨骼綁定使用了大約80個關節,大部分是爲了頭髮的運動和臉部毛髮。
臉部使用了Technoprop公司先進的配有立體紅外攝像頭的固定在頭部的面部捕捉裝置。
綜合使用了750個融合變形(blend shapes)。
系統使用了複雜的傳統軟件和三種深度學習AI引擎。
皮膚渲染技術通過數十年的發展,由最初的單張貼圖+倫勃朗的渲染方式到近期的基於物理的SSSSS(屏幕空間次表面散射)。由此衍生出的皮膚渲染技術層出不窮,其中最最基礎也最具表明性的是次表面散射(SSS)。
在虛擬角色渲染中,皮膚的渲染尤其關鍵。由於皮膚是人們天天親眼目擊的很是熟悉的東西,若是稍微渲染很差或細節處理不足,便會陷入恐怖谷(Uncanny Valley )理論。至於什麼是恐怖谷理論,參看這裏。
上圖因爲皮膚的細節處理不到位,陷入了恐怖谷理論
人類皮膚的物理構成很是複雜,其表層和內部都由很是複雜的構成物質,剖面圖以下:
絨毛(hair shaft)。附着於皮膚表面的細小的毛。
油脂(oil)。皮膚表層有一層薄薄的油脂覆蓋,是皮膚高光的主要貢獻者。
表皮(epidermis)。油脂層下是表皮覆蓋,是形成次表面散射的物質之一。
真皮(dermis)。表皮下面是真正的皮膚組織,也是形成次表面散射的物質之一。
毛囊(hair follicle)。絨毛的皮下組織和根基。
靜脈(vein)。呈深藍色的血管。
動脈(artery)。呈暗紅色的血管。
脂肪組織(fatty tissue)。脂肪組織也是形成次表面散射的次要貢獻物質。
其它:皮膚表面的紋理、皺紋、毛孔、雀斑、痘痘、黑痣、疤痕、油脂粒等等細節。
真實皮膚包含了很是多的細節:毛孔、絨毛、痘痘、黑痣、油脂......
皮膚表面油脂層主要貢獻了皮膚光照的反射部分(約6%的光線被反射),而油脂層下面的表皮層和真皮層則主要貢獻了的次表面散射部分(約94%的光線被散射)。
雖然皮膚構成很是複雜,但圖形渲染界的先賢者們利用簡化的思惟將皮膚建模成若干層。
以上展現的是BRDF建模方式,只在皮膚表面反射光線,但實際上在三層建模中,還會考慮表皮層和真皮層的次表面散射(BSSRDF),見下圖中間部分BSSRDF。
皮膚渲染涉及的高級技術有:
皮膚渲染的過程能夠抽象成如下步驟:
皮膚反射。
直接反射部分採用Cook-Torrance的BRDF,公式:
\[ f_{cook-torrance} = \frac {D(h)F(l,h)G(l,v,h)}{4(n\cdot l)(n\cdot v)} \]
具體解析和實現請參看《由淺入深學習PBR的原理和實現》的章節3.1.3 反射方程。
UE的皮膚渲染採用雙鏡葉高光(Dual Lobe Specular)。雙鏡葉高光度爲兩個獨立的高光鏡葉提供粗糙度值,兩者組合後造成最終結果。當兩者組合後,會爲皮膚提供很是出色的亞像素微頻效果,呈現出一種天然面貌。
其中UE默認的混合公式是:
\[ Lobe1 \cdot 0.85 \ + \ Lobe2 \cdot 0.15 \]
下圖顯示了UE4混合的過程和最終成像。
左:較柔和的高光層Lobe1; 中:較強烈的高光層Lobe2; 右:最終混合成像
非直接反射部分採用預卷積的cube map。
具體解析和實現請參看《由淺入深學習PBR的原理和實現》的章節3.3.2 鏡面的IBL(Specular IBL)。
皮膚毛孔。
皮膚毛孔內部構造很是複雜,會形成反射(高光和漫反射)、陰影、遮擋、次表面散射等效應。
人類毛孔放大圖,內部構造異常複雜,由此產生很是複雜的光照信息
在渲染毛孔細節時,需注意不少細節,不然會渲染結果陷入恐怖谷理論。
理論上,接近物理真實的渲染,毛孔的渲染公式以下:
\[ cavity \cdot Specular(gloss) \cdot Fresnel(reflectance) \]
其中:
\(cavity\)是凹陷度。可從cavity map(下圖)中採樣得到。
\(Specular(gloss)\)代表高光項。
\(Fresnel(reflectance)\)是與視覺角度相關的反射。
然而,這種物理真實,使得凹陷太明顯,視覺不美觀,有點讓人不適:
嘗試微調高光和cavity的位置,可得到下面的渲染結果:
上圖能夠看出,高光太強,凹陷細節不足,也是不夠真實的皮膚渲染結果。
實際上,可摒棄徹底物理真實的原理,採用近似法:
\[ Specular(gloss) \cdot Fresnel(cavity \cdot reflectance) \]
最終可渲染出真實和美觀相平衡的畫面:
UE4採用漫反射+粗糙度+高光度+散射+法線等貼圖結合的方式,以高精度還原皮膚細節。
從左到右:漫反射、粗糙度、高光度、散射、法線貼圖
具體光照過程跟Cook-Torrance的BRDF大體同樣,這裏不詳述。
全局光照。
皮膚的全局光照是基於圖像的光照(IBL)+改進的AO結合的結果。
其中IBL技術請參看3.3 基於圖像的光照(Image Based Lighting,IBL)。
上圖:疊加了全局光照,但無AO的畫面
AO部分是屏幕空間環境光遮蔽(SSAO),其中AO貼圖混合了Bleed Color(皮膚一般取紅色)。
增長了紅色Bleed Color的AO,使得皮膚渲染更加貼切,皮膚暗處的亮度和顏色更真實美觀。
次表面散射(BSSRDF)。
這部份內容將在2.2更詳細描述。
次表面散射(Subsurface scattering)是模擬皮膚、玉石、牛奶等半透光性物質的一種物理渲染技術。
它與普通BRDF的區別在於,同一條入射光進入半透光性物質後,會在內部通過屢次散射,最終在入射點附近散射出若干條光線。
因爲R、G、B在物質內擴散的曲線不同,由此產生了與入射光不同的顏色。
紅色光因爲穿透力更強,更容易在皮膚組織穿透,造成紅色光。
BSSRDF是基於次表面散射的一種光照模型,充分考慮了入射光在物質內部通過若干次散射後從新反射出來的光。
左:BRDF;右:BSSRDF,考慮了輸入光在物質內散射後從新射出的若干條光
上圖描述了BRDF、BTDF、BSSRDF之間的關係:
下面兩圖展現了使用BRDF和BSSRDF的皮膚渲染結果:
BRDF光照模型渲染的皮膚
BSSRDF光照模型渲染的皮膚
可見BSSRDF渲染的皮膚效果更真實,更美觀,防止陷入恐怖谷效應。
回顧一下BRDF的方程,它是一次反射光照的計算是在光線交點的法線半球上的球面積分:
\[ L_o(p,\omega_o) = \int\limits_{\Omega} f_r(p,\omega_i,\omega_o) L_i(p,\omega_i) n \cdot \omega_i d\omega_i \]
對於BSSRDF來講,每一次反射在物體表面上每個位置都要作一次半球面積分,是一個嵌套積分:
\[ L_o(p_o,\omega_o) = \int\limits_{A} \int\limits_{\Omega} S(p_o,\omega_o,p_i,\omega_i) L_i(p_i,\omega_i) n \cdot \omega_i d\omega_i dA \]
\(S(p_o,\omega_o,p_i,\omega_i)\)項代表了次表面散射的計算過程,具體公式:
\[ \begin{eqnarray} S(p_o,\omega_o,p_i,\omega_i) &\stackrel {def}{=}& \frac{dL_r(p_o,\omega_o)}{d\Phi_r(p_i,\omega_i)} \\ &=& \frac{1}{\pi}F_t(p_o,\omega_o)R_d(\parallel p_i-p_o\parallel)F_t(p_i,\omega_i) \\ \end{eqnarray} \]
其中:
\(\frac{dL_r(p_o,\omega_o)}{d\Phi_r(p_i,\omega_i)}\)代表BSSRDF的定義是出射光的輻射度和入射通量的比值。
\(F_t\)是菲涅爾透射效應。
\(R_d(\parallel p_i-p_o\parallel)\)是擴散反射(Diffuse reflectance),與入射點和出射點的距離相關。
\[ R_d(\parallel p_i-p_o\parallel) = -D\frac{(n\cdot \triangle\phi(p_o))}{d\Phi_i(p_i)} \]
因而可知,\(S\)項的計算過程比較複雜,對於實時渲染,是幾乎不可能完成的。由此可採用近似法求解:
\[ S(p_o,\omega_o,p_i,\omega_i) \approx (1-F_r(\cos\theta_o))S_p(p_o,p_i)S_\omega(\omega_i) \]
其中:
\(F_r(\cos\theta_o)\)是菲涅爾反射項。
\(S_p(p_o,p_i)\)是點\(p\)處的次表面散射函數。它能夠進一步簡化:
\[ S_p(p_o,p_i) \approx S_r(\parallel p_o - p_i\parallel) \]
也就是說點\(p\)處的次表面係數只由入射點\(p_i\)和出射點\(p_o\)相關。
\(S_r\)跟介質的不少屬性有關,可用公式表達及簡化:
\[ \begin{eqnarray} S_r(\eta,g,\rho,\sigma_t,r) &=& \sigma^2_t S_r(\eta,g,\rho,1,r_{optical}) \\ &\approx& \sigma^2_t S_r(\rho,r_{optical}) \\ r_{optical} &=& \rho_t r \end{eqnarray} \]
簡化後的\(S_r\)只跟\(\rho\)和\(r\)有關,每種材料的\(\rho\)和\(r\)可組成一個BSSRDF表。
上圖展現了\(\rho=0.2\)和\(r=0.5\)的索引表。
經過\(\rho\)和\(r\)可查詢到對應的\(S_r\),從而化繁爲簡,實現實時渲染的目標。
\(S_\omega(\omega_i)\)是有縮放因子的菲涅爾項,它的公式:
\[ S_\omega(\omega_i) = \frac{1-F_r(\cos\theta_i)}{c\cdot \pi} \]
其中\(c\)是一個嵌套的半球面積分:
\[ \begin{eqnarray} c &=& \int_0^{2\pi} \int_0^{\frac{\pi}{2}} \frac{1-F_r(\eta,\cos\theta)}{\pi}\sin\theta \ \cos\theta \ d\theta \ d\phi \\ &=& 1 - 2 \int_0^{\frac{\pi}{2}} F_r(\eta,\cos\theta)\sin\theta \ \cos\theta \ d\theta \ d\phi \end{eqnarray} \]
BSSRDF公式更具體的理論、推導、簡化過程可參看下面兩篇論文:
2.2.2 次表面散射的空間模糊
次表面散射本質上是採樣周邊像素進行加權計算,相似特殊的高斯模糊。也就是說,次表面散射的計算能夠分爲兩個部分:
(1)先對每一個像素進行通常的漫反射計算。
(2)再根據某種特殊的函數\(R(r)\)和(1)中的漫反射結果,加權計算周圍若干個像素對當前像素的次表面散射貢獻。
上述(2)中提到的\(R(r)\)就是次表面散射的擴散剖面(Diffusion Profile)。它是一個次表面散射的光線密度分佈,是各向同性的函數,也就是說一個像素受周邊像素的光照影響的比例只和兩個像素間的距離有關。
實際上全部材質都存在次表面散射現象,區別只在於其密度分佈函數\(R(r)\)的集中程度,若是該函數的絕大部分能量都集中在入射點附近(r=0),就表示附近像素對當前像素的光照貢獻不明顯,能夠忽略,則在渲染時咱們就用漫反射代替,若是該函數分佈比較均勻,附近像素對當前像素的光照貢獻明顯,則須要單獨計算次表面散射。
利用擴散剖面技術模擬的次表面散射,爲了獲得更柔和的皮膚質感,須要對畫面進行若干次不一樣參數的高斯模糊。從模糊空間劃分,有兩種方法:
紋理空間模糊(Texture Space Blur)。利用皮膚中散射的局部特性,經過使用紋理座標做爲渲染座標展開3D網格,在2D紋理中有效地對其進行模擬。
屏幕空間模糊(Screen Space Blur)。跟紋理空間不一樣的是,它在屏幕空間進行模糊,也被稱爲屏幕空間次表面散射(Screen Space SubSurface Scattering,SSSSS)。
紋理空間和屏幕空間進行0, 3, 5次高斯模糊的結果
上圖:屏幕空間的次表面散射渲染過程
2.2.3 可分離的次表面散射(Separable Subsurface Scattering)
次表面散射的模糊存在卷積分離(Separable Convolution)的優化方法,具體是將橫向座標U和縱向座標V分開卷積,再作合成:
由此產生了可分離的次表面散射(Separable Subsurface Scattering,也叫SSSS或4S),這也是UE目前採用的人類皮膚渲染方法。它將\(R_d\)作了簡化:
\[ R_d(x,y) \approx A_g(x,y) = \sum_{i=1}^N \omega_i G(x,y,\sigma_i) \]
具體的推導過程請參看:Separable Subsurface Scattering。
該論文還提到,爲了給實時渲染加速,還須要預積分分離的卷積核(Pre-integrated Separable Kernel):
\[ A_p(x,y) = \frac{1}{\parallel R_d \parallel_1} a_p(x)a_p(y) \]
利用奇異值分解(Singular Value Decomposition,SVD)的方法將其分解爲一個行向量和一個列向量,而且保證了分解後的表示方法基本沒有能量損失。下圖展現了它的計算過程:
本節將從UE的C++和shader源碼分析皮膚渲染的實現。UE源碼下載的具體步驟請看官方文檔:下載虛幻引擎源代碼。
再次給擁有充分共享精神的Epic Game點個贊!UE的開源使咱們能夠一窺引擎內部的實現,再也不是黑盒操做,也使咱們有機會學習圖形渲染的知識,對我的、項目和公司都大有裨益。
皮膚渲染的方法不少,UE使用的是可分離的次表面散射(Separable Subsurface Scattering,也叫SSSS或4S)。最早由暴雪的Jorge等人,在GDC2013的演講《Next-Generation Character Rendering》中首次展現了SSSS的渲染圖,並在2015年經過論文正式提出了Separable Subsurface Scattering。其經過水平和垂直卷積2個Pass來近似,效率更進一步提高,這是目前遊戲裏採用的主流技術。
UE源碼中,與SSSS相關的主要文件(筆者使用的是UE 4.22,不一樣版本可能有所差異):
\Engine\Shaders\Private\SeparableSSS.ush:
SSSS的shader主要實現。
\Engine\Shaders\Private\PostProcessSubsurface.usf:
後處理階段爲SeparableSSS.ush提供數據和工具接口的實現。
\Engine\Shaders\Private\SubsurfaceProfileCommon.ush:
定義了SSSS的常量和配置。
\Engine\Source\Runtime\Engine\Private\Rendering\SeparableSSS.cpp:
實現CPU版本的擴散剖面、高斯模糊及透射剖面等邏輯,可用於離線計算。
\Engine\Source\Runtime\Engine\Private\Rendering\SubsurfaceProfile.cpp:
SSS Profile的管理,紋理的建立,及與SSSS交互的處理。
SeparableSSS.ush是實現SSSS的主要shader文件,先分析像素着色器代碼。(下面有些接口是在其它文件定義的,經過名字就能夠知道大體的意思,無需關心其內部實現細節也不妨礙分析核心渲染算法。)
// BufferUV: 紋理座標,會從GBuffer中取數據; // dir: 模糊方向。第一個pass取值float2(1.0, 0.0),表示橫向模糊;第二個pass取值float2(0.0, 1.0),表示縱向模糊。這就是「可分離」的優化。 // initStencil:是否初始化模板緩衝。第一個pass須要設爲true,以便在第二個pass得到優化。 float4 SSSSBlurPS(float2 BufferUV, float2 dir, bool initStencil) { // Fetch color of current pixel: // SSSSSampleSceneColorPoint和SSSSSampleSceneColor就是獲取2.2.2步驟(1)中提到的已經計算好的漫反射顏色 float4 colorM = SSSSSampleSceneColorPoint(BufferUV); // we store the depth in alpha float OutDepth = colorM.a; colorM.a = ComputeMaskFromDepthInAlpha(colorM.a); // 根據掩碼值決定是否直接返回,而不作後面的次表面散射計算。 BRANCH if(!colorM.a) { // todo: need to check for proper clear // discard; return 0.0f; } // 0..1 float SSSStrength = GetSubsurfaceStrength(BufferUV); // Initialize the stencil buffer in case it was not already available: if (initStencil) // (Checked in compile time, it's optimized away) if (SSSStrength < 1 / 256.0f) discard; float SSSScaleX = SSSParams.x; float scale = SSSScaleX / OutDepth; // 計算採樣周邊像素的最終步進 float2 finalStep = scale * dir; // ideally this comes from a half res buffer as well - there are some minor artifacts finalStep *= SSSStrength; // Modulate it using the opacity (0..1 range) FGBufferData GBufferData = GetGBufferData(BufferUV); // 0..255, which SubSurface profile to pick // ideally this comes from a half res buffer as well - there are some minor artifacts uint SubsurfaceProfileInt = ExtractSubsurfaceProfileInt(GBufferData); // Accumulate the center sample: float3 colorAccum = 0; // 初始化爲非零值,是爲了防止後面除零異常。 float3 colorInvDiv = 0.00001f; // 中心點採樣 colorInvDiv += GetKernel(SSSS_N_KERNELWEIGHTOFFSET, SubsurfaceProfileInt).rgb; colorAccum = colorM.rgb * GetKernel(SSSS_N_KERNELWEIGHTOFFSET, SubsurfaceProfileInt).rgb; // 邊界溢色。 float3 BoundaryColorBleed = GetProfileBoundaryColorBleed(GBufferData); // 疊加周邊像素的採樣,即次表面散射的計算,也可看作是與距離相關的特殊的模糊 // SSSS_N_KERNELWEIGHTCOUNT是樣本數量,與配置相關,分別是六、九、13。可由控制檯命令r.SSS.SampleSet設置。 SSSS_UNROLL for (int i = 1; i < SSSS_N_KERNELWEIGHTCOUNT; i++) { // Kernel是卷積核,卷積核的權重由擴散剖面(Diffusion Profile)肯定,而卷積核的大小則須要根據當前像素的深度(d(x,y))及其導數(dFdx(d(x,y))和dFdy(d(x,y)))來肯定。而且它是根據Subsurface Profile參數預計算的。 // Kernel.rgb是顏色通道的權重;Kernel.a是採樣位置,取值範圍是0~SUBSURFACE_KERNEL_SIZE(即次表面散射影響的半徑) half4 Kernel = GetKernel(SSSS_N_KERNELWEIGHTOFFSET + i, SubsurfaceProfileInt); float4 LocalAccum = 0; float2 UVOffset = Kernel.a * finalStep; // 因爲卷積核是各向同性的,因此能夠簡單地取採樣中心對稱的點的顏色進行計算。可將GetKernel調用下降至一半,權重計算消耗降至一半。 SSSS_UNROLL // Side的值是-1和1,經過BufferUV + UVOffset * Side,便可得到採樣中心點對稱的兩點作處理。 for (int Side = -1; Side <= 1; Side += 2) { // Fetch color and depth for current sample: float2 LocalUV = BufferUV + UVOffset * Side; float4 color = SSSSSampleSceneColor(LocalUV); uint LocalSubsurfaceProfileInt = SSSSSampleProfileId(LocalUV); float3 ColorTint = LocalSubsurfaceProfileInt == SubsurfaceProfileInt ? 1.0f : BoundaryColorBleed; float LocalDepth = color.a; color.a = ComputeMaskFromDepthInAlpha(color.a); #if SSSS_FOLLOW_SURFACE == 1 // 根據OutDepth和LocalDepth的深度差校訂次表面散射效果,若是它們相差太大,幾乎無次表面散射效果。 float s = saturate(12000.0f / 400000 * SSSParams.y * // float s = saturate(300.0f/400000 * SSSParams.y * abs(OutDepth - LocalDepth)); color.a *= 1 - s; #endif // approximation, ideally we would reconstruct the mask with ComputeMaskFromDepthInAlpha() and do manual bilinear filter // needed? color.rgb *= color.a * ColorTint; // Accumulate left and right LocalAccum += color; } // 因爲中心採樣點兩端的權重是對稱的,colorAccum和colorInvDiv原本都須要*2,但它們最終colorAccum / colorInvDiv,因此*2能夠消除掉。 colorAccum += Kernel.rgb * LocalAccum.rgb; colorInvDiv += Kernel.rgb * LocalAccum.a; } // 最終將顏色權重和深度權重相除,以規範化,保持光能量守恆,防止顏色過曝。(對於沒有深度信息或者沒有SSS效果的材質,採樣可能失效!) float3 OutColor = colorAccum / colorInvDiv; // alpha stored the SceneDepth (0 if there is no subsurface scattering) return float4(OutColor, OutDepth); }
此文件還有SSSSTransmittance
,但筆者搜索了整個UE的源代碼工程,彷佛沒有被用到,因此暫時不分析。下面只貼出其源碼:
//----------------------------------------------------------------------------- // Separable SSS Transmittance Function // @param translucency This parameter allows to control the transmittance effect. Its range should be 0..1. Higher values translate to a stronger effect. // @param sssWidth this parameter should be the same as the 'SSSSBlurPS' one. See below for more details. // @param worldPosition Position in world space. // @param worldNormal Normal in world space. // @param light Light vector: lightWorldPosition - worldPosition. // @param lightViewProjection Regular world to light space matrix. // @param lightFarPlane Far plane distance used in the light projection matrix. float3 SSSSTransmittance(float translucency, float sssWidth, float3 worldPosition, float3 worldNormal, float3 light, float4x4 lightViewProjection, float lightFarPlane) { /** * Calculate the scale of the effect. */ float scale = 8.25 * (1.0 - translucency) / sssWidth; /** * First we shrink the position inwards the surface to avoid artifacts: * (Note that this can be done once for all the lights) */ float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0); /** * Now we calculate the thickness from the light point of view: */ float4 shadowPosition = SSSSMul(shrinkedPos, lightViewProjection); float d1 = SSSSSampleShadowmap(shadowPosition.xy / shadowPosition.w).r; // 'd1' has a range of 0..1 float d2 = shadowPosition.z; // 'd2' has a range of 0..'lightFarPlane' d1 *= lightFarPlane; // So we scale 'd1' accordingly: float d = scale * abs(d1 - d2); /** * Armed with the thickness, we can now calculate the color by means of the * precalculated transmittance profile. * (It can be precomputed into a texture, for maximum performance): */ float dd = -d * d; float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) + float3(0.1, 0.336, 0.344) * exp(dd / 0.0484) + float3(0.118, 0.198, 0.0) * exp(dd / 0.187) + float3(0.113, 0.007, 0.007) * exp(dd / 0.567) + float3(0.358, 0.004, 0.0) * exp(dd / 1.99) + float3(0.078, 0.0, 0.0) * exp(dd / 7.41); /** * Using the profile, we finally approximate the transmitted lighting from * the back of the object: */ return profile * saturate(0.3 + dot(light, -worldNormal)); }
SeparableSSS.cpp主題提供了擴散剖面、透射剖面、高斯模糊計算以及鏡像卷積核的預計算。
爲了更好地理解源代碼,仍是先介紹一些前提知識。
擴散剖面的模擬可由若干個高斯和函數進行模擬,其中高斯函數的公式:
\[ f_{gaussian} = e^{-r^2} \]
下圖是單個高斯和的擴散剖面曲線圖:
因而可知R、G、B的擴散距離不同,而且單個高斯函數沒法精確模擬出複雜的人類皮膚擴散剖面。
實踐代表多個高斯分佈在一塊兒能夠對擴散剖面提供極好的近似。而且高斯函數是獨特的,由於它們同時是可分離的和徑向對稱的,而且它們能夠相互卷積來產生新的高斯函數。
對於每一個擴散分佈\(R(r)\),咱們找到具備權重\(\omega_i\)和方差\(v_i\)的\(k\)個高斯函數:
\[ R(r) \approx \sum_{i=1}^k\omega_iG(v_i,r) \]
而且高斯函數的方差\(v\)有如下定義:
\[ G(v, r) := \frac{1}{2\pi v} e^{\frac{-r^2}{2v}} \]
能夠選擇常數\(\frac{1}{2v}\)使得\(G(v, r)\)在用於徑向2D模糊時不會使輸入圖像變暗或變亮(其具備單位脈衝響應(unit impulse response))。
對於大部分透明物體(牛奶、大理石等)用一個Dipole Profile就夠了,可是對於皮膚這種擁有多層結構的材質,用一個Dipole Profile不能達到理想的效果,能夠經過3個Dipole接近Jensen論文中的根據測量得出的皮膚Profile數據。
實驗發現,3個Dipole曲線可經過如下6個高斯函數擬合獲得(具體的擬合推導過程參見:《GPU Gems 3》:真實感皮膚渲染技術總結):
\[ \begin{eqnarray} R(r) &=& 0.233\cdot G(0.0064,r) + 0.1\cdot G(0.0484,r) + 0.118\cdot G(0.187,r) \\ &+& 0.113\cdot G(0.567,r) + 0.358\cdot G(1.99,r) + 0.078\cdot G(7.41,r) \end{eqnarray} \]
上述公式是紅通道Red的模擬,綠通道Green和藍通道Blue的參數不同,見下表:
R、G、B通道擬合出的曲線有所不一樣(下圖),可見R通道曲線的擴散範圍最遠,這也是皮膚顯示出紅色的緣由。
首先分析SeparableSSS_Gaussian
:
// 這個就是上一小節提到的G(v,r)的高斯函數,增長了FalloffColor顏色,對應不一樣顏色通道的值。 inline FVector SeparableSSS_Gaussian(float variance, float r, FLinearColor FalloffColor) { FVector Ret; // 對每一個顏色通道作一次高斯函數技術 for (int i = 0; i < 3; i++) { float rr = r / (0.001f + FalloffColor.Component(i)); Ret[i] = exp((-(rr * rr)) / (2.0f * variance)) / (2.0f * 3.14f * variance); } return Ret; }
再分析SeparableSSS_Profile
:
// 天啦嚕,這不正是上一小節提到的經過6個高斯函數擬合獲得3個dipole曲線的公式麼?參數一毛同樣有木有? // 其中r是次表面散射的最大影響距離,單位是mm,可由UE編輯器的Subsurface Profile界面設置。 inline FVector SeparableSSS_Profile(float r, FLinearColor FalloffColor) { // 須要注意的是,UE4將R、G、B通道的參數都統一使用了R通道的參數,它給出的理由是FalloffColor已經包含了不一樣的值,而且方便模擬出不一樣膚色的材質。 return // 0.233f * SeparableSSS_Gaussian(0.0064f, r, FalloffColor) + // UE4屏蔽掉了第一個高斯函數,理由是這個是直接反射光,而且考慮了strength參數。(We consider this one to be directly bounced light, accounted by the strength parameter) 0.100f * SeparableSSS_Gaussian(0.0484f, r, FalloffColor) + 0.118f * SeparableSSS_Gaussian(0.187f, r, FalloffColor) + 0.113f * SeparableSSS_Gaussian(0.567f, r, FalloffColor) + 0.358f * SeparableSSS_Gaussian(1.99f, r, FalloffColor) + 0.078f * SeparableSSS_Gaussian(7.41f, r, FalloffColor); }
接着分析如何利用上面的接口進行離線計算Kernel的權重:
// 因爲高斯函數具體各向同性、中心對稱性,因此橫向卷積和縱向卷積同樣,經過鏡像的數據減小一半計算量。 void ComputeMirroredSSSKernel(FLinearColor* TargetBuffer, uint32 TargetBufferSize, FLinearColor SubsurfaceColor, FLinearColor FalloffColor) { check(TargetBuffer); check(TargetBufferSize > 0); uint32 nNonMirroredSamples = TargetBufferSize; int32 nTotalSamples = nNonMirroredSamples * 2 - 1; // we could generate Out directly but the original code form SeparableSSS wasn't done like that so we convert it later // .A is in mm check(nTotalSamples < 64); FLinearColor kernel[64]; { // 卷積核時先給定一個默認的半徑範圍,不能太大也不能過小,根據nTotalSamples數量調整Range是必要的。(單位是毫米mm) const float Range = nTotalSamples > 20 ? 3.0f : 2.0f; // tweak constant const float Exponent = 2.0f; // Calculate the offsets: float step = 2.0f * Range / (nTotalSamples - 1); for (int i = 0; i < nTotalSamples; i++) { float o = -Range + float(i) * step; float sign = o < 0.0f ? -1.0f : 1.0f; // 將當前的range和最大的Range的比值存入alpha通道,以便在shader中快速應用。 kernel[i].A = Range * sign * FMath::Abs(FMath::Pow(o, Exponent)) / FMath::Pow(Range, Exponent); } // 計算Kernel權重 for (int32 i = 0; i < nTotalSamples; i++) { // 分別取得i兩邊的.A值作模糊,存入area float w0 = i > 0 ? FMath::Abs(kernel[i].A - kernel[i - 1].A) : 0.0f; float w1 = i < nTotalSamples - 1 ? FMath::Abs(kernel[i].A - kernel[i + 1].A) : 0.0f; float area = (w0 + w1) / 2.0f; // 將模糊後的權重與6個高斯函數的擬合結果相乘,得到RGB的最終權重。 FVector t = area * SeparableSSS_Profile(kernel[i].A, FalloffColor); kernel[i].R = t.X; kernel[i].G = t.Y; kernel[i].B = t.Z; } // 將offset爲0.0(即中心採樣點)的值移到位置0. FLinearColor t = kernel[nTotalSamples / 2]; for (int i = nTotalSamples / 2; i > 0; i--) { kernel[i] = kernel[i - 1]; } kernel[0] = t; // 規範化權重,使得權重總和爲1,保持顏色能量守恆. { FVector sum = FVector(0, 0, 0); for (int i = 0; i < nTotalSamples; i++) { sum.X += kernel[i].R; sum.Y += kernel[i].G; sum.Z += kernel[i].B; } for (int i = 0; i < nTotalSamples; i++) { kernel[i].R /= sum.X; kernel[i].G /= sum.Y; kernel[i].B /= sum.Z; } } /* we do that in the shader for better quality with half res // Tweak them using the desired strength. The first one is: // lerp(1.0, kernel[0].rgb, strength) kernel[0].R = FMath::Lerp(1.0f, kernel[0].R, SubsurfaceColor.R); kernel[0].G = FMath::Lerp(1.0f, kernel[0].G, SubsurfaceColor.G); kernel[0].B = FMath::Lerp(1.0f, kernel[0].B, SubsurfaceColor.B); for (int i = 1; i < nTotalSamples; i++) { kernel[i].R *= SubsurfaceColor.R; kernel[i].G *= SubsurfaceColor.G; kernel[i].B *= SubsurfaceColor.B; }*/ } // 將正向權重結果輸出到TargetBuffer,刪除負向結果。 { check(kernel[0].A == 0.0f); // center sample TargetBuffer[0] = kernel[0]; // all positive samples for (uint32 i = 0; i < nNonMirroredSamples - 1; i++) { TargetBuffer[i + 1] = kernel[nNonMirroredSamples + i]; } } }
此文件還實現了ComputeTransmissionProfile
:
void ComputeTransmissionProfile(FLinearColor* TargetBuffer, uint32 TargetBufferSize, FLinearColor SubsurfaceColor, FLinearColor FalloffColor, float ExtinctionScale) { check(TargetBuffer); check(TargetBufferSize > 0); static float MaxTransmissionProfileDistance = 5.0f; // See MAX_TRANSMISSION_PROFILE_DISTANCE in TransmissionCommon.ush for (uint32 i = 0; i < TargetBufferSize; ++i) { //10 mm const float InvSize = 1.0f / TargetBufferSize; float Distance = i * InvSize * MaxTransmissionProfileDistance; FVector TransmissionProfile = SeparableSSS_Profile(Distance, FalloffColor); TargetBuffer[i] = TransmissionProfile; //Use Luminance of scattering as SSSS shadow. TargetBuffer[i].A = exp(-Distance * ExtinctionScale); } // Do this is because 5mm is not enough cool down the scattering to zero, although which is small number but after tone mapping still noticeable // so just Let last pixel be 0 which make sure thickness great than MaxRadius have no scattering static bool bMakeLastPixelBlack = true; if (bMakeLastPixelBlack) { TargetBuffer[TargetBufferSize - 1] = FLinearColor::Black; } }
ComputeMirroredSSSKernel
和ComputeTransmissionProfile
的觸發是在FSubsurfaceProfileTexture::CreateTexture
內,然後者又是在關卡加載時或者編輯器操做時觸發調用(也就是說預計算的,非運行時計算):
void FSubsurfaceProfileTexture::CreateTexture(FRHICommandListImmediate& RHICmdList) { // ... (隱藏了卷積前的處理代碼) for (uint32 y = 0; y < Height; ++y) { // ... (隱藏了卷積前的處理代碼) // 根據r.SSS.SampleSet的數值(0、一、2),卷積3個不一樣尺寸的權重。 ComputeMirroredSSSKernel(&TextureRow[SSSS_KERNEL0_OFFSET], SSSS_KERNEL0_SIZE, Data.SubsurfaceColor, Data.FalloffColor); ComputeMirroredSSSKernel(&TextureRow[SSSS_KERNEL1_OFFSET], SSSS_KERNEL1_SIZE, Data.SubsurfaceColor, Data.FalloffColor); ComputeMirroredSSSKernel(&TextureRow[SSSS_KERNEL2_OFFSET], SSSS_KERNEL2_SIZE, Data.SubsurfaceColor, Data.FalloffColor); // 計算透射剖面。 ComputeTransmissionProfile(&TextureRow[SSSS_TRANSMISSION_PROFILE_OFFSET], SSSS_TRANSMISSION_PROFILE_SIZE, Data.SubsurfaceColor, Data.FalloffColor, Data.ExtinctionScale); // ...(隱藏了卷積後的處理代碼) } }
此文件爲SeparableSSS.ush
定義了大量接口和變量,而且是調用SeparableSSS
的使用者:
// .... (隱藏其它代碼) #include "SeparableSSS.ush" // .... (隱藏其它代碼) // input0 is created by the SetupPS shader void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, out float4 OutColor : SV_Target0) { float2 BufferUV = UVAndScreenPos.xy; #if SSS_DIRECTION == 0 // horizontal float2 ViewportDirectionUV = float2(1, 0) * SUBSURFACE_RADIUS_SCALE; #else // vertical float2 ViewportDirectionUV = float2(0, 1) * SUBSURFACE_RADIUS_SCALE * (View.ViewSizeAndInvSize.x * View.ViewSizeAndInvSize.w); #endif #if MANUALLY_CLAMP_UV ViewportDirectionUV *= (View.ViewSizeAndInvSize.x * View.BufferSizeAndInvSize.z); #endif // 得到次表面散射顏色 OutColor = SSSSBlurPS(BufferUV, ViewportDirectionUV, false); #if SSS_DIRECTION == 1 // second pass prepares the setup from the recombine pass which doesn't need depth but wants to reconstruct the color OutColor.a = ComputeMaskFromDepthInAlpha(OutColor.a); #endif }
而且在調用MainPS
前,已經由其它代碼計算好了漫反射顏色,後續還會進行高光混合。若是在預計算卷積核以前就混合了高光,會獲得很差的渲染結果:
UE4的次表面散射雖然能提升很是逼真的皮膚渲染,但也存在如下限制(摘自官方文檔:次表面輪廓明暗處理模型):
該功能不適用於非延遲(移動)渲染模式。
將大屏幕設置爲散射半徑,將會在極端照明條件下顯示出帶狀瑕疵。
目前,沒有照明反向散射。
目前,當非SSS材質遮擋SSS材質時,會出現灰色輪廓。(經筆者測試,4.22.1並不會出現,見下圖)
本節將開始解析Mike的皮膚材質。皮膚材質主要是M_Head。
皮膚材質節點總覽
它的啓用了次表面散射的着色模型,此外,還開啓了與骨骼動做和靜態光一塊兒使用標記,以下:
對於基礎色,是由4張漫反射貼圖(下圖)做爲輸入,經過MF_AnimatedMapsMike輸出混合的結果,再除以由一張次表面散射遮罩圖(T_head_sss_ao_mask)控制的係數,最終輸入到Base Color引腳。
4張漫反射貼圖,每張都表明着不一樣動做狀態下的貼圖。
其中MF_AnimatedMapsMike是一個通用的材質函數,內部控制着不一樣動做下的貼圖混合權重,而混合不一樣動做參數的是m_headMask_01
、m_headMask_02
、m_headMask_03
三個材質函數:
而m_headMask_01
、m_headMask_02
、m_headMask_03
三個材質函數又分別控制了一組面部Blend Shape動做,其中以m_headMask_01
爲研究對象:
由上圖可見,m_headMask_01
有5張貼圖(head_wm1_msk_01 ~ head_wm1_msk_04,head_wm13_msk_03),利用它們的共19個通道(head_wm1_msk_04的alpha通道沒用上)提供了19組blend shape遮罩,而後它們與對應的參數相做用。
此外,m_headMask_02
有3張貼圖控制了10個Blend Shape動做;m_headMask_03
有3張貼圖控制了12個Blend Shape動做。
至於遮罩數據和blend shape參數如何計算,還得進入fn_maskDelta_xx
一探究竟,下面以fn_maskDelta_01
爲例:
不要被衆多的材質節點搞迷糊了,其實就是將每一個Blend Shape遮罩與參數相乘,再將結果與其它參數相加,最終輸出結果。抽象成公式:
\[ f = \sum_{i=1}^N m_i \cdot p_i \]
其中\(m_i\)表示第\(i\)個Blend Shape的遮罩值,\(p_i\)表示第\(i\)個Blend Shape的參數值。奏是辣麼簡單!
高光度主要由Mike_head_cavity_map_001的R通道提供,經過Power
和Lerp
調整強度和範圍後,再通過Fresnel
菲涅爾節點加強角色邊緣的高光反射(下圖)。
上述結果通過T_head_sss_ao_mask
貼圖的Alpha通道控制高光度和BaseSpecularValue
調整後,最終輸出到Specular
引腳。(下圖)
其中鼻子區域的高光度經過貼圖T_RGB_roughness_02
的R通道在原始值和0.8
之間作插值。
粗糙度的計算比較複雜,要分幾個部分來分析。
這部分跟基礎色相似,經過4張不一樣動做狀態的粗糙度貼圖(Toksvig_mesoNormal,Toksvig_mesoNormal1,Toksvig_mesoNormal2,Toksvig_mesoNormal3)混合成初始粗糙度值。
如上圖,由Toksvig_mesoNormal
的G通道加上基礎粗糙度BaseRoughness
,再進入材質函數MF_RoughnessRegionMult
處理後輸出結果。
其中,MF_RoughnessRegionMult
的內部計算以下:
簡而言之,就是經過3張mask貼圖(head_skin_mask4,T_siren_head_roughmask_02,T_siren_head_roughmask_01)的10個通道分別控制10個部位的粗糙度,而且每一個部位的粗糙度提供了參數調節,使得每一個部位在\([1.0, mask]\)之間插值。
上圖所示,RoughnessVariation
經過Mike_T_specular_neutral
的R通道,在Rough0
和Rough1
之間作插值;EdgeRoughness
則經過Fresnel
節點增強了角色視角邊緣的粗糙度;而後將它們和前倆小節的結果分別作相乘和相加。
如上圖,將紋理座標作偏移後,採用微表面細節貼圖skin_h
,接着增強對比度,並將值控制在\([0.85, 1.0]\)之間,最後與上一小節的結果相乘,輸出到粗糙度引腳。
其中微表面細節貼圖skin_h
見下:
首先須要說明,當材質着色模型是Subsurface Profile時,材質引腳Opacity的做用再也不是控制物體的透明度,而變成了控制次表面散射的係數。
由貼圖T_head_sss_ao_mask
的G通道(下圖)提供主要的次表面散射數據,將它們限定在[ThinScatter
,ThickScatter
]之間。
次表面散射遮罩圖。可見耳朵、鼻子最強,鼻子、嘴巴次之。
另外,經過貼圖T_RGB_roughness_02
的B、A通道分別控制上眼瞼(UpperLidScatter)和眼皮(LidScatter)部位的次表面散射係數。
與漫反射、粗糙度相似,法線的主要提供者也是由4張圖控制。
此外,還提供了微觀法線,以增長鏡頭很近時的皮膚細節。
主法線和微觀法線分別通過NormalStrength
和MicroNormalStrength
縮放後(注意,法線的z通道數據不變),再經過材質節點BlendAngleCorrectedNormals
將它們疊加起來,最後規範化輸入到法線引腳。(見下圖)
不妨進入材質節點BlendAngleCorrectedNormals
分析法線的混合過程:
從材質節點上看,計算過程並不算複雜,將它轉成函數:
Vector3 BlendAngleCorrectedNormals(Vector3 BaseNormal, Vector3 AdditionalNormal) { BaseNormal.b += 1.0; AdditionalNormal.rg *= -1.0; float dot = Dot(BaseNormal, AdditionalNormal); Vector3 result = BaseNormal * dot - AdditionalNormal * BaseNormal.b; return result; }
另外,Normal Map Blending in Unreal Engine 4一文提出了一種更簡單的混合方法:
將兩個法線的XY相加、Z相乘即獲得混合的結果。
AO控制很是簡單,直接用貼圖T_head_sss_ao_mask
的R通道輸入到AO引腳。其中T_head_sss_ao_mask
的R通道以下:
可見,五官內部、下顎、脖子、頭髮都屏蔽了較多的環境光。
前面能夠看到,皮膚渲染涉及的貼圖很是多,多達幾十張。
它們的製做來源一般有如下幾種:
掃描出的超高清貼圖。例如漫反射、高光、SSS、粗糙度、法線等等。
轉置貼圖。好比粗糙度、副法線、微觀法線等。
粗糙度貼圖由法線貼圖轉置而成。
遮罩圖。這類圖很是多,標識了身體的各個區域,以便精準控制它們的各種屬性。來源有:
PS等軟件製做。此法最傳統,也最容易理解。
插件生成。利用Blend Shape、骨骼等的權重信息,自動生成遮罩圖。
Blend Shape記錄了頂點的權重,能夠將它們對應的UV區域生成遮罩圖。