在unity中我们经常会使用shader,但是从来没有深究过,最近在做项目时遇到相关问题,无从下手,决定系统学习一番,在此前提下把我学习的过程做一个记录。学习过程中参考了浅墨和风宇冲两位高人的博客,本文有不对的地方,还望指正。

按照学习技术的习惯,在第一次接触shader时,我们首先会想这两个个问题,

问题一:什么是shader,它能做什么?

问题二:unity中shader使用的语言是什么?



问题一:shader,也就是着色器,它本质就是一段程序,这段程序的作用是把Mesh(网格)以指定的方式以及指向的贴图或颜色等处理后,然后通过绘制把处理后的图像显示在屏幕上。所以简而言之:

什么是shader,shader就是一段处理颜色,贴图纹理并且进行计算和变换到渲染器的程序。

unity中shader分两类:Surface Shader(表面着色器)和Vertex Shader &Fragment Shader(顶点着色器&片段着色器)以及Fixed Function Shader(固定功能着色器)下面会讲如何在区分。

shader能做什么呢,比如光影特效,颜色变换等。暂时只想到这些


问题二:unity中的shader使用的是一种shaderLab的语言编写的。它的语法风格类似NVIDIA的CgFX和Direct3D,它具备了显示材质(Material)所需要的一切信息,在unity的shader代码中我们可以使用CG语言写CG程序,所谓CG语言就是C for Graphics,说到一种语言我们就介绍下的他的数据类型,CG语言有6中数据类型,分别是 

1.    float,32位浮点数据,一个符号位。浮点数据类型被所有的profile支持(但是DirectX8 pixel profiles在一些操作中降低了浮点数的精度和范围);

2.    half,16为浮点数据;

3.    int,32位整形数据,有些profile会将int类型作为float类型使用;

4.    fixed,12位定点数,被所有的fragment profiles所支持;

5.    bool,布尔数据,通常用于if和条件操作符(?:),布尔数据类型被所有的profiles支持;

6.    sampler*,纹理对象的句柄(the handle to a texture object),分为6类:sampler, sampler1D, sampler2D, sampler3D, samplerCUBE,和samplerRECT。DirectX profiles不支持samplerRECT类型,除此之外这些类型被所有的pixel profiles和    NV40 vertex program profile所支持(CgUsersManual 30页)。由此可见,在不远的未来,顶点程序也将广泛支持纹理操作;

除了上面的基本数据类型外,Cg还提供了内置的向量数据类型(built-in vector data types),内置的向量数据类型基于基础数据类型。例如:float4,表示float类型的4元向量;bool4,表示bool类型4元向量。

    注意:向量最长不能超过4元,即在Cg程序中可以声明float1、float2、float3、float4类型的数组变量,但是不能声明超过4元的向量,例如:

float5 array ;// 编译报错

向量初始化方式一般为:

float4 array = float4(1.0, 2.0, 3.0, 4.0);

较长的向量还可以通过较短的向量进行构建:

float2 a = float2(1.0, 1.0);

float4 b = float4(a, 0.0, 0.0);

此外,Cg还提供矩阵数据类型,不过最大的维数不能超过4*4阶。例如:

float1x1 matrix1; // 等价于 float matirx1; x 是字符,并不是乘号!

float2x3 matrix2; // 表示2*3 阶矩阵,包含6 个 float 类型数据

float4x2 matrix3;// 表示 4*2 阶矩阵,包含 8 个 float 类型数据

float4x4 matrix4 ;// 表示4*4 阶矩阵,这是最大的维数

矩阵的初始化方式为:

float2x3 matrix5 = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};


CG语言中也有数组数据类型,它在Cg程序中的作用是:作为函数的形参,用于大量数据的转递。

Cg中声明数组变量的方式和C语言类似:例如:

float a[10]; // 声明了一个数组,包含10 个 float 类型数据

float4 b[10]; // 声明了一个数组,包含10 个 float4 类型向量数据




注意:Cg中向量、矩阵与数组是完全不同,向量和矩阵是内置的数据类型(矩阵基于向量),而数组则是一种数据结构,不是内置数据类型!这一点和C\C++ 中不太一样,在C\C++中,这三者同属于数据结构,数组可以构建向量和矩阵。下一节中将详细阐述Cg中的数组类型。





下面我们来看一段unity的shader代码。打开unity,Assets->Create->Shader,会在Assets路径下创建了一个shader脚本,可以用文本编辑工具打开,我使用的是一个VS的shader插件,可以显示语法高亮。代码如下:

Shader "Custom/NewShader" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Lambert

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		void surf (Input IN, inout SurfaceOutput o) {
			half4 c = tex2D (_MainTex, IN.uv_MainTex);
			o.Albedo = c.rgb;
			o.Alpha = c.a;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}



我们先来看看这段代码的结构,它大体分四块Shader “Custom/NewShader” ,Properties和SubShader以及FallBack “Diffuse”。

<span style="font-family: Arial, Helvetica, sans-serif;"><span style="font-size:14px;"></span></span>

简单的可以用如下的形式来概括:

Shader "name" { [Properties] SubShaders[Fallback] }

用图来说

unity shader特效 unity的shader用法_unity shader特效


Shader “Custom/NewShader” :它定义了我们自定义的这个shader的目录结构,Custom下名字叫做NewShader。目录和它的名字我们可以随意修改 ,并且不必和文件名相同。    “/”用来构建子菜单,作用便于管理,如Custom/canglang/NewShader那么在 Inspector

unity shader特效 unity的shader用法_贴图_02

                                                                                  

Properties:unity圣典的解释是:


Shaders can define a list of parameters to be set by artists in Unity’s material inspector. The Properties block in the shader file definesthem.我简单理解为属性定义并且属性会将display name显示在材质检视器中,显示GUI元素,包括纹理,颜色,滑动条,float值等,能够很便利的让我们进行修改

语法结构为:Properties { Property [Property ...] } 

定义属性块,其中可包含多个属性,其定义如下:

name ("display name", type) =Defaultvalue

type的类型有7种:Range(min,max),Color ,2D,Rect,Cube,Float,Vector,每一种的具体含义及定义如下:

name ("display name", Range (min, max)) =number

定义浮点数属性,在检视器中可通过一个标注最大最小值的滑条来修改。

name ("display name", Color) =(number,number,number,number)

定义颜色属性

name ("display name", 2D) = "name" {options }

定义2D纹理属性

name ("display name", Rect) = "name"{ options }

定义长方形(非2次方)纹理属性

name ("display name", Cube) = "name"{ options }

定义立方贴图纹理属性

name ("display name", Float) = number

定义浮点数属性

name ("display name", Vector) =(number,number,number,number)

定义一个四元素的容器(相当于Vector4)属性


例如_MyColor ("My Color", Color) =(1,1,1,0)为例,

_MyColor ------Internal name(内部名称)

My Color-------Inspector title(Inspector窗口显示的名称)

Color-----------Property type(属性类型)

(1,1,1,0)--------Default value(默认值)

注意:

1、Properties块内的语法都是单行的。每个属性都是由内部名称开始,后面括号中是显示在检视面板(Inspector)中的名字和该属性的类型。等号后边跟的是默认值。

2、对于Range和Float类型的属性只能是单精度值。

3、对于Color和Vector类型的属性将包含4个由括号围住的数描述。

4、对于纹理(2D, Rect, Cube) 缺省值既可以是一个空字符串也可以是某个内置的缺省纹理:"white", "black", "gray" or"bump"

SubShader:子着色器,是shader代码的主体,一个shader可以包含一个或多个子着色器,每个子着色器可以包含一个或多个pass,但是在运行时具体能不能执行或者限制性哪个子着色器完全由运行平台决定。因为机子不同,运行的显卡支持环境也不同。所以多个子着色器的作用就在于此,例如对于一个效果,预想想好ABC多套方案,A在牛逼机子上能运行,B在一般机子上能运行,C在烂机子上运行。全都不行了还有FallBack呢。而且在运行时系统也会把子着色器中代码分成多个合适的pass。

具体结构如下:

SubShader

{

pass

{

}

}

FallBack “Diffuse”:回滚,也称备胎,作用是处理所有SubShader都不能运行的情况,以防万一,留个备胎,不然显示不了咋办呢。这里设的备胎是Diffuse(漫反射)。

注意:SubShader在Shader代码中必须有,而properties和fallback是可以不写的

下面来看下我们新建的NewShader中SubShader的代码



Tags { "RenderType"="Opaque" }:Tags是标签,SubShader可以写多个Tags(标签)来标定这个SubShader,当然也可以不写,硬件平台通过这些标签来决定什么时候调用这个SubShader进行渲染,在我们新建的这个shader中使用的是unity默认的"RenderType" = "Opaque",它的含义是告诉硬件平台在渲染非透明物体时调用该SubShader,与之对应的,渲染含有透明效果的物体时是"RenderType" = "Transparent",我在这列举我们经常会使用的几个标签如下:

"RenderType" = "Opaque" 渲染非透明物体

"RenderType" = "Transparent" 渲染含有透明效果的物体

"IgnoreProjector"="True" 不被Projectors影 响

"ForceNoShadowCasting"="True" 从不产生阴影

"Queue"="xxx"指定渲染顺序队列

我们要着重讲下这个Queue,Queue制定了该物体的渲染顺序,所以尤其是在透明与不透明物体混合时我们要注意,不然可能显示不出来。

预订的Queue有如下几个:

Background最先被调用的渲染,一般用来渲染天空和或者背景

Geometry默认值,用来渲染非透明物体(普通情况下,场景中绝大多数物体应该是非透明的)

AlphaTest用来熏染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑

Transparent以从后往前的顺序渲染透明物体

Overlay用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)

这 些预定义的值本质上是一组定义整数,Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最后Overlay = 4000。在我们实际设置Queue值时,不仅能使用上面的几个预定义值,我们也可以指定自己的Queue值,写成类似这 样:"Queue"="Transparent+100",表示一个在Transparent之后100的Queue上进行调用。通过调整Queue值, 我们可以确保某些物体一定在另一些物体之前或者之后渲染,这个技巧有时候很有用处。


LOD 200:LOD(Level of Detail),他的值决定我们写的Shader在unity设定的品质环境下能不能用,在Unity的Quality Settings中我们可以设定允许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用。Unity 内建Shader定义了一组LOD的数值,我们在实现自己的Shader的时候可以将其作为参考来设定自己的LOD数值,这样在之后调整根据设备图形性能 来调整画质时可以进行比较精确的控制。

  • VertexLit及其系列 = 100
  • Decal, Reflective VertexLit = 150
  • Diffuse = 200
  • Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit = 250
  • Bumped, Specular = 300
  • Bumped Specular = 400
  • Parallax = 500
  • Parallax Specular = 600


  • CGPROGRAM:标记语句,标识从这里开始是一顿CG程序(我们在写Unity的Shader时用的是Cg/HLSL语言)。最后一行的ENDCG与CGPROGRAM是对应的,表明CG程序到此结束。
  • #pragma surface surf Lambert : 这是一条编译指令,它声明了我们写的这个shader的类型,以及光照模型。他的语法如下:
  • #pragma surface surfaceFunction lightModel [optionalparams]
  • surface - 声明该Shader是一个Surface Shader(表面着色器)

surfaceFunction - shader代码方法的名字

lightModel - 使用的光照模型

所以例子中我们这句指令的意义就是:声明一个表面着色器(Surface Shader),实际的代码在surf函数,使用Lambert(普通的diffuse漫反射)为光照模型。


sampler2D _MainTex:解释sampler2D之前,我们先说下_MainTex,我们会发现在前面的Properties模块已经有了一个_MainTex的贴图属性声明。这里为什么还要声明它,原因是CGPROGRAM...ENDCG定义的代码块是一段CG程序,而shader的其他部分使用shaderLab写的,ShaderLab是unity可以直接使用和编译的,CG程序不能,所以必须使用和之前变量相同的名字再次进行声明,这样就能链接到Properties的变量,使得我们的CG代码能够使用这个变量。再来说这个sampler2D是一个2D贴图的类型,从他可以获得2d贴图的像素与坐标的对应数据


struct Input首先它是一个结构体,名字叫做Input,这个结构体名字是CG规定的,我们不能改变,它是作为下面suf函数参数需要传入进去的,但是他的内容可改,这就给了我们发挥的空间,我们可以自己把需要参与计算的数据放进这个结构体,例子中的结构体定义了定义了一个uv_MainTex的变量,类型是float2,CG中经常有float2,float3,float4的类型,它其实就是把2个或者3个或者4个float类型打包在一起。但是这个变量有些特别,我们在前面已经定义了一个_MainTex的2D贴图变量,在这我们的uv_MainTex只是比它多了一个uv_前缀,感觉这么写有什么含义似的,没错!就是有特别意义的,在CG程序中,有个很特别的做法,就是在一个贴图变量之前加上一个uv_前缀,就代表提取它的uv值,那么我们就会问什么是uv值,所谓uv值就是值贴图纹理的二维坐标,在这个例子中我们就能去取前面定义的贴图_MainTex的坐标值来参与计算了。


void surf (Input IN, inout SurfaceOutput o) :这是一个函数,也是这段CG代码的核心处理函数,和C语言的语法一样,我们说过CG语言就是C for graphics,是一种偏C的语言,所以他的含义很好理解,但是注意的是CG语言规定了这个函数参数的类型和名字,我们没法改,只能按照规定写,返回类型为空,两个参数一个是 IN类型是我们刚定义的Input结构体,一个是inout的SurfaceOutput结构体,Input我们刚刚已经解释过了,SurfaceOutput是已经定义好了内部类型的输出结构,接下来我们来解释例子中这个函数的函数体的具体做的事情,half4 c = tex2D (_MainTex, IN.uv_MainTex);tex2D函数,是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个half4(half是CG语言中的半精度类型我们可以就理解为float,精度较低,效率高),他有两个参数,参数一是一张贴图变量,参数二是该贴图变量的uv信息。o.Albedo = c.rgb; o.Alpha = c.a;这两句把取得采样信息,赋值给SurfaceOutput输出结构,这样shader就按照贴图上的uv信息,直接使用rgba信息来进行着色,于是就在屏幕中显示出来了。 


ok,通过例子中的shader代码,我们把shader的整个基础结构作了一个简单的介绍。明天我们继续....