shader復習與深入:Normal Map(法線貼圖)Ⅱ(轉)


本文寫得很好,轉自 http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map-2.html

1. 怎樣獲得頂點的TBN

其實我覺得這個是實踐部分最麻煩的地方。OpenGL提供了諸如glNormal、normal-vbo之類的接口設置頂點的法線,然后在shader中以gl_Normal等方式取得頂點法線數據,但是沒有提供切線和副法線的。當然兩者只要其一就足夠了(另一者可通過叉乘和左/右手定則獲得)。因為要把TBN導入shader,干脆就設置attribute變量,記錄每個頂點的切線。切線一般就是相鄰頂點的差向量了(其實這有時候是非常繁重的工作)。

如果是通常的3DS模型的話,頂點法線是共頂點的面的面法線的加權,這樣法線就不一定垂直於某個面,即與切線不垂直。但只要它們還是近似垂直的,上篇提及的Gram-Schmidt 算法應該可以處理。或者在shader中,把法線與切線叉乘出副法線,再用法線與副法線叉乘得新的切線,也能確保兩兩垂直。這樣之前的TBN矩陣的轉置矩陣就能直接作為其逆矩陣,完成向量從模型坐標系往切線空間坐標系的變換了。

問題不只這樣。對於一些模型,共享頂點的三角面片面法線差角太大,這時候計算出的該頂點法線和切線就可能帶來麻煩。在橙書(OpenGL Shading Language)中,談及了切線必須是一致的(consistently),面片相鄰的頂點切線不應該差距太大。但若相鄰面片夾角太大,得到的該頂點法線就可能與“共享該頂點的面片”上的其他頂點的法線差異很大,從而切線也會相差很大,直接導致光向量等在這兩頂點的切線空間差異很大,插值的各個針對像素的光向量方向差異很大,與像素法線點乘的cos也會差異得很明顯(而現實中一般的凹凸面漫反射光線不會有太大方向差異)。解決方法是把該出了問題的頂點拆成兩個(原地拷貝,3DS模型就不用了- -),一個面片用一個,其法線只受所屬的面片的面法線決定(這樣最后會形成突出的邊緣,但夾角大的面片之間實際上就應該會是有這樣的效果吧)。

另一個問題,我們向shader傳入頂點法線切線,希望副法線由兩者叉乘得出。但既然叉乘就有個方向問題(結果可以有兩個方向,AXB與BXA是不一樣的,我以前弄shadow volume就曾被它這種特性作弄過)。AXB改成BXA實際上會導致凹凸感反向,原來凹的變凸了,原來凸的變凹了(要仔細比對,不然會有首因效應)。一般就用N X T吧,因為基本上都是這個順序的,結果也符合原Normal Map。

2. GLSL 1.2 Shader實現代碼

沒什么好說的,就是前面算法翻譯成GLSL。

Vertex Shader:

  1. // vertex shader
  2. uniform vec3 lightpos; //傳入光源的模型坐標吧
  3. uniform vec4 eyepos;
  4.  
  5. varying vec3 lightdir;
  6. varying vec3 halfvec;
  7. varying vec3 norm;
  8. varying vec3 eyedir;
  9.  
  10. attribute vec3 rm_Tangent;
  11.  
  12. void main(void)
  13. {
  14.    vec4 pos = gl_ModelViewMatrix * gl_Vertex;
  15.    pos = pos / pos.w;
  16.    
  17. //把光源和眼睛從模型空間轉換到視圖空間
  18.    vec4 vlightPos = (gl_ModelViewMatrix * vec4(lightpos, 1.0));
  19.    vec4 veyePos   = (gl_ModelViewMatrix * eyepos);
  20.    
  21.    lightdir = normalize(vlightPos.xyz - pos.xyz);
  22.    vec3 eyedir = normalize(veyePos.xyz - pos.xyz);
  23.    
  24.   //模型空間下的TBN
  25.    norm = normalize(gl_NormalMatrix * gl_Normal);
  26.  
  27.    vec3 vtangent  = normalize(gl_NormalMatrix * rm_Tangent);
  28.  
  29.    vec3 vbinormal = cross(norm,vtangent);
  30.    
  31.    //將光源向量和視線向量轉換到TBN切線空間
  32.    lightdir.x = dot(vtangent,  lightdir);
  33.    lightdir.y = dot(vbinormal, lightdir); 
  34.    lightdir.z = dot(norm     , lightdir);
  35.    lightdir = normalize(lightdir);
  36.    
  37.    eyedir.x = dot(vtangent,  eyedir);
  38.    eyedir.y = dot(vbinormal, eyedir);
  39.    eyedir.z = dot(norm     , eyedir);
  40.    eyedir = normalize(eyedir);
  41.    
  42.    halfvec = normalize(lightdir + eyedir);
  43.  
  44.    gl_FrontColor = gl_Color;
  45.    
  46.    gl_TexCoord[0] = gl_MultiTexCoord0;
  47.    
  48.    gl_Position = ftransform();
  49. }

傳入的lightPos,eyePos,gl_Vertex,gl_Normal,rm_Tangent是其模型坐標系下的坐標、向量,乘以ModelView矩陣(法線切線乘以ModelView矩陣的轉置逆矩陣)到了視圖空間(vlightPos,veyePos,pos,norm, vtangent);在視圖空間它們已經有了“世界”的概念了,因此可以平等地相互影響(在各自封閉的模型空間是享受不了的),可以作各種點乘叉乘加減乘除計算。

注意,lightPos,eyePos雖說是在其各自模型坐標系下定義的,但不對它們弄什么平移旋轉縮放操作的話,其模型矩陣就是一單位陣,此時其“世界坐標 == 模型坐標”。所以這時我可以當它是在世界空間定義的坐標(實際上一般我們都會在世界空間定義這兩個點)。(注意,前提是不對它們做模型變換。)

從以上量得到光源向量、視線向量后(它們在視圖空間),N、T叉乘得B(注意它們現在都在視圖空間),通過TBN矩陣逆矩陣把兩向量變換到當前頂點的切線空間,交給光柵去插值。 

對以上有不理解的朋友,可能是沒看上篇:[shader復習與深入:Normal Map(法線貼圖)Ⅰ]

fragment shader:

  1. //fragment shader
  2. uniform float shiness;
  3. uniform vec4 ambient, diffuse, specular;
  4.  
  5. uniform sampler2D bumptex;
  6. uniform sampler2D basetex;
  7.  
  8. float amb = 0.2;
  9. float diff = 0.2;
  10. float spec = 0.6;
  11.  
  12. varying vec3 lightdir;
  13. varying vec3 halfvec;
  14. varying vec3 norm;
  15. varying vec3 eyedir;
  16.  
  17. void main(void)
  18. {
  19.    vec3 vlightdir = normalize(lightdir);
  20.    vec3 veyedir = normalize(eyedir);
  21.  
  22.    vec3 vnorm =   normalize(norm);
  23.    vec3 vhalfvec =  normalize(halfvec);  
  24.    
  25.    vec4 baseCol = texture2D(basetex, gl_TexCoord[0].xy); 
  26.    
  27.    //Normal Map里的像素normal定義於該像素的切線空間
  28.    vec3 tbnnorm = texture2D(bumptex, gl_TexCoord[0].xy).xyz;
  29.    
  30.    tbnnorm = normalize((tbnnorm  - vec3(0.5))* 2.0); 
  31.    
  32.    float diffusefract =  max( dot(lightdir,tbnnorm) , 0.0); 
  33.    float specularfract = max( dot(vhalfvec,tbnnorm) , 0.0);
  34.    
  35.    if(specularfract > 0.0){
  36.    specularfract = pow(specularfract, shiness);
  37.    }
  38.    
  39.    gl_FragColor = vec4(amb * ambient.xyz * baseCol.xyz
  40.                  + diff * diffuse.xyz * diffusefract * baseCol.xyz
  41.                  + spec * specular.xyz * specularfract ,1.0);
  42. }

注意把normal map里的normal由(0,1)映射回(-1,1)。baseCol得到的是基底紋理的像素顏色。其余部分就是per pixel lighting的東西了。[Shader快速復習:Per Pixel Lighting(逐像素光照)]


(上為底紋理和法線紋理,下為它們與某破壁模型合作的效果,紋理from planetpixelemporium.com)
 Normal Map   www.zwqxin.com

Normal Map   www.zwqxin.com
(我想是游戲最常用的用途:磚牆。我想是最常用的NormalMap,from NEHE)
Normal Map   www.zwqxin.com
Normal Map   www.zwqxin.com
(自己把牆壁BaseMap放入Photoshop的normalMapFilter里弄的NormalMap,呃.....)

參考資料:N多網上資料+OpenGL Shading Language 2nd Edition


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com