前言
縱然前文提及的Disney BRDF可以用來描述(渲染)世間絕大多數不透明幾何物體,但是一套BRDF也絕不是靈丹妙藥精刷,解決不了所有渲染上的需求。比如角色渲染中非常重要的皮膚處理疯搅,人們一般不會自縛手腳將通用的PBR渲染方程套用在這類“特殊”材質上(需求和判斷標準與傳統BRDF渲染結果有較大差異)崎场。本文簡述了一種主流的基于預積分次表面散射(SSS)的渲染方案,介紹中會提及SSS效果的成因砂竖,以及如何利用數學模型對這種物理現象進行描述真椿,另外還結合《GPU Pro 2》中的“Pre-Integrated Skin Shading”一文簡要介紹了對次表面散射的預積分模型;同時又結合了《GPU Gen 3》中相關內容乎澄,對一種皮膚高光處理的預積分模型也進行了梳理突硝;最后我們在Unity URP管線下展示了基于預積分技術對面部皮膚進行實時渲染的效果。
什么是SSS
SSS既Subsurface Scattering置济,次表面散射解恰,其實在前文介紹有關PBR的一些重要概念時已經概略的提及過,是一種在微觀尺度上對物體表面漫反射光的解構舟肉。我們知道當一束光線照射到材質表面修噪,一部分光(光能)被立即鏡面反射回了空氣中,另一部分進入物體內部路媚,要么被吸收,要么再次發(fā)生折射或反射樊销。對于不太透明的電介質來說整慎,比如人的皮膚脏款,這些折射光不太可能直接從另一側透射出人體,更可能的是裤园,在某一層介質中經過了多次光路改變撤师,最后出射光以一個相對隨機的角度再次回到入射光一側的空氣中,成為漫反射拧揽。這里有2處需要額外說明的地方:
- 其一是隨機性剃盾,由于材質內部介質通常是不連續(xù)的,如皮膚有油脂層淤袜,表皮層,真皮層铡羡,以及大量毛細血管和里面的血液構成,光路在內部的折射/反射往往是無規(guī)律可循的尽爆,導致最終出射時方向的隨機化漱贱;
- 其二是電介質的著色性夭委,既漫反射光會被染上顏色,這是因為光線在物體內部傳播時闰靴,一部分波長的光能被吸收所致(通常是金屬成分中的自由電子)彪笼,比較典型的是人體皮膚中血液的鐵離子吸收了藍綠波長的光譜,從而形成表皮翻紅的染色效果蚂且。
借用一下網上的示例圖:
漫反射來自于這些重新回到空氣中的折射光配猫。當觀察尺度足夠小,我們便不能近似的認為這些漫反射光的出射點恰好位于入射光所在的入射點處杏死,而是如上圖所示泵肄,出射光被認為分布在入射光周圍一定范圍內(綠色圓圈)。在圖像渲染領域淑翼,因為顏色的離散化(既光路以及顏色相關的值最終都會通過積分腐巢,以離散化數值的形式記錄到像素點上),所以如果上圖中綠色圓圈的區(qū)域恰好落在某個像素內玄括,那么反映的宏觀表現層面冯丙,我們看到的依然是普通漫反射效果。
那么在一般觀察距離上遭京,會不會有“綠圈”兜不住一個像素的情況呢胃惜?其實挺常見的泞莉,舉個例子,我們知道人的皮膚一般可以分為表面油脂層船殉,上皮層和下皮層(真皮層)鲫趁,當光線穿透油脂和上皮層,遇到人體皮膚中富含血管和組織液的真皮部分利虫,光線很可能會沿著局部的均質(血液挨厚,組織液)做較長范圍的折射遷移疫剃。
如果一速光線能夠沿著路徑在入射介質中傳播足夠遠時,我們就正式進入了次表面散射的世界蹄溉。
如上圖所示,這種情況下棉胀,我們必須考慮因長距離傳播再出射而導致的著色差異唁奢。我們不再將計算局限于當前像素(圖中綠圈)所在的范圍,此時材質上(入射光點附近區(qū)域內)其他各個方向的影響分量都需要納入考量脊奋,并反映到出射點光亮度的計算過程中。
關于BSSDF
在了解了SSS效果成因后久又,我們也許會好奇這么復雜的現象烈评,如何用簡練的數學語言描述清楚,進而指導我們構建渲染方程呢适瓦?類似于 BRDF,人們專門為SSS引入了新的方程嗦随,全稱是 Bidirectional Surface Scattering Distribution Function(雙向次表面散射分布方程),也可以簡稱為 BSSDF署恍,該方程最早源自2001年由Jensen發(fā)布的論文,其核心概念可以簡單總結為如下公式:
等式左側的積分輸出是出射位置和出射角度下的光亮度Lo
呼巷。為了得到Lo
,我們先從最右側積分看起:
在給定某一個面積微元A
的前提下配名,對半球空域Ω
所有入射光的光亮度積分,這種積分需要考慮光線對表面投影造成的衰減(i dot ni)
芋膘,當第一輪積分做完臂拓,我們得到的是單位面元(像素)的輝度值(irradiance)。
之后將積分所得輝度帶入到外側積分中理解:我們很容易可以看出孵滞,這部分是對面積A
進行積分坊饶,被積函數是R
與半球空域輝度值的乘積。仔細觀察法線痘绎,函數R的入參是|xi - xo|
,代表了從入射點到出射點之間距離散庶。多提一嘴,積分函數R
的本質是一個模糊函數(Blur Kernel)须教,用于近似我們后文會提及的散射方程Scattering(xi, i; xo, o)
轻腺。為啥?當然是因為完整的散射方程十分且非常且尤為復雜误算,它既要考慮入射和出射的位置咖杂,還要考慮入射和出射的角度诉字,是個高緯方程导披。而我們的R
函數之所以能夠成立撩匕,是因為我們假設了物體表面是各向同性的(isotropic)止毕,如此一來忍疾,我們可以認為散射方程只受到光線在材質內傳播的距離|xi - xo|影響卤妒,既 Scattering(|xi - xo|)
,而和具體入射和出射的方位無關士复。R
的選擇多種多樣,可以是Gaussian kernel
冗荸,smoothstep
俏竞,cubic function
或者是Disney的normalized diffusion
玻佩,但最好保證能量的守恒,既對整個A區(qū)域積分這些核的結構應該等于1烦秩,同時積分結果也要符合人眼對于目標材質的預期兜蠕。
次表面散射中的預積分皮膚概念
如BSSDF方程所示,如果要考慮SSS效果晶府,并得到某一個點處沿著某個方向發(fā)射的光強度,那么就得老老實實對整個區(qū)域的每個點積分较沪,這顯然非常的“昂貴”购对。因此自然有人開始嘗試預積分技術,將BSSDF的部分或整體預先計算出來解幽,并以一定方式保存進紋理。我們來參考下來自《GPU Pro 2》中由Eric Penner和George Borshukov提出的預積分技術的部分相關內容:
The obvious caveat of pre-intergation is that in order to pre-integrate a function, we need to know that it won't change in the future. Since the incident light on skin can conceivably be almost arbitrary, it seems as though precomputing this effect will prove difficult, especially for changing surfaces. However, by focusing only on skin rather than arbitrary materials, and choosing specifically where and what to pre-integrate, we found what we believe is a happy medium. In total, we pre-integrate the effect of scattering in three special steps:
* on the lighting model
* on small surface details
* on occluded light(shadows)
By applying all of these in tandem, we achieve similar results to texture-space diffusion approaches in a completely local pixel shader, with few additional constraints.
To understand the reasoning behind our approach, it first helps to picture a completely flat piece of skin under uniform directional light. In this particular case, no visible scattering will occur because the incident light is the same everywhere. The only three things that introduce visible scattering are
* changes in the surrounding mesh curvature,
* bumps in the normal map,
* and occluded light(shadow).
We deal with each of these phenomena seperately.
上述文章的片段概述了預積分技術在次表面散射方面的局限霜定,為了確保預計算的東西不被模型和表面參數所束縛辖所,就必須限定積分對象的材質(這里自然是指皮膚),并且僅預計算那些不變的組成部分酥宴。
至于SSS的形成條件及處理步驟,這邊稍作整理用中文再輸出一邊重點:
對宏觀上平坦的表面倒庵,由于平行光均勻分布于各個點,我們是無法察覺到次表面散射現象的,要達成這一現象必須滿足3個要素顾彰,分別是有弧度變化的模型,有法線的擾動以及存在光線遮擋厕隧;而預積分也分為3個步驟,分別是處理光照,處理微表面以及處理遮蔽波势。對于3個步驟拴曲,我個人的理解是:
- 處理光照 -> 確定Li 入射光函數
- 處理微表面 -> 確定散射分布函數 S 或者 R
- 處理遮蔽 -> 計算光照的投影 N dot L
文章片段還提及了形成SSS效果的3條重要因素:
- 模型弧度變化 -> 統一了模型的尺寸和形狀2個維度,對于人體而言能用一個標量很好的區(qū)分諸如臉頰蕉汪、耳鼻等不同部位的SSS特征福澡。
- 法線擾動 -> 表面法線完全沒有擾動的話,那么微表面區(qū)域完全鏡面和平坦算利,SSS失去了形成細節(jié)特征的條件;如果表面法線擾動過于銳利缎患,那么看起來效果會不好(像非散射的邊緣效果)风题。
- 光線的遮擋 -> 就是說必須包含陰影俯邓,從而形成局部由暗至明的轉向骡楼,避免全方位的亮光造成SSS效果無法被察覺。
如何理解3要素呢稽鞭?我相信很多人可能和我一樣鸟整,對“模型需要有弧度的變化”印象最為深刻,畢竟它將微觀上的SSS現象同物體的宏觀形狀關聯了起來朦蕴。這乍聽起來有點不可思議篮条,然而深究其原因涉茧,連同另外2個要素一起思考很容易發(fā)現,它們表示的都是一個意思,既:在物體表面要有明顯的光強度變化舰涌,要能夠形成明和暗部乾闰。
一種預積分SSS技術
講解了半天各種前因后果硫朦,也許你會好奇到底SSS效果是如何通過預積分方式在實時渲染過程中體現的呢祷舀?別急铺纽,這節(jié)我就和大家一起來梳理下《GPU Pro 2》提出的預積分技術原理和實現。首先大家需要注意所謂預積分積分的是皮膚表面漫反射分量,是基于BSSDF進行的呜笑,這點和BRDF著重于Specular是不同的(皮膚高光我們后面會談到)。此外不得不感謝網絡上的幽玄大神對公式的細心推理,本節(jié)大部分數學推導是基于他的工作進行的碉咆。
我們先從原文作者給出的對SSS這一物理現象的數學模型示意圖開始吧:
我先簡要歸納下圖中要點竟稳。
- 左圖表示BSSDF中的散射函數
R
,入參x
應當是一個角度標量混聊,可以通過關聯半徑r
轉換成從入射點到出射點的距離(Distance
)冻记,而返回值(既縱坐標)是對應距離的貢獻強度摄悯。 - 右圖中的圓形代表抽象的幾何模型廉涕,你可以把這圓想象成角色頭模。
- 右圖中由法線
N
與入射光線L所構成的夾角定義為θ
涤伐。 - 右圖中圓形的半徑為
r
眷射。 - 紅綠藍色的包絡線代表不同R函數(但是能量守恒)冗茸,是以當前點(法線
N
與圓的交點)為中心挂绰,對周圍各點計算光亮度貢獻所得的曲線。 - 右圖中
N+θ
表示從法線N
開始逆時針旋轉θ
角對于的位置。
下面是作者Eric給出的積分公式暂氯,我這邊采用幽玄大神重解讀后的改版,與原本相比亮蛔,積分區(qū)間縮小到了半球空域痴施,同時顯式得引入了模型半徑r
。注意原始公式是為了描述關于距離的一維函數(上圖左側Profile函數)究流,所以r
被默認設置成了1,我們最終的積分函數是需要包含r
和distance
(或者角度x
)這2個控制維度的芬探。
因為式子中已經提出了作為常量的入射光強 Li
,為了便于理解灯节,我們姑且認為其值為1。如下圖所示炎疆,對于圓形上任一點Q
卡骂,假設圓心O
與Q
構成的向量QO
與法線N
的夾角為x
(x
可以在-π/2
到+π/2
之間取值)形入。那么該點的入射光強度可以簡單得表示為:Lq = Li* (OQ dot OL) = 1 * cos(x + θ)
設法線N
與圓的交點為P
,這是我們的積分目標點亿遂,所有圍繞在點P
周圍半球空域內的表面積都需要被積分公式舔一遍浓若,加總它們對點P
的貢獻度。Q
點我們抽象出來的無數被積點之一蛇数,假設Q
對P
的散射率是q(x)
,前面說過碌上,x
是OQ
與法線N
的夾角,它隨Q
的移動而變化馏予。采用微分思想盔性,我們視點Q
所在的表面弧長為△x
,那么這段小區(qū)間所切走的概率密度蛋糕應當是 q(x)△x
冕香,然后乘以Q
點接受到的光亮度后豫,我們可以得到該處△x
區(qū)域提供給P點的散射分量(既有多少光線從Q
點所在的△x
區(qū)域折射進物體內部箩言,然后傳播到了P
點并出射)硬贯,公式如下:
我們對每一個符合要求的點Q
做同樣的事情:
前面我們引入了概率密度函數q(x)
,它本質上就是圖(SSS Distribution)中的散射分布函數R
鸵赖,只不過這里的入參使用的是角度x
。現在我們希望使用切線距離(distance
)作為函數的入參它褪,從x
到d
的轉換公式如下,應用了一點三角函數和中學幾何知識居触,這里不再贅述。
我們還知道散射分布函數 R
需要保證能量守恒轮洋,也就是如下積分結果:
理解所謂的能量守恒需要一些技巧抬旺,我們并不是說來自所有半球面積上的微元分量△x
加總的貢獻度等于100%的當前點P的入射能量,這是不合理也不可能的开财,上式的物理意義在于說明汉柒,對于所有符合條件的當前點P
,計算他們周邊區(qū)域的散射貢獻度總和應當是一個確定的常數责鳍,比如此處的常數1碾褂。更深一層的含義是:只要給定某種材質,那么光能在材質內沿著特定方向傳播的概率和損耗與觀察位置無關历葛,只與傳播的距離有關正塌。
其實q(x)
長什么樣并不重要,重要的是
- 分布方程需要讓最終結果看起來還不錯啃洋。
- 分布方程需要滿足“能量守恒”传货。
假設我們找到了一個滿足條件公式(1)的分布函數R(d)
,為了使之也滿足條件公式(2)宏娄,我們不妨令q(x) = k*R(d)
,其中k
是歸一化因子孵坚,確保分布R
在半球積分后能量守恒窥淆。
于是我們可以很容易帶入積分公式巍杈,求取k的表達式,以及q
與R
直接的關系:
將q(x)
以R形式的表達式導入到公式(1)式中词裤,這樣同時考慮到了各點的光亮度:
上式最后就是《GPU Pro 2》相關文章中提出的公式原型鳖宾,注意這個公式中,只有θ
和r
是變量渔肩。
將上述公式轉化為代碼拇惋,可以求取不同曲率和角度下的散射分布,并將結果記錄到一張完整的紋理上:
private void PreIntegrateSSSLUT(Texture texture)
{
for (int j = 0; j < texture.height; ++j)
{
for (int i = 0; i < texture.width; ++i)
{
float NDotL = Mathf.Lerp(-1f, 1f, i / (float)texture.width);
float oneOverR = 2.0f * 1f / ((j + 1) / (float)texture.height);
//Integrate Diffuse Scattering
Vector3 diff = Integrate(NDotL, oneOverR);
texture.SetPixel(i, j, new Color(diff.x, diff.y, diff.z, 1));
}
}
}
private Vector3 Integrate(float cosTheta, float skinRadius)
{
float theta = Mathf.Acos(cosTheta); // theta -> the angle from lighting direction
Vector3 totalWeights = Vector3.zero;
Vector3 totalLight = Vector3.zero;
float a = -(Mathf.PI / 2.0f);
const float inc = 0.05f;
while (a <= (Mathf.PI / 2.0f))
{
float sampleAngle = theta + a;
float diffuse = Mathf.Clamp01(Mathf.Cos(sampleAngle));
// calc distance
float sampleDist = Mathf.Abs(2.0f * skinRadius * Mathf.Sin(a * 0.5f));
// estimated by Gaussian pdf
Vector3 weights = Gaussian(sampleDist);
totalWeights += weights;
totalLight += diffuse * weights;
a += inc;
}
Vector3 result = new Vector3(totalLight.x / totalWeights.x, totalLight.y / totalWeights.y, totalLight.z / totalWeights.z);
return result;
}
如何使用預積分LUT
我們先看看積分結果被保存到2D紋理后的樣子蓉坎,如下圖:
橫坐標是NdotL
磷仰,對應上節(jié)最終公式里的cosθ
;縱坐標是1/r
伺通,對應公式中的模型半徑r
的倒數逢享,也可以理解為物體當前點附近的曲率。我們知道uv
坐標的定義域是[0,1]弓柱,而NdotL
和1/r
(r
>=1)恰好是符合[0,1]區(qū)間的定義侧但,這使得將預積分數據壓縮在一張2D紋理中成為可能。
那么如何在運行時采樣取值呢禀横?需要處理好如下2個采樣維度:
N dot L
這部分相對簡單,對于主平行光酿箭,L
應當能夠直接從引擎中獲取到;法線N
建議使用法線貼圖獲取缭嫡,一方面是為了更好的精度和細節(jié)表現,另一方面是我們可以視情況對法線貼圖進行預處理(濾波器模糊或者使用mipmap技術)耕突,之所以要模糊法線貼圖评架,理由原作者的解釋是“避免看起來過于僵硬的邊界出現”,我想為了更好的藝術效果古程,大家還是盡量讓法線的分布盡量均勻連續(xù)喊崖,避免過于陡峭的“邊界”出現。1/r
其本質是半徑為r
的圓上任意一點的曲率荤懂,我們已知1/r
定義在[0,1]區(qū)間,這就意味著r
的取值范圍被規(guī)定在[1, ∞]區(qū)間上晤锥,當r
遞減至最小單位1時廊宪,1/r
趨向于1,從pre-integrated LUT圖來看箭启,散射能量在各個光照角度都相對提高了,且在可以類比下人臉上的鼻子和耳廓等處在光線照射下陰影附近透紅的效果放妈;另一方面當r非常大時荐操,1/r
趨向0,LUT采樣的結果更加接近一般光照下的明暗交替托启,無明顯SSS效果。
那么我們如何在運行時獲取到1/r
呢唉堪?考慮到采樣LUT圖時的運行上下文環(huán)境,可以得出一個很簡單的方法:嘗試找到局部曲率链方,然后把曲率映射成為球體半徑r
灶搜,最后用1/r
采樣LUT。于是重點就變成了如何尋找這個局部曲率割卖,特別是如何在像素著色器中找到它:
Eric 給出了一個很巧妙的方法:先求取當前像素點對應法線的局部4領域差值△N
(相當于求導數),然后比上模型頂點坐標相對于其周圍4領域像素的差值△p
(也相當于求導數)罢维,根據如上圖左側所示的等比三角形法則丙挽,所得結果就是 1/r
的近似。
計算代碼如下:
float cuv = saturate(_CurveFactor * (length(fwidth(worldNormal)) / length(fwidth(worldPos))));
其中
fwidth(v) = abs(ddx(v))+ abs(ddy(v))
特別得:
ddx(v) = 該像素點右邊的v值 - 該像素點的v值
ddy(v) = 該像素點下面的v值 - 該像素點的v值
fwidth(Position) -> 該像素與相鄰兩個像素的坐標位置的差值
fwidth(Normal) -> 該像素與相鄰兩個像素的法線的差值
而_CurveFactor用于調整比值的強度平窘,用于影響最終效果凳怨。
實時渲染的片元著色器代碼
float curve = saturate(_CurveFactor * (length(fwidth(worldNormal)) / length(fwidth(worldPos))));
fixed NDotL = dot(blurNormalDirection, lightDirection);
fixed4 sssColor = tex2D(_SSSLUTTex, float2(NDotL * 0.5 + 0.5, curve)) * _LightColor0;
當然我們也可以走捷徑,預先將模型表面各點的曲率烘焙到uv貼圖上肤舞,然后運行時直接采樣獲得1/r
的計算結果。
計算皮膚的高光
人們經過測量發(fā)現只有很少一部分的光在接觸到皮膚表面后會被鏡面反射弊琴,這種反射光具有若干性質杖爽,可以歸納如下:
- 反射光不帶有任何顏色信息,這是電介質作為反射層的固有特性腋寨;
- 反射光中的大部分來自于皮膚最表面那很薄的一層油脂層(Thin Oily Layer)
- 反射光遵循菲尼爾效應化焕,掠射角處反射量增大
我們還知道人類皮膚的反射光不可能構成干凈又存粹的鏡面像(吳克的頭?),這是由于皮膚的微表面其實并不平整键兜,所有反射光線會分布到一個范圍內穗泵,很顯然要描述好皮膚的Specular必須要引入BRDF(bidirectional reflectance distribution function雙向反射分布函數),那么如何選擇合適的BRDF呢佃延?本文的后續(xù)內容將依據《GPU Gem 3》第14.3節(jié)內容,簡單梳理一下基于Kelemen/Szirmay-Kalos BRDF模型的皮膚高光渲染方案仔沿。
首先我們回顧下PBR高光模型的一般實現形式尺棋,它由直接入射光強度(可能被遮蔽影響),BRDF項膘螟,還有一個余弦函數( 控制入射光能的衰減強度)組成,具體形式參考如下代碼:
specularLight += lightColor[i] * lightShadow[i] * rho_s * specBRDF( N, V, L[i], eta, m) * saturate( dot( N, L[i] ) );
上述代碼中的rho_s
是一個引入的“非物理”因子用于控制整體強度,而specBRDF
一般是由菲尼爾項F
脊阴,幾何遮蔽項G
蚯瞧,還有法線分布項D
構成的,需要用到的入參有:法線方向N
埋合, 視方向V
,光方向L
蜜猾,菲尼爾系數F0
振诬,以及反應粗糙度的m
項。
首先是菲尼爾項F赶么,和Disney BRDF一樣《GPU Gem 3》也采用了Schlick菲尼爾近似,它有如下代碼形式清钥,簡單理解就是當視方向V
越接近掠射角(與微表面法線H
垂直)所形成的反射光照強度越高。
float fresnelReflectance( float3 H, float3 V, float F0 )
{
float base = 1.0 - dot( V, H );
float exponential = pow( base, 5.0 );
return exponential + F0 * ( 1.0 - exponential );
}
另外代碼中的F0表示當皮膚遇到垂直入射光照時的反射率缕坎,《GPU Gem 3》中使用的是測量常數0.028从橘。
下面是G
項和D
項,結合論文 《A Microface Based Coupled Specular-Matte BRDF Model with Importance Sampling》所提出的一種簡化版 Cook-Torrance模型BRDF:
公式中的 P
項代表微表面法線分布的概率密度恰力,也就是傳統意義上的D
項。而右側分子上的F
就是菲尼爾項停局,余下的部分是簡化后的G
項香府,沒錯,整個Cook-Torrance模型中的G
項連同 1/(4(NdotL)(NdotV))
因子被合并簡化成了 1/(hdoth)
企孩,而h代表的是(V+L)
勿璃,既半角向量未被歸一化前的樣子。
我們現在已經明確G
項和F
項的公式补疑,這部分計算可以放在PS階段處理,一來是因為計算消耗不大诊胞,而來也是因為涉及多項入參锹杈,特別是光照方向L
和物體表面的菲尼爾系數F0
,這些量變化范圍廣竭望,很難壓縮到速查表里。為啥要強調這些呢霞扬?那是因為我們希望盡可能預計算公式中的復雜部分,以供實時快速查詢和取用喻圃,比如余下我們還沒說得P
項(D
項),因為只涉及到微表面法線H
一個控制變量雀扶,非常有利于我們創(chuàng)建速查表肆汹。舉個例子(實際處理并非如此):我們有一張2D的紋理,讓uv
各自代表球面坐標系中的天頂角和方位角浪册,就能模擬出H
的朝向岗照,然后以uv
組合為索引,采樣紋理獲得預計算好的概率密度值攒至。
再來看Kelemen/Szirmay-Kalos在論文中引用的的P項表述,其本質是Beckmann distribution:
式中α
表示宏觀法線N
與微表面法線H的夾角库菲。符號m
代表當前面元的粗糙度志膀,可以通過對各個方向梯度值求取均方根(root mean square)來得到,但在實際工程中一般是由導入的粗糙度貼圖采樣獲得。所以上式是一個由夾角α
以及粗糙度m
一共2個維度變量控制的函數败去。
We employ a similar approach to efficiently compute the Kelemen/Szirmay-Kalos specular BRDF, but instead we precompute a single texture (the Beckmann distribution function) and use the Schlick Fresnel approximation for a fairly efficient specular reflectance calculation that allows m, the roughness parameter, to vary over the object.
上述片段來自于《GPU Gem 3》,表述了作者對Beckmann分布函數預處理以便生成速查紋理的設想广鳍,如下代碼同樣摘錄自書中吓妆,是對編碼紋理的實現:
float PHBeckmann( float ndoth, float m )
{
float alpha = acos( ndoth );
float ta = tan( alpha );
float val = 1.0/(m*m*pow(ndoth,4.0))*exp(-(ta*ta)/(m*m));
return val;
}
// Render a screen-aligned quad to precompute a 512x512 texture.
float KSTextureCompute(float2 tex : TEXCOORD0)
{
// Scale the value to fit within [0,1] – invert upon lookup.
return 0.5 * pow( PHBeckmann( tex.x, tex.y ), 0.1 );
}
很簡單,PHBeckmann方法負責計算Beckmann分布祖秒,注意其入參為ndoth
和m
,其中m
不必多說竭缝,ndoth
是宏觀法線N
與微表面法線H
的夾角的余弦值,具體計算時需要使用反余弦函數acos
處理出真實α
夾角咙俩。方法KSTextureCompute是對紋理的編碼湿故,主要利用了uv
軸[0,1]區(qū)間的特性,將y=cosα
以及y=m
在這個區(qū)間上展開坛猪,帶入PHBeckmann方法進行計算。有2點需要注意独撇,其一是粗糙度m需要確保提前轉化到[0,1]區(qū)間躁锁;其二是我們需要將計算結果同樣壓縮到[0,1]區(qū)間中,具體方法參考代碼搜立。
實時取用的方法參考如下代碼槐秧,同樣很簡單,共分為3個步驟:
- 首先是準備諸如
h
刁标,H
以及ndoth
等中間變量; - 其次是采樣預計算紋理顿锰,注意采樣
uv
是float2(ndoth启搂,m)
,同時注意要反壓縮采樣獲得的數據胳赌; - 最后是參考Kelemen/Szirmay-Kalos提出的簡化版BRDF,裝配上
G
項F
項和P
項熏版。
float KS_Skin_Specular(
float3 N, // Bumped surface normal
float3 L, // Points to light
float3 V, // Points to eye
float m, // Roughness
float rho_s, // Specular brightness
uniform texobj2D beckmannTex )
{
float result = 0.0;
float ndotl = dot( N, L );
if( ndotl > 0.0 )
{
float3 h = L + V; // Unnormalized half-way vector
float3 H = normalize( h );
float ndoth = dot( N, H );
float PH = pow( 2.0*f1tex2D(beckmannTex,float2(ndoth,m)), 10.0 );
float F = fresnelReflectance( H, V, 0.028 );
float frSpec = max( PH * F / dot( h, h ), 0 );
result = ndotl * rho_s * frSpec; // BRDF * dot(N,L) * rho_s
}
return result;
}
下圖展示了預計算速查紋理(右),以及對人像采樣該圖后直接輸出的效果(左)
DEMO
TODO...[稍后放出]
Reference
[1] Advanced Techniques for Realistic Real-Time Skin Rendering
[2] Pre-Integrated Skin Shading
[3] Pre-Integrated Skin Shading 數學模型理解
[4] Pre-Integrated Skin Shading實現筆記
[5] GPU Gems 3 全書提煉總結