面向电子游戏的 GPU 编程 -- HLSL 入门

HLSL 入门

介绍

微软开发的 HLSL 和英伟达开发的 CG 最初有所不同,但它们很快就趋于一致,因此人们倾向于交替使用 HLSL 和 CG 这两个术语。

OpenGL 有自己的着色语言,叫做 GLSL。如果你找到用 GLSL 编写的代码,可以很轻松地将其转换为 HLSL,反之亦然。

Vulkan 是 OpenGL 的继任者,它是一个全新的 API。Vulkan 的主要作用是将 OpenGL 驱动程序隐式处理的许多决策交给程序员,以便程序员可以进行更精细的优化。

Apple 本来可以直接使用 Vulkan,但他们必须另辟蹊径,因为他们是 Apple,所以他们选择了 Metal。Vulkan 和 Metal 都有各自的着色语言。这些语言实际上与 GLSL、HLSL 等并没有太大区别,它们都具有相同的底层语义,只是语法不同。这意味着,对于像 Unity 这样的引擎开发者来说,他们可以轻松地让用户使用 HLSL 编写代码,然后 Unity 会根据需要将其转换为其他语言。

HLSL/CG

HLSL 或 CG 中与 C、C++、C#、Java 或其他语言不同的地方在于 uniform 变量和变量输入的概念。uniform 变量是所有顶点和像素共有的变量,例如表示模型空间到世界空间、视图空间到裁剪空间变换的矩阵。这些变量必须在着色器代码之外的某个地方设置,例如 C++、C# 或其他任何语言中,只要你在函数外部定义变量(无论如何,统一变量都必须在函数外部定义,否则就毫无意义),就不需要显示地包含关键字 uniform 来定义 uniform 变量。你会在代码顶部看到这些定义。这些是所有你正在处理的顶点和所有你正在创建的像素的通用属性。

还有另一种输入,它们基本上是从主内存发送到 GPU 的大型顶点缓冲区。此外,还有各种其他属性,例如插值器创建的像素的位置和颜色。这些属性使用称为语义的东西来定义。同样,语义是 HLSL/CG 特有的,你在标准的 C、C++、C# 等语言中看不到它们。语义与 GPU 上的特定硬件绑定,它们不是通用的。

  • Attributes associated with each vertex or pixel
  • Declared using semantics

The uniform type qualifier

使用 Unity 的一个棘手之处在于,Unity 试图尽可能简化游戏编写,这是一个很好的目标,因此它试图对用户隐藏许多细节,这实际上与我们的主要目标背道而驰。在本课程中,我们将深入探讨 GPU 的细节,最终结果是 Unity 运行时会为我们定义很多变量。如果你使用 Unreal、Godot 或其他引擎,情况也是如此。我喜欢把这类引擎比作 "自带电池",但一旦达到一定的复杂程度,你就必须创建一些自定义变量,并使用自定义脚本来定义它们。如果你使用原始的 DirectX、OpenGL、Vulkan、Metal 或其他任何技术编写的代码(很可能是用 C++),那么所有这些都必须你自己完成,Unity 或 Unreal 运行时不会自动为你分配这些变量。

你在这里拥有的信息,比如灯光的位置和颜色,都存储在上一节课中看到的常量寄存器中。再次强调,这些变量被称为统一变量,因为它们对于着色器代码处理的所有顶点和像素都保持不变。

语义

这里有一个语义的例子:变量名基本由你决定。Unity 有一些它喜欢使用的约定,我基本上会遵循这些约定。无论我在代码中发现什么,我都会遵循,但有时我也会打破常规,这取决于我的心情。语义由这些关键字定义,它们位于冒号之后,并且是大写字母。你不能随意选择这些关键字,不能随笔编造一个,比如叫 Fred,因为它们实际上与 GPU 上的特定硬件绑定。它们服务于特定的功能,例如 Tech 得分为零,Tech 得分为一等等,这些都与 GPU 绑定。插值硬件,以及当你将大量顶点和定义各种三角形的顶点表中的索引数组传递给 API 时,API 调用会引用位置和法线等特定信息。

Unity 运行时内置的渲染器会为我们处理很多这类工作,但如果你使用纯 C++ 结合 DirectX 编写自己的游戏,那么你必须定义这些缓冲区,将顶点放入其中,并进行必要的 API 调用,将这些缓冲区中的数据发送到 GPU 等等。Unity 会为我们处理大部分工作。如果你愿意,Unity 也内置了调用底层例程的功能,所以你可以忽略 Unity 运行时的渲染功能,手动编写所有代码。但这会非常麻烦,通常只有在遇到一些特殊情况时才会这样做:你的游戏大部分都使用常规的 Unity 渲染例程处理,但可能存在一些需要手动实现的特殊效果。通常情况下,你应该避免这样做。这否定了使用 Unity 这类工具的初衷。这些语义将 CPU 端连接到 GPU 端,然后连接 GPU 的各种硬件元素。

  • A colon and a keyword, e.g.,

    • VertexPos : POSITION
    • VertexColor : COLOR0
    • VertexNormal : NORMAL
    • VertexUVcoord : TEXCOORD0
  • A glue that

    • Binds a shader program to the rest of the graphics pipeline
    • Connects the semantic variables to the program

数学操作

在着色器代码中进行的大多数数学运算都涉及浮点值或浮点值的向量和数组。着色器代码的缺点是没有指针,所以无法创建和使用复杂的数据结构。优点是没有指针,所以不会出现指针相关的错误。因此,我们不需要任何与指针相关的常用语法。着色器模型 4.0 引入了整数以及对整数的各种运算,但我没见过任何使用这些东西的 3D 图形着色器代码示例。我认为这些东西主要保留用于更通用的 GPU 编程框架,例如用于比特币挖矿、加密或其他用途。

  • Most commonly used C/C++ operations are supported
  • No pointer support or indirection in Cg/HLSL, so no * or ->
  • Some ops in Shader Model 4.0 and higher only:
    • Bitwise logic operation (&, ^, |, &=, |=, ^=...)
    • Shift: <<, >>, <<=, >>=
    • Modular: %

Tricks with mul

因此,在深入探讨之前,我们需要简单了解一下 HLSL 中的向量,特别是如何进行乘法运算。所以在 MATLAB 这样的语言中,有列向量和行向量的概念;而在 HLSL 中,我认为很多其他着色器语言并没有类似的概念,只有向量和矩阵。如何解释什么是行向量,什么是列向量,实际上取决于你使用 multiply 命令的方式。这与 multiply 命令处理参数的方式以及数组的存储方式有关。

如果你把向量放在矩阵前面,你可以像通常那样解释为将行向量乘以矩阵;如果你把矩阵放在向量前面,你可以像通常那样解释为将矩阵乘以列向量。

但如果你想把这里的向量当作列向量来处理,你可以把 v 放在第一个参数中,这相当于列向量乘以矩阵的转置。类似地,如果你想把 v 当作行向量来处理,你可以把第二个参数放在 multiply 命令中,这相当于将 v 后乘矩阵转置。当你查看不同的资料各种着色器代码示例时,这可能会造成很大的困扰,因为有时很难确定它是行向量还是列向量。答案取决于你如何处理它,取决于你如何思考它。最终的结果是,我从未见过任何显式使用转置命令的着色器代码示例。无论何时使用转置,要么是在 CPU 端预先计算并作为 uniform 参数传递,要么基本上是以某种方式使用 small 命令并以某种方式解释结果。因此,虽然在像 MATLAB 这样的语言中,你经常会看到人们写 v,然后在后面加一个单引号来进行转置操作,但在着色器代码中很少看到这样的做法。这需要一些时间来适应。

标准函数库

HLSL 和大多数其他着色器语言都内置了许多命令,其中包含一些你期望的功能。当然,如果你愿意,可以显式地写出点积和叉积的表达式。

hlsl 复制代码
dot(a, b)
cross(a, b)
distance(pt1, pt2)  :  Euclidean distance

插值命令的一个有趣之处在于,f 的值不要介于 0 和 1 之间,因此你可以将其外推到不同的极端值。

hlsl 复制代码
lerp(a, b, f) : r = (1 - f) * a + f * b

这个 lik 命令有点奇怪,它映射到着色器模型和类似指令。如果你预先计算法向量与光照向量的点积以及法向量与半向量的点积,它将计算传统的 Blinn-Phong 光照模型。然而,我见过的所有实际实现 Blinn-Phong 的着色器代码似乎都没有使用这个命令,它们只是直接编写了各种方程。

hlsl 复制代码
lit(NL, NH, pwr)

我们已经讨论过乘法了。

hlsl 复制代码
mul(M, N)

归一化(Normalized)正如你所想的那样,它的作用就是将值限制在 0 和 1 之间。你会经常用到它。

hlsl 复制代码
normalize(v)
saturate(a) : clamp to values between 0 and 1

当我们进行环境贴图时,我们需要使用这个反射命令。

hlsl 复制代码
reflect(I, N) : calculate reflection vector of ray I

现在再使用这个符号余弦命令,这很有意思,因为列表中的所有其他命令都接受特定的输入,然后像函数一样提供一个输出。这里你给它一个输入,但实际的输出放在这个 SNC 组件中。因此,HLSL 可以有函数将输出分配给参数列表中的对象,尽管这在大多数库函数中并不常见。

hlsl 复制代码
sincos(x, s, c) : calculate sin(x) and cos(x)

如果你想知道在哪里可以使用它,请记住旋转矩阵。如果你想象一下,一个物体处于特定位置和方向,这些正弦或余弦值将被预先计算,以创建某种矩阵,然后将其应用于所有顶点。但你可能想象在着色器代码中进行某种旋转动画,在这种情况下,像这样的命令可能会派上用场。

相关推荐
SmalBox1 天前
【节点】[Floor节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[Ceiling节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[Saturate节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox7 天前
【节点】[Remap节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox7 天前
【节点】[RandomRange节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox8 天前
【节点】[OneMinus节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox9 天前
【节点】[Minimum节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox11 天前
【节点】[Fraction节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox12 天前
【节点】[Clamp节点]原理解析与实际应用
unity3d·游戏开发·图形学