對各向異性高光的理解

lxycg發表於2021-01-01

在渲染頭髮、絲綢等材質時,常要用到各向異性高光(Anisotropic highlighting)。什麼是各向異性高光呢?先來個直觀的對比,如下面圖1、圖2。

Blinn-Phong
圖1 普通的Blinn-Phong高光

Aniso
圖2 各項異性高光

圖1和圖2是在同一個場景裡、同一個模型(一個球)、相機、光源(只有一個平行光),甚至它們的漫反射光照也一樣,只有高光的計算方法不同。

圖1使用的傳統的Blinn-Phong高光,可以看到高光呈一個圓形亮光斑,比較集中。

// Blinn-Phong specular highlight
vec3 col = vec3(0.);
vec3 halfDir = normalize(viewDir + lightDir);
float nh = dot(normal, halfDir);
float spec = pow(nh, 100.);        
col += nl * lightColor * albedo + specColor * spec;

圖2中渲染的是各向異性高光,呈環形,在頭髮渲染中又稱為“天使環”,這裡使用的光照模型是Kajiya-Kay Model。在很多遊戲中,頭髮渲染都使用了Kajiya-Kay Model,比如崩壞3(當然崩壞3在這個基礎的模型基礎上進行了一些創新,主要是一些引數的控制,我後面會說)。

// anisotropic highlighting
// http://web.engr.oregonstate.edu/~mjb/cs519/Projects/Papers/HairRendering.pdf 
// 計算球在當前點的切線
vec3 tangent = SphereTangent(pos, normal);
// 切線偏移,可以移動“天使環”的位置,在上面連結裡的PDF中詳細說明,我不細說了
//float shift = texelFetch(iChannel0, ivec2(0), 0).r;
//shift = shift * 2. - 1.;
//tangent = ShiftTangent(tangent, normal, shift);

vec3 col = vec3(0.);
vec3 halfDir = normalize(viewDir + lightDir);
float dotTH = dot(tangent, halfDir);
// 關於dirAtten的計算說明見下文
float dirAtten = smoothstep(-1., 0., dotTH);
float sinTH = sqrt(1. - dotTH * dotTH);
// Kajiya-Kay Model
col += nl * lightColor * albedo + dirAtten * specColor * pow(sinTH, 100.);

sinT
圖3 Kajyiya-Kay Model

從上面程式碼來看Kajiya-Kay Model模型使用切線T和半形向量H(即程式碼中的halfDir)之間夾角的正弦值來計算高光係數(即pow(sinTH,100)),而不是Blinn-Phong中的法線和H向量之間夾角的餘弦(即pow(nh, 100.))。

圖3中,黃色的粗圓柱表示一根頭髮,T是其切線,V是視線,L是光源方向, H是L和V之間夾角的一半。其實T和H夾角的正弦恰好就是圖3中H和N之間的餘弦值,這樣說來Kajiya-Kay Model本質上還是使用的餘弦值來計算高光係數。但是值得注意的是在每根頭髮的固定點處,T總是保持不變的,N是隨視線V變化而變化的,N總是在T和V組成平面內。也就是在一個被渲染的點處,其法線在各個(視線)方向是不同的這大概所謂就是的“各向異性”。而我們之所以要有T就是為了計算出這個隱藏在背後的法線N。當然,我們不需要計算出這個N的確切值,其蘊含在T和H的正弦值中,V蘊含在H中。所以,我認為Kajiyaa-Kay Model本質上仍然是Blinn-Phong,只是它用切線T幫我們找到當前視線下的使高光最強的法線N

另外,注意上面的方向衰減係數dirAtten的計算:

float dirAtten = smoothstep(-1., 0., dotTH);

為什麼要這麼計算?貌似好多人都只知道這是個衰減係數,而不知道為什麼要這樣計算,或者說不太清楚背後的幾何意義。這個方向衰減係數其實涉及到兩個方向,一個是光源的方向L,一個是視線方向V,它們都蘊含在H中。首先,這裡smoothstep的第3個引數傳的是dotTH,即切線和和H的餘弦值。而這裡smoothstep的第1個引數-1表明當dotTH小於或等於-1時(實際上最多等於-1,不會比-1還小,因為所有參與計算的向量都是規範化的),dirAtten值為0。什麼時候dotTH為-1呢,就是圖3中,H的方向恰好和T的方向相反時,這時候T和H之間的夾角為180度。而smoothstep的第2個引數0表明當dotTH大於或等於0時,dirAtten的值為1。注意dotTH最大值為1,此時T和H的方向剛好相同,兩者之間的夾角為0度。所以smoothstep(-1., 0., dotTH)的作用就是取和切線T角夾在0至180度之間的H。當T和H之間的夾角不在這個範圍內時,必然出現H和對應N的點積小於0,即此時高光為0 (此時光源照不到當前著色點或相機看不到當前著色點),此時dirAtten的值就應當為0(即沒有高光)。

另外,一般的我們可以定製各向異性高光的引數,比如計算各向異性高光時dirAtten * specColor * pow(sinTH, 100.);,其中的100就是一個可調引數,這個引數值越大,“天使環”的越細,其值越大,“天使環”越寬。這一點大家都可以理解。而我們可以更進一步,直接使用一張貼圖來控制“天使環”在各處的寬度,使其在各處的寬度不同,甚至可以把“天使環”調成各種有趣的圖案,以達到想要的藝術效果。還有渲染頭髮一般有兩個“天”使環,第二個會較暗一些,且是有自己的顏色的,更靠近髮根。只要理解了第一個“天使環”的原理,第二個只是在第一個基礎上進行了偏移,顏色稍微有點不同而已,我不再贅述。

我寫了一個Shadertoy: Anisotropic highlighting (shadertoy.com),有原始碼,在電腦上開啟瀏覽器,可線上執行,可進行互動,調整天使環的位置,希望能幫助到一些同學。如果因為某些原因,打不開網址,我也錄製了一個[視訊](A shdertoy: Anisotropic Hightlighting - 知乎 (zhihu.com)),可在知乎上觀看。

最後,我想記錄一下在寫這個Shadertoy時用到的一個小技巧: 球的切線的計算。

在程式碼中,那個紅色的球是用數學公式建模的即 $ length(p - center) = r$,然後從相機處發射射線,判斷射線與球是否相交以及距相機的距離,來求得交點,即當前著色點。法線的計算很簡單,不贅述。但是怎麼計算出切線呢?我是這樣做的: 用當前著色點的三維座標及其法線確定一個平面,然後把當前著色點的y座標加1(當然加別的數值應該也可以)得到偏移後的一個點,然後把這個偏移後的點再投影到剛剛確定的那個平面上,則投影得到點減去當前著色點得到的向量就是當前著色點的一個切線向量,進行規範化即可。關於平面方程及點投影到平面,可參考這篇文章:平面(Plane) - 知乎 (zhihu.com)。

程式碼如下:

// pos是當前著色點的三維座標
vec3 SphereTangent(vec3 pos, vec3 normal) {
    vec3 posOffseted = pos;
    posOffseted.y += 1.;
    float D = - dot(normal, pos);
    float distToPlane = dot(normal, posOffseted) + D;
    vec3 proj = posOffseted - normal * distToPlane;
    vec3 tangent = normalize(proj - pos);
    return tangent;
}

我不知道有沒有人這樣做過,我是臨時想到的,實現了一下效果還行。

參考:

Anisotropic highlighting (shadertoy.com)

Hair Rendering and Shading

平面(Plane) - 知乎 (zhihu.com)

相關文章