Unity ShaderLab 顶点-片段着色器基础

一、创建Shader

Shader创建方式:

Assets->Create->Shader->Unlit Shader,即可创建出一个顶点-片段着色器文件。

Unity中Shader的两种用途:

(1)指定给材质,用于物体渲染。

(2)指定给脚本,用于图像处理。

二、编写Shader方式

Unity渲染管道中的Shader编写:

(1)顶点-片段着色器(Vertex and Fragment Shader)

(2)表面着色器(Surface Shader)

(3)固定函数着色器(Fixed Function Shader)

三、CG语言基础

1.编译指令

CG程序片段通过指令嵌入到Pass中,夹在指令CGPROGRAM和ENDCG之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
Pass
{
//...设置渲染状态..

CGPROGRAM
//编译指令
#pragma vertex vert
#pragma fragment frag

//CG代码

ENDCG
}

在CG程序片段之前,通常需要先使用#pragma声明编译指令。

编译指令 作用
#pragma vertex name 定义顶点着色器的名称,通常会使用vert
#pragma fragment name 定义片段着色器的名称,通常会使用frag
#pragma target name 定义Shader要编译的目标级别,默认2.5

编译目标等级:

编写完Shader程序之后,其中CG代码可以被编译到不同的Shader Models中,为了能够使用更高级的GPU功能,需要对应使用更高等级的编译目标。

但并不是编译目标登记越高越好,高等级的编译目标可能会导致Shader无法在旧的GPU上运行。

2.着色器函数

2.1 无返回值的函数

语法结构:

1
2
3
4
void name(in 参数,out 参数) 
{
//函数体
}

in:输入参数,语法为:in+数据类型+名称,一个函数可以有多个输入参数

out:输出翻书,语法为:out+数据类型+名称,一个函数可以有多个输出参数

2.2 有返回值的函数

语法结构:

1
2
3
4
5
type name
{
//函数体
return 返回值;
}

2.3 顶点函数和片段函数中支持的数据类型

数据类型 描述
fixed,fixed2,fixed3,fixed4 低精度浮点值,使用11位精度进行存储,数值区间为[-2.0,2.0],用于存储颜色、标准化后的向量
half,half2,half3,half4 中精度浮点值,使用16位精度进行存储,数值区间为[-60000,60000]
float,float2,float3,float4 高精度浮点值,使用32位精度进行存储,用于存储顶点坐标、为标准化的向量、纹理坐标等
struct 结构体

3. 语义

3.1 顶点着色器的输入语义

在顶点着色器中,顶点数据是以输入参数的方式传递给顶点函数,每一个输入的参数都需要填充一个语义,用于表示所传递的链接。

语义 描述
POSITION 顶点的坐标信息,通常为float3或float4类型
NORMAL 顶点的法向信息,通常为float3类型
TEXCOORD0 模型的第一套UV坐标,通常为float2、float3或float4类型,TEXCOORD0到TEXCOORD3分别对应为第一到第四套UV坐标
TANGENT 顶点的切向量,通常为float4类型
COLOR 顶点的颜色信息,通常为float4类型

3.2 顶点着色器输出和片段着色器输入语义

顶点着色器需要输出顶点在裁切空间中的坐标。

在顶点函数中,这个输出参数需要使用float4类型的SV_POSITION语义进行填充。

顶点着色器的输出即为片段着色器的输入。

语义 描述
SV_POSITION 顶点在裁切空间中的坐标,float4类型
TEXCOORD0、TEXCOORD1等 用于声明任意高精度的数据,例如纹理坐标、向量等
COLOR0、COlOR1等 用于声明任意低精度的数据,例如顶点颜色、数值区间[0,1]的变量

片段着色器会自动获取顶点着色器输出的裁切空间顶点坐标,所以片段函数输入的SV_POSITION可以省略。

3.3 片段着色器输出语义

片段着色器通常只会输出一个fixed4类型的颜色信息,输出的值会存储到渲染目标(Render Target)中,输出参数使用SV_TARGET语义进行填充。

4.在CG中调用属性变量

4.1 CG中声明属性变量

在Shader程序中访问Properties代码块声明开放出来的属性,需要在CG代码块中再次声明,语法为:

1
type name;

type为变量的类型。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Shader "Custom/Simple Shader"
{
Properties
{
_MyFloat("Float Property",Float)=1 //浮点类型
_MyRange("Range Property",Range(0,1))=0.1 //范围类型
_MyColor("Color Property",Color)=(1,1,1,1) //颜色类型
_MyVector("Vector Property",Vector)=(0,0,0,0) //向量类型
_MyTex("Texture Property",2D)="white"{} //2D贴图类型
_MyCube("Cube Property",Cube)=""{} //立方体贴图类型
_My3D("3D Property",3D)=""{} //3D贴图类型
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

//在CG中声明属性变量
float _MyFloat; //浮点类型
float _MyRange; //范围类型
fixed4 _MyColor; //颜色类型
float4 _MyVector; //向量类型
sampler2D _MyTex; //2D贴图类型
samplerCUBE _MyCube; //立方体贴图类型
sampler3D _My3D; //3D贴图类型

void vert()
{

}

void frag()
{

}

ENDCG
}
}
Fallback "Diffuse"
}

开放属性与CG属性变量的对应关系:

开放属性的类型 CG中属性变量的类型
Float,Range 浮点和范围类型的属性。可根据精度使用float,half,fixed声明
Color,Vector 颜色和向量类的属性,可以使用float4,half4或fixed4声明
2D 2D纹理贴图属性,使用sampler2D声明
Cube 立方体纹理贴图属性,使用samplerCube声明
3D 3D纹理贴图属性,使用sampler3D声明

4.2 在Shader中使用颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Shader "Custom/Color Property"
{
Properties
{
//开放颜色属性_MainColor
_MainColor("MainColor",Color)=(1,1,1,1)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

//声明属性变量
fixed4 _MainColor;

void vert(in float4 vertex : POSITION,out float4 position : SV_POSITION)
{
position=UnityObjectToClipPos(vertex);
}

void frag(out fixed4 color : SV_Target)
{
color=_MainColor;
}
ENDCG
}
}
}

4.3 在Shader中使用贴图

与其他属性不同的是,纹理贴图需要在CG中额外声明一个变量用于存储贴图的其他信息,例如平铺(Tiling)和偏移(Offset)属性。

在CG中,声明一个纹理变量的Tiling和Offset的语法结构:

1
float4 {TexutreName}_ST

(1)TextureName:纹理属性的名称。

(2)ST:Scale和Transform的首字母,表示UV的缩放和平移。

该float4类型中的x和y分量分别代表Tiling的x和y值,z和w分量分别代表Offset的x和y值。

纹理坐标的计算公式:

texcoord=uv{TextureName}.xy+{TextureName}.zwtexcoord=uv \cdot \left \{ TextureName \right \}.xy+ \left \{ TextureName \right \}.zw

纹理坐标计算时,一定是先放大,在平移,即先乘以平铺直再加偏移值。

修改上面的Shader代码,增加其对纹理资源的支持:

·Properties语句块掳爱中开放名称为_MainTex的纹理属性,默认值为白色。

1
_MainTex("MainTex",2D)="white"{}

·在CG中重新声明_MainTex属性变量以及它的平铺偏移变量_MainTex_ST

1
2
sampler2D _MainTex;
float4 _MainTex_ST;

·在顶点函数中,添加一个名称为uv的输入参数用于获取顶点的UV数据之后,有添加了一个名称为texcoord的输出参数用于保存顶点函数中级算出来的纹理坐标。

1
2
3
4
5
6
7
void vert(in float4 vertex : POSITION,in float2 uv : TEXCOORD0,out float4 position : SV_POSITION,out float2 texcoord : TEXCOORD0)
{
position=UnityObjectToClipPos(vertex);

//使用公式计算纹理坐标
texcoord = uv * _MainTex_ST.xy + _MainTex_ST.zw;
}

·在片段着色器中添加一个名称为texcoord的输入参数用于接收从顶点着色器传递过来的纹理变量。然后调用tex2D()函数,使用纹理坐标texcoord对纹理_MainTex进行采用,最后将采样结果乘上_MainColor进行输出。

1
2
3
4
void frag(in float4 position : SV_POSITION,in float2 texcoord : TEXCOORD0,out fixed4 color : SV_Target)
{
color = tex2D(_MainTex,texcoord) * _MainColor;
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Shader "Custom/Color Property"
{
Properties
{
//开放颜色属性_MainColor
_MainColor("MainColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}

}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

//声明属性变量
fixed4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;

void vert(in float4 vertex : POSITION,in float2 uv : TEXCOORD0,out float4 position : SV_POSITION,out float2 texcoord : TEXCOORD0)
{
position=UnityObjectToClipPos(vertex);

//使用公式计算纹理坐标
texcoord = uv * _MainTex_ST.xy + _MainTex_ST.zw;
}

void frag(in float4 position : SV_POSITION,in float2 texcoord : TEXCOORD0,out fixed4 color : SV_Target)
{
color = tex2D(_MainTex,texcoord) * _MainColor;
}
ENDCG
}
}
}

4.4 在Shader中使用立方体贴图

立方体贴图的采样函数为:texCUBE(Cube,r)

·Cube:表示立方体的贴图

·r:表示视线方向在物体表面上的反射方向

物体表面的反射向量

(1)公式1:r=2avr=2a-v

(2)公式2:a=v+ba=v+b

(3)公式3:b=bn=vcosαn=cosαnb = \left| b \right| \cdot n = \left| v \right|cos \alpha \cdot n =cos \alpha \cdot n

(4)由于vv是标准化向量,长度为1,在乘法中可以省略。

α\alpha计算:(v)n=vncosα=cosα(-v) \cdot n = \left| v \right| \cdot \left| n \right| \cdot cos \alpha = cos \alpha

​ 因为v-vnn 都是标准化向量,长度为1,则可以得到:

​ ·b=[(v)n]nb=[(-v) \cdot n]n

​ ·a=v+[(v)n]na=v+[(-v) \cdot n]n

​ ·r=2v+[(v)n]nv=2[(v)n]n+vr=2{v+[(-v) \cdot n]n}-v=2[(-v) \cdot n]n+v

修改代码使Shader添加对于立方体贴图的支持:

添加开放属性(Properties)。

1
2
3
//添加Cubemap属性和反射强度
_Cubemap("Cubemap",Cube)=""{}
_Reflection("Reflection",Range(0,1))=0

在CG中再次声明属性变量。

1
2
3
//声明Cubemap和反射属性变量
samplerCUBE _Cubemap;
fixed _Reflection;

在顶点函数的输入参数中,添加了模型的法线向量normal后,输出参数中添加了世界空间顶点坐标worldPos和世界空间法线向量worldNormal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vert(in float4 vertex : POSITION,in float2 uv : TEXCOORD0,in float3 normal : NORMAL,
out float4 position : SV_POSITION,out float4 worldPos : TEXCOORD0,
out float3 worldNormal : TEXCOORD1,out float2 texcoord : TEXCOORD2)
{
position=UnityObjectToClipPos(vertex);

//将顶点坐标变换到世界空间
worldPos=mul(unity_ObjectToWorld,vertex);

//将法线向量变换到世界空间
worldNormal=mul(normal,(fixed3x3)unity_WorldToObject);
worldNormal=normalize(worldNormal);

//使用公式计算纹理坐标
texcoord = uv * _MainTex_ST.xy + _MainTex_ST.zw;
}

将世界空间顶点坐标worldPos和世界空间法向worldNormal传入到片段函数之后,使用worldPos,xyz减去摄像机坐标得到摄像机指向顶点的方向向量viewDir并规范化。然后套用反射向量的公式计算反射向量refDir,使用texCUBE()函数对其进行采样,得到反射颜色reflection。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void frag(in float4 position : SV_POSITION,in float4 worldPos : TEXCOORD0,in float3 worldNormal,
in float2 texcoord : TEXCOORD1,out fixed4 color : SV_Target)
{
fixed4 main=tex2D(_MainTex,texcoord)*_MainColor;

//计算世界空间中摄像机指向顶点的方向向量
float3 viewDir=worldPos.xyz-_WorldSpaceCameraPos;
viewDir=normalize(viewDir);

//套用公式计算反射向量
float3 refDir=2*dot(-viewDir,worldNormal);
refDir=normalize(refDir);

//对Cubemap采样
fixed4 reflection=texCUBE(_Cubemap,refDir);

//使用_Reflection对颜色和反射进行线性插值计算
color=lerp(main,reflection,_Reflection);
}

最后完整的Shader代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Shader "Custom/Cubemap Property"
{
Properties
{
//开放颜色属性_MainColor
_MainColor("MainColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}

//添加Cubemap属性和反射强度
_Cubemap("Cubemap",Cube)=""{}
_Reflection("Reflection",Range(0,1))=0
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

//声明属性变量
fixed4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;

//声明Cubemap和反射属性变量
samplerCUBE _Cubemap;
fixed _Reflection;

void vert(in float4 vertex : POSITION,in float2 uv : TEXCOORD0,in float3 normal : NORMAL,
out float4 position : SV_POSITION,out float4 worldPos : TEXCOORD0,
out float3 worldNormal : TEXCOORD1,out float2 texcoord : TEXCOORD2)
{
position=UnityObjectToClipPos(vertex);

//将顶点坐标变换到世界空间
worldPos=mul(unity_ObjectToWorld,vertex);

//将法线向量变换到世界空间
worldNormal=mul(normal,(fixed3x3)unity_WorldToObject);
worldNormal=normalize(worldNormal);

//使用公式计算纹理坐标
texcoord = uv * _MainTex_ST.xy + _MainTex_ST.zw;
}

void frag(in float4 position : SV_POSITION,in float4 worldPos : TEXCOORD0,in float3 worldNormal,
in float2 texcoord : TEXCOORD1,out fixed4 color : SV_Target)
{
fixed4 main=tex2D(_MainTex,texcoord)*_MainColor;

//计算世界空间中摄像机指向顶点的方向向量
float3 viewDir=worldPos.xyz-_WorldSpaceCameraPos;
viewDir=normalize(viewDir);

//套用公式计算反射向量
float3 refDir=2*dot(-viewDir,worldNormal);
refDir=normalize(refDir);

//对Cubemap采样
fixed4 reflection=texCUBE(_Cubemap,refDir);

//使用_Reflection对颜色和反射进行线性插值计算
color=lerp(main,reflection,_Reflection);
}
ENDCG
}
}
}

效果图

参数

4.5 结构体

在编写过程中,着色去需要输入输出多个参数,要简化代码编写,可以使用结构体。

4.5.1 结构体语法
1
2
3
4
5
6
7
8
struct Type
{
//变量_1;
//变量_2;
//变量_3;
//...
//变量_n;
}

(1)struct:定义结构体的关键字。

(2)Type:给当前结构体定义一个类型。

将Color Property改写成结构体形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Shader "Custom/In Out Struct"
{
Properties
{
_MainColor("MainColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}

}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

//定义顶点着色器的输入结构体
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

//定义顶点着色器的输出结构体
struct v2f
{
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};

fixed4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;

//使用结构体传入传出参数
void vert(in appdata v,out v2f o)
{
o.position=UnityObjectToClipPos(v.vertex);

o.texcoord = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
}

void frag(in v2f i,out fixed4 color : SV_Target)
{
color = tex2D(_MainTex,i.texcoord) * _MainColor;
}
ENDCG
}
}
}
4.5.2返回结构体的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Shader "Custom/Return Struct"
{
Properties
{
_MainColor("MainColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}

}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};

fixed4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;

v2f vert(appdata v)
{
v2f o;
o.position=UnityObjectToClipPos(v.vertex);
o.texcoord = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;

//返回结构体
return o;
}

fixed4 frag(v2f i) : SV_Target
{
return tex2D(_MainTex,i.texcoord) * _MainColor;
}
ENDCG
}
}
}