Search code examples
c++matrixopenglrotationquaternions

Why does my object in OpenGL rotate in the opposite direction than expected using quaternions?


I am writing a program with OpenGL/GLUT using the fixed function pipeline (I know, I know, it's university). I've written the Quaternion class from scratch with help of other implementations and the internet and it essentially works fine, but it's rotating along every axis in the opposite direction to what I thought it would.

I thought about posting this on the Math stack exchange, but given it's OpenGL/GLUT I thought it would be better understood here.

The axis below are: green -> Y, red -> X, blue -> Z. The darker sides are the positive directions. I've rotated it a little to show the positive Z axis. The axes are the world coordinate axes.

enter image description here

I have defined pressing "a" as a positive rotation in Y. The right hand rule states that this is a counterclockwise rotation in the Y axis. The image shows the rotation of the cube after pressing "a". As you can see, it has rotated clockwise. This occurs for every axis.

My Quaternion class:

#define _USE_MATH_DEFINES
#include <cmath>

#include "Quaternion.h"
#include "Utility.h"

Quaternion::Quaternion() : X(0), Y(0), Z(0), W(1) {}

Quaternion::Quaternion(Vector3D axis, float angle) {
    float mag = Vector3D::magnitude(axis);
    angle = utility::toRadians(angle);
    float sine = sinf(angle * 0.5f);

    // Divide by magnitude for pure quaternion
    X = axis.X * sine / mag;
    Y = axis.Y * sine / mag;
    Z = axis.Z * sine / mag;
    W = cosf(angle * 0.5f);
}

Quaternion::Quaternion(float x, float y, float z, float w) :
    X(x), Y(y), Z(z), W(w) {}

float Quaternion::magnitude(const Quaternion &q) {
    return sqrtf(q.X * q.X + q.Y * q.Y + q.Z * q.Z + q.W * q.W);
}

Quaternion Quaternion::normalise(const Quaternion &q) {
    float mag = magnitude(q);

    return Quaternion(q.X / mag, q.Y / mag, q.Z / mag, q.W / mag);
}

std::array<float, 16> Quaternion::toMatrix(const Quaternion& q) {
    float X = q.X;
    float Y = q.Y;
    float Z = q.Z;
    float W = q.W;

    return {
        1 - 2*(Z*Z + Y*Y),  2*(X*Y - W*Z),      2*(Z*X + W*Y),      0,
        2*(X*Y + W*Z),      1 - 2*(X*X + Z*Z),  2*(Y*Z - W*X),      0,
        2*(Z*X - W*Y),      2*(Y*Z + W*X),      1 - 2*(X*X + Y*Y),  0,
        0,                  0,                  0,                  1
    };
}

Quaternion Quaternion::conjugate(const Quaternion& q) {
    return Quaternion(-q.X, -q.Y, -q.Z, q.W);
}

Quaternion operator*(Quaternion lhs, Quaternion rhs) {
    return lhs *= rhs;
}

Quaternion& Quaternion::operator*=(const Quaternion& rhs) {
    float rhsX = rhs.getX(), rhsY = rhs.getY(),
        rhsZ = rhs.getZ(), rhsW = rhs.getW();

    Quaternion q;

    q.X = W * rhsX + X * rhsW + Y * rhsZ - Z * rhsY;
    q.Y = W * rhsY - X * rhsZ + Y * rhsW + Z * rhsX;
    q.Z = W * rhsZ + X * rhsY - Y * rhsX + Z * rhsW;
    q.W = W * rhsW - X * rhsX - Y * rhsY - Z * rhsZ;

    *this = q;

    return *this;
}

// Quaternion->Vector multiplication is not commutiative, must be Q*V
Vector3D operator*(Quaternion lhs, Vector3D rhs) {
    Quaternion pure = Quaternion(rhs.X, rhs.Y, rhs.Z, 0);
    Quaternion right = pure * Quaternion::conjugate(lhs); // v * q-1
    Quaternion left = lhs * right; // q * (v * q-1)
    return Vector3D(left.getX(), left.getY(), left.getZ());
}

float Quaternion::getX() const { return X; }
float Quaternion::getY() const { return Y; }
float Quaternion::getZ() const { return Z; }
float Quaternion::getW() const { return W; }

std::ostream& operator<<(std::ostream& ostream, Quaternion& q)
{
    ostream << q.X << " " << q.Y << " "
            << q.Z << " " << q.W << " " << std::endl;
    return ostream;
}

Pressing "a" (and similar) calls:

void GameManager::handleKeyboardInput() {
    // ...

    if (keyboard->isPressed('a')) {
        ship->rotate(Axis::y, Direction::positive, dt);
    }

    // ...

    glutPostRedisplay();
}

Which, when the object updates itself in the display function, calls:

void Ship::rotate(const Axis axis, const Direction direction, const float dt) {
    int sign = direction == Direction::positive ? 1 : -1;
    float speed = sign * ROTATION_SPEED;

    if (axis == Axis::x) {
        rotation = Quaternion(Vector3D::right(), speed * dt) * rotation;
    }
    else if (axis == Axis::y) {
        rotation = Quaternion(Vector3D::up(), speed * dt) * rotation;
    }
    else if (axis == Axis::z) {
        rotation = Quaternion(Vector3D::forward(), speed * dt) * rotation;
    }
}

Where rotation is the object's current rotation stored as a Quaternion, left multiplied so it's local coordinates.

The Vectors are:

Vector3D Vector3D::up() { return Vector3D(0, 1, 0); }
Vector3D Vector3D::right() { return Vector3D(1, 0, 0); }
Vector3D Vector3D::forward() { return Vector3D(0, 0, 1); }

And finally the object is drawn in the main display loop:

glPushMatrix();
    glMultMatrixf(Quaternion::toMatrix(rotation).data());
    glColor3f(1.0, 1.0, 1.0);
    glutWireCube(8);
glPopMatrix();

The easy solution is to just flip the signs of my rotations, but I'd rather know what's wrong. Thank you.

Edit: For what it's worth, I can confirm that if I hold "a" such that the cube is rotated 45 degrees (so a little more rotated than the second image), my cube's rotation (x, y, z, w) = (0, 0.389125, 0, 0.918102), which agrees with this page if I set y = 1 and the angle (radians) to 0.785398 (45 degrees). So the final rotation quaternion is correct, but my cube still rotates in the wrong direction. This makes me think there is something wrong with my code.

This is the final quaternion rotation matrix after holding "a" until it rotates 45 degrees clockwise:

{0.705871, 0, 0.708341, 0, }
{0, 1, 0, 0, }
{-0.708341, 0, 0.705871, 0, }
{0, 0, 0, 1, }

Which according to the same site is { [ 0, 1, 0 ], 45.1000806 }, which is what I want, but still the rotation is clockwise, not anticlockwise.


Solution

  • You build your matrix in row-major order, but glMultMatrixf expects a column-major matrix. Transposing a rotation matrix is equivalent to inverting it, i.e. rotating in the opposite direction.

    To fix it, either build your matrix in column-major order, transpose it, or use glMultTransposeMatrixf.