四元数相机

 
 

写在前面

  本文的坐标系及旋转均采用 OpenGL 的默认定义,位于右手系,并搭配使用 glm 数学库。
 

万向节锁和矩阵

  在很多场合下,需要一个可以朝任意方向旋转的相机,比如,空战类游戏,建模程序。但是绕着多个轴组合一些旋转会导致万向节锁(Gimbal lock)的发生,这会导致不正确的结果。关于万向节锁的产生原因,本文不再赘述,详情请见 Gimbal lock
  由于矩阵表示的是一个空间向另一个空间转换的变换关系,请见 Matrix。那么,依此类推,三维空间的旋转变换矩阵由旋转后的空间在世界空间的三个基来表示,简而言之,矩阵由三个轴构成。而产生万向节锁的原因就是旋转到一定程度,某一个轴可以被其他两个轴线性表示,那么就消失了一个维度,构成旋转空间的基础也就崩塌了。综上所述,只要使用矩阵来表示旋转,就有发生万向节锁的风险
 

Camera 和 CameraController

  本文的实现将 Camera 类和操纵部分分离,操纵部分被封装在 CameraController 中。Camera 类只有维持 View Matrix 和 Projection Matrix 的功能。请见 Camera 类声明:

    class Camera
    {
    public:
        Camera();
        void ViewParams(glm::vec3 const & eye_pos, glm::vec3 const &lookat);
        void ViewParams(glm::vec3 const & eye_pos, glm::vec3 const &lookat, glm::vec3 const &up);
        void ProjParams(float fov, float aspect, float near_plane, float far_plane);
        void ProjOrthoParams(float w, float h, float near_plane, float far_plane);

        const glm::mat4 &ViewMatrix() const { return view_mat_; }
        const glm::mat4 &InverseViewMatrix() const { return inv_view_mat_; }
        const glm::mat4 &ProjMatrix() const { return proj_mat_; }
        const glm::mat4 &InverseProjMatrix() const { return inv_proj_mat_; }

        const glm::vec3 &EyePos() const { return *reinterpret_cast<const glm::vec3 *>(&inv_view_mat_[3]); }
        const glm::vec3 &RightVec() const { return *reinterpret_cast<const glm::vec3 *>(&inv_view_mat_[0]); }
        const glm::vec3 &UpVec() const { return *reinterpret_cast<const glm::vec3 *>(&inv_view_mat_[1]); }
        glm::vec3 ForwardVec() const { return -(*reinterpret_cast<const glm::vec3 *>(&inv_view_mat_[2])); }
        glm::vec3 LookAt() const { return this->EyePos() + this->ForwardVec() * this->LookAtDist(); }

        float LookAtDist() const { return look_at_dist_; }
        float FOV() const { return fov_; }
        float Aspect() const { return aspect_; }
        float NearPlane() const { return near_plane_; }
        float FarPlane() const { return far_plane_; }

    private:
        float        look_at_dist_;            // 相机视野距离,可用于第三人称相机
        glm::mat4    view_mat_;            // view matrix
        glm::mat4    inv_view_mat_;   // view matrix 的逆矩阵

        float        fov_;                         // y 方向视野角
        float        aspect_;                   // 长宽比
        float        near_plane_, far_plane_;          // 近平面和远平面
        glm::mat4    proj_mat_;          // projection matrix
        glm::mat4    inv_proj_mat_;   // projection matrix 的逆矩阵
    };

 

旋转的描述

  旋转,应该是三种坐标变换——缩放、旋转和平移,中最复杂的一种了。矩阵旋转使用了一个4*4大小的矩阵来表示绕任意轴旋转的变换矩阵,而欧拉选择则是按照一定的坐标轴顺序(例如先x、再y、最后z),每个轴旋转一定角度来变换坐标或向量,它实际上是一系列坐标轴旋转的组合。
 

yaw、pitch 和 roll

Yaw, pitch 和 roll 其实是来自欧拉角,可以视为三维空间中旋转的基本元素。通过按特定顺序结合这三种旋转,就可以表示空间中任何的方向。如下图所示,

![yaw pitch roll][1]


  • Yaw 表示偏航角,绕 Y (上)轴 (0,1,0) 旋转。头部左转表示为正,右转表示为负。
  • Pitch 表示俯仰角,绕 X (右)轴 (1,0,0) 旋转。抬头表示为正,低头表示为负。
  • Roll 表示翻滚角,绕 Z ( 前)轴 (0,0,1) 旋转。头部向左倾斜表示为正,向右倾斜表示为负。

  如果已经通过鼠标或者键盘得到了 yaw、pitch 和 roll 的值,就可以通过类似下面的方法计算得到 view 矩阵。

glm::mat4 CalculateView(float yaw, float pitch, float roll, glm::vec3 eye_pos)
{
 glm::mat4 matRoll  = glm::rotate(matRoll,  roll,  glm::vec3(0.0f, 0.0f, 1.0f));
 glm::mat4 matPitch = glm::rotate(matPitch, pitch, glm::vec3(1.0f, 0.0f, 0.0f));
 glm::mat4 matYaw   = glm::rotate(matYaw,  yaw,    glm::vec3(0.0f, 1.0f, 0.0f));

 // 顺序是非常重要的
 glm::mat4 rotate =  matYaw * mattRoll * matPitch;

 glm::mat4 translate = glm::translate(translate, -eye_pos);

 viewMatrix = rotate * translate;
}

  在上面的代码中,matYaw,matRoll 和 matPitch 的顺序是非常重要的。虽然理论上三者任意组合都可以表示三维中的方向,但是这是完全没有约束的情况下。在实际使用中,我们规定了上方向和前方向,这就是对四元数的一个限制。这个规定说明了,如果我们需要让旋转往认知的方向靠拢就必须有一定的顺序。
 

四元数

  在计算机图形学中,四元数本质上是一种高阶复数,是一个四维空间。四元数的虚部包含了三个虚数单位,i、j、k,即一个四元数可以表示为 x = a + bi + cj + dk。那么,它和旋转为什么会有关系呢?
  给定一个单位长度的旋转轴 (x,y,z) 和一个角度 w ,对应的四元数为:q=(cos{\theta\over 2},(x,y,z)sin{\theta\over 2})  有的库规定 w 在最后,glm 库规定 w 在最前,但这并不影响使用。这种四元数也叫作单位四元数,如下图所示。

![unit quaternion][2]

  一个四元数可以和一个矩阵旋转对应,而没有了旋转矩阵的万象节锁困扰。使用四元数来表示旋转有多方面的原因:

  • 需要更少的存储空间(4 floats vs 16 floats)
  • 绕任意轴旋转非常方便,而旋转矩阵实现非常复杂
  • 方便追踪旋转
  • 旋转结合时计算量较少
  • 方便平滑插值,而旋转矩阵的实现方法不能保证绝对平滑
     

四元数实现

  由于四元数用四维欧几里得空间来表示三维球面,所以四元数非常难可视化,可能需要花费一些时间来理解他们是如何工作的。但是,除此之外,如果只是不求甚解,按照严格证明的公式进行操作还是非常简单的。使用四元数创建第一人称相机需要一些东西:


  • 需要一个基准方向,后面所有的方向都由基准方向转换而来。
  • 获取 pitch,yaw,roll 信息
  • 将 pitch,yaw,roll 各自转换成四元数
  • 将转换成的四元数对基准方向进行转换

  如果上述条件都具备了之后,非常容易就能将矩阵替换成四元数。

glm::mat4 CalculateView(float yaw, float pitch, float roll, glm::vec3 eye_pos)
{
  glm::quat key_quat = glm::quat(glm::vec3(pitch, yaw, roll));

  camera_quat = key_quat * camera_quat;
  camera_quat = glm::normalize(camera_quat);
  glm::mat4 rotate = glm::mat4_cast(camera_quat);

  glm::mat4 translate = glm::translate(translate, -eyeVector);

  viewMatrix = rotate * translate;
 }

  实现之后得到的结果如下面所示:


first camera quaternion

  但是,这种简单直接的方法其实有个缺陷。它只根据上一次的正方向作为基准方向,也就是说经过一次旋转之后世界的前向量、上向量、右向量都发生了旋转。如果是为了空战游戏,那么第一人称相机到这一步就可以使用了。注意到上面视频的最后发生了一些偏移吗?
 

优化

  如果要像通常的相机一样有一个恒定的上向量,就需要存储一些而外的变量,而这个变量就是球面坐标系的 \theta\phi 角,如下图所示,定义了如下所示的 \theta\phi 角,相机在右手坐标系中默认 Y 轴为上轴。



  而 \theta\phi 角并不是直接存在程序上,会将它们转化为正弦、余弦来进行存储。最终,我的第一人称相机代码如下所示:

       inline void sincos(float x, float &sin, float &cos)
    {
        sin = std::sin(x);
        cos = std::cos(x);
    };
    void FirstPersonCameraController::RotateRelated(float yaw, float pitch, float roll)
    {
        if (camera_) {
            glm::vec2 delta_pitch, delta_yaw_, delta_roll;
            sincos(yaw, delta_yaw_.x, delta_yaw_.y);
            sincos(pitch, delta_pitch.x, delta_pitch.y);
            sincos(roll, delta_roll.x, delta_roll.y);

            rot_pitch_ = glm::vec2(
                rot_pitch_.x * delta_pitch.y + rot_pitch_.y * delta_pitch.x,
                rot_pitch_.y * delta_pitch.y - rot_pitch_.x * delta_pitch.x);
            rot_yaw_ = glm::vec2(
                rot_yaw_.x * delta_yaw_.y + rot_yaw_.y * delta_yaw_.x,
                rot_yaw_.y * delta_yaw_.y - rot_yaw_.x * delta_yaw_.x);
            rot_roll_ = glm::vec2(
                rot_roll_.x * delta_roll.y + rot_roll_.y * delta_roll.x,
                rot_roll_.y * delta_roll.y - rot_roll_.x * delta_roll.x);

            glm::quat quat_pitch(rot_pitch_.y, rot_pitch_.x, 0, 0);
            glm::quat quat_yaw(rot_yaw_.y, 0, rot_yaw_.x, 0);
            glm::quat quat_roll(rot_roll_.y, 0, 0, rot_roll_.x);

            rot_ = quat_yaw * quat_pitch * quat_roll;

            glm::vec3 forward_vec = glm::rotate(rot_, glm::vec3(0, 0, -1));
            glm::vec3 up_vec = glm::rotate(rot_, glm::vec3(0, 1, 0));

            camera_->ViewParams(camera_->EyePos(), camera_->EyePos() + forward_vec * camera_->LookAtDist(), up_vec);
        }
    }

  这样,就保证了上轴的稳定。上面的代码展示了一些四元数的运算。
  在上面的代码中,rot_pitch_,rot_yaw_,rot_roll_ 都是类型为 glm::vec2 的成员变量。根据一开始四元数的定义,绕 X 轴旋转的变量写作四元数的形式为q=(cos{\theta\over 2},(x,0,0)sin{\theta\over 2})  那么 glm::vec2 就可以存储整个四元数的元素,在构造 quat_roll,quat_yaw,quat_pitch 中也可以体现出来。rot_pitch_,rot_yaw_,rot_roll_,用存储角度的变化量再叠加上这一帧的变化量,直接应用了四元数的乘法公式:\begin{matrix}
q_1 * q_2 =& (w_1 * w_2 – x_1 * x_2 – y_1 * y_2 -z_1 * z_2) \\
&+ (w_1 * x_2 + x_1 * w_2 + y_1 * z_2 – z_1 * y_2) i \\
&+ (w_1 * y_2 – x_1 * z_2 + y_1 * w_2 + z_1 * x_2) j \\
& + (w_1 * z_2 + x_1 * y_2 – y_1 * x_2 + z_1 * w_2) k
\end{matrix}

 

四元数相关证明请见:四元数及旋转有关证明

About the Author

3 thoughts on “四元数相机

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注