前言
出于興趣對游戲《Kena: bridge of spirits》的拆解還在繼續(xù)栏账,到目前為止已經(jīng)在Unity的URP中搭建了簡略版的UE4延遲渲染管線藐唠,雖然管線內(nèi)部還有許多“填空題”待完成鞋拟,但是框架算是完整了,作為核心一部分的幾個UberPass也完成了反解羊始,植入到了URP自己的 Stencil-Deffered Pass 中“镓遥現(xiàn)在要做的就是一塊一塊把缺失的部分補全。本文屬于是對Kena角色皮膚渲染方案(準確說是UE自己的SSSS渲染方案)的一點梳理和總結(jié)宵蛀,在這里分享出來昆著,同時也給自己備忘一下吧。
(1) UE4 次表面材質(zhì)方案的概述和特點
UE4用來渲染“真實質(zhì)感皮膚”或者“蠟質(zhì)表面”的著色模型可以大致整理為如下3種方案:
- Subsurface术陶,
- Preintegrated Skin宣吱,
- Subsurface Profile
它們間的效果依次遞進,但是性能消耗同樣也遞進瞳别。其中最后一項全名叫做:Subsurface Profile Shading (又稱為SSSS渲染),是UE自家電子人項目實際采用的渲染方案杭攻,可謂效果非凡祟敛。游戲《Kena》中人物皮膚材質(zhì)的渲染就使用了它。
SSSS相對傳統(tǒng)SSS渲染的主要區(qū)別在于:
- 要求延遲渲染兆解,不然很多全局參數(shù)獲取不便
- 是屏幕空間算法
- 要求輸入分離的Diffuse和Specular
- 算法核心的卷積(濾波)運算馆铁,工作在后處理之后,tonemapping之前
(2) 簡單回顧次表面(如何建模皮膚)
我們知道高光反射(Specular)是指光線在物體表面直接彈走的那部分光能锅睛,如下圖中黃色出射箭頭所示埠巨,由于未進入物體內(nèi)部與介質(zhì)相互作用,所以反射光波一般保持不變(入射光的樣子)现拒,這就是高光顏色一般為光源顏色的物理學解釋辣垒。
而漫反射(Diffuse)指的是另一部分(對于像皮膚這樣的電介質(zhì)來說是絕大部分)進入物體內(nèi)部的光能。它們在物體內(nèi)與介質(zhì)相互作用印蔬,經(jīng)過吸收勋桶,多次折射和反射后最終回到入射表面形成了漫反射。如下圖中藍色和淺藍色箭頭所示侥猬,由于在物體內(nèi)部傳輸導致的方向隨機性例驹,漫反射一般在出射方面是均勻分布于半球空間的,另一方面由于存在被介質(zhì)吸收的情況退唠,漫反射光通常也會被染色鹃锈,對應了不同材質(zhì)的F0
系數(shù)。
所謂次表面散射瞧预,其實說的還是漫反射屎债,只不過當我們站在更加微觀的尺度去觀察漫反射現(xiàn)象時仅政,不能再簡單的把入射點等同于出射點了。參考上圖綠色圓圈扔茅,它們定義了最小的觀察點(可類比于像素點)已旧,左側(cè)的觀察到涵蓋了入射和出射光線的絕大部分,所以對于藍色出射箭頭來說它們是漫反射召娜。右側(cè)的觀察點則無法包含大部分經(jīng)過介質(zhì)傳播后出射的藍色箭頭运褪,此時人們定義它們?yōu)榇伪砻嫔⑸洹?/p>
皮膚是典型的次表面材質(zhì):
簡言之,皮膚共可分為三層:
- 表皮油脂 (Thin Oily Layer):模擬皮膚的高光反射玖瘸。
- 表皮層 (Epidermis):模擬次表面散射的貢獻層秸讹。
- 真皮層 Dermis):模擬次表面散射的貢獻層。
油脂層主要貢獻了皮膚光照的反射部分(約6%的光線被反射)雅倒,而油脂層下面的表皮層和真皮層則主要貢獻了的次表面散射部分(約94%的光線被散射)璃诀。上圖分別展示了雙向反射看待問題的方式(左側(cè)BRDF)以及次表面散射處理問題的方式(右側(cè)BSSRDF),可見BRDF只考慮皮膚表面點反射光線的分布蔑匣,但實際上由于存在表皮層和真皮層的次表面散射(SSS)劣欢,次表面散射模型才能準確反應皮膚的光照分布。
(3)次表面光照模型(BSSRDF )
首先是皮膚的高光反射裁良,這部分相對來說變化較少凿将,一般直接采用Cook-Torrance的BRDF公式計算,UE是在UberPass的高光部分進行渲染的价脾,公式如下:
對應的光照方程如下:
因為不涉及次表面模型牧抵,具體不再贅述,感興趣的同學可以參考BRDF的渲染流程相關(guān)解讀侨把。
所謂的BSSRDF犀变,是 Bidirectional Surface Scattering Reflectance Distribution Function 的簡稱,既雙向次表面散射反射分布函數(shù)秋柄,它描述的是:當一束光以任意角度入射到某個確定的微表面p
上時获枝,有多少輻射率(Radiance)能夠從任意一個給定的微表面q
上以某個給定的出射角度反射出去。與之對應的光照方程如下:
這是一個嵌套的二重積分华匾,內(nèi)層對半球空域積分映琳,積分對象是入射光的角度微元dω
,這部分參考上面的BRDF光照方程蜘拉,可以認為是對微表面全部入射光通量進行積分求和的過程萨西;而外層積分的是一塊區(qū)域A
(Area),積分對象dA
代表一份微面元旭旭,如果把被積函數(shù)S
移到外側(cè)積分中去可以理解為是對處于區(qū)域A
內(nèi)的各個微面元貢獻的光照進行權(quán)重調(diào)節(jié)和加總谎脯,最終獲得等式右側(cè)的輻射率L0
(Radiance),它代表了處于指定位置p0
持寄,朝向指定出射方向ω0
的輻射通量有多少源梭,既攝像機能觀察到的光亮度娱俺。
于是次表面散射方程S
的通用定義可以寫作如下形式:
既方程S
能將位置xi
處以wi
角度入射的光能轉(zhuǎn)化為位置x0
處w0
方向的出射光能,別看它定義非撤下椋籠統(tǒng)寬泛荠卷,實驗科學家可以在定義的基礎上開展測試,描繪方程S
在不同維度的物理特性烛愧,輸出曲線油宜,形成所謂的 Ground-Truth 作為比對和參考!
特別的怜姿,當我們抽離出散射方程S
慎冤,并稍加推演后可以得到如下經(jīng)驗公式:
其中除法項表明了BSSRDF的本質(zhì)是:出射輻射率(Radiance)的微分與入射輻射通量(Radiant Flux)的微分之比。
式中Ft
是菲涅爾透射項沧卢,用于模擬入射和出射過程的損耗蚁堤,一般只和材質(zhì)屬性和角度有關(guān)。最后是Rd
但狭,既擴散反射函數(shù)本體披诗,它的物理含義是光線進入物體內(nèi)部經(jīng)過“多次”散射后最終穩(wěn)定形成的光線分布。實驗表明這種穩(wěn)定后的分布與入射出射點點精確位置無關(guān)立磁,與它們之間的距離有關(guān)藤巢。
完全遵循物理的次表面散射模擬會使得函數(shù)S
異常復雜而龐大,對于實時渲染來說要做的是盡可能所見控制變量息罗,同時尋找合適的快速擬合方式去逼近 Ground-Truth。下文介紹的SeparableSSS就是一種效果和性能俱佳的實時渲染解決方案才沧。
(4)聊聊SSSS(SeparableSSS)的原理與模型
其實在學術(shù)界迈喉,實時渲染狀態(tài)下的次表面散射模型大多具有以下2點特征:
- 先對每個像素進行一般的漫反射計算。
- 再根據(jù)某種特殊的函數(shù)
Rd(r)
和上述漫反射結(jié)果温圆,加權(quán)計算周圍若干個像素對當前像素的次表面散射貢獻挨摸。
換言之就是利用巧妙設計過的卷積核心,或在紋理空間里岁歉,或在屏幕空間中得运,對次表面材質(zhì)上累計的輻照度進行模糊操作,最終得到近似的次表面散射結(jié)果锅移。 進一步講熔掺,Rd(r)
函數(shù)與卷積核心是同一個概念,如上文所述非剃,正式學術(shù)名叫:擴散反射函數(shù)置逻。將函數(shù)繪制出來就得到了所謂的擴散剖面(Diffusion Profile)。如下圖所示:
對于一塊面積無限备绽,介質(zhì)均勻券坞,厚度無限的理想表面來說鬓催,當一速激光照射上去時會引發(fā)光線向周圍的擴散,形成以照射點為中心的穩(wěn)定光暈恨锚。利用儀器記錄這些分布在3D空間中的光強信息宇驾,使用圖標將數(shù)據(jù)直觀展示出來的話,大概能夠得到如下結(jié)果:
這里所說的“擴散剖面”本質(zhì)就是上圖投影到x-z或y-z平面上的一塊投影(剖面)猴伶。它直觀的反映了次表面散射模型中散射光強隨距離遠景的分布趨勢课舍,實驗表明這種趨勢只受到材質(zhì)屬性(波長)以及擴散距離(r
)的影響,與光線的入射/出射位置或者光線的入射/出射方向無關(guān)蜗顽,因而是各向同性(isotropy)的分布函數(shù)布卡。
回到SeparableSSS模型,它源自Jimenez和Gutierrez在2015年的論文中提出的實時BSSRDF方案雇盖,其主要貢獻是摒棄了非常耗時的2D
卷積忿等,將其成功替換為2個相關(guān)的1D
卷積,既所謂卷積分離(Separable Convolution)優(yōu)化方法崔挖。
參考上式贸街,A
是前面提及的Rd
擴散反射函數(shù)的一種近似(Approximation),x
和y
則對應了紋理空間的uv
展開狸相,Jimenez等人通過拆分近似函數(shù)A薛匪,獲得了2個彼此在不同維度上獨立展開的卷積內(nèi)核a(x)
和b(y)
。其拆分過程的合法性證明大致可歸納如下:
式中的E
代表輻照度(Irradiance)脓鹃,Rd
就是擴散反射函數(shù)逸尖,x
和y
是出射點位置,x'
和y'
則是積分變量瘸右,dx'dy'
代表了積分區(qū)域R^2
上的一塊微面元娇跟。整個第一個行對應了BSSRDF的光照方程。而在第一個約等號后太颤,我們看到Rd
被替換成了一個近似函數(shù)A
苞俘,它需要具有被降級拆分的能力,該函數(shù)原型后文會介紹龄章,此處先按下不表吃谣。再下面一行近似函數(shù) A
被展開成多組低維函數(shù)之和,每組低維函數(shù)是由控制x
通道和控制y
通道的卷積核相乘而得做裙。接下來將求和符號Σ
提到積分外并不重要岗憋,事實上我們在計算時會先完成求和再積分。最后一行是重點锚贱,由于卷積核a(x)
和a(y)
彼此獨立澜驮,沒有依賴關(guān)系,故而我們可以將面積積分R^2
拆分成x
軸向和y
軸向上的2次1D
積分惋鸥,從而極大提高積分效率杂穷,這就是SSSS算法的核心目的了悍缠。
為了確定A
,必須先找到靠譜的Rd
耐量,那么到底如何在數(shù)學上定義擴散反射函數(shù)呢飞蚓?這就涉及到Jensen在2001年提出的散射模型了[Jenson et al.2001],由于存在大量公式推演和解讀廊蜒,受限于篇幅和精力趴拧,我就從簡介紹了。Jensen在建模擴散反射函數(shù)時引入了偶極子(Dipole)的概念山叮,如下圖:
在當前語境下著榴,所謂偶極子指的是一對互為正負的點光源。Jensen將其中的正極(positive real light source)放置在表皮平面的下方Zr
深度處屁倔,理想狀態(tài)下正光源向外均勻輻射光強脑又,同時受到介質(zhì)內(nèi)部散射的衰減。另一方面Jensen將負極(negative virtual light source)放置在了表皮平面與正極對立的另一側(cè)锐借,高度為Zv
涝登,這個虛擬的光源負責吸收從表面散射出的光強撕捍,從而調(diào)節(jié)強度分布衔憨。在正負極子共同作用下烟具,表面某處(x
)的的光通量可以表示為下方等式:
其中σtr
是透射吸收因子,大D
是散射常數(shù)布轿,dr
是點(x
)到正極的直線距離哮笆,dv
是點(x
)到負極的直線距離。Jensen通過計算距離和調(diào)節(jié)系數(shù)汰扭,模擬出了次表面散射中出Rd
表數(shù)學定義:
很顯然疟呐,這是個復雜的表達式,而且或許對于諸如牛奶或大理石這類材質(zhì)东且,一個偶極子剖面足以描述散射分布,但是對于皮膚這樣多層結(jié)構(gòu)的材質(zhì)本讥,一般需要通過布置3個偶極子才能達到理想的效果珊泳,這進一步增加了計算公式的復雜度,綜合來說不適合直接拿來做實時渲染拷沸。
現(xiàn)在是時候回過頭看看我們的擴散剖面圖:
不難發(fā)現(xiàn)色查,擴散剖面輪廓線類似于高斯函數(shù)(Gaussians),SSSS模型正是通過多個不同參數(shù)的高斯函數(shù)相互疊加來擬合的撞芍!實踐表明秧了,雖然單個高斯分布不能精確的描述擴散分布,但將多個不同的高斯分布加權(quán)在一起是可以對擴散剖面提供極好的近似的序无。高斯函數(shù)表達式具有一些很好的特性:當我們將擴散剖面表示為高斯和(Sum-of-Gaussians Diffusion)時验毡,可以非常有效地求解次表面散射衡创,因為它們同時滿足了可分離性和徑向?qū)ΨQ性,天然滿足卷積分離目標需求晶通。因此利用一些數(shù)學工具(諸如奇異值分解 Singular Value Decomposition)璃氢,Jimenez等人分離出了6個高斯函數(shù)用來擬合皮膚等帶有3層偶極子的剖面(Dipole Prolie),并得到了不錯的效果狮辽。
其中高斯函數(shù)定義如下:
均值為0一也,方差vi
,權(quán)重wi
用于控制參與求和的高斯函數(shù)形狀喉脖,r
是散射距離椰苟。通過調(diào)節(jié)函數(shù)的各項特征,經(jīng)過加權(quán)和之后獲得擬合結(jié)果树叽,這個過程的示意圖如下:
SSSS模型在論文中對皮膚的擬合結(jié)果給出了如下的最終參數(shù)舆蝴,可見R、G菱皆、B通道擬合出的曲線有所不同须误,而R通道曲線的擴散范圍最遠,這也是皮膚顯示出紅色的原因:
需要注意的是仇轻,對于每個顏色通道的剖面京痢,高斯項的權(quán)重和為1.0
。這是因為處于性能考慮篷店,我們在實際工程應用中祭椰,只使用紅色通道的擬合值去做高斯模糊,因為即便如此疲陕,對于藍綠色的散射效果也沒有明顯的瑕疵方淤。
下式是擬合后求取Rd
近似函數(shù)值的公式,該函數(shù)基于6個高斯函數(shù)的加權(quán)和蹄殃,入?yún)?code>r的單位是mm携茂,代表入射點到出射點之間的直線距離。
額外2點:
(1)關(guān)于內(nèi)核大凶缪摇:
對于一個簡單的屏幕空間模糊操作讳苦,通常使用一個固定大小的內(nèi)核(常量尺寸)就足夠了,然而這在SSS的情況下是不可能的吩谦,因為內(nèi)核通常代表一些基于物理的漫反射剖面鸳谜。因此,有必要視不同的區(qū)域?qū)傩赃m時調(diào)整內(nèi)核尺寸式廷。卷積核大小是由三個因素的乘積決定的咐扭,即內(nèi)核縮放系數(shù)(Kernal Scale)、SSS寬度 (SSS Width)和SSS強度(SSS Strength)。如果只是簡單的使用固定大小內(nèi)核對次表面像素作卷積蝗肪,那么得到的就是一個恒定的整體模糊的表現(xiàn)效果袜爪,完全沒有表面透視投影造成的區(qū)域畸變。對于每個屏幕空間表面像素來說穗慕,它們與相機的距離可能各不相同饿敲,加上透視投影帶來的縮放效果,每一個像素所代表的真實次表面面積顯然是可能不一樣的逛绵。因此怀各,有必要針對每個像素進行內(nèi)核縮放,從而適應表面區(qū)域所關(guān)聯(lián)的實際面積术浪。
內(nèi)核縮放系數(shù)(Kernal Scale)-> 定義如下:
其中fy
是攝像機的 field-of-view瓢对,既視場角,pd
代表了像素深度胰苏,公式含義是硕蛹,內(nèi)核尺寸應當隨著像素的深度增加而變小,同時隨著視場角的變小而增大硕并。
SSS寬度 -> 是用戶定義的次表面寬度法焰,用于框定在什么樣的尺度上能夠看到次表面效果(類似定義了最遠傳播距離)。
SSS強度 -> 也是用戶定義的次表面強度值倔毙,它通常以皮膚貼圖的形式存在埃仪,用來人為規(guī)定不同地方的次表面強度變化。
(2)幾何過濾:
屏幕空間連續(xù)的對次表面材質(zhì)不一定在幾何空間時連續(xù)的陕赃,因此SSS渲染必須考慮這種幾何上的跳變差異卵蛉,不然就有可能混淆幾何上迥異的2塊次表面材質(zhì),造成過渡不自然么库。使用的方案是比較當前采樣點(in-center sample)和周圍采樣點(off-center sample)的像素顏色和深度值:
其中ic
是當前像素點顏色傻丝,oc
是周邊像素顏色,id
是當前點深度诉儒,od
是周邊像素點深度葡缰。
(5) UE的工程化方案
5.1 渲染流程
SSSS后處理流程示意:核心是兩次分離,既對Diffuse和Specular的分離忱反,以及對2D
卷積核的分離泛释。
SSSS后處理流程在Renderdoc截幀中展示的順序:核心是2次應用高斯模糊的Pass。
SSSS完整渲染流程:
- MRT pass輸出UE4 Deffered shading GBuffer所需數(shù)據(jù)
- 在UberPass中(AmbientCubemap缭受,GI,DirLight)計算并分離出次表面材質(zhì)的Diffuse部分和Specular部分该互,在Separable Half-Res模式下米者,這2部分數(shù)據(jù)被分別編碼在了原始分辨率下的不同棋盤格中
- SSSS后處理的第一步是在半分辨率(Half-Res)下解碼出次表面材質(zhì)對應的Diffuse,雖說是Diffuse,實際上是表面輻照度蔓搞,因此偏亮
- 后面經(jīng)過2個Pass進行高斯模糊處理胰丁,高斯核是預計算的,存放在了名為"ActualSSProfilesTexture"的查找表中
- 最后將模糊和衰減后的次表面散射強度升采樣到原始分辨率喂分,與基礎顏色混合锦庸,同時疊加上Specular部分
5.2 計算分離的Diffue和Specular
(一)實現(xiàn)高光與漫反射分離:
在UberPass中拆分Diffuse和Specular的做法首先起始于如下函數(shù),CheckerFormPixelPos用于確定每一個次表面像素的歸屬蒲祈,返回“true”或“false”甘萧。
bool CheckerFromPixelPos(uint2 PixelPos)
{
uint TemporalAASampleIndex = View.TemporalAAParams.x;
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
return (PixelPos.x + PixelPos.y + TemporalAASampleIndex) % 2;
#else
return (uint)(fmod(PixelPos.x + PixelPos.y + TemporalAASampleIndex, 2)) != 0;
#endif
}
同時為了借助TAA在時間域上的積累,彌補半分辨率的損失梆掸,UE在這個函數(shù)中還引入了TAASampleIndex
變量扬卷,確保前后兩幀之間的棋盤格返回值正好是互逆的。
那么UE是如何使用這個棋盤紋理去影響UberPass中正常計算的直接光和間接光的呢酸钦?很簡單怪得,就在解碼GBuffer數(shù)據(jù)的時候。具體來說會依據(jù)當前像素是否是次表面材質(zhì)來開啟如下分支邏輯卑硫,所做處理就是依據(jù)棋盤狀態(tài)bChecker
徒恋,判斷是否將基礎色(BaseColor)或高光強度(Specluar)等核心參數(shù)設置為0
,從而控制最終寫入到colorAttachement上的像素顏色值是否包含有Diffuse或Specular分量的數(shù)據(jù)欢伏。
FGBufferData DecodeGBufferData(..., bool bChecker)
{
...
if (UseSubsurfaceProfile(GBuffer.ShadingModelID)) //對次表面材質(zhì)來說入挣,會進入此分支
{ //bChecker:對應棋盤狀紋理 -> 這里需要開啟或屏蔽皮膚像素對應的 BaseCol,SpecCol等原始數(shù)據(jù)
AdjustBaseColorAndSpecularColorForSubsurfaceProfileLighting(GBuffer.BaseColor, GBuffer.SpecularColor, GBuffer.Specular, bChecker);
}
...
}
(二)SubsurfaceProfileBXDF 基本流程
這一塊專職于皮膚表面的高光處理颜懊,和前面長篇大論的次表面剖面沒關(guān)系财岔,而且對于非真實質(zhì)感的皮膚渲染幫助也不大(Kena的皮膚并非完全的真實質(zhì)感,有點類似迪士尼卡通風格)河爹,但是作為構(gòu)成UE SSSS 材質(zhì)完整渲染流程的一個重要組成部分匠璧,還是得簡要介紹一下的。
UE的皮膚渲染采用雙鏡葉高光(Dual Lobe Specular)咸这,它是由兩個獨立的高光鏡葉組成夷恍,各自使用不同的粗糙度,二者的線性和形成最終結(jié)果媳维。從真實膚質(zhì)渲染效果來看酿雪,這種組合方式會為皮膚提供非常出色的亞像素微頻效果,程序出更加自然的面貌侄刽。
UE的默認混合公式是:Lobe_0 * 0.85 + Lobe_1 * 0.15
指黎,但是在工程上UE會將用戶的預設值存入查找表中,在運行時實時采樣獲戎莸ぁ:
void GetProfileDualSpecular(FGBufferData GBuffer, out float AverageToRoughness0, out float AverageToRoughness1, out float LobeMix)
{
// 0..255, which SubSurface profile to pick
uint SubsurfaceProfileInt = ExtractSubsurfaceProfileInt(GBuffer); //角色索引
float4 Data = ActualSSProfilesTexture.Load(int3(SSSS_DUAL_SPECULAR_OFFSET, SubsurfaceProfileInt, 0));
AverageToRoughness0 = Data.x * SSSS_MAX_DUAL_SPECULAR_ROUGHNESS;
AverageToRoughness1 = Data.y * SSSS_MAX_DUAL_SPECULAR_ROUGHNESS;
LobeMix = Data.z;
}
上述方法用于采樣獲取當前像素對應角色的專屬皮膚高光參數(shù)醋安,如前文所述杂彭,一共有3個參數(shù):針對Lobe_0
的粗糙度系數(shù),針對Lobe_1
的粗糙度系數(shù)吓揪,以及一個用于混合雙鏡葉的LobeMix
系數(shù)亲怠。
接下來是高光部分的漫反射Dissue,使用迪士尼Burley在2012年提出的“基于物理渲染”一文中的經(jīng)典方案:
對此公式此處不再贅述柠辞,有興趣的同學可以翻看迪士尼BRDF相關(guān)文章团秽。
高光項由于是雙鏡葉的關(guān)系,要稍微復雜些:
float3 DualSpecularGGX(float AverageRoughness, float Lobe0Roughness, float Lobe1Roughness, float LobeMix, float3 SpecularColor, BxDFContext Context, float NoL, FAreaLight AreaLight)
{
float AverageAlpha2 = Pow4(AverageRoughness);
float Lobe0Alpha2 = Pow4(Lobe0Roughness);
float Lobe1Alpha2 = Pow4(Lobe1Roughness);
float Lobe0Energy = EnergyNormalization(Lobe0Alpha2, Context.VoH, AreaLight);
float Lobe1Energy = EnergyNormalization(Lobe1Alpha2, Context.VoH, AreaLight);
// Generalized microfacet specular
float D = lerp(D_GGX(Lobe0Alpha2, Context.NoH) * Lobe0Energy, D_GGX(Lobe1Alpha2, Context.NoH) * Lobe1Energy, LobeMix);
float Vis = Vis_SmithJointApprox(AverageAlpha2, Context.NoV, NoL); // Average visibility well approximates using two separate ones (one per lobe).
float3 F = F_Schlick(SpecularColor, Context.VoH);
return (D * Vis) * F;
}
還是基于 Cook-Torrance 模型的高光計算叭首,有微表面法線分布D
习勤,幾何項G
(這里是Visibility),以及菲尼爾項F相乘構(gòu)成放棒。由于涉及雙高光姻报,最開始計算了不同Lobe的alpha^2
值和歸一化能量系數(shù)。UE4接下來使用了兩次由 Trowbridge-Reitz 提出的各向同性GGX方法:
并通過插值(Lerp)來獲得最終的D间螟,注意這里直接使用了從查找表里采樣到的 LobeMix
作為比例系數(shù)吴旋。
幾何項Vis使用了對經(jīng)典的 Joint-Smith項(聯(lián)合史密斯)的一種近似高效方案:
最后的菲尼爾項沒什么特別,使用了那個要對 VdotH
返回值執(zhí)行5次方的Schlick-Fresnel方案厢破,此處也不贅述了荣瑟。
UE將上述3項合成并返回,一次性完成2組Lobe高光的計算摩泪。
5.3 卷積核的計算和使用
(一)LUT
我們最好先梳理下貫穿了UE4次表面材質(zhì)渲染始末的這張LUT圖笆焰,或者叫SSProfilesTexture(次表面剖面紋理)。簡單來說见坑,查找表的每一行代表了一種獨特的次表面材質(zhì)嚷掠,而行中的每一個元素(逐列)存放了不同的預設值或預計算參數(shù),我通過解析源碼的方式還原了表中各列的含義荞驴,具體參考下圖:
上文在介紹雙鏡葉高光計算時其實已經(jīng)使用到了它不皆,當時我們通過在這張LUT圖X
軸坐標偏移為5
的地方進行采樣編碼數(shù)據(jù)Data
,從Data.xy
中獲得需要的兩層粗糙度熊楼,又從Data.z
中獲得混合系數(shù)霹娄。
回到我們現(xiàn)在關(guān)心的次表面卷積模糊處理,UE采樣的是上圖中存放在Kernel_0鲫骗,Kernel_1或Kernel_2為起始地址的一連串預計算卷積結(jié)果犬耻。具體而言,3個Kernel偏移對應了3種不同的SSSS質(zhì)量:
#if SUBSURFACE_QUALITY == SUBSURFACE_QUALITY_LOW // <- Kena 使用此設置
#define SSSS_N_KERNELWEIGHTCOUNT SSSS_KERNEL2_SIZE //6
#define SSSS_N_KERNELWEIGHTOFFSET SSSS_KERNEL2_OFFSET //28
#elif SUBSURFACE_QUALITY == SUBSURFACE_QUALITY_MEDIUM
#define SSSS_N_KERNELWEIGHTCOUNT SSSS_KERNEL1_SIZE //9
#define SSSS_N_KERNELWEIGHTOFFSET SSSS_KERNEL1_OFFSET //19
#else // SUBSURFACE_QUALITY == SUBSURFACE_QUALITY_HIGH
#define SSSS_N_KERNELWEIGHTCOUNT SSSS_KERNEL0_SIZE //13
#define SSSS_N_KERNELWEIGHTOFFSET SSSS_KERNEL0_OFFSET //6
#endif
而Kernel內(nèi)存放的數(shù)據(jù)就Rd
(擴散反射函數(shù))的近似計算結(jié)果执泰,上文說過了枕磁,該函數(shù)是基于6
個高斯函數(shù)的加權(quán)和,入?yún)的單位是mm
术吝,代表入射點到出射點之間的直線距離计济。我們來看看UE4是怎么計算它的:
// helper function for ComputeMirroredSSSKernel
// r is in mm
inline FVector SeparableSSS_Profile(float r, FLinearColor FalloffColor)
{
/**
* We used the red channel of the original skin profile defined in
* [d'Eon07] for all three channels. We noticed it can be used for green
* and blue channels (scaled using the falloff parameter) without
* introducing noticeable differences and allowing for total control over
* the profile. For example, it allows to create blue SSS gradients, which
* could be useful in case of rendering blue creatures.
*/
// first parameter is variance in mm^2
return // 0.233f * SeparableSSS_Gaussian(0.0064f, r, FalloffColor) + /* We consider this one to be directly bounced light, accounted by the strength parameter (see @STRENGTH) */
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);
}
除了第一項被排除以外晴楔,其他一模一樣,權(quán)重系數(shù)和紅色通道的對應方差在數(shù)值上一點不差峭咒。至于為何去除第一項,UE的說法是需要將其定義為直接反射光的范疇里纪岁,但其實是為了給UE自己附加的各種調(diào)控系數(shù)騰挪空間凑队。
高斯核心(Kernel)具體裝填過程如下:
...
// 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;
kernel[i].A = Range * sign * FMath::Abs(FMath::Pow(o, Exponent)) / FMath::Pow(Range, Exponent);
}
// Calculate the weights:
for (int32 i = 0; i < nTotalSamples; i++)
{
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;
FVector t = area * SeparableSSS_Profile(kernel[i].A, FalloffColor);
kernel[i].R = t.X;
kernel[i].G = t.Y;
kernel[i].B = t.Z;
}
...
簡單說分兩步計算:
依據(jù)已有參數(shù)計算出步進,得到高斯函數(shù)的入?yún)⒆兞?code>r 幔翰,
r
對應了散射過程中入射點到出射點之間的直線距離漩氨,單位是mm
-> 對應 Kernel.a使用上一步計算出的距離
r
和一個用來控制RGB通道散射強度的 FalloffColor(紅光最遠,所以紅色分量占優(yōu))遗增,預計算不同距離條件下的光強分布
-> 對應 Kernel.rgb
(二)卷積核采樣
SSSSBlurPS是包含了卷積采樣主邏輯的函數(shù)叫惊,2個卷積Pass都調(diào)用到了它,但是在不同Pass執(zhí)行期間做修,函數(shù)入?yún)⒅械?dir
會有差異霍狰,具體而言第一次dir
被設置為 float2(1, 0)
,第二次則是 float2(0, 1)
饰及,代表著分別沿著紋理空間中的u
軸朝向和v
軸朝向做高斯模糊蔗坯,既所謂的分離的1D
卷積。
函數(shù)的核心邏輯有興趣的同學可以參考如下代碼:
// @param dir Direction of the blur: First pass: float2(1.0, 0.0), Second pass: float2(0.0, 1.0)
float4 SSSSBlurPS(float2 BufferUV, float2 dir)
{
...
// 獲取藝術(shù)家通過貼圖傳遞來的次表面強度值:0..1
float SSSStrength = GetSubsurfaceStrength(BufferUV);
// finalStep用于確定如何步進燎含,以便采樣四周輻照度
// 步進反比與“像素深度”宾濒,正比于“次表面強度”,而SubsurfaceParams.x存放的是全局縮放系數(shù)
float2 finalStep = SubsurfaceParams.x / PixelDepth * dir * SSSStrength;
FGBufferData GBufferData = GetGBufferData(BufferUV);
// 當前像素對應的散射剖面索引:0..255屏箍,舉例绘梦,Kena的皮膚和飛鳥的皮膚分屬不同的散射剖面
uint SubsurfaceProfileInt = ExtractSubsurfaceProfileInt(GBufferData);
...
// 對當前卷積窗口的中心點采樣,使用第一個Kernel來初始化如下變量
// InvDiv 用于累積誤差赴魁,確保卷積結(jié)果能量守恒卸奉,因為在一些情況下卷積窗口可能超出了材質(zhì)范圍
colorInvDiv += GetSubsurfaceProfileKernel(SSSS_N_KERNELWEIGHTOFFSET, SubsurfaceProfileInt).rgb;
// Accum 用于累積散射能量,并最終被InvDiv修正
colorAccum = colorM.rgb * GetSubsurfaceProfileKernel(SSSS_N_KERNELWEIGHTOFFSET, SubsurfaceProfileInt).rgb;
float3 BoundaryColorBleed = GetSubsurfaceProfileBoundaryColorBleed(GBufferData); //處理邊界點時所用的替代顏色
//卷積主循環(huán)
for (int i = 1; i < SSSS_N_KERNELWEIGHTCOUNT; i++) //COUNT == 6 -> 共需要步進5次尚粘,采樣對應的Kernel
{
// Kernel.a = 0..SUBSURFACE_KERNEL_SIZE (radius) -> 對應距離r
// Kernel.rgb 對應各顏色通道在給定距離r時的散射比率
half4 Kernel = GetSubsurfaceProfileKernel(SSSS_N_KERNELWEIGHTOFFSET + i, SubsurfaceProfileInt);
float4 LocalAccum = 0;
float2 UVOffset = Kernel.a * finalStep; //這是UV偏移的絕對值
//由于是對稱的择卦,一個Kernel需要連續(xù)采樣正負偏移下的坐標點
for (int Side = -1; Side <= 1; Side += 2)
{
float2 LocalUV = BufferUV + UVOffset * Side;
float4 color = SSSSSampleSceneColor(LocalUV); //采樣獲得附近點的輻照度(注意a通道存放了像素深度)
// 需要排除的情況(一):附近獲取的采樣點與當前點不屬于同一個散射剖面(或者說不屬于同一個角色)
uint LocalSubsurfaceProfileInt = SSSSSampleProfileId(LocalUV);
float3 ColorTint = LocalSubsurfaceProfileInt == SubsurfaceProfileInt ? 1.0f : BoundaryColorBleed;
float LocalDepth = color.a;
color.a = GetMaskFromDepthInAlpha(color.a);
// 需要排除的情況(二):附近獲取的采樣點深度與當前點深度差距過大(幾何差距過大,默認也不屬于同一個角色)
float s = saturate(12000.0f / 400000 * SubsurfaceParams.y * abs(PixelDepth - LocalDepth));
color.a *= 1 - s;
color.rgb *= color.a * ColorTint; //這里的ColorTin用于處理邊界顏色郎嫁,非邊界狀態(tài)時恒為1
LocalAccum += color;
}
// 卷積的過程被近似為連續(xù)求和的過程
colorAccum += Kernel.rgb * LocalAccum.rgb;
colorInvDiv += Kernel.rgb * LocalAccum.a;
}
// 歸一化秉继,返回
float3 OutColor = colorAccum / colorInvDiv;
return float4(OutColor, PixelDepth);
}
代碼雖長,但是理解起來并不麻煩泽铛,個人認為主要看點是這幾個:
- 采樣步進 finalStep:該值定義了在
uv
空間內(nèi)一次采樣偏移的單位長度尚辑。步進越大,該點的模糊效果(SSS效果)可能越明顯盔腔。- 它正比于全局縮放因子 SSSScale
- 它正比于由藝術(shù)家提供的 SSSStrength
- 它反比于像素的深度
- 它受到卷積朝向的控制杠茬,只在
u
或v
軸上展開
- 卷積主循環(huán) :由內(nèi)外兩層循環(huán)嵌套而成月褥,形成卷積窗口
- 外層負責采樣Kernel(卷積核)
- 內(nèi)層依據(jù)Kernel中的預計算偏移量負責采樣正反兩個方向的周邊區(qū)域輻照度
- 累加輻照度與對應次表面散射強度的乘積,獲得周邊區(qū)域?qū)Ξ斍包c的散射貢獻總和
- 受不同質(zhì)量設置的控制瓢喉,總循環(huán)次數(shù)可以在
12
次宁赤,18
次到26
次之前切換
5.4 合并 + 升采樣
下面的代碼用于從棋盤格中重建全分辨率光照,是經(jīng)過精簡的栓票,只保留了采樣雙鄰域的版本决左,在更高質(zhì)量檔是需要采樣上下左右四鄰域并加權(quán)混合的。從這個方法中我們能看到UE4使用了當前幀的棋盤格狀態(tài)符 bChecker
來控制插值器在“當前采樣值Quant0
”還是“雙鄰域采樣加權(quán)值Quant1
”之間切換走贪,從而填充Diffuse和Specular佛猛,升采樣到全分辨率。
SDiffuseAndSpecular ReconstructLighting(float2 UVSceneColor)
{
bool bChecker = CheckerFromSceneColorUV(UVSceneColor);
float3 Quant0 = LookupSceneColor(UVSceneColor, int2(0, 0));
float3 Quant1 = 0.5f * (
LookupSceneColor(UVSceneColor, int2( 1, 0)) +
LookupSceneColor(UVSceneColor, int2(-1, 0))
);
SDiffuseAndSpecular Ret;
Ret.Diffuse = lerp(Quant1, Quant0, bChecker);
Ret.Specular = lerp(Quant0, Quant1, bChecker);
return Ret;
}
最后坠狡,說完升采樣我們再來說說合并继找,UE的合并分為兩步:
- 第一步是將卷積后和散射強度與原始Diffuse做合并,使用的方式是線性插值(Lerp)逃沿,比例系數(shù)主要來自計算得出的LerpFactor婴渡,UE為了避免在較遠距離時次表面材質(zhì)因為模糊而看不清,通過修正插值系數(shù)LerpFactor來達到隨著距離拉遠對不同尺寸的次表面材質(zhì)逐漸“去模糊化”的效果凯亮。ComputeFullResLerp方法內(nèi)部使用到了“像素深度”缩搅,“全局調(diào)節(jié)系數(shù)”和“預計算的次表面弧度(radius)”等參數(shù),具體實現(xiàn)方式此處不再深入触幼。
- 第二步則是對最終輸出顏色的合并硼瓣,既
OutColor = Diffuse + Specluar
其中Diffuse對應了我們的散射強度,Specular則帶有兩層鏡葉高光置谦。
void SubsurfaceRecombinePS(...)
{
...
float4 SSSColor = Texture2DSample(SubsurfaceInput1_Texture, SharedSubsurfaceSampler1, BufferUV); //符合次表面散射剖面的SSS強度值
// 避免在遠距離時次表面材質(zhì)因為模糊而看不清堂鲤,這里修正了權(quán)重因子,方法內(nèi)部使用到了“像素深度”媒峡,“全局調(diào)節(jié)系數(shù)”和“預計算的次表面弧度(radius)”等參數(shù)
LerpFactor = ComputeFullResLerp(ScreenSpaceData, BufferUV, SubsurfaceInput1_ExtentInverse);
...
SDiffuseAndSpecular DiffuseAndSpecular = ReconstructLighting(BufferUV, ReconstructMethod); //重建全分辨率 Diffuse + Specular
float3 ExtractedNonSubsurface = DiffuseAndSpecular.Specular;
float3 SubsurfaceColor = GetSubsurfaceProfileColor(ScreenSpaceData.GBuffer); //查找LUT圖瘟栖,獲取 SUBSURFACE_COLOR 對應顏色
float3 FadedSubsurfaceColor = SubsurfaceColor * LerpFactor;
// combine potentially half res with full res
float3 SubsurfaceLighting = lerp(DiffuseAndSpecular.Diffuse, SSSColor, FadedSubsurfaceColor); //注意只涉及 Diffuse 部分
OutColor = float4(SubsurfaceLighting * StoredBaseColor + ExtractedNonSubsurface, 0); //合成 Diffuse 和 Specular
}
5.5 MRT部分
MRT材質(zhì)有藝術(shù)家和TA自定義的成分較大,但是為了正確通過SSSS渲染皮膚谅阿,UE會要求如下輸入紋理:
下圖是一些涉及SSSS的參數(shù):
Demo
Ref
1:docs.unrealengine
2:剖析UE超真實皮膚渲染技術(shù)
3:Separable Subsurface Scattering
4:虛幻引擎中的皮膚渲染方案
5:GPU Gems 3