Использование кватернионов для вращения OpenGL

Итак, я пишу программу, где объекты перемещаются по пространству-стилю, чтобы научиться плавно перемещать вещи в 3D-пространстве. Немного поразмыслив с углами Эйлера, кажется, что они не подходят для движения в свободной форме в произвольных направлениях, поэтому я решил перейти к тому, что, по-видимому, лучше всего подходит для работы - кватернионов. Я намерен, чтобы объект вращался вокруг своих локальных осей X-Y-Z во все времена, никогда вокруг глобальных осей X-Y-Z.

Я попытался внедрить систему вращения с использованием кватернионов, но что-то не работает. При вращении объекта вдоль одной оси, если никаких предыдущих поворотов не предпринималось, предмет вращается тонко вдоль данной оси. Однако при применении одного вращения за другим второе вращение не всегда вдоль локальной оси, которую оно должно вращать вдоль - например, после поворота около 90 ° вокруг оси Z поворот вокруг оси Y все еще происходит вокруг глобальной оси Y, а не новой локальной оси Y, которая выровнена с глобальной осью X.

Да. Так что пусть это пройдет шаг за шагом. Ошибка должна быть где-то здесь.

ШАГ 1 - Ввод записи

Я подумал, что лучше использовать углы Эйлера (или схему Pitch-Yaw-Roll) для захвата входа игрока. На данный момент клавиши со стрелками управляют Pitch и Yaw, тогда как Q и ​​E управляют Roll. Таким образом, я захватываю вход игрока (я использую 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 - Обновить кватернион

Я не уверен на 100%, как это должно быть сделано, но это то, что я пробовал делать в 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);

Как вы можете видеть, я не уверен, что лучше всего построить новый кватернион из обновленных углов Эйлера, или я должен умножить кватернион, представляющий изменение, с кватернионом, представляющим общее текущее вращение, которое это впечатление, которое я получил при чтении этого руководства. Если последний, мой код будет выглядеть так:

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

m_Rotation - текущее вращение объекта, хранящееся в формате PYR; Вращение - это изменение, требуемое входным сигналом игрока. В любом случае проблема может быть в моей реализации моего класса Quaternion. Вот и все:

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;
}

Вероятно, что-то там, чтобы заставить кого-то мудрее меня съесть, но я не вижу этого. Для преобразования из углов Эйлера в кватернион я использовал "первый метод" в соответствии с этот источник, что также, по-видимому, предполагает, что уравнение автоматически создает единичный кватернион ( "четко нормированный" ). Для умножения кватернионов я снова обратился к этому руководству С++.

ШАГ 3 - Получение матрицы вращения из кватерниона

Как только это будет сделано, как ответ Р. Мартиньо Фернандеса на этот вопрос, я пытаюсь построить матрицу вращения из кватерниона и использовать ее для обновления моего вращение объекта, используя вышеуказанный код 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 VBO

glPopMatrix();

Я оставил свои предыдущие неудачные попытки, прокомментировал.

Заключение - Sad panda

Это завершение жизненного цикла входа игрока, от колыбели до могилы, управляемой OpenGL.

Я, очевидно, ничего не понял, поскольку поведение, которое я получаю, - это не то поведение, которое я хочу или ожидаю. Но я не особенно разбираюсь в матричной математике или кватернионах, поэтому у меня нет понимания, чтобы увидеть ошибку в моих путях.

Может кто-нибудь помочь мне здесь?

Ответ 1

Все, что вы сделали, эффективно реализует углы Эйлера с кватернионами. Это не помогает.

Проблема с углами Эйлера заключается в том, что при вычислении матриц каждый угол относится к вращению матрицы, которая была перед ним. Вы хотите взять текущую ориентацию объекта и применить поворот вдоль некоторой оси, создавая новую ориентацию.

Вы не можете сделать это с углами Эйлера. Вы можете с помощью матриц, и вы можете с кватернионами (поскольку они всего лишь часть вращения матрицы). Но вы не можете этого сделать, делая вид, что они углы Эйлера.

Это делается, если не хранить углы. Вместо этого у вас есть только кватернион, который представляет текущую ориентацию объекта. Когда вы решите применить к нему поворот (с некоторым углом на какую-либо ось), вы строите кватернион, который представляет это вращение на угол вокруг этой оси. Затем вы умножаете этот кватернион с текущим ориентационным кватернионом, создавая новую текущую ориентацию.

Когда вы визуализируете объект, вы используете текущую ориентацию как... ориентацию.

Ответ 2

Кватернионы представляют ориентацию вокруг осевых трехмерных осей. Но они также могут представлять "дельта-вращения".

Чтобы "поворачивать ориентацию", нам нужна ориентация (кват) и поворот (также кват), и мы умножаем их вместе, в результате чего (вы уже догадались) quat.

Вы заметили, что они не являются коммутативными, это означает, что порядок мы умножаем их на абсолютно важные вопросы, как и для матриц. Порядок, как правило, зависит от реализации вашей математической библиотеки, но на самом деле существует только два возможных способа сделать это, поэтому вам не нужно слишком долго размышлять над тем, какой из них правильный: если вещи "вращаются", вместо "поворота", тогда у вас есть неправильный путь.

Для вашего примера рыскания и высоты тона я бы построил кватернион "дельта-поворот" с углами рыскания, углов наклона и наклона, с установленным в ноль рулоном, а затем применил это к моему "ориентационному" кватерниону, вместо того, чтобы делать повороты по одной оси за раз.