大家好,我是阿赵。
这里通过手写一个最简单的shader,来介绍一下在Unity里面编写Shader的一些基础知识。
一、Shader基本结构
新建一个shader,把里面的内容都删掉,然后输入下面这些内容
shader "testShader"
{
Properties
{
}
SubShader
{
Pass
{
}
}
}
可以发现,现在这个Shader就已经能运作了,新建一个材质球,使用刚才写的shader,然后赋给一个Cube,可以看到Cube被正常的显示出来
只是这时候Cube的颜色是一片纯白,也没有光影的效果
分析一下上面的Shader结构,可以看到,
1、在最开始的shader单词后面跟着一个字符串shader “testShader”,那个testShaderr就是Shader的名字,这个名字用于Shader的查找,可以用斜杠作为目录,比如shader “azhao/testShader”
2、Properties结构体,这里是声明Shader对外显示的属性
3、SubShader结构体,子渲染器,一个Shader可以包含多个SubShader,至少要有一个
4、Pass结构体,语义块,通常一个Pass就代表了一次的渲染。一个SubShader里面可以包含多个Pass,至少要有一个Pass
二、对外属性
写在Properties结构体里面的属性,用于暴露给玩家修改的属性
类型:
1、数字类型
(1)Float:浮点
(2)Int:整型
(3)Range:数值范围
2、四元数和颜色
(1)Color
(2)Vector
都是用(number,number,number,number)的形式来表示
3、贴图类
(1)2D
(2)Cube
(3)3D
都是用”默认颜色”{}来表示
代码举例:
Properties
{
_floatVal(“浮点变量”,Float) = 0
_intVal(“整型变量”,Int) = 1
_rangeVal(“数值范围”,Range(0,1)) = 0
_col("颜色",Color) = (1,1,1,1)
_vectVal("四元数",Vector) = (0,0,0,0)
_2dTex("2D贴图",2D) = "white"{}
_cubeTex("Cube贴图",Cube) = "green"{}
_3dTex("3D贴图",3D) = "black"{}
}
效果:
可以看出,一行属性的构成是:变量名(“显示名称”,类型) = 默认值
定义了的变量,如果要在Pass里面使用,还需要在Pass里面再声明一次和Properties里面一样的名字,这个在下面CG代码再说明
三、CG代码和各种定义
1、CGPROGRAM结构
目前主流的着色器语言有HLSL,GLSL,Cg。三者在语法上也有诸多共通之处,选择一种学习即可。而Unity选择Cg作为着色器语言。如果要编写Cg代码,需要在Pass里面定义一个cg代码的开始和结束位置,使用CGPROGRAM开头,使用ENDCG结尾,夹在中间的就是cg代码了
2、顶点和片段程序
在上一步加入了没有内容的CG代码开始和结束标记之后,会发现shader出问题了,Unity给出了警告
第一条警告的意思大概是当前这个Shader是不被支持的,没有子渲染器或者fallback被支持,这是因为我们的CG代码部分不完整,Unity不知道我们想做什么。
第二条警告,是说我们使用了CG代码,但没有指定顶点(vertex)和片段(fragment)程序
定义顶点和片段程序的方法是:
#pragma vertex 顶点程序名称
#pragma fragment 片段程序的名称
比如我这样写:
Pass
{
CGPROGRAM
#pragma vertex azhaoVert
#pragma fragment azhaoFrag
ENDCG
}
这个时候,shader依然是报错的
这是因为我们声明了顶点和片段程序,但没有具体实现这两个程序。
这两个程序的具体写法会在下面说明。
说句题外话:结合之前讲的渲染管线流程,可以得知一个最基础的shader,应该要包括顶点片段程序,所以顶点片段程序是最基础和最核心的编写Shader方式,Unity还提供了其他的形式的Shader编写方式,比如Surface之类,其实我一直都不是很推荐一开始就使用的,因为那些都是Unity在基础的顶点片段程序基础上再封装了的形式,它使用可能会很方便,直接指定环境色、法线之类的值,就能看到不错的效果,但实际上一个shader他为什么能出现具体的效果,我个人感觉还是要在顶点片段程序里面才能比较好的体现出来。
3、引用库
Unity给我们准备了很多东西,比如一些常用的函数方法、常用的常量、转换的矩阵等。我们也可以自己写一些自己的函数以便以后直接使用。这些已经写好的东西,一般会放在cginc文件里面。有一个包含了很多常用函数的库,叫做UnityCG.cginc
通过#include “文件名.cginc”就可以导入。
Pass
{
CGPROGRAM
#pragma vertex azhaoVert
#pragma fragment azhaoFrag
#include "UnityCG.cginc"
ENDCG
}
4、声明可用变量
之前在shader开头的地方声明了一些对外的变量
Properties
{
_floatVal(“浮点变量”,Float) = 0
_intVal(“整型变量”,Int) = 1
_rangeVal(“数值范围”,Range(0,1)) = 0
_col("颜色",Color) = (1,1,1,1)
_vectVal("四元数",Vector) = (0,0,0,0)
_2dTex("2D贴图",2D) = "white"{}
_cubeTex("Cube贴图",Cube) = "green"{}
_3dTex("3D贴图",3D) = "black"{}
}
为了能在CG程序里面使用这些变量,所以我们要在CG程序里面再声明一次
Pass
{
CGPROGRAM
#pragma vertex azhaoVert
#pragma fragment azhaoFrag
#include "UnityCG.cginc"
float _floatVal;
float _intVal;
float _rangeVal;
float4 _col;
float4 _vectVal;
sampler2D _2dTex;
float4 _2dTex_ST;
samplerCUBE _cubeTex;
sampler3D _3dTex;
ENDCG
}
这里需要注意几点:
1.不论在外部声明了变量是float、int、Vector、color,在实际使用的时候,就只有float,差别只在于float是几维的,比如float、float2、float3、float4。
2.当float3要补全到float4时,如果原来的float3是坐标,则在后面补1,如果原来的float3是向量,则后面补0
3.贴图类型的变量,都是sampler采样器,区别在于是sampler2D、samplerCUBE、sampler3D
4.贴图类型的变量,如果想读取材质球上面的平铺和缩放参数,需要再定义一个float4,名字是贴图变量名字加_ST,比如上面的_2dTex_ST
这个ST变量的xy代表了Tiling,zw代表了Offset
四、顶点程序
顶点程序是一个有输入和输出结构体的程序,所以在编写顶点程序之前,需要先定义好输入和输出的结构体。
1、输入结构
一般习惯上,定义输入顶点程序的结构体的名字为appdata,它代表着可以直接从模型上面获取到的数据,比如
struct appdata
{
float4 pos : POSITION;//顶点坐标
float2 uv : TEXCOORD0;//uv1
float2 uv2 : TEXCOORD1;//uv2
float2 uv3 : TEXCOORD2;//uv3
float2 uv4 : TEXCOORD3;//uv4
float3 normal : NORMAL;//法线
float4 tangent:TANGENT;//切线
float4 color:COLOR;//顶点颜色
};
这些数据,一般会在模型上可以直接获取得到,但也不一定都有值。比如模型可能会没有uv2-uv4的信息,没有顶点颜色,之类。需要注意,这里的TEXCOORD是uv坐标的寄存器,最大支持4组UV。
在引用了UnityCG.cginc之后,可以直接使用一些已经定义好的输入结构:
struct appdata_base {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct appdata_tan {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
fixed4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
但我还是喜欢自己定义,因为可以根据自己的需要去增加减少需要读取的参数
2、输出结构
输出结构的名字一般习惯会命名为v2f,意思是vertex to fragment。因为这个结构体是顶点程序的输出结果,同时也是片段程序的输入数据。
struct v2f
{
float4 pos:SV_POSITION;
float4 col:COLOR;
float2 val1:TEXCOORD0;
float3 val2:TEXCOORD1;
float4 val3:TEXCOORD2;
//……
};
这里需要注意2个地方:
1.片段程序的pos定义的类型是SV_POSITION,而顶点程序定义的坐标是POSITION,SV_POSITION是输入片段程序固定的坐标使用类型。
2.片段程序里面的TEXCOORD不再是代表UV坐标了,每一个TEXCOORD寄存器都是一个Vector4,可以寄存任意你想存的自定义数据。比如我们可以写成这样,用TEXCOORD0保存uv坐标,TEXCOORD1保存世界空间法线,TEXCOORD2保存世界空间切线。
struct v2f
{
float4 pos:SV_POSITION;
float4 col:COLOR;
float2 uv:TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float4 worldTangent:TEXCOORD2;
};
3、编写顶点程序
v2f azhaoVert(appdata i)
{
v2f o;
//模型顶点坐标转世界空间坐标
float4 worldPos = mul(unity_ObjectToWorld, i.pos);
//世界空间顶点坐标转观察空间坐标
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
//观察空间坐标转裁剪空间坐标
float4 clipPos = mul(UNITY_MATRIX_P, viewPos);
o.pos = clipPos;
o.col = i.color;
o.uv = i.uv*_2dTex_ST.xy+ _2dTex_ST.zw;
o.worldNormal = UnityObjectToWorldNormal(i.normal);
o.worldTangent = UnityObjectToWorldDir(i.tangent);
return o;
}
说明:
1.顶点程序使用appdata结构体作为输入数据,v2f结构体作为输出数据,我个人习惯,appdata的变量命名为i(input),而v2f的变量命名为o(output)。
2.顶点程序主要控制和改变模型的顶点位置,例子里面用繁琐的方式表达了一个顶点坐标是怎样从模型坐标空间转换到裁剪空间的,如果我们有需要对顶点坐标进行修改,就可以根据需要在其中某一步进行修改,如果没有需要修改的地方,其实可以简写成
float4 clipPos = mul(UNITY_MATRIX_MVP, i.pos);
或者
float4 clipPos = UnityObjectToClipPos(i.pos );
3.在v2f结构体里面定义了多少个使用的变量,那么在顶点程序里面就要给多少个变量赋值。
4.为了使用uv的平铺和偏移,我把_2dTex_ST放进去计算了,这里其实可以简写成
o.uv = TRANSFORM_TEX(i.uv,_2dTex);
五、片段程序
写一个最简单的片段程序:
half4 azhaoFrag(v2f o) : SV_Target
{
return half4(1,1,1,1);
}
这个片段程序使用v2f作为输入数据结构,half4作为输出数据,SV_Target是DX10+用于fragment函数着色器颜色输出的语义。
由于这里只是展示一个格式,所以v2f结构体并没有参与运算,直接返回了一个half4(1,1,1,1)颜色。
如果我们写得详细点,比如把刚才的一些贴图参数和颜色参数用上,片段程序可以写成这样:
half4 azhaoFrag(v2f o) : SV_Target
{
half4 texCol = tex2D(_2dTex,o.uv);
half3 finalCol = texCol.rgb*_col.rgb;
return half4(finalCol, texCol.a);
}
这里使用了tex2D来采样了2D贴图,并获得它的颜色值,和之前定义的_col颜色值相乘,然后再返回结果。这样,我们就可以通过贴图和颜色来控制模型的外观了。
经过上面的编写,一个算是比较完整的shader就编写完了,现在整个shader代码是这样的:
shader "testShader"
{
Properties
{
_floatVal("浮点变量",Float) = 0
_intVal("整型变量",Int) = 1
_rangeVal("数值范围",Range(0,1)) = 0
_col("颜色",Color) = (1,1,1,1)
_vectVal("四元数",Vector) = (0,0,0,0)
_2dTex("2D贴图",2D) = "white"{}
_cubeTex("Cube贴图",Cube) = "green"{}
_3dTex("3D贴图",3D) = "black"{}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex azhaoVert
#pragma fragment azhaoFrag
#include "UnityCG.cginc"
float _floatVal;
float _intVal;
float _rangeVal;
float4 _col;
float4 _vectVal;
sampler2D _2dTex;
float4 _2dTex_ST;
samplerCUBE _cubeTex;
sampler3D _3dTex;
struct appdata
{
float4 pos : POSITION;//顶点坐标
float2 uv : TEXCOORD0;//uv1
float2 uv2 : TEXCOORD1;//uv2
float2 uv3 : TEXCOORD2;//uv3
float2 uv4 : TEXCOORD3;//uv4
float3 normal : NORMAL;//法线
float4 tangent:TANGENT;//切线
float4 color:COLOR;//顶点颜色
};
struct v2f
{
float4 pos:SV_POSITION;
float4 col:COLOR;
float2 uv:TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float3 worldTangent:TEXCOORD2;
};
v2f azhaoVert(appdata i)
{
v2f o;
//模型顶点坐标转世界空间坐标
float4 worldPos = mul(unity_ObjectToWorld, i.pos);
//世界空间顶点坐标转观察空间坐标
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
//观察空间坐标转裁剪空间坐标
float4 clipPos = mul(UNITY_MATRIX_P, viewPos);
o.pos = clipPos;
o.col = i.color;
o.uv = i.uv*_2dTex_ST.xy+ _2dTex_ST.zw;
o.worldNormal = UnityObjectToWorldNormal(i.normal);
o.worldTangent = UnityObjectToWorldDir(i.tangent);
return o;
}
half4 azhaoFrag(v2f o) : SV_Target
{
half4 texCol = tex2D(_2dTex,o.uv);
half3 finalCol = texCol.rgb*_col.rgb;
return half4(finalCol, texCol.a);
}
ENDCG
}
}
}
六、变量精度问题
对于数字变量,可以使用的精度有3种,分布是float,half,fixed,上面的例子里面就有用到了half和float的例子。他们的区别只在于精度,其他用法一样,比如表达多维时,可以是float4、half4、fixed4。
下面分别介绍一下:
1、float:32位浮点数,精度最高,一般用于世界坐标计算、需要高精度计算的变量
2、half:16位,取值范围是[-60000,+60000],精度为3位小数,一般用于本地坐标、方向向量、HDR颜色等
3、fixed:11位,取值范围是[-2,+2],进度是1/256,一般用于颜色、低精度运算等。
需要注意的是,fixed现在已经很少用到了,在很多手机硬件里面,half和fixed的精度其实是一样的。
七、背面剔除
Cull背面剔除模式:
Cull Back:默认,剔除背面
Cull Front:剔除正面
Cull Off:不进行剔除(双面显示)
直接写字SubShader里面或者Pass里面就可以了,比如
SubShader
{
Cull Off
Pass
{
……
或者
SubShader
{
Pass
{
Cull Off
……
这种枚举形式的模式选择,也可以暴露在变量里面让玩家选择
Properties
{
[Enum(UnityEngine.Rendering.CullMode)]
_cullMode("剔除模式",float) = 2
}
SubShader
{
Cull [_cullMode]
Pass
{
……
这样在材质球里面就可以直接选择剔除模式了。默认值是2,因为默认的Back的值就是2
八、透明度测试
AlphaTest透明度测试,当同一个片元多次写入透明度数据时,需要一个规则去判断最终显示哪一个信息
1、AlphaTest Off:不做测试,所有都通过
2、AlphaTest Greater Value:大于某个值的透明度才能通过
3、AlphaTest GEqual Value:大于等于某个值才能通过
4、AlphaTest Less Value:小于某个值才能通过
5、AlphaTest LEqual Value:小于等于某个值才能通过
6、AlphaTest Equal Value:等于某个值才能通过
7、AlphaTest NotEqual Value:不等于某个值才能通过
8、AlphaTest Always:等于off,所有情况都通过
9、AlphaTest Never:所有情况都不通过
替代用法:
AlphaTest GEqual 0.1
等效于
clip(color.a - 0.1f)
九、半透明混合
当需要渲染半透明混合的时候,我们需要做几件事情:
1、把深度写入关掉:
ZWrite Off
2、把渲染队列改成2500以上:
Tags{"Queue" = "Transparent"}
3、计算alpha值要控制在0-1之间
saturate(alpha)
4、使用Blend命令控制混合方式
格式:Blend SrcFactor DstFactor
其中
SrcFactor是源因子
DstFactor是目标因子
Blend因子有(为了便于说明,没有按枚举来排列):
1.One:使用源和目标的所有颜色
2.Zero:去除源和目标的所有颜色
3.SrcColor:乘以源的颜色值
4.SrcAlpha:乘以源的alpha值
5.SrcAlphaSaturate:乘以源的alpha值(0-1)
6.DstColor:乘以目标颜色值
7.DstAlpha:乘以目标alpha值
8.OneMinusSrcColor:乘以(1-源颜色值)
9.OneMinusSrcAlpha:乘以(1-源alpha值)
10.OneMinusDstColor:乘以(1-目标颜色值)
11.OneMinusDstAlpha:乘以(1-目标alpha值)
常用混合效果:
1、传统半透明
Blend SrcAlpha OneMinusSrcAlpha
2、预乘半透明
Blend One OneMinusSrcAlpha
3、叠加
Blend One One
4、柔和叠加
Blend OneMinusDstAlpha One
Blend SrcAlpha One
5、倍增
Blend DstColor Zero
6、2x倍增
Blend DstColor SrcColor
和之前介绍过的剔除模式一样,这些枚举同样可以在暴露属性里面给使用者选择
十、总结:
在最后,贴一下完整的shader:
shader "testShader"
{
Properties
{
_floatVal("浮点变量",Float) = 0
_intVal("整型变量",Int) = 1
_rangeVal("数值范围",Range(0,1)) = 0
_col("颜色",Color) = (1,1,1,1)
_vectVal("四元数",Vector) = (0,0,0,0)
_2dTex("2D贴图",2D) = "white"{}
_cubeTex("Cube贴图",Cube) = "green"{}
_3dTex("3D贴图",3D) = "black"{}
[Enum(UnityEngine.Rendering.CullMode)]
_cullMode("剔除模式",float) = 2
[Enum(UnityEngine.Rendering.BlendMode)]
_blend1("源因子",float) = 0
[Enum(UnityEngine.Rendering.BlendMode)]
_blend2("目标因子",float) = 0
}
SubShader
{
Cull[_cullMode]
ZWrite Off
Tags{"Queue" = "Transparent"}
Blend [_blend1] [_blend2]
Pass
{
CGPROGRAM
#pragma vertex azhaoVert
#pragma fragment azhaoFrag
#include "UnityCG.cginc"
float _floatVal;
float _intVal;
float _rangeVal;
float4 _col;
float4 _vectVal;
sampler2D _2dTex;
float4 _2dTex_ST;
samplerCUBE _cubeTex;
sampler3D _3dTex;
struct appdata
{
float4 pos : POSITION;//顶点坐标
float2 uv : TEXCOORD0;//uv1
float2 uv2 : TEXCOORD1;//uv2
float2 uv3 : TEXCOORD2;//uv3
float2 uv4 : TEXCOORD3;//uv4
float3 normal : NORMAL;//法线
float4 tangent:TANGENT;//切线
float4 color:COLOR;//顶点颜色
};
struct v2f
{
float4 pos:SV_POSITION;
float4 col:COLOR;
float2 uv:TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float3 worldTangent:TEXCOORD2;
};
v2f azhaoVert(appdata i)
{
v2f o;
//模型顶点坐标转世界空间坐标
float4 worldPos = mul(unity_ObjectToWorld, i.pos);
//世界空间顶点坐标转观察空间坐标
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
//观察空间坐标转裁剪空间坐标
float4 clipPos = mul(UNITY_MATRIX_P, viewPos);
o.pos = clipPos;
o.col = i.color;
o.uv = i.uv*_2dTex_ST.xy+ _2dTex_ST.zw;
o.worldNormal = UnityObjectToWorldNormal(i.normal);
o.worldTangent = UnityObjectToWorldDir(i.tangent);
return o;
}
half4 azhaoFrag(v2f o) : SV_Target
{
half4 texCol = tex2D(_2dTex,o.uv);
half3 finalCol = texCol.rgb*_col.rgb;
return half4(finalCol, texCol.a);
}
ENDCG
}
}
}