Unity Shader入门精要 第七章——基础纹理

目录

前言

 一、单张纹理

1、实践

 2、纹理的属性

(1)纹理类型(Texture Type)

(2)平铺模式(Wrap Mode)

 (3)Filter Mode属性

(4)纹理最大尺寸和纹理模式

二、凹凸纹理

1、高度映射

 2、法线映射

3、实践

(1)切线空间

(2)世界空间

4、Unity中的法线纹理类型

三、渐变纹理

 四、遮罩纹理


前言

        纹理的最初目的,就是使用一张图片来控制模型的外观,相当于贴图,把一张图贴在一个模型上,可以逐纹素(纹素类似于像素)地控制模型的颜色,这种技术叫纹理映射技术。

        在建模软件中会利用纹理展开技术把纹理映射坐标存储在每个顶点上。纹理映射坐标定义了该顶点在纹理图片中对应的2D坐标(也就是每个顶点对应图片中的位置)。这个坐标用一个二位变量(u,v)表示,u是横坐标,v是纵坐标,所有纹理映射坐标也成为UV坐标。

        纹理图片的大小可以是多种多样的,如256*256或者1024*1024,但转换为UV坐标时,通常都会被归一化到[0,1]范围内(如下图)。但纹理采样时使用的纹理坐标不一定在[0,1],如果不在该范围内,就要看纹理的平铺模式,它决定不在范围内的纹理坐标如何进行纹理采样,具体后面会讲到。

图1 unity对应的纹理坐标

        在OpenGL和DirectX中纹理的原点有所不同(如下图),一个在左下角,一个在左上角,Unity使用的是前者,但绝大多数情况下Unity为我们处理好了这个差异问题。

图2 OpenGL、DirectX的区别

 一、单张纹理

1、实践

         这节内容我们要实现使用一张纹理来代替物体的漫反射颜色,结果如下:

图3
Shader "MyShader/Chapter 7/Single Texture" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)//为了控制物体的整体色调
		_MainTex ("Main Tex", 2D) = "white" {} //声明一个纹理, "white" 是内置纹理的名字,
也就是一个全白的纹理
		_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;
            //每个在Properties声明的纹理属性,都要在这里声明一个float4 纹理名_ST 用来声明一个纹理的属性
			//用 纹理名_ST 的方式来声明某个纹理的屈性。ST表示的是缩放(scale)和平移(translation)的意思
			float4 _MainTex_ST;//_MainTex_ST.xy可以得到缩放值,_MainTex_ST.zw可以得到平移值
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;//TEXCOORD0语句声明的变量,Unity 会将模型的第一组纹理坐标存储到该变量中
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;//存储纹理坐标的变量UV,以便在片元着色器中使用该坐标进行纹理采样。
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				//使用纹理的属性值,对纹理进行变换
				o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;//先对纹理坐标进行缩放,再平移(偏移)
				// 也可以使用Unity自带的内置函数,如下:
				//o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);//这个函数会自动利_MainTex_ST来进行计算
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);//世界空间的法线方向
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));//世界空间的光照方向
				
				// Use the texture to sample the diffuse color
				fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;//tex2D是CG自带的函数,用于对纹理进行采样。i.uv用来返回计算得到的纹素值
				//使用采样结果和颜色属性_Color 的乘积来作为材质的反射率albedo
                
                //albedo和环境光照相乘得到环境光部分
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				//漫反射,albedo代替了_Diffuse.rgb
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				
				//计算高光反射(Blinn-Phong 光照模型)
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
				
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			
			ENDCG
		}
	} 
	FallBack "Specular"
}

        敲完上面代码后,要记得在材质选择对应的纹理图片:

图4 点击select选择图片

 2、纹理的属性

图5 纹理的属性

(1)纹理类型(Texture Type)

        向Unity导入一张纹理(图片)资源后,我们可以调整该纹理的一些属性,如上图,第一个属性就是纹理类型(Texture Type),在单张纹理这一节我们使用的是Default的类型,下面的法线纹理一节,我们会使用到Normal map的类型。我们只有选择合适的类型,才能让Unity知道我们的意图,为Unity Shader传递正确的纹理,还能进行一定的优化。

(2)平铺模式(Wrap Mode)

        Wrap Mode。这就是在前言提到的平铺模式,决定纹理坐标超过[0,1]之后将如何平铺,里面有很多终模式,重要的主要有两种:

  • Repeat模式,这种模式下,如果纹理坐标超过了1,那么整数部分将会被舍弃,直接使用小数部分进行采样,这样纹理将会不断地重复,相当于循环。
  • Clamp模式,这种模式下,如果坐标大于1,就截取到1,小于0,就截取到0。超过范围的部分会被截取到边界值。

        两种模式平铺同一张纹理的结果:

图6.1 peat
图6.2 Clamp
图6.3 纹理原图​​

        要纹理得到这样的效果,必须使用纹理的属性(纹理名_ST 变量)在Unity Shader钟对顶点纹理坐标进行相应的变换,也就是说需要有类似下面的代码:

//使用纹理的属性值,对纹理进行变换
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;//先对纹理坐标进行缩放,再平移(偏移)
// 也可以使用Unity自带的内置函数,如下:
//o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);//这个函数会自动利_MainTex_ST来进行计算

        我们还可以在材质面板钟调整纹理的缩放量和偏移量,如下面三张图,Tilling对应纹理的缩放,Offset对应纹理的偏移。下面三张图展示了Repeat模式下,不同值的情况。

图7.1
图7.2 改变tiling
图7.3 改变offest

 (3)Filter Mode属性

         图5中Wrap Mode的下面的属性就是Filter Mode属性,它决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式。Filter Mode支持三种模式:Point、Bilinear、Trilinear。三种模式的滤波效果依次提升,消耗的性能也依次增大。纹理滤波会影响放大或者缩小时得到的图片质量。例如,当我们把一张64×64 大小的纹理贴在一个512×512 大小的平面上时,就需要放大纹理。下图就是在纹理放大情况下,使用三种模式得到的结果

图8 纹理放大情况下,使用三种模式得到的结果

         纹理缩小的过程回避放大更加复杂一些,此时原纹理中的多个像素将会对应一个目标像素。更加复杂的原因是我们往往需要处理抗锯齿的问题,最常用的方法就

是使用多级渐远纹理(mipmapping)。多级渐远纹理技术将原纹理提前用滤波处理来得到很多更小的图像,形成了一个图像金字塔,每一层都是对像一层图像降采样的结果。这样实时运行是就可以快速得到结果像素,但会多占用33%的内存空间。我们可以在下图的位置启用这项技术。

图9 多级渐远纹理技术的打开

        

图10

       

        右图展示了一个从倾斜角度观察一个网格结构地板时使用不同Filter Mode(同时也使用了多级渐远纹理技术)得到的效果。

  • Point模式使用了最邻近(nearest neighbor)滤波,在放大缩小时,它的采样像素数目通常只有一个。因此看起来有点像素风的效果。
  • Bilinear模式使用了线性滤波,每个目标像素会找到四个附近像素,如何进行线性插值混合,得到最终像素,因此图像看起来被模糊了。
  • Trilinear滤波和Bilinear几乎一样,只是Trilinear还会在多级渐远纹理之间混合,如果没有开启多级渐远纹理,那么得到的结果就是一样的。

        通常我们会选择Bilinear滤波模式,如果我们希望有点像素风,可以选择Point模式。

(4)纹理最大尺寸和纹理模式

        我们为不同平台发布有些时,需要考虑目标平台的纹理尺寸和质量问题,Unity允许我们为不同的目标平台选择不同分辨的纹理。

        如果导入的纹理大小超过了Max Size中的设置值,那么 Unity 将会把该纹理缩放为这个最大分辨率。理想情况下,导入的纹理可以是非正方形的,但长宽的大小应该是2的幂,如果使用了非2的幂大小(Non Power of Two,NPOT)的纹理,那么这些纹理往往会占用更多的内存空间,而且GPU读取该纹理的速度也会有所下降。有一些平台甚至不支持这种NPOT纹理,这时Unity在内部会把它缩放成最近的2的幂大小。出于性能和空间的考虑,我们应该尽量使用2的幂大小的纹理。
        而 Format 决定了 Unity 内部使用哪种格式来存储该纹理。需要知道的是,使用的纹理格式精度越高,占用的内存空间越大,但得到的效果也越好。我们可以从纹理导入面板的最下方看到存储该纹理需要占用的内存空间(如果开启了多级渐远纹理技术,也会增加纹理的内存占用),右图的右下角就显示了图片所占空间 

图11 选择纹理最大尺寸和纹理模式

二、凹凸纹理

        纹理的另一种常见的应用就是凹凸映射。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是看起来“凹凸不平”。

        主要有两种方法拉进行凹凸映射:

  • 高度映射:使用高度纹理来模拟表面位移,然后得到一个修改后的法线值。
  • 法线映射:直接储存表面的法线。我们常常会把凹凸映射和法线映射当成是相同的技术。

1、高度映射

        使用一张凹凸图来实现凹凸映射。高度图中存储的是强度值,用于表示模型表面局部的海拔高度。颜色越浅表示表面越向外凸起,越深表示该位置越向里凹。这种方法可以非常直观的表示出一个模型表面的凹凸情况,但缺点是计算更加复杂,需要像素的灰度值计算而得,因此消耗更多的性能。

        高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息。也就是说。我们通常会使用法线映射来修改光照。

图12 高度图

 2、法线映射

        法线纹理中存储的是表面的法线方向。由于法相方向的分量范围在[-1,1],而像素在[0,1],所以说我们需要做一个映射:

pixel=\frac{normal+1}{2}

        所以我们在对法线纹理采样过后,还要对结果进行一次反映射的过程,得到原先的法线方向,也就是上面函数的逆函数:

normal=pixel*2-1

        由于方向是相对于坐标空间来说的,有这么多的坐标空间,那么法线纹理中存储的法线方向在哪个坐标空间中呢?对于模型顶点自带的法线是定义在模型空间中的,因此一种想法就是将修改后的模型空间的表面法线存储在一张纹理中,被称为模型空间的法线纹理。但我们往往会采用另一种坐标空间,即模型顶点的切线空间来存储法线,这种纹理被称为切线空间的法线纹理

        切线空间:模型的每个顶点都有属于自己的切线空间,如图13所示,切线空间的原点就是顶点本身,z轴是顶点的法线方向(n),x轴是切线方向(t),y轴由法线和切线的叉积而得,也叫副切线(bitangent, b)。

        图14展示了两种空间下的法线纹理对比。模型空间的法线纹理因为所有法线处在同一个坐标空间下,即模型空间,每个点存储的法线方向是各异的,(0,1,0)的法线方向映射后就对应了(0.5,1,0.5)浅绿色,(0,-1,0)的法线方向映射后就对应了(0.5,0,0.5)紫色,所以颜色很丰富。而切线空间下的法线纹理看起来几乎都是浅蓝色。这是因为每个法线方向所在的坐标空间是不一样的,即表面每个点各自的切线空间。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向。所以说如果一个点的法线方向不变的话,那么新的法线方向就是z轴方向,即(0,0,1),经过映射后存储在纹理中就对应了RGB(0.5,0.5,1)浅蓝色。所以浅蓝色就是说明顶点的大部分法线是和模型本身的法线一样,不需要改变。

图13 切线空间
图14 两种空间下的法线纹理对比

        模型空间存储法线纹理的优点:

  • 实现简单,更加直观
  • 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界

        切线空间存储法线纹理的优点:

  • 自由度很高
  • 可进行UV动画
  • 可重用法线纹理。比如,一个砖块,只需要使用一张法线纹理就可以用到所有六个面
  • 可压缩。切线空间的法线纹理的z方向总是正方向,所有只需要存储xy方向,从而推导得到z方向。

        所有切线空间很多情况下都优于模型空间,也可以节省美术人员的工作。下面使用的也是切线空间。

3、实践

        我们需要在计算光照模型中同意各个方向矢量所在的坐标空间。我们通常有两种选择:

  • 切线空间下计算光照。需要把光照方向、视角方向变换到切线空间。
  • 世界空间下计算光照。需要把采样得到的法线方向变换到世界空间,再和世界空间下的光照方向和视角方向进行计算。

        第一种方法效率更高,但第二种方法通用性更好。最后得到的结果如下:

图15 凹凸纹理

(1)切线空间

        基本思路是:在顶点着色器中计算了世界空间到切线空间的变换矩阵,然后用该矩阵计算切线空间下的视角方向和光照方向,在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向和光照方向等进行计算,得到最终的光照结果。

Shader "MyShader/Chapter 7/Normal Map In Tangent Space" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
		// 声明法线纹理,使用 bump 作为默认值,bump 是 Unity 内置的法线纹理
		_BumpMap ("Normal Map", 2D) = "bump" {}
		// Bump Scale 用于控制凹凸程度,当它为0时,意味着法线纹理不会对光照产生任何影响
		_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;
				// 使用 TANGENT 语义来描述 float4 的变量,以告诉 Unity 将顶点的切线方向填充至 tangent 中
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				//由于我们使用了两张纹理,因此需要存储两个纹理坐标。为此,我们把 v2f 中的 UV 变量的类型定义为 float4 类型 
				//其中 xy 分扯存储了_MainTex 的纹理坐标 zw 分量存_BumpMap纹理坐标
				float4 uv : TEXCOORD0;
				float3 lightDir: TEXCOORD1;
				float3 viewDir : TEXCOORD2;
			};

			

			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				// o.uv 设置为 float4 类型,xy存储 _MainTex 的纹理坐标,zw 存储 _BumpMap 的纹理坐标
                // 实际使用中不必如此计算,因为 _MainTex 和 _BumpMap 通常会使用同一组纹理坐标
				//出于减少插值寄存器的使用数目的目的,我们往往只计算和存储一个纹理坐标即可
				o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

				// 构造一个矩阵,将点/向量从切线空间转换为世界空间
				// 下面三行分别计算了法线、切线、副切线在世界坐标下的方向
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);//法线  
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);//切线  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; //副切线,cross函数——向量或矩阵的叉乘。v.tangent.w分量来决定切线空间中的第三个坐标轴一副切线的方向性。
			
				//如果把切线、副切线、法线按列排列就是切线空间到世界空间的变换矩阵,记为矩阵A
				//A的逆矩阵A(-)就是世界空间到切线空间的变换矩阵,但是如果只需要考虑平移和旋转变化,逆矩阵就等于转置矩阵A(t)
				//转置矩阵就相当于把切线、副切线、法线按行排列,所以下面这个矩阵就是A(t)=A(-),就是世界空间到切线空间的变换矩阵
				float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);

				//光照方向、视角方向 世界->切线空间
				o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
				o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));

				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {				
				fixed3 tangentLightDir = normalize(i.lightDir);
				fixed3 tangentViewDir = normalize(i.viewDir);
				
				//tex2D对法线纹理进行采样,采样后得到的是像素值
				fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
				fixed3 tangentNormal;
				// 如果没有把纹理类型设置为"Normal map" 则要加上下面的代码,进行手动反映射
				//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
				//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
				
				// 如果设置为Normal Map的话就可以用Unity自带的这个函数来反映射。
				tangentNormal = UnpackNormal(packedNormal);//使用UnpackNormal函数把packedNormal反映射回法线。
				tangentNormal.xy *= _BumpScale;
				// tangentNormal 的 z 分量可以通过 xy 计算而得
                // 由于使用的是切线空间下的法线纹理,因此可以保证法线方向的 z 分量为正
				// 因为法线都是单位矢量,使用x^2+y^2+z^2=1
				tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));//saturate函数将值限制到(0,1)
				//dot是点积(x,y)(x,y) = x^2+y^2				

				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 halfDir = normalize(tangentLightDir + tangentViewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
				
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			
			ENDCG
		}
	} 
	FallBack "Specular"
}

(2)世界空间

        在世界空间计算光照模型我们只需要修改v2f结构体、顶点着色器、片元着色器。

            struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
				// 一个插值寄存器最多只能存储 float4 大小的变量,对于矩阵这样的变量,我们可以把它拆成多个变量进行存储
                // 实际上,对方向向量的变换只需要使用 3x3 的矩阵,也就是说,下面的变量声明为 float3 即可
                // 但为了充分利用插值寄存器的存储空间,我们可以用下面变量的 w 分量来存储世界坐标下的顶点位置,所以用float4
				float4 TtoW0 : TEXCOORD1;  
				float4 TtoW1 : TEXCOORD2;  
				float4 TtoW2 : TEXCOORD3; 
			};
			
			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 * _BumpMap_ST.xy + _BumpMap_ST.zw;
				
				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; //cross返回两个三维向量的叉积
				
				//xyz储存矩阵,w储存世界坐标
				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 {
				// 获得在世界空间下的位置		
				float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
				// 计算在世界空间下的光照方向、视角方向
				fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				
				// 得到切线空间下的法线方向
				fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
				bump.xy *= _BumpScale;
				bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
				// 将 法线 从切线空间转换至世界空间
				bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
				
				fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

				fixed3 halfDir = normalize(lightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
				
				return fixed4(ambient + diffuse + specular, 1.0);
			}

4、Unity中的法线纹理类型

        上面的代码有提到,把法线纹理的纹理类型设置成Normal map时,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。

        把法线纹理的纹理类型设置成Normal map,可以让Unity根据不同平台对纹理进行压缩。,再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样。

        选择Normal map之后会出现一个复选框Create from Grayscale,这个选项是用于从高度图中生成法线纹理的,勾选了之后Unity会根据高度图生成一张切线空间下的法线纹理。勾选之后下面会出现两个选项——Bumpiness和Filtering,前者用于控制凹凸程度,后者决定我们使用那种方式来计算凹凸程度,它有两种选项:Smooth,这个模式生成后的法线纹理会比较平滑;Sharp,这个模式会使用Soble滤波来生成法线。

图16 Normal map纹理类型

三、渐变纹理

        下面的代码展示了如何使用一张渐变纹理来控制漫反射光照。

Shader "MyShader/Chapter 7/Ramp Texture" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_RampTex ("Ramp Tex", 2D) = "white" {}
		_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 _RampTex;
			float4 _RampTex_ST;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				//半兰伯特模型
				fixed halfLambert  = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
				//我们使用 haltLambert 来构建一个纹理坐标,并用这 纹理坐标对渐变纹理 RampTex 进行采样。
				//由于 RampTex 实际就是一个一维纹理(它在纵轴方向上颜色不变) 纹理坐标的u和v方向我们都使用了 halfLambert 。
				//然后,把从渐变纹理采样得到的颜色和材质颜色_Color 相乘得到最终的漫反色颜色。			
				fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
				
				fixed3 diffuse = _LightColor0.rgb * diffuseColor;
				//高光反射
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
				
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			
			ENDCG
		}
	} 
	FallBack "Specular"
}

        需要注意的是,我们需要把渐变纹理的Wrap Mode设为Clamp模式,防止对纹理进行采样时由于浮点数精度而造成的问题。因为理论上halfLambert再[0,1]之间,但是可能会出现1.00001这样的值出现,如果使用Repeat模式,就会舍弃整数部分,只保留小数部分变成了0.00001,对应了渐变图中最左边的值。最后得到的结果如下:

图17 渐变纹理

 四、遮罩纹理

        遮罩纹理(mask texture)。遮罩允许我们可以保护某些区域,使他们免于修改。比如我们想要某些区域的反光强一些,某些区域弱一些,就可以使用一张遮罩纹理来控制光照。另一种常见的应用是再制作地形材质时需要混合多张图片,如草地的纹理、石子的纹理、土地的纹理,使用遮罩纹理可以控制如何混合这些纹理。

        使用遮罩纹理的流程:通过采样得到遮罩纹理的纹素值 ,然后使用其中某个(或某几个)通道的值(例如 texel.r )来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保 护表面不受该属性的影响。总而言之使用遮罩纹理可以让美术 员更加精准(像素级别)地控制模型表面的各种性质。

        下面的代码展示了如何使用一张高光遮罩纹理,逐像素地控制模型表面的高光反射强度。

Shader "MyShader/Chapter 7/Mask Texture" {
	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
		_SpecularMask ("Specular Mask", 2D) = "white" {}
		_SpecularScale ("Specular 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;
			//三个纹理共同使用_MainTex_ST这一个纹理属性变量。所以再材质面板修改主纹理的平铺系数和偏移系数会同时影响到3个纹理的采样
			//这种方式可以节省需要存储的纹理坐标数目。顶点着色器中可以使用的插值寄存器是有限的。
			//因为很多时候我们不需要对纹理进行平铺和位移操作,或者很多纹理可以使用同一种平铺和唯一操作,就可以对这些纹理使用同一个纹理属性变量
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float _BumpScale;
			sampler2D _SpecularMask;
			float _SpecularScale;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float2 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;
				
				TANGENT_SPACE_ROTATION;//是Unity提供的一个宏,它定义了一个rotation矩阵用于从Object Space变换到Tangent Space。
				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(_BumpMap, i.uv));
				tangentNormal.xy *= _BumpScale;
				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 halfDir = normalize(tangentLightDir + tangentViewDir);
			 	// 对specularMask采样,这里我们选择使用r分量来计算掩码值,然后与_SpecularScale相乘,所以r值为0的时候_SpecularScale就无法影响高光
			 	fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
			 	// 计算高光,与specularMask相乘
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
			
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			
			ENDCG
		}
	} 
	FallBack "Specular"
}

         下面是效果展示以及使用的高光遮罩纹理:

图18
图19 高光遮罩纹理

猜你喜欢

转载自blog.csdn.net/buzhengli/article/details/131904719
今日推荐