第5章 开始 Unity Shader 学习之旅
注意:图片的来源基本来自作者冯乐乐的GitHub,感谢作者分享
https://github.com/candycat1992/Unity_Shaders_Book
Mac 使用的图象编程接口是 基于 OpenGL 的,而其他平台 如 Windows 是基于 DirectX。
OpenGL:(0,0)在左下角,
DirectX:(0,0)在左上角
Unity 5.x 版本以上 默认的天空盒子不为空,取消Unity内置的天空盒子:
Windows -> Lighting -> Skybox 将该项置空
Unity 提供的 CG/HLSL 语义:
语义实际上是一个赋给 Shader 输入和输出的字符串,这个字符串表达了这个参数的含义。
这些语义可以让 Shader 知道从哪里读取数据,并且把数据输出到哪里。
在 CG/HLSL 的 Shader流水线 中不可获取
注意:Unity没有支持所有的语义。
自行决定输入输出变量的用途,Shader流水线并不关心这些变量存储的是什么
Unity对一些语义进行特别含义规定,从而方便对于模型数据的传输(从顶点着色器到片元着色器的数据传输)。Unity 会对 输入结构体中的语义 进行识别,然后把模型数据填充到语义对应的变量中。
注意:即使是语义名称一样但是如果出现的位置不同,含义也会不同。TEXCOORD0 在描述顶点着色器的输入结构体 a2f 中,是把模型的第一组纹理坐标存储在这个变量中,但是在描述输出结构体 v2f 中,TEXCOORD0 修饰的变量的含义可以由开发者决定。
系统数值语义类型:SV
SV:系统数值
在渲染流水线中有特殊含义
SV_POSITION:修饰顶点着色器的输出变量 pos,那么就表示 pos 包含了可以用于光栅化的变换后的顶点坐标(即齐次裁剪空间中的坐标)。SV_POSITION 是 DirectX 10 中引入的系统数值语义。
系统数值语义描述的变量不可以随便赋值,因为流水线需要使用它们完成特定的目的。例如渲染引擎会把用 SV_POSITION 修饰的变量经过光栅化后显示在屏幕上。
在绝大部分平台,SV_POSITION 和 POSITION 等价。但是某些平台(索尼PS4)必须使用 SV_POSITION 修改顶点着色器的输出,否则Shader无法正常工作。
因此为了跨平台性,对于有特殊含义的变量最好使用 SV 开头的语义进行修饰。
从 应用阶段 到 顶点着色器阶段:
TEXCOORDn:
Shader Model 2(Unity 默认编译到的 Shader Model 版本):n 等于8
Shader Model 3:n 等于8
Shader Model 4:n 等于16
Shader Model 5:n 等于16
一个模型的纹理坐标数组一般不超过2,即一般使用 TEXCOORD0 和 TEXCOORD1
Unity 内置数据结构体 appdata_full,最多使用 6 个坐标纹理组
从 顶点着色器阶段 到 片元着色器阶段:
注意:SV_POSITION 是系统数值语义,有特殊含义,不可以随便存储任意值到这个语义描述变量中
如何定义复杂的变量类型:
注意:一个语义可以使用的寄存器只能处理4个浮点值(float)。因此如果想要定义矩阵类型(如 float3×4、float4×4等变量)就需要更多的空间。
解决方法:将这些需要更多空间的变量(如 矩阵类型变量)拆分成多个变量,例如对于float4×4的矩阵类型,可以拆分成4个float4类型的变量,每个变量都存储了矩阵中的一行数据。
使用语义来修饰不同类型变量的:
struct v2f {
float4 pos : SV_POSITION;
fixed3 color0 : COLOR0;
fixed4 color1 : COLOR1;
half value0 : TEXCOORD0;
float2 value1 : TEXCOORD1;
}
Unity中对 Unity Shader 的调试方法:
调试方法一:使用假彩色图像(false-color image):
使用假彩色技术生成的一种图像。可以用于可视化一些数据。
照片:真彩色图像(true-color image)
把需要调试的变量映射到 [ 0 , 1 ] 之间,然后作为颜色输出到屏幕上,最后通过屏幕上显示的像素颜色来判断这个值是否正确。由于分量范围 在 [ 0 , 1 ] 之间,因此需要小心处理需要调试的变量的范围。
如果已知值域范围:映射到 [ 0 , 1 ] 之间再进行输出
如果不知道值域范围:只能不停实验。但是颜色分量中任何大于1的数值会被设置为1,任何小于0的数值会被设置为0,因此可以尝试使用不同的映射直到颜色发生变化,颜色发生变化就意味着得到了0~1 之间的值。
非常原始的做法,调试信息模糊,得到信息有限。
调试的数据:
1、一维数组,对一个单独的颜色分量(如 R 分量) 进行输出,把其他颜色分量置为0.
2、多维数组,对每一个分量单独调试,或者 选择多个颜色分量进行输出
Unity的内置结构体:
appdata_full:几乎包含了所有的模型数据
调试方法二:利用 Visual Studio:
VS 中提供了对 Unity Shader 的调试:Graphics Debugger
可以看到每个像素的最终颜色、位置等信息,可以对顶点着色器和片元着色器进行单步调试。
限制:必须保证Unity运行在 DirectX 11 平台上,而且 Graphics Debugger 本身也存在bug
调试方法三:帧调试器(Frame Debugger)
针对渲染的调试器
可以看到游戏图象的某一帧是如何一步步渲染出来的
可以查看渲染该帧时进行的各种渲染事件(DrawCall序列,清空缓存等操作)
使用了停止渲染的方法来查看渲染事件的结果,没有实现真正的帧拾取(frame capture)的功能。
得到的信息有限,如果需要获得更多的信息就需要使用外部工具得到帧拾取功能(VS插件、Intel GPA、RenderDoc、NVIDIA NSight、AMD GPU PerfectStudio 等工具)
帧调试器的三个部分:
1、开启/关闭(Enable 按钮)帧调试功能
开启后:
1) 最上方的移动条(或者单击前进和后退按钮),可以重放渲染事件
2) 左侧区域显示 所有事件的树状图
每个叶子节点就是一个事件
每个父节点的右侧显示了这个节点下的事件数目
从事件的名字了解这个事件的操作(Draw开头的事件通常是一个DrawCall)
3) 点击树状图中的事件后,右侧窗口显示事件细节
几何图形的细节以及使用的Shader 等。
在Game 视图中也可以看到对应的效果
4) 如果点击的事件是一个 DrawCall,
如果是对应了场景中的一个 GameObject,那么这个 GameObject也会在 Hierarchy 视图总被高亮显示。
如果是对一个渲染纹理(RenderTexture)的渲染操作,那么这个渲染纹理就会显示在 Game 视图中,而且右侧面板上方的工具栏也会出现更多的渲染,例如在 Game 视图中单独显示 R、G、B 和 A 通道。
注意渲染平台的差异:
1、渲染纹理的坐标差异(OpenGL 和 DirectX 使用了不同的屏幕空间坐标)
OpenGL:(0,0)点对应了屏幕的左下角
DirectX:(0,0)点对应了屏幕的左上角
不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标(RenderTarget)中,然后使用 渲染纹理(Render Texture)保存渲染结果。
当使用渲染到纹理技术,把屏幕图象渲染到一张渲染纹理中的时候,一般都会出现纹理翻转的情况。但是 Unity为了达到不同平台的一致性,因此会在DirectX上使用渲染到纹理技术的时候会自动翻转屏幕图象处理,因此开发者无需专门针对进行翻转。
特殊情况:抗锯齿(Edit -> Project Settings -> Quality -> Anti Aliasing)
如果开启了抗锯齿的情况下,使用了渲染到纹理技术:
Unity首先渲染得到屏幕图象,再由硬件进行抗锯齿处理后,得到一张渲染纹理供开发者后续处理,但是此时在DirectX平台下,Unity不会自动翻转得到的输入屏幕图象。
1、如果屏幕特效只是需要处理一张渲染图象,无论是否开启了抗锯齿的情况,都不需要在意纹理的翻转问题,因为在调用 Graphics.Blit 函数的时候,Unity已经为开发者对屏幕图像的采样坐标进行处理,只需要按照正常的采样过程处理屏幕图像即可
2、但是如果在开启了抗锯齿的情况下,需要同时处理 多张渲染图像,例如需要同时处理屏幕图像和法线纹理,那么这些图像在竖直方向的朝向就可能是不同的(只有在Direct X这样的平台上才会有这样的问题),这时候就需要自己在顶点着色器上翻转某些渲染纹理的纵坐标,使之都符合DirectX平台的规则(比如深度纹理或者其他由脚本传递过来的纹理)。
//UNITY_UV_STARTS_AT_TOP 判断平台是否是 DirectX
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
//在DirectX平台下开启抗锯齿后主纹理的纹理大小在竖直方向上会变成负值,以方便对主纹理进行正确采样,因此可以判断是否小于0来检验是否开启了抗锯齿,如果是则需要对除了主纹理以外的其他纹理的采样坐标进行竖直方向上的翻转
uv.y = 1 - uv.y;
#endif
Shader 的语法差异:
相对于OpenGL,DirectX 9/11 对 Shader 的语义更加严格:
应该使用如下写法:
float4 v = float4( 0.0, 0.0, 0.0, 0.0 );
表面着色器中的顶点函数(注意不是顶点着色器)需要对参数的所有的成员变量都进行初始化:
void vert(inout appdata_full v, out Input o){
//使用Unity内置的 UNITY_INITIALIZE_OUTPUT 宏对输出结构体 o 进行初始化
UNITY_INITIALIZE_OUTPUT(Input, o);
}
DirectX 9/11 不支持在 顶点着色器中 使用 tex2D函数:
tex2D是对纹理进行采样的函数。
在 DirectX 9/11 的顶点着色器阶段, tex2D函数需要这样的UV 偏导信息(这和纹理采样时使用的数学运算有关),而此时的 Shader 无法得到 UV 偏导。如果的确需要在顶点着色器中访问纹理,就需要使用 tex2Dload 函数替代:
tex2Dlod(tex,float4(uv,0,0))
还需要添加编译指令,因为 tex2Dlod 是 Shader Model 3.0 中的特性
#pragma target 3.0
Shader 的整洁:规范 Shader 代码
1、在 CG/HLSL 中有3种精度的数值类型 float、half 和 fixed
精度决定了计算结果的数值范围
注意:在 不同的平台 和 GPU 上, float、half 和 fixed 这些精度范围可能会和表里面给的范围不一致
1、大多数现代的桌面GPU会把所有计算都按照最高的浮点精度进行计算,即:float、half、fixed 在这些平台上是等价的,在PC上很难看出因为 half 和 fixed 精度而带来的不同
2、在移动平台的GPU上,会有不同的精度范围,而且不同精度的浮点值的运算速度也会有差异,因此需要确保在真正的移动平台上验证Shader。
3、fixed 精度 实际上只是在一些较旧的移动平台上有用,在大多数现代的GPU上,GPU 内部会把 fixed 和 half 当成同等精度来对待
因此,1、尽量使用精度较低的类型,可以优化Shader的性能,这一点在移动平台上非常重要,
2、在真实的手机上测试编写的shader
规范语法:DirectX平台上需要使用更加严格的语法(例如使用和变量类型相匹配的参数数目来对变量进行初始化)
避免在Shader中(尤其是片元着色器中)进行不必要的计算,这样会使得需要的临时寄存器数目或者指令数目超过当前可以支持的数目。不同的Shader Target、不同的着色器阶段,可以使用的临时寄存器和指令数目都是不同的。
或者可以通过 预计算 的方式提供更多的数据。
指定更高等级的 Shader Target 可以消除这些过多运算造成的限制上的错误
注意:所有类似的 OpenGL 的平台(包括移动平台)被当成是支持到 Shader Model 3.0 。而 WP8/WinRT 平台则只是支持到 Shader Model 2.0
Shader Model:一个规范。这个规范决定了 Shader 中各个 特性 和 能力。
Shader Model 等级越高,Shader能够使用的运算指令数目、寄存器个数 等方面 的能力就越大。
尽量避免使用 分支 和 循环语句 :
不鼓励在Shader中( 顶点着色器 和 片元着色器 中) 使用流程控制语句。因为会降低 GPU 的并行处理操作。
if-else、for 和 while 这种流程控制指令,虽然现代的 GPU 已经可以使用这些流程控制紫菱,但是这些 GPU 使用不同于 CPU 的技术来实现分支语句,这样可能会造成最坏的情况下是花在一个分支语句的时间相当于运行了所有分支语句的时间。
使用大量的流程控制语句后,这个Shader 的性能可能会成倍下降。
应该把计算向流水线上端移动。如:把放在片元着色器中的计算放到顶点着色器中,或者直接在 CPU 中进行预计算,再把结果传递给 Shader。如果不可避免地要使用分支语句进行运算:
1、分支判断语句中使用的 条件变量 最好是常数,即在 Shader 运算过程中不会发生变化
2、每个分支中包含的操作指令数尽可能少
3、分支的嵌套层数尽可能少
关于运算:不要除以 0
在一些渲染平台上,除以 0.0 的操作不会造成 Shader 的崩溃,但是这样运算得到的结果不是确定的。
而在一些平台上,Shader 可能直接崩溃。
解决方法:对 除数为 0 的情况,强制截取到 非0 的范围。
如使用 if 语句判断除数 是否为0 的判断