簡介
以前經常聽說“模型不好看啊,怎么辦啊?”答曰“加法線”,”做了個高模,准備烘一下法線貼圖”,“有的美術特別屌,直接畫法線貼圖”.....法線貼圖到底是個什么鬼,當年天真的我真的被這個圖形學的奇淫雜技忽悠了,然而畢竟本人還算有點刨根問底的精神,決定研究一下法線貼圖的原理以及Unity下的實現。本人才疏學淺,如有錯誤,歡迎指正。
法線貼圖是目前游戲開發中最常見的貼圖之一。我們知道,一般情況下,模型面數越高,可以表現的細節越多,效果也越好。但是,由於面數多了,頂點數多了,計算量也就上去了,效果永遠是和性能成反比的。怎么樣用盡可能簡單模型來做出更好的效果就成了大家研究的方向之一。紋理映射是最早的一種,通過紋理直接貼在模型表面,提供了一些細節,但是普通的紋理貼圖只是影響最終像素階段輸出的顏色值,不能讓模型有一些凹凸之類的細節表現。而法線貼圖就是為了解決上面的問題,給我們提供了通過低面數模型來模擬高面數模型的效果,增加細節層次感,效果與高模相差不多,但是大大降低了模型的面數。
法線貼圖原理
如果還是沒理解,再看一套圖片,同樣一張圖片,旋轉180度后的結果完全相反。不信可以去截圖放到MSPaint里面轉一下試試,反正我是試了....
既然一個面的光照條件(亮度)的改變,就可以讓我們感覺這個面有凹凸感,那么上面說的,通過改變法線來改變面上某點的光照條件,進而忽悠觀察者,讓他們感覺這個面有凹凸感的方法就行得通了。
假如下面是我們的低面數模型,上面是我們的高面數模型,上面的模型在計算光照時,由於面數多,每個面的法線方向不同,所以各個面的光照計算結果都不同,就有凹凸的感覺了,而下面的低模,只有一個面,整個面的光照條件都是一致的,就沒有凹凸的感覺了。我們如果把上面的高模的法線信息保存下來,類似紋理貼圖那樣,存在一張圖里,再給低模使用,低模就可以有跟高模一樣的法線,進而在計算光照時達到和高模類似的效果,這也就是常說的烘法線的原理。
凹凸貼圖(Bump Map)

- //Bump Map
- //by:puppet_master
- //2016.12.13
- Shader "ApcShader/BumpMap"
- {
- //屬性
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _MainTex("Base 2D", 2D) = "white"{}
- _BumpMap("Bump Map", 2D) = "black"{}
- _BumpScale ("Bump Scale", Range(0.1, 30.0)) = 10.0
- }
- //子着色器
- SubShader
- {
- Pass
- {
- //定義Tags
- Tags{ "RenderType" = "Opaque" }
- CGPROGRAM
- //引入頭文件
- #include "Lighting.cginc"
- //定義Properties中的變量
- fixed4 _Diffuse;
- sampler2D _MainTex;
- //使用了TRANSFROM_TEX宏就需要定義XXX_ST
- float4 _MainTex_ST;
- sampler2D _BumpMap;
- float4 _BumpMap_TexelSize;
- float _BumpScale;
- //定義結構體:應用階段到vertex shader階段的數據
- struct a2v
- {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 texcoord : TEXCOORD0;
- };
- //定義結構體:vertex shader階段輸出的內容
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- //轉化紋理坐標
- float2 uv : TEXCOORD1;
- };
- //定義頂點shader
- v2f vert(a2v v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //把法線轉化到世界空間
- o.worldNormal = mul(v.normal, (float3x3)_World2Object);
- //通過TRANSFORM_TEX宏轉化紋理坐標,主要處理了Offset和Tiling的改變,默認時等同於o.uv = v.texcoord.xy;
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- //定義片元shader
- fixed4 frag(v2f i) : SV_Target
- {
- //unity自身的diffuse也是帶了環境光,這里我們也增加一下環境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
- //歸一化法線,即使在vert歸一化也不行,從vert到frag階段有差值處理,傳入的法線方向並不是vertex shader直接傳出的
- fixed3 worldNormal1 = normalize(i.worldNormal);
- //采樣bump貼圖,需要知道該點的斜率,xy方向分別求,所以對於一個點需要采樣四次
- fixed bumpValueU = tex2D(_BumpMap, i.uv + fixed2(-1.0 * _BumpMap_TexelSize.x, 0)).r - tex2D(_BumpMap, i.uv + fixed2(1.0 * _BumpMap_TexelSize.x, 0)).r;
- fixed bumpValueV = tex2D(_BumpMap, i.uv + fixed2(0, -1.0 * _BumpMap_TexelSize.y)).r - tex2D(_BumpMap, i.uv + fixed2(0, 1.0 * _BumpMap_TexelSize.y)).r;
- //用上面的斜率來修改法線的偏移值
- fixed3 worldNormal = fixed3(worldNormal1.x * bumpValueU * _BumpScale, worldNormal1.y * bumpValueV * _BumpScale, worldNormal1.z);
- //把光照方向歸一化
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
- //根據半蘭伯特模型計算像素的光照信息
- fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
- //最終輸出顏色為lambert光強*材質diffuse顏色*光顏色
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
- //進行紋理采樣
- fixed4 color = tex2D(_MainTex, i.uv);
- return fixed4(diffuse * color.rgb, 1.0);
- }
- //使用vert函數和frag函數
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- //前面的Shader失效的話,使用默認的Diffuse
- FallBack "Diffuse"
- }


法線貼圖是怎樣存儲的
- 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
- }

為什么法線貼圖存儲在切線空間

N = T × normalize(dx/dv, dy/dv, dz/dv)
B = N × T
- // Declares 3x3 matrix 'rotation', filled with tangent space basis
- #define TANGENT_SPACE_ROTATION \
- float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
- float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
為什么法線貼圖都是藍色的

Unity下法線貼圖Shader實現
- //Bump Map
- //by:puppet_master
- //2016.12.14
- Shader "ApcShader/NormalMap"
- {
- //屬性
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _MainTex("Base 2D", 2D) = "white"{}
- _BumpMap("Bump Map", 2D) = "bump"{}
- _BumpScale ("Bump Scale", Range(0.1, 30.0)) = 10.0
- }
- //子着色器
- SubShader
- {
- Pass
- {
- //定義Tags
- Tags{ "RenderType" = "Opaque" }
- CGPROGRAM
- //引入頭文件
- #include "Lighting.cginc"
- //定義Properties中的變量
- fixed4 _Diffuse;
- sampler2D _MainTex;
- //使用了TRANSFROM_TEX宏就需要定義XXX_ST
- float4 _MainTex_ST;
- sampler2D _BumpMap;
- float _BumpScale;
- //定義結構體:vertex shader階段輸出的內容
- struct v2f
- {
- float4 pos : SV_POSITION;
- //轉化紋理坐標
- float2 uv : TEXCOORD0;
- //tangent空間的光線方向
- float3 lightDir : TEXCOORD1;
- };
- //定義頂點shader
- v2f vert(appdata_tan v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //這個宏為我們定義好了模型空間到切線空間的轉換矩陣rotation,注意后面有個;
- TANGENT_SPACE_ROTATION;
- //ObjectSpaceLightDir可以把光線方向轉化到模型空間,然后通過rotation再轉化到切線空間
- o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
- //通過TRANSFORM_TEX宏轉化紋理坐標,主要處理了Offset和Tiling的改變,默認時等同於o.uv = v.texcoord.xy;
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- //定義片元shader
- fixed4 frag(v2f i) : SV_Target
- {
- //unity自身的diffuse也是帶了環境光,這里我們也增加一下環境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
- //直接解出切線空間法線
- float3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
- //normalize一下切線空間的光照方向
- float3 tangentLight = normalize(i.lightDir);
- //根據半蘭伯特模型計算像素的光照信息
- fixed3 lambert = 0.5 * dot(tangentNormal, tangentLight) + 0.5;
- //最終輸出顏色為lambert光強*材質diffuse顏色*光顏色
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
- //進行紋理采樣
- fixed4 color = tex2D(_MainTex, i.uv);
- return fixed4(diffuse * color.rgb, 1.0);
- }
- //使用vert函數和frag函數
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- //前面的Shader失效的話,使用默認的Diffuse
- FallBack "Diffuse"
- }
