Bevy 坐标系统概览
Bevy 与多数游戏引擎类似,采用了多种坐标系统,每种系统都为特定任务量身定制。本文将主要探讨以下几种系统:
- 世界空间(World Space)
- 局部空间(Local Space)
- 全局(实体世界)空间(Global (Entity World) Space)
- 屏幕/UI 空间(Screen/UI Space)
- 视口(Viewport)
- 归一化设备坐标(Normalized Device Coordinates, NDC)。
全局世界坐标系统
Bevy 为其游戏世界采用的是右手坐标系,Y 轴向上。这是一个基础设定,影响着 3D 几何体和变换的解释方式。开发者可以通过“右手定则”来形象化轴向:通常,右手拇指指向 X 轴正方向,食指指向 Y 轴正方向,中指指向 Z 轴正方向。值得注意的是,此坐标系统在 2D 和 3D 场景中保持一致,这简化了在两者之间切换或共享逻辑的开发过程。
轴向详解
- +X 轴: 从左指向右。
- +Y 轴: 从下指向上(即“上方”)。
- +Z 轴 (2D 上下文): 从远指向近,实际上是“从屏幕内部指向观察者”。这用于 2D 精灵的层叠,Z 值较大的精灵会渲染在 Z 值较小的精灵之上
原点
- 对于 2D 场景,原点 (0.0, 0.0) 默认位于屏幕中心。
- 对于 3D 场景,原点是抽象游戏世界中的一个点,其在屏幕上的具体位置取决于相机的位置和朝向。
3D 特定约定
- 尽管 Y 轴始终指向上,但在 3D 环境中,前进方向通常被认为是沿着 -Z 轴。这是诸如 OpenGL 等系统中常见的约定。
- 这意味着一个位于 (0,0,0) 并面向“前方”的物体,其视线将朝向 (0,0,-1)。
- Bevy 的坐标系统与 Godot、Maya 和 OpenGL 相同,但与 Unity 相比,特别是在 Z 轴的朝向上有所不同。
实体特定坐标系统:Transform
与 GlobalTransform
实体变换简介
在 Bevy 中,实体在游戏世界中的位置、旋转和缩放通过变换组件进行控制。Bevy 为此使用了两个主要组件:Transform
和 GlobalTransform
。代表游戏世界中物体的实体通常需要同时拥有这两个组件。当插入 Transform
组件时,GlobalTransform
会被自动插入。
局部空间 (Transform
组件)
Transform
组件定义了实体相对于其父实体坐标系的位置、旋转和缩放。如果一个实体没有 Parent
组件,则其 Transform
是相对于世界原点的。
- 字段:
translation: Vec3
: 实体相对于其父实体的平移。在 2D 中,Z 分量常用于 Z 轴排序。rotation: Quat
: 实体相对于其父实体的朝向(旋转),以四元数表示。scale: Vec3
: 实体相对于其父实体的缩放。
- 开发者通常通过修改
Transform
组件来移动、旋转或缩放实体。例如,Transform::from_xyz(5.0, 6.0, 7.0)
会创建一个具有特定局部平移的变换。
世界空间 (GlobalTransform
组件)
GlobalTransform
组件表示实体在世界空间中的绝对位置、旋转和缩放。它是一个从实体局部坐标到世界空间坐标的仿射变换。它由 Bevy 内部管理,并通过逐级应用层级结构中每个祖先实体的 Transform
来计算得出。
GlobalTransform
中的平移、旋转和缩放不能直接作为独立字段访问,因为复杂的层级变换可能包含剪切(shearing),而剪切无法用简单的平移、旋转和缩放来表示。它以一种优化的方式存储(例如使用 Affine3A
结构)。存在一些方法尝试将其分解,如 translation()
、to_scale_rotation_translation()
和 compute_transform()
。
变换传播 (Transform Propagation)
Transform
和 GlobalTransform
之间的同步由一个名为“TransformSystem::TransformPropagate”的 Bevy 内部系统处理。该系统在 PostUpdate
调度阶段运行。
一个关键点是:当一个 Transform
被修改时,对应的 GlobalTransform
不会立即更新。它们会保持不同步,直到变换传播系统运行。可以使用 TransformHelper
系统参数在传播之前获取最新的 GlobalTransform
,但这需要考虑性能影响 , 因为TransformHelper
只是帮你转换最新的GlobalTransform
的数据, 并没有更新组件里的数据.后面Bevy会再运算一次.
设计考量与影响
父子关系和局部 Transform
组件使得构建复杂的、有关节的物体(例如角色的肢体、带有炮塔的车辆)变得容易。对父实体变换的修改会自动影响其所有子实体。这种层级系统 是游戏引擎场景图的基础。开发者设置的局部 Transform
与计算得出的世界状态 GlobalTransform
之间的区别是核心。潜在的问题源于 GlobalTransform
的延迟更新。如果在 Update
阶段的一个系统修改了某个 Transform
,而同一阶段的另一个系统在该帧的 PostUpdate
之前查询其 GlobalTransform
,它将获得过时的数据。如果不理解这一点,可能会导致难以察觉的错误。
虽然开发者主要操作 Transform
,但最终决定实体在世界中如何显示和交互的是 GlobalTransform
。渲染系统、物理引擎和射线投射通常会使用 GlobalTransform
。GlobalTransform
是到 世界空间坐_的仿射变换。
GlobalTransform
可能由于层级结构中的剪切而无法直接表示为简单的平移/旋转/缩放,并且以 Affine3A
形式存储 ,这表明了优化和正确性的考量。仿射矩阵 (Affine3A
) 可以表示平移、旋转、缩放和剪切的任意组合。如果一个子实体被非均匀缩放,然后其父实体再进行旋转,最终的全局变换可能包含剪切。试图将其强制转换回独立的 Vec3
(缩放)和 Quat
(旋转)可能会丢失信息。将其存储为矩阵(或仿射变换结构)保留了完整的变换信息,这对于复杂的层级结构是一种更鲁棒的方法。
屏幕、UI 及光标坐标系统
与世界坐标的区别
Bevy 为 UI 元素、屏幕空间效果和光标/鼠标输入使用一个独立的坐标系统,该系统遵循 UI 工具和 Web 开发中常见的约定。
原点
原点 (0.0, 0.0) 位于屏幕或 UI 节点的左上角。
轴向
-
+X 轴: 从左指向右,范围从 0.0 (屏幕左边缘) 到屏幕宽度像素数 (屏幕右边缘)。
-
+Y 轴: 向下指向,范围从 0.0 (屏幕上边缘) 到屏幕高度像素数 (屏幕下边缘)。
单位
单位表示逻辑像素,这些像素已针对 DPI (每英寸点数) 缩放进行了补偿。这确保了 UI 元素在不同像素密度的显示器上显示尺寸一致。
用途
- 主要用于 UI 布局,通常从上到下排列。
- 光标位置和其他窗口 (屏幕空间) 坐标也遵循此约定。
- 例如,
UiScale
资源可以影响 UI 的缩放。
相机、视口与归一化设备坐标 (NDC)
Camera
组件的角色
相机是 Bevy 渲染的核心;它们定义了要绘制什么、如何绘制以及在哪里绘制(绘制到哪个渲染目标)。至少需要一个相机实体才能显示任何内容。Camera
组件存储了诸如投影类型(透视/正交)、视场角 (FOV)、裁剪平面、渲染目标和视口等属性。
视口 (Viewport)
Viewport
是渲染目标(例如窗口)内的一个可选矩形区域,相机将内容渲染到该区域。如果未设置,相机将渲染到整个渲染目标。视口坐标通常以物理像素为单位,定义了渲染目标的一个子区域。Camera
组件拥有如 logical_viewport_size()
和 physical_viewport_size()
等方法来获取此视口的尺寸。
归一化设备坐标 (Normalized Device Coordinates, NDC)
NDC 是渲染管线中,在投影变换之后、视口变换之前使用的一种规范化的中间坐标系统。在 NDC 中,可见体积(视锥体)被映射到一个立方体。X 和 Y 轴的坐标通常范围从 -1.0 到 1.0,Z 轴(深度)的坐标范围从 0.0 到 1.0。Bevy 的 Camera
提供了在世界坐标与 NDC 之间转换的方法:world_to_ndc()
和 ndc_to_world()
。
投影 (Projection)
相机的投影(例如,通过 Camera3d
或 Camera2d
设置的 Projection::Perspective
或 Projection::Orthographic
)决定了 3D 世界坐标如何映射到 2D 屏幕坐标。透视投影产生真实的深度感,物体越远显得越小 ,FOV 是其关键参数。正交投影则无论物体距离远近都以相同尺寸渲染,适用于 2D 游戏、CAD 或特定艺术风格。
设计考量与影响
NDC 是图形管线中一个关键的、通常“不可见”的标准化空间。所有 3D 几何体,无论其世界坐标或相机投影如何,都会被转换到这个通用的 [-1,1] 立方体(或类似范围)中,然后才被映射到屏幕像素。GPU 的光栅化器在归一化空间中操作。通过将所有内容转换到 NDC ,后续的裁剪(丢弃视图外的几何体)和视口映射(将 NDC 缩放和平移到像素坐标)步骤变得通用,并且独立于原始世界尺度或相机设置。理解 NDC 是掌握 3D 投影基本工作原理的关键。
视口 不仅仅用于定义渲染区域;它对于实现分屏多人游戏、小地图或渲染到纹理的特定区域等技术至关重要。每个 Camera
都可以有自己的视口。这允许多个相机渲染到同一窗口的不同部分。相机视图与整个窗口的解耦为呈现方式提供了显著的灵活性。
Bevy 坐标系统比较
特性 | 世界空间 (2D/3D 场景) | 实体局部空间 (Transform) | 实体世界空间 (GlobalTransform) | 屏幕/UI/光标空间 | 视口空间 | 归一化设备坐标 (NDC) |
---|---|---|---|---|---|---|
原点 | 默认:屏幕中心 (2D), 游戏定义 (3D) | 相对于父实体的原点 | 世界中的绝对位置 | 窗口/UI 节点的左上角 | 渲染目标内视口矩形的左上角 | 投影视图的中心 (X=0, Y=0, Z=0.5) |
X-轴 | 指向右 | 相对于父实体的 X-轴 | 世界中指向右 | 指向右 | 指向右 | 指向右 (-1 到 1) |
Y-轴 | 指向上 | 相对于父实体的 Y-轴 | 世界中指向上 | 指向下 | 指向下 | 指向上 (-1 到 1) |
Z-轴 | 指向观察者 (2D), -Z 常为前进方向 (3D) | 相对于父实体的 Z-轴 | 世界中指向观察者/前进方向 | N/A (或用于分层) | (深度, 视上下文而定) | “出”屏幕/深度 (0 到 1) |
手性 | 右手 | (继承/定义局部框架) | 右手 | (通常为 2D, N/A) | (通常为 2D, N/A) | 左手 (通常在投影后) |
单位 | 游戏定义的逻辑单位 | 游戏定义的逻辑单位 | 游戏定义的逻辑单位 | 逻辑像素 | 物理像素 | 归一化 (-1 到 1 或 0 到 1) |
主要用途 | 定位所有游戏元素, 全局参考 | 定义实体相对于其父实体的姿态 | 实体的最终世界姿态,用于渲染/物理 | UI 布局, 鼠标输入 | 定义渲染目标的子区域 | 渲染管线的中间步骤, 裁剪 |
坐标系统间的转换
Camera
组件提供了在不同空间之间转换坐标的主要 API,特别是涉及世界、视口/屏幕和 NDC 的转换。下表总结了 bevy::render::camera::Camera
中核心的坐标转换 API:
方法 | 输入参数 | 输出 | 主要用例 |
---|---|---|---|
world_to_viewport(…) | &GlobalTransform (相机), Vec3 (世界坐标) | Result<Vec2, ViewportConversionError> | 获取 3D 世界点对应的 2D 屏幕像素坐标。 |
viewport_to_world(…) | &GlobalTransform (相机), Vec2 (视口坐标) | Result<Ray3d, ViewportConversionError> | 从相机通过屏幕点向 3D 世界投射一条射线 (用于 3D拾取)。 |
viewport_to_world_2d(…) | &GlobalTransform (相机), Vec2 (视口坐标) | Result<Vec2, ViewportConversionError> | 从屏幕点获取 2D 世界坐标 (用于 2D 游戏/正交相机)。 |
world_to_ndc(…) | &GlobalTransform (相机), Vec3 (世界坐标) | Option | 将世界坐标转换为归一化设备坐标。 |
ndc_to_world(…) | &GlobalTransform (相机), Vec3 (NDC 坐标) | Option | 将归一化设备坐标转换回世界坐标。 |
logical_viewport_size() | &self | Option | 获取相机视口的逻辑尺寸。 |
physical_viewport_size() | &self | Option | 获取相机视口的物理像素尺寸。 |