實踐篇:簡單法線紋理

前言:本篇博客只是一個簡單的瞭解法線紋理創建和採樣的例子,主要是當做筆記使用。

凹凸映射:由於場景中的模型會存在凹凸不平的效果,如果把這個凹凸效果交給建模師來構建模型的話,這樣模型的三角面片將會十分的巨大,從而造成gpu的渲染壓力。這時就可以將模型貼圖的每個像素點的高度信息轉換成法線信息並以像素的形式存儲在法線紋理中。當渲染模型時就會從法線紋理中採樣獲取像素信息,並將該像素信息轉換成法線信息,然後與光照進行計算,從而得到凹凸不平的效果。

法線紋理獲取方式:如下所示:
1.使用第三方工具(CrazyBumpSetup或者ShaderMap Pro等)將源貼圖生成一張法線紋理貼圖。
2.使用unity設置面板來獲取。將類型設置成法線紋理類型,如果源紋理中不是存儲的法線數據的話,可以從源紋理灰度中獲取法線數據。如圖所示:
在這裏插入圖片描述
3.自己編寫法線紋理生成算法。以unity中從源紋理的灰度值獲取法線紋理算法爲例:
核心流程如下:
1>.將源紋理中每個像素的右邊和左邊深度差組成水平方向的差分向量,然後將像素的下邊和上邊深度差組成的垂直方向的差分向量。
2>.將水平和垂直差分向量進行叉乘來獲取法線向量,並將該法線向量轉換爲值域空間在0~1的像素數據。
3>.由於windows上用的法線解析函數是UnpackNormalDXT5nm,這個函數中法線x分量取顏色w分量,法線y分量取顏色y分量,法線z分量是在法線x和y分量上進行計算後獲取。所以將2步驟中獲取的像素中的r分量存在w位置,顏色中的g分量還是存在y位置。
4>.從法線紋理中進行採樣來獲取顏色數據,然後將顏色數據按照3步驟中的算法進行解碼來獲取到值域在-1~1之間的法線數據。
5>.將獲取到的法線數據和光照進行處理,從而得到凹凸效果。

核心代碼如下:

using UnityEngine;
using System.Collections;

public class CreateNormalMap : MonoBehaviour {
    // 原始紋理
    public Texture2D srcTex;
    // 法線紋理
    public Texture2D normalTex;

	// Use this for initialization
	void Start () {
        for (int h = 1; h < srcTex.height - 1; ++h)
        {
            for (int w = 1; w < srcTex.width - 1; ++w)
            {
                // 獲取當前像素位置的前和後對應的高度差值,此處高度值取像素中的r分量
                float uLeft = srcTex.GetPixel(w - 1, h).r;
                float uRight = srcTex.GetPixel(w +1, h).r;
                float u = uRight - uLeft;
                // 獲取當前像素水平位置的高度差分向量:u代表水平方向,x爲1,y爲0,z軸存放高度差值
                Vector3 vec_u = new Vector3(1, 0, u);
                
                // 獲取當前像素位置上和下對應的高度差值,此處高度值取像素中的r分量
                float vTop = srcTex.GetPixel(w, h - 1).r;
                float vBottom = srcTex.GetPixel(w, h + 1).r;
                float v = vBottom - vTop;
                // 獲取當前像素垂直位置的高度差分向量:v代表垂直方向,x爲0,y爲1,z軸存放高度差值
                Vector3 vec_v = new Vector3(0, 1, v);

                // 當前像素位置的水平和垂直高度差分向量進行叉乘得到法線向量
                Vector3 N = Vector3.Cross(vec_u, vec_v);
                // 將法線紋理映射到0~1的法線紋理像素中
                N.x = N.x * 0.5f + 0.5f;
                N.y = N.y * 0.5f + 0.5f;
                N.z = N.z * 0.5f + 0.5f;
                // 由於windows上用的法線解析函數是UnpackNormalDXT5nm,這個函數中法線x分量取顏色w分量,法線y分量取
                // 顏色y分量,法線z分量是在法線x和y分量上進行計算後獲取,所以此處代碼如下:
                normalTex.SetPixel(w, h, new Color(0, N.y, 0, N.x));
            }
        }

        // 同步更新法線紋理修改
        normalTex.Apply();
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

使用效果如圖所示:移動點光源到u字母附近會發現凹凸處高亮和變暗等效果。
在這裏插入圖片描述
法線紋理與光照交互:獲取到法線紋理中的法線數據後,一般需要與光源進行交互。我推薦使用內建的着色器,因爲它做了很多處理才能達到逼真的凹凸效果。但是這裏我也簡單表達下法線紋理與光照進行交互的基礎細節。
核心要點如下:
1.法線紋理當中記錄的法線數據在模型不同面上可能存在相同的採樣座標。當光源固定在某個位置時就會出現某些面的光照強度錯誤,以及背面也被渲染出來的錯誤。如圖所示:
在這裏插入圖片描述
2.頂點紋理座標系指的是一個垂直於頂點所在面的法線向量代表z軸,一個經過頂點並垂直於法線向量的切線向量代表x軸,一個垂直於法線向量和切線向量的次法線向量代表的y軸。如圖所示:
在這裏插入圖片描述
3.頂點的紋理座標系就是用來解決1中所提到的問題。它可以將光照向量轉換到頂點的紋理座標系中,從而保證每個頂點的光照是不同的。如圖所示:
在這裏插入圖片描述
4.一個pass中使用ForwardBase光照模型來處理平行光,一個pass使用ForwardAdd光照模型來處理點光源。然後使用blend one one將兩個pass中光照處理後的顏色進行混合輸出。

5.法線紋理採樣後需要使用UnpackNormal來轉換到-1~1之間的法線向量,然後將光照向量轉換到紋理座標系空間中並和獲取的法線向量求點積來獲取光照強度。由於平行光任何方向上強度一樣,點光源隨着距離物體距離越遠強度會越弱,此時可以通過光照向量w分量是否不爲0來判斷是點光源,然後使用光照向量長度來粗略的當做衰減係數使用。

核心代碼如下:

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/Bumped" {
	Properties {
		_NormalMap("Normal Map", 2D) = "white"{}
	}

	SubShader {
		// 法線紋理和平行光交互
		pass {
			// ForwardBase光照模型用來將平行光作爲重要光源進行處理
			Tags { "LightMode" = "ForwardBase" }

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "unitycg.cginc"
			#include "lighting.cginc"

			struct v2f {
				float4 pos:POSITION0;
				float2 uv:POSITION1;
				float3 lightDir:POSITION2;
			};

			sampler2D _NormalMap;

			v2f vert(appdata_tan v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				// 獲取紋理採樣座標:不考慮偏移和縮放
				o.uv = v.texcoord.xy;
				// 將頂點的切線向量和法線向量進行叉乘來獲取一個垂直於頂點切線向量和頂點法線向量的頂點次法線向量
				float3 binormal = cross(v.tangent.xyz, v.normal);
				// 獲取頂點的紋理座標系變換矩陣:頂點切線向量,頂點法線向量以及頂點次法線向量組成的三維座標系
				float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
				// 將光照向量轉換到紋理座標系空間中:目的是爲了避免不同面有相同採樣座標時造成的光照方向和背面被光照的問題
				o.lightDir = mul(rotation, _WorldSpaceLightPos0.xyz);

				return o;
			}

			fixed4 frag(v2f IN):COLOR0
			{
				// 獲取歸一化的光照向量
				float3 L = normalize(IN.lightDir);
				// 對法線紋理進行採樣來獲取像素,然後將該像素轉換成歸一化的法線信息(由原始紋理的高度值按照指定算法獲取)
				float3 N = normalize(UnpackNormal(tex2D(_NormalMap, IN.uv)));
				// 獲取光照強度,由於負值代表光照在物體背面,此時可以不做光照處理,所以值要限定在0~1之間
				float ndotl = saturate(dot(N, L));
				// 獲取平行光顏色
				fixed4 col1 = _LightColor0 * ndotl;
				// 獲取環境光 
				fixed4 col2 = UNITY_LIGHTMODEL_AMBIENT;

				return col1 + col2;
			}
			ENDCG
		}

		// 法線紋理和點光源交互
		pass {
			// ForwardAdd光照模型用來將點光源作爲重要光源進行處理
			Tags { "LightMode" = "ForwardAdd" }
			// 混合上面pass對平行光和處理
			blend one one

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "unitycg.cginc"
			#include "lighting.cginc"

			struct v2f {
				float4 pos:POSITION0;
				float2 uv:POSITION1;
				float3 lightDir:POSITION2;
			};

			sampler2D _NormalMap;

			v2f vert(appdata_tan v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				// 獲取紋理採樣座標:不考慮偏移和縮放
				o.uv = v.texcoord.xy;
				// 將頂點的切線向量和法線向量進行叉乘來獲取一個垂直於頂點切線向量和頂點法線向量的頂點次法線向量
				float3 binormal = cross(v.tangent.xyz, v.normal);
				// 獲取頂點的紋理座標系變換矩陣:頂點切線向量,頂點法線向量以及頂點次法線向量組成的三維座標系
				float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
				// 將光照向量轉換到紋理座標系空間中:目的是爲了避免不同面有相同採樣座標時造成的光照方向和背面被光照的問題
				o.lightDir = mul(rotation, _WorldSpaceLightPos0.xyz);

				return o;
			}

			fixed4 frag(v2f IN):COLOR0
			{
				// 獲取歸一化的光照向量
				float3 L = normalize(IN.lightDir);
				// 對法線紋理進行採樣來獲取像素,然後將該像素轉換成歸一化的法線信息(由原始紋理的高度值按照指定算法獲取)
				float3 N = normalize(UnpackNormal(tex2D(_NormalMap, IN.uv)));
				// 獲取光照強度,由於負值代表光照在物體背面,此時可以不做光照處理,所以值要限定在0~1之間
				float ndotl = saturate(dot(N, L));
				// 獲取衰減係數:距離物體越遠,點光源強度越弱
				float atten = 1.0;
				if (_WorldSpaceLightPos0.w != 0)
				{
					atten = 1.0 / length(IN.lightDir);
				}
				// 獲取點光源顏色
				fixed4 col = _LightColor0 * ndotl * atten;

				return col;
			}
			ENDCG
		}
	}
}

使用效果如圖所示:點光源距離物體越遠光照越暗直到不光照導致物體變黑。平行光夾角越偏離物體物體變黑,否則物體變量。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章