unity shader基础之——基础纹理:uv坐标、纹理采样、纹理属性介绍

纹理最初的目的就是使用一张图片来控制模型的外观。使用纹理映射技术可以把一张图黏在模型表面,逐纹素(名字和像素进行区分)地控制模型的颜色。

美术建模时候通常会在建模软件中利用纹理展开技术把纹理映射坐标存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。通常坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,v是纵向坐标,因此纹理映射坐标也被称为uv坐标。

纹理大小可以是多种多样的,例如可以是256X256或者1024X1024,但顶点uv坐标范围通常都被归一化到[0,1]范围内。需要注意,纹理采样时使用的纹理坐标不一定是在[0,1]范围内,实际上这种不在[0,1]范围内的纹理坐标有时会非常有用。与之关系紧密的是纹理的平铺模式,它决定渲染引擎在遇到不在[0,1]范围内的纹理坐标时如何进行纹理采样。

unity使用的纹理空间通常只有一种坐标系,unity使用的纹理空间是符合OpenGL的传统的,也就是说原点位于纹理的左下角,如下图:


一、单张纹理

我们通常使用一张纹理来代替物体的漫反射颜色,本节在shader中使用单张纹理作为模拟颜色。


1.1实践

本节使用Blinn-Phong光照模型来计算光照。

1. 首先定义纹理属性:

Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
		_Specular ("Specular", Color) = (1, 1, 1, 1)
		_Gloss ("Gloss", Range(8.0, 256)) = 20
	}

2. 然后声明pass的光照模式、指令、相应变量:

SubShader {		
		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Specular;
			float _Gloss;

与其他属性类型不同的是,我们还需要为纹理类型的属性声明一个float4类型的变量_MainTex_ST。其中_MainTex_ST的名字不是任意起的。在unity中,需要使用纹理名_ST的方式来声明某个纹理的属性。其中ST是缩放和平移的缩写。_MainTex_ST可以让我们得到我们得到该纹理的缩放和平移(偏移)值,_MainTex_ST.xy存储的是缩放量,而_MainTex_ST.zw存储的是偏移值,这些值可以在材质面板的纹理属性中调节,如下图:


3. 定义顶点着色器的输入和输出结构体:

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;
			};

首先在a2v结构体中使用TEXCOORD0语义声明了一个新的变量texcoord,这样unity就会将模型的第一组纹理坐标存储到该变量中,然后在v2f结构体中添加了用于存储纹理坐标的变量uv,以便于在片元着色器中使用该坐标进行纹理采样。

4. 然后定义顶点着色器:

v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, 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;
				// Or just call the built-in function
               //o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				return o;
			}

在顶点着色器中,我们使用纹理的属性值_MainTex_ST来对顶点纹理坐标进行变换,得到最终的纹理坐标。计算过程是,首先使用缩放属性_MainTex_ST.xy对顶点纹理坐标进行缩放,然后在使用偏移属性_MainTex_ST.zw对结果进行偏移。unity提供了一个内置宏TRANSFORM_TEX来帮助我们计算上述过程,是在unityCG.cginc文件中找到,可以直接使用它。

// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

他接受两个参数,第一个参数是顶点纹理坐标,第二个参数是纹理名,在它的实现中,将利用纹理名_ST的方式来计算变换后的纹理坐标。

5. 我们还需要实现片元着色器,并计算漫反射时使用纹理坐标的纹素值:

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;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				
				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);
			}

首先计算了世界空间下的法线方向和光照方向,然后使用Cg的tex2D函数对纹理进行采样,它的第一参数是需要被采样的纹理,第二个参数是float2类型的纹理坐标,它将计算返回得到的纹素值,我们使用采样结果和颜色属性_Color的乘积来作为材质的反射率albedo,并把它和环境光照相乘得到环境光的部分,随后使用albedo来计算漫反射的光照结果,并和环境光照、高光反射光照相加后返回。

1.2 纹理的属性

在我们向unity中导入一张纹理资源后,可以在它的材质面板上调整其属性,如下图所示:


纹理属性面板中第一个属性是纹理类型,我们当前使用的是Texture类型,还有Normal map、Cubemap类型等高级纹理类型。我们之所以要为导入的纹理选择合适的类型,是因为只有这样才能让unity知道我们的意图,为unity shader传递正确的纹理,并在一些情况下可以让unity对该纹理进行优化。

当把纹理类型设置为default时(unity低版本对应的是Texture),下面会有一个Alpha from Grayscale复选框,如果勾选了它,那么透明通道的值将每个像素的灰度值生成。

下面一个属性Wrap Mode。它决定了当纹理坐标超过[0,1]范围后将会如何被平铺。Wrap Mode有两种模式:一种是Repeat,这种模式下,如果纹理坐标超过了1,那么它的整数部分相会被舍弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复;另一种是Clamp,这种模式下,如果纹理坐标大于1,那么将会截取到1,如果小于0,将会截取到0。

如下图:



上图展示了在纹理的平铺(Tiling)属性为(3,3)时分别使用两种Wrap Mode的结果。图一使用了Repeat模式,这种模式下纹理将会不断重复,图二使用了Clamp模式,这种模式下超过范围的部分将会截取到边界值,形成一个条形结构。

需要注意的是,想要让纹理得到这样的效果,必须使用纹理的属性(_MainTex_ST变量)在unity shader中对顶点纹理坐标进行相应的变换,也就是说代码中需要包含类似下面的代码:

	o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				// Or just call the built-in function
               //o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

纹理导入面板的下一个属性是Filter Mode属性,他决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式,Filter Mode支持3种模式:Point、Bilinear、Trilinear。他们得到的图片滤波效果依次提升,但需要耗费的性能依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量,例如我们把一张64X64大小的纹理贴在一个512X512大小的平面上时,就需要放大纹理。

缩小纹理过程比放大复杂一些,此时原纹理中多个像素将会对应一个目标像素,纹理缩小更加复杂的原因在于我们往往需要处理抗锯齿问题,一个最常用的方法就是使用多级渐远纹理技术(mipmapping)。这个技术是将原纹理提取用滤波处理来得到很多更小的图像,形成了一个图像金字塔,每一层都是对上一层图像降采样的结果。这样在实时运行时,就可以快速得到结果像素,例如当物体远离摄像机时,可以直接使用较小的纹理。但缺点是需要使用一定的空间用于存储这些多级渐远纹理,通常会多占用33%的内存空间。这是一种典型的空间换取时间的方法。在unity中,我们可以在纹理导入面板中,首先将纹理类型(Texture Type)选择成 Advanced,在勾选Generate Mip Maps即可开启多级渐远纹理技术。同时还可以选择生成多级渐远纹理时是否使用线性空间(用于伽马校正)以及采用的滤波器等。

在内部实现上,Point模式使用了最近邻滤波,在放大或缩小时,它的采样像素数目通常只有一个,因此图像会看起来有种像素风格的效果。而Bilinear滤波使用了线性滤波,对于每个目标像素,它会找到4个邻近像素,然后对它们进行线性插值混合后得到最终像素,因此图像看起来被模糊了。而Trilinear滤波几乎是和Bilinear一样的,只是Trilinear还会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远技术,那么Trilinear得到的结果是和Bilinear就一样的。通常我们会选择Bilinear滤波模式。需要注意的是,有时我们不希望纹理看起来是模糊的,例如对于一些类似棋盘的纹理,我们希望它就是像素风的,这时我们可能会选择Point模式。

最后我们讲一下纹理的最大尺寸和纹理模式,当我们在为不同平台发布游戏时,需要考虑目标平台的纹理尺寸和质量问题。unity允许我们为不同目标平台选择不同的分辨率,如下图所示:


如果导入的纹理大小超过了Max Texture Size中的设置值,那么unity将会把该纹理缩放为这个最大分辨率。理想情况下,导入的纹理可以是非正方形的,但长宽大小应该是2的幂,如果使用了非2的幂的大小的纹理,那么这些纹理往往会占用更多的内存空间,而且GPU读取该纹理的速度也会有所下降。有一些平台甚至不支持这种NPOT纹理,这时unity在内部会把他缩放成最近的2的幂大小。出于性能和空间的考虑,我们应该尽量使用2的幂大小的纹理。

而Format 决定了unity内部使用哪种格式来存储该纹理。如果我们将Texture Type设置为Advanced,那么会有更多的Format供我们选择。使用的纹理格式精度越高,占用的内存空间越大,但得到的效果也越好。我们可以从纹理导入面板的最下方看到存储该纹理需要占用的内存空间(如果开启了多级渐远纹理技术,也会增加纹理的内存占用)。当游戏使用了大量Truecolor类型的纹理时,内存可能会迅速增加,因此对于一些不需要使用很高精度的纹理(例如用于漫反射颜色的纹理),我们应该尽量使用压缩格式。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/102499606

推荐阅读