UnityShader学习——简单法线纹理

简介

纹理的另一种常见的应用就是凹凸映射(bump mapping)。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。

(1)有两种主要的方法可以用来进行凹凸映射:

  • 高度纹理(height map):模拟表面位移(displacement),然后得到一个修改后的法线值,这种方法也被称为高度映射(height mapping);
  • 法线纹理(normal map):直接存储表面法线,这种方法又被称为法线映射(normal mapping)。

(2) 法线纹理是如何存储表面的法线方向的?

  • 由于法线方向的分量范围在[-1, 1],而像素的分量范围为[0, 1],因此我们需要做一个映射,通常使用的映射就是:pixel=(normal+1)/2。

(3)法线纹理中存储的法线方向的坐标空间:

  • 模型空间:对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理。
  • 切线空间:对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n), x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,也被称为是副切线(bitangent, b)或副法线。使用切线空间可以重用法线纹理,所以应用很广。

在世界空间计算

【注意】:这两个函数的功能都是获取从顶点出发的世界空间下的光源方向。但一个参数是模型空间的顶点坐标,另一个参数是世界空间的顶点坐标。

//【模型空间顶点】
WorldSpaceLightDir(v.vertex)
//【世界空间顶点】
UnityWorldSpaceLightDir(worldPos)

重点是vert函数和frag函数,注释中包含具体步骤。

Shader "ShaderBook/Chapter7/NormalMap" {
	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
	}
	SubShader {
		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
		    //物体颜色属性			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			//法线属性
			sampler2D _BumpMap;
			float4 _BumpMap_ST;
			float _BumpScale;
			//高光属性
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
				float4 TtoW0 : TEXCOORD1;  
				float4 TtoW1 : TEXCOORD2;  
				float4 TtoW2 : TEXCOORD3; 
			};
			
			v2f vert(a2v v) {
				//【1】定义返回数据类型
				v2f o;
				//【2】转换顶点坐标到裁剪空间,存到返回结构体中
				o.pos = UnityObjectToClipPos(v.vertex);
				//【3】把纹理坐标存到返回的结构体中				
				o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
				//【4】得到世界空间下的顶点位置、法线、切线、副切线					
				float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 
				//【5】构建切线空间到世界空间的矩阵,即切线、副切线、法线垂直堆叠。
				//     最后一列存储世界空间顶点位置。
				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);		
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {				
				//【1】物体颜色:主纹理 x 颜色
				//获取纹理颜色:tex2D(name,uv).rgb
				fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;	
				
				//【2】环境光:主灯光颜色 x 物体颜色
				//主灯光颜色:UNITY_LIGHTMODEL_AMBIENT.xyz
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;	
				
				//【3】漫反射:主灯光颜色 x 物体颜色 x 漫反射系数(世界空间法线和灯光方向的点积)
				
				// A. 世界空间法线:
				//UnpackNormal()函数是对法线纹理的采样结果的一个反映射操作,其对应的法线纹理需要设置为Normal map的格式,才能使用该函数。
				fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));//从法线纹理中采样,并反映射到(-1,1)范围内
				bump.xy *= _BumpScale;//法线的xy分量是必要的,进行scale
				bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));//法线的z分量根据xy分量计算得到
				//把法线从切线空间转换到世界空间: 3x3矩阵 乘以 3x1矩阵 得到 3x1矩阵。
				bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
				
				// B. 世界空间顶点位置:TtoW矩阵的最后一列
				float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
				
				// C. 灯光方向:根据世界空间的顶点位置得到:UnityWorldSpaceLightDir(worldPos)
				fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));			
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
				
				//【4】高光:灯光颜色 x 高光颜色 x 高光系数
				//高光系数:法线和视角方向的点积的幂
				//视角方向:根据世界空间的顶点位置得到:UnityWorldSpaceViewDir(worldPos)
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				fixed3 halfDir = normalize(lightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);	
				
				//【5】最后的颜色:环境光+漫反射+高光
				return fixed4(ambient + diffuse + specular, 1.0);
			}			
			ENDCG
		}
	} 
	FallBack "Specular"
}

当我们把纹理类型设置成Normal map时到底发生了什么呢?简单来说,这么做可以让Unity根据不同平台对纹理进行压缩,再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样

在某些平台上由于使用了DXT5nm的压缩格式,因此需要针对这种格式对法线进行解码。在DXT5nm格式的法线纹理中,纹素的a(rgba)通道(即w分量)对应了法线的x分量,g通道对应了法线的y分量,而纹理的r和b通道则会被舍弃,法线的z分量可以由xy分量推导而得。

在切线空间计算

重点是vert函数和frag函数,注释中包含具体步骤。

Shader "ShaderBook/Chapter7/NormalMapTagent" {
	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
	}
	SubShader {
		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			//物体颜色属性
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			//法线属性
			sampler2D _BumpMap;
			float4 _BumpMap_ST;
			float _BumpScale;
			//高光属性
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
				float3 lightDir: TEXCOORD1;
				float3 viewDir : TEXCOORD2;
			};

			v2f vert(a2v v) {
				//【1】定义返回数据类型
				v2f o;
				//【2】转换顶点坐标到裁剪空间,存到返回结构体中
				o.pos = UnityObjectToClipPos(v.vertex);
				//【3】把纹理坐标存到返回的结构体中				
				o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
				//【4】构建世界空间到切线空间的矩阵
				// A. 先得到世界空间下的切线、副切线、法线。
				//    可以直接构建出切线空间到世界空间的矩阵。切线、副切线、法线垂直堆叠。				
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 				
				// B. 如果一个变换中仅存在平移和旋转变换,那么这个变换的逆矩阵就等于它的转置矩阵.
				//而从切线空间到模型空间的变换正是符合这样要求的变换。
				//所以将切线、副切线、法线横向堆叠就好。
				float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
				//【5】把灯光方向、视角方向从世界空间转换到切线空间,存到返回结构体中
				//时间空间的灯光、视角方向可以从模型空间的顶点位置得到。
				o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
				o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));

				//【6】另一种更简单的把灯光和视角向量变换到切线空间的方法
				//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 {				
				//【1】计算物体颜色
				fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
				//【2】计算环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				//【3】计算漫反射
				// A. 切线空间灯光方向
				fixed3 tangentLightDir = normalize(i.lightDir);
				// B.切线空间法线法向
				fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
				fixed3 tangentNormal;
				tangentNormal = UnpackNormal(packedNormal);
				tangentNormal.xy *= _BumpScale;
				tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
				// C.漫反射公式
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
				
				//【4】计算高光
				// A. 切线空间视角方向
				fixed3 tangentViewDir = normalize(i.viewDir);
				// B. 高光公式
				fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
				
				//【5】 最终颜色
				return fixed4(ambient + diffuse + specular, 1.0);
			}			
			ENDCG
		}
	} 
	FallBack "Specular"
}

发布了195 篇原创文章 · 获赞 59 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_36622009/article/details/105489328
今日推荐