欢迎来到本书的第2 篇一一初级篇。在基础篇中,我们学习了渲染流水线, 并给出了Unity Shader 的基本概况, 同时还打下了一定的数学基础。从本章开始,我们将真正开始学习如何在Unity中编写Unity Shader 。
本章的结构如下:
在5.1 节, 我们将给出编写本书时使用的软件,包括Unity 的版本等。这是为了让读者可以在实践时不会出现因版本不同而造成困扰。在5.2 节,我们将看到一个最简单的顶点/片元着色器,并详细地解释这个顶点/片元着色器的组成结构。
5.3 节将介绍Unity 内置的Unity Shader 文件,以及提供给用户的一些包含文件、内置变量和函数等。
5.4 节则向读者阐述Unity Shader 中使用的CG 语义, 这是很多初学者容易困惑的地方。
在5.5 节中, 我们会介绍如何对Unity Shader 进行调试。
5.6 节将介绍平台差异对Unity Shader 的影响。
最后, 5.7 节将给出一些在编写Unity Shader 时很容易实现的优化技巧。为了让读者养成良好的编程习惯, 我们在这节也给出了一些建议。
-
Shader ” MyShaderName” {
-
Properties {
-
//属性
-
}
-
SubShader {
-
// 针对显卡A 的SubShader
-
Pass {
-
//设置渲染状态和标签
-
// 开始CG 代码片段
-
CGPROGRAM
-
//该代码片段的编译指令,例如:
-
#pragma vertex vert
-
#pragma fragment frag
-
-
//CG代码写在这里
-
-
ENDCG
-
//其他设置
-
}
-
//其他需要的Pass
-
}
-
SubShader {
-
//针对显卡B 的SubShader
-
}
-
//上述SubShader 都失败后用于回调的Unity Shader
-
Fallback ” VertexLit”
-
}
-
Shader
"Unity Shaders Book/Chapter 5/Simple Shader" {
-
SubShader {
-
Pass {
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
float4 vert(float4 v : POSITION) : SV_POSITION {
-
return mul(UNITY_MATRIX_MVP, v);
-
}
-
-
fixed4 frag() : SV_Target {
-
return fixed4(
1.0,
1.0,
1.0,
1.0);
-
}
-
-
ENDCG
-
}
-
}
-
}
保存并返回Unity 查看结果。
-
#pragma vertex vert
-
#pragma fragment frag
它们将告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。更通用的编译指令表示如下:
-
#pragma vertex name
-
#pragma fragment name
其中
name 就是我们指定的函数名,这两个函数的名字不一定是 vert 和 frag,它们可以是任意自定义的合法函数名,但我们一般使用vert 和 frag 来定义这两个函数,因为它们很直观。
-
float4 vert(float4 v : POSITION) : SV_POSITION {
-
return mul(UNITY_MATRIX_MVP, v);
-
}
这就是本例使用的顶点着色器代码,它是逐顶点执行的。vert 函数的输入v 包含了这个顶点的位置,这是通过POSITION 语义指定的。它的返回值是一个float4 类型的变量,它是该顶点在裁剪空间中的位置, POSITION 和SV_POSITION 都是CG/HLSL 中的语义( semantics ),它们是不可省略的,这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么。例如这里,POSITION将告诉Unity,把模型的顶点坐标填充到输入参数 v 中, SV_POSITION 将告诉Unity,顶点着色器的输出是裁剪空间中的顶点坐标。如果没有这些语义来限定输入和输出参数的话,渲染器就完全不知道用户的输入输出是什么,因此就会得到错误的效果。在5.4 节中,我们将总结这些语义。在本例中,顶点着色器只包含了一行代码,这行代码读者应该已经很熟悉了(起码对这个数学操作应该很熟悉了),这一步就是把顶点坐标从模型空间转换到裁剪空间中。UNITY_MATRIX_MVP 矩阵是Unity 内置的模型·观察·投影矩阵,我们在4.8 节已经见过它了。
-
fixed4 frag() : SV_Target {
-
return fixed4(
1.0,
1.0,
1.0,
1.0);
-
}
在本例中,frag 函数没有任何输入。它的输出是一个fixed4 类型的变量,并且使用了SV_Target语义进行限定。SV_Target 也是HLSL 中的一个系统语义,它等同于告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。片元着色器中的代码很简单,返回了一个表示白色的fixed4 类型的变量。片元着色器输出的颜色的每个分量范围在
-
Shader
"Unity Shaders Book/Chapter 5/Simple Shader" {
-
SubShader {
-
Pass {
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
uniform fixed4 _Color;
-
-
struct a2v {
-
float4 vertex : POSITION;
-
float3 normal : NORMAL;
-
float4 texcoord : TEXCOORD0;
-
};
-
-
// 使用一个结构体来定义顶点着色器的输入
-
struct a2v {
-
// POSITION 语义告诉Unity ,用模型空间的顶点坐标填充vertex 变量
-
float4 vertex : POSITION;
-
// NORMAL 语义告诉Unity ,用模型空间的法线方向填充normal 变量
-
float3 normal : NORMAL;
-
// TEXCOORDO 语义告诉Unity ,用模型的第一套纹理坐标填充texcoord 变量
-
float4 texcoord : TEXCOORDO;
-
};
-
-
float4 vert(a2v v) : SV_POSITION {
-
// 使用v.vertex 来访问在模型空间的顶点坐标
-
return mul(UNITY—_MATRIX_MVP, v.vertex);
-
}
-
-
fixed4 frag() : SV_Target {
-
return fixed4(
1.0,
1.0,
1.0,
1.0);
-
}
-
-
ENDCG
-
}
-
}
-
}
在上面的代码中,我们声明了一个新的结构体a2v,它包含了顶点着色器需要的模型数据。
-
struct StructName {
-
Type Name : Semantic;
-
Type Name : Semantic;
-
……
-
};
其中,语义是不可以被省略的。在5.4 节中,我们将给出这些语义的含义和用法。
-
Shader
"Unity Shaders Book/Chapter 5/Simple Shader" {
-
SubShader {
-
Pass {
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
struct a2v {
-
float4 vertex : POSITION;
-
float3 normal : NORMAL;
-
float4 texcoord : TEXCOORD0;
-
};
-
-
// 使用一个结构体来定义顶点着色器的输入
-
struct a2v {
-
// POSITION 语义告诉Unity ,用模型空间的顶点坐标填充vertex 变量
-
float4 vertex : POSITION;
-
// NORMAL 语义告诉Unity ,用模型空间的法线方向填充normal 变量
-
float3 normal : NORMAL;
-
// TEXCOORDO 语义告诉Unity ,用模型的第一套纹理坐标填充texcoord 变量
-
float4 texcoord : TEXCOORDO;
-
};
-
-
// 使用一个结构体来定义顶点着色器的输出
-
struct v2f {
-
// SV_POSITION 语义告诉Unity, pos 里包含了顶点在裁剪空间中的位置信息
-
float4 pos : SV POSITION ;
-
// COLORO 语义可以用于存储颜色信息
-
fixed3 color : COLORO;
-
};
-
-
float4 vert(a2v v) : SV_POSITION {
-
声明输出结构
-
v2f o;
-
// 使用v.vertex 来访问在模型空间的顶点坐标
-
o.pos = mul(UNITY—_MATRIX_MVP, 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_Target {
-
// 将插值后的i.color 显示到屏幕上
-
return fixed4(i.color,
1.0);
-
}
-
-
ENDCG
-
}
-
}
-
}
在上面的代码中,我们声明了一个新的结构体v2f。 v2f 用于在顶点着色器和片元着色器之间传递信息。同样的, v2f 中也需要指定每个变量的语义。在本例中,我们使用了SV_POSITION 和COLOR0 语义。顶点着色器的输出结构中,必须包含一个变量, 它的语义是SV_POSITION。否则,渲染器将无法得到裁剪空间中的顶点坐标,也就无法把顶点渲染到屏幕上。COLOR0 语义中的数据则可以由用户自行定义,但一般都是存储颜色,例如逐顶点的漫反射颜色或逐顶点的高光反射颜色。类似的语义还有COLOR1 等,具体可以详见5.4 节。
-
Shader
"Unity Shaders Book/Chapter 5/Simple Shader" {
-
Properties {
-
//声明一个Color 类型的属性
-
_Color (
"Color Tint", Color) = (
1,
1,
1,
1)
-
}
-
SubShader {
-
Pass {
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
// 在CG 代码中,我们需要定义-个与属性名称和类型都匹配的变量
-
uniform fixed4 _Color;
-
-
struct a2v {
-
float4 vertex : POSITION;
-
float3 normal : NORMAL;
-
float4 texcoord : TEXCOORD0;
-
};
-
-
struct v2f {
-
float4 pos : SV_POSITION;
-
fixed3 color : COLOR0;
-
};
-
-
v2f vert(a2v v) {
-
v2f o;
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
o.color = v.normal *
0.5 + fixed3(
0.5,
0.5,
0.5);
-
return o;
-
}
-
-
fixed4 frag(v2f i) : SV_Target {
-
fixed3 c = i.color;
-
// 使用Color 属性来控制输出颜色
-
c *= _Color.rgb;
-
return fixed4(c,
1.0);
-
}
-
-
ENDCG
-
}
-
}
-
}
在上面的代码中,我们首先添加了Properties 语义块中,并在其中声明了一个属性_Color,它的类型是_Color,初始值是
uniform fixed4 _Color;
unifom 关键词是CG 中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息(这和其他一些图像编程接口中的uniform 关键词的作用不太一样)。在Unity Shader 中, uniform 关键词是可以省略的。
-
CGPROGRAM
-
// ……
-
#include ”UnityCG.cginc”
-
// ……
-
ENDCG
那么,这些文件在哪里呢?我们可以在官方网站( http://unity3d.com/cn/get-unity/download/archive )上选择下载->内置着色器来直接下载这些文件,图5.3 显示了由官网压缩包得到的文件。
-
struct v2f {
-
float4 pos : SV_POSITION;
-
fixed3 colorO : COLORO;
-
fixed4 colorl : COLORl;
-
half valueO : TEXCOORDO;
-
float2 valuel : TEXCOORDl;
-
}
关于何时使用哪种变量类型,我们会在5.7.1 节给出一些建议。但需要注意的是, 一个语义可以使用的寄存器只能处理4 个浮点值( float)。因此,如果我们想要定义矩阵类型,如float3 × 4、float4 × 4 等变量就需要使用更多的空间。一种方法是,把这些变量拆分成多个变量,例如对于float4 × 4 的矩阵类型,我们可以拆分成4 个float4 类型的变量,每个变量存储了矩阵中的一行数据。
-
Shader
"Unity Shaders Book/Chapter 5/False Color" {
-
SubShader {
-
Pass {
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
#include "UnityCG.cginc"
-
-
struct v2f {
-
float4 pos : SV_POSITION;
-
fixed4 color : COLOR0;
-
};
-
-
v2f vert(appdata_full v) {
-
v2f o;
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
-
// Visualize normal
-
o.color = fixed4(v.normal *
0.5 + fixed3(
0.5,
0.5,
0.5),
1.0);
-
-
// Visualize tangent
-
o.color = fixed4(v.tangent.xyz *
0.5 + fixed3(
0.5,
0.5,
0.5),
1.0);
-
-
// Visualize binormal
-
fixed3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;
-
o.color = fixed4(binormal *
0.5 + fixed3(
0.5,
0.5,
0.5),
1.0);
-
-
// Visualize the first set texcoord
-
o.color = fixed4(v.texcoord.xy,
0.0,
1.0);
-
-
// Visualize the second set texcoord
-
o.color = fixed4(v.texcoord1.xy,
0.0,
1.0);
-
-
// Visualize fractional part of the first set texcoord
-
o.color = frac(v.texcoord);
-
if (any(saturate(v.texcoord) - v.texcoord)) {
-
o.color.b =
0.5;
-
}
-
o.color.a =
1.0;
-
-
// Visualize fractional part of the second set texcoord
-
o.color = frac(v.texcoord1);
-
if (any(saturate(v.texcoord1) - v.texcoord1)) {
-
o.color.b =
0.5;
-
}
-
o.color.a =
1.0;
-
-
// Visualize vertex color
-
// o.color = v.color;
-
-
return o;
-
}
-
-
fixed4 frag(v2f i) : SV_Target {
-
return i.color;
-
}
-
-
ENDCG
-
}
-
}
-
}
在上面的代码中, 我们使用了Unity 内置的一个结构体——appdata_full。我们在5.3 节讲过该结构体的构成。我们可以在UnityCG.cginc 里找到它的定义:
-
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;
-
};
可以看出, appdata_full 几乎包含了所有的模型数据。
-
#if UNITY_UV_STARTS_AT_TOP
-
if (_MainTex_TexelSize.y <
0)
-
uv.y =
1 - uv.y;
-
#endif
其中, UNITY_UV_STARTS_AT_TOP 用于判断当前平台是否是DirectX 类型的平台,而当在这样的平台下开启了抗锯齿后,主纹理的纹素大小在竖直方向上会变成负值,以方便我们对主纹理进行正确的采样。因此,我们可以通过判断 _MainTex_TexelSize.y 是否小于0 来检验是否开启了抗锯齿。如果是,我们就需要对除主纹理外的其他纹理的采样坐标进行竖直方向上的翻转。我们会在第13 章中再次看到上面的代码。
-
void vert (inout appdata_full v , out Input o) {
-
//使用Unity 内置的UNITY_INITIALIZE_OUTPUT 宏对输出结构体o进行初始化
-
UNITY_INITIALIZE_OUTPUT (Input, o);
-
// ……
-
}
除了上述两点语法不同外, DirectX9 / 11 也不支持在顶点着色器中使用tex2D 函数。tex2D是一个对纹理进行采样的函数,我们在后面的章节中将会具体讲到。之所以DirectX 9 / 11 不支持顶点阶段中的tex2D 运算,是因为在顶点着色器阶段Shader 无法得到UV 偏导,而tex2D 函数需要这样的偏导信息〈这和纹理采样时使用的数学运算有关)。如果我们的确需要在顶点着色器中访问纹理,需要使用tex2Dlod 函数来替代,如:
tex2Dlod(tex , float4(uv,0,0))
而且我们还需要添加句#pragma target 3.0 ,因为tex2Dlod 是Shader Model 3.0 中的特性。
-
fixed4 frag(v2f i) : SV_Target
-
{
-
return fixed4(
0.0/
0.0,
0.0/
0.0,
0.0/
0.0,
1.0);
-
}
这样代码的结果往往是不可预测的。在某些渲染平台上,上面的代码不会造成Shader 的崩溃,但即使不会崩溃得到的结果也是不确定的,有些会得到白色(由无限大截取到1.0 ),有些会得到黑色,但在另一些平台上,我们的Shader 可能就会直接崩溃。因此,即便在开发游戏的平台上,我们看到的结果可能是符合预期的,但在目标平台上可能就会出现问题。