Metal - 9. 深入剖析 3D 场景
一个 3D 场景通常由一个或多个摄影机、光源和模型构成。随着项目的复杂性增加,如果在 Renderer
类中处理所有这些对象的添加和复杂的游戏逻辑,将变得越来越不切实际。更优的选择是将**场景设置(scene setup)和游戏逻辑(game logic)**从渲染代码中分离出来,进行抽象化处理。
本章的核心目标是创建一个抽象的场景结构来容纳模型,并引入一个摄影机结构,使得开发者可以在一个新的文件中设置和更新场景,而无需涉足复杂的渲染器细节。
一、 启动项目 (The Starter Project)
Navigating a 3d Scene
1. 场景抽象(Scene Abstraction)
项目设置了一个 GameScene
类,其主要职责是封装场景代码和设置 (abstract game code and scene setup away from the rendering code)。
GameScene
结构体持有场景中需要渲染的所有模型(例如house
和ground
),这些模型被存储在一个models: [Model]
数组中。- 模型通常采用 惰性初始化(lazy var) 的方式创建,例如初始化房屋模型 (
Model(name: "lowpoly-house.obj")
) 或地面模型 (plane.obj
),并在初始化时设置模型的属性(如tiling
和scale
)。 - 通过将场景逻辑封装在
GameScene
中,Renderer
类的工作被简化为只负责实例化GameScene
,并迭代调用数组中的模型进行渲染。
2. 摄影机结构(Camera Structure)
相机是移动和导航场景的必备元素。一个相机需要位置(position)和旋转(rotation)信息,因此相机结构体必须遵守可变换协议(Transformable protocol)。
- 所有的相机都拥有投影矩阵 (Projection Matrix) 和视图矩阵 (View Matrix)。
- 相机结构体必须实现方法来处理窗口大小变化(例如更新宽高比)以及每帧的更新。
- 本章首先创建一个名为
FPCamera
(第一人称相机,First-Person Camera)的结构体,它包含一个Transform
属性,并遵守Camera
协议。最终,该相机将支持使用键盘控制在 3D 场景中移动。
二、 输入处理(Input)
为了实现场景导航,需要处理来自用户的输入。输入可以来自各种形式,包括游戏控制器、键盘、鼠标和触控板。
1. GCController API
在 macOS 和 iPadOS 上,Apple 提供了 GCController API 来处理这些类型的输入。
2. 事件与轮询(Events or Polling)
GCController API 支持两种输入处理模式:
- 事件或中断 (Events or Interrupts):当用户按下或释放按键时触发动作。可以通过设置代理方法或闭包(closures)在事件发生时运行代码。
- 轮询 (Polling):在每一帧中处理所有当前被按下的按键状态。
本章采用轮询模式进行处理:
- 代码通过添加观察者(observer)来设置
keyChangedHandler
,该处理器在键盘首次连接到应用时运行。 - 当玩家按下或抬起按键时,
keyChangedHandler
会运行,并将对应的按键代码(keyCode
)添加到keysPressed
集合中,或从中移除。 - 这种方法使得在渲染循环的更新阶段可以随时检查当前按下了哪些键。
3. 设置参数(Settings)
为了让相机和鼠标移动平滑,项目引入了 Settings
结构体来存储关键参数。这些参数通过与**时间增量(Delta Time)**结合,用于计算每帧的精确运动量:
rotationSpeed
:相机每秒应旋转的弧度。translationSpeed
:相机每秒应移动的距离。mouseScrollSensitivity
和mousePanSensitivity
:用于调整鼠标追踪和滚动的敏感度。
三、 相机移动(Camera Movement)
本节实现 WASD 键控制的第一人称相机移动:W(前进)、A(向左平移)、S(后退)、D(向右平移)。
1. 移动原理
- 相机的移动发生在 X 轴和 Z 轴上。
- 相机需要一个方向向量 (direction vector)。当按下 W 键时,相机将沿着 Z 轴的正方向移动。
- 如果同时按下 W 和 D 键,相机将斜向移动 (move diagonally)。
- 按下左右箭头键会改变相机的旋转,从而改变相机的前进方向向量 (forward direction vector)。
2. 前进向量的计算
forwardVector
是一个关键的计算属性,用于确定相机在世界空间中的朝向。
- 计算公式:基于当前的 Y Y Y 轴旋转值 (
rotation.y
),前进向量被计算为normalize([sin(rotation.y), 0, cos(rotation.y)])
。 - 右侧向量 (rightVector):用于实现平移(strafing)的右侧向量是与前进向量垂直的向量。
- 应用移动:在处理所有按下的按键后,会生成一个最终的
direction
向量(例如,按下 W 和 A 会产生[-1, 0, 1]
)。然后,计算出的translationAmount
(等于deltaTime * Settings.translationSpeed
)被用于缩放前进向量和右侧向量,并将结果加到相机的位置上 (transform.position
)。
四、 轨道球摄影机(Arcball Camera)
轨道球摄影机是一种特殊的相机类型,用于环绕一个目标点 (orbits a target point) 进行观察,而不是像 FPCamera 那样自由移动。
1. 关键属性
Arcball Camera 由以下三个属性定义:
- Target (目标点):相机围绕其轨道运行的点。
- Distance (距离):相机与目标点之间的距离。玩家通过鼠标滚轮控制缩放。
- Rotation (旋转):相机围绕目标点进行的旋转。玩家通过左键点击鼠标并拖拽来控制。
2. 鼠标输入集成
mouseMovedHandler
记录鼠标的 Δ X , Δ Y \Delta X, \Delta Y ΔX,ΔY 变化。scroll.valueChangedHandler
记录滚动的 Δ X , Δ Y \Delta X, \Delta Y ΔX,ΔY 变化。- 如果玩家拖拽鼠标左键,相机旋转值会根据
mouseDelta
进行更新。
3. 位置计算
Arcball Camera 的世界位置通过以下序列计算:
- 根据相机旋转 (
rotation
),创建一个rotateMatrix
。 - 创建一个
distanceVector
,例如float4(0, 0, -distance, 0)
。 - 将旋转矩阵与距离向量相乘,得到旋转后的向量
rotatedVector
。 - 最终位置是 目标位置 加上 旋转向量。
- 矩阵操作顺序:
float4x4(rotationYXZ:)
方法确保了旋转矩阵是按 Y 轴、X 轴、Z 轴的顺序进行组合的。
- 矩阵操作顺序:
五、 正交摄影机(Orthographic Camera)
1. 概念与用途
正交摄影机 渲染时没有透视效果 (renders without perspective)。这意味着所有渲染到 2D 屏幕上的顶点,看起来与摄影机的距离都是相同的,远处物体不会显得更小。
- 投影矩阵:正交投影矩阵定义了一个立方体视椎体(orthographic frustum)。
- 应用场景:正交相机常用于创建俯视整个棋盘的 2D 游戏,或在实现定向光源的阴影贴图时(因为定向光的光线是平行的)。
2. 缩放与宽高比
- 相机的
update(size:)
方法会设置aspect
(宽高比),用于正交投影矩阵。 - 玩家可以通过鼠标滚动来改变
viewSize
,进而实现缩放或放大场景。
六、 Metal 与 OpenGL 3D 导航对比
Metal 和 OpenGL 在实现 3D 场景导航时,都遵循通过矩阵模拟摄影机的图形学原理,但在 API、数学库和坐标约定上有所不同。
概念 | Metal (《Metal Tutorials》) | OpenGL (《Learn OpenGL》) | 核心差异与相似点 |
---|---|---|---|
相机模拟 | 通过创建 Camera 结构体,计算 viewMatrix (通常是相机世界位置的逆矩阵)来实现场景的反向移动。 | OpenGL 本身不了解相机的概念,通过 view matrix 移动场景中的所有对象来产生相机移动的错觉。 | 核心原理一致,都是通过矩阵变换实现。 |
视图空间 | Camera Space 或 View Space。 | 通常称为 View Space 或 Camera Space 或 Eye Space。 | |
输入处理 | 依赖 Apple 平台的 GCController API 来处理键盘、鼠标和游戏控制器输入。 | 依赖于外部库(如 GLFW)来创建窗口并处理输入事件。 | Metal 的输入处理与 Apple 生态系统深度集成。 |
数学库 | 使用 Apple 的原生 SIMD 框架进行矩阵和向量操作 (float4x4 , simd_float3 )。 | 依赖于 GLM (OpenGL Mathematics) 库 (glm::mat4 ) 来创建和操作变换矩阵。 | |
正交投影 | 使用 Orthographic Camera 实现无透视渲染,NDC 的 Z Z Z 轴范围是 0 0 0 到 1 1 1。 | Orthographic Projection 常用于 2D 游戏或阴影贴图。 NDC 的 X , Y , Z X, Y, Z X,Y,Z 轴范围都是 − 1.0 -1.0 −1.0 到 1.0 1.0 1.0。 | 投影后的 Z Z Z 轴范围不同。 |
变换组合 | 顶点着色器接收组合矩阵 (uniforms.projectionMatrix * uniforms.viewMatrix * uniforms.modelMatrix ) 并执行乘法。 | 顶点着色器执行矩阵乘法,通常顺序是 projection * view * model 。 | |
输出位置 | 最终顶点位置输出到 [[position]] 属性。 | 最终顶点位置写入内置变量 gl_Position 。 |