主要参考《Unity Shader入门精要》一书,外加自己的一些总结
Shader "MyShaderName"
{
Properties{
//属性
}
SubShader{
//针对显卡A的SubShader
Pass{
//设置渲染状态和标签
//开始CG代码片段
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
//CG代码写在这里
ENDCG
}
//其他需要的Pass
}
SubShader{
//针对显卡B的SubShader
}
//上述SubShader都失败后用于回调的Unity Shader
Fallback "VertexLit"
}
其中,最重要的部分是Pass语义块,我们绝大部分的代码都是写在这个语义块里面的。下面来创建一个简单的顶点/片元着色器。
1,新建一个场景,命名为Scene_1,如下图所示:
可以看到,场景里包含了一个摄像机和一个平行光。场景的背景是一个天空盒子(Skybox)。这是因为Unity5.x版本中,默认的天空盒子不为空,而是Unity内置的一个天空盒子。为了得到更加原始的效果,我们去掉这个天空盒子。做法:Unity菜单中,Windows->Lighting->Settings,Skybox Material材质选为None。
2,新建一个Unity Shader;
3,新建一个材质,并将上一步的Unity Shader赋给这个材质;
4,场景里新建一个球体,把上一步的材质赋给这个球体。
打开第2步创建的Unity Shader,编写下面的代码:
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
SubShader{
//针对显卡A的SubShader
Pass{
//开始CG代码片段
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
//CG代码写在这里
float4 vert(float4 v : POSITION) : SV_POSITION{
return UnityObjectToClipPos(v);
}
fixed4 frag() : SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
//上述SubShader都失败后用于回调的Unity Shader
Fallback "VertexLit"
}
最后,得到
一个白色的球,如下图所示:
这是我们编写的第一个真正意义上的顶点/片元着色器,下面详细解释一下:
细心地人会发现,上面的代码并没有用到Properties语义块,没错,Properties语义并不是必需的,可以选择不声明任何材质属性。
接下来具体看一下vert函数的定义:
float4 vert(float4 v : POSITION) : SV_POSITION{
return UnityObjectToClipPos(v);
}
这是本例使用的顶点着色器代码,它是逐顶点执行的。vert 函数的输入v 包含了这个顶点的位置,这是通过POSITON语义指定的。它的返回值是一个float4类型的变量,也就是该顶点在剪裁空间中的位置。POSITION 和SV_POSITION都是CG/HLSL中的语义,是不可忽略的,这些语义告诉系统用户需要哪些输入值,以及用户的输出是什么。UnityObjectToClipPos,是将顶点坐标从模型空间转换到剪裁空间中。
fixed4 frag() : SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
frag 函数没有任何输入,它的输出是一个fixed4 类型的变量,并使用了SV_Target语义进行限定,它等于告诉渲染器,把用户的输出颜色存储到一个渲染 目标(render target)中,这里将输出到默认的帧缓存中。片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色。
在上面的例子中,在顶点着色器中使用POSITION语义得到了模型的顶点位置。那么,如果我们想要得到更多的模型数据该怎么办呢?
解决办法就是,我们为顶点着色器定义一个新的输入参数,这个参数不再是一个简单的数据类型,而是一个结构体。修改后的代码如下:
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
SubShader{
//针对显卡A的SubShader
Pass{
//开始CG代码片段
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
struct a2v {
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的发现方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
//CG代码写在这里
float4 vert(a2v v) : SV_POSITION{
return UnityObjectToClipPos(v.vertex);
}
fixed4 frag() : SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
//上述SubShader都失败后用于回调的Unity Shader
Fallback "VertexLit"
}
Unity支持的语义有:POSITON,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,COLOR等,Unity 会根据这些语义来填充这个结构体。
我们自定义的结构体的格式如下:(语义是不可省略的)
struct StructName {
Type Name : Semantic;
Type Name : Semantic;
Type Name : Semantic;
......
};
那么我们上面定义的结构名称 a2v 到底有什么含义,为什么起这个名字呢?
那么填充到POSITON等语义中的这些数据究竟从哪里来的呢?
在Unity中,它们是由使用该材质的Mesh Render组件提供的。在每帧调用Draw Call时,Mesh Render组件会把它负责渲染的模型数据发送给Unity Shader。我们知道,一个模型通常包含一组三角面片,每个三角面片由3的顶点构成,而每个顶点又包含了一些数据,如顶点位置、法线、切线、纹理坐标、顶点颜色等。通过上面的方法,我们就可以在顶点着色器中访问顶点的这些模型数据。
在实践中,我们往往希望从顶点着色器输出的一些数据(模型的法线、纹理坐标等)传递给片元着色器,这就涉及到顶点着色器和片元着色器之间的通信。因此,我们需要再定义一个新的结构体,来储存顶点着色器的输出数据,并作为片元着色器的输入参数。修改后的代码如下:
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
SubShader{
//针对显卡A的SubShader
Pass{
//开始CG代码片段
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
struct a2v {
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的发现方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
//使用一个结构体来定义顶点着色器的输出
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};
//CG代码写在这里
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_Target{
//将插值后的i.color 显示在屏幕上
return fixed4(i.color,1.0);
}
ENDCG
}
}
//上述SubShader都失败后用于回调的Unity Shader
Fallback "VertexLit"
}
上面的代码中,我们声明了一个新的结构体v2f,v2f 用于在顶点着色器和片元着色器之间传递信息,同样,v2f 中也需要指定每个变量的语义。顶点着色器的输出结构中,必须包含一个变量,它的语义是SV_POSITION。否则渲染器就无法得到剪裁空间中的顶点坐标,也就无法把顶点渲染到屏幕上。
至此,我们完成了顶点着色器和片元着色器之间的通信。需要注意的是,顶点着色器是逐顶点调用的,而片元着色器是逐片元调用的。片元着色器中的输入实际是把顶点着色器的输出进行插值后得到的结果。
如何使用属性
材质提供给我们一个可以方便调节Unity Shader中参数的方式,而这些参数就需要写在Properties语义块中。
现在,我们有了一个新的需求,我们想要在材质面板上显示一个颜色拾取器,来直接控制模型在屏幕上显示的颜色,修改后的代码如下:
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
Properties{
_Color("Color Tint",Color) = (1.0,1.0,1.0,1.0)
}
SubShader{
//针对显卡A的SubShader
Pass{
//开始CG代码片段
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
//在CG代码中,定义一个与属性名称和类型都匹配的变量
fixed4 _Color;
struct a2v {
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的发现方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
//使用一个结构体来定义顶点着色器的输出
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};
//CG代码写在这里
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_Target{
fixed3 c = i.color;
c *= _Color.rgb;
//将插值后的i.color 显示在屏幕上
return fixed4(c,1.0);
}
ENDCG
}
}
//上述SubShader都失败后用于回调的Unity Shader
Fallback "VertexLit"
}
上面的代码中,我们首先添加了Properties语义块,在这个语义块中声明了一个属性_Color,它的类型是Color,初始值是(1.0,1.0,1.0,1.0),对应白色。为了在CG代码中能访问到它,我们需要在CG代码片段中提前定义一个新的变量,这个变量的名称必须和Properties语义块中的属性定义一致。
ShaderLab中属性的类型和CG变量类型的匹配关系如下表: