Esfog_UnityShader教程_NormalMap法線貼圖


  咳咳,好久沒有更新了,一來是這段時間很忙很忙,再來就是自己有些懶了,這個要不得啊,趕緊補上.在前面我們已經介紹過了漫反射和鏡面反射,這兩個是基本的光照類型,僅僅依靠它們就想制作出精美的效果是遠遠不夠的,這一篇我們就來了解一下如何利用一種叫做法線貼圖的技術並結合我們前面講過的知識來制作出更精細的效果.


法線貼圖NormalMap


  首先要提到的是,什么是法線貼圖,如果大家想看更專業的解釋可以自行求助搜索引擎,這里我說一下我的個人理解:在游戲中,如果角色或物體模型做的越精細(面數越多),那么渲染后效果也就越好,但很多時候處於對時間成本(據說一個美術做一個高模是要花不少時間的)和游戲性能(面數越多,GPU的運算量就越大)的考慮,我們一般在游戲內使用的是底模(面數較少的模型),而通過其它的一些技術手段來達到相似的效果.而應用的最廣泛的可能就要數法線貼圖了.在有光照的環境下,如果物體表面是凹凸不平的,那么它在接受光照的時候在不同的區域就會呈現出不同的明暗效果來展現這種凹凸感,上兩篇中我們介紹過漫反射和鏡面反射的計算中我們都用到了物體表面的法線,正因為物體表面法線的不同才導致了最終光照結果的不同,如果我們能夠把整個模型表面各個位置的法線映射到一張二維貼圖上,然后在這張貼圖上存儲上法線的信息,不就可以達到通過底模+二維貼圖達到高模效果了么?而這里的二維貼圖就是我們所說的法線貼圖.

  為什么叫它法線貼圖呢?它和我們之前一直使用的紋理貼圖有何區別呢?紋理貼圖中我們存儲的是顏色值RGBA,而法線貼圖里存儲的是物體表面的法線,兩類貼圖的讀取映射方式都是一致的,都是通過頂點自帶信息里的texcoord里的uv坐標來讀取,不過法線讀取之后並不能直接使用,還要經過一些處理,我們會在后面說.

   下面在正式進入代碼之前,我們先來了解幾個知識點,很重要。

  1.切線空間

  這個概念並不是十分好理解,但只要仔細想想也是可以弄清楚的。我想大家對本地空間一定不陌生,一般美術做完的模型里面每個頂點的坐標都是本地坐標,也就是說對於模型上的各個部位共用一個一個統一的坐標原點,但有時候這樣並不是很方便,比如建了一個人體模型,如果我們只是想以相對手為基准而進行一些動作,而不是坐標原點,這時候原本的本地坐標系便不再適應。我們可以以手臂為基准再建立一個坐標系。綜上不難理解,之所以存在不同的坐標系,根本上是為了方便我們只考慮相關的因素,而排除不相關的因素。就像如果我想以手為基准進行一個彎手指的操作是不需要考慮我這個手指在模型空間的位置坐標的,這樣有效的降低了問題的復雜度。而切線空間的概念提出也是為了方便使用法線貼圖(當然了也許有其他用途)。下面結合圖(圖片來源CSDN作者BonChoix)來說一下:

     

    其實紋理坐標可以以一個三角面作為一個單位來分析,在一個三角面上的三個頂點他們都擁有自己的紋理坐標,而又因為三點確定一個平面(前提是三點不在一條直線啊),那么紋理坐標其實也就是個二維空間的坐標(一般橫軸是u,縱軸是v)。那對於一個三角面來說,其上的三個頂點所擁有的切線和法線(圖中T和N)其實是一樣的,通過叉乘我們可以求得笛卡爾坐標系中的兩一個坐標軸B(Binormal,一般稱之為負法線).實際上紋理坐標上的u對應的就是切線方向,而V對應的就是負法線方向。法線貼圖上存儲的法線信息也就是對應的這個三維空間。這個地方比較繞,我說的也不是很清楚,大家結合圖好好理解一下吧。

   2.DXT5nm壓縮格式

       

  美術做出來的一張法線貼圖一般來說使用的RGB三個通道的,分別用來存放法線的XYZ三個軸向的坐標。但是Unity在導入法線貼圖的時候會自動將法線貼圖壓縮成DXT5nm格式,這個格式的好處是它只使用ag兩個通道來存放兩個軸向的坐標值而另外一個軸向的坐標值由於是單位坐標可以通過1減去另外前兩個軸向坐標的平方和來得到,從而可以以同樣的容量存放更大尺寸的法線貼圖,我們都知道圖片的顏色通道存的都是非負數(法線貼圖生成的時候已經把[-1,1]壓縮為[0-1]),而我們的三維空間是[-1,1],所以我們要把它解析放大一下,方法就是對對應的顏色通道值乘以2再減1,有一點要額外說明,如果你是針對移動平台來開發游戲那么Unity不會為你壓縮法線貼圖,這意味着你還要使用RGB通道來解析法線貼圖。至於他為什么呈現藍色,因為貼圖本身還是用RGB來存的,而其中B通道對應的是z軸的方向,代表三角面的法線方向,一般來說一個片段對應的法線知識於平面法線方向有少許偏移,所以z還是接近於1的所以說會呈現出藍色。

  

  下面我們進行實例代碼的分析:

 1 Shader "Esfog/NormalMap" 
 2 {
 3     Properties 
 4     {
 5         _MainTex ("Base (RGB)", 2D) = "white" {}
 6         _NormalMap("NormalMap",2D) = "Bump"{}
 7         _SpecColor("SpecularColor",Color) = (1,1,1,1)
 8         _Shininess("Shininess",Float) = 10
 9     }
10     SubShader 
11     {
12         
13         Pass
14         {
15             Tags { "LightMode"="ForwardBase" }
16             CGPROGRAM
17             #pragma vertex vert
18             #pragma fragment frag
19             #include "UnityCG.cginc"
20             uniform sampler2D _MainTex;
21             uniform sampler2D _NormalMap;
22             uniform float4 _SpecColor;
23             uniform float _Shininess;
24             uniform float4 _LightColor0;
25 
26             struct VertexOutput 
27             {
28                 float4 pos:SV_POSITION;
29                 float2 uv:TEXCOORD0;
30                 float3 lightDir:TEXCOORD1;
31                 float3 viewDir:TEXCOORD2;
32             };
33 
34             VertexOutput vert(appdata_tan v)
35             {
36                 VertexOutput o;
37                 o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
38                 o.uv = v.texcoord.xy;
39                 float3 normal = v.normal;
40                 float3 tangent = v.tangent;
41                 float3 binormal= cross(v.normal,v.tangent.xyz) * v.tangent.w;
42                 float3x3 Object2TangentMatrix = float3x3(tangent,binormal,normal);
43                 o.lightDir = mul(Object2TangentMatrix,ObjSpaceLightDir(v.vertex));
44                 o.viewDir = mul(Object2TangentMatrix,ObjSpaceViewDir(v.vertex));
45                 return o;
46             }
47 
48             float4 frag(VertexOutput input):COLOR
49             {
50                 float3 lightDir = normalize(input.lightDir);
51                 float3 viewDir = normalize(input.viewDir);
52                 float4 encodedNormal = tex2D(_NormalMap,input.uv);
53                 float3 normal = float3(2.0*encodedNormal.ag - 1,0.0);
54                 normal.z = sqrt(1 - dot(normal,normal));
55                 float4 texColor = tex2D(_MainTex,input.uv);
56                 float3 ambient = texColor.rgb * UNITY_LIGHTMODEL_AMBIENT.rgb;
57                 float3 diffuseReflection = texColor.rgb * _LightColor0.rgb * max(0,dot(normal,lightDir));
58                 float facing;
59                 if(dot(normal,lightDir)<0)
60                 {
61                     facing = 0;
62                 }
63                 else
64                 {
65                     facing = 1;
66                 }
67                 float3 specularRelection = _SpecColor.rgb * _LightColor0.rgb * facing * pow(max(0,dot(reflect(-lightDir,normal),viewDir)),_Shininess);
68 
69                 return float4(ambient + diffuseReflection + specularRelection,1);
70             }
71             ENDCG
72         }
73         
74     } 
75     FallBack "Diffuse"
76 }

     其中一部分內容與上幾節中已經提到,這里只解釋本節新內容部分。

   第6行,由於這節中我需要額外一張法線貼圖,所以我們在Properties中定義一個新的變量來存放貼圖。與定義存放紋理貼圖的變量方式沒什么區別。

       第30~31行,之前說過計算光照在任何一個空間都可以,只要參與計算的點和向量都是在同一空間內的就可以。我們在VertexOutput里定義這兩個變量,是為了在切線空間下的入射光方向以及視線方向。這里要解釋一下為什么選擇了切線空間來計算。主要是考慮的計算量對效率的影響,如果我們在世界空間內進行計算(當然你也可以在其他空間,比如視圖空間等等),那么我們要生成一個從世界空間到切線空間的變換矩陣,由於在頂點着色器中我們無法進行紋理讀取tex2D操作,也就是無法獲得法線貼圖的數據,那么我們就必須把組成變換矩陣的相關變量通過VertexOutput傳到片段着色器里面重新組裝起來,我們需要把參與計算的光線視線轉到世界空間,再把從法線貼圖中得到的切線空間的法線向量通過矩陣轉到世界空間參與計算。由於這個操作是所有逐像素計算的所以計算量相對來說較大。而如果我們在切線空間來計算的話,就可以省去很多麻煩,由於我們現在頂點着色器中建立起一個模型空間到切線空間的轉換矩陣(具體原理后面解釋),然后把后面片段着色器計算光照需要用到的光線方向和視線方向通過矩陣轉到切線空間。而在片段着色器中我們把切線空間中的法線值拿來之后不需要做任何處理就可以參與光照計算了。這種把主要計算量從fragment轉移到vertex的思想是渲染優化的常用方法。而實際上SurfaceShader在計算光照的時候也是在切線空間的。 

      第39~42行,這四行代碼主要是為了后面建立變換矩陣。因為我們要建立從模型空間到切線空間的變換矩陣,所以我們需要使用切線空間中三個坐標軸在模型空間(本地空間)的表示,而且要單位化。為什么這么做網上可能有專業的數學解釋,這里面我只談談我自己糾結了很久很久以后所整理的一套理解方法,在第42行可以看到我們最終構造的變換矩陣第一行是T(切線)在本地坐標的表示,第二行是B(負法線)在本地坐標的表示,第三行是N(法線)在本地坐標中的表示。將這三個都單位化之后,假設T為(Tx,Ty,Tz),當我們將一個模型空間的向量以列向量(設起為(a,b,c))的方式左乘這個變換矩陣的時候。那么得到的變換后的在切線空間坐標的x = Tx*a + Ty*b + Tz *c 。看着這個式子然后你這樣想:在原本的模型空間中x軸正方向的單位向量為n(1,0,0),可以理解為在一個對於給定的向量v=(a,b,c),那么如果我們將兩者點乘則得到v·n = 1*a + b*0 + c*0=a。其實因為n是單位向量摸為1,所以v·n = |v||n|cosθ=|v|cosθ,這下明白了吧,實際上當n為單位向量時候v·n得到的是v在n方向上的投影大小.那么如果我們把一個本地空間的向量轉換到切線空間,實際上也就是要求這個向量在切線空間三個坐標軸單位方向上的投影,以之前提到的T為例,那么如果我們將本地空間中的向量v·T那么得到的就是v在切線空間x軸方向上的投影長度也就是新的x坐標了。對於另外兩個方向也是同樣的道理.就不多說了,這是我的理解方式,大家也可以以自己的想法去思考一下.在第41行中我們用cross方法進行了切線和法線的叉乘來得到負法線,注意順序,不可以寫反,而至於后面為什么乘一個v.tangent.w呢,我在國外論壇查了查,收獲不是很多,英語有限大概知道的意思是這個值只可能是1或者-1,主要是因為紋理空間的uv方向可能由於左右手不同的原因是倒着的,最終會導致差乘以后的負法線是反着的,可以通過這個w值來矯正,如果有哪位同學知道的更多請告訴我.這四行代碼可以使用Unity定義的一個宏TANGENT_SPACE_ROTATION來代替,它在UnityCG.cginc中定義.

  第43~44行,這個就是將入射光方向和視線方向從本地空間轉換到切線空間傳給片段着色器用來參與計算光照.原因上面說過了.這里我們用到了兩個新的函數ObjSpaceLightDir(float4 v)和ObjSpaceViewDir(float4 v),分別是Unity為我們封裝好的用來將得到光線和視線在本地空間中的方向向量.他們在UnityCG.cginc中有定義,大家可以自己去看。當然你也可以自己寫,不過為了減少代碼量和健壯性以后盡量使用Unity為你封裝好的除非你有特殊需求。

  第52~54行,這三行代碼是為了從法線貼圖中解析出正確的法線向量。先是從法線貼圖中讀取出rgba,然后我們說過由於是DXT5nm壓縮格式,所以我們只使用ag兩個通道,其中存放x坐標,g存放y坐標,上面說過了他們是[0-1]的范圍,我們要把他們轉換到[-1,1],再通過1-(x*x+y*y)來得到z.這樣我們就得到了法線貼圖中所要表達的法線坐標了.后面就用我們求得的法線去參與漫反射和高光的計算吧.這幾行代碼也可以用Unity提供的UnpackNormal函數代替,它在UnityCG.cginc中定義.

  

  

  (~ o ~)~系列教程的第五篇到此結束了,這篇的內容要比之前的知識在理解上有更高的難度,主要是涉及到一些數學思想在里面,如果要想在渲染方面有深入研究的話,數學是避不開的東西,所以各位同學做好准備迎接挑戰吧,我的數學也很渣渣.只能咬着牙堅持下去啊.我還法線寫博客真的是個好的鍛煉,當把自己認為已經理解了的東西寫成文字講給別人的時候你會發現新的問題,甚至之前自己理解的誤區,比如上面提到的法線關於構造變換矩陣里面的投影的想法,我是剛才寫着寫着才突然發現的.那種感覺真是爽啊(好吧,如果大家早就想到了就請無情的鄙視我吧)。這么長時間沒更新教程,我非常自責,有時候一累一懶就不寫了,這是一種自我放棄的表現,以后要注意。

  老規矩上圖看效果

          

           這是上一篇的漫反射+高光效果

    

    這是使用了咱們剛才寫的Shader的效果,很精細有沒有,腰帶上的紋路,臉上的法令紋等等.當然有點光有點爆,效果不是特別好。

    尊重他人智慧成果,歡迎轉載,請注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/3608833.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM