本系列为UnityShader入门精要读书笔记总结,
原作者博客链接:http://blog.csdn.net/candycat1992/article/
书籍链接:http://product.dangdang.com/23972910.html
这些相关案例使用的Unity为5.6.1版本,系统为W10系统。
Shader "MyShaderName"
{
Properties
{
//属性
}
SubShader
{
//针对显卡A的SubShader
Pass
{
//设置渲染状态和标签
//开始CG代码片段
CGPROGRAM
//该代码的预编译指令,例如:
#pragma vertex vert
#pragma fragment frag
//CG代码写在这儿
ENDCG
//其他设置
}
}
SubShader
{
//针对显卡B的SubShader
}
}
其中,最重要的部分是Pass语义块,我们绝大部分的代码都是写在这个语义块里面的。下面我们来创建一个最简单的顶点/片元着色器。
1)新建一个场景,命名为Scene_5_2,如下:
可以看到,场景中已经包含了一个摄像机、一个平行光。而且场景的背景不是纯色的,而是一个天空盒子。我们可以Window->Lighting->SkyBox,把该项设置为空,去掉天空盒子。
2)新建一个Unity Shader,把它命名为Chapter5-SimpleShader。
3)新建一个材质球,把它命名为SimpleShaderMat。把第2步中新建的Unity Shader赋给它。
4)新建一个球体,拖拽它的位置以便在Game视图中更可以合适地显示出来。把第3步中新建的材质拖拽给它。
5)双击打开第2步中创建的Unity Shader。删除里面所有的代码。把下面的代码粘贴进去。
熟悉Unity的可以直接使用作者的源代码案例,直接删掉Shader部分,我们只实现Shader部分就好。
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
//定义了这个Unity Shader的名字
SubShader
{
//我们声明了SubShader 和 Pass 语义块
Pass
{
CGPROGRAM
//它们告诉Unity,哪个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码。其中vert和frag就是我们指定的函数名
#pragma vertex vert
#pragma fragment frag
//顶点着色器代码 :后边代表参数类型
float4 vert(float4 v : POSITION) : SV_POSITION
{
//顶点坐标从模型空间转换到裁剪空间中。
//UNITY_MATRIX_MVP 矩阵是unity 内置的模型·观察·投影矩阵。
//等同于return mul(UNITY_MATRIX_MVP, v);
return UnityObjectToClipPos(v);
}
//片元着色器代码
fixed4 frag() : SV_Target
{
//SV_Target 也是HLSL 中的一个系统语义,告诉渲染器,把用户的输出颜色存储到一个渲染目标中
//直接返回一个白色
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
POSITION 将告诉Unity,把模型的顶点坐标填充到参数v中,SV_POSITION 将告诉Unity,顶点着色器的输出是裁剪空间中的顶点坐标。
在顶点着色器我们使用了POSITION 语义得到了模型的顶点位置 。那么,如果我们想要得到更多模型数据怎么办呢?
现在,我们想得到模型上每个顶点的纹理坐标和法线方向。这个需求是很常见的,我们需要使用纹理坐标来访问纹理,而法线可用于计算光照。因此,我们需要为顶点着色器定义一个新的输入参数,这个参数不再是一个简单的数据类型,而是一个结构体。修改后的代码如下:
SubShader
{
//我们声明了SubShader 和 Pass 语义块
Pass
{
CGPROGRAM
//它们告诉Unity,哪个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码。其中vert和frag就是我们指定的函数名
#pragma vertex vert
#pragma fragment frag
//使用一个结构体来定义顶点着色器的输入
struct a2v
{
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertext变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
//TEXTCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充textcoord变量
//float4 texcoord : TEXTCOORD0;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0 语义可以用于存储颜色信息
fixed3 color : COLOR0;
};
//顶点着色器代码 :后边代表参数类型
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
}
}
a2v 中 a 表示应用,v 表示顶点着色器。a2v的意思是把数据从应用阶段传递到顶点着色中。
顶点着色器的输出结构中,必须包含一个变量,它的语义是 SV_POSITION 。否则,渲染器将无法得到裁剪空间中的顶点坐标。
在实践中,我们往往希望从顶点着色去输出一些数据。例如把模型的法线、纹理坐标等传递给片元着色器。这就涉及顶点着色器和片元着色器之间的通信。
v2f用于在顶点着色器和片元着色器之间传递信息。
现在,我们有了新的需求,我们想要在材质面板显示一个颜色拾取器,从而可以直接控制模型在屏幕上显示的颜色。为此,我们继续修改上面的代码。
Properties
{
//声明一个Color 类型的属性
_Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
}
//定义了这个Unity Shader的名字
SubShader
{
//我们声明了SubShader 和 Pass 语义块
Pass
{
CGPROGRAM
//它们告诉Unity,哪个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码。其中vert和frag就是我们指定的函数名
#pragma vertex vert
#pragma fragment frag
//在CG代码中,我们需要定义一个与属性名称和类型都匹配的变量
fixed4 _Color;
//使用一个结构体来定义顶点着色器的输入
struct a2v
{
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertext变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
//TEXTCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充textcoord变量
float4 texcoord : TEXCOORD0;
};
struct v2f
{
//SV_POSITION语义告诉Unity,pos 里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0 语义可以用于存储颜色信息
fixed3 color : COLOR0;
};
//顶点着色器代码 :后边代表参数类型
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
}
}
在上面的代码中,我们首先添加了Properties语义块中,并在其中声明了一个属性_Color,它的类型是Color,初始值是(1.0,1.0,1.0,1.0),对应白色。为了在CG代码中可以访问它,我们还需要再CG代码片段中提前定义一个新的变量,这个变量的名称和类型必须与Properties语义块中的属性定义相匹配。
可以看到属性面板多了一个颜色调节。
为了方便开发者开发,Unity提供了很多内置文件。这些文件包含了很多提前定义的函数、变量和宏等。
包含文件是类似于C++中头文件的一种文件。在Unity中,它的后缀是.cgnic。在编写Shader时,我们可以使用#include指令把这些文件包含进来,这样我们就可以使用Unity为我们提供的一些非常有用的变量和帮助函数。例如:
CGPROGRAM
//...
#include "UnityCG.cgnic"
//...
ENDCG
我们可以在官网(https://unity3d.com/cn/get-unity/download/archive)上选择下载->内置着色器来直接下载这些文件,下图显示了由官网压缩包得到的文件。
CGIncludes 文件夹中包含了所有的内置包含文件;
DefaultResouces 文件夹中包含了一些内置组件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader;
DefaultResourcesExtra 则包含了所有Unity中内置的Unity Shader;
Editor 文件夹目前只包含了一个脚本文件,它用于定义Unity5 引入的 Standard Shader 所用的材质面板
我们也可以从Unity应用程序中直接找到CGIncludes文件夹。在Windows上,它的位置是:
Unity的安装路径/Data/CGIncludes。
下表给出了CGIncludes 中主要包含文件以及它们的用处。
可以看出,有一些文件即使我们没有包含进来,Unity也会帮我们自己包含。
UnityCG.cgnic文件是我们最常接触的一个文件。下表给出了一些结构体的名称和包含的变量。
除了上述结构体外,UnityCG.cgnic也提供了一些常用的帮助函数。下表给出了一些函数名和它们的描述
需要注意的是,Unity并没有支持所有的语义。
通常情况下,这些输入输出变量并不需要有特别的意义,也就是说,我们可以自行决定这些变量的用途。例如在上面的代码中,定点着色器的输出结构体中,我们用COLOR0去描述color变量。color变量本身存储了什么,Shader流水线并不关心。其实任何东西都是数据,我们只是给他一个外在的名字,具体我们拿来这个数据怎么用,完全可以自己决定,只要格式是相同的。
如何定义一个复杂的变量类型?
struct v2f
{
float4 pos : SV_POSITION;
fixed3 color0 : COLOR0;
fixed4 color1 : COLOR1;
half value0 : TEXCOORD0;
float2 value1 : TEXCOORD1;
};
相关的博文连接:
http://blog.csdn.net/u012632851/article/details/64124352
http://blog.csdn.net/wpapa/article/details/51204347
Unity 在渲染平台的差异
OpenGL 和 DirectX 在屏幕空间坐标存在差异,如下图
Shader的语法语义差异
更多的差异,可以查看官方文档
https://docs.unity3d.com/Manual/SL-PlatformDifferences.html
在CG/HLSL中,有3种精度的数值类型:float,half和fixed。这些精度将决定计算结果的数值范围。下表给出了3种精度在通常情况下的数值范围。
一个基本建议是,尽可能使用精度较低的类型,因为这可以优化Shader 的性能,这一点在移动平台上尤其重要。从它们大体的值域范围来看,我们可以使用fixed 类型来存储颜色和单位矢量,如果要存储更大范围的精度可以选择half类型,最差情况下再选择使用float、如果我们的目标平台是移动平台,一定要确保在真实的手机上测试我们的Shader,这点非常重要。
Shader中进行了过多的运算,使得需要的临时寄存器数目或指令数目超过了当前可支持的数目。通常,我们可以通过制定更高级的Shader Target 来消除这些错误。下表给出了Unity 目前支持的Shader Target。
如果我们再Shader 中使用了大量的流程控制语句,那么这个Shader 的性能可能会成倍下降。一个解决方法是,我们应该尽量把计算向流水线上端移动,例如把放在片元着色器中的计算放到顶点着色器中,或者直接在CPU中进行预计算,再把结果传递给Shader。当然,有时我们不可避免地要使用分支语句来进行计算,那么一些建议是:
分支判断语句中使用的条件变量最好是常数,即在Shader运行过程中不会发生变化;
每个分支中包含的操作指令数尽可能少;
分支的嵌套层数尽可能少。
另外在Shader编程过程中,不要除以0。