在学习 Unity Shader 前,最好是对 CG Shader 有一定的了解,起码要知道 Shader 数据类型,语义这些最基本的东西。(推荐啃 Nvidia 的《The Cg Tutorial》,当然可以找中文版的来看,叫《可编程实时图形权威指南》,这本书现在已经基本买不到正版,看 PDF 版的吧)
在 Unity 中,Shader 的使用要基于物体和材质,我们可以通过新建一个材质,然后指定对应的 Shader 文件,最后把材质赋予物体进行渲染。
我们先看一个比较简单的 Unity Shader:
Shader "Ojors/Simple Shader"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
struct VertexOut
{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
float2 uv_MainTex : TEXCOORD0;
};
VertexOut vert(appdata_base i)
{
VertexOut o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.color = i.normal * 0.5 + float3(0.5, 0.5, 0.5);
o.uv_MainTex = i.texcoord.xy;
return o;
}
float4 frag(VertexOut i) : SV_Target
{
return float4(i.color, 1.0) + tex2D(_MainTex, i.uv_MainTex);
}
ENDCG
}
}
FallBack "Diffuse"
}
下面开始分析这串代码:
- 首先是第一行代码:
Shader "Ojors/SimpleShader"
这句代码表示我们的 Shader 目录,定义之后,我们可以在 Shader 列表中根据该目录找到我们的 Shader 文件。
- Properties 部分
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
在这里我们可以定义一些可以调节的参数,通过在 Unity Inspector 面板对属性进行调节,在 Scene 面板中实时观察效果。
上述代码中:
_MainTex 表示在 Shader 代码中定义的属性名
"Base (RGB)" 表示在 Inspector 面板中显示的名称
2D 表示该属性的类型,在此处为 ShaderLab 2D 贴图
"white" {} 表示该属性的默认值为了可以在我们的代码中使用该属性,我们还需要在我们的CG代码中定义该变量,该变量的类型和命名必须与属性定义一致。
sampler2D _MainTex;
有的Shader代码中会定义成:uniform sampler2D _MainTex;
在Unity Shader中,这两种写法效果一样,uniform 关键字是可以省略的
属性与变量类型关系如下:
属性类型 | CG变量类型 | 定义方式 |
---|---|---|
Int | int | _IntType ("Count", Int) = 1 |
Float | float,half,fixed | _FloatType("Rate", Float) = 1.5 |
Vector | float4,half4,fixed4 | _VectorType ("Vector", Vector) = (1,2,3,4) |
Range | float,half,fixed | _RangeType ("Range", (0.1, 0.5)) = 0.2 |
Color | float4,half4,fixed4 | _ColorType ("Color", Color) = (1,1,1,1) |
2D | sampler2D | _2DType ("2DTex", 2D) = ""{} |
Cube | samplerCube | _CubeType ("CubeTex", Cube) = "white"{} |
3D | sampler3D | _3DType ("3DTex", 3D) = "black"{} |
其中贴图属性的字符串要么为空(""),要么为内置的纹理名称("white", "black", "gray", "bump")
SubShader 块
在一个 Shader 中,可能会包含一个或多个 SubShader。Unity在加载 Shader 时,会对文件里的 SubShader 进行顺序扫描,然后选择第一个能在当前平台下运行的 SubShader,如果都不行,就会执行 Fallback 所指定的 ShaderPass 块
在一个 SubShader 中会有一个或多个 Pass。不同于 SubShader 在执行某个 SubShader 时,Unity 会把里面定义的 Pass 都执行一次。而多个 Pass 可以提高代码的复用率和实现一些复杂的效果.
UsePass "MyShader/PassName"
上面的使用方式可以对已经写好的 Pass 进行复用。
- 主体 Shader 代码部分
CGPROGRAM
...
ENDCG
在 CGPROGRAM-ENDCG 关键字中的代码就是我们用CG语言所写的 Shader 代码。
当然也可以 OpenGL 的 Shader 代码,那就要包含在 GLSLPROGRAM-ENDGLSL 关键字中。
#pragma vertex vert
#pragma fragment frag
这里定义了顶点着色器和片段着色器的函数声明,Unity 可以在这里知道哪里是顶点着色器的入口,哪里是片段着色器的入口。我们需要做的是在这两个函数里编写我们的代码。(这系列会以顶点着色器和片段着色器为主,而 Unity 新的表面着色器先不探讨)
#include "UnityCG.cginc"
这句代码表示包含 Unity 自带的 UnityCG.cginc 头文件,这里包含了 Unity 为我们提供的很多很使用的变量和常量,在现在的 Unity 版本(4.0以上)中会自动包含进来,但是为了兼容性还是加上为好。
sampler2D _MainTex;
这里为定义属性上对应的变量,也就是上面说过的 Properties 部分
struct VertexOut
{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
float2 uv_MainTex : TEXCOORD0;
};
这里定义了一个顶点着色器数据输出到片段着色器用的结构体,变量后的为语义
例如:float4 pos : SV_POSITION;
这里定义了一个 float4 类型的位置变量,语义为 SV_POSITION,表示这是一个坐标点(很多其他 Shader 代码中会使用 POSITION 语义,SV_ 前缀与没有前缀其实没有多大区别,但是为了平台兼容性我们还是使用 SV_ 前缀,例如 PS4 平台使用的就是带前缀的语义)
而下面的 COLOR0 和 TEXCOORD0 则表示是颜色和贴图,后面的数字0,1,2等等之类的表示这是第一组颜色(贴图),第二组颜色(贴图),具体的数字可以到多少要看显卡性能。
VertexOut vert(appdata_base i)
{
VertexOut o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.color = i.normal * 0.5 + float3(0.5, 0.5, 0.5);
o.uv_MainTex = i.texcoord.xy;
return o;
}
这个代码块为顶点着色器的代码块。返回类型为我们之前所定义的 VertexOut 结构体,参数类型为 UnityCG.cginc 自带的数据结构体,里面包含了很多数据相关的数值,例如:顶点、法线、贴图等,具体内容可以参照 Unity\Editor\Data\CGIncludes 目录下的 UnityCG.cginc 文件。
下面给出这三个常用的数据结构体:
struct appdata_base {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct appdata_tan {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API_XBOX360)
half4 texcoord4 : TEXCOORD4;
half4 texcoord5 : TEXCOORD5;
#endif
fixed4 color : COLOR;
};
然后是下面的代码:
VertexOut o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.color = i.normal * 0.5 + float3(0.5, 0.5, 0.5);
o.uv_MainTex = i.texcoord.xy;
return o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex):进行从模型空间到裁剪空间的矩阵变换,这个在前面的篇幅中已经说明过,这里不再赘述。
o.color = i.normal * 0.5 + float3(0.5, 0.5, 0.5) :这里是对顶点的颜色进行计算。
o.uv_MainTex = i.texcoord.xy:这里是对输入的贴图的 UV 信息对输出结构体进行赋值
float4 frag(VertexOut i) : SV_Target
{
return float4(i.color, 1.0) + tex2D(_MainTex, i.uv_MainTex);
}
这个代码块为片段着色器的代码块。其中输入的参数为从顶点着色器中返回的数据结构体。函数后面的 SV_Target 语义意思为输出的是颜色数据(一些 Shader 中会把语义写成 COLOR,跟上面说的一样,SV_Target 跟 COLOR 没有多大区别,基于平台兼容性我们还是选择 SV_Target 语义)
而返回的数据 float4(i.color, 1.0) + tex2D(_MainTex, i.uv_MainTex) 为对上面返回的颜色和贴图进行混合,生成最终的效果(tex2D 函数为取样函数,能从贴图中按照 UV 坐标获取到对应点的颜色,在低版本的显示驱动中不允许在顶点着色器中进行取样,若真要使用要使用 tex2Dlod 函数,并添加 #pragma target 3.0,因为 tex2Dlod 是 Shader Model 3.0 中的特性)
好了,到此为止,我们的第一个 Shader 文件已经解释完毕,下面上一下 Shader 效果图:
下面给出在书上看到的我觉得比较重要的优化点:
不鼓励在 Shader 中使用流程控制语句(if-else,for,while等),因为会降低 GPU 的并行处理效率。
几条建议:
- 把片段着色器上的计算放到顶点着色器中,或者在 CPU 中进行预计算
- 分支判断语句的条件最好是常量
- 每个分支中的操作尽可能少
- 分支嵌套尽可能少