如何创建第一人称 "space flight" 相机

How to create a first-person "space flight" camera

我目前正在尝试创建第一人称 space 飞行相机。

首先,请允许我定义一下我的意思。

注意我目前在我的数学库中使用行主矩阵(意思是,我的 4x4 矩阵中的基向量按行排列,仿射平移部分在第四行)。希望这有助于阐明我乘以矩阵的顺序。

到目前为止我有什么

至此,我已经成功实现了一个简单的第一人称视角。代码如下:

 fn fps_camera(&mut self) -> beagle_math::Mat4 {
    let pitch_matrix = beagle_math::Mat4::rotate_x(self.pitch_in_radians);
    let yaw_matrix = beagle_math::Mat4::rotate_y(self.yaw_in_radians);

    let view_matrix = yaw_matrix.get_transposed().mul(&pitch_matrix.get_transposed());
    let translate_matrix = beagle_math::Mat4::translate(&self.position.mul(-1.0));

    translate_matrix.mul(&view_matrix)
}

这按预期工作。我可以用鼠标四处走动和环顾四周。

我正在尝试做什么

然而,此实现的一个明显限制是,由于俯仰和偏航始终是相对于全局“向上”方向定义的,当我俯仰超过 90 度时,世界基本上是颠倒的,我的偏航运动是倒转的。

我想尝试实现的是更像是第一人称“space 飞行”相机。也就是说,无论您当前的方向是什么,用鼠标向上和向下倾斜在游戏中始终会转化为相对于您当前方向的向上和向下。使用鼠标左右偏航将始终转换为相对于您当前方向的左右方向。

不幸的是,这个问题已经困扰我好几天了。请耐心等待,因为我是线性代数和矩阵变换领域的新手。所以我一定是误解或忽略了一些基本的东西。因此,到目前为止我已经实现的可能看起来...愚蠢和天真 :) 可能是因为它是。

到目前为止我尝试过的

我总是回过头来思考这个问题的方式基本上是每一帧都重新定义世界的方向。也就是说,在一个框架中,您使用视图矩阵平移、俯仰和偏航世界坐标 space。然后您以某种方式将此方向重新定义为新的默认或零旋转。通过这样做,您可以在下一帧中根据这个新的默认方向应用新的俯仰和偏航旋转,这(无论如何,根据我的想法)意味着鼠标移动将始终直接转换为向上、向下、向左、是的,无论你如何定位,因为你基本上总是根据你以前的方向重新定义世界坐标 space,而不是简单的第一人称相机,它总是从相同的开始初始坐标 space.

我尝试实现我的想法的最新代码如下:

 fn space_camera(&mut self) -> beagle_math::Mat4 {
    let previous_pitch_matrix = beagle_math::Mat4::rotate_x(self.previous_pitch);
    let previous_yaw_matrix = beagle_math::Mat4::rotate_y(self.previous_yaw);
    let previous_view_matrix = previous_yaw_matrix.get_transposed().mul(&previous_pitch_matrix.get_transposed());

    let pitch_matrix = beagle_math::Mat4::rotate_x(self.pitch_in_radians);
    let yaw_matrix = beagle_math::Mat4::rotate_y(self.yaw_in_radians);

    let view_matrix = yaw_matrix.get_transposed().mul(&pitch_matrix.get_transposed());
    let translate_matrix = beagle_math::Mat4::translate(&self.position.mul(-1.0));

    // SAVES
    self.previous_pitch += self.pitch_in_radians;
    self.previous_yaw += self.yaw_in_radians;

    // RESETS
    self.pitch_in_radians = 0.0;
    self.yaw_in_radians = 0.0;

    translate_matrix.mul(&(previous_view_matrix.mul(&view_matrix)))
}

然而,这并不能解决问题。它实际上给出了与 fps 相机完全相同的结果和问题。

我在这段代码背后的想法基本上是:始终跟踪累积的俯仰和偏航(在 previous_pitch 的代码中previous_yaw) 基于每帧的增量。增量为 pitch_in_radianspitch_in_yaw,因为它们总是每帧重置。

然后我开始构建一个视图矩阵,该矩阵将表示世界之前的方向,即 previous_view_matrix。然后我根据这个帧的增量构造一个新的视图矩阵,即 view_matrix.

然后我尝试创建一个视图矩阵来执行此操作:

  1. 沿表示相机当前位置的相反方向平移世界。这里与 FPS 相机没有什么不同。
  2. 根据我到目前为止的方向定位那个世界(使用 previous_view_matrix。我希望它代表的是我当前帧运动的增量。
  3. 使用当前视图矩阵应用当前帧的增量,由view_matrix
  4. 表示

我希望在第 3 步中,以前的方向将被视为新旋转的起点。如果世界在以前的方向上是颠倒的,view_matrix 将根据相机的“向上”应用偏航,这样就可以避免倒置的问题控件。

我肯定是从错误的角度解决问题,或者误解了矩阵乘法与旋转的基本部分。

任何人都可以帮助确定我哪里出错了吗?

[编辑] - 即使你只俯仰和偏转相机也能滚动

对于任何偶然发现这个问题的人,我通过标记答案和 Locke 答案的组合来修复它(最终,在我的问题给出的例子中,我也弄乱了矩阵乘法顺序)。

此外,当您正确设置相机时,您可能会偶然发现奇怪的副作用,即让相机保持静止,然后简单地俯仰和偏转它(例如将鼠标绕圈移动)会导致在你的世界里也在慢慢滚动

这不是错误,这就是旋转在 3D 中的工作方式。凯文在他的回答中添加了一条评论来解释它,此外,我还发现 this GameDev Stack Exchange answer 对其进行了更详细的解释。

问题是俯仰和偏航这两个数字提供的 自由度不足 无法在 space 中表示一致的自由旋转行为,而没有任何“horizon”。两个数字可以表示 look-direction 向量 但它们不能表示相机方向的第三个分量,称为 roll(围绕“深度”旋转”屏幕的轴)。因此,无论您如何实现控件,您都会发现在某些方向上相机会奇怪地滚动,因为尝试使用此信息进行数学运算的 效果 是每一帧滚动 picked/reconstructed 基于俯仰和偏航。

对此的最小解决方案是向相机状态添加滚动组件。然而,这种方法(“欧拉角”)既难以计算又存在数值稳定性问题(“万向节锁定”)。

相反,您应该将 camera/player 方向表示为 四元数 ,这是一种适用于表示任意旋转的数学结构。四元数的使用有点像旋转矩阵,但分量更少;您将四元数乘以四元数以应用玩家输入,并将四元数转换为矩阵以进行渲染。

通用游戏引擎使用四元数来描述 objects' 旋转是很常见的。我还没有亲自编写四元数相机代码(还!),但我相信互联网上包含许多示例和更长的解释,您可以从中得到帮助。

看起来您遇到的很多困难是由于尝试规范化转换以应用新的翻译。看起来这可能是让你绊倒的很大一部分原因。我建议改变你存储位置和旋转的方式。相反,请尝试让您的视图矩阵定义您的位置。

/// Apply rotation based on the change in mouse position
pub fn on_mouse_move(&mut self, dx: f32, dy: f32) {
    // I think this is correct, but it might need tweaking
    let rotation_matrix = Mat4::rotate_xy(-y, x);

    self.apply_movement(&rotation_matrix, &Vec3::zero())
}

/// Append axis-aligned movement relative to the camera and rotation
pub fn apply_movement(&mut self, rotation: &Mat4<f32>, translation: &Vec3<f32>) {
    // Create transformation matrix for translation
    let translation = Mat4::translate(translation);

    // Append translation and rotation to existing view matrix
    self.view_matrix = self.view_matrix * translation * rotation;
}

/// You can get the position from the last column [x, y, z, w] of your view matrix.
pub fn translation(&self) -> Vec3<f32> {
    self.view_matrix.column(3).into()
}

我对图书馆做了几个假设:

  • Mat4 实现了 Mul<Self>,因此您无需显式调用 x.mul(y),而是可以使用 x * ySub.
  • 也是如此
  • 存在一个Mat4::rotate_xy函数。如果没有,则相当于 Mat4::rotate_xyz(delta_pitch, delta_yaw, 0.0)Mat4::rotate_x(delta_pitch) * Mat4::rotate_y(delta_yaw).

我有点盯着方程式所以希望这是正确的。主要思想是从之前的输入中获取增量并从中创建矩阵,然后可以将其添加到之前的 view_matrix 之上。如果您在创建转换矩阵后尝试取差,这只会为您(和您的处理器)带来更多工作。

作为旁注,我看到您正在使用 self.position.mul(-1.0)。这告诉我你的投影矩阵可能是倒退的。您可能希望通过在 z 轴上将其缩放 -1 来调整投影矩阵。