在渲染頭發、絲綢等材質時,常要用到各向異性高光(Anisotropic highlighting)。什么是各向異性高光呢?先來個直觀的對比,如下面圖1、圖2。
圖1 普通的Blinn-Phong高光
圖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 + nl * dirAtten * specColor * pow(sinTH, 100.);
圖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;
}
我不知道有沒有人這樣做過,我是臨時想到的,實現了一下效果還行。
參考: