【问题标题】:Quaternion-based First Person View Camera基于四元数的第一人称视角相机
【发布时间】:2018-04-02 10:37:12
【问题描述】:

我一直在按照教程学习 OpenGL,该教程位于 https://paroj.github.io/gltut/

通过基础知识后,我在理解四元数及其与空间方向和变换的关系方面有点卡住了,尤其是从世界空间到相机空间,反之亦然。在Camera-Relative Orientation一章中,作者制作了一个相机,它在世界空间中相对于相机方向旋转模型。引用:

我们想要应用一个方向偏移 (R),它在相机空间中获取点。如果我们想将其应用于相机矩阵,只需将其乘以相机矩阵:R * C * O * p。这很好,但我们想对 O 应用变换,而不是对 C。

我未受过教育的猜测是,如果我们将偏移应用到相机空间,我们将获得第一人称相机。它是否正确?相反,偏移应用于 world 空间中的模型,使飞船相对于该空间而非相机空间旋转。我们只是观察它从相机空间旋转。

至少受到对四元数的一些理解的启发(或者我是这么认为的),我尝试实现第一人称相机。它有两个属性:

struct Camera{ 
  glm::vec3 position; // Position in world space.
  glm::quat orientation; // Orientation in world space.
}

位置会随着键盘动作而改变,而方向会因鼠标在屏幕上的移动而改变。

注意:GLM 重载了 glm::quat * glm::vec3* 运算符,其关系是将矢量旋转四元数(v' = qvq^-1 的更紧凑形式)

例如向前和向右移动:

glm::vec3 worldOffset;
float scaleFactor = 0.5f;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
    worldOffset = orientation * (axis_vectors[AxisVector::AXIS_Z_NEG]); // AXIS_Z_NEG = glm::vec3(0, 0, -1)
    position += worldOffset * scaleFactor;
}
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
    worldOffset = orientation  * (axis_vectors[AxisVector::AXIS_X_NEG]); // AXIS_Z_NEG = glm::vec3(-1, 0, 0)
    position += worldOffset * scaleFactor;
}

方向和位置信息被传递给glm::lookAt 矩阵,用于构建世界到相机的转换,如下所示:

auto camPosition = position;
auto camForward = orientation * glm::vec3(0.0, 0.0, -1.0);
viewMatrix = glm::lookAt(camPosition, camPosition + camForward, glm::vec3(0.0, 1.0, 0.0));

结合模型、视图和投影矩阵并将结果传递给顶点着色器显示一切正常 - 就像人们期望从第一人称 POV 看到事物的方式一样。但是,当我添加鼠标移动,跟踪 x 和 y 方向的移动量时,事情变得一团糟。我想绕 world y 轴和 local x 轴旋转:

auto xOffset = glm::angleAxis(xAmount, axis_vectors[AxisVector::AXIS_Y_POS]); // mouse movement in x-direction
auto yOffset = glm::angleAxis(yAmount, axis_vectors[AxisVector::AXIS_X_POS]); // mouse movement in y-direction
orientation = orientation * xOffset; // Works OK, can look left/right
orientation = yOffset * orientation; // When adding this line, things get ugly

这里会出现什么问题? 我承认,我没有足够的知识来正确调试鼠标移动代码,我主要是照着说“右乘以在世界空间中应用偏移量,左乘以在相机空间中执行”。

我觉得我了解了一半,从有关该主题的大量电子资源中得出结论,同时得到更多的教育和更多的困惑。 感谢您的任何回答。

【问题讨论】:

    标签: c++ opengl glm-math


    【解决方案1】:

    旋转代表方向的 glm 四元数:

    //Precomputation: 
    //pitch (rot around x in radians), 
    //yaw (rot around y in radians), 
    //roll (rot around z in radians)
    //are computed/incremented by mouse/keyboard events
    

    计算视图矩阵:

    void CameraFPSQuaternion::UpdateView()
    {
      //FPS camera:  RotationX(pitch) * RotationY(yaw)
      glm::quat qPitch = glm::angleAxis(pitch, glm::vec3(1, 0, 0));
      glm::quat qYaw = glm::angleAxis(yaw, glm::vec3(0, 1, 0));
      glm::quat qRoll = glm::angleAxis(roll,glm::vec3(0,0,1));  
    
      //For a FPS camera we can omit roll
      glm::quat orientation = qPitch * qYaw;
      orientation = glm::normalize(orientation);
      glm::mat4 rotate = glm::mat4_cast(orientation);
    
      glm::mat4 translate = glm::mat4(1.0f);
      translate = glm::translate(translate, -eye);
    
      viewMatrix = rotate * translate;
    }
    

    如果您想存储四元数,那么您可以在偏航、俯仰或滚动发生变化时重新计算它:

    void CameraFPSQuaternion::RotatePitch(float rads) // rotate around cams local X axis
    {
      glm::quat qPitch = glm::angleAxis(rads, glm::vec3(1, 0, 0));
    
      m_orientation = glm::normalize(qPitch) * m_orientation;
      glm::mat4 rotate = glm::mat4_cast(m_orientation);
    
      glm::mat4 translate = glm::mat4(1.0f);
      translate = glm::translate(translate, -eye);
    
      m_viewMatrix = rotate * translate;
    }
    

    如果要给出围绕给定轴的旋转速度,请使用 slerp:

    void CameraFPSQuaternion::Update(float deltaTimeSeconds)
    {
      //FPS camera:  RotationX(pitch) * RotationY(yaw)
      glm::quat qPitch = glm::angleAxis(m_d_pitch, glm::vec3(1, 0, 0));
      glm::quat qYaw = glm::angleAxis(m_d_yaw, glm::vec3(0, 1, 0));
      glm::quat qRoll = glm::angleAxis(m_d_roll,glm::vec3(0,0,1));  
    
      //For a FPS camera we can omit roll
      glm::quat m_d_orientation = qPitch * qYaw;
      glm::quat delta = glm::mix(glm::quat(0,0,0,0),m_d_orientation,deltaTimeSeconds);
      m_orientation = glm::normalize(delta) * m_orientation;
      glm::mat4 rotate = glm::mat4_cast(orientation);
    
      glm::mat4 translate = glm::mat4(1.0f);
      translate = glm::translate(translate, -eye);
    
      viewMatrix = rotate * translate;
    }
    

    【讨论】:

    • 我很欣赏这个答案,但这里 SLERP 的用例是什么?我可以通过简单地将代表它们的两个四元数相乘来执行两次旋转,而无需 SLERPing。我主要对如何正确实现围绕局部 x 轴的旋转感兴趣 - 即相机空间和世界空间之间的正确转换顺序以实现这一目标。
    • @blairo SLERP 用于在初始方向和最终方向之间进行平滑过渡。您描述的方法执行离散转换,无法控制旋转速度。
    • 此用例基于您能够围绕给定轴设置角速度(由 delta_pitch 等表示)。我会用更直接的方法更新我的答案。
    • @IanYoung 嘿!在 Free look 的情况下,这台相机是否会考虑云台锁定?
    • @VigneshGunasekaran 万向节锁对于四元数来说并不是真正的问题,因为它本质上表示一个向量,以及围绕该向量的旋转角度。例如,万向节锁定是当您的“向上”矢量最终与您的“向前”矢量相同时,这会破坏视图矩阵。四元数巧妙地解决了这个问题。
    【解决方案2】:

    问题在于使用glm::lookAt 构造视图矩阵。相反,我现在正在构建视图矩阵,如下所示:

    auto rotate = glm::mat4_cast(entity->orientation);
    auto translate = glm::mat4(1.0f);
    translate = glm::translate(translate, -entity->position);
    viewMatrix = rotate * translate;
    

    对于翻译,我现在只乘以方向的倒数而不是方向。

    glm::quat invOrient = glm::conjugate(orientation);
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
      worldOffset = invOrient * (axis_vectors[AxisVector::AXIS_Z_NEG]);
      position += worldOffset * scaleFactor;
    }
    ...
    

    除了鼠标移动代码中的一些偏移四元数归一化之外,其他一切都相同。

    相机现在的行为和感觉就像第一人称相机。

    我仍然没有正确理解视图矩阵和查看矩阵之间的区别,如果有的话。但这是另一个问题的主题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-24
      • 1970-01-01
      • 1970-01-01
      • 2012-07-07
      • 2022-07-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多