Opengl: camera holding Arcball up-vector aligned with the y-axis


I'm essentially trying to mimic the way the camera rotates in Maya. The arcball in Maya is always aligned with the with the y-axis. So no matter where the up-vector is pointing, it's still rotated or registered with it's up-vector along the y-axis.

I've been able to implement is arcball in OpenGL using C++ and Qt. But I can't figure out how to keep it's up-vector aligned. I've been able to keep it aligned at times by my code below:

void ArcCamera::setPos (Vector3 np)
    Vector3 up(0, 1, 0);

    Position = np;
    ViewDir = (ViewPoint - Position); ViewDir.normalize();

    RightVector =  ViewDir ^ up; RightVector.normalize();

    UpVector = RightVector ^ ViewDir;  UpVector.normalize();

This works up until the position is at 90-degrees, then the right vector changes and everything is inverted.

So instead I've been maintaining the total rotation (in quaternions) and rotating the original positions (up, right, pos) by it. This works best to keep everything coherent, but now I simply can't align the up-vector to the y-axis. Below is the function for the rotation.

void CCamera::setRot (QQuaternion q)
    tot = tot * q;

    Position  = tot.rotatedVector(PositionOriginal);

    UpVector = tot.rotatedVector(UpVectorOriginal);

    RightVector  = tot.rotatedVector(RightVectorOriginal);

The QQuaternion q is generated from the axis-angle pair derived from the mouse drag. I'm confident this is done correctly. The rotation itself is fine, it just doesn't keep the orientation aligned.

I've noticed in my chosen implementation, dragging in the corners provides a rotation around my view direction, and I can always realign the up-vector to straighten out to the world's y-axis direction. So If I could figure out how much to roll I could probably do two rotations each time to make sure it's all straight. However, I'm not sure how to go about this.

The reason this isn't working is because Maya's camera manipulation in the viewport does not use an arcball interface. What you want to do is Maya's tumble command. The best resource I've found for explaining this is this document from Professor Orr's Computer Graphics class.

Moving the mouse left and right corresponds to the azimuth angle, and specifies a rotation around the world space Y axis. Moving the mouse up and down corresponds to the elevation angle, and specifies a rotation around the view space X axis. The goal is to generate the new world-to-view matrix, then extract the new camera orientation and eye position from that matrix, based on however you've parameterized your camera.

Start with the current world-to-view matrix. Next, we need to define the pivot point in world space. Any pivot point will work to begin with, and it can be simplest to use the world origin.

Recall that pure rotation matrices generate rotations centered around the origin. This means that to rotate around an arbitrary pivot point, you first translate to the origin, perform the rotation, and translate back. Remember also that transformation composition happens from right to left, so the negative translation to get to the origin goes on the far right:

translate(pivotPosition) * rotate(angleX, angleY, angleZ) * translate(-pivotPosition)

We can use this to calculate the azimuth rotation component, which is a rotation around the world Y axis:

azimuthRotation = translate(pivotPosition) * rotateY(angleY) * translate(-pivotPosition)

We have to do a little additional work for the elevation rotation component, because it happens in view space, around the view space X axis:

elevationRotation = translate(worldToViewMatrix * pivotPosition) * rotateX(angleX) * translate(worldToViewMatrix * -pivotPosition)

We can then get the new view matrix with:

newWorldToViewMatrix = elevationRotation * worldToViewMatrix * azimuthRotation

Now that we have the new worldToView matrix, we're left with having to extract the new world space position and orientation from the view matrix. To do this, we want the viewToWorld matrix, which is the inverse of the worldToView matrix.

newOrientation = transpose(mat3(newWorldToViewMatrix))
newPosition = -((newOrientation * newWorldToViewMatrix).column(3))

At this point, we have the elements separated. If your camera is parameterized so that you're only storing a quaternion for your orientation, you just need to do the rotation matrix -> quaternion conversion. Of course, Maya is going to convert to Euler angles for display in the channel box, which will be dependent on the camera's rotation order (note that the math for tumbling doesn't change when the rotation order changes, just the way that the rotation matrix -> Euler angles conversion is done).

Here's a sample implementation in Python:

#!/usr/bin/env python

import numpy as np
from math import *

def translate(amount):
    'Make a translation matrix, to move by `amount`'
    t = np.matrix(np.eye(4))
    t[3] = amount.T
    t[3, 3] = 1
    return t.T

def rotateX(amount):
    'Make a rotation matrix, that rotates around the X axis by `amount` rads'
    c = cos(amount)
    s = sin(amount)

    return np.matrix([
        [1, 0, 0, 0],
        [0, c,-s, 0],
        [0, s, c, 0],
        [0, 0, 0, 1],

def rotateY(amount):
    'Make a rotation matrix, that rotates around the Y axis by `amount` rads'
    c = cos(amount)
    s = sin(amount)
    return np.matrix([
        [c, 0, s, 0],
        [0, 1, 0, 0],
       [-s, 0, c, 0],
        [0, 0, 0, 1],

def rotateZ(amount):
    'Make a rotation matrix, that rotates around the Z axis by `amount` rads'
    c = cos(amount)
    s = sin(amount)
    return np.matrix([
        [c,-s, 0, 0],
        [s, c, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1],

def rotate(x, y, z, pivot):
    'Make a XYZ rotation matrix, with `pivot` as the center of the rotation'
    m = rotateX(x) * rotateY(y) * rotateZ(z)

    I = np.matrix(np.eye(4))
    t = (I-m) * pivot
    m[0, 3] = t[0, 0]
    m[1, 3] = t[1, 0]
    m[2, 3] = t[2, 0]

    return m

def eulerAnglesZYX(matrix):
    'Extract the Euler angles from an ZYX rotation matrix'
    x = atan2(-matrix[1, 2], matrix[2, 2])
    cy = sqrt(1 - matrix[0, 2]**2)
    y = atan2(matrix[0, 2], cy)
    sx = sin(x)
    cx = cos(x)
    sz = cx * matrix[1, 0] + sx * matrix[2, 0]
    cz = cx * matrix[1, 1] + sx * matrix[2, 1]
    z = atan2(sz, cz)
    return np.array((x, y, z),)

def eulerAnglesXYZ(matrix):
    'Extract the Euler angles from an XYZ rotation matrix'
    z = atan2(matrix[1, 0], matrix[0, 0])
    cy = sqrt(1 - matrix[2, 0]**2)
    y = atan2(-matrix[2, 0], cy)
    sz = sin(z)
    cz = cos(z)
    sx = sz * matrix[0, 2] - cz * matrix[1, 2]
    cx = cz * matrix[1, 1] - sz * matrix[0, 1]
    x = atan2(sx, cx)
    return np.array((x, y, z),)

class Camera(object):
    def __init__(self, worldPos, rx, ry, rz, coi):
        # Initialize the camera orientation.  In this case the original
        # orientation is built from XYZ Euler angles.  orientation is the top
        # 3x3 XYZ rotation matrix for the view-to-world matrix, and can more
        # easily be thought of as the world space orientation.
        self.orientation = \
            (rotateZ(rz) * rotateY(ry) * rotateX(rx))

        # position is a point in world space for the camera.
        self.position = worldPos

        # Construct the world-to-view matrix, which is the inverse of the
        # view-to-world matrix.
        self.view = self.orientation.T * translate(-self.position)

        # coi is the "center of interest".  It defines a point that is coi
        # units in front of the camera, which is the pivot for the tumble
        # operation.
        self.coi = coi

    def tumble(self, azimuth, elevation):
        '''Tumble the camera around the center of interest.

        Azimuth is the number of radians to rotate around the world-space Y axis.
        Elevation is the number of radians to rotate around the view-space X axis.
        # Find the world space pivot point.  This is the view position in world
        # space minus the view direction vector scaled by the center of
        # interest distance.
        pivotPos = self.position - (self.coi * self.orientation.T[2]).T

        # Construct the azimuth and elevation transformation matrices
        azimuthMatrix = rotate(0, -azimuth, 0, pivotPos)
        elevationMatrix = rotate(elevation, 0, 0, self.view * pivotPos)

        # Get the new view matrix
        self.view = elevationMatrix * self.view * azimuthMatrix

        # Extract the orientation from the new view matrix
        self.orientation = np.matrix(self.view).T
        self.orientation.T[3] = [0, 0, 0, 1]

        # Now extract the new view position
        negEye = self.orientation * self.view
        self.position = -(negEye.T[3]).T
        self.position[3, 0] = 1


pos = np.matrix([[5.321, 5.866, 4.383, 1]]).T
orientation = radians(-60), radians(40), 0
coi = 1

camera = Camera(pos, *orientation, coi=coi)
print 'Initial attributes:'
print np.round(np.degrees(eulerAnglesXYZ(camera.orientation)), 3)
print np.round(camera.position, 3)
print 'Attributes after tumbling:'
camera.tumble(azimuth=radians(-40), elevation=radians(-60))
print np.round(np.degrees(eulerAnglesXYZ(camera.orientation)), 3)
print np.round(camera.position, 3)