1. 程式人生 > >Shader筆記十九——三種凹凸紋理實現

Shader筆記十九——三種凹凸紋理實現

Normal Map

法線紋理是通過一張與漫反射紋理相對應的法線圖,儲存法線資訊,使用的時候對應紋理座標進行取樣,通過法線值影響光影計算的結果,從而產生凹凸效果。Normal Map可能是目前使用最為廣泛的一種凹凸貼圖技術了。之前的內容也有介紹過, https://zhuanlan.zhihu.com/p/31450857 這裡不詳述。貼一下Shader程式碼:

Shader "Bump/001_normal"
{
Properties{
	_MainColor("MainColor",Color)=(1,1,1,1)
	_SpecularColor("SpecularColor",Color)=(1,1,1,1)
	_MainTex("MainTex",2D)="white"{}
	_BumpTex("BumpTex",2D)="bump"{}
	_BumpScale("BumpScale",Float)=1.0
	_Gloss("Gloss",Range(8.0,256))=20
	}
SubShader{
	Pass{
		Tags{"RenderType"="Opaque" "LightMode"="ForwardBase"}
		CGPROGRAM

		#pragma vertex vert
		#pragma fragment frag
		#include "Lighting.cginc"
		#define PI 3.14159265359

		fixed4 _MainColor;
		fixed4 _SpecularColor;
		sampler2D _MainTex;
		float4 _MainTex_ST;
		sampler2D _BumpTex;
		float4 _BumpTex_ST;
		float _BumpScale;
		float _Gloss;

		struct a2v{
			float4 vertex:POSITION;
			float4 texcoord:TEXCOORD0;
			float3 normal:NORMAL;
			float4 tangent:TANGENT;
		};

		struct v2f{
			float4 pos:SV_POSITION;
			float4 uv:TEXCOORD0;
			float3 lightDir:TEXCOORD1;
			float3 viewDir:TEXCOORD2;
		};

		v2f vert(a2v v){
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			//主紋理與法線紋理通常使用同一組紋理座標
			o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
			o.uv.zw=v.texcoord.xy*_BumpTex_ST.xy+_BumpTex_ST.zw;
			//內建巨集,取得切線空間旋轉矩陣
			TANGENT_SPACE_ROTATION;
			o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz);
			o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz);

			return o;
		}

		fixed4 frag(v2f i):SV_Target{
			fixed3 tangentLightDir=normalize(i.lightDir);
			fixed3 tangentViewDir=normalize(i.viewDir);

			fixed3 tangentNormal=UnpackNormal(tex2D(_BumpTex,i.uv.zw));
			tangentNormal.xy*=_BumpScale;
			tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));

			fixed3 albedo=_MainColor.rgb*tex2D(_MainTex,i.uv.xy);
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;

			//改進版 BRDF 函式
			fixed3 diffuse=_LightColor0.rgb*albedo*max(0,saturate(dot(tangentNormal,tangentLightDir)))/PI;
			fixed3 halfDir=normalize(tangentLightDir+tangentViewDir);
			fixed3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss)*max(0,saturate(dot(tangentNormal,tangentLightDir)))*(_Gloss+8)/(8*PI);

			return fixed4(ambient+diffuse+specular,1.0);
		}


	ENDCG
	}
  }
}

實現效果:

Parallax Map

法線紋理在運用中有一個問題是,當視角發生變化時,並不會影響到凹凸的結果(漫反射計算與視角方向無關)。而實際上,當視角發生變化時,觀察到的凹凸不平表面的結果是不同的,為了儘量反映出凹凸效果與視角的相關性,有了Parallax Map和後面的Relief Map。Parallax Map 叫做視差貼圖,通過下圖(來自“Parallax Mapping with Offset Limiting: A PerPixel Approximation of Uneven")簡單瞭解下: 假如我們從eye所示方向觀察表面,由於表面的凹凸關係,我們實際看到的點應該是B點(即此時應該從B點處的紋理座標進行取樣),但由於紋理本身是一張平面圖,所以此時計算用的是A點的紋理座標取樣結果。為了糾正這一偏差結果,需要將取樣點的紋理座標進行適當偏移,使它靠近正確的B點,所以 Parallax Map 又叫做 Offset Map。但是想要正確的找到A點相對於B點的偏移量是比較麻煩的,大多是採用近似的偏移量來靠近B點(並不能精確到B點),這裡說一種:

通過視角方向在切線空間下的分量來確定方向,通過對應的高度圖中的取樣結果來確定偏移距離,即高度值大的偏移距離大:

//根據切線空間下的視角方向計算UV的取樣偏移
		inline float2 CaculParallaxUVOffset(v2f i){
			//高度圖高度取樣
			float height=tex2D(_HeightTex,i.uv).r;
			float3 viewDir=normalize(i.viewDir);
			float2 offset=viewDir.xy/viewDir.z*height*_HeightScale;
			return offset;
		}   

這裡用一個_HightScale係數外部控制偏移程度。Parallax Map的關鍵在於對切線空間的法線進行取樣並計算之前,通過視角方向上的偏移糾正取樣時的紋理座標,使取樣結果儘量靠近正確的取樣點。 完整Shader:

Shader "Bump/002_parallax"
{
Properties{
	_MainColor("MainColor",Color)=(1,1,1,1)
	_SpecularColor("SpecularColor",Color)=(1,1,1,1)
	_MainTex("MainTex",2D)="white"{}
	_BumpTex("BumpTex",2D)="bump"{}
	_HeightTex("HeightTex",2D)="black"{}
	_HeightScale("HeightScale",Range(0,0.2))=0.05
	_Gloss("Gloss",Range(8,255))=20
}
SubShader{
	Pass{
		Tags{"RenderType"="Opaque" "LightMode"="ForwardBase"}

		CGPROGRAM

		#pragma vertex vert
		#pragma fragment frag

		#include "Lighting.cginc"
		#define PI 3.14159265359

		fixed4 _MainColor;
		fixed4 _SpecularColor;
		sampler2D _MainTex;
		float4 _MainTex_ST;
		sampler2D _BumpTex;
		sampler2D _HeightTex;
		float _HeightScale;
		float _Gloss;

		struct a2v{
			float4 vertex:POSITION;
			float4 texcoord:TEXCOORD0;
			float3 normal:NORMAL;
			float4 tangent:TANGENT;
		};

		struct v2f{
			float4 pos:SV_POSITION;
			float2 uv:TEXCOORD0;
			float3 lightDir:TEXCOORD1;
			float3 viewDir:TEXCOORD2;
		};
		//根據切線空間下的視角方向計算UV的取樣偏移
		inline float2 CaculParallaxUVOffset(v2f i){
			//高度圖高度取樣
			float height=tex2D(_HeightTex,i.uv).r;
			float3 viewDir=normalize(i.viewDir);
			float2 offset=viewDir.xy/viewDir.z*height*_HeightScale;
			return offset;
		}

		v2f vert(a2v v){
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
			TANGENT_SPACE_ROTATION;
			o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz);
			o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz);

			return o;
		}

		fixed4 frag(v2f i):	SV_Target{
			fixed3 albedo=_MainColor.rgb*tex2D(_MainTex,i.uv);
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*albedo;
			//在對法線進行取樣前,新進行UV偏移
			i.uv+=CaculParallaxUVOffset(i);
			fixed3 tangentNormalDir=UnpackNormal(tex2D(_BumpTex,i.uv));
			fixed3 tangentLightDir=normalize(i.lightDir);
			fixed3 tangentViewDir=normalize(i.viewDir);
			fixed3 halfDir=normalize(tangentViewDir+tangentLightDir); 
			//改進版 BRDF
			fixed3 diffuse=_LightColor0.rgb*ambient*max(0,saturate(dot(tangentNormalDir,tangentLightDir)))/PI;
			fixed3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(tangentNormalDir,halfDir)),_Gloss)*max(0,saturate(dot(tangentNormalDir,tangentLightDir)))*(_Gloss+8)/(8*PI);
			
			return fixed4(ambient+diffuse+specular,1.0);
		}
		ENDCG
		}
	}
}

實現效果: 這裡將之前的Normal Map與Parallax Map進行對比(左:Normal 右:Parallax),在視角變化的情況下,Parallax的凹凸效果會發生變化。

Relief Map

Relief Map又叫 浮雕紋理 ,是對 Parallax的進一步精確。如果說Parallax只是根據視角方向在切線空間下的投影和高度圖作近似的偏移,那麼Relief Map則是以找到正確點B點進行取樣為目標,具體分為兩步:

  • 通過步進法找到交點的大致範圍
  • 通過二分法進一步找到交點

步進法找到交點大致範圍,其中一種思路: 根據切線空間下的視角方向的垂直分量,確定層級高度和UV每次偏移取樣的步進距離,視角方向的垂直分量越大,說明需要偏移的距離越小,因此劃分的密度越大。每次步進時,層高逐層增加,並沿著視角方向的UV偏移進行高度圖取樣,直到滿足 currentLayerDepth>currentDepthMapValue的條件,即圖中紅線和黃線所示,說明此時已經找到對應交點的大致範圍,進入二分法精確確定交點,這一步的具體演算法:

//根據 切線空間視角方向在垂直於紋理表面的分量,確定步進的層數,越接近垂直,層數越多,步進距離越小
			float layerNum=lerp(_MinLayerNum,_MaxLayerNum,abs(dot(float3(0,0,1),tangent_viewDir)));
			float layerDepth=1.0/layerNum;
			float currentLayerDepth=0.0;
			float2 deltaUV=tangent_viewDir.xy/tangent_viewDir.z*_HeightScale/layerNum;

			float2 currentTexCoords=uv;
			float currentDepthMapValue=tex2D(_DepthTex,currentTexCoords).r;
		
			while(currentLayerDepth<currentDepthMapValue){
				currentTexCoords-=deltaUV;
				//在迴圈內需要加上unroll來限制迴圈次數或者改用tex2Dlod,直接使用tex2D取樣會出現報錯
				currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
				currentLayerDepth+=layerDepth;
			}   

二分法精確求交點: 在確定大致範圍後,步進距離每次減半,直到逼近目標值,一般是進行五次二分逼近能得到比較接近的結果:

Relief Map與Parallax Map的區別在於一個是精確求點,一個是向正確方向大致偏移,後續的處理結果類似。 完整Shader:

Shader "Bump/003_relief"
{
Properties
{
	_MainColor("MaincColor",Color)=(1,1,1,1)
	_SpecularColor("SpecualrColor",Color)=(1,1,1,1)
	_MainTex("MainTex",2D)="white"{}
	_BumpTex("BumpTex",2D)="bump"{}
	_DepthTex("DepthTex",2D)="black"{}
	_Gloss("Gloss",Range(8,256))=20
	_HeightScale("HightScale",Range(-1.0,1.0))=0.1
	_MinLayerNum("MinlayerNum",Range(0,100))=30
	_MaxLayerNum("MaxLayerNum",Range(0,200))=50
}
SubShader{
	Tags{"RenderType"="Opaque"}
	Pass{
		Tags{"LightMode"="ForwardBase"}

		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag

		#include "UnityCG.cginc"
		#include "Lighting.cginc"
		#define PI 3.14159265359

		fixed4 _MainColor;
		fixed4 _SpecularColor;
		sampler2D _MainTex;
		float4 _MainTex_ST;
		sampler2D _BumpTex;
		sampler2D _DepthTex;
		float _Gloss;
		float _HeightScale;
		float _MinLayerNum;
		float _MaxLayerNum;

		struct a2v{
			float4 vertex:POSITION;
			float4 texcoord:TEXCOORD0;
			float3 normal:NORMAL;
			float4 tangent:TANGENT;
		};

		struct v2f{
			float4 pos:SV_POSITION;
			float2 uv:TEXCOORD0;
			float3 lightDir:TEXCOORD1;
			float3 viewDir:TEXCOORD2;
		};

		//通過步進方式找到 視角方向 與紋理交點 的實際高度值
		float2 ReliefMappingUV(float2 uv,float3 tangent_viewDir){
			//根據 切線空間視角方向在垂直於紋理表面的分量,確定步進的層數,越接近垂直,層數越多,步進距離越小
			float layerNum=lerp(_MinLayerNum,_MaxLayerNum,abs(dot(float3(0,0,1),tangent_viewDir)));
			float layerDepth=1.0/layerNum;
			float currentLayerDepth=0.0;
			float2 deltaUV=tangent_viewDir.xy/tangent_viewDir.z*_HeightScale/layerNum;

			float2 currentTexCoords=uv;
			float currentDepthMapValue=tex2D(_DepthTex,currentTexCoords).r;
		
			while(currentLayerDepth<currentDepthMapValue){
				currentTexCoords-=deltaUV;
				//在迴圈內需要加上unroll來限制迴圈次數或者改用tex2Dlod,直接使用tex2D取樣會出現報錯
				currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
				currentLayerDepth+=layerDepth;
			}

			//進行二分法查詢
			float2 halfDeltaUV=deltaUV/2.0;
			float halfLayerDepth=layerDepth/2.0;

			currentTexCoords+=halfDeltaUV;
			currentLayerDepth+=halfLayerDepth;

			int searchesNum=5;
			for(int i=0;i<searchesNum;i++){
				halfDeltaUV=halfDeltaUV/2.0;
				halfLayerDepth=halfLayerDepth/2.0;

				currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
				if(currentLayerDepth<currentDepthMapValue){
					currentTexCoords-=halfDeltaUV;
					currentLayerDepth+=halfLayerDepth;
				}
				else{
					currentTexCoords+=halfDeltaUV;
					currentLayerDepth-=halfLayerDepth;
				}
			}

			return currentTexCoords;
		}

		v2f vert(a2v v){
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
			
			TANGENT_SPACE_ROTATION;
			o.lightDir=normalize(mul(rotation,ObjSpaceLightDir(v.vertex).xyz));
			o.viewDir=normalize(mul(rotation,ObjSpaceViewDir(v.vertex).xyz));
			
			return o;
		}

		fixed4 frag(v2f i):SV_Target{
			float3 tangent_lightDir=normalize(i.lightDir);
			float3 tangent_viewDir=normalize(i.viewDir); 
			
			float2 uv=ReliefMappingUV(i.uv,tangent_viewDir);
			//去掉邊緣越界造成的紋理取樣異常
			if(uv.x>1.0||uv.y>1.0||uv.x<0.0||uv.y<0.0)
				discard;

			float3 albedo=_MainColor.rgb*tex2D(_MainTex,uv).rgb;
			float3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*albedo;

			float3 tangent_normal=normalize(UnpackNormal(tex2D(_BumpTex,uv)));
			
			//改進版 BRDF
			float3 diffuse=_LightColor0.rgb*albedo*max(0,saturate(dot(tangent_normal,tangent_lightDir)))/PI;
			float3 halfDir=normalize(tangent_viewDir+tangent_lightDir);
			float3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(saturate(dot(halfDir,tangent_normal)),_Gloss)*(8+_Gloss)/(8*PI);

			return fixed4(ambient+diffuse+specular,1.0);
		}

		ENDCG

	}
}
}

實際效果: (左:Normal 中:Parallax 右:Relief) Relief由於在Shader中進行了迴圈操作,比較費效能。