WebGL學習之法線貼圖

JeffZhong發表於2019-05-01

實際效果請看demo:紋理貼圖

法線貼圖

為了增加額外細節,提升真實感,我們使用了漫反射貼圖和高光貼圖,它們都是向三角形進行附加紋理。但是從光的視角來看是表面法線向量使表面被視為平坦光滑的表面。以光照演算法的視角考慮的話,只有一件事決定物體的形狀,那就是垂直於它的法線向量。磚塊表面只有一個法向量,表面完全根據這個法向量被以一致的方式照亮。如果每個片元都用不同的法線會怎樣?這樣我們就可以根據表面細微的細節對法線向量進行改變;這樣就會獲得一種表面看起來要複雜得多的幻覺:

表面法線

每個片元使用了自己的法線,我們就可以讓光照相信一個表面由很多微小的(垂直於法線向量的)平面所組成,物體表面的細節將會得到極大提升。這種每個片元使用各自的法線,替代一個面上所有片元使用同一個法線的技術叫做法線貼圖(normal mapping)或凹凸貼圖(bump mapping)。

以上都是從 LearnOpenGL CN 相關的文章摘抄 法線貼圖。沒辦法,WebGL相關比較深入的知識你只能去看openGL,好在原理基本相同,WebGL1就是基於openGL es 2.0,WebGL2就是基於openGL es 3.0。

法線貼圖

法線貼圖就是用紋理中的顏色向量r、g、b儲存法線向量的x、y、z。不過它們有另外的稱呼:t (切線)、b (副切線)、n (法線),它們組成了一個切線空間,被稱為TBN座標系。由於顏色與方向的表示範圍有區別,顏色範圍是[0,1],而作為表示位置方向的TBN座標系則是[-1,1],那麼從法線貼圖取出來的值要使用的話,得進行轉換。

// 將法線向量轉換為範圍[-1,1]
vec3 normal = normalize(normal * 2.0 - 1.0);
複製程式碼

法線貼圖偏藍是因為所有法線的指向都偏向z軸(0, 0, 1), 對應於rgb 中的 blue分量,也就是藍色。法線向量從z軸方向向其他方向輕微偏移,於是顏色也就發生輕微變化,這樣看起來便有了一種深度。例如,你可以看到頂部顏色傾向於偏綠,這是因為頂部的法線偏向於指向正y軸方向(0, 1, 0)對應於rgb總的 green分量,也就是綠色。

法線貼圖顏色

法線貼圖的優點是可以用一個低精度模型表現出非常高的細節,看起來像高精度模型那樣。

高精度模型

只需要500個三角形的簡單網格加上法線貼圖就能達到媲美4M個三角形的精細網格模型的效果,可以說法線貼圖優勢巨大,處理4M個三角形的複雜度簡直不可想象。

但法線貼圖也不是萬能的,它也有缺點。因為它只是改變了物體表面的光照計算方式,所以不適合用在凹凸起伏較大的物體上,這些物體會有遮擋的效果,法線貼圖是無法實現的。

而文章 法線貼圖 裡面有很詳細的原理講解和切線推導過程,最後求出如下的公式,我這裡也不再敘述了。

公式

著色器

我這裡使用了另外一種更加方便的演算法,能達到同樣的效果,原理就是使用導數(dFdx/dFdy)求出每個畫素在插值化傳值過來的點的變化率當成一個法線,請看函式 dHdxy_fwd。然後再通過與當前平面的法向量進行叉積 (cross),即可求得同時垂直於這兩個方向的法向量,請看函式 perturbNormalArb,而最終的這個法向量就是我們所要求的值,非常地高明。下面是glsl 內建的幾個關鍵函式。

dFdx(p) //在x方向的偏導數
dFdy(p) //在y方向的偏導數
cross(p0,p1) //向量p0,p1的叉乘
複製程式碼

我們用到的求導函式 dFdx / dFdy,在WebGL1是需要開啟擴充套件的,頂點著色器無需變動,主要變動的是片元著色器。具體的計算過程,請看如下片元著色器程式碼:

#extension GL_OES_standard_derivatives : enable// 注意要開啟該擴充套件
//...
uniform sampler2D u_diffMap;
uniform sampler2D u_specMap;
uniform sampler2D u_normMap;
//...
vec2 dHdxy_fwd() {
    vec2 dSTdx = dFdx( v_texcoord );
    vec2 dSTdy = dFdy( v_texcoord );
    float Hll = bumpScale * texture2D( u_normMap, v_texcoord ).x;
    float dBx = bumpScale * texture2D( u_normMap, v_texcoord + dSTdx ).x - Hll;
    float dBy = bumpScale * texture2D( u_normMap, v_texcoord + dSTdy ).x - Hll;
    return vec2( dBx, dBy );
}
vec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy ) {
    vec3 vSigmaX = vec3( dFdx( surf_pos.x ), dFdx( surf_pos.y ), dFdx( surf_pos.z ) );
    vec3 vSigmaY = vec3( dFdy( surf_pos.x ), dFdy( surf_pos.y ), dFdy( surf_pos.z ) );
    vec3 vN = surf_norm;
    vec3 R1 = cross( vSigmaY, vN );
    vec3 R2 = cross( vN, vSigmaX );
    float fDet = dot( vSigmaX, R1 );
    fDet *= ( float( gl_FrontFacing ) * 2.0 - 1.0 );
    vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );
    return normalize( abs( fDet ) * surf_norm - vGrad );
}

//...
// 從法線貼圖計算出逐畫素法線向量
vec3 normal = normal = perturbNormalArb( -v_position, normal, dHdxy_fwd());
//...
// 總的光照
gl_FragColor = vec4(ambient + diffuse + specular, diffuseColor.a);
複製程式碼

最後效果請看demo:紋理貼圖

後記

相關資料 LearnOpenGL CN

相關文章