目錄app
@函數
紋理另外一種常見的應用就是凹凸映射(bump mapping)。凹凸映射的目的是使用一張紋理來修改模型表面的法線,以便爲模型提供更多的細節。這種方法不會真的改變模型的頂點位置,只是讓模型看起來好像是「凹凸不平」的,但能夠從模型的輪廓處看出「破綻」。
有兩種主要的方法能夠用來進行凹凸映射:一種方法是使用一張高度紋理(height map)來模擬表面位移(displacement),而後獲得一個修改後的法線值,這種方法也被稱爲高度映射(height mapping);另外一種方法則是使用一張法線紋理(normal map)來直接存儲表面法線,這種方法又被稱爲法線映射(normal mapping)。儘管咱們經常將凹凸映射和法線映射當成是相同的技術,但讀者須要知道它們之間的不一樣。性能
咱們首先來看第一種技術,即便用一張高度圖來實現凹凸映射。高度圖中存儲的是強度值(intensity),它用於表示模型表面局部的海拔高度。所以,顏色越淺代表該位置的表面越向外凸起,而顏色越深代表該位置越向裏凹。這種方法的好處是很是直觀,咱們能夠從高度圖中明確的知道一個模型表面的凹凸狀況,但缺點是計算更加複雜,在實時計算時不能直接獲得表面法線,而是要由像素的灰度值計算而得,所以須要消耗更多的性能。
高度圖一般會和法線映射一塊兒使用,用於給出表面凹凸的額外信息。也就是說,咱們一般會使用法線映射來修改光照。優化
而法線紋理存儲的就是表面的法線方向。因爲法線方向的份量範圍在[-1,1],而像素的份量範圍爲[0,1],所以咱們要作一個映射,一般使用的映射就是:
這就要求,咱們正在Shader中對法線紋理進行紋理採樣後,還須要對結果進行一次反映射的過程,以獲得原先的法線方向。反映射的過程實際上就是使用上面映射函數的逆函數:
然而,因爲方向是相對於座標空間來講的,那麼法線紋理中存儲的法線的方向在哪一個座標空間中呢?對於模型頂點自帶的法線,它們是定義在模型空間中的,所以一種直接的想法就是將修改後的模型空間中的表面法線存儲在一張紋理中,這種紋理被稱爲是模型空間的法線紋理(object-space normal map)。然而,在實際製做中,咱們每每會採用另外一種座標空間,即模型頂點的切線空間(tangent space)來存儲法線。對於模型的每一個頂點,它都有一個屬於本身的切線空間,這個切線空間的原點就是該頂點自己,而z軸是頂點的法線方向(n),x軸是頂點的切線方向(t),而y軸能夠由法線和切線叉積獲得,也被稱爲是副切線(bitangent,b)或副法線,以下圖所示
這種紋理被稱爲是切線空間的法線紋理(tangent-space normal map)。下圖分別給出了模型空間和切線空間的法線紋理。
從圖中能夠看出,模型空間下的法線紋理看起來是「五光十色」的。這是由於全部法線所在的座標空間是同一個座標空間,即模型空間,而每一個點存儲的法線方向是各異的,有的是(0,1,0),通過映射後存儲到紋理中就對應了RGB(0.5,1,0.5)淺綠色,有的是(0,-1,0),通過映射後存儲到紋理中就對應了(0.5,0,0.5)紫色。而切線空間下的法線紋理看起來幾乎所有是淺藍色的。這是由於,每一個法線方向所在的座標空間是不同的,既是表面每點各自的切線空間,這種法線紋理其實就是存儲了每一個點在各自的切線空間中的法線擾動方向。也就是說,若是一個點的法線方向不變,那麼在它的切線空間中,新的法線方向就是z軸方向,即值爲(0,0,1),通過映射後存儲在紋理中就對應了RGB(0.5,0.5,1)淺藍色。而這個顏色就是法線紋理中大片的藍色。這些藍色實際上說明頂點的大部分法線和模型法線自己是同樣的,不須要改變。
整體來講,模型空間下的法線紋理更符合人類的直觀認識,並且法線紋理自己也很直觀,容易調整,由於不一樣的法線方向就表明了不一樣的顏色。但美術人員更喜歡用切線空間下的法線紋理。那麼爲何他們更偏好使用這種看起來「很蹩腳」的切線空間呢?
實際上,法線自己存儲在哪一個座標系裏都是能夠的,咱們甚至能夠選擇存儲在世界空間下。但問題是,咱們並非單純的想要獲得法線,後續的光照計算纔是咱們的目的。而選擇哪一個座標系意味着咱們須要把不一樣的信息轉換到相應的座標系中。例如,若是選擇了切線空間,咱們須要把從法線紋理中獲得的法線方向從切線空間轉換到世界空間或其它空間中。動畫
整體來講,使用模型空間存儲法線的優勢以下:
(1)實現簡單,更加直觀。咱們甚至不須要模型原始的法線和切線等信息,也就是說,計算更少。生成它也很是簡單,而若是要生成切線空間下的法線紋理,因爲模型的切線通常和UV方向相同,所以想要獲得比較好的法線映射就要求紋理映射也是連續的。ui
(2)在紋理座標的縫合處和尖銳的邊角部分,可見的突變(縫隙)較少,便可以提供平滑的邊界。這是由於模型空間下的法線紋理存儲的是同一座標系下的法線信息,所以在邊界處經過插值獲得的法線能夠平滑變換。而切線空間下的法線紋理中的法線信息是依靠紋理座標的方向獲得的結果,可能會在邊緣處或尖銳的部分形成更多可見的縫合跡象。
但使用切線空間有更多優勢:
(1)自由度很高。模型空間下的法線紋理記錄的是絕對法線信息,便可用於建立它時的那個模型,而應用到其它模型上的效果就徹底錯誤了。而切線空間下的法線紋理記錄的是相對法線信息,這意味着,即使把該紋理應用到一個徹底不一樣的網格上,也能夠獲得一個合理的結果。
(2)可進行UV動畫。好比,咱們能夠移動一個紋理的UV座標來實現一個凹凸移動的效果,但使用模型空間下的法線紋理會獲得徹底錯誤的結果。緣由同上。這種UV動畫在水或火山熔岩這種類型的物體上會常常用到。
(3)能夠重用法線紋理。好比一個磚塊,咱們僅使用一張法線紋理就能夠用到全部的六個面上,緣由同上。
(4)可壓縮,因爲切線空間下的法線紋理中的法線z方向老是正方向,所以咱們能夠僅存儲XY方向,從而推導獲得Z方向。而模型空間下的法線紋理因爲每一個方向都是可能的,所以必須存儲3個方向的值,不可壓縮。
切線空間下的法線紋理的前兩個優勢足以讓不少人放棄模型空間下的法線紋理而選擇它。從上面的優勢能夠看出,切線空間在不少狀況下都優於模型空間,並且能夠節省美術人員的工做。所以咱們也使用切線空間下的法線紋理編碼
咱們須要在計算光照模型中統一各個方向矢量所在的座標空間。因爲法線紋理中存儲的法線是切線空間下的方向,所以,咱們一般有兩種選擇:
(1)在切線空間下進行光照計算,此時咱們須要把光照方向、視角方向轉換到切線空間下。
(2)在世界空間下進行光照計算,此時咱們須要把採樣獲得的法線方向變換到世界空間下,再和世界空間下的光照方向和視角方向進行計算。從效率上來講,第一種方法每每要優於第二種方法,由於咱們在頂點着色器中就能夠完成對光照方向和視角方向的變換,而第二種方法因爲要先對法線紋理進行採樣,因此變換過程必須在片元着色器中實現,這意味着咱們須要在片元着色器中進行一次矩陣操做。但從通用性角度來講,第二種方法要優於第一種方法,由於有時咱們須要在世界空間下進行一些計算,例如用Cubemap進行環境映射時,咱們須要使用世界空間下的反射方向對Cubemap進行採樣。若是同時須要進行法線映射,咱們就須要把法線方向變換到世界空間下。固然讀者能夠選擇其餘的座標空間進行計算,例如模型空間等,但切空間和世界空間是最爲經常使用的兩個空間。下面,咱們一次實現上述的兩種方法。spa
基本思路是:在片元着色器中經過紋理採樣獲得切線空間下的法線,而後再與切線空間下的視角方向、光照方向等進行計算,獲得最終的光照結果。爲此,咱們首先要在頂點着色器中把視角方向和光照方向從模型空間變換到切線空間中,即咱們須要知道從模型空間到切線空間的變換矩陣。這個變換矩陣的逆矩陣,即從切線空間到模型空間的變換矩陣是很容易求得的,咱們在頂點着色其中按切線(x軸)、副切線(y軸)、法線(z軸)的順序按列排列便可獲得。在之前咱們講過,若是在一個變換中僅存在平移和旋轉變換,那麼這個變換矩陣的逆矩陣就等於它的轉置矩陣,而從切線空間到模型空間的變換正符合這個要求。所以從模型空間到切線空間變換的逆矩陣就是從切線空間到模型空間的轉置矩陣,咱們把切線(x軸)、副切線(y軸)、法線(z軸)的順序按行排列便可獲得,在本節的最後,咱們能夠獲得相似下圖的效果:
(1)在Properties語義塊中添加法線紋理屬性,以及用於控制凹凸程度的屬性:code
Properties{ _Color("Color Tint", Color) = {1,1,1,1} _MainTex("Main Tex",2D)="white"{} _BumpMap("Normal Map",2D)="bump"{} _BumpScale("Bump Scale",Float) = 1.0 _Specular("Specular",Color)=(1,1,1,1) _Gloss("Gloss",Range(8.0,256))=20 }
對於法線紋理_BumpMap,咱們使用"bump"做爲它的默認值。''bump''是Unity內置的法線紋理,當沒有提供任何法線紋理時。''bump''就對應了模型自帶的法線信息。_BumpScale則是用於控制凹凸程度的,當它爲0時,意味着該法線紋理不會對光照產生任何影響。
(2)咱們在SubShader語義塊中定義了一個Pass語義塊,而且在Pass的第一行指明瞭該Pass的光照模式:component
SubShader{ Pass{ Tags{"LightMode"="ForwardBase"} } }
LightMode標籤是Pass標籤的一種,它用於定義該Pass在Unity的光照流水線中的角色。
(3)接着,咱們使用CGPROGRAM和ENDCG來包圍住Cg代碼片,以定義最重要的頂點着色器和片元着色器代碼。首先咱們使用#pragma指令來告訴Unity,咱們定義的頂點着色器和片元着色器叫什麼名字,在本例中它們的名字分別是vert和frag:
CGPROGRAM #pragma vertex vert #pragma fragment frag
(4)爲了使用Unity內置的一些變量,例如_LightColor0,還須要包含進Unity的內置文件Lighting.cginc:
#include "Lighting.cginc"
(5)爲了和Properties語義塊中的屬性創建聯繫,咱們在Cg代碼塊中聲明瞭和上述類型匹配的變量:
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; float _Gloss;
爲了獲得該紋理的屬性(平鋪和偏移係數),咱們爲_MainTex和_BumpMap定義了_MainTex_ST和_BumpMap_ST變量。
(6)咱們已經知道,切線空間是由頂點法線和切線構建出的一個座標空間,所以咱們須要獲得頂點的切線信息。爲此,咱們修改頂點着色器的輸入結構體a2v:
struct a2v{ float4 vertex:POSITION; float3 normal:NORMAL; float4 tangent:TANGENT; float4 texcoord:TEXCOORD0; };
咱們使用TANGENT語義來描述float4類型的tangent變量,以告訴Unity把頂點的切線方向填充到tangent變量中。須要注意的是,和法線方向normal不一樣,tangent的類型是float4,而非float3,這是由於咱們須要tangent.w份量來決定切線空間中的第三個座標軸——副切線的方向性。
(7)咱們須要在頂點着色器中計算切線空間下的光照和視角方向,所以咱們在v2f結構體中添加了兩個變量來存儲變換後的光照和視角方向:
struct v2f{ float4 pos:SV_POSITION; float4 uv:TEXCOORD0; float3 lightDir:TEXCOORD1; float3 viewDir:TEXCOORD2; }
(8)定義頂點着色器:
v2f vert(a2v v){ v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; o.uv.zw=v.texcoord.xy*_BumpMap_ST.xy+_BumpMap_ST.zw; //Compute the binormal // float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w; // //Construct a matrix which transform vectors from object space to tangent space // float3×3 rotation = float3×3(v.tangent.xyz,binormal,v.normal); //Or just use the bulid-in macro TANGENT_SPACE_ROTATION; //Transform the light direction from object space to tangent space o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; //Transform the view direction from object space to tangent space o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; return o; }
因爲咱們使用了兩張紋理,所以咱們須要存儲兩個紋理座標。爲此,咱們把v2f中的uv變量的類型定義爲float4類型,其中xy份量存儲了_MainTex的紋理座標,而zw份量存儲了_BumpMap的紋理座標(實際上,_MainTex和_BumpMap一般會使用同一組紋理座標,出於減小插值寄存器的使用數目的目的,咱們每每只計算和存儲一個紋理座標便可)。而後咱們把模型空間下的切線方向、副切線方向和法線方向按行排列來獲得從模型空間到切線空間的變換矩陣rotation。須要注意的是,在計算副切線時咱們使用v.tangent.w和叉積的結果相乘,這是由於和切線與法線方向都垂直的反向有兩個,而w決定了咱們選擇其中哪個方向。Unity也提供了一個內置宏TANGENT_SPACE_ROTATION(在UnityCG.cginc中被定義)來幫助咱們直接計算獲得rotation變換矩陣,它的實現和上述代碼徹底同樣。而後咱們使用Unity的內置函數ObjSpaceLightDir和ObjSpaceViewDir來獲得模型空間下的光照和視角方向,再利用變換矩陣rotation把它們從模型空間變換到切線空間中。
(9)因爲咱們在頂點着色器中完成了大部分工做,所以片元着色器只須要採樣獲得切線空間下的法線方向再在切線空間下進行光照計算便可:
fixed4 frag(v2f i):SV_Target{ fixed3 tangentLightDir=normalize(i.lightDir); fixed3 tangentVieDir=normalize(i.viewDir); //Get the texel in the normal map fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw); fixed3 tangentNormal; //If the texture is not marked as"Normal map" //tangentNormal.xy = (packedNormal.xy*2-1)*_BumpScale; //tengentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy))); //Or mark the texture as"Normal map",and use the built-in function; tangentNormal=unpackNormal(packedNormal); tangentNormal.xy*=_BumplScale; tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy))); fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo; fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir)); fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDIR)),_Gloss); return fixed(ambient+diffuse+specular,1.0); }
在上面的代碼中,咱們首先利用tex2D對法線紋理_BumpMap進行採樣。正如咱們前面所講,法線紋理中存儲的是把法線通過映射後獲得的像素值,所以咱們須要把它們反映射回來。若是咱們沒有在Unity裏把該法線紋理的類型設置成Normal map,就須要在代碼中手動進行這個過程。咱們首先把packedNormal的xy份量按以前提到的公式映射回法線方向,而後乘以_BumpleSclae(控制凹凸程度)來獲得tangentNormal的xy份量。因爲法線都是單位矢量,所以tangent.z份量能夠由tangentNormal.xy計算而得。因爲咱們使用的是切線空間下的法線紋理,所以咱們能夠保證法線方向的z份量爲正。在Unity中,爲了方便Unity對法線紋理的存儲進行優化,咱們一般會把法線紋理的紋理類型標識成Normal map,Unity會根據平臺來選擇不一樣的壓縮方法。這時,若是咱們再使用上面的方法來計算就會獲得錯誤的結果,由於此時_BumpMap的rgb份量並再也不是切線空間下法線方向的xyz值了,後面咱們會解釋。在這種狀況下,咱們可使用Unity的內置函數UnpackNormal來獲得正確法線方向。
(10)最後,咱們爲該Unity Shader設置合適的Fallback;
Fallback"Specular"
保存後返回Unity查看。在NormalMapTangentMat的面板上。咱們能夠調整材質面板中的Bump Scale屬性來改變模型的凹凸程度。下圖給出了不一樣的Bump Scale屬性值下獲得的結果。
如今,咱們來實現第二種方法,即在世界空間下計算光照模型。咱們須要在片元着色器中把法線方向從切線空間變換到世界空間下。這種方法的基本思想是:在頂點着色器中計算從切線空間到世界空間的變換矩陣,並把它傳遞給片元着色器。變換矩陣能夠由定點的切線、副切線和法線在世界空間下的表示來獲得。最後,咱們只需在片元着色器中把法線紋理的法線方向從切線空間變換到世界空間下便可。儘管這種方法須要更多的計算,但在須要使用Cubemap進行環境映射等狀況下,咱們就須要使用這種方法。
(1)咱們須要修改頂點着色器的輸出結構體v2f,使它包含從切線空間到世界空間的變換矩陣:
struct v2f{ float4 pos:SV_POSITION; float4 uv:TEXCOORD0; float4 TtoW0:TEXCOORD1; float4 TtoW1:TEXCOORD2; float4 TtoW2:TEXCOORD3; }
咱們在之前講過,一個插值寄存器最多隻能存儲float4大小的變量,對於矩陣這樣的變量,咱們能夠把它們按行拆分紅多個變量再進行存儲。上面代碼中的TtoW0、TtoW一、TtoW2就依次存儲了從切線空間到世界空間的變換矩陣的每一行。實際上,對方向矢量的變換隻須要使用3×3大小的矩陣,也就是說每一行只須要使用float3類型的變量便可。但爲了利用插值寄存器的存儲空間,咱們把世界空間下的頂點位置存儲在這些變量的w份量中。
(2)修改頂點着色器,計算從切線空間到世界空間的變換矩陣:
v2f vert(a2v v){ v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; o.uv.zw=v.texcoord.xy*_Bump_ST+_BumpMap_ST.zw; float3 worldPos=mul(_Object2World,v.vertex).xyz; fixed3 worldNormal=UnityObjectToWorldNormal(v.normal); fixed3 worldTangent=UnityObjectToWorldDir(v.tangent.xyz); fixed3 worldBinormal=cross(worldNormal,worldTangent)*v.tangent.w; //Compute the matrix that transform directions from tangent space to world space //put the world position in w component for optimization o.TtoW0=float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x); o.TtoW1=float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y); o.TtoW2=float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z); }
在上面的代碼中,咱們計算了世界空間下的頂點切線、副切線和法線的矢量表示,並把它們按列擺放獲得從切線空間到世界空間的變換矩陣。咱們把該矩陣的每一行分別存儲在TtoW0、TtoW一、TtoW2中,並把世界空間下的頂點位置的xyz份量分別存儲在了這些變量的w份量中,以便充分利用插值寄存器的存儲空間。
(3)修改片元着色器,在世界空間下在進行光照計算:
fixed4 frag(v2f i):SV_Target{ //Get the position in world space float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); //Compute the light and vie Dir in world space fixed3 lightDir=normalize(UnityWorldSpaceLightDir(worldpos)); fixed3 viewDir=normalize(UnityWorldSpaceViewDir(worldPos)); //Get the normal in tangent space fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw)); bump.xy*=_BumpScale; bump.z=sqrt(1.0-saturate(dot(bump.xy,bump.xy))); //Transform the normal from the tangent space to world space bump=normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump))); ..... }
咱們首先從TtoW0、TtoW一、TtoW2的w份量中構建世界空間下的座標。而後,使用內置的UnityWorldSpaceLightDir和UnityWorldSpaceViewDir函數獲得世界空間下的光照和視角方向。接着咱們使用內置的UnpackNormal函數對法線紋理進行採樣和編碼(須要把法線的紋理格式標識成Normal map),並使用_BumpScale對其進行縮放。最後咱們使用TtoW0、TtoW1和TtoW2存儲的變換矩陣把法線變換到世界空間下。這是使用點乘操做來實現矩陣的每一行和法線相乘來獲得的。
從視覺表現上,在切線空間下和在世界空間下計算光照幾乎沒有任何差異。在Unity4.x版本中,在不須要使用Cubemap進行環境映射的狀況下,內置的UnityShader使用的都是切線空間來進行法線映射和光照計算。而在Unity5.x中,全部內置的Unity Shader都是用了世界空間來進行光照計算。這也是爲何Unity5.x中表面着色器更容易報錯,由於它們使用了更多的插值寄存器來存儲變換矩陣(還有一些額外的插值寄存器是用來輔助計算霧效的,更多內容後面咱們會見到)。
上面咱們提到了當把法線紋理的紋理類型標識成Normalmap時,可使用Unity的內置函數UnpackNormal來獲得正確的法線方向,以下圖所示
當咱們須要使用那些包含了法線映射的內置的Unity Shader時,必須把使用的法線紋理按上面的方式標識成Normalmap才能獲得正確的結果(即使你忘了這麼作,Unity也會在材質面板中提醒你修正這個問題)。這是由於UnityShader都使用了內置的UnpackNormal函數來採樣法線方向。那麼當咱們把紋理類型設置成Normalmap時到底發生了什麼呢?爲何要這麼作呢?
簡單來講,這麼作可讓Unity根據不一樣的平臺對紋理進行壓縮(例如使用DXT5nm格式),再經過UnpackNormal函數來針對不一樣的壓縮格式對法線紋理進行正確的採樣。咱們能夠在UnityCG.cginc裏找到UnpackNormal函數的內部實現:
inline fixed3 UnpackNormalDXT5nm(fixed4 packednormal) { fixed3 normal; normal.xy=packednormal.wy*2-1; normal.z=sqrt(1-saturate(dot(normal.xy,normal.xy))); return normal; inline fixed3 UnpackNormal(fixed4 packednormal) { #if defined(UNITY_NO_DXT5nm) return packednormal.xyz*2-1; #else return UnpackNormalDXT5nm(packednormal); #endif } }
從代碼中能夠看出,在某些平臺因爲使用了DXT5nm的壓縮格式,所以須要針對這種格式對法線進行解碼。在DXT5nm格式的法線紋理中,紋素的a通道(即w份量)對應了法線的x份量,g通道對應了法線的y份量,而紋理的r和b通道會被捨棄,法線的z份量可由xy份量推導而得。爲何以前的普通紋理不能按這種方式壓縮,而法線就要按DXT5nm格式來進行壓縮呢?這是由於,按咱們以前的處理方式,法線紋理被當成一個和普通紋理無異的圖,但實際上,它只有兩個通道是真正必不可少的,由於第3個通道的值能夠用另外兩個推導出來(法線時單位向量,而且切線空間下的法線方向的z份量始終爲正)。使用這種壓縮方法就能夠減小法線紋理佔用的內存空間。
當咱們把紋理設置成Normal map後,還有一個複選框是Creat from Grayscale,那麼它是作什麼用的呢?讀者應該還記得在本節開始咱們提到過另外一種凹凸映射的方法,即便用高度圖,而這個複選框就是用於從高度圖中生成法線紋理的。高度圖中自己記錄的是相對高度,是一張灰度圖,白色表示相對更高,黑色表示相對更低。當咱們把一張高度圖導入Unity後,除了須要把它的紋理類型設置成Normal map外,還須要勾選Create from Grayscale,這樣就能夠獲得相似下圖的效果,而後咱們就能夠把它和切線空間下的法線紋理同等對待了。
當勾選了Creat from Grayscale後,還多出了兩個選項——Bumpiness和Filtering。其中Bumpiness用於控制凹凸程度,而Filtering決定咱們使用哪一種方式來計算凹凸程度,它有兩種選項:一種是Smooth,這使得生成後的法線紋理會比較平滑;另外一種是Sharp,它會使用Sobel濾波(一種邊緣檢測時使用的濾波器)來生成法線。Sobel濾波的實現很是簡單,咱們只須要在一個3×3的濾波器中計算x和y方向上的導數,而後從中獲得法線便可。具體的方法是:對於高度圖中的每一個像素,咱們考慮它與水平方向和豎直方向的像素差,把它們的差當成該點的對應的法線在x和y方向上的位移,而後使用以前提到的映射函數存儲成到法線紋理的r和g份量便可。