5.1 本书使用的软件和环境
- Unity 5.2.1
- MacOS (GPI 基于 OpenGL,影响渲染纹理的坐标原点位置)
5.2 一个最简单的顶点/片元着色器
5.2.1 顶点/片元着色器的基本结构
Unity Shader 的基本结构,顶点/片元着色器的与之大体类似
- 最重要的部分是
Pass 语义块,绝大部分的代码都写在内
创建一个最简单的顶点/片元着色器
- 将场景中的天空盒去掉,Window - Liginting - Skybox,将该项置空
- 新建一个 Unity Shader
- 新建一个材质,把上一步骤创建的 Unity Shader 赋给它
- 在场景中新建一个球体,把上一步骤的材质拖曳给它
- 打开第 2 步的 Unity Shader。替换代码:
- 两行非常重要的编译指令,它们告诉 Unity,哪个函数包含了顶点/片元着色器的代码
#pragma vertex vert
#pragma fragment frag
顶点着色器 vert 函数
- vert 函数的输入 v 包含了这个
顶点的位置,这是通过POSITION 语义指定的 - 返回值是一个 float4 类型的变量——该顶点在裁剪空间中的位置
POSITION和SV_POSITION都是 CG/HLSL 中的语义。它们是不可省略的,它们告诉系统,用户需要哪些输入值和用户的输出是什么- 顶点着色器只有一行代码,功能是把顶点坐标从
模型空间转换到裁剪空间。UNITY_MATRIX_MVP是 Unity 内置的 模型*观察*投影矩阵
片元着色器 frag 函数
- frag 函数没有任何输入,输出是一个 fixed4 类型的变量,且使用了 SV_Target 语义进行限定
SV_Target告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中- 片元着色器的代码返回了一个表示白色的 fixed4 类型的变量
5.2.2 模型数据从哪里来
- 如果我们想要更多模型数据(纹理坐标/法线),需要为顶点着色器定义一个新的输入参数(结构体)
- 对于顶点着色器的输入,Unity 支持的语义有:
- POSITION
- TANGENT
- NORMAL
- TEXCOORDn(如 TEXCOORD1)
- COLOR
- 这些语义中的数据是由使用该材质的
Mesh Render 组件提供的 - 每帧调用
Draw Call时,Mesh Render 组件会把它负责渲染的模型数据(网格-面片-顶点-数据)发送给 Unity Shader
5.2.3 顶点着色器和片元着色器之间如何通信
我们希望从顶点着色器输出一些数据传递到片元着色器,为此需要再定义一个新的结构体:
- 声明了新的结构体 v2f,负责顶点着色器和片元着色器之间的信息传递
- v2f 中也需要指定每个变量的语义
SV_POSITION:裁剪空间中的顶点坐标COLOR0:可由用户自行定义,一般是存储颜色
- 片元着色器中的输入实际上是把顶点着色器的输出进行
插值后的结果
5.2.4 如何使用属性
- 将参数写在 Properties 语义块中,可以方便地通过
材质来调整 - 我们想要在材质面板显示一个颜色拾取器,以直接控制模型的显示颜色。修改代码:
5.3 强大的援手:Unity 提供的内置文件和变量
Unity 提供了很多内置文件,它们包含了很多提前定义的函数、变量和宏等,方便开发者编码
5.3.1 内置的包含文件
- 使用
#include 指令将文件包含进来,我们就可以使用 Unity 提供的功能和变量
这些文件在 CGIncludes 目录中,在电脑的这个地方能找到:
- Mac: /Applications/Unity/Unity.app/Contents/CGIncludes
- Windows: Unity 的安装路径/Data/CGIncludes
CGIncludes 中主要包含的文件及用处
- 有些文件尽管我们没使用 #include 指令,它们也会被自动包含进来
- UnityCG.cginc 是最常接触和使用的包含文件。例如,使用其中的
结构体作为顶点着色器的输入和输出。下面给出一些结构体的名称和包含的变量
除了结构体,UnityCG.cginc 也提供了一些常用的帮助函数
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 支持的常用语义
顶点着色器 --> 片元着色器 时 Unity 支持的常用语义
Unity 中支持的片元着色器的输出语义
5.4.3 如何定义复杂的变量类型
例子:
5.5 程序员的烦恼:Debug
5.5.1 使用假彩色图像
指的是用假彩色技术生成的一种图像
- 可用于可视化一些数据,可用它来对 Shader 进行调试
- 思路:把需要调试的变量映射到 [0, 1] 之间,把它们作为颜色输出到屏幕上来判断这个值是否正确
- 若调试的是一个一维数据,选择一个单独的颜色分量(如 R)进行输出
- 若调试的是多维数据,可选择对它的每一个分量单独调试,或选择多个颜色分量进行输出
5.5.2 利用神器:Visual Studio
Graphics Debugger
5.5.3 最新利器:帧调试器
- 可用于查看渲染该帧时进行的各种
渲染事件。如Draw Call、清空帧缓存等 - 窗口分为 3 个部分
- 最上面可开启/关闭帧调试功能。当开启了,拖动最上方的滑动条可重放这些渲染事件
- 左侧显示了所有事件的树状图,每个叶节点就是一个事件。单击某个事件,右侧会显示事件细节
- Draw 开头的事件通常是一个 Draw Call
- 若对应了场景中的一个 GameObject,那么这个 GameObject 也会在 Hierarchy 视图中被高亮显示
- 若是对一个渲染纹理的渲染操作,那么这个纹理就会显示在 Game 视图中
5.6 小心:渲染平台的差异
Unity 为我们隐藏了实现跨平台的很多细节,有时我们需要自己处理它们
5.6.1 渲染纹理的坐标差异
- 在 OpenGL(包括 ES)中,原点对应屏幕左下角
- 在 DirectX(Meta 1 也是)中,原点对应屏幕左上角
- 我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的
渲染目标(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
- 大多数现代桌面 GPU会把所有计算都按最高精度进行计算
- 在 移动平台的 GPU上它们会有不同的精度范围,我们应该确保在真正的移动平台上验证 Shader
- fixed 实际上只在一些较旧的移动平台上有用,在大多数现代 GPU 上,fixed 会被当成 half 来看待
- 尽量使用精度较低的类型,在移动平台上这种优化尤其重要
- fixed 类型存储颜色和单位矢量
- 更大范围的使用 half
- 最差情况下再选择使用 float
5.7.2 规范语法
要发布到 Dx 平台上的话就要使用更严格的语法
5.7.3 避免不必要的计算
- 不同的 Shader Target、不同的着色器阶段,可使用的
临时寄存器和指令数目都是不同的
Q:什么是 Shader Model?
A:是微软提出的一套规范(类似 ECMAScript 规范),决定了 Shader 中各个特性和能力,它们决定了 Shader 能使用的寄存器和指令数目等各个方面。
这些规范的本质:核心作用是统一 “生产者”(开发者)和 “消费者”(硬件 / 软件引擎)之间的交互规则,确保兼容性。
5.7.4 慎用分支和循环语句
- 会降低 GPU 的并行处理性能
- 解决:尽量把这些逻辑计算向流水线上端移动,最好直接在 CPU 中进行预计算,再把结果传给 Shader
- 若一定要使用,建议:
- 分支判断中使用的
条件变量最好是常数 - 每个分支中包含的
操作指令数尽可能少 - 分支的
嵌套层数尽可能少
- 分支判断中使用的