Unity Shader入门精要之Unity 提供的内置文件和变量

Unity系列文章目录

前言

上一节讲述了如何在Unity 中编写一个基本的顶点/片元着色器的过程。顶点/片元着色的复杂
之处在于,很多事情都需要我们“亲力亲为”,例如我们需要自己转换法线方向,自己处理光照、
阴影等。为了方便开发者的编码过程,Unity 提供了很多内置文件,这些文件包含了很多提前定
义的函数、变量和宏等。如果读者在学习他人编写的Unity Shader 代码时,遇到了一些从未见过
的变量、函数,而又无法找到对应的声明和定义,那么很有可能就是这些代码使用了Unity 内置
文件提供的函数和变量。
本节将给出这些文件和变量的概览。

5.3.1 内置的包含文件

包含文件(include file),是类似于C++中头文件的一种文件。在Unity 中,它们的文件后缀
是.cginc。在编写Shader 时,我们可以使用#include 指令把这些文件包含进来,这样我们就可以使
用Unity 为我们提供的一些非常有用的变量和帮助函数。例如:
CGPROGRAM
// …
#include “UnityCG.cginc”
// …
ENDCG
那么,这些文件在哪里呢?我们可以在官方网站(http://unity3d.com/cn/get-unity/download/
archive)上选择下载 -> 内置着色器来直接下载这些文件,图5.3 显示了由官网压缩包得到的文件。

在这里插入图片描述

从图5.3 中可以看出,从官网下载的文件中包含了多个文件夹。其中,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。

在这里插入图片描述
可以看出,有一些文件是即便我们没有使用#include 指令,它们也是会被自动包含进来的,
例如UnityShaderVariables.cginc。因此,在前面的例子中,我们可以直接使用UNITY_MATRIX_
MVP 变量来进行顶点变换。除了表5.2 中列出的包含文件外,Unity 5 引入了许多新的重要的包含
文件,如UnityStandardBRDF.cginc、UnityStandardCore.cginc 等,这些包含文件用于实现基于物理
的渲染,我们会在第18 章中再次遇到它们。
UnityCG.cginc 是我们最常接触的一个包含文件。在后面的学习中,我们将使用很多该文件提
供的结构体和函数,为我们的编写提供方便。例如,我们可以直接使用UnityCG.cginc 中预定义的
结构体作为顶点着色器的输入和输出。表5.3 给出了一些结构体的名称和包含的变量。

在这里插入图片描述
强烈建议读者找到UnityCG.cginc 文件并查看上述结构体的声明,这样的过程可以帮助我们快
速理解Unity 中一些内置变量的工作原理。
除了结构体外,UnityCG.cginc 也提供了一些常用的帮助函数。表5.4 给出了一些函数名和它
们的描述。

在这里插入图片描述
我们建议读者在UnityCG.cginc 文件找到这些函数的定义,并尝试理解它们。一些函数我们完
全可以自己实现,例如UnityObjectToWorldDir 和UnityWorldToObjectDir,这两个函数实际上就是
对方向矢量进行了一次坐标空间变换。而UnityCG.cginc 文件可以帮助我们提高代码的复用率。
UnityCG.cginc 还包含了很多宏,在后面的学习中,我们就会遇到它们。

5.3.2 内置的变量

我们在4.8 节给出了一些用于坐标变换和摄像机参数的内置变量。除此之外,Unity 还提供了
用于访问时间、光照、雾效和环境光等目的的变量。这些内置变量大多位于UnityShader
Variables.cginc 中,与光照有关的内置变量还会位于Lighting.cginc、AutoLight.cginc 等文件中。当
我们在后面的学习中遇到这些变量时,再进行详细的讲解。

二、Unity 提供的Cg/HLSL 语义

读者在平时的Shader 学习中可能经常看到,在顶点着色器和片元着色器的输入输出变量后还
有一个冒号以及一个全部大写的名称,例如在5.2 节看到的SV_POSITION、POSITION、COLOR0。
这些大写的名字是什么意思呢?它们有什么用呢?
5.4.1 什么是语义
实际上,这些是Cg/HLSL 提供的语义(semantics)。如果读者从前接触过Cg/HLSL 编程的
话,可能对这些语义很熟悉。读者可以在微软的关于DirectX 的文档(https://msdn.microsoft.com/
en-us/library/windows/desktop/bb509647(v=vs.85).aspx#VS)中找到关于语义的详细说明页面。根据
文档我们可以知道,语义实际上就是一个赋给Shader 输入和输出的字符串,这个字符串表达了这
个参数的含义。通俗地讲,这些语义可以让Shader 知道从哪里读取数据,并把数据输出到哪里,
它们在Cg/HLSL 的Shader 流水线中是不可或缺的。需要注意的是,Unity 并没有支持所有的语义。
通常情况下,这些输入输出变量并不需要有特别的意义,也就是说,我们可以自行决定这些
变量的用途。例如在上面的代码中,顶点着色器的输出结构体中我们用COLOR0 语义去描述color
变量。color 变量本身存储了什么,Shader 流水线并不关心。
而Unity 为了方便对模型数据的传输,对一些语义进行了特别的含义规定。例如,在顶点着
色器的输入结构体a2v 用TEXCOORD0 来描述texcoord,Unity 会识别TEXCOORD0 语义,以把
模型的第一组纹理坐标填充到texcoord 中。需要注意的是,即便语义的名称一样,如果出现的位
置不同,含义也不同。例如,TEXCOORD0 既可以用于描述顶点着色器的输入结构体a2v,也可
用于描述输出结构体v2f。但在输入结构体a2v 中,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 节中会总结更多这种因为平台差异而造成的问题。
5.4.2 Unity 支持的语义
表5.5 总结了从应用阶段传递模型数据给顶点着色器时Unity 使用的常用语义。这些语义虽
然没有使用SV 开头,但Unity 内部赋予了它们特殊的含义。

在这里插入图片描述
其中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 个坐标纹理组。
表5.6 总结了从顶点着色器阶段到片元着色器阶段Unity 支持的常用语义。

在这里插入图片描述

上面的语义中,除了SV_POSITION 是有特别含义外,其他语义对变量的含义没有明确要求,
也就是说,我们可以存储任意值到这些语义描述变量中。通常,如果我们需要把一些自定义的数
据从顶点着色器传递给片元着色器,一般选用TEXCOORD0 等。
表5.7 给出了Unity 中支持的片元着色器的输出语义。

在这里插入图片描述
5.4.3 如何定义复杂的变量类型
上面提到的语义绝大部分用于描述标量或矢量类型的变量,例如fixed2、float、float4、fixed4

等。下面的代码给出了一个使用语义来修饰不同类型变量的例子:
struct v2f {
float4 pos : SV_POSITION;
fixed3 color0 : COLOR0;
fixed4 color1 : COLOR1;
half value0 : TEXCOORD0;
float2 value1 : TEXCOORD1;
};
关于何时使用哪种变量类型,我们会在5.7.1 节给出一些建议。但需要注意的是,一个语义可
以使用的寄存器只能处理4 个浮点值(float)。因此,如果我们想要定义矩阵类型,如float3×4、
float4×4 等变量就需要使用更多的空间。一种方法是,把这些变量拆分成多个变量,例如对于
float4×4 的矩阵类型,我们可以拆分成4 个float4 类型的变量,每个变量存储了矩阵中的一行数据。

5.5 程序员的烦恼:Debug

有这样一个笑话,据说只有程序员才能看懂:

问:程序员最讨厌康熙的哪个儿子?
答:胤禩。因为他是八阿哥(谐音:bug)。
调试(debug),大概是所有程序员的噩梦。而不幸的是,对一个Shader 进行调试更是噩梦中
的噩梦。这也是造成Shader 难写的原因之一—如果发现得到的效果不对,我们可能要花非常多
的时间来找到问题所在。造成这种现状的原因就是在Shader 中可以选择的调试方法非常有限,甚
至连简单的输出都不行。
本节旨在给出Unity 中对Unity Shader 的调试方法,这主要包含了两种方法。
5.5.1 使用假彩色图像
假彩色图像(false-color image)指的是用假彩色技术生成的一种图像。与假彩色图像对应的
是照片这种真彩色图像(true-color image)。一张假彩色图像可以用于可视化一些数据,那么如
何用它来对Shader 进行调试呢?
主要思想是,我们可以把需要调试的变量映射到[0, 1]之间,把它们作为颜色输出到屏幕上,
然后通过屏幕上显示的像素颜色来判断这个值是否正确。读者心里可能已经在咆哮:“什么?!这
方法也太原始了吧!”没错,这种方法得到的调试信息很模糊,能够得到的信息很有限,但在很长
一段时间内,这种方法的确是唯一的可选方法。
需要注意的是,由于颜色的分量范围在[0, 1],因此我们需要小心处理需要调试的变量的范围。
如果我们已知它的值域范围,可以先把它映射到[0, 1]之间再进行输出。如果你不知道一个变量的
范围(这往往说明你对这个Shader 中的运算并不了解),我们就只能不停地实验。一个提示是,
颜色分量中任何大于1 的数值将会被设置为1,而任何小于0 的数值会被设置为0。因此,我们可
以尝试使用不同的映射,直到发现颜色发生了变化(这意味着得到了0~1 的值)。
如果我们要调试的数据是一个一维数据,那么可以选择一个单独的颜色分量(如R 分量)进
行输出,而把其他颜色分量置为0。如果是多维数据,可以选择对它的每一个分量单独调试,或
者选择多个颜色分量进行输出。
作为实例,下面我们会使用假彩色图像的方式来可视化一些模型数据,如法线、切线、纹理
坐标、顶点颜色,以及它们之间的运算结果等。我们使用的代码如下:

Shader "Unity Shaders Book/Chapter 5/False Color" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_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.texcoord1)) {
o.color.b = 0.5;
}
o.color.a = 1.0;
// 可视化顶点颜色
//o.color = v.color;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return i.color;
}
ENDCG
}
}
}```


在上面的代码中,我们使用了Unity 内置的一个结构体—appdata_full。我们在5.3 节讲过
该结构体的构成。我们可以在UnityCG.cginc 里找到它的定义:

```cpp
struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API_XBOX360)
half4 texcoord4 : TEXCOORD4;
half4 texcoord5 : TEXCOORD5;
#endif
fixed4 color : COLOR;
};

可以看出,appdata_full 几乎包含了所有的模型数据。
我们把计算得到的假彩色存储到了顶点着色器的输出结构体—v2f 中的color 变量里,并且
在片元着色器中输出了这个颜色。读者可以对其中的代码添加或取消注释,观察不同运算和数据
得到的效果。图5.4 给出了这些代码得到的显示效果。读者可以先自己想一想代码和这些效果之
间的对应关系,然后再在Unity 中进行验证。
为了可以得到某点的颜色值,我们可以使用类似颜色拾取器的脚本得到屏幕上某点的RGBA
值,从而推断出该点的调试信息。在本书的附带工程中,读者可以找到这样一个简单的实例脚本:
Assets -> Scripts -> Chapter5 -> ColorPicker.cs。把该脚本拖曳到一个摄像机上,单击运行后,可以
用鼠标单击屏幕,以得到该点的颜色值,如图5.5 所示。

在这里插入图片描述
5.5.2 利用神器:Visual Studio
本节是Windows 用户的福音,Mac 用户的噩耗。Visual Studio 作为Windows 系统下的开发利
器,在Visual Studio 2012 版本中也提供了对Unity Shader 的调试功能—Graphics Debugger。
通过Graphics Debugger,我们不仅可以查看每个像素的最终颜色、位置等信息,还可以对顶
点着色器和片元着色器进行单步调试。具体的安装和使用方法可以参见Unity 官网文档中使用
Visual Studio 对DirectX 11 的Shader 进行调试一文(http://docs.unity3d.com/Manual/SL-Debugging
D3D11ShadersWithVS.html)。
当然,本方法也有一些限制。例如,我们需要保证Unity 运行在DirectX 11 平台上,而且Graphics
Debugger 本身存在一些bug。但这无法阻止我们对它的喜爱之情!而Mac 用户可能就只能无奈地眼馋了。
5.5.3 最新利器:帧调试器
尽管Mac 用户无法体验Visual Studio 的强大功能,但幸运的是,Unity 5 除了带来全新的UI
系统外,还给我们带来了一个新的针对渲染的调试器—帧调试器(Frame Debugger)。与其他
调试工具的复杂性相比,Unity 原生的帧调试器非常简单快捷。我们可以使用它来看到游戏图像
的某一帧是如何一步步渲染出来的。
要使用帧调试器,我们首先需要在Window -> Frame Debugger 中打开帧调试器窗口,如图5.6 所示。
在这里插入图片描述
帧调试器可以用于查看渲染该帧时进行的各种渲染事件(event),这些事件包含了Draw Call
序列,也包括了类似清空帧缓存等操作。帧调试器窗口大致可分为3 个部分:最上面的区域可以
开启/关闭(单击Enable 按钮)帧调试功能,当开启了帧调试时,通过移动窗口最上方的滑动条
(或单击前进和后退按钮),我们可以重放这些渲染事件;左侧的区域显示了所有事件的树状图,
在这个树状图中,每个叶子节点就是一个事件,而每个父节点的右侧显示了该节点下的事件数目。
我们可以从事件的名字了解这个事件的操作,例如以Draw 开头的事件通常就是一个Draw Call;
当单击了某个事件时,在右侧的窗口中就会显示出该事件的细节,例如几何图形的细节以及使用
了哪个Shader 等。同时在Game 视图中我们也可以看到它的效果。如果该事件是一个Draw Call
并且对应了场景中的一个GameObject,那么这个GameObject 也会在Hierarchy 视图中被高亮显示
出来,图5.7 显示了单击渲染某个对象的深度图事件的结果。

在这里插入图片描述
如果被选中的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 等工具。

5.6 小心:渲染平台的差异

Unity 的优点之一是其强大的跨平台性—写一份代码可以运行在很多平台上。绝大多数情
况下,Unity 为我们隐藏了这些细节,但有些时候我们需要自己处理它们。本节给出了一些常见
的因为平台不同而造成的差异。
5.6.1 渲染纹理的坐标差异
在2.3.4 节和4.2.2 节中,我们都提到过OpenGL 和DirectX 的屏幕空间坐标的差异。在水平
方向上,两者的数值变化方向是相同的,但在竖直方向上,两者是相反的。在OpenGL(OpenGL
ES 也是)中,(0, 0)点对应了屏幕的左下角,而在DirectX(Metal 也是)中,(0, 0)点对应了左上
角。图5.8 可以帮助读者回忆它们之间的这种不同。

在这里插入图片描述
需要注意的是,我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标
(Render Target)中。这时,我们需要使用渲染纹理(Render Texture)来保存这些渲染结果。我们
将在第12 章中学习如何实现这样的目的。
大多数情况下,这样的差异并不会对我们造成任何影响。但当我们要使用渲染到纹理技术,
把屏幕图像渲染到一张渲染纹理中时,如果不采取任何措施的话,就会出现纹理翻转的情况。幸
运的是,Unity 在背后为我们处理了这种翻转问题—当在DirectX平台上使用渲染到纹理技术时,
Unity 会为我们翻转屏幕图像纹理,以便在不同平台上达到一致性。
在一种特殊情况下Unity 不会为我们进行这个翻转操作,这种情况就是我们开启了抗锯齿(在
Edit -> Project Settings -> Quality -> Anti Aliasing 中开启)并在此时使用了渲染到纹理技术。在这
种情况下,Unity 首先渲染得到屏幕图像,再由硬件进行抗锯齿处理后,得到一张渲染纹理来供
我们进行后续处理。此时,在DirectX 平台下,我们得到的输入屏幕图像并不会被Unity 翻转,也
就是说,此时对屏幕图像的采样坐标是需要符合DirectX 平台规定的。如果我们的屏幕特效只需
要处理一张渲染图像,我们仍然不需要在意纹理的翻转问题,这是因为在我们调用Graphics.Blit
函数时,Unity 已经为我们对屏幕图像的采样坐标进行了处理,我们只需要按正常的采样过程处
理屏幕图像即可。但如果我们需要同时处理多张渲染图像(前提是开启了抗锯齿),例如需要同时
处理屏幕图像和法线纹理,这些图像在竖直方向的朝向就可能是不同的(只有在DirectX 这样的
平台上才有这样的问题)。这种时候,我们就需要自己在顶点着色器中翻转某些渲染纹理(例如深
度纹理或其他由脚本传递过来的纹理)的纵坐标,使之都符合DirectX 平台的规则。例如:
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1-uv.y;
#e ndif
其中,UNITY_UV_STARTS_AT_TOP 用于判断当前平台是否是DirectX 类型的平台,而当在
这样的平台下开启了抗锯齿后,主纹理的纹素大小在竖直方向上会变成负值,以方便我们对主纹
理进行正确的采样。因此,我们可以通过判断_MainTex_TexelSize.y 是否小于0 来检验是否开启
了抗锯齿。如果是,我们就需要对除主纹理外的其他纹理的采样坐标进行竖直方向上的翻转。我
们会在第13 章中再次看到上面的代码。
在本书资源的项目中,我们开启了抗锯齿选项。在第12 章中,我们将学习一些基本的屏幕后
处理效果。这些效果大多使用了单张屏幕图像进行处理,因此我们不需要考虑平台差异化的问题,
因为Unity 已经在背后为我们处理过了。但在12.5 节中,我们需要在一个Pass 中同时处理屏幕图
像和提取得到的亮部图像来实现Bloom 效果。由于需要同时处理多张纹理,因此在DirectX 这样
的平台下如果开启了抗锯齿,主纹理和亮部纹理在竖直方向上的朝向就是不同的,我们就需要对
亮部纹理的采样坐标进行翻转。在第13 章中,我们需要同时处理屏幕图像和深度/法线纹理来实
现一些特殊的屏幕效果,在这些处理过程中,我们也需要进行一些平台差异化处理。在15.3 节中,
尽管我们也在一个Pass 中同时处理了屏幕图像、深度纹理和一张噪声纹理,但我们只对深度纹理
的采样坐标进行了平台差异化处理,而没有对噪声纹理进行处理。这是因为,类似噪声纹理的装
饰性纹理,它们在竖直方向上的朝向并不是很重要,即便翻转了效果往往也是正确的,因此我们
可以不对这些纹理进行平台差异化处理。
5.6.2 Shader 的语法差异
读者在Windows 平台下编译某些在Mac 平台下工作良好的Shader 时,可能会看到类似下面
的报错 信息:
in correct number of arguments to numeric-type constructor (compiling for d3d11)
或者
ou tput parameter ‘o’ not completely initialized (compiling for d3d11)
上面的报错都是因为DirectX 9/11 对Shader 的语义更加严格造成的。例如,造成第一个报错
信息的原因是,Shader 中可能存在下面这样的代码:
// v 是float4 类型,但在它的构造器中我们仅提供了一个参数
fl oat4 v = float4(0.0);
在OpenGL 平台上,上面的代码是合法的,它将得到一个4 个分量都是0.0 的float4 类型的变量。
但在DirectX 11 平台上,我们必须提供和变量类型相匹配的参数数目。也就是说,我们应该写成:
fl oat4 v = float4(0.0, 0.0, 0.0, 0.0);
而对于第二个报错信息,往往是出现在表面着色器中。表面着色器的顶点函数(注意,不是
顶点着色器)有一个使用了out 修饰符的参数。如果出现这样的报错信息,可能是因为我们在顶
点函数中没有对这个参数的所有成员变量都进行初始化。我们应该使用类似下面的代码来对这些
参数进行初始化:

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 运算,是因为在顶点着色器阶段Shader 无法得到UV 偏导,而tex2D 函数需
要这样的偏导信息(这和纹理采样时使用的数学运算有关)。如果我们的确需要在顶点着色器中访
问纹理,需要使用tex2Dlod 函数来替代,如:
tex2Dlod(tex, float4(uv, 0, 0)).
而且我们还需要添加#pragma target 3.0,因为tex2Dlod 是Shader Model 3.0 中的特性。
5.6.3 Shader 的语义差异
我们在5.4 节讲到了Shader 中的语义是什么,其中我们讲到了一些语义在某些平台下是等价
的,例如SV_POSITION 和POSITION。但在另一些平台上,这些语义是不等价的。为了让Shader
能够在所有平台上正常工作,我们应该尽可能使用下面的语义来描述Shader 的输入输出变量。
 使用SV_POSITION 来描述顶点着色器输出的顶点位置。一些Shader 使用了POSITION 语
义,但这些Shader 无法在索尼PS4 平台上或使用了细分着色器的情况下正常工作。
 使用SV_Target 来描述片元着色器的输出颜色。一些Shader 使用了COLOR 或者COLOR0
语义,同样的,这些Shader 无法在索尼PS4 上正常工作。
5.6.4 其他平台差异
本书只给出了一些最常见的平台差异造成的问题,还有一些差异不再列举。如果读者发现一
些Shader 在平台A 下工作良好,而在平台B 下出现了问题,可以去Unity 官方文档(http://docs.
unity3d.com/Manual/SL-PlatformDifferences.html)中寻找更多的资料。

5.7 Shader 整洁之道

在本章的最后,我们给出一些关于如何规范Shader 代码的建议。当然,这些建议并不是绝对
正确的,读者可以根据实际情况做出权衡。写出规范的代码不仅是让代码变得漂亮易懂而已,更
重要的是,养成这些习惯有助于我们写出高效的代码。
5.7.1 float、half 还是fixed
在本书中,我们使用Cg/HLSL 来编写Unity Shader 中的代码。而在Cg/HLSL 中,有3 种精
度的数值类型:float,half 和fixed。这些精度将决定计算结果的数值范围。表5.8 给出了这3 种
精度在通常情况下的数值范围。
在这里插入图片描述
上面的精度范围并不是绝对正确的,尤其是在不同平台和GPU 上,它们实际的精度可能和上
面给出的范围不一致。通常来讲。
 大多数现代的桌面GPU 会把所有计算都按最高的浮点精度进行计算,也就是说,float、
half、fixed 在这些平台上实际是等价的。这意味着,我们在PC 上很难看出因为half 和fixed
精度而带来的不同。
 但在移动平台的GPU 上,它们的确会有不同的精度范围,而且不同精度的浮点值的运算
速度也会有所差异。因此,我们应该确保在真正的移动平台上验证我们的Shader。
 fixed 精度实际上只在一些较旧的移动平台上有用,在大多数现代的GPU 上,它们内部把
fixed 和half 当成同等精度来对待。
尽管有上面的不同,但一个基本建议是,尽可能使用精度较低的类型,因为这可以优化Shader
的性能,这一点在移动平台上尤其重要。从它们大体的值域范围来看,我们可以使用fixed 类型
来存储颜色和单位矢量,如果要存储更大范围的数据可以选择half 类型,最差情况下再选择使用
float。如果我们的目标平台是移动平台,一定要确保在真实的手机上测试我们的Shader,这一点
非常重要。关于移动平台的优化技术,读者可以在第16 章中找到更多内容。
5.7.2 规范语法
在5.6.2 节,我们提到DirectX 平台对Shader 的语义有更加严格的要求。这意味着,如果我们
要发布到DirectX 平台上就需要使用更严格的语法。例如,使用和变量类型相匹配的参数数目来
对变量进行初始化。
5.7.3 避免不必要的计算
如果我们毫无节制地在Shader(尤其是片元着色器)中进行了大量计算,那么我们可能很快
就会收到Unity 的错误提示:
temporary register limit of 8 exceeded

Arithmetic instruction limit of 64 exceeded; 65 arithmetic instructions needed to compile
program
出现这些错误信息大多是因为我们在Shader 中进行了过多的运算,使得需要的临时寄存器数
目或指令数目超过了当前可支持的数目。读者需要知道,不同的Shader Target、不同的着色器阶
段,我们可使用的临时寄存器和指令数目都是不同的。
通常,我们可以通过指定更高等级的Shader Target 来消除这些错误。表5.9 给出了Unity 目
前支持的一些Shader Target。

在这里插入图片描述
需要注意的是,由于Unity 版本的不同,Unity 支持的Shader Target 种类也不同,读者可以在
官方手册上找到更为详细的介绍。
读者:什么是Shader Model 呢?
我们:Shader Model 是由微软提出的一套规范,通俗地理解就是它们决定了Shader 中各个特
性(feature)的能力(capability)。这些特性和能力体现在Shader 能使用的运算指令数目、寄存器
个数等各个方面。Shader Model 等级越高,Shader 的能力就越大。具体的细节读者可以参见本章
的扩展阅读部分。
虽然更高等级的Shader Target 可以让我们使用更多的临时寄存器和运算指令,但一个更好的
方法是尽可能减少Shader 中的运算,或者通过预计算的方式来提供更多的数据。
5.7.4 慎用分支和循环语句
在我们学习第一门语言的课上,类似分支、循环语句这样的流程控制语句是最基本的语法之
一。但在编写Shader 的时候,我们要对它们格外小心。
在最开始,GPU 是不支持在顶点着色器和片元着色器中使用流程控制语句的。随着GPU 的发
展,我们现在已经可以使用if-else、for 和while 这种流程控制指令了。但是,它们在GPU 上的实现
和在CPU 上有很大的不同。深究这些指令的底层实现不在本书的讨论范围内,读者可以在本章的
扩展阅读中找到更多的内容。大体来说,GPU 使用了不同于CPU 的技术来实现分支语句,在最坏
的情况下,我们花在一个分支语句的时间相当于运行了所有分支语句的时间。因此,我们不鼓励在
Shader 中使用流程控制语句,因为它们会降低GPU 的并行处理操作(尽管在现代的GPU 上已经有
了改进)。
如果我们在Shader 中使用了大量的流程控制语句,那么这个Shader 的性能可能会成倍下降。
一个解决方法是,我们应该尽量把计算向流水线上端移动,例如把放在片元着色器中的计算放到
顶点着色器中,或者直接在CPU 中进行预计算,再把结果传递给Shader。当然,有时我们不可避
免地要使用分支语句来进行运算,那么一些建议是:
 分支判断语句中使用的条件变量最好是常数,即在Shader 运行过程中不会发生变化;
 每个分支中包含的操作指令数尽可能少;
 分支的嵌套层数尽可能少。
5.7.5 不要除以0
虽然在用类似C#等高级语言进行编程的时候,我们会谨记不要除以0 这个基本常识(就算你
没这么做,编辑器可能也会报错),但有时在编写Shader 的时候我们会忽略这个问题。
例如,我们在Shader 里写下如下代码:
fixed4 frag(v2f i) : SV_Target
{
return fixed4(0.0/0.0,0.0/0.0, 0.0/0.0, 1.0);
}
这样代码的结果往往是不可预测的。在某些渲染平台上,上面的代码不会造成Shader 的崩溃,
但即便不会崩溃得到的结果也是不确定的,有些会得到白色(由无限大截取到1.0),有些会得到
黑色,但在另一些平台上,我们的Shader 可能就会直接崩溃。因此,即便在开发游戏的平台上,
我们看到的结果可能是符合预期的,但在目标平台上可能就会出现问题。
一个解决方法是,对那些除数可能为0 的情况,强制截取到非0 范围。在一些资料中,读者
可能也会看到使用if 语句来判断除数是否为0 的例子。另一个方法是,使用一个很小的浮点值,
例如0.000001 来保证分母大于0(前提是原始数值是非负数)。

参考

Unity Shader入门精要
作者:冯乐乐

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity Shader是一种用于渲染图形的程序,它可以控制对象的表面颜色、纹理、透明度、反射等属性,从而实现特殊的视觉效果。对于游戏开发者来说,掌握Shader编写技巧是非常重要的。 以下是关于Unity Shader入门精要: 1. ShaderLab语言 ShaderLab是Unity中用于编写Shader的语言,它是一种基于标记的语言,类似于HTML。ShaderLab可以用于定义Shader的属性、子着色器、渲染状态等信息。 2. CG语言 CG语言是Unity中用于编写Shader的主要语言,它是一种类似于C语言的语言,可以进行数学运算、向量计算、流程控制等操作。CG语言可以在ShaderLab中嵌入,用于实现Shader的具体逻辑。 3. Unity的渲染管线 Unity的渲染管线包括顶点着色器、片元着色器、几何着色器等组件,每个组件都有不同的作用。顶点着色器用于对对象的顶点进行变换,片元着色器用于计算每个像素的颜色,几何着色器用于处理几何图形的变形和细节等。 4. 模板和纹理 在Shader中,我们可以使用纹理来给对象添加图案或者贴图,也可以使用模板来控制对象的透明度、反射等属性。纹理可以通过内置函数tex2D()来获取,模板可以通过内置函数clip()来实现裁剪。 5. Shader的实现 Shader的实现需要注意以下几点: - 在ShaderLab中定义Shader的属性、子着色器、渲染状态等信息。 - 在CG语言中实现Shader的具体逻辑,包括顶点着色器、片元着色器等内容。 - 使用纹理和模板来实现特定的视觉效果。 - 在对象上应用Shader,通过调整Shader的属性来达到不同的效果。 以上是关于Unity Shader入门精要,希望对你有所帮助。如果你想更深入地了解Shader的编写技巧,可以参考官方文档或者相关教程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值