URL
status
type
date
slug
summary
tag
category
password
icon
一个最简单的顶点/片元着色器
我们在3.3节已经看到了Unity Shader的基本结构。它包含了Shader、Properties、SubShader、Pass、Fallback等语义块。顶点/片元着色器的结构与之大体类似,它的结构如下:
其中,最重要的部分是Pass语义块,我们绝大部分的代码都是写在这个语义块里面的。
接下来让我们自己写一个Shader,用几句话概括操作步骤:
(1)创建一个Shader,写入
(2)新建一个材质,把刚才的Shader赋给它;
(3)新建一个球体,把材质赋给它,最后得到下图的结果;
代码详解
Shader
Shader代码的第一行通过Shader语义定义了Shader的名字——"Unity Shaders Book/Chapter 5/Simple Shader",应该保持一个好的命名规范,以便于为材质球选择Shader时的查找。在上面的代码中没有使用Properties语义,因为它不是必需的。
SubShader
使用默认的渲染设置和标签设置,没有加任何东西。
Pass
也没有加东西
CG代码块
#pragma vertex vert //顶点着色器 #pragma fragment frag //片元着色器
这两句代码用于指定 Unity ShaderLab 中的顶点和片元(像素)着色器的入口点。具体来说,它们告诉编译器哪个函数应该用作顶点着色器和片段着色器。
更通用的编译指令表示如下:
#pragma vertex name #pragma fragment name
其中name就是我们指定的函数名,这两个函数的名字不一定是vert和frag,它们可以是任意自定义的合法函数名,但我们一般使用vert和frag来定义这两个函数,因为它们很直观。
vert函数的定义
float4 vert(float4 v : POSITION) : SV_POSITION { return mul (UNITY_MATRIX_MVP, v); }
这就是顶点着色器代码,它是逐顶点进行的。
vert函数的输入参数v被POSITION语义指定为模型的顶点坐标。
函数的返回值是一个float4类型的变量,是该顶点在裁剪空间中的位置。
vert函数的输出由SV_POSITION指定为裁剪空间的顶点坐标。
POSITION和SV_POSITION都是Cg/HLSL中的语义(semantics),它们是不可省略的,这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么,如果没有它们,渲染器就会得到错误的效果。
mul (UNITY_MATRIX_MVP, v)的操作前面已经讲过,就是模型空间通过乘上Unity内置的模型·观察·投影矩阵(UNITY_MATRIX_MVP)转换到裁剪空间。
frag函数的定义
fixed4 frag() : SV_Target { return fixed4(1.0, 1.0, 1.0, 1.0); }
本例中的frag函数没有任何参数。
它的输出是一个用SV_Target语义限定的fixed4类型的变量。
SV_Target也是HLSL中的一个系统语义,它等同于告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。
片元着色器中的代码很简单,返回了一个表示白色的fixed4类型的变量。片元着色器输出的颜色的每个分量范围在[0, 1],其中(0, 0, 0)表示黑色,而(1, 1, 1)表示白色。
结构体的使用
上面的例子中,在顶点着色器函数的参数使用了POSITION得到了模型的顶点位置,如果想要得到更多的模型数据,就可以使用结构体。
在上面的代码中,我们声明了一个新的结构体a2v,它包含了顶点着色器需要的模型数据。在a2v的定义中,我们用到了更多Unity支持的语义,如NORMAL和TEXCOORD0,当它们作为顶点着色器的输入时都是有特定含义的,因为Unity会根据这些语义来填充这个结构体。对于顶点着色器的输入,Unity支持的语义有:POSITION, TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,COLOR等。
为了创建一个自定义的结构体,我们必须使用如下格式来定义它:
那么,填充到POSITION,TANGENT,NORMAL这些语义中的数据究竟是从哪里来的呢?在Unity中,它们是由使用该材质的Mesh Render组件提供的。在每帧调用Draw Call的时候,Mesh Render组件会把它负责渲染的模型数据发送给Unity Shader。
我们知道,一个模型通常包含了一组三角面片,每个三角面片由3个顶点构成,而每个顶点又包含了一些数据,例如顶点位置、法线、切线、纹理坐标、顶点颜色等。通过上面的方法,我们就可以在顶点着色器中访问顶点的这些模型数据。
实现顶点着色器和片元着色器之间的通信
在实践中,我们往往希望从顶点着色器输出一些数据,例如把模型的法线、纹理坐标等传递给片元着色器。这就涉及顶点着色器和片元着色器之间的通信。
为此,我们需要再定义一个新的结构体。修改后的代码如下:
至此,我们就完成了顶点着色器和片元着色器之间的通信。需要注意的是,顶点着色器是逐顶点调用的,而片元着色器是逐片元调用的。片元着色器中的输入实际上是把顶点着色器的输出进行插值后得到的结果。
使用属性(Properties)
材质提供给我们一个可以方便地调节Unity Shader中参数的方式,通过这些参数,我们可以在编辑器中随时调整材质的效果。而这些参数就需要写在Properties语义块中。
ShaderLab中属性的类型和Cg中变量的类型之间的匹配关系如下表所示。
ShaderLab属性类型 | Cg变量类型 |
Color, Vector | float, half4, fixed4 |
Range, Float | float, half, fixed |
2D | sampler2D |
Cube | samplerCube |
3D | sampler3D |
有时,读者可能会发现在Cg变量前会有一个uniform关键字,例如:
uniform关键词是Cg中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息(这和其他一些图像编程接口中的uniform关键词的作用不太一样)。在Unity Shader中,uniform关键词是可以省略的。
Unity提供的内置文件和变量
上一节讲述了如何在Unity中编写一个基本的顶点/片元着色器的过程。顶点/片元着色的复杂之处在于,很多事情都需要我们“亲力亲为”,例如我们需要自己转换法线方向,自己处理光照、阴影等。
为了方便开发者的编码过程,Unity提供了很多内置文件,这些文件包含了很多提前定义的函数、变量和宏等。如果读者在学习他人编写的Unity Shader代码时,遇到了一些从未见过的变量、函数,而又无法找到对应的声明和定义,那么很有可能就是这些代码使用了Unity内置文件提供的函数和变量。
内置的包含文件
包含文件(include file),是类似于C++中头文件的一种文件。在Unity中,它们的文件后缀是.cginc。在编写Shader时,我们可以使用#include指令把这些文件包含进来,这样我们就可以使用Unity为我们提供的一些非常有用的变量和帮助函数。例如:
那么,这些文件在哪里呢?我们可以在官方网站上选择下载 -> 内置着色器来直接下载这些文件,下图显示了由官网压缩包得到的文件。
可以看出,从官网下载的文件中包含了多个文件夹。其中,
- CGIncludes文件夹中包含了所有的内置包含文件;
- DefaultResources文件夹中包含了一些内置组件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader;
- DefaultResourcesExtra则包含了所有Unity中内置的Unity Shader;
- Editor文件夹目前只包含了一个脚本文件,它用于定义Unity 5引入的Standard Shader(详见第18章)所用的材质面板。
这些文件都是非常好的参考资料,在我们想要学习内置着色器的实现或是寻找内置函数的实现时,都可以在这里找到内部实现。但在本节中,我们只关注CGIncludes文件夹下的相关文件。
我们也可以从Unity的应用程序中直接找到CGIncludes文件夹。在Mac上,它们的位置是:/Applications/Unity/Unity.app/Contents/CGIncludes;
在Windows上,它们的位置是:Unity的安装路径/Data/CGIncludes。
下表给出了CGIncludes中主要的包含文件以及它们的主要用处。
文件名 | 描述 |
UnityCG.cginc | 包含了最常使用的帮助函数、宏和结构体等 |
UnityShaderVariables.cginc | 在编译Unity Shader时,会被自动包含进来。包含了许多口置的全局变量,如UNITY_MATRIX_MVP等 |
Lighting.cginc | 包含了各种内置的光照模型,如果编写的是Surface Shade
的话,会自动包含进来 |
HLSLSupport.cginc | 在编译Unity Shader时,会被自动包含进来。声明了很多后于跨平台编译的宏和定义 |
可以看出,有一些文件是即便我们没有使用#include
指令,它们也是会被自动包含进来的,例如UnityShaderVariables.cginc。因此,在前面的例子中,我们可以直接使用UNITY_MATRIX_ MVP变量来进行顶点变换。除了上表中列出的包含文件外,Unity 5引入了许多新的重要的包含文件,如UnityStandardBRDF.cginc、UnityStandardCore.cginc等,这些包含文件用于实现基于物理的渲染,我们会在第18章中再次遇到它们。
UnityCG.cginc是我们最常接触的一个包含文件。在后面的学习中,我们将使用很多该文件提供的结构体和函数,为我们的编写提供方便。例如,我们可以直接使用UnityCG.cginc中预定义的结构体作为顶点着色器的输入和输出。表5.3给出了一些结构体的名称和包含的变量。
名称 | 描述 | 包含的变量 |
appdata_base | 可用于顶点着色器的输入 | 顶点位置、顶点法线、第一组纹理坐标 |
appdata_tan | 可用于顶点着色器的输入 | 顶点位置、顶点切线、顶点法线、第一组纹理坐标 |
appdata_full | 可用于顶点着色器的输入 | 顶点位置、顶点切线、顶点法线、四组(或更多)
纹理坐标 |
appdata_img | 可用于顶点着色器的输入 | 顶点位置、第一组纹理坐标 |
v2f_img | 可用于顶点着色器的输出 | 裁剪空间中的位置、纹理坐标 |
除了结构体外,UnityCG.cginc也提供了一些常用的帮助函数。下表给出了一些函数名和它们的描述。
函数名 | 描述 |
float3 WorldSpaceViewDir (float4 v) | 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向 |
float3 ObjSpaceViewDir
(float4 v) | 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向 |
float3 WorldSpaceLightDir (float4 v) | 仅可用于前向渲梁中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化 |
float3 ObjSpaceLightDir
(float4 v) | 仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化 |
float3
UnityObjectToWorldNormal
(float3 norm) | 把法线方向从模型空间转换到世界空间中 |
float3 UnityObjectToWorldDir
(float3 dir) | 把方向矢量从模型空间变换到世界空间中 |
float3
UnityWorldToObjectDir(float3
dir) | 把方向矢量从世界空间变换到模型空间中 |
我们建议读者在UnityCG.cginc文件找到这些函数的定义,并尝试理解它们。一些函数我们完全可以自己实现,例如UnityObjectToWorldDir和UnityWorldToObjectDir,这两个函数实际上就是对方向矢量进行了一次坐标空间变换。而UnityCG.cginc文件可以帮助我们提高代码的复用率。UnityCG.cginc还包含了很多宏,在后面的学习中,我们就会遇到它们。
内置的变量
我们在4.8节给出了一些用于坐标变换和摄像机参数的内置变量。除此之外,Unity还提供了用于访问时间、光照、雾效和环境光等目的的变量。这些内置变量大多位于UnityShader Variables.cginc中,与光照有关的内置变量还会位于Lighting.cginc、AutoLight.cginc等文件中。当我们在后面的学习中遇到这些变量时,再进行详细的讲解。
Cg语义(semantics)
读者在平时的Shader学习中可能经常看到,在顶点着色器和片元着色器的输入输出变量后还有一个冒号以及一个全部大写的名称,例如在5.2节看到的SV_POSITION
、POSITION、COLOR0。这些大写的名字是什么意思呢?它们有什么用呢?
什么是语义
一个赋给Shader输入和输出参数的字符串,这个字符串表达了参数的含义。它们可以让Shader知道从哪里读取数据,并把数据输出到哪里,它们在Cg/HLSL的Shader流水线中是不可或缺的。需要注意的是,Unity并没有支持所有的语义。
如果读者从前接触过Cg/HLSL编程的话,可能对这些语义很熟悉。读者可以在微软的关于DirectX的文档中找到关于语义的详细说明页面。
一般语义
通常情况下,这些输入输出变量并不需要有特别的意义,也就是说,我们可以自行决定这些变量的用途。例如在上面的代码中,顶点着色器的输出结构体中我们用
COLOR0
语义去描述color变量。color变量本身存储了什么,Shader流水线并不关心。位置区别语义
而Unity为了方便对模型数据的传输,对一些语义进行了特别的含义规定。例如,在顶点着色器的输入结构体a2v用
TEXCOORD0
来描述texcoord,Unity会识别TEXCOORD0
语义,以把模型的第一组纹理坐标填充到texcoord中。需要注意的是,即便语义的名称一样,如果出现的位置不同,含义也不同。例如,
TEXCOORD0
既可以用于描述顶点着色器的输入结构体a2v,也可用于描述输出结构体v2v。但在输入结构体a2f中,TEXCOORD0有特别的含义,即把模型的第一组纹理坐标存储在该变量中,而在输出结构体v2f中,TEXCOORD0修饰的变量含义就可以由我们来决定。系统数值语义
在DirectX 10以后,有了一种新的语义类型,就是系统数值语义(system-value semantics)。这类语义是以SV开头的,SV代表的含义就是系统数值(system-value)。这些语义在渲染流水线中有特殊的含义。例如在上面的代码中,我们使用
SV_POSITION
语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的顶点坐标(即齐次裁剪空间中的坐标)。用这些语义描述的变量是不可以随便赋值的,因为流水线需要使用它们来完成特定的目的,例如渲染引擎会把用SV_POSITION修饰的变量经过光栅化后显示在屏幕上。读者有时可能会看到同一个变量在不同的Shader里面使用了不同的语义修饰。例如,一些Shader会使用POSITION
而非SV_POSITION来修饰顶点着色器的输出。SV_POSITION是DirectX 10中引入的系统数值语义,在绝大多数平台上,它和POSITION语义是等价的,但在某些平台(例如索尼 PS4)上必须使用SV_POSITION来修饰顶点着色器的输出,否则无法让Shader正常工作。同样的例子还有COLOR和SV_Target。因此,为了让我们的Shader有更好的跨平台性,对于这些有特殊含义的变量我们最好使用以SV开头的语义进行修饰。我们在5.6节中会总结更多这种因为平台差异而造成的问题。Unity支持的语义
下表总结了从应用阶段传递模型数据给顶点着色器时Unity使用的常用语义。这些语义虽然没有使用SV开头,但Unity内部赋予了它们特殊的含义。
语义 | 描述 |
POSITION | 模型空间中的顶点位置,通常是float4类型 |
NORMAL | 顶点法线,通常是float3类型 |
TANGENT | 顶点切线,通常是float4类型 |
TEXCOORDn, 如TEXCOORDO, TEXCOORD1 | 该顶点的纹理坐标,TEXCOORDO表示第一组纹理坐标,依此类推。通常是float2或float4类型 |
COLOR | 顶点颜色,通常是fxed4或foat4类型 |
其中TEXCOORDn中n的数目是和Shader Model有关的,例如一般在Shader Model 2(即Unity默认编译到的Shader Model版本)和Shader Model 3中,n等于8,而在Shader Model 4和Shader Model 5中,n等于16。
通常情况下,一个模型的纹理坐标组数一般不超过2,即我们往往只使用TEXCOORD0和TEXCOORD1。在Unity内置的数据结构体appdata_full中,它最多使用了6个坐标纹理组。
下表总结了从顶点着色器阶段到片元着色器阶段Unity支持的常用语义。
语义 | 描述 |
SV_POSITION | 裁剪空间中的顶点坐标,结构体中必须包含一个用该语义修饰的变量。
等同于DirectX 9中的POSITION,但最好使用SV_POSITION |
COLORO | 通常用于输出第一组顶点颜色,但不是必需的 |
COLOR1 | 通常用于输出第二组顶点颜色,但不是必需的 |
TEXCOORDO
~TEXCOORD7 | 通常用于输出纹理坐标,但不是必需的 |
上面的语义中,除了SV_POSITION是有特别含义外,其他语义对变量的含义没有明确要求,也就是说,我们可以存储任意值到这些语义描述变量中。通常,如果我们需要把一些自定义的数据从顶点着色器传递给片元着色器,一般选用TEXCOORD0等。
下表是片元着色器输出时Unity支持的常用语义
语义 | 描述 |
SV_Target | 输出值将会存储到渲染目标 (render target)中。等同于Direct 9中的COLOR语义,但最好使用SV_Target |
如何定义复杂的变量类型
上面提到的语义绝大部分用于描述标量或矢量类型的变量,例如fixed2、float、float4、fixed4等。下面的代码给出了一个使用语义来修饰不同类型变量的例子:
关于何时使用哪种变量类型,我们会在5.7.1节给出一些建议。但需要注意的是,一个语义可以使用的寄存器只能处理4个浮点值(float)。因此,如果我们想要定义矩阵类型,如float3×4、float4×4等变量就需要使用更多的空间。一种方法是,把这些变量拆分成多个变量,例如对于float4×4的矩阵类型,我们可以拆分成4个float4类型的变量,每个变量存储了矩阵中的一行数据。
调试Unity Shader
主要有三种方式,用于调试Unity Shader
假彩色图像
假彩色图像(false-color image)指的是用假彩色技术生成的一种图像。与假彩色图像对应的是照片这种真彩色图像
(true-color image)。一张假彩色图像可以用于可视化一些数据,那么如何用它来对Shader进行调试呢?
主要思想是,我们可以把需要调试的变量映射到[0, 1]之间,把它们作为颜色输出到屏幕上,然后通过屏幕上显示的像素颜色来判断这个值是否正确。读者心里可能已经在咆哮:“什么?!这方法也太原始了吧!”没错,这种方法得到的调试信息很模糊,能够得到的信息很有限,但在很长一段时间内,这种方法的确是唯一的可选方法。
需要注意的是,由于颜色的分量范围在[0, 1],因此我们需要小心处理需要调试的变量的范围。如果我们已知它的值域范围,可以先把它映射到[0, 1]之间再进行输出。如果你不知道一个变量的范围(这往往说明你对这个Shader中的运算并不了解),我们就只能不停地实验。
Visual Studio的Graphics Debugger
本节是Windows用户的福音,Mac用户的噩耗。Visual Studio作为Windows系统下的开发利器,在Visual Studio 2012版本中也提供了对Unity Shader的调试功能——Graphics Debugger。
通过Graphics Debugger,我们不仅可以查看每个像素的最终颜色、位置等信息,还可以对顶点着色器和片元着色器进行单步调试。具体的安装和使用方法可以参见Unity官网文档中使用Visual Studio对DirectX 11的Shader进行调试一文。
帧调试器(Frame Debugger)
尽管Mac用户无法体验Visual Studio的强大功能,但幸运的是,Unity 5除了带来全新的UI系统外,还给我们带来了一个新的针对渲染的调试器——帧调试器(Frame Debugger)。与其他调试工具的复杂性相比,Unity原生的帧调试器非常简单快捷。我们可以使用它来看到游戏图像的某一帧是如何一步步渲染出来的。要使用帧调试器,我们首先需要在Window -> Frame Debugger中打开帧调试器窗口,如图所示。
帧调试器可以用于查看渲染该帧时进行的各种渲染事件
(event),这些事件包含了Draw Call序列,也包括了类似清空帧缓存等操作。
帧调试器窗口大致可分为3个部分:最上面的区域可以开启/关闭(单击Enable按钮)帧调试功能,当开启了帧调试时,通过移动窗口最上方的滑动条(或单击前进和后退按钮),我们可以重放这些渲染事件;
左侧的区域显示了所有事件的树状图,在这个树状图中,每个叶子节点就是一个事件,而每个父节点的右侧显示了该节点下的事件数目。我们可以从事件的名字了解这个事件的操作,例如以Draw开头的事件通常就是一个Draw Call;当单击了某个事件时,在右侧的窗口中就会显示出该事件的细节,例如几何图形的细节以及使用了哪个Shader等。
同时在Game视图中我们也可以看到它的效果。如果该事件是一个Draw Call并且对应了场景中的一个GameObject,那么这个GameObject也会在Hierarchy视图中被高亮显示出来,下图显示了单击渲染某个对象的深度图事件的结果。
如果被选中的Draw Call是对一个渲染纹理(RenderTexture)的渲染操作,那么这个渲染纹理就会显示在Game视图中。而且,此时右侧面板上方的工具栏中也会出现更多的选项,例如在Game视图中单独显示R、G、B和A通道。
Unity 5提供的帧调试器实际上并没有实现一个真正的帧拾取(frame capture)的功能,而是仅仅使用停止渲染
的方法来查看渲染事件的结果。例如,如果我们想要查看第4个Draw Call的结果,那么帧调试器就会在第4个Draw Call调用完毕后停止渲染。这种方法虽然简单,但得到的信息也很有限。如果读者想要获取更多的信息,还是需要使用外部工具,例如5.5.2节中的Visual Studio插件,或者Intel GPA、RenderDoc、NVIDIA NSight、AMD GPU PerfStudio等工具。
平台差异对Shader的影响
Unity的优点之一是其强大的跨平台性——写一份代码可以运行在很多平台上。绝大多数情况下,Unity为我们隐藏了这些细节,但有些时候我们需要自己处理它们。本节给出了一些常见的因为平台不同而造成的差异。
在2.3.4节和4.2.2节中,我们都提到过OpenGL和DirectX的屏幕空间坐标的差异。在水平方向上,两者的数值变化方向是相同的,但在竖直方向上,两者是相反的。在OpenGL(OpenGL ES也是)中,(0, 0)点对应了屏幕的左下角,而在DirectX(Metal也是)中,(0, 0)点对应了左上角。下图可以帮助读者回忆它们之间的这种不同。
渲染纹理技术
需要注意的是,我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标(Render Target)中。这时,我们需要使用渲染纹理(Render Texture)来保存这些渲染结果。我们将在第12章中学习如何实现这样的目的。
DirectX→自动翻转
大多数情况下,这样的差异并不会对我们造成任何影响。但当我们要使用渲染到纹理技术,把屏幕图像渲染到一张渲染纹理中时,如果不采取任何措施的话,就会出现纹理翻转的情况。幸运的是,Unity在背后为我们处理了这种翻转问题——当在DirectX平台上使用渲染到纹理技术时,Unity会为我们翻转屏幕图像纹理,以便在不同平台上达到一致性。
抗锯齿+DirectX→无法自动翻转
在一种特殊情况下Unity不会为我们进行这个翻转操作,这种情况就是我们开启了抗锯齿(在Edit -> Project Settings -> Quality -> Anti Aliasing中开启)并在此时使用了渲染到纹理技术。在这种情况下,1.Unity首先渲染得到屏幕图像,2.再由硬件进行抗锯齿处理后,3.得到一张渲染纹理来供我们进行后续处理。4.此时,在DirectX平台下,我们得到的输入屏幕图像并不会被Unity翻转,也就是说,此时对屏幕图像的采样坐标是需要符合DirectX平台规定的。
单张渲染
如果我们的屏幕特效只需要处理一张渲染图像,我们仍然不需要在意纹理的翻转问题,这是因为在我们调用
Graphics.Blit
函数时,Unity已经为我们对屏幕图像的采样坐标进行了处理,我们只需要按正常的采样过程处理屏幕图像即可。多张渲染
但如果我们需要同时处理多张渲染图像(前提是开启了抗锯齿),例如需要同时处理屏幕图像和法线纹理,这些图像在竖直方向的朝向就可能是不同的(只有在DirectX这样的平台上才有这样的问题)。这种时候,我们就需要自己在顶点着色器中翻转某些渲染纹理(例如深度纹理或其他由脚本传递过来的纹理)的纵坐标,使之都符合DirectX平台的规则。例如:
- 其中,UNITY_UV_STARTS_AT_TOP用于判断当前平台是否是DirectX类型的平台,
- 而当在这样的平台下开启了抗锯齿后,主纹理的纹素大小在竖直方向上会变成负值,以方便我们对主纹理进行正确的采样。因此,我们可以通过判断_MainTex_TexelSize.y是否小于0来检验是否开启了抗锯齿。
- 如果是,我们就需要对除主纹理外的其他纹理的采样坐标进行竖直方向上的翻转。我们会在第13章中再次看到上面的代码。
基本的屏幕后处理效果不考虑平台差异
在本书资源的项目中,我们开启了抗锯齿选项。在第12章中,我们将学习一些基本的屏幕后处理效果。这些效果大多使用了单张屏幕图像进行处理,因此我们不需要考虑平台差异化的问题,因为Unity已经在背后为我们处理过了。
Bloom效果考虑平台差异
但在12.5节中,我们需要在一个Pass中同时处理屏幕图像和提取得到的亮部图像来实现Bloom效果。由于需要同时处理多张纹理,因此在DirectX这样的平台下如果开启了抗锯齿,主纹理和亮部纹理在竖直方向上的朝向就是不同的,我们就需要对亮部纹理的采样坐标进行翻转。
同时处理屏幕图像和深度/法线纹理考虑平台差异
在第13章中,我们需要同时处理屏幕图像和深度/法线纹理来实现一些特殊的屏幕效果,在这些处理过程中,我们也需要进行一些平台差异化处理。在15.3节中,尽管我们也在一个Pass中同时处理了屏幕图像、深度纹理和一张噪声纹理,但我们只对深度纹理的采样坐标进行了平台差异化处理,而没有对噪声纹理进行处理。这是因为,类似噪声纹理的装饰性纹理,它们在竖直方向上的朝向并不是很重要,即便翻转了效果往往也是正确的,因此我们可以不对这些纹理进行平台差异化处理。
语法差异
读者在Windows平台下编译某些在Mac平台下工作良好的Shader时,可能会看到类似下面的报错信息:
incorrect number of arguments to numeric-type constructor (compiling for d3d11) 数值类型构造函数的参数数量不正确(为d3d11编译)
output parameter 'o' not completely initialized (compiling for d3d11) 输出参数'o'未完全初始化(为d3d11编译)
上面的报错都是因为DirectX 9/11对Shader的语义更加严格造成的。
例如,造成第一个报错信息的原因是,Shader中可能存在下面这样的代码:
在OpenGL平台上,上面的代码是合法的,它将得到一个4个分量都是0.0的float4类型的变量。但在DirectX 11平台上,我们必须提供和变量类型相匹配的参数数目。也就是说,我们应该写成:
而对于第二个报错信息,往往是出现在表面着色器中。表面着色器的顶点函数(注意,不是顶点着色器)有一个使用了out修饰符的参数。如果出现这样的报错信息,可能是因为我们在顶点函数中没有对这个参数的所有成员变量都进行初始化。我们应该使用类似下面的代码来对这些参数进行初始化:
除了上述两点语法不同外,DirectX9/11也不支持在顶点着色器中使用tex2D函数。tex2D是一个对纹理进行采样的函数,我们在后面的章节中将会具体讲到。之所以DirectX9 /11不支持顶点阶段中的tex2D运算,是因为在顶点着色器阶段Shader无法得到UV偏导,而tex2D函数需要这样的偏导信息(这和纹理采样时使用的数学运算有关)。如果我们的确需要在顶点着色器中访问纹理,需要使用tex2Dlod函数来替代,如:
而且我们还需要添加
#pragma target 3.0
,因为tex2Dlod是Shader Model 3.0中的特性。语义差异
我们在5.4节讲到了Shader中的语义是什么,其中我们讲到了一些语义在某些平台下是等价的,例如SV_POSITION
和POSITION。但在另一些平台上,这些语义是不等价的。为了让Shader能够在所有平台上正常工作,我们应该尽可能使用下面的语义来描述Shader的输入输出变量。
- 使用SV_POSITION来描述顶点着色器输出的顶点位置。一些Shader使用了POSITION语义,但这些Shader无法在索尼PS4平台上或使用了细分着色器的情况下正常工作。
- 使用SV_Target来描述片元着色器的输出颜色。一些Shader使用了COLOR或者COLOR0语义,同样的,这些Shader无法在索尼PS4上正常工作。
其他差异
本书只给出了一些最常见的平台差异造成的问题,还有一些差异不再列举。如果读者发现一些Shader在平台A下工作良好,而在平台B下出现了问题,可以去Unity官方文档(http://docs. unity3d.com/Manual/SL-PlatformDifferences.html)中寻找更多的资料。
Shader一般规范
在本章的最后,我们给出一些关于如何规范Shader代码的建议。当然,这些建议并不是绝对正确的,读者可以根据实际情况做出权衡。写出规范的代码不仅是让代码变得漂亮易懂而已,更重要的是,养成这些习惯有助于我们写出高效的代码。
float、half还是fixed
在本书中,我们使用Cg/HLSL来编写Unity Shader中的代码。而在Cg/HLSL中,有3种精度的数值类型:float,half和fixed。这些精度将决定计算结果的数值范围。下表给出了这3种精度在通常情况下的数值范围。
类型 | 精度 |
float | 最高精度的浮点值。通常使用32位来存储 |
half | 中等精度的浮点值。通常使用16位来存储,精度范围是-60 000~+60 000 |
fixed | 最低精度的浮点值。通常使用11位来存储,精度范围是一2.0~+2.0 |
上面的精度范围并不是绝对正确的,尤其是在不同平台和GPU上,它们实际的精度可能和上面给出的范围不一致。通常来讲。
- 大多数现代的桌面GPU会把所有计算都按最高的浮点精度进行计算,也就是说,float、half、fixed在这些平台上实际是等价的。这意味着,我们在PC上很难看出因为half和fixed精度而带来的不同。
- 但在移动平台的GPU上,它们的确会有不同的精度范围,而且不同精度的浮点值的运算速度也会有所差异。因此,我们应该确保在真正的移动平台上验证我们的Shader。
- fixed精度实际上只在一些较旧的移动平台上有用,在大多数现代的GPU上,它们内部把fixed和half当成同等精度来对待。
尽管有上面的不同,但一个基本建议是,尽可能使用精度较低的类型,因为这可以优化Shader的性能,这一点在移动平台上尤其重要。从它们大体的值域范围来看,我们可以使用fixed类型来存储颜色和单位矢量,如果要存储更大范围的数据可以选择half类型,最差情况下再选择使用float。如果我们的目标平台是移动平台,一定要确保在真实的手机上测试我们的Shader,这一点非常重要。关于移动平台的优化技术,读者可以在第16章中找到更多内容。
规范语法
在5.6.2节,我们提到DirectX平台对Shader的语义有更加严格的要求。这意味着,如果我们要发布到DirectX平台上就需要使用更严格的语法。例如,使用和变量类型相匹配的参数数目来对变量进行初始化。
避免不必要的计算
如果我们毫无节制地在Shader(尤其是片元着色器)中进行了大量计算,那么我们可能很快就会收到Unity的错误提示:
temporary register limit of 8 exceeded 超过8个临时注册限制
Arithmetic instruction limit of 64 exceeded; 65 arithmetic instructions needed to compile program 超过64个算术指令限制;编译程序需要65个算术指令
出现这些错误信息大多是因为我们在Shader中进行了过多的运算,使得需要的临时寄存器数目或指令数目超过了当前可支持的数目。读者需要知道,不同的Shader Target、不同的着色器阶段,我们可使用的临时寄存器和指令数目都是不同的。
通常,我们可以通过指定更高等级的Shader Target来消除这些错误。表5.9给出了Unity目前支持的一些Shader Target。
指令 | 描述 |
#pragma target 2.0 | 默认的Shader Target等级。相当于Direct3D 9上的Shader Model 2.0,不支持对顶点纹理的采样,不支持显式的LOD纹理采样等 |
#pragma target 3.0 | 相当于Direct3D 9上的Shader Model 3.0,支持对顶点纹理的采样等 |
#pragma target 4.0 | 相当于Direct3D 10上的Shader Model 4.0,支持几何着色器等 |
#pragma target 5.0 | 相当于Direct3D 11上的Shader Model 5.0 |
需要注意的是,由于Unity版本的不同,Unity支持的Shader Target种类也不同,读者可以在官方手册上找到更为详细的介绍。
读者:什么是Shader Model呢?
我们:Shader Model是由微软提出的一套规范,通俗地理解就是它们决定了Shader中各个特性(feature)的能力(capability)。这些特性和能力体现在Shader能使用的运算指令数目、寄存器个数等各个方面。Shader Model等级越高,Shader的能力就越大。具体的细节读者可以参见本章的扩展阅读部分。
虽然更高等级的Shader Target可以让我们使用更多的临时寄存器和运算指令,但一个更好的方法是尽可能减少Shader中的运算,或者通过预计算的方式来提供更多的数据。
慎用分支和循环语句
过去的GPU并不支持if-else、for和while这种流程控制指令了,但随着GPU的发展,这变得可以实现,但它们在GPU与CPU上的实现技术不同,在GPU上运行流程控制指令,会降低GPU的并行处理操作,对其工作效率有负面影响。
如果在Shader中使用了大量的流程控制语句,如何降低它对于性能的影响呢?一个解决方法是,尽量把计算向流水线上端移动,把片元着色器的计算放到顶点着色器中,或者直接在CPU中进行预计算,最后将结果传给Shader。
在无可避免使用流程控制指令时,应尽量做到以下几点:
- 分支判断语句中使用的条件变量最好是常数,即在Shader运行过程中不会发生变化;
- 每个分支中包含的操作指令数尽可能少;
- 分支的嵌套层数尽可能少。
不要除以0
例如,我们在Shader里写下如下代码:
这样代码的结果往往是不可预测的。在某些渲染平台上,上面的代码不会造成Shader的崩溃,但即便不会崩溃得到的结果也是不确定的,有些会得到白色(由无限大截取到1.0),有些会得到黑色,但在另一些平台上,我们的Shader可能就会直接崩溃。因此,即便在开发游戏的平台上,我们看到的结果可能是符合预期的,但在目标平台上可能就会出现问题。
一个解决方法是,对那些除数可能为0的情况,强制截取到非0范围。在一些资料中,读者可能也会看到使用if语句来判断除数是否为0的例子。另一个方法是,使用一个很小的浮点值,例如0.000001来保证分母大于0(前提是原始数值是非负数)。
扩展阅读
读者可以在《GPU精粹2》中的GPU流程控制一章[1]
中更加深入地了解为什么流程控制语句在GPU上会影响性能。在5.7.3节我们提到了Shader中临时寄存器数目和运算指令都有限制,实际上Shader Model对顶点着色器和片元着色器中使用的指令数、临时寄存器、常量寄存器、输入/输出寄存器、纹理等数目都进行了规定。读者可以在Wiki的相关资料[2]和HLSL的手册[3]中找到更多的内容。
[1]Mark Harris, Ian Buck. "GPU Flow-Control Idioms." In GPU Gems 2. 中译本:GPU精粹2:高性能图形芯片和通用计算编程技巧,法尔译,清华大学出版社,2007年。
- 作者:Cloud
- 链接:https://cloud09.xyz/article/f36477e4-e155-45b3-8932-e2ae9f04abd2
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。