Shader(着色器)实际上就是一段小程序,负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。

绘图单元依据这个将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的Shader,以及Shader的特定参数设置,将这些内容打包存储,得到一个Material之后,赋予renderer来进行渲染。

Shader分为两种:

表面着色器(Suface Shader)已经做了大部分工作,只需要简单的技巧即可实现不错的效果。

片段着色器(Fragment Shader)比较难写,使用片段着色器的主要目的是可以在较低的层级上进行更复杂的开发。

Shader程序的基本结构

unity表面着色器 unity3d着色器_unity表面着色器


首先属性定义

Properties{
}

接下来是一个或多个子着色器,在实际运行中哪一个子着色器被使用是由运行的平台所决定的。子着色器是代码的主体,每一个子着色器中包含一个或多个Pass

在计算着色时,平台先选择可以使用的着色器,然后依次运行其中的Pass,然后得到输出结果,最后指定一个回滚,用来处理所有的Subshader都不能运行的情况

(比如在目标设备实在太老,不支持所有的Subshader特性)


新建一个shader命名为Diffuse Texture

Shader "Custom/Diffuse Texture" {
       //----------------------属性
         Properties {
		_Color ("Color", Color) = (1,1,1,1) 
		_MainTex ("Albedo (RGB)", 2D) = "white" {} //纹理
		_Glossiness ("Smoothness", Range(0,1)) = 0.5 
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}
	//----------------开始一个子着色器
        SubShader {
               //渲染类型Opaque,不透明
                Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		// Physically based Standard lighting model, and enable shadows on all light types
		#pragma surface surf Standard fullforwardshadows

		// Use shader model 3.0 target, to get nicer looking lighting
		#pragma target 3.0
                
                //变量声明
		sampler2D _MainTex;//主纹理

		struct Input {
			float2 uv_MainTex;//纹理贴图
		};
                //变量声明
		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		void surf (Input IN, inout SurfaceOutputStandard o) {
			// Albedo comes from a texture tinted by color
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                        //表面反射颜色为纹理颜色
                        o.Albedo = c.rgb;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

Properties


在Properties{}中定义着色器属性,每一条属性的定义的语法是这样的:


_Name("Display Name",type) = defaultValue[{options}]

_Name 属性名字,就是变量名,在之后整个shader代码中将使用这个名字获取该属性的内容

Display Name 这个字符串将显示在unity的材质编辑器中作为Shader的使用者可读的内容


type 这个属性的类型,可能的type所表示的内容有以下几种{


 Color 一种颜色,由RGBA四个量定义;

2D 一张2的阶数大小(256,512)之类的贴图,这张贴图将在采样后转为对应基于模型UV的每个像素的颜色,最终被显示出来

Rect 一个非2阶数大小的贴图

Cube 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图组合,主要是用来做反射效果的(比如天空盒盒动态反射),也会被转换为对应点的采样

Range(min,max)一个介于最小值和最大值之间的浮点数,一般用来当做调整shader某些特性的参数(比如透明度渲染)

float

vector 一个四维数

defaultValue定义这个属性的默认值,通过输入一个符合格式的默认值来指定对应属性的初始值(某些效果可能需要某些特定的参数值来表达到需要的效果,虽然这些值可以在之后进行调整,但是如果默认就指定为想要的话就省去调整时间)

Color 以0~1定义的RGBA颜色(1,1,1,1);

2D/Rect/Cube 对于贴图来说,默认值可以为一个代表认tint颜色的字符串,可以是空字符串或者white,black,gray,bump中的一个

float,Range 某个指定的浮点数

Vector 一个4维数,写为(x,y,z,w)

另有一个{option},只对2D,Rect或者Cube贴图相关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时,可以把其写在这对花括号里。如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear,EyeLinear,SphereMap,CubeReflect,CubeNormal中的一个,这些都是OpenGL中TexGen的模式

}

所以一组属性的声明:

Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}

unity表面着色器 unity3d着色器_浮点数_02



Tags

表面着色器可以被若干的标签(tags)所修饰,而硬件通过判定这些标签来决定什么时候调用该着色器。


例如:

Tags{"RenderType"="Opaque"}

告诉了系统应该在渲染非透明物体时调用,unity定义了一些列这样的渲染过程,与RenderType是Opaque相对应的显而易见的是“RenderType” = “Transparant”,表示渲染含有透明效果的物体时调用。在这里Tags其实暗示了你的Shader输出是什么,如果输出中都是非透明物体,那就写在Opaque里;如果想渲染透明或者半透明的像素,那应该写在Transparent中。

另外“IgnoreProjector”="True"(不被Projectors影响),“ForceNoShadowCating”=“True”(从不产生阴影),“Queue”= “xxx”(指定渲染队列)。

这里说一下Queue标签,如果你使用Unity做过一些透明和不透明物体的混合的话,很可能遇到过不透明物体无法呈现在透明物体之后的情况。这种情况很可能是由Shader的渲染序列顺序不正确导致的。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

LOD很简单,它是Level of Detail的缩写,这里我们指定其为200(这是unity内建Diffuse着色器的设定值)。这个数值决定了我们用什么样的Shader。在unity的Quality Settings 中我们可以设定最大LOD,当设定的LOD小于subShader所指定的LOD时,这个subShader将不可用。unity内建Shader定义了一组LOD的数值,我们在实现自己的Shader的时候可以将其作为参考来设定自己的LOD数值,我们在实现自己的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


Shader本体

前面杂项说完,我们现在来看看主体部分,也就是输入转输出的代码部分。


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

我们逐行来看,首先是CGPROGRAM。这是一个开始标记,表明从这里开始是一段CG程序(我们在写Unity的Shader时勇士的Cg/HLSL语言)

最后一行的ENDCG与CGPROGRAM是对应的,表示CG程序到此结束。

接下来是一个编译指令:#pragma suface surf Lambert,它声明了我们要写一个表面Shader,并指定了光照模型。

它的写法是这样的

#pragma surface surfaceFunction lightModel [optionalparams]

surface - 声明一个表明着色器

surfaceFunction - 着色器代码的方法的名字

lightModel - 使用光照模型

所以在我们的例子中,我们声明了一个表明着色器,实际的代码在surf函数中(下面的代码能找到对应的函数),使用Lambert(也就是普通的diffuse)作为光照模型

接下来一句 sampler2D _MainTex ;

sampler2D在CG中,sampler2D就是和texture所绑定的一个数据容器接口。简单理解,所谓加载以后的texture(贴图)说白了不过是一块内存存储的,使用了RGB(也许还有A)通道,且每个通道8bits的数据。而具体地想知道像素与坐标的对应关系,以及获取这些数据,我们总不能一次一次去自己计算内存地址或者偏移,因此可以通过sample2D来对贴图进行操作。更简单地理解,sampler2D就是GLSL中的2D贴图类型,相应的,还有sampler1D,sampler3D,samplerCube等等格式。

解释完sampler2D是什么之后,还需要解释下为什么在这里需要一句对_MainTex的声明,之前我们不是已经在Properties里声明过它是贴图了么。答案是我们用来实例的这个shader其实是两个相对独立的块组成的,外层的属性声明,回滚等等是Unity可以直接使用和编译的ShaderLab;而现在我们是在CGPROGRAM....ENDCG这样一个代码模块中,这是一个CG程序。对于这段CG程序,要想访问在Properties中所定义的变量的话,必须使用和之前变量相同的名字进行声明。于是,其实sampler2D _MainTex; 做的事情就是再次声明链接了 _MainTex,

使得接下来的CG程序能使用这个变量。


接下来是struct结构体,先跳过,直接看surf函数。

上面#pragma段已经指出了我们的着色器代码的方法的名字叫做surf,就是这段代码是我们着色器的核心部分。

着色器就是给定输入,然后给出输出进行着色代码。CG规定了声明为表明着色器的方法(就是我们这里的surf)的类型和名字,因此我们没有权利决定surf的输入输出参数的类型,只能按照规定写。

这个规定就是第一个参数是一个Input结构,第二个参数是一个inout的SurfaceOutput结构。

Input其实需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个Input结构中,传入surf函数使用;SurfaceOutput是已经定义好了里面类型输出结构,但是一开始的时候内容暂时是空白的,我们需要向里面填写输出,这样就可以完成着色了。

一下为Input结构体


struct Input{

float2 uv_MainTex;
};

作为输入的结构体必须命名为Input,这个结构体定义了一个float2的变量,float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数。例如下面:


//Define a 2d vector variable
vec2 coordinate;
//Define a color variable
float4 color;
//Multiply out a color
float3 multipliedColor = color.rgb * coordinate.x;

在访问这些值时,我们既可以只是用名称来获取整组值,也可以使用下标的方式(比如.xyw .rgba或他们的部分 .x)来获取某个值。在这个例子里,我们声明了一个叫做uv_MainTex的包含两个浮点数的变量。

如果你对3D开发稍有耳闻的话,一定不会对uv陌生。UV mapping的作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。在CG程序中,我们这样约定,在一个贴图变量(例如_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标)。我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。

如果你看到这里,恭喜你,距离读完一个shader只有一步之遥了

我们回到surf函数,它的两个参数,第一个是Input,我们已经明白:在计算输出是shader会多次调用surf函数,每次给入一个贴图上的点坐标,来计算输出。第二个参数是一个可写的SurfaceOutput,SurfaceOutput是预定义的输出结构,我们的surf函数的目标就是根据输入把这个输出结构填上。

SurfaceOutput结构体的定义如下


struct SurfaceOutput {
    half3 Albedo;     //像素的颜色
    half3 Normal;     //像素的法向值
    half3 Emission;   //像素的发散颜色
    half Specular;    //像素的镜面高光
    half Gloss;       //像素的发光强度
    half Alpha;       //像素的透明度
};


这里的half和我们常见的float,double类似,都表示浮点数,只不过精度不一样。float为单精度浮点数,double为双精度浮点数,half为半精度浮点数,精度最低,运算性能相对高精度浮点数高一些,因此被大量使用

在surf函数中:


half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;

这里用到一个tex2d函数,这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4,这里对_MainTex在输入点上进行了采样,并将其颜色的RGB值赋予了输出的像素颜色,将A值赋予了透明度。于是,着色器就明白了应该如何工作:既找到贴图上对应的uv点,直接使用颜色信息来进行着色。


以后的学习就是参考Unity的Surface Shader Examples多接触基本Shader。然后配合Google进行下一步学习。


补充一下,shader有三种类型


1.固定功能着色器(Fixed Function Shader)

 

2.表面着色器(Surface Shader)

 

3.顶点着色器&片段着色器 (Vertex Shader & Fragment Shader)



关于固定功能着色器



这里的固定功能着色器可以说是Unity为Shader的书写自带的一层壳,Unity已经在内部为我们做了大量的工作,我们只要稍微记住一些关键字、一些规范就可以实现出很多不错的效果。固定功能着色器是我们初学Unity Shader的最近几篇文章中的主要学习对象。而后面的表面着色器、顶点着色器以及片段着色器就是在固定功能着色器的基础上嵌套了CG语言的代码而成的更加复杂的着色器。我们来看看他们的一些基本概念。

 

 


 关于表面着色器



表面着色器(Surface Shader)这个概念更多的只是在Unity中听说,可以说是Unity自己发扬光大的一项使Shader的书写门槛降低和更易用的技术。我们会在接下来的学习中逐渐意识到Unity是如何为我们把Shader的复杂性包装起来,使其书写的过程更便捷和易用的。


 



关于顶点着色器和片段着色器

研究过Direct3D和OpenGL着色器编程的童鞋们一定对这两者不陌生。我们来简单介绍一下他们的用途。

顶点着色器:产生纹理坐标,颜色,点大小,雾坐标,然后把它们传递给裁剪阶段。

片段着色器:进行纹理查找,决定什么时候执行纹理查找,是否进行纹理查找,及把什么作为纹理坐标




如何区分Unity中的Shader类型



在Unity中想要区分他们很简单。后面熟悉了自然知道。在这里浅墨先剧透一下:


  • 没有嵌套CG语言,也就是代码段中没有CGPROGARAM和ENDCG关键字的,就是固定功能着色器。
  • 嵌套了CG语言,代码段中有surf函数的,就是表面着色器。
  • 嵌套了CG语言,代码段中有#pragma vertex name和  #pragma fragment frag声明的,就是顶点着色器&片段着色器。