我们主要来介绍一下FSM在Unity中的实现。Unity5引入了state machine behaviors,它是Mecanim动画系统的扩展。我们可以利用这个功能来快速实现FSM系统。

FSMs的使用

虽然我们主要使用FSM来实现游戏AI,但其实FSM可以用于许多场景。状态在现实生活中也无处不在,比如水有固体、液体、气体三种状态,这是由温度这一条件决定的,编程中就会使用温度变量来决定状态的改变。 FSM的一个特征是同一时刻只能有一个状态。但此外,一个agent可以有多个FSMs,而一个状态也可以再包含状态机。

使用AnimationController构造FSM

Unity5中,状态机主要还是动画系统的一部分,所以我们会使用AnimationController来实现。我们新建Unity项目,新建一个AnimationController,并打开Animator窗口。 在Aniamtor窗口,我们可以在左上角看到有Layer和Parameters窗口。Layer代表不同的层,Parameters表示参数列表,具体含义可以查看官方手册。 State machine behaviors是Unity5引进的概念,Mecanim会实现各种状态,你不用关心如何进入、转换或退出一个状态,这回通过内部逻辑来完成。

创建状态

在Animator窗口可以创建状态,包括Empty,Blend Tree和Sub-State Machine。详细解释请参考官方手册。 选中l状态,点击Inspector界面的Add Behaviour按钮,输入名称,就会新建一个脚本,它继承自StateMachineBehaviour,默认会有注释的多个函数,每个函数用途都有注释说明。 我们这里暂时不需要OnStateMove和OnStateIK函数,这两个函数是针对动画的,所以先删掉。并留下OnStateEnter、OnStateUpdate、OnStateExit三个函数并取消注释。 每个函数都有Animator、AnimatorStateInfo和layerIndex三个参数。Animator就是获取当前animator controller的接口,我们要知道,state machine behavior是以asset的形式存在,而不是类的形式,因此使用这个参数是在运行时获取它引用的最好方式。AnimatorStateInfo提供了当前状态的信息。而Layer则是当前的层。

设置条件

我们可以设置一些条件是AI在不同状态切换,其实就是设置参数。参数有四种类型,Int、float、Trigger、bool,然后选中状态直接的Transition,就可以设置对应参数的检测条件,比如某个参数大于5或某个参数为否等。而在脚本里,可以通过函数设置参数,从而达到切换条件的目的。

使用脚本实现FSM:

当然,除了使用Animator,也可以自己写脚本实现FSM,这方面网上的资料也很多。 简单的可以使用 Switch 或是if-else实现,也可以配合委托和事件来实现。

可以参考一下Unity wiki的这个实现Finite State Machine

Artificial Intelligence(AI),是一个复杂高深的课题,应用在许多领域,在游戏中自然也有重要的作用。本质上来说,AI就是让计算机能像生物一样进行思考和决定来执行某些特定操作。除了游戏特定的技术,AI还应用到了计算机视觉、自然语义处理、机器学习等许多领域。 这个系列,我们主要就是参考《Unity AI Game Programming Sencond Edition》,介绍如何在Unity中实现一些AI技术。

定义agent

在开始前,我们先要介绍一个将会频繁使用到的术语 - agent。Agent对于AI来说,就是我们的人工智能实体。当我们讨论AI时,并不是特指某个角色,而是一个表现复杂行为模式的实体,这个实体可以是角色、动物、交通工具等等任何东西。Agent会自主的执行我们赋予的模式和行为。

有限状态机(Finite State Machines)

Finite State Machines (FSM)可以被认为是最简单的AI模型。一个状态机基本由一组状态(state)组成,这组状态由它们之间的转移(transition)来连接。一个游戏实体任一时刻只能处于一个状态。 FSM十分容易实现并且易于理解,只要使用if/else或者switch就可以创建FSM。但随着状态和转移的增多,状态机会变得越来越混乱。后面我们会详细介绍如何实现简单的状态机。

通过agent观察世界

为了使AI更有说服力,我们的agent也需要能对周围环境、角色的事件产生回应。好像生物一样,agent也可以依靠看、听或其他“感官”。视觉、听觉或其他感觉,本质上也是数据。然而模拟真实的数据是非常复杂的,但我们仍可以通过一些方法模拟数据来产生类似的结果。

路径跟踪和驾驶

有时,我们需要AI能在游戏世界中根据预定的路径行走。比如赛车比赛中的对手,或是RTS游戏中的单位通过地形和其他单位导航到特定位置。 为了展现智能,agents要能决定自己要到哪、是否能到达,寻找最有效率的路径,并且遇到障碍可以绕过。 后面我们会介绍A*寻路系统,以及Unity内置的navigation mesh (NavMesh)功能。

A*寻路

A是一种寻路算法,因为其性能和精度广泛用于游戏中。我们来看个简单例子了解它是如何工作的。假设我们想让单位从A移动到B,但中间有堵墙挡着不能直接过去,这是就要找到一条路绕过去。 我们先把整个地图划分成许多小格,使地图成网格形(当然也可以划分成别的如六边形、三角形),这样会令搜索地图更加简单,这是非常重要的一步。 然后我们就可以开始寻找最佳路径,通过计算每一格距离无障碍的起始格的移动分数,选择最少的消耗的格子。我们会在后面详细介绍A的实现方法。

A*需要通过大量计算得出最佳路径。于是为了更简单快速,人们又想出了通过路径点来查找路径的方法。还是假设上面的从A点到B点的例子,这时我们可以使用三个路径点。

我们现在要做的是找到最近的路径点然后跟随它的连接网点到达目的地。使用路径点计算更加高效,但也有问题,比如当障碍更新,路径点也要必须更新。

AI在两个节点间是走直线的,如果路径离墙很近那么AI就可能撞到墙上并卡住。虽然我们可以调整路径来躲避障碍,但问题是路径点并不能提供环境信息,如果我们调整的路径是在悬崖或是桥边呢?这样的路径并不安全。如果我们想让AI能有效的行动,就需要大量的路径点,但这是非常难以实现和管理的。

这时使用NavMesh就会更加合理。NavMesh是另一种表示游戏世界的图表结构。如下所示:

一个navigation mesh使用凸面多边形(convex polygons)来表示AI实体可以通过的地形区域。navmesh最重要的优点是相比路径点,可以提供许多环境信息。现在我们可以安全的调整我们的路径因为我们知道哪块区域是可以通过的。另一个好处是,使用navmesh,同样的mesh可以用于不同AI实体。 但是通过编程对场景生成navmesh是比较复杂的处理。不过幸运的是,Unity中已经自带了这一功能。后面我们将详细介绍。

集群(flocking)和人群动力学(crowd dynamics)

许多生物比如鸟、鱼、昆虫等都会成组的执行某些行为。假如我们想模拟一群鸟在空中飞翔,如果对每个鸟都做动画实现,那是非常困难的。我们可以对每个鸟应用一些规则,就可以达到自然群聚的智能行为。 同样的,一大群人,相比于控制每一个人的行为,模拟整个人群的行为更为可行。群组中的每个个体,只需要知道整个群组的方向和自己最相邻的个体的目的来实现整个系统的功能。

行为树(Behavior trees)

行为树是另一个表示和控制AI逻辑行为的模式。前面我们大概介绍了FSM,这是一种非常简单但高效的方法来定义agent的行为。但是FSM很难调整,因为它们很快就会变得很笨拙而且需要大量手动设置。我们需要一种可扩展的方法来解决大量问题的情景。这就是行为树提出的背景。
行为树是节点的集合,以分层顺序组合,不同于状态互相连接,它是节点连接到父节点,就好像树一样。
行为树的基本元素是任务节点。有许多不同的任务,比如序列(Sequence)、选择器(Selector)和平行装饰(Parallel Decorator)等。 首先来看一下选择器。如图,用圆形表示选择器。它会按从左到右顺序评估子节点。首先选择attack,如果返回成功,选择器就完成并返回父节点。如果attack返回失败,就继续尝试Chase任务。同理,chase返回失败就继续尝试Patrol。

接下来看一下序列任务,通过内部带箭头的矩形表示。根选择器会选择第一个序列事件来执行。这个序列事件第一个任务是检测是否足够近以实施攻击,如果任务成功,会继续下一个任务,即攻击 。如果攻击也返回成功,则整个序列任务成功,选择器结束并不会执行其他序列事件。如果第一个检测距离任务失败,则第一个序列事件失败,选择器会选择下一个序列事件执行。

另外两个常用组件是平行任务和装饰。平行任务会同时执行所有它的子任务,而选择器和序列只会依次的执行子任务。装饰是另一种类型的任务,它只有一个子节点。它可以更改子节点任务的行为,比如对于子任务是否执行、何时执行等。 后面我们会更详细的介绍行为树。

以模糊逻辑思考

最后,我们来介绍模糊逻辑。简单的说,模糊逻辑指的是近似输出而不是标准的二进制结果。 假设我们Agent是一个敌人士兵,无论它使用了FSM还是行为树,它都要作出许多决定。比如是否转换到状态a/b/c?这个任务返回true还是false?没有使用模糊逻辑时,我们需要一个二进制值来解答这些问题。比如这个士兵是否看到了玩家?答案是一个yes/no的二进制状态。但我们可以进一步抽象这个决定,来使士兵完成一些有趣的行为。当士兵确定能看到主角时,它可以问自己是否有足够的弹药、是否有足够的生命值完成战斗、主角是否有其他盟友在周围等。这样,我们的AI就会变得更加有趣、不可预测且更可信。

总结

学术AI和游戏AI有着许多不同之处。学术AI主要解决真实世界的问题和提出一些没有资源限制的理论。而游戏AI主要专注于在有限资源下构建有人工智能的NPC。游戏AI的目的主要是提供有挑战性的对手来使游戏更有趣味。 在后面的内容,我们会分别介绍上面提到的这些AI技术。

顶点数据

对Cg/HLSL顶点程序,Mesh的顶点数据是作为输入传入到顶点着色器函数的。每个输入需要指定语义,比如POSITION表示顶点位置,NORMAL表示顶点法线等。

通常使用结构体输入顶点数据,而不是一个个分开的。几个常用的顶点数据都定义在 UnityCG.cginc 文件,通常使用它们就足够了。这些结构体包括:

  • appdata_base: position, normal and one texture coordinate.
  • appdata_tan: position, tangent, normal and one texture coordinate.
  • appdata_full: position, tangent, normal, four texture coordinates and color.

如果你想获取不同的顶点数据,你需要自己声明结构体,或者添加输入参数。顶点数据通过语义区分,并且来自以下列表:

  • POSITION : 顶点坐标,通常是 float3 / float4.
  • NORMAL : 顶点法线,通常是 float3.
  • TEXCOORD0 : 第一个 UV coordinate, 通常是 float2..float4.
  • TEXCOORD1 .. TEXCOORD3 : 第二到第四个 UV coordinates.
  • TANGENT : 切向量 (用于法线映射), 通常是float4.
  • COLOR : 每个顶点的颜色, 通常是 float4.

当mesh数据包含的少于需要的输入数据,多余的会填充为0,除了 .w 默认为1。比如,通常纹理坐标是 2D 向量,如果定义了float4类型的TEXCOORD0输入,那么获取的值是:(x,y,0,1)。

例子

可视化UVs

下面的例子使用顶点位置和第一个纹理坐标作为顶点输入(在appdata内定义)。这个shader对于调试UV坐标非常有用。UV坐标显示为红色和绿色,而超过0-1范围的坐标显示为额外的蓝色。

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

            // vertex input: position, UV
            struct appdata {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
            };
            
            v2f vert (appdata v) {
                v2f o;
                o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
                o.uv = float4( v.texcoord.xy, 0, 0 );
                return o;
            }
            
            half4 frag( v2f i ) : SV_Target {
                half4 c = frac( i.uv );
                if (any(saturate(i.uv) - i.uv))
                    c.b = 0.5;
                return c;
            }
            ENDCG
        }
    }
}

可视化顶点颜色

这个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
Shader "Debug/Vertex color" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // vertex input: position, color
            struct appdata {
                float4 vertex : POSITION;
                fixed4 color : COLOR;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                fixed4 color : COLOR;
            };
            
            v2f vert (appdata v) {
                v2f o;
                o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
                o.color = v.color;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target { return i.color; }
            ENDCG
        }
    }
}

可视化法线

这个shader使用顶点位置和法线作为输入。法线的x、y、z元素作为颜色显示。因为法线是在[-1, 1]范围,所以调整偏移它们到[0, 1]范围:

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

            // vertex input: position, normal
            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                fixed4 color : COLOR;
            };
            
            v2f vert (appdata v) {
                v2f o;
                o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
                o.color.xyz = v.normal * 0.5 + 0.5;
                o.color.w = 1.0;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target { return i.color; }
            ENDCG
        }
    }
}

可视化切线(tangents)和次法线(binormals)

切线和次法线用于法线映射。Unity中只有切线向量存储在顶点,而次法线是通过法线和切线推出的。

这个shader使用顶点位置和切线作为输入。切线的x、y、z元素作为颜色显示。

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

            // vertex input: position, tangent
            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                fixed4 color : COLOR;
            };
            
            v2f vert (appdata v) {
                v2f o;
                o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
                o.color = v.tangent * 0.5 + 0.5;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target { return i.color; }
            ENDCG
        }
    }
}

下面的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
Shader "Debug/Bitangents" {
    SubShader {
        Pass {
            Fog { Mode Off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // vertex input: position, normal, tangent
            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 color : COLOR;
            };
            
            v2f vert (appdata v) {
                v2f o;
                o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
                // calculate bitangent
                float3 bitangent = cross( v.normal, v.tangent.xyz ) * v.tangent.w;
                o.color.xyz = bitangent * 0.5 + 0.5;
                o.color.w = 1.0;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target { return i.color; }
            ENDCG
        }
    }
}

参考:
Providing vertex data to vertex programs

当编写Cg/HLSL程序,输入和输出变量需要通过“语义”来表达它们的意图。这是HLSL语言中的概念,可以查看Semantics documentation on MSDN

顶点着色器输入语义:

主顶点函数(#pragma vertex 指定的函数)对于所有输入参数都需要有语义。这些是与单一的Mesh元素对应的,比如顶点位置、法线、纹理坐标等。

这里有一个顶点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
Shader "Unlit/Show UVs"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert (
                float4 vertex : POSITION, // vertex position input
                float2 uv : TEXCOORD0 // first texture coordinate input
                )
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, vertex);
                o.uv = uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(i.uv, 0, 0);
            }
            ENDCG
        }
    }
}

不同于分别拼出所有的输入,可以使用结构体来声明,并对每个结构体每个单独的成员指定语义。

片段着色器输出语义

一般片断着色器是输出一个颜色,并且有一个SV_Target语义。比如上面的例子中:

1
fixed4 frag (v2f i) : SV_Target

这个frag函数有一个fixed4类型的返回值,它只返回一个值由SV_Target语义示意。

也可以输出一个结构体,上面的例子可以改写成这样:

1
2
3
4
5
6
7
8
9
struct fragOutput {
    fixed4 color : SV_Target;
};            
fragOutput frag (v2f i)
{
    fragOutput o;
    o.color = fixed4(i.uv, 0, 0);
    return o;
}

返回结构体通常用于返回值不是一个单一的颜色。片断着色器还支持的其他语义有:

Multiple Render Targets: SV_TargetN
SV_Target1, SV_Target2, … - 由shader写的额外颜色。用于一次渲染到多于一个的渲染目标时。SV_Target0 和 SV_Target 一样。

Pixel Shader Depth Output: SV_Depth
SV_Depth - 覆盖深度缓存值。通常片段着色器不覆盖Z缓存,而是来自常规的三角形光栅化。然而为了实现某些效果,可以输出每个像素用户自定义的深度值。

注意对许多GPU为了深度缓存优化,这个是关闭的。所以除非必要不要重写Z缓存。SV_Depth的消耗与具体的GPU架构有关,但一般与alpha testing(HLSL内建的clip()函数)基本相当。

输出的深度值需要是一个float。

顶点着色器输出和片段着色器输入

一个顶点着色器需要输出每个顶点的最终”裁剪空间“位置,以便GPU知道渲染的位置以及光栅化深度。这个输出需要有SV_POSITION语义,并且是float4类型。

顶点着色器输出的值,会在渲染的三角形面之间进行插值,然后每个像素的这个值会作为输入传递到片段着色器。

许多现代GPU并不关心这些值有什么语义,然而许多老的系统(SM2.0或是DX9)还是对语义有特殊要求:

  • TEXCOORD0, TEXCOORD1, … , 用于指示任意精度数据 - 纹理坐标、位置等。
  • COLOR0 和 COLOR1,用于低精度0-1范围数据(颜色)。

为了更好的跨平台支持,通常把顶点输出和片段输入标记为 TEXCOORDn 语义。

其他特殊语义

屏幕空间像素位置: VPOS

一个片段着色器可以接收被渲染的像素的位置。这个特性只支持SM2.0,所以需要声明 #pragma target 3.0 编译指令。

在不同平台,像素坐标的类型是不同的,所以为了方便使用 UNITY_VPOS_TYPE 类型。

另外,使用了像素坐标语义,会使在一个v2f结构体内同时获取裁剪空间坐标(SV_POSITION)和VPOS变得困难。所以顶点着色器会通过一个单独的输出变量,来输出裁剪空间坐标。请看一个例子:

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
Shader "Unlit/Screen Position"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            // note: no SV_POSITION in this struct
            struct v2f {
                float2 uv : TEXCOORD0;
            };

            v2f vert (
                float4 vertex : POSITION, // vertex position input
                float2 uv : TEXCOORD0, // texture coordinate input
                out float4 outpos : SV_POSITION // clip space position output
                )
            {
                v2f o;
                o.uv = uv;
                outpos = mul(UNITY_MATRIX_MVP, vertex);
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos : VPOS) : SV_Target
            {
                // screenPos.xy will contain pixel integer coordinates.
                // use them to implement a checkerboard pattern that skips rendering
                // 4x4 blocks of pixels

                // checker value will be negative for 4x4 blocks of pixels
                // in a checkerboard pattern
                screenPos.xy = floor(screenPos.xy * 0.25) * 0.5;
                float checker = -frac(screenPos.r + screenPos.g);

                // clip HLSL instruction stops rendering a pixel if value is negative
                clip(checker);

                // for pixels that were kept, read the texture and output it
                fixed4 c = tex2D (_MainTex, i.uv);
                return c;
            }
            ENDCG
        }
    }
}    

面的方向: VFACE

片段着色器可以接收一个变量,指示渲染的表面是面向摄像机还是背向摄像机。这对于需要渲染双面的几何体很有效。VFACE语义的输入值,对正面会包含一个正值,反面包含一个负值。

只支持SM3.0。

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
Shader "Unlit/Face Orientation"
{
    Properties
    {
        _ColorFront ("Front Color", Color) = (1,0.7,0.7,1)
        _ColorBack ("Back Color", Color) = (0.7,1,0.7,1)
    }
    SubShader
    {
        Pass
        {
            Cull Off // turn off backface culling

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            float4 vert (float4 vertex : POSITION) : SV_POSITION
            {
                return mul(UNITY_MATRIX_MVP, vertex);
            }

            fixed4 _ColorFront;
            fixed4 _ColorBack;

            fixed4 frag (fixed facing : VFACE) : SV_Target
            {
                // VFACE input positive for frontbaces,
                // negative for backfaces. Output one
                // of the two colors depending on that.
                return facing > 0 ? _ColorFront : _ColorBack;
            }
            ENDCG
        }
    }
}

首先使用Cull Off关闭背面剔除(默认是剔除背面的)。然后对正反面设置不同的颜色。

顶点ID:SV_VertexID

顶点着色器可以接收一个变量,它包含一个unsigned int类型的顶点编号。通常用于你希望从贴图或ComputeBuffers获取额外的每顶点信息。

只支持DX10(SM4.0)或 GLCore / OpenGL ES 3,所以需要编译指令:#pragma target es3.0。

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
Shader "Unlit/VertexID"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target es3.0

            struct v2f {
                fixed4 color : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert (
                float4 vertex : POSITION, // vertex position input
                uint vid : SV_VertexID // vertex ID, needs to be uint
                )
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, vertex);
                // output funky colors based on vertex ID
                float f = (float)vid;
                o.color = half4(sin(f/10),sin(f/100),sin(f/1000),0) * 0.5 + 0.5;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return i.color;
            }
            ENDCG
        }
    }
}

参考:
Shader Semantics

shader程序使用Cg/HLSL语言编写,为嵌入在pass内的代码片段,一般如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Pass {
    // ... the usual pass state setup ...
    
    CGPROGRAM
    // compilation directives for this snippet, e.g.:
    #pragma vertex vert
    #pragma fragment frag
    
    // the Cg/HLSL code itself
    
    ENDCG
    // ... the rest of pass setup ...
}

Cg/HLSL片段

Cg/HLSL片段写在 CGPROGRAM 和 ENDCG 之间。
在片段开始可以通过提供的 #pragma 命令声明编译指令。Unity认可的指令有以下这些:

  • #pragma vertex name - 以name为名字的 vertex shader 函数。
  • #pragma fragment name - 以name为名字的 fragment shader 函数。
  • #pragma geometry name - 以name为名字的 DX10 geometry shader 函数。使用这个指令,会默认打开 #pragma target 4.0。
  • #pragma hull name - 以name为名字的 DX11 hull shader 函数。使用这个指令,会默认打开 #pragma target 5.0。
  • #pragma domain name - 以name为名字的 DX11 domain sahder 函数。使用这个指令,会默认打开 #pragma target 5.0

其他编译指令:

  • #pragma target name - 指定编译的 shader target。
  • #pragma only_renderers space separated names - 只对指定的渲染器编译。
  • #pragma exclude_renderers space separated names - 不编译指定的渲染器。
  • #pragma multi_compile …_ - 用于multiple shader variants。
  • #pragma enable_d3d11_debug_symbols - 对DirectX 11的shader编译生成debug信息,允许你使用VS Graphics debugger 调试shader。

每个代码片段至少要包含一个顶点程序和一个片段程序。因此 #pragma vertex 和 #pragma fragment 指令是必须的。

顶点和片断程序

当你使用顶点和片断程序(即可编程管线)时,显卡的大部分硬编码(固定功能管线)功能将关闭。 例如,使用一个顶点程序完全可以做到关闭标准的3D变换,灯光和纹理坐标的功能。类似的,使用一个片段程序可以替换任何纹理混合模式,而这些纹理混合模式都在在SetTexture命令中有定义,因此SetTexture命令是不需要的。

编写顶点/片断程序需要对3D转换、照明和坐标空间有透彻的了解。因为你要自己写出像OpenGL实现的固定功能一样的效果。另外,还可以实现内置功能以外自己需要的功能。

在ShaderLab中使用Cg/HLSL

ShaderLab中的shader通常使用Cg/HLSL编写。Cg和DX9的HLSL几乎一样,所以可以交换使用。

Shader代码写在“Cg/HLSL代码段“中。代码段会被编译为低级着色器集合,并且最终的着色器是包含在你的游戏数据文件内的。当你在Project View选中一个shader文件,Inspetor窗口会有个按钮可以查看编译后的着色器代码,可以帮助调试。Unity会自动编译Cg片段到相关平台。因为Cg/HLSL代码是通过Unity editor编译的,所以不能在运行时创建shader。

下面的例子演示了一个完整Cg程序的着色器,它的结果是颜色随着法线而变化:

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
Shader "Tutorial/Display Normals" {
    SubShader {
        Pass {

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                o.color = v.normal * 0.5 + 0.5;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4 (i.color, 1);
            }
            ENDCG

        }
    }
}

这个着色器没有属性,有一个SubShader,包含一个pass。我们来分析一下这段Cg代码:

1
2
3
4
5
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// ...
ENDCG   

整个Cg片段写在CGPROGRAM与ENDCG关键字中间。开始编译的指令是 #pragma 给出,指定了顶点函数vert 和 片段函数frag。

1
#include UnityCg.cginc

这是普通的Cg代码,包含一个内置的文件。 UnityCg.cginc文件包含了常用的声明,所以可以使着色器简短。这里我们使用文件中定义的 appdata_base 结构体。

接下来我们定义了一个”vertex to fragment”结构,即v2f。定义了从顶点程序传递到片段程序的数据。我们传递了位置和颜色参数。颜色会在顶点程序中计算然后在片段程序中输出。

在顶点函数-vert中,我们计算顶点位置,然后将输入的法线输出为颜色:

1
o.color = v.normal * 0.5 + 0.5;

法线的范围是[-1, 1],而颜色范围是[0, 1]。所以通过上面计算调整。接下来定义一个片段程序,它只返回计算好的颜色:

1
return fixed4 (i.color, 1);

到此,我们就分析完了这个着色器。虽然只是个简单的着色器,但很方便查看网格的法线。

在Cg/HLSL代码中使用shader属性

例子介绍

在Cg代码中使用着色器属性(shader properties),你必须定义一个变量,变量的的名字和类型要与它相匹配。 这里有个完整的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
Shader "Tutorial/Textured Colored" {
    Properties {
        _Color ("Main Color", Color) = (1,1,1,0.5)
        _MainTex ("Texture", 2D) = "white" { }
    }
    SubShader {
        Pass {

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"

        fixed4 _Color;
        sampler2D _MainTex;

        struct v2f {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        float4 _MainTex_ST;

        v2f vert (appdata_base v)
        {
            v2f o;
            o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
            o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
            return o;
        }

        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 texcol = tex2D (_MainTex, i.uv);
            return texcol * _Color;
        }
        ENDCG

        }
    }
}

这个shader和上面的shader结构相同。在这个例子里,我们定义了两个属性,名字为 _Color 和 _MainTex。于是在Cg/HLSL代码中,我们也相应的定义了属性:

1
2
fixed4 _Color;
sampler2D _MainTex;

这里的顶点程序使用UnityCG.cginc里的TRANSFORM_TEX,用来保证纹理(texture)正确的缩放和偏移。片断(fragment)程序只是对纹理(texture)进行采样然后乘以颜色值。

属性类型:

对于下面的shader属性:

1
2
3
4
5
_MyColor ("Some Color", Color) = (1,1,1,1) 
_MyVector ("Some Vector", Vector) = (0,0,0,0) 
_MyFloat ("My float", Float) = 0.5 
_MyTexture ("Texture", 2D) = "white" {} 
_MyCubemap ("Cubemap", CUBE) = "" {} 

Cg/HLSL代码中为了获取,需要定义为:

1
2
3
4
5
fixed4 _MyColor; //对于颜色通常低精度类型就足够了
float4 _MyVector;
float _MyFloat; 
sampler2D _MyTexture;
samplerCUBE _MyCubemap;

Cg/HLSL也可以接受uniform,但是没有必要:

1
uniform float4 _MyColor;

ShaderLab的映射到Cg/HLSL的属性类型:

  • Color 和 Vector 属性映射为 float4, half4 或 fixed4 变量。
  • Range 和 Float 属性映射为 float, half 或 fixed 变量。
  • Texture 属性。对于2Dtextures 为 sampler2D; Cubemaps 为 samplerCUBE; 3D textures 为 sampler3D.

特殊的贴图属性:

Texture tiling & offset:

对于贴图材质通常有Tiling 和 Offset数据块。在shader中,这个信息是通过一个名为{TextureName}_ST的float4属性传递:

x 是 X tiling; y 是 Y tiling; z 是 X offset; w 是 Y offset。

比如,一个贴图名为 _MainTex,那么它的tilling信息为一个_MainTex_ST的vector。

Texture size:

{TextureName}_TexelSize - 一个float4属性包含了贴图的size信息:

x 是 1.0/width; y 是 1.0/height; z 是 width; w 是 height

Texture HDR parameters:

{TextureName}_HDR - 一个float4属性,包含了怎样解码潜在的HDR贴图信息。


参考:
Writing vertex and fragment shaders
Shaders: Vertex and Fragment Programs
Accessing shader properties in Cg/HLSL