Unity-Shader读书笔记(4)

一个最简单的顶点/片元着色器

UnityShader代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Shader "Learn/SimpleShaderr"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

float4 vert(float4 v : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(v);
}

fixed4 frag() : SV_TARGET0
{
return fixed4(1.0,1.0,1.0,1.0);
}

ENDCG
}
}
}

PS:书中是mul(UNITY_MATRIX_MVP,v),但是我的编译器2019.4.40f1自动将其变成了UnityObjectToClipPos(v)。Unity的解释:Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)

        现在我们来了解一下上述的GC代码。所有的CG代码由CGPROGRAMENDCG包裹。然后我们会遇到两行非常重要的编译指令。

1
2
#pragma vertex vert   // 指定顶点
#pragma fragment frag // 指定片元

这两行告诉Unity哪个函数包含了顶点着色器的代码,哪个包含了片元着色器的代码。更加通用的写法如下:

1
2
#pragma vertex name   // 指定顶点 name表示指定的函数名
#pragma fragment name // 指定片元 name表示指定的函数名

name不一定要是我们上面定义的vert或是frag。只要符合函数规范的都可以。下面展示的就是例子中的顶点着色器。

1
2
3
4
float4 vert(float4 v : POSITION) : SV_POSITION
{
    return UnityObjectToClipPos(v);
}

通过POSITION语义,我们指定vert函数输入v包含了顶点信息,然后使用UnityObjectToClipPos函数将模型空间中的顶点变成裁剪空间中的顶点给片元着色器。上面我们可以看到两个语义:POSITION和SV_POSITION。他们都是CG/HLSL的语义,它们也不可省略。这些语义会告诉系统用户需要哪些输入值,以及用户的输出是什么。比如POSITION就是告诉Unity把模型的顶点位置填充到输入参数v中,SV_POSITION告诉Unity顶点着色器输出的是裁剪空间下的坐标。着色器语义 - Unity 手册

        最后的片元着色器:

1
2
3
4
fixed4 frag() : SV_TARGET0
{
return fixed4(1.0,1.0,1.0,1.0);
}

在例子中frag没有任何的输入,它仅仅输出一个fixed4类型的变量,并且使用了SV_TARGET0语义进行限定(在我这个Unity版本中SV_TARGET等同于SV_TARGET0)。而片元着色器最终的输出就表示其物体显示的颜色。

更多数据的例子

        上面的例子中我们仅使用了POSITION来获取到模型的顶点位置,如果我们想要更多的模型数据呢?比如模型上每个顶点的法线方向和纹理坐标。因此我们需要为顶点着色器定义一个新的输入参数,这个参数不再是简单的数据类型而是一个结构体。例子如下:

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
Shader "Unlit/MoreDataShader"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 使用一个结构体来定义顶点着色器的输入
struct a2v
{
float4 vertex : POSITION; // 模型的顶点坐标
float3 normal : NORMAL; // 模型的法线
// 模型的纹理坐标,TEXCOORD0表示用第一套纹理坐标来填充texcoord
float4 texcoord : TEXCOORD0;
};

float4 vert(a2v v) : SV_POSITION
{
return UnityObjectToClipPos(v.vertex);
}

fixed4 frag() : SV_TARGET0
{
return fixed4(1.0,1.0,1.0,1.0);
}

ENDCG
}
}
}

这里我们定义了一个a2v的结构体,自定义的结构体我们必须使用如下的格式来定义它:

1
2
3
4
5
struct StructName{
Type Name : Semantic;
    Type Name : Semantic;
    ......
}

这里语义是不能被省略的。

顶点着色器和片元着色器之间的通信

        我们希望将顶点着色器中的一些数据传给片元着色器。比如模型的法线、纹理坐标等。所以我们要再定义一个结构体来表示片元着色器的数据。例子如下:

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
Shader "Learn/FinalSimpleShader"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 使用一个结构体来定义顶点着色器的输入
struct a2v
{
float4 vertex : POSITION; // 模型的顶点坐标
float3 normal : NORMAL; // 模型的法线
// 模型的纹理坐标,TEXCOORD0表示用第一套纹理坐标来填充texcoord
float4 texcoord : TEXCOORD0;
};

// 使用一个结构体定义顶点着色器的输出
struct v2f
{
// 告诉Unity,pos包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
// COLOR0可以用于存储颜色信息
float3 color : COLOR0;
};

v2f vert(a2v v)
{
// 声明输出
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// v.normal 包含了顶点的法线方向,其分量在[-1.0,1.0]
// 下面代码把分量范围映射到了[0.0,1.0],并存储在o.color
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o;
}

fixed4 frag(v2f i) : SV_TARGET0
{
return fixed4(i.color,1.0);
}

ENDCG
}
}
}
Ps:这时候顶点着色器函数的定义变成了v2f而不是float4,如果大家没有改过来就会报cannot convert from 'struct v2f' to 'float4' 的错误

在上面的代码中,我们声明了一个新的结构体v2f来进行顶点着色器和片元着色器间的通信。我们需要注意的是顶点着色器是逐顶点而片元着色器是逐片元的。所以片元着色器中的输入是依靠顶点着色器的输出做插值得到的。

使用属性的Shader

        在Unity Shader中,我们可以通过设置属性与材质中的数据进行联系。从而我们只需要设置Unity Shader的属性就可以更改材质的数据。现在我们有一个需求,我们希望材质所显示的颜色可以在Inspector面板上进行调试。

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
Shader "Learn/ShaderUseProperties"
{
Properties
{
// 声明一个Color类型的属性
_Color ("Color Select", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 在CG代码中我们要声明一个与属性名称和类型都匹配的变量
fixed4 _Color;

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcood : TEXCOORD0;
};

struct v2f
{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o;
};

fixed4 frag(v2f i) : SV_TARGET0
{
fixed3 rgb = i.color;
// 使用_Color属性来控制输出的颜色
rgb *= _Color.rgb;
return fixed4(rgb,1);
}

ENDCG
}
}
}

上面的代码中,我们添加了Properties语义块,并在其中声明了一个属性_Color,它的类型就是Color且初始值为(1,1,1,1)即白色。为了在CG代码块中能够使用它,我们还要再CG代码中定义一个变量,这个变量的名称类型必须和Properties语义块中的属性定义相匹配。从上面我们可以知道ShaderLab中属性的类型Color对应的CG中变量的类型是fixed4。更多的对应项可以查看官方的文档使用 Cg/HLSL 访问着色器属性 - Unity 手册

Unity提供的内置文件和变量

        我们可以查看官网中的内置着色器 include 文件 - Unity 手册信息来了解。或是直接查看Unity应用程序的CGIncludes文件夹中的信息。在Windows平台下的位置为Unity安装路径/Data/CGIncludes。