使用四元数实现OpenGL旋转

19
所以我正在编写一个程序,让对象以spacesim风格在空间中移动,以学习如何在三维空间中平稳地移动物体。在尝试了一些欧拉角之后,它们似乎并不适用于任意方向的自由形式三维运动,因此我决定转向似乎最适合这项工作的东西-四元数。我打算让对象始终围绕其本地X-Y-Z轴旋转,而不是围绕全局X-Y-Z轴旋转。
我尝试使用四元数实现旋转系统,但某些事情并没有起作用。当沿单个轴旋转物体时,如果没有进行先前的旋转,则物体沿着给定轴线很好地旋转。然而,在执行一次旋转之后再应用另一次旋转时,第二次旋转并不总是沿着它应该旋转的本地轴线进行-例如,在Z轴周围旋转约90°之后,绕Y轴的旋转仍然发生在全局Y轴周围,而不是与全局X轴对齐的新本地Y轴周围。
哦。那么让我们一步一步地进行。错误一定在这里某处。 步骤1-捕获输入

我认为最好使用欧拉角(或俯仰-偏航-翻滚方案)来捕获玩家输入。目前,箭头键控制俯仰和偏航,而Q和E控制翻滚。我这样捕获玩家输入(我正在使用SFML 1.6):

    ///SPEEDS
    float ForwardSpeed = 0.05;
    float TurnSpeed = 0.5;

    //Rotation
    sf::Vector3<float> Rotation;
    Rotation.x = 0;
    Rotation.y = 0;
    Rotation.z = 0;
    //PITCH
    if (m_pApp->GetInput().IsKeyDown(sf::Key::Up) == true)
    {
        Rotation.x -= TurnSpeed;
    }
    if (m_pApp->GetInput().IsKeyDown(sf::Key::Down) == true)
    {
        Rotation.x += TurnSpeed;
    }
    //YAW
    if (m_pApp->GetInput().IsKeyDown(sf::Key::Left) == true)
    {
        Rotation.y -= TurnSpeed;
    }
    if (m_pApp->GetInput().IsKeyDown(sf::Key::Right) == true)
    {
        Rotation.y += TurnSpeed;
    }
    //ROLL
    if (m_pApp->GetInput().IsKeyDown(sf::Key::Q) == true)
    {
        Rotation.z -= TurnSpeed;
    }
    if (m_pApp->GetInput().IsKeyDown(sf::Key::E) == true)
    {
        Rotation.z += TurnSpeed;
    }

    //Translation
    sf::Vector3<float> Translation;
    Translation.x = 0;
    Translation.y = 0;
    Translation.z = 0;

    //Move the entity
    if (Rotation.x != 0 ||
        Rotation.y != 0 ||
        Rotation.z != 0)
    {
        m_Entity->ApplyForce(Translation, Rotation);
    }

m_Entity 是我尝试旋转的物体。它还包含表示对象旋转的四元数和旋转矩阵。

第2步 - 更新四元数

我不确定这是否是应该这样做的方式,但这就是我在 Entity::ApplyForce() 中尝试做的。

//Rotation
m_Rotation.x += Rotation.x;
m_Rotation.y += Rotation.y;
m_Rotation.z += Rotation.z;

//Multiply the new Quaternion by the current one.
m_qRotation = Quaternion(m_Rotation.x, m_Rotation.y, m_Rotation.z);// * m_qRotation;

m_qRotation.RotationMatrix(m_RotationMatrix);

正如您所看到的,我不确定是最好只从更新的欧拉角构建一个新的四元数,还是应该将表示变化的四元数与表示整体当前旋转的四元数相乘,这是我在阅读this guide时得到的印象。如果是后者,我的代码将如下所示:

//Multiply the new Quaternion by the current one.
m_qRotation = Quaternion(Rotation.x, Rotation.y, Rotation.z) * m_qRotation;

m_Rotation 是对象当前以 PYR 格式存储的旋转;Rotation 是由玩家输入所需的变化。无论如何,问题可能在于我的四元数类的实现。以下是整个内容:

Quaternion::Quaternion(float Pitch, float Yaw, float Roll)
{
    float Pi = 4 * atan(1);

    //Set the values, which came in degrees, to radians for C++ trig functions
    float rYaw = Yaw * Pi / 180;
    float rPitch = Pitch * Pi / 180;
    float rRoll = Roll * Pi / 180;

    //Components
    float C1 = cos(rYaw / 2);
    float C2 = cos(rPitch / 2);
    float C3 = cos(rRoll / 2);
    float S1 = sin(rYaw / 2);
    float S2 = sin(rPitch / 2);
    float S3 = sin(rRoll / 2);

    //Create the final values
    a = ((C1 * C2 * C3) - (S1 * S2 * S3));
    x = (S1 * S2 * C3) + (C1 * C2 * S3);
    y = (S1 * C2 * C3) + (C1 * S2 * S3);
    z = (C1 * S2 * C3) - (S1 * C2 * S3);
}

//Overload the multiplier operator
Quaternion Quaternion::operator* (Quaternion OtherQuat)
{
    float A = (OtherQuat.a * a) - (OtherQuat.x * x) - (OtherQuat.y * y) - (OtherQuat.z * z);
    float X = (OtherQuat.a * x) + (OtherQuat.x * a) + (OtherQuat.y * z) - (OtherQuat.z * y);
    float Y = (OtherQuat.a * y) - (OtherQuat.x * z) - (OtherQuat.y * a) - (OtherQuat.z * x);
    float Z = (OtherQuat.a * z) - (OtherQuat.x * y) - (OtherQuat.y * x) - (OtherQuat.z * a);
    Quaternion NewQuat = Quaternion(0, 0, 0);
    NewQuat.a = A;
    NewQuat.x = X;
    NewQuat.y = Y;
    NewQuat.z = Z;
    return NewQuat;
}

//Calculates a rotation matrix and fills Matrix with it
void Quaternion::RotationMatrix(GLfloat* Matrix)
{
    //Column 1
    Matrix[0] = (a*a) + (x*x) - (y*y) - (z*z);
    Matrix[1] = (2*x*y) + (2*a*z);
    Matrix[2] = (2*x*z) - (2*a*y);
    Matrix[3] = 0;
    //Column 2
    Matrix[4] = (2*x*y) - (2*a*z);
    Matrix[5] = (a*a) - (x*x) + (y*y) - (z*z);
    Matrix[6] = (2*y*z) + (2*a*x);
    Matrix[7] = 0;
    //Column 3
    Matrix[8] = (2*x*z) + (2*a*y);
    Matrix[9] = (2*y*z) - (2*a*x);
    Matrix[10] = (a*a) - (x*x) - (y*y) + (z*z);
    Matrix[11] = 0;
    //Column 4
    Matrix[12] = 0;
    Matrix[13] = 0;
    Matrix[14] = 0;
    Matrix[15] = 1;
}

里面可能有一些让比我聪明的人感到不舒服的东西,但我看不出来。对于将欧拉角转换为四元数,我使用了this source中的“第一种方法”,该方法似乎还表明该方程式会自动创建一个单位四元数(“显然归一化”)。对于乘法四元数,我再次参考了this C++ guide

步骤3 - 从四元数中导出旋转矩阵

完成上述步骤后,根据R. Martinho Fernandes在this question中的回答,我尝试从四元数构建旋转矩阵,并使用该矩阵更新对象的旋转,使用上述Quaternion :: RotationMatrix()代码在以下行中:

m_qRotation.RotationMatrix(m_RotationMatrix);

我应该注意到,m_RotationMatrix 是 GLfloat m_RotationMatrix[16],根据 glMultMatrix所需的参数,我相信我在显示对象时会用到它。 它初始化为:

m_RotationMatrix = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};

我相信这是“中性”的OpenGL旋转矩阵(每4个值一起表示一列,对吗?再次,我从glMultMatrix页面获得此信息)。

步骤4 - 显示!

最后,我们来到了每个周期运行的函数,用于显示对象。

glPushMatrix();

glTranslatef(m_Position.x, m_Position.y, m_Position.z);
glMultMatrixf(m_RotationMatrix);

//glRotatef(m_Rotation.y, 0.0, 1.0, 0.0);
//glRotatef(m_Rotation.z, 0.0, 0.0, 1.0);
//glRotatef(m_Rotation.x, 1.0, 0.0, 0.0);

//glRotatef(m_qRotation.a, m_qRotation.x, m_qRotation.y, m_qRotation.z);

//[...] various code displaying the object's VBO

glPopMatrix();

我已经把之前失败的尝试注释掉了。

结论 - 悲伤的熊猫

这就是玩家输入生命周期的结论,从摇篮到OpenGL管理的坟墓。

显然我没有理解某些东西,因为我得到的行为不是我想要或期望的行为。但我对矩阵数学或四元数并不是特别有经验,所以我没有必要的洞察力来看到我的错误。

有人能帮帮我吗?


1
所有这些文字,你都没有提到你期望得到什么和你实际得到了什么。 - BЈовић
我确实说过“轴似乎保持固定于全局坐标系”,但再次检查后,情况并非完全如此(可能也不太清楚)。我将更新帖子的开头。 - GarrickW
2个回答

25

你所做的只是使用四元数有效地实现欧拉角,这并没有什么帮助。

欧拉角的问题在于,当计算矩阵时,每个角度都是相对于前一个矩阵旋转的。你想要做的是获取物体当前的方向,并沿着某个轴应用旋转,产生一个新的方向。

欧拉角无法做到这一点。矩阵可以做到,四元数也可以(因为它们只是矩阵的旋转部分)。但你不能假装它们是欧拉角。

这可以通过根本不存储角度来完成。相反,你只需拥有一个表示物体当前方向的四元数。当你决定对其应用旋转(某些轴上的某些角度),你构造一个表示绕该轴旋转一定角度的四元数。然后,将该四元数右乘以当前方向四元数,产生一个新的当前方向。

渲染物体时,你使用当前方向作为…… 方向。


好的,所以我已经从实体类中清除了所有角度存储。现在它只存储四元数。但是,如何将箭头键输入转换为四元数而不使用欧拉角呢?我是否必须实时计算给定输入将使对象围绕哪个轴旋转,考虑到其当前方向(由其四元数表示)?此外,从四元数中提取旋转矩阵的方法是否合适或不必要? - GarrickW
我可以简单地将[-1,0,1]中的一个传递给实体(Entity)的每个本地X-Y-Z轴,并让实体(Entity)解释它,但这不会归结于同样的问题吗? - GarrickW
1
啊,我刚刚发现了http://www.arcsynthesis.org/gltut/Positioning/Tutorial%2008.html上的教程-我要去那里找找看。 - GarrickW
所以我已经阅读了相关页面,但仍有一些不确定的地方。它说:“例如,当我们想要增加音高时,我们将采取当前方向并将其乘以表示几度俯仰旋转的四元数。”首先,这是否意味着从轴角创建四元数,其中角度是俯仰变化(比如5度),而轴(x、y、z)是(1,0,0),无论当前方向如何? - GarrickW
1
其次,由于四元数乘法是非交换的,如果玩家同时输入俯仰和翻滚变化怎么办?我只需要制作两个单独的偏移四元数,一个用于翻滚,一个用于俯仰,然后将它们相乘吗?我应该按什么顺序相乘? - GarrickW
1
"使用四元数有效地实现欧拉角。" 我也曾尝试过,花了两天时间来解决我的问题。在找到你的答案后,我在不到3分钟内修复了我的代码。该死。 - stil

2
四元数代表了围绕3D复合轴的方向。 但它们也可以表示“增量旋转”。
要“旋转方向”,我们需要一个方向(一个四元数)和一个旋转(也是一个四元数),然后将它们相乘,得到(你猜对了)一个四元数。
你注意到它们不是可交换的,这意味着我们相乘的顺序绝对重要,就像矩阵一样。 顺序往往取决于您的数学库的实现,但实际上只有两种可能的方法来完成它,所以您不应该花费太长时间来弄清楚哪个是正确的 - 如果事物“绕着”而不是“旋转”,那么您就把它们搞错了。
对于您的偏航和俯仰的示例,我会从偏航、俯仰和滚动角度构建我的“增量旋转”四元数,并将其应用于我的“方向”四元数,而不是逐个轴进行旋转。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接