通过实现一个只有颜色属性可调节的简单材质效果更好的了解顶点与片段着色器

一、顶点着色器

   顶点着色器就是处理顶点的着色器,每个顶点都会执行一次顶点着色器。我们先认识下顶点函数的结构:

  1. 顶点着色器函数的名称,在上面我们已经指定了顶点着色器的名称就是vert,所以这里我们必须要用vert作为名称。
  2. 其中float4 vertex是我们自己定义的一个四维向量,名字叫vertex(名字我们可以随便起),仅仅定义一个四维向量并不能使它拥有我们模型的顶点信息,所以这里我们需要为它指定一个语义——POSITION,POSITION就是代表着模型的顶点位置信息。此时变量vertex就表示着我们模型的顶点位置。
  3. 在顶色着色器中最主要的事情就是将顶点从模型坐标转换到裁剪坐标(说白了就是将模型显示在二维显示器上时需要做的一些矩阵转换)。不会矩阵转换怎么办,没关系,Unity已经为我们准备好现成的命令了,只需调用UnityObjectToClipPos即可,后面括号中加上我们的顶点位置变量就可以了。
  4. 然后呢,在后面片断着色器中我们需要顶点着色器中的输出结果,所以3中需要加上return来将转换后的顶点返回,float4就是用来定义我们返回的是四维向量。
  5. 经过变换后返回的顶点位置,我们也需要利用语义来标记一下,以便片断着色器可以知道哪个是从顶点着色器输出过来的顶点位置信息。所以我们在函数的后面加上: SV_POSITION。
简单地说POSITION语义是用于顶点着色器,用来指定模型的顶点位置,是在变换前的顶点的本地空间坐标。SV_POSITION语义则用于像素着色器,用来标识经过顶点着色器变换之后的顶点坐标。
关于语义,我们在补充下以下几种,根据这些语义顶点着色器就可以知道谁是模型的顶点数据谁又是模型的法线数据
struct appdata
	{
		float4 vertex : POSITION;		//顶点
		float4 tangent : TANGENT;		//切线
		float3 normal : NORMAL;			//法线
		float4 texcoord : TEXCOORD0;	        //UV1
		float4 texcoord1 : TEXCOORD1;	        //UV2
		float4 texcoord2 : TEXCOORD2;	        //UV3
		float4 texcoord3 : TEXCOORD3;	        //UV4
		fixed4 color : COLOR;			//顶点色
	};
在顶点着色器中处理顶点时,我们首先需要获取到模型的顶点数据(比如顶点位置、法线信息、顶点颜色等等),那么这些数据都是直接存储在模型中的,我们在Shader中只需要通过标识语义就可以自动获得。

此时我们的Shader还不能正常编译,因为了除了顶点着色器外,还们需要一个片断着色器。

二、片段着色器:

      片断着色器也被称作像素着色器,主要是处理最终显示在屏幕上的像素结果。经过顶点着色器的处理,我们已经得到了最终显示在屏幕上的顶点矩阵,内部会自动进行插值计算,以获得当前模型的所有片断像素,然后每个像素都会执行一次片断着色器,得到最终每个像素的颜色值。

  1. 片断着色器的函数名,其中()中是空的,因为在这个简单的示例中我们并不需要额外的数据传过来,所以暂时为空。
  2. 在Cg/HLSL中使用Properties中的变量前还需要在Cg/HLSL中再重新声明一次,名称要求一致。这是死规则,我们只能按照要求来执行。float、half、fixed,这三都是浮点数的表示,只是分别对应的精度不一样,主要用此可以进行更进一步的优化。
  3. 去们直接返回_Color,也就是直接返回我们在材质面板中定义的颜色,这也是我们这个小例子想要的效果。
  4. 同样,返回的值是个四维向量,我们用float4来表示,如果想优化的话就用fixed4来表示,关于精度问题我们后面会专门讲解,这里不是重点。
  5. SV_TARGET是系统值,表示该函数返回的是用于下一个阶段输出的颜色值,也就是我们最终输出到显示器上的值。

        顶点着色器与片断着色器的执行并不是1:1的,举个例子,一个三角面片,只有三个顶点,顶点着色器只需执行3次,而片断着色器由最终的像素数来决定,执行几百上千都是很正常的。所以从性能的角度来考虑,我们要尽量把计算放在顶点着色器中去执行。其次在片断着色器中也要尽量的简化算法,节省开支。

三、整合顶点与片段着色器

Shader "Unlit/MyFirstShader"
{
	Properties
	{
		_Color("Color", Color) = (1,1,1,1)
	}
	
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Color;
			
			float4 vert ( float4 vertex : POSITION ) : SV_POSITION
			{
				return UnityObjectToClipPos(vertex);
			}
			
			fixed4 frag () : SV_Target
			{
				return _Color;
			}
			ENDCG
		}
	}	
}

        如上图所示的代码中,我们在应用程序阶段把模型的顶点位置信息(float4 vertex:POSITION)传输到了几何阶段,然后在顶点着色器中利用UnityObjectToClipPos矩阵转换把模型顶点坐标从本地转换到齐次裁剪坐标中,并输出转换后的坐标(SV_POSITION)到光栅化阶段中,最后在光栅化的片元着色器中我们给所有像素都返回了一个颜色值_Color。

       但是仔细思索我们会发现有些局限,如果我想在应用程序阶段传递多个值呢,除了顶点位置还想传递顶点色、UV坐标等信息那要怎么办呢?同时现在从几何阶段的顶点着色器输出的只有顶点坐标(SV_POSITION),如果我也想传递其它值到片元着色器中又该怎么办呢?

这个时候就该结构体(structure)出场了。

四、Struct

     结构体就像是一个组或者说是一个容器,我们可以在其中存放多个变量,然后在各个阶段传递时我们就传递这个结构体就好了,这样子就把结构体中我们定义的多个变量一同传递过去了。

    struct具体语法:

  1. 结构体的声明以关键字 struct 开始,然后紧跟结构体的名字,结构体范围由{}定义,最后以分号结尾。
  2. 使用“.”引用结构体中的成员变量和成员函数。

于是,我们将原来的代码利用struct功能改进后如下:

注意,这里的fixed4 _Color;这条语句一定要定义在它用到的函数之前,否会报错。

虽然相较之前的版本会显示的代码多了不少,但是这样做更加灵活方便。

Shader "Unlit/NewUnlitShader"
{
	Properties
	{
	   _Color("Color", Color) = (1,1,1,1)
	}
		SubShader
	{

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag   

			fixed4 _Color;

	        //POSITION语义是用于顶点着色器,用来指定模型的顶点位置,是在变换前的顶点的本地空间坐标。
            struct appdata
            {
                float4 vertex : POSITION;//模型顶点位置坐标            
            };

			//SV_POSITION语义则用于像素着色器,用来标识经过顶点着色器变换之后的顶点坐标。
            struct v2f
            {                           
                float4 pos : SV_POSITION;		
            }; 

			//这是顶点着色器,顶点着色器是属于几何阶段
			//appdata v 模型的顶点位置信息
            v2f vert (appdata v)
            {
                v2f o;
				//矩阵转换把模型顶点坐标从本地转换到齐次裁剪坐标中,说白了就是将模型显示在二维显示器上时需要做的一些矩阵转换
                o.pos = UnityObjectToClipPos(v.vertex);//                 
                return o;//输出转换后的顶点坐标(SV_POSITION)到片段着色器中
            }
			//这是片段着色器,片段着色器是属于光栅化阶段
			//SV_Target,是系统值,表示该函数返回的是用于下一个阶段输出的颜色值,也就是我们最终输出到显示器上的值。
            fixed4 frag () : SV_Target
            {                                   
               //直接返回我们在材质面板中定义的颜色,给所有像素都返回了一个颜色值_Color
				return  _Color;		
            }
            ENDCG
        }
    }
}

效果如下:

4.1 、结构体中使用到多个变量

  例如最后输出模型的UV做为颜色信息来显示:

Shader "Unlit/NewUnlitShader"
{
	Properties
	{
	   _Color("Color", Color) = (1,1,1,1)
	}
		SubShader
	{

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag   

			fixed4 _Color;

	        //POSITION语义是用于顶点着色器,用来指定模型的顶点位置,是在变换前的顶点的本地空间坐标。
            struct appdata
            {
                float4 vertex : POSITION;//模型顶点位置坐标
                float2 uv : TEXCOORD0;//添加了UV变量
            };

			//SV_POSITION语义则用于像素着色器,用来标识经过顶点着色器变换之后的顶点坐标。
            struct v2f
            {                           
                float4 pos : SV_POSITION;
			    float2 uv : TEXCOORD0; //添加了UV变量
            }; 

			//这是顶点着色器,顶点着色器是属于几何阶段
			//appdata v 模型的顶点位置信息
            v2f vert (appdata v)
            {
                v2f o;
				//矩阵转换把模型顶点坐标从本地转换到齐次裁剪坐标中,说白了就是将模型显示在二维显示器上时需要做的一些矩阵转换
                o.pos = UnityObjectToClipPos(v.vertex);//
                o.uv = v.uv;   //添加了UV变量          
                return o;//输出转换后的顶点坐标(SV_POSITION)和模型的UV到片段着色器中
            }
			//这是片段着色器,片段着色器是属于光栅化阶段
			//SV_Target,是系统值,表示该函数返回的是用于下一个阶段输出的颜色值,也就是我们最终输出到显示器上的值。
            //v2f i  顶点着色器返回的模型顶点坐标与模型UV
			fixed4 frag (v2f i) : SV_Target
            {                                   
               //直接返回我们在材质面板中定义的颜色,给所有像素都返回了一个颜色值_Color
				//return  _Color;
			  return fixed4(i.uv,0,1);//传过来的模型的UV,作为颜色信息显示
            }
            ENDCG
        }
    }
}

这里属性中的_Color没有使用到,先不管它。

自定义函数

有的时候我们可能需要在顶点着色器或者片断着色器中写大量的代码,这样就会使得代码不够整洁,这个时候我们就可以使用自定义函数的方式,将部分代码整合进去,使其看起来直观易懂。

比如在上面的基础上,我们想实现一个利用模型自身UV来产生棋盘格的效果(不用贴图,而是程序生成的方式生成棋盘格),如下:

关键代码如下:

Shader "Unlit/NewUnlitShader"
{
	Properties
	{
	   _Color("Color", Color) = (1,1,1,1)
	}
		SubShader
	{

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag   

			fixed4 _Color;

	        //POSITION语义是用于顶点着色器,用来指定模型的顶点位置,是在变换前的顶点的本地空间坐标。
            struct appdata
            {
                float4 vertex : POSITION;//模型顶点位置坐标
                float2 uv : TEXCOORD0;//添加了UV变量
            };

			//SV_POSITION语义则用于像素着色器,用来标识经过顶点着色器变换之后的顶点坐标。
            struct v2f
            {                           
                float4 pos : SV_POSITION;
			    float2 uv : TEXCOORD0; //添加了UV变量
            }; 

			//这是顶点着色器,顶点着色器是属于几何阶段
             //vert方法是自动调用的,类似Unity的Update
			//appdata v 模型的顶点位置信息
            v2f vert (appdata v)
            {
                v2f o;
				//矩阵转换把模型顶点坐标从本地转换到齐次裁剪坐标中,说白了就是将模型显示在二维显示器上时需要做的一些矩阵转换
                o.pos = UnityObjectToClipPos(v.vertex);//
                o.uv = v.uv;   //添加了UV变量          
                return o;//输出转换后的顶点坐标(SV_POSITION)和模型的UV到片段着色器中
            }
		
			//checker是我们自己定义的函数
			//利用模型自身UV来生成棋盘效果的方法
            //checker返回的就是一个浮点数,所以你用float或者fixed都可以,
            //不过你得把checker函数放在frag函数的上面,要不然会报错,原理是要先定义才能调用。
			fixed checker(float2 uv) 
			{
                //UV取值范围是0-1,repeatUV取值范围是0-10,是float类型
				float2 repeatUV = uv*10;
               // floor(repeatUV)取值结果都变为整数类型
                //floor(repeatUV)/2 ,小数点后是0或者是0.5两种情况             
				float2 c = floor(repeatUV)/2;//floor 对输入参数向下取整
                 //c.x+c.y的结果的小数点部分可能是0或0.5,
                  //frac取得是小数部分,即取得是0.5或者是0
                 //返回的checker是0.5*2=1或者是0*2=0,即checker等于1或者0
				float checker = frac(c.x+c.y)*2;//它返回标量或每个矢量中各分量的小数部分
				return checker;
			}					
			//这是片段着色器,片段着色器是属于光栅化阶段
			//SV_Target,是系统值,表示该函数返回的是用于下一个阶段输出的颜色值,也就是我们最终输出到显示器上的值。
            //v2f i  顶点着色器返回的模型顶点坐标与模型UV
			fixed4 frag (v2f i) : SV_Target
            {                                                				
				fixed col = checker(i.uv);//调用函数得到返回值
			    return col;
              //col应该是一维的?但却返回了fixed4,这是为什么呢?
              //因为shader会以这个数值构造一个fixed4类型的返回值,每个分量的值都一样。
              //即返回的可能是(0,0,0,0)代表黑色,(1,1,1,1)代表白色
            }
            ENDCG
        }
    }
}

checker是我们自己定义的函数,内部就是利用模型自身UV来生成棋盘效果的方法,返回类型为fixed,然后在frag函数中,我们直接调用得到返回值,并最终输出即可。

注意,这里的checker函数同样也要在其用到之前进行声明,和上面的变量声明是一样的,这一点与脚本程序代码是有所不同的,ShaderLab中要先声明再调用。

 补充: 片段着色器中的纹理采样

(贴纹理的过程)例如下面给Cube贴纹理

如果纹理贴图大小跟Cube显示区域不匹配怎么办?

1. 纹理跟显示区域相等   所有顶点一 一映射

2. 纹理大于显示区域

第(1)种原则:因为不能一对一的映射 ,所以采用等比例映射的原则,可以把图片划分成二维坐标系(UV坐标)

第(2)种原则:点击图片,看图片的Filter Mode 属性(同样是UV坐标,单位长度相等)

3. 纹理小于显示区域

如果纹理大小小于显示区域出现马赛克或锯齿对应的解决方案可以把纹理的Filter Mode属性,选择Bilinear或Trilinear,可以减少锯齿。

相关知识借鉴:千锋视频资源库-Java视频教程海量下载-千锋教育

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐