1. 程式人生 > >剖析Unreal Engine超真實人類的渲染技術Part 1 - 概述和面板渲染

剖析Unreal Engine超真實人類的渲染技術Part 1 - 概述和面板渲染

一、概述

1.1 數字人類的概要

數字人類(Digital Human)是利用計算機模擬真實人類的一種綜合性的渲染技術。也被稱為虛擬人類、超真實人類、照片級人類。

它是一種技術和藝術相結合的綜合性模擬渲染,涵蓋計算機圖形渲染、模型掃描、3D建模、肢體驅動、AI演算法等領域。

數字人類概念圖

1.1.1 數字人類的歷史和現狀

隨著計算機渲染技術的發展,數字人類在電影領域早有應用。在上世紀80年代的《星球大戰》系列、《異形》系列等電影,到後來的《終結者》系列、《黑客帝國》系列、《指環王》系列等,再到近期的漫威、DC動畫電影,都存在著虛擬角色的身影,他們或是天賦異稟的人類,或是奇形怪狀的怪物。

《星球大戰I》中的虛擬角色:尤達大師(Master Yoda)

《黑客帝國》的主角很多鏡頭是採用計算機渲染而成的虛擬數字人

電影《戰鬥天使》的畫面。主角阿麗塔也是虛擬角色。

由於近些年計算機硬體效能和渲染技術的提升,除了在離線渲染領域的電影和動漫的廣泛應用之外,實時領域的應用也得到長足的進步。例如,次世代遊戲、3A大作、VR遊戲以及泛娛樂領域的直播領域。

《孤島驚魂5》中的虛擬遊戲角色

R&D和暴雪在GDC2013展示的次世代虛擬角色

Unreal Engine在GDC2018展示的虛擬角色Siren,可由演員實時驅動動作、表情、肢體等資訊。

1.1.2 數字人類的製作流程

數字人類的步驟多,工序繁瑣。但總結起來,通常有以下幾個步驟:

  • 模型掃描。通常藉助光學掃描器或由單反相機組成的360度的攝影包圍盒,對掃描物件進行全方位的掃描,從而獲得原始的模型資料。

    上圖展示了模型掃描器,由很多攝影和燈光裝置組成的球形矩陣。

  • 模型調整。由掃描階段獲取的初始模型通常有瑕疵,無法直接投入渲染。需要美術人員利用3D建模工具(如Maya、3DMax等)進行調整、優化、重新拓撲,最終調整成合適的可用模型。

    左:掃描的初始模型;中:調整後的中間模型;右:優化了細節的可用模型。

  • 製作貼圖。在此階段,用建模軟體或材質製作軟體(如Substance)採納高精度模型烘焙或製作出漫反射、法線、粗糙度、AO、散射、高光等等貼圖,為最後的渲染做準備。這些貼圖的原始尺寸通常都非常大,4K、8K甚至16K,目的是高精度還原虛擬人類的細節。

    漫反射貼圖

    法線貼圖

  • 匯入引擎。在此階段,將之前製作的模型和貼圖匯入到渲染引擎(如UE4、Unity等),加入光照、材質、場景等元素,結合角色的綜合性PBR渲染技術,獲得最終成像。

    Unreal Engine渲染出的虛擬角色

1.2 Unreal Engine的數字人類

1.2.1 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》虛擬角色的細節,與數碼相機攝製的照片如出一轍

1.2.2 《Meet Mike》專案

筆者本想以《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個關節,大部分是為了頭髮的運動和臉部毛髮。

  • 臉部模型大約只用了10個關節,分別用在下巴、眼睛和舌頭,目的是為了運動更加圓滑。
  • 臉部使用了Technoprop公司先進的配有立體紅外攝像頭的固定在頭部的面部捕捉裝置。

  • 綜合使用了750個融合變形(blend shapes)。

  • 系統使用了複雜的傳統軟體和三種深度學習AI引擎。

二、面板渲染

面板渲染技術經過數十年的發展,由最初的單張貼圖+倫勃朗的渲染方式到近期的基於物理的SSSSS(螢幕空間次表面散射)。由此衍生出的面板渲染技術層出不窮,其中最最基礎也最具代表性的是次表面散射(SSS)。

在虛擬角色渲染中,面板的渲染尤為關鍵。因為面板是人們每天親眼目睹的非常熟悉的東西,如果稍微渲染不好或細節處理不足,便會陷入恐怖谷(Uncanny Valley )理論。至於什麼是恐怖谷理論,參看這裡。

上圖由於面板的細節處理不到位,陷入了恐怖谷理論

2.1 面板的構成和理論

2.1.1 面板構成

人類面板的物理構成非常複雜,其表層和內部都由非常複雜的構成物質,剖面圖如下:

  • 絨毛(hair shaft)。附著於面板表面的細小的毛。

  • 油脂(oil)。面板表層有一層薄薄的油脂覆蓋,是面板高光的主要貢獻者。

  • 表皮(epidermis)。油脂層下是表皮覆蓋,是造成次表面散射的物質之一。

  • 真皮(dermis)。表皮下面是真正的面板組織,也是造成次表面散射的物質之一。

  • 毛囊(hair follicle)。絨毛的皮下組織和根基。

  • 靜脈(vein)。呈深藍色的血管。

  • 動脈(artery)。呈暗紅色的血管。

  • 脂肪組織(fatty tissue)。脂肪組織也是造成次表面散射的次要貢獻物質。

  • 其它:面板表面的紋理、皺紋、毛孔、雀斑、痘痘、黑痣、疤痕、油脂粒等等細節。

    真實面板包含了非常多的細節:毛孔、絨毛、痘痘、黑痣、油脂......

2.1.2 面板建模

面板表面油脂層主要貢獻了面板光照的反射部分(約6%的光線被反射),而油脂層下面的表皮層和真皮層則主要貢獻了的次表面散射部分(約94%的光線被散射)。

雖然面板構成非常複雜,但圖形渲染界的先賢者們利用簡化的思維將面板建模成若干層。

  • 表面油脂層(Thin Oily Layer):模擬面板的高光反射。
  • 表皮層(Epidermis):模擬次表面散射的貢獻層。
  • 真皮層(Dermis):模擬次表面散射的貢獻層。

以上展示的是BRDF建模方式,只在面板表面反射光線,但實際上在三層建模中,還會考慮表皮層和真皮層的次表面散射(BSSRDF),見下圖中間部分BSSRDF。

2.1.3 面板渲染流程

面板渲染涉及的高階技術有:

  • 線性空間光照工作流。這部分可以參看《Technical Artist 的不歸路 —— 線性空間光照》。
  • 基於物理的光照(PBR)。這部分的理論和實踐可以參看筆者的另一篇技術文章:《由淺入深學習PBR的原理和實現》
  • 大量後處理。
  • 1~5個實時光照和1個預計算光照探頭。

面板渲染的過程可以抽象成以下步驟:

  • 面板反射。

    • 直接反射部分採用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更詳細描述。

2.2 次表面散射

次表面散射(Subsurface scattering)是模擬面板、玉石、牛奶等半透光性物質的一種物理渲染技術。

它與普通BRDF的區別在於,同一條入射光進入半透光性物質後,會在內部經過多次散射,最終在入射點附近散射出若干條光線。

由於R、G、B在物質內擴散的曲線不一樣,由此產生了與入射光不一樣的顏色。

紅色光由於穿透力更強,更容易在面板組織穿透,形成紅色光。

2.2.1 BSSRDF

BSSRDF是基於次表面散射的一種光照模型,充分考慮了入射光在物質內部經過若干次散射後重新反射出來的光。

左:BRDF;右:BSSRDF,考慮了輸入光在物質內散射後重新射出的若干條光

上圖描述了BRDF、BTDF、BSSRDF之間的關係:

  • BRDF:雙向反射分佈函式,用於表述在介質入射點的反射光照模型。
  • BTDF:雙向透射分佈函式,用於描述光線透過介質後的光照模型。
  • BSSRDF:雙向次表面反射分佈函式,用於描述入射光在介質內部的光照模型。
    • BSDF = BRDF + BTDF。
    • BSSRDF是BSDF的升級版。

下面兩圖展示了使用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)} \]

    • \(D\)是漫反射常量:
      \[ D=\frac{1}{3\sigma_t'} \]

由此可見,\(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公式更具體的理論、推導、簡化過程可參看下面兩篇論文:

  • A Practical Model for Subsurface Light Transport
  • BSSRDF Explorer: A Rendering Framework for the 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)的方法將其分解為一個行向量和一個列向量,並且保證了分解後的表示方法基本沒有能量損失。下圖展示了它的計算過程:

2.3 UE底層實現

本節將從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互動的處理。

2.3.1 SeparableSSS.ush

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是樣本數量,與配置相關,分別是6、9、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));
}

2.3.2 SeparableSSS.cpp

SeparableSSS.cpp主題提供了擴散剖面、透射剖面、高斯模糊計算以及映象卷積核的預計算。

為了更好地理解原始碼,還是先介紹一些前提知識。

2.3.2.1 高斯和的擴散剖面(Sum-of-Gaussians Diffusion Profile)

擴散剖面的模擬可由若干個高斯和函式進行模擬,其中高斯函式的公式:
\[ 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通道曲線的擴散範圍最遠,這也是面板顯示出紅色的原因。

2.3.2.2 原始碼分析

首先分析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;
    }
}

ComputeMirroredSSSKernelComputeTransmissionProfile的觸發是在FSubsurfaceProfileTexture::CreateTexture內,而後者又是在關卡載入時或者編輯器操作時觸發呼叫(也就是說預計算的,非執行時計算):

void FSubsurfaceProfileTexture::CreateTexture(FRHICommandListImmediate& RHICmdList)
{
    // ... (隱藏了卷積前的處理程式碼)

    for (uint32 y = 0; y < Height; ++y)
    {
        // ... (隱藏了卷積前的處理程式碼)
        
        // 根據r.SSS.SampleSet的數值(0、1、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);

        // ...(隱藏了卷積後的處理程式碼)
    }
}

2.3.3 PostProcessSubsurface.ush

此檔案為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前,已經由其它程式碼計算好了漫反射顏色,後續還會進行高光混合。如果在預計算卷積核之前就混合了高光,會得到不好的渲染結果:

2.3.4 UE次表面散射的限制

UE4的次表面散射雖然能提高非常逼真的面板渲染,但也存在以下限制(摘自官方文件:次表面輪廓明暗處理模型):

  • 該功能不適用於非延遲(移動)渲染模式。

  • 將大螢幕設定為散射半徑,將會在極端照明條件下顯示出帶狀瑕疵。

  • 目前,沒有照明反向散射。

  • 目前,當非SSS材質遮擋SSS材質時,會出現灰色輪廓。(經筆者測試,4.22.1並不會出現,見下圖)

2.4 面板材質解析

本節將開始解析Mike的面板材質。面板材質主要是M_Head。

面板材質節點總覽

它的啟用了次表面散射的著色模型,此外,還開啟了與骨骼動作和靜態光一起使用標記,如下:

2.4.1 基礎色(Base Color)

對於基礎色,是由4張漫反射貼圖(下圖)作為輸入,通過MF_AnimatedMapsMike輸出混合的結果,再除以由一張次表面散射遮罩圖(T_head_sss_ao_mask)控制的係數,最終輸入到Base Color引腳。

4張漫反射貼圖,每張都代表著不同動作狀態下的貼圖。

其中MF_AnimatedMapsMike是一個通用的材質函式,內部控制著不同動作下的貼圖混合權重,而混合不同動作引數的是m_headMask_01m_headMask_02m_headMask_03三個材質函式:

m_headMask_01m_headMask_02m_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的引數值。奏是辣麼簡單!

2.4.2 高光(Specular)

高光度主要由Mike_head_cavity_map_001的R通道提供,通過PowerLerp調整強度和範圍後,再經過Fresnel菲涅爾節點增強角色邊緣的高光反射(下圖)。

上述結果經過T_head_sss_ao_mask貼圖的Alpha通道控制高光度和BaseSpecularValue調整後,最終輸出到Specular引腳。(下圖)

其中鼻子區域的高光度通過貼圖T_RGB_roughness_02的R通道在原始值和0.8之間做插值。

2.4.3 粗糙度(Roughness)

粗糙度的計算比較複雜,要分幾個部分來分析。

2.4.3.1 動作混合的粗糙度

這部分跟基礎色類似,通過4張不同動作狀態的粗糙度貼圖(Toksvig_mesoNormal,Toksvig_mesoNormal1,Toksvig_mesoNormal2,Toksvig_mesoNormal3)混合成初始粗糙度值。

2.4.3.2 基於微表面的粗糙度

如上圖,由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]\)之間插值。

2.4.3.3 粗糙度調整和邊緣粗糙度

上圖所示,RoughnessVariation通過Mike_T_specular_neutral的R通道,在Rough0Rough1之間做插值;EdgeRoughness則通過Fresnel節點加強了角色視角邊緣的粗糙度;然後將它們和前倆小節的結果分別做相乘和相加。

2.4.3.4 微表面細節加強

如上圖,將紋理座標做偏移後,採用微表面細節貼圖skin_h,接著加強對比度,並將值控制在\([0.85, 1.0]\)之間,最後與上一小節的結果相乘,輸出到粗糙度引腳。

其中微表面細節貼圖skin_h見下:

2.4.4 次表面散射(Opacity)

首先需要說明,當材質著色模型是Subsurface Profile時,材質引腳Opacity的作用不再是控制物體的透明度,而變成了控制次表面散射的係數。

由貼圖T_head_sss_ao_mask的G通道(下圖)提供主要的次表面散射資料,將它們限定在[ThinScatter,ThickScatter]之間。

次表面散射遮罩圖。可見耳朵、鼻子最強,鼻子、嘴巴次之。

另外,通過貼圖T_RGB_roughness_02的B、A通道分別控制上眼瞼(UpperLidScatter)和眼皮(LidScatter)部位的次表面散射係數。

2.4.5 法線(Normal)

與漫反射、粗糙度類似,法線的主要提供者也是由4張圖控制。

此外,還提供了微觀法線,以增加鏡頭很近時的面板細節。

主法線和微觀法線分別經過NormalStrengthMicroNormalStrength縮放後(注意,法線的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相乘即得到混合的結果。

2.4.6 環境光遮蔽(Ambient Occlusion)

AO控制非常簡單,直接用貼圖T_head_sss_ao_mask的R通道輸入到AO引腳。其中T_head_sss_ao_mask的R通道如下:

可見,五官內部、下顎、脖子、頭髮都遮蔽了較多的環境光。

2.5 面板貼圖製作

前面可以看到,面板渲染涉及的貼圖非常多,多達幾十張。

它們的製作來源通常有以下幾種:

  • 掃描出的超高清貼圖。例如漫反射、高光、SSS、粗糙度、法線等等。

  • 轉置貼圖。比如粗糙度、副法線、微觀法線等。

    粗糙度貼圖由法線貼圖轉置而成。

  • 遮罩圖。這類圖非常多,標識了身體的各個區域,以便精準控制它們的各類屬性。來源有:

    • PS等軟體製作。此法最傳統,也最容易理解。

    • 外掛生成。利用Blend Shape、骨骼等的權重資訊,自動生成遮罩圖。

      Blend Shape記錄了頂點的權重,可以將它們對應的UV區域生成遮罩圖。

特別說明

  • 本系列還有眼球、毛髮、其它身體部位的分析,未完待續。
  • 感謝參考文獻的作者們。

參考文獻

  • Next-Generation-Character-Rendering (ACM Transactions on Graphics, Vol. 29(5), SIGGRAPH Asia 2010)
  • Separable Subsurface Scattering
  • Real-Time Realistic Skin Translucency
  • 《GPU Gems 3》:真實感面板渲染技術總結
  • 角色渲染技術——面板
  • 細緻到毛孔 ! 深度揭祕超真實面板的實時渲染技術(上篇)
  • 細緻到毛孔 ! 深度揭祕超真實面板的實時渲染技術(下篇)
  • 《由淺入深學習PBR的原理和實現》
  • Fast subsurface scattering
  • BRDF representation and acquisition
  • A BSSRDF Model for Efficient Rendering of Fur with Global Illumination
  • Parameter Estimation of BSSRDF for Heterogeneous Translucent Materials
  • NVIDIA官方展示HairWorks“海飛絲”(1.1 by Tarkan Sarim)
  • Q132:PBRT-V3,BSSRDF(雙向散射表面反射分佈函式)(5.6.2章節、11.4章節)
  • BSSRDF Explorer: A Rendering Framework for the BSSRDF
  • BSSRDF Importance Sampling
  • A Practical Model for Subsurface Light Transport
  • Digital Mike頭髮製作及渲染的深度揭祕
  • SIGGRAPH 2017|迄今為止最高品質實時數字人
  • NEXT Story S02.03 - 虛擬人(1)
  • 虛幻引擎在GDC教你做人
  • 數字人類
  • 照片級角色
  • 次表面輪廓明暗處理模型
  • Gaussian Models
  • 《Technical Artist 的不歸路 —— 線性空間光照》
  • Kim Libreri暢談虛擬製片、數字化人物和Epic Games的後續發展
  • Siren亮相FMX 2018:實時穿越《恐怖谷》
  • Star Wars
  • Facial Action Coding System
  • 探究《地獄之刃:塞娜的獻祭》(Hellblade: Senua's Sacrifice)背後的理念
  • EPIC win: previs to final in five minutes
  • 恐怖谷理論
  • Normal Map Blending in Unreal Engine 4