Unity Shader入门精要学习笔记 - 第5章 开始 Unity Shader 学习之旅
2017-12-27 16:14
801 查看
Unity Shader入门精要学习笔记 - 第5章 开始 Unity Shader 学习之旅
本系列为UnityShader入门精要读书笔记总结,原作者博客链接:http://blog.csdn.net/candycat1992/article/
书籍链接:http://product.dangdang.com/23972910.html
第5章 开始 Unity Shader 学习之旅
5.1 使用的软件和系统
这些相关案例使用的Unity为5.6.1版本,系统为W10系统。5.2 一个最简单的顶点/片元着色器
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,顶点着色器的输出是裁剪空间中的顶点坐标。
保存并返回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 } }
保存并返回Unity查看结果,如下:
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语义块中的属性定义相匹配。
可以看到属性面板多了一个颜色调节。
5.3 强大的援手:Unity提供的内置文件和变量
为了方便开发者开发,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也提供了一些常用的帮助函数。下表给出了一些函数名和它们的描述
5.4 Unity提供的CG/HLSL语义
需要注意的是,Unity并没有支持所有的语义。通常情况下,这些输入输出变量并不需要有特别的意义,也就是说,我们可以自行决定这些变量的用途。例如在上面的代码中,定点着色器的输出结构体中,我们用COLOR0去描述color变量。color变量本身存储了什么,Shader流水线并不关心。其实任何东西都是数据,我们只是给他一个外在的名字,具体我们拿来这个数据怎么用,完全可以自己决定,只要格式是相同的。
如何定义一个复杂的变量类型?
struct v2f { float4 pos : SV_POSITION; fixed3 color0 : COLOR0; fixed4 color1 : COLOR1; half value0 : TEXCOORD0; float2 value1 : TEXCOORD1; };
5.5 程序员的烦恼:Debug
相关的博文连接:http://blog.csdn.net/u012632851/article/details/64124352
http://blog.csdn.net/wpapa/article/details/51204347
5.6 渲染平台的差异
Unity 在渲染平台的差异OpenGL 和 DirectX 在屏幕空间坐标存在差异,如下图
Shader的语法语义差异
更多的差异,可以查看官方文档
https://docs.unity3d.com/Manual/SL-PlatformDifferences.html
5.7 Shader整洁之道
在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。
相关文章推荐
- Unity Shader入门精要学习笔记 - 第5章 开始 Unity Shader 学习之旅
- Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照
- Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照
- UnityShader入门精要学习笔记(十九):卷积与边缘检测
- Unity Shader入门精要学习笔记 - 第11章 让画面动起来
- Unity Shader入门精要学习笔记 - 第2章 渲染流水线
- 学习UnityShader入门精要笔记1——渲染流程概述
- Unity Shader入门精要学习笔记 - 第16章 Unity中的渲染优化技术
- 凹凸映射 Bump mapping(unityshader入门精要学习笔记)
- Unity Shader入门精要笔记(二):Unity Shader基础
- Unity Shader入门精要学习笔记 - 第4章 学习 Shader 所需的数学基础
- UnityShader入门精要学习笔记(九):基础纹理之渐变纹理与遮罩纹理
- 【Unity Shader入门精要】— 开始Unity Shader之旅
- UnityShader入门精要学习笔记(十二):渲染路径与光源类型
- UnityShader入门精要学习笔记(二十一):深度和法线纹理
- Unity Shader入门精要学习笔记 - 第4章 学习 Shader 所需的数学基础
- Unity Shader入门精要学习笔记 - 第8章 透明效果
- Unity Shader入门精要学习笔记 - 第8章 透明效果
- Unity Shader入门精要学习笔记 - 第9章 更复杂的光照
- UnityShader入门精要学习笔记(十一):透明效果-下部分