《Unity Shader入门精要》五、开始 Unity Shader 学习之旅

116 阅读10分钟

5.1 本书使用的软件和环境

  • Unity 5.2.1
  • MacOS (GPI 基于 OpenGL,影响渲染纹理的坐标原点位置)

5.2 一个最简单的顶点/片元着色器

5.2.1 顶点/片元着色器的基本结构

Unity Shader 的基本结构,顶点/片元着色器的与之大体类似

image.png

  • 最重要的部分是 Pass 语义块,绝大部分的代码都写在内

创建一个最简单的顶点/片元着色器

  1. 将场景中的天空盒去掉,Window - Liginting - Skybox,将该项置空
  2. 新建一个 Unity Shader
  3. 新建一个材质,把上一步骤创建的 Unity Shader 赋给它
  4. 在场景中新建一个球体,把上一步骤的材质拖曳给它
  5. 打开第 2 步的 Unity Shader。替换代码:

image.png

  • 两行非常重要的编译指令,它们告诉 Unity,哪个函数包含了顶点/片元着色器的代码
#pragma vertex vert
#pragma fragment frag

顶点着色器 vert 函数

  • vert 函数的输入 v 包含了这个顶点的位置,这是通过 POSITION 语义指定的
  • 返回值是一个 float4 类型的变量——该顶点在裁剪空间中的位置
  • POSITIONSV_POSITION 都是 CG/HLSL 中的语义。它们是不可省略的,它们告诉系统,用户需要哪些输入值和用户的输出是什么
  • 顶点着色器只有一行代码,功能是把顶点坐标从模型空间转换到裁剪空间UNITY_MATRIX_MVP 是 Unity 内置的 模型*观察*投影矩阵

片元着色器 frag 函数

  • frag 函数没有任何输入,输出是一个 fixed4 类型的变量,且使用了 SV_Target 语义进行限定
  • SV_Target 告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存
  • 片元着色器的代码返回了一个表示白色的 fixed4 类型的变量

5.2.2 模型数据从哪里来

  • 如果我们想要更多模型数据(纹理坐标/法线),需要为顶点着色器定义一个新的输入参数(结构体)

image.png

  • 对于顶点着色器的输入,Unity 支持的语义有:
    • POSITION
    • TANGENT
    • NORMAL
    • TEXCOORDn(如 TEXCOORD1)
    • COLOR
  • 这些语义中的数据是由使用该材质的 Mesh Render 组件提供的
  • 每帧调用 Draw Call 时,Mesh Render 组件会把它负责渲染的模型数据(网格-面片-顶点-数据)发送给 Unity Shader

5.2.3 顶点着色器和片元着色器之间如何通信

我们希望从顶点着色器输出一些数据传递到片元着色器,为此需要再定义一个新的结构体:

image.png

  • 声明了新的结构体 v2f,负责顶点着色器和片元着色器之间的信息传递
  • v2f 中也需要指定每个变量的语义
    • SV_POSITION:裁剪空间中的顶点坐标
    • COLOR0:可由用户自行定义,一般是存储颜色
  • 片元着色器中的输入实际上是把顶点着色器的输出进行插值后的结果

5.2.4 如何使用属性

  • 将参数写在 Properties 语义块中,可以方便地通过材质来调整
  • 我们想要在材质面板显示一个颜色拾取器,以直接控制模型的显示颜色。修改代码:

image.png

5.3 强大的援手:Unity 提供的内置文件和变量

Unity 提供了很多内置文件,它们包含了很多提前定义的函数、变量和宏等,方便开发者编码

5.3.1 内置的包含文件

  • 使用 #include 指令将文件包含进来,我们就可以使用 Unity 提供的功能和变量

image.png

这些文件在 CGIncludes 目录中,在电脑的这个地方能找到:

  • Mac: /Applications/Unity/Unity.app/Contents/CGIncludes
  • Windows: Unity 的安装路径/Data/CGIncludes

CGIncludes 中主要包含的文件及用处

image.png

  • 有些文件尽管我们没使用 #include 指令,它们也会被自动包含进来
  • UnityCG.cginc 是最常接触和使用的包含文件。例如,使用其中的结构体作为顶点着色器的输入和输出。下面给出一些结构体的名称和包含的变量

image.png

除了结构体,UnityCG.cginc 也提供了一些常用的帮助函数

image.png

5.4 Unity 提供的 CG/HLSL 语义

5.4.1 什么是语义

语义实际上就是一个赋给 Shader 输入和输出的字符串,它表达了这个参数的含义

  • 通俗地讲,这些语义可以让 Shader 知道从哪读取数据,并把数据输出到哪
  • 注意,Unity 并没有支持所有语义
  • Unity 为了方便对模型数据的传输,对一些语义进行了含义规定。如:
    • 顶点着色器输入结构体 a2f 中,Unity 会识别到 TEXCOORD0 语义,把模型的第一组纹理坐标填充到 texcoord 中
    • 即使语义名称一样,若位置不同,含义也不同。片元着色器输入结构体 v2f 中,TEXCOORD0 修饰的变量含义由开发者决定

系统数值语义

  • 在 DX10 以后,有一种新的语义类型——系统数值语义。这类语义以 SV 开头,代表含义就是 系统数值(system-value)
  • 在渲染流水线中有特殊的含义,如上面代码中的 SV_POSITION 语义修饰的变量 pos
    • pos 包含了可用于光栅化的顶点坐标(齐次裁剪空间中的坐标)
    • 用这些语义修饰的变量不能随便赋值,因为流水线需要用它们来完成特定任务
  • 为了让 Shader 有更好的跨平台性,对于有特殊含义的变量我们最好以 SV 开头的语义来修饰

5.4.2 Unity 支持的语义

应用阶段 --> 顶点着色器 时 Unity 支持的常用语义

image.png

顶点着色器 --> 片元着色器 时 Unity 支持的常用语义

image.png

Unity 中支持的片元着色器的输出语义

image.png

5.4.3 如何定义复杂的变量类型

例子:

image.png

5.5 程序员的烦恼:Debug

5.5.1 使用假彩色图像

指的是用假彩色技术生成的一种图像

  • 可用于可视化一些数据,可用它来对 Shader 进行调试
  • 思路:把需要调试的变量映射到 [0, 1] 之间,把它们作为颜色输出到屏幕上来判断这个值是否正确
  • 若调试的是一个一维数据,选择一个单独的颜色分量(如 R)进行输出
  • 若调试的是多维数据,可选择对它的每一个分量单独调试,或选择多个颜色分量进行输出

5.5.2 利用神器:Visual Studio

Graphics Debugger

5.5.3 最新利器:帧调试器

image.png

  • 可用于查看渲染该帧时进行的各种渲染事件。如 Draw Call清空帧缓存
  • 窗口分为 3 个部分
    • 最上面可开启/关闭帧调试功能。当开启了,拖动最上方的滑动条可重放这些渲染事件
    • 左侧显示了所有事件的树状图,每个叶节点就是一个事件。单击某个事件,右侧会显示事件细节
  • Draw 开头的事件通常是一个 Draw Call
    • 若对应了场景中的一个 GameObject,那么这个 GameObject 也会在 Hierarchy 视图中被高亮显示
    • 若是对一个渲染纹理的渲染操作,那么这个纹理就会显示在 Game 视图中

5.6 小心:渲染平台的差异

Unity 为我们隐藏了实现跨平台的很多细节,有时我们需要自己处理它们

5.6.1 渲染纹理的坐标差异

  • 在 OpenGL(包括 ES)中,原点对应屏幕左下角
  • 在 DirectX(Meta 1 也是)中,原点对应屏幕左上角

image.png

  • 我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标(Render Target)中。这时我们需要使用渲染纹理(Render Texture)来保存这些渲染结果(12 章会学习)
  • 当我们使用渲染到纹理技术时,Unity 在背后为我们处理了这种翻转问题
  • 在一种特殊情况下 Unity 不会处理翻转问题——开启了抗锯齿,并使用了渲染到纹理技术,因为得到的渲染纹理会供我们后续处理。但我们仍然不需要在意这个问题,因为我们调用 Graphics.Blit 函数时,Unity 会为屏幕图像的采样坐标进行了处理。在 DirectX 下必须自己处理翻转问题的情况:
    • 同时处理多张渲染图像(开启了抗锯齿)。如需要同时处理屏幕图像和法线纹理
    • 处理方法:在顶点着色器中翻转某些纹理的纵坐标:
    #if UNITY_UV_STARTS_AT_TOP
    if (_MainTex_TexelSize.y < 0) // 开启抗锯齿后竖直纹素大小变成负值
        uv.y = 1 - uv.y;
    #endif
    

5.6.2 Shader 的语法差异

在某个平台上工作良好的 Shader,在另一个平台上可能会报错。Dx9/11 对 Shader 的语义更加严格

报错一

incorrect number of arguments to numeric-type constructor (compiling for d3d1l)

  • 报错解释:构造函数参数错误
  • 报错原因:在 Dx11 平台上,我们必须提供和变量类型相匹配的参数数目

问题代码可能是:

float4 v = float4(0.0);

解决:

float4 v = float4(0.0, 0.0, 0.0, 0.0);

报错二

output parameter 'o' not completely initialized (compiling for d3d11)

  • 报错解释:输出结果 o 没完全初始化
  • 报错原因:往往出现在表面着色器中,里面的顶点函数有一个使用了 out 修饰符的参数,我们没有对它的所有成员变量都进行初始化

解决:

void vert(inout appdata_full v, out Input o) {
    // 使用 Unity 内置的 UNITY_INITIALIZE_OUTPUT 宏对输出结构体进行初始化
    UNITY_INITIALIZE_OUTPUT(Input, o);
    // ...
}

其他

  • Dx9/11 不支持在顶点着色器中使用 tex2D 函数。解决方法是使用 tex2Dload 来代替:
#pragma target3.0

tex2Dload(tex, float4(uv, 0, 0));

5.6.3 Shader 的语义差异

  • 使用 SV_POSITION 来描述顶点着色器输出的顶点位置,而不是 POSITION
  • 使用 SV_Target 来描述片元着色器的输出颜色,而不是 COLOR/COLOR0

5.6.4 其他平台差异

官方链接

5.7 Shader 整洁之道

5.7.1 float、half 还是 fixed

image.png

  • 大多数现代桌面 GPU会把所有计算都按最高精度进行计算
  • 移动平台的 GPU上它们会有不同的精度范围,我们应该确保在真正的移动平台上验证 Shader
  • fixed 实际上只在一些较旧的移动平台上有用,在大多数现代 GPU 上,fixed 会被当成 half 来看待
  • 尽量使用精度较低的类型,在移动平台上这种优化尤其重要
    • fixed 类型存储颜色和单位矢量
    • 更大范围的使用 half
    • 最差情况下再选择使用 float

5.7.2 规范语法

要发布到 Dx 平台上的话就要使用更严格的语法

5.7.3 避免不必要的计算

  • 不同的 Shader Target、不同的着色器阶段,可使用的临时寄存器指令数目都是不同的

image.png

Q:什么是 Shader Model?

A:是微软提出的一套规范(类似 ECMAScript 规范),决定了 Shader 中各个特性和能力,它们决定了 Shader 能使用的寄存器和指令数目等各个方面。

这些规范的本质:核心作用是统一 “生产者”(开发者)和 “消费者”(硬件 / 软件引擎)之间的交互规则,确保兼容性。

5.7.4 慎用分支和循环语句

  • 会降低 GPU 的并行处理性能
  • 解决:尽量把这些逻辑计算向流水线上端移动,最好直接在 CPU 中进行预计算,再把结果传给 Shader
  • 若一定要使用,建议:
    • 分支判断中使用的条件变量最好是常数
    • 每个分支中包含的操作指令数尽可能少
    • 分支的嵌套层数尽可能少

5.7.5 不要除以 0