Esfog_UnityShader教程_UnityShader语法实例浅析
作者:esfog 原文链接:http://www.taidous.com/thread-25286-1-1.html
距离上次首篇前言发布已有一段时间,这段时间我一直比较忙。今天是周末,不能再拖延了。经过一段时间的思考,我决定这一系列教程会避免过于深入细节。一来可避免误导部分同学,二来能防止文章过于冗长难读,三来能让大家有更多自主思考的时间。若要讲述一些细节问题,我会另开一个系列。
UnityShader语法实例浅析
上一次在前言中,我大致介绍了图形渲染的流程以及Shader的参与方式。本系列教程更注重实际应用,因此在这一节,为后续学习打基础,我们来分析一下UnityShader的语法结构。若你还没看过前言,建议先去看看,这有助于提高学习效率,前言链接为:http://www.cnblogs.com/Esfog/p/3534435.html。
我们先来看一段简单且完整的Shader代码。本系列教程若无特殊声明,所有UnityShader均为Vertex&Fragment Shader。
Shader "Esfog/SimpleShader"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader
{
Pass
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
struct VertexOutput
{
float4 pos:SV_POSITION;
float2 uv_MainTex:TEXCOORD0;
};
VertexOutput vert(appdata_base input)
{
VertexOutput o;
o.pos = mul(UNITY_MATRIX_MVP,input.vertex);
o.uv_MainTex = input.texcoord.xy;
return o;
}
float4 frag(VertexOutput i):COLOR
{
float4 col = tex2D(_MainTex,i.uv_MainTex);
return col;
}
ENDCG
}
}
FallBack "Diffuse"
}
下面我们来逐步分析这段代码。在此给大家一些建议,如果遇到暂时无法理解的知识,可以合理选择暂时跳过。我不会面面俱到,很多知识点需要结合具体实例才能讲解清楚,希望大家理解。知识的学习并非一蹴而就,随着不断积累,很多以前不理解的地方会逐渐明晰。不过,首先你要有坚持学习和探索的精神。
第1行
代码 Shader "Esfog/SimpleShader" 类似于HTML中的 <html></html>,是整个Shader的最外层结构。在后面的引号中,你可以为Shader命名,且该名称不必与Shader文件的名称一致。其中的左斜线是目录分隔符,命名后,在材质上选择Shader时,就能通过这个名字找到对应的Shader。
第3 - 6行
这部分代码用于定义Shader的可调节参数。定义后,在Unity中选择该材质,可在Inspector面板中调节这些变量,从而影响Shader的展示效果。由于很多时候我们无法准确预估所有参数值,因此需要将这些变量暴露出来,以便美术同学或程序开发者通过调节达到理想效果。这里仅定义了一个2D贴图变量。下面解释这行代码 _MainTex ("Base (RGB)", 2D) = "white" {},_MainTex(名称可随意,但建议以下划线开头)是Shader内部识别该变量的名称;Base (RGB) 是显示在Inspector中的名称,可随意命名;2D 指定了变量类型,在Unity的帮助文档中,有多种类型可供选择,如 float、color、range 等,这里不再赘述;等号后面是为变量设置的默认值,不同变量类型的默认值设置方式不同,2D贴图按此方式设置即可。
第8行
SubShader 需要着重解释。一个Shader中可以包含任意多个 SubShader,但最终只有一个会被显卡选中并执行。这是因为Shader代码可能运行在不同机型上,而各显示渲染设备存在差异,所以通常会在不同的 SubShader 中编写针对不同渲染能力设备的Shader代码,显卡会自动选择最适合自身性能的 SubShader 执行。若所有 SubShader 都未被选中,则会默认执行 FallBack 后面跟随的Shader(见第43行代码)。一般在自己电脑上学习时,编写一个 SubShader 即可。
第10行
Pass 也很重要。在一个 SubShader 中可以定义多个 Pass,与 SubShader 不同,每个 Pass 都会按顺序依次执行。我们需要在 Pass 中编写具体的着色器代码。使用多个 Pass 可能是为了实现一些特殊效果,在网上大神的Shader代码中经常能看到多个 Pass 的情况。需要注意的是,在Unity主推的Surface Shader中不能编写 Pass,因为在Unity的光照模型中会自动定义一些 Pass,所以不允许额外编写。
第12行
Tags { "RenderType"="Opaque" } 是ShaderLab(UnityShader中除CG以外的语法)提供的可配置选项。这些选项可以为每个 Pass 单独设置,也可以在 SubShader 下统一设置,供所有 Pass 共用。除 Tags 外,还有 Cull、Blend、ZTest 等选项,这些都很重要,后续章节会具体讲解,有兴趣的同学可查阅文档。这里简单介绍一下 Tags,其语法类似于HTML中定义标签属性的方式,用于配置渲染参数。RenderType = "Opaque" 告知渲染设备,使用该Shader的材质是不透明物体。这是因为场景中存在大量半透明和不透明物体,不同类型的材质在渲染上存在差异,会影响游戏场景的最终展示效果。
第14 - 40行
被 CGPROGRAM 和 ENDCG 包含的部分是真正的CG代码,顶点着色器和片段着色器都要写在此处。下面继续详细分析。
第15 - 16行
这两行代码告知渲染设备顶点着色器(vertex)和片段着色器(fragment)的名称,这是为了让显卡在渲染时能准确找到它们,必须进行声明。后续编写着色器时,要使用声明的名称(不一定是 vert 和 frag,可随意命名,但要保证前后一致)。
第17行
#include "UnityCG.cginc" 是因为Unity提供了大量可用的变量和常量,如一些空间变换矩阵,添加这行代码可提高开发效率。在Unity 4.0之后,这行代码可省略,因为会默认包含,但为了向下兼容,建议保留。
第18行
uniform sampler2D _MainTex; 声明了一个名为 _MainTex 的变量,类型为 sampler2D(即可采样的2D贴图),uniform 可省略,其作用是表明该变量由外部赋值。为保证严谨性,建议添加。这里的 _MainTex 与 Properties 中的 _MainTex 名称一致,通过这种方式,可在着色器中使用 Properties 中声明的变量。外部提供变量的方式不止 Properties 一种,还可通过脚本直接为Shader的变量赋值,后续会结合具体应用详细说明。
第20 - 24行
这部分代码声明了一个顶点着色器的输出结构,同时也是片段着色器的输入结构。当然,也可以不使用结构体,通过 out 返回结果,但为了提高代码可读性,建议使用结构体。CG中的类型与C语言有一些差异,增加了 float4、float2、float4x4 等类型,更多信息可参考Unity文档。需要注意的是,这些是CG的基本类型,并非复合类型,因为显卡的工作模式与CPU不同,这种类型的变量更便于渲染设备处理。与C语言的最大区别在于,每个变量声明后都跟有类似 :SV_POSITION 的语法,在CG中称为语义(semantic),其作用是将Shader中的变量与显卡中的相应寄存器关联起来,这与渲染管线密切相关。简单来说,显卡设置了一些特定的寄存器,用于存放指定的特殊变量,渲染时可直接取用,这样能提高处理速度,也便于像素着色器和顶点着色器之间的通信。SV_POSITION 专门用于存放模型顶点在投影空间的坐标,若对变换不太了解,可参考我上一篇前言。TEXCOORD0 可用于存放任意变量,光栅化时该变量会被插值。类似的还有 TEXCOORD1、TEXCOORD2 等,具体数量取决于显卡性能。在这个结构中,添加了两个变量:一个是顶点投影坐标,这是必需的;另一个是顶点的纹理坐标,并非必需,这里添加是为了使用2D贴图为模型上色。纹理坐标即uv坐标,美术在制作模型时,为了将2D贴图贴到3D模型上,会为模型的每个顶点指定一个纹理坐标,这是一个二维坐标,根据该坐标可在贴图上查找相应颜色。光栅化时,纹理坐标会被插值,最终只需为几个顶点指定纹理坐标,就能让整个模型添加纹理颜色。若对此不太理解,可自行百度或暂时跳过。
第26 - 32行
这是顶点着色器的代码部分。着色器可理解为函数,其返回类型为之前声明的输出结构,名称与 CG 代码开头通过 #pragma 关联的名称一致。参数方面,顶点着色器接受的是模型最原始的参数,即美术同学指定的模型本身的参数,包括顶点位置、法线、纹理坐标、颜色等。Shader通过语义(semantic)关联这些参数,而非声明外部变量接收。appdata_base 是Unity定义好的结构,在 #include "UnityCG.cginc" 中包含,可在Unity安装目录的 /Editor/Data/CGIncludes 文件夹中查看其源代码。
struct appdata_base {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
在顶点着色器中,可通过 appdata_base.XXX 的方式直接获取模型的顶点信息。由于要返回一个输出结构,所以在第28行先定义一个结构变量,然后为其中的子变量赋值。模型的原始顶点位置在模型空间,而输出结构中的顶点位置在投影空间,需要进行一系列空间变换。若看过我写的前言,应该知道要先进行“模型空间 -> 世界空间”变换,再进行“世界空间 -> 摄像机空间”变换,最后进行“摄像机空间 -> 投影空间”变换。不过,Unity已经将这些变换综合为一个变换矩阵 UNITY_MATRIX_MVP,若想了解矩阵相关知识,可查阅线性代数资料。通过 o.pos = mul(UNITY_MATRIX_MVP, input.vertex) 完成对顶点位置的赋值。这里要注意,由于Unity是左手坐标系,必须将顶点位置右乘到变换矩阵上,即 mul(UNITY_MATRIX_MVP, input.vertex),而 mul(input.vertex, UNITY_MATRIX_MVP) 是错误的,具体原因可查阅线性代数资料。对于 o.uv_MainTex = input.texcoord.xy,模型的纹理坐标最初存储在 TEXCOORD0 中,这里再次赋值可能是因为只有通过顶点着色器返回的结果才会被插值。在使用纹理坐标时,只需要前两位,因此通过 .xy 的方式提取,.rgba 或 .xyzw 是CG提供的特殊语法,可快速提取子变量,提高效率。在这个例子中,只需要 xy 坐标就能在纹理贴图上找到相应颜色,为减少计算量,可舍弃 zw 坐标。完成输出结构的赋值后,返回该结构即可。
第34 - 38行
这是片段着色器的代码。片段着色器的任务是计算出一个颜色,供渲染设备进行最终处理,因此返回值为 float4(存储的是 rgba)。函数名称与前面 #pragma 约定的一致,参数为顶点着色器的输出结构。后面的 :COLOR 是为了将返回结果与 COLOR 语义关联的寄存器绑定,渲染设备最终处理时会从该寄存器获取片段着色器计算的结果。这里使用了 tex2D 函数,该函数根据纹理坐标查询2D贴图的颜色,第一个参数是贴图,即上面声明的 _MainTex,第二个参数是要查询的坐标,直接填入顶点输出结构中赋值的纹理坐标即可,最终返回贴图上的颜色。需要注意的是,tex2D 函数不能在顶点着色器中使用,这是受渲染管线结构和目前显卡硬件设备的限制,不过未来这个问题有望得到解决。
(~ o ~)~ 写到这里,本章节即将结束,不知不觉写了3个小时。最后上几张图,展示一下效果,这只是最基础的为模型添加贴图颜色的效果。
如果你完全没有使用过Shader,建议查看官方文档,下面简单介绍使用步骤:
- 新建一个Shader,将代码写入其中。
- 新建一个材质球,或使用现有的材质球,在材质球上选择编写的Shader,如下图在Shader选项中选择,并设置相应参数。在本例子中,将贴图原图拖到右侧的Select处即可。
- 将材质球拖到模型上,或修改模型自带材质球的Shader。
最好查看一下效果和两张用到的贴图,这有助于更好地理解纹理坐标的意义。
- 上面这张是使用默认Shader “Diffuse” 且无贴图时的效果。
- 上面这张是使用刚刚编写的Shader并添加贴图后的效果。
下面是用到的两张贴图,在游戏制作过程中,这些贴图通常由美术提供,若想从事技美工作,也可以自行学习制作。
好了,本次章节到此结束。如果有不理解的地方,欢迎留言;若发现文章中有错误,请及时指出。同时,鼓励大家多写博客,与他人分享,共同提高。谢谢!