
在上一章中,您通过在 vertex 函数中计算position,来平移顶点和在屏幕上移动对象。但是,在 3D 空间中,您还想执行更多操作,例如旋转和缩放对象。您还需要一个场景内摄像机,以便您可以在场景中移动。
要移动、缩放和旋转三角形,您将使用矩阵 - 一旦您掌握了一个三角形,就可以一次旋转具有数千个三角形的模型!
对于我们这些不是数学天才的人来说,向量和矩阵可能有点可怕。幸运的是,在使用数学时,您不必总是需要知道引擎盖下的内容。为了提供帮助,本章的重点不是数学,而是矩阵。在学习本章的过程中,您将逐渐扩展您的线性代数知识,因为您将了解矩阵可以为您做什么以及如何操作它们。
变换
查看下面的图片:

使用矢量图像编辑器 Affinity Designer,您可以通过一系列仿射变换来缩放和旋转猫。Affinity Designer 不会单独计算每个位置,而是创建一个包含变换组合的变换矩阵。然后,它将变换应用于每个元素。
注: 仿射表示完成变换后,所有平行线都保持平行。
当然,没有人愿意移动、缩放和旋转猫,因为它们可能会咬人。因此,您将平移、缩放和旋转三角形。
开始项目和设置
➤ 打开并运行位于本章 starter 文件夹中的 starter 项目。

此项目渲染一个三角形两次,而不是渲染一个四边形。
在 Renderer 的 draw(in:) 中,您将看到两个 draw 调用(每个三角形一个)。渲染器将 position 传递给 vertex 函数,将 color 传递给 fragment 函数;它对每个三角形执行此操作。灰色三角形位于其原始位置,红色三角形应用变换。
➤ 在继续下一步之前,请确保您理解 Renderer 的 draw(in:) 中的代码和 Shaders.metal 中的 vertex 函数。
ContentView.swift 现在位于 SwiftUI 视图组中,它在Metal View上显示网格,以便您可以更轻松地可视化顶点位置。
平移
starter项目展示两个三角形:
• 没有任何变换的灰色三角形。
• 使用position = simd_float3(0.3, -0.4, 0) 平移的红色三角形。

在上一章的第一个挑战中,您计算了着色器函数中每个顶点的位置。更常见的计算机图形范例是,在顶点缓冲区中设置模型每个顶点的位置,通常从文件加载,然后将矩阵发送到顶点着色器,着色器包含模型当前位置、旋转和缩放。
顶点和矩阵
您可以更好地将position描述为位移矢量[0.3, -0.4, 0]。每个顶点从 x 方向上移动 0.3 个单位,在 y 方向上移动 -0.4 个单位。
在下图中,蓝色箭头是矢量。

左侧蓝色箭头是值为 [-1, 2] 的向量。右侧的蓝色箭头(靠近 cat 的箭头)也是值为 [-1, 2] 的向量。position(point)是空间中的位置,而向量是空间中的位移。换句话说,向量包含移动的数量和方向。如果你要用蓝色向量来移动猫,它最终会到达点 (2, 4)。这是猫的位置 (3, 2) 加上向量 [-1, 2]。
此 2D 向量是一个 1x2 矩阵。它有一列和两行。
注: 您可以按行或列对矩阵进行排序。simd 库按列优先顺序构造矩阵,这意味着列在内存中是连续的。simd_double2x4 是包含两列四行的矩阵。
矩阵是二维数组。即使是单个数字 1 也是一个 1×1 矩阵。实际上,数字1是独一无二的,因为用任何数字乘以1都是它本身。所有方阵(行数和列数相等的矩阵),都有一个矩阵具有和1的概念相等的矩阵------单位矩阵。任何向量或矩阵乘以单位矩阵,都是它本身。
4×4 单位矩阵如下所示(全是 0,对角线 1 除外):

3D 变换矩阵有 4 行和 4 列。变换矩阵左上角的 3×3 矩阵中包含缩放和旋转信息,平移信息在最后一列中。将向量和矩阵相乘时,左侧矩阵或向量的列数必须等于右侧的行数。例如,您不能将 float3 乘以 float4×4。
矩阵的魔力
当您将矩阵相乘时,您将它们合并为一个矩阵。然后,您可以将向量乘以此矩阵来变换向量。例如,您可以设置旋转矩阵和平移矩阵。然后,您可以使用以下代码计算转换后的位置:
translationMatrix * rotationMatrix * positionVector
矩阵乘法从右向左进行。在这里,旋转应用于平移之前的位置。
这是线性代数的基础------如果你想继续学习计算机图形学,你需要更全面地理解线性代数。目前,了解设置转换矩阵的概念可以让你走很长的路。
创建矩阵
➤ 打开 Renderer.swift,找到你在 draw(in:) 中渲染第一个灰色三角形的位置。
➤ 将position代码从:
Swift
var position = simd_float3(0, 0, 0)
renderEncoder.setVertexBytes(
&position,
length: MemoryLayout<SIMD3<Float>>.stride,
index: 11)
改为:
Swift
var translation = matrix_float4x4()
translation.columns.0 = [1, 0, 0, 0]
translation.columns.1 = [0, 1, 0, 0]
translation.columns.2 = [0, 0, 1, 0]
translation.columns.3 = [0, 0, 0, 1]
var matrix = translation
renderEncoder.setVertexBytes(
&matrix,
length: MemoryLayout<matrix_float4x4>.stride,
index: 11)
在这里,您将创建一个单位矩阵和一个要发送到 GPU 的渲染命令。
➤ 找到第二个红色三角形的position代码并更改:
Swift
position = simd_float3(0.3, -0.4, 0)
renderEncoder.setVertexBytes(
&position,
length: MemoryLayout<SIMD3<Float>>.stride,
index: 11)
为:
Swift
let position = simd_float3(0.3, -0.4, 0)
translation.columns.3.x = position.x
translation.columns.3.y = position.y
translation.columns.3.z = position.z
matrix = translation
renderEncoder.setVertexBytes(
&matrix,
length: MemoryLayout<matrix_float4x4>.stride,
index: 11)
您将使用此矩阵来平移顶点着色器中的position。
➤ 打开 Shaders.metal,然后更改:
Swift
constant float3 &position [[buffer(11)]])
为:
Swift
constant float4x4 &matrix [[buffer(11)]])
您将接收传入着色器中的矩阵。
➤ 在 vertex 函数中,更改:
Swift
float3 translation = in.position.xyz + position;
为:
Swift
float3 translation = in.position.xyz + matrix.columns[3].xyz;
使用矩阵的第四列作为位移向量。
➤ 构建并运行。到目前为止,输出是相同的。

请记住,此矩阵还将保存旋转和缩放信息。要计算position,您需要执行矩阵乘法,而不是添加平移位移向量。
➤ 将 vertex 函数的内容改为:
Swift
float4 translation = matrix * in.position;
VertexOut out {
.position = translation
};
return out;
➤ 构建并运行应用程序,您会发现仍然没有变化。
现在,您可以在 Renderer 中向矩阵添加缩放和旋转,而无需每次更改着色器函数。
缩放
➤ 打开 Renderer.swift,然后在 draw(in:) 中找到第二个红色三角形中设置 matrix 的位置。
➤ 在 matrix = translation 之前,添加以下内容:
Swift
let scaleX: Float = 1.2
let scaleY: Float = 0.5
let scaleMatrix = float4x4(
[scaleX, 0, 0, 0],
[0, scaleY, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1])
您可以像这样初始化一个矩阵,将列定义为数组,而不是像在"平移"中所做的那样分配给列。scaleMatrix.columns.0 现在包含 [1.2, 0, 0, 0]。
无需过多地进行数学研究,就可以使用此代码来设置缩放矩阵。
➤ 将 matrix = translation更改为: matrix = scaleMatrix
将平移矩阵乘以缩放矩阵,而不是乘以平移矩阵。
➤ 构建并运行应用程序。

在 vertex 函数中,矩阵将三角形的每个顶点乘以 x 和 y 缩放因子。
➤ 将 matrix = scaleMatrix 更改为: matrix = translation * scaleMatrix
此代码平移缩放的三角形。
➤ 构建并运行应用程序。

旋转
执行旋转的方式与缩放类似。
➤ 将 matrix = translation * scaleMatrix,改为:
Swift
let angle = Float.pi / 2.0
let rotationMatrix = float4x4(
[cos(angle), -sin(angle), 0, 0],
[sin(angle), cos(angle), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1])
matrix = rotationMatrix
在这里,您可以设置围绕 z 轴旋转的角度(以弧度为单位)。
注意:Float.pi / 2.0 与 90° 相同,均为 1.5708 弧度。弧度是计算机图形学中的标准单位。这是将度数转换为弧度的公式:度数 * pi / 180 = 弧度。
➤ 构建并运行,您将看到红色三角形的每个顶点如何围绕原点 [0, 0, 0] 旋转 90°。

➤ 将 matrix = rotationMatrix 替换为:
Swift
matrix = translation * rotationMatrix * scaleMatrix
此代码首先缩放每个顶点,然后旋转,然后平移。
➤ 构建并运行。

矩阵运算的顺序很重要。尝试更改顺序,看看会发生什么。
缩放和旋转发生在原点 (坐标 [0, 0, 0])。但是,有时您可能希望围绕不同的点进行旋转。例如,当三角形处于其原始位置(即与灰色三角形相同的位置和旋转角度)时,让我们围绕三角形的最右边点旋转三角形。
要旋转三角形,您需要设置一个平移矩阵,其中向量位于原点和最右侧点之间,执行以下步骤:
-
使用平移矩阵平移所有顶点。
-
旋转。
-
再次平移回来。
➤ 在对红色小三角设置矩阵之前,添加以下代码:
Swift
translation.columns.3.x = triangle.vertices[2].x
translation.columns.3.y = triangle.vertices[2].y
translation.columns.3.z = triangle.vertices[2].z
将变换矩阵设置为(从原点)移动到(灰色)三角形的第三个顶点,即最右侧的点。
记住这些步骤。第 1 步是按距原点的距离平移所有顶点。您可以通过将矩阵设置为顶点的向量值并使用平移矩阵的逆矩阵来实现此目的。
在执行以下每个步骤后,不要忘记构建并运行应用程序,以便您可以查看矩阵乘法的作用。
➤ 将matrix = translation * rotationMatrix * scaleMatrix 更改为: matrix = translation.inverse
此代码将最右侧的顶点放在原点处,以相同的距离平移所有其他顶点。

➤ 将您刚刚输入的代码更改为:
Swift
matrix = rotationMatrix * translation.inverse
三角形绕原点旋转 90°。

➤ 将您刚刚输入的代码更改为:
matrix = translation * rotationMatrix * translation.inverse
匪夷所思!您正在执行按最右侧顶点与原点的距离平移每个顶点的所有步骤。之后,您将旋转每个顶点并再次将其平移回来,使三角形围绕其最右侧的点旋转。
