书中提及所使用的软件及环境为:Unity 5+Mac OS X10.9.5
shader”MyShaderName” properties{ //shader’s properties } SubShader{ // Subshader for graphics card A Pass{ // set the render state and label //start the cg code snippet CGPROGRAM //compile instructions of the code snippet, such as: #pragma vertex vert # pragma fragment frag // here is the cg code ENDCG //the other settings } // the other parts which need the state of pass } SubShader{ //Subshader for graphic cardB } //if all the subshader fail, it will callback to the Unity Shader Fallback”VertexLit” }
简单案例步骤:
[1]利用UNITY新建场景
[2]新建一个Unity Shader,并命名为Chapter5-SimpleShader
[3]构建材质,命名为SimpleShaderMat,并把步骤2新建的shader附加给该材质
[4]新建一个GameObject是球体,并把该材质付给该对象
[5]打开步骤2新建的shader并删除里面的代码后,把下面代码粘贴
Shader”Unity Shaders Book/Chapter 5/Simple Shader”{ //通过shader语义定义该名字 //Properties语义不是必须的,所以可以选择不声明任何材质属性 SubShader{ //在本例子中不需要进行任何渲染设置和标签设置,从而设定默认渲染设置和标签设置 Pass{ //pass也一样,并无进行任何自定义的渲染设置和标签设置 CGPROGRAM #pragma vertex vert //该函数包含顶点着色器的代码,一般更通用的编译指令为:#pragma vertex name //name 则是该函数名字,一般使用vert来进行命名,不用这个名字也是可以的 #pragma fragment frag //该函数包含片元着色器的代码,一般更通用的编译指令为:#pragma fragment name //name 则是该函数名字,一般使用frag来进行命名 float4 vert(float4 v : POSITION) : SV_POSITION{ //输入v的顶点位置,position是告诉unity讲模型顶点坐标填充到输入参数v中,而sv_position是将顶点着色器的输出是裁剪空间中的顶点坐标 return mul(UNITY_MATRIX_MVP, v); //上文提及过该矩阵,这一步把顶点坐标从模型空间转换到裁剪空间中(MATRIX_MVP是模型-观察-投影矩阵) //通过调用该函数,输入v的顶点信息,继承SV_POSITION语义后,输出将会是该顶点的裁剪空间的顶点坐标 } fixed4 frag() : SV_Target{ //无输入,并使用SV_Target语义进行限定,其相当于告诉渲染器,把用户的输出颜色存储到一个渲染目标中,这里则将输出到默认的帧缓存中 return fixed4(1.0, 1.0, 1.0, 1.0); //返回一个白色的fixed4类型的变量(片元着色器输出的颜色的每个分量范围在[0,1],(0,0,0)表示黑色,(1,1,1)表示白色) } ENDCG } } }
图1.1.23 得到一个白色的圆
而如果想要得到更多的数据模型,则可以通过结构体struct来获取,将上述代码进行修改(关于结构体的使用可以百度,其相当于构建一个容器,往里面塞入数据,后期可通过调用该结构体来访问里面的数据模型,而struct与class有一定的区别):
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; //用模型空间的法线方向填充normal变量 float4 texture :TEXCOORD0; //用模型的第一套纹理坐标填充texcoord变量 }; float4 vert(a2v v) : SV_POSITION{ return mul(UNITY_MATRIX_MVP, v.vertex); //调用结构体v中的模型顶点坐标数据 } fixed4 frag() : SV_Target{ return fixed4(1.0, 1.0, 1.0, 1.0); ) } ENDCG } } }
(unity支持的语义有:POSITION、TANGENT(切线)、NORMAL、TEXCOORD0、TEXCOORD1、TEXCOORD2、TEXCOORD3、COLOR)
创建结构体的格式来进行定义:
struct name {
Type name : Semantic;
Type name : Semantic;
… …
}
往往开发者希望从顶点着色器输出一些数据,例如吧模型的法线、纹理坐标等传给片元着色器,此时将设计顶点着色器与片元着色器之间的通信,通过定义新的结构体可以解决该问题。把上述代码进行修改:
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 texture :TEXCOORD0; }; struct v2f{ //定义顶点着色器的输出 float4 pos : SV_POSITION; //pos里面包含顶点在裁剪空间中的位置信息 fixed3 color : COLOR0;//COLOR0语义可以用来存储颜色信息 }; v2f vert(a2v v) { //声明输出结构 v2f o; //构建名为o的vef结构体 o.pos = mul(UNITY_MATRI_MVP, v.vertex ); //九三裁剪空间中的位置信息后赋值给o.pos o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); //吧分量范围[-1,-1]映射到[0,1]后存储到o.color中传递给片元着色器 return o; //返回命名为o的结构体,也就是如果下次如果调用vef,则在其输入原有数据后通过计算得到后返回新的结构体,从而得到里面其中的新的数据 } fixed4 frag(v2f i) : SV_Target{ return fixed4(i.color, 1.0); // } ENDCG } } }
材质提供给开发者一个可以方便地调节Unity Shader中参数的方式,通过这些参数可以随时调整材质的效果,而该参数需要写在Properties语义块中。
开发者需在材质面板上显示一个颜色拾取器,从而可以直接控制模型在屏幕上显示的颜色,例:
Shader”Unity Shaders Book/Chapter 5/Simple Shader”{ Properties{ //声明一个Color类型的属性 _Color (“Color Tint”, Color) = (1.0,1.0,1.0,1.0) //声明了一个属性_Color,其类型为Color,初始值为(1.0,1.0,1.0,1.0),对应白色 } SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag //在Cg代码中,开发者需要定义一个与属性名称和类型都匹配的变量,目的在于为了能在Cg代码中可以访问它,因此需在Cg代码片段中提前定义一个新的变量,该变量的名称和类型必须与Properties语义块中的属性定义所对应 fixed4 _Color; struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texture :TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; fixed3 color : COLOR0; } v2f vert(a2v v) { v2f o; o.pos = mul(UNITY_MATRI_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 } } }
ShaderLab中属性的类型和Cg中变量的类型之间的匹配关系如下图所示:
图 1.24 匹配关系
而如果Cg变量前有一个uniform的关键字,如:uniform fixed4 _Color;
uniform关键词是Cg中修饰变量和参数的一种修饰词,其仅仅用于提供一些关于该变量的初始值是如何制定和存储的相关信息,在unity shader中,uniform可以被省略。
包含文件类似C++的头文件,不同于后缀是.h,它们的后缀为.cginc,包含文件书写方式:
CGPROGRAM //… … #include “UnityCg.cgnic” //… … ENDCG
文件可以在官方网站上选择下载内置着色器来得到。
CGincludes包含所有内置包含文件,位置如下:
图 1.25 位置
不同文件的主要作用如下图所示
图 1.26 作用
图 1.27 UnityCG.cginc中常用的结构体
图 1.28 UnityCG.cginc中常用的帮助函数
语义实际上是一个赋给shader输入和输出的字符串,该字符串表达了这个参数的含义,即这些语义可以让shader知道从哪里读取数据,并把数据输出到哪里。
这些输入输出并不需要有特别的意义,开发者可以自行决定这些变量的用途。如在顶点着色器的输出结构体中所使用COLOR0语义去描述color变量。而color变量本身存储什么shader流水线不关心。
系统数值语义由SV开头,SV代表的含义是系统数值(system-value)。例如上文中,SV_POSITION语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的顶点坐标(即其次裁剪空间中的坐标)。而有时候开发者可能会看到同一个变量在不同的shader中国使用了不同的语义进行修饰。如一些shader回使用POSITION而非SV_POSITION来修饰顶点着色器的输出。SV_POSITION是DirectX 10中引入的系统数值语义,在大多数平台上它和POSITION是等价的,而在一些平台(如PS4)上必须使用SV_POSITION来修饰顶点着色器的输出,否则无法让shader工作。同样例子还有COLOR与SV_Target。从而为了跨平台,对于特殊含义的变量最好以SV开头的语义进行修饰。
下图是Unity支持的语义。
图 1.29 从应用阶段传递模型数据给顶点着色器时Unity支持的语义
TEXCOORDn中n的数目是和shader model有关,一般在shader model2和3中,n=8;而在shader model4和5中,n=16。通常情况下,一个模型的纹理坐标组数一般不超过2,即我们往往只使用TEXCOORD0和1.
图 1.30 从顶点着色器传递数据给片元着色器时Unity使用的常用语义
图 1.31 片元着色器输出时Unity支持的常用语义
下面代码给出了一个使用语义来修饰不同类型变量的例子:
struct v2f{ float4 pos : SV_POSITION; fixed3 color0 : COLOR0; fixed4 color1 : COLOR1; half value 0 : TEXCOORD0; float2 value1 : TEXCOORD1; };
需要注意一个语义可以使用的寄存器只能处理4个浮点值,因此如果想要定义矩阵类型,如float3x4、float4x4等变量就需要使用更多的空间。一个方法是,把这些变量拆分成多个变量,例如对于float4x4的矩阵类型,可以拆分成4个float4类型的变量,每个变量存储了矩阵中的一行数据。
假彩色图像(false-color image)指的是用加彩色技术生成的一种图像,与假彩色图像对应的照片是真彩色图像。一张假彩色图像可以用于可视化一些数据。
实例中将会采用假彩色图像的方式来可视化一些模型数据:
Shader”Unity Shader Book/Chapter5/False Color”{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag #include”UnityCG.cgnic” struct v2f{ float4 pos : POSITION; fixed4 color : COLOR0; }; v2f vert(appdata_full v){ v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); //裁剪空间的顶点坐标 o.color = fixed4(v.normal * 0.5 + fixed3(0.5,0.5,0.5), 1.0); //可视化法线方向 o.color = fixed4(v.tangent.xyz * 0.5 + fixed3(0.5,0.5,0.5), 1.0); //可视化切线方向 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); //可视化副切线方向 o.color = fixed4(v.texcoord.xy, 0.0, 1.0); //可视化第一组纹理坐标 o.color = fixed4(v.texcoord1.xy, 0.0, 1.0); //可视化第二组纹理坐标 //可视化第一组纹理坐标的小数部分 o.color = frac(v.texcoord); if(any(saturate(v.texcoord) – v.texcoord)){ o.color.b = 0.5; } o.color.a = 1.0; //可视化第二组纹理坐标的小数部分 o.color = frac(v.texcoord1); if(any(saturate(v.texcoord1) – v.texcoord)){ o.color.b = 0.5; } o.color.a = 1.0; //可视化顶点颜色 //o.color.a = v.color; return 0; } fixed4 frag(v2f i) :SV_Target{ return i.color; } ENDCG } } }
在书的附带工程中有一个简单脚本ColorPicker.cs,把该脚本拖拽到一个摄像机上,单机运行后可以用鼠标点击屏幕以得到该点的颜色值。
同时也可以利用VS来检测,VS中有对于Unity Shader的调试功能—Graphic Debugger。
而对于mac用户来说,unity有针对渲染的调试器——帧调试器(Frame Debugger),unity原声的帧调试器非常简单快捷,开发者可以通过它来看到游戏图像的某一帧是如何一步步渲染出来。
使用帧调试器,可以在windows —>Frame Debugger中打开帧调试器窗口,如下图所示:
图 1.32 帧调试器
帧调试器可以用于查看渲染该帧时进行的各种渲染事件(event),该事件包括Draw Call序列,类似清空帧缓存等操作。而帧调试器窗口可分为三部分:最上面的区域开启/关闭(点击Enable按钮)帧调试功能,党开启了帧调试时,通过移动窗口最上方的滑动条可以重放渲染事件;左侧区域显示了所有事件的树状图,在该图中,每个叶子结点是一个事件,每个父节点的右侧显示了该节点下的事件数量;在Game视图中也可以看到它的效果。
在Cg/HLSL中有3种精度的数值类型:float、fixed、half。
图 1.33 精度类型
如果在shader中进行过多的运算,那么将会可能出现错误,使得需要的临时寄存器数目或指令数目超过了当前可支持的数目。不同的shader target、不同的着色器片阶段,开发者可使用的临时寄存器和指令数目都是不同的。
开发者可以通过指定更高等级的shader target来消除这些错误。其中shader model等级越高,shader的能力就越大。而尽可能减少shader中的运算或者通过预计算的方式来提供更多的数据可以有效避免产生不必要的计算错误。
图 1.34 Unity支持的shader target
谨慎分支和循环语句,而如果必不可免要使用分支语句来进行运算,则可以:
[1]分支判断语句中使用的条件变量最好是常数,即在shader运算过程中不会发生变化
[2]每个分支中包含的操作指令数尽可能少
[3]分支的嵌套层尽可能少
不要除0,如: return fixed4(0.0/0.0, 0.0/0.0, 0.0/0.0, 0.0/0.0,1.0)