# How to tumble a camera about a point

I'm trying to implement camera tumbling as described by this document. I have a camera that defines a view position and orientation. Additionally, there is a center of interest, which is a distance in front of the camera (along the view space Z-axis), and is used to define the center of rotation for when the camera is tumbled. I'm creating the orientation with Euler angles in an XYZ order.

For example, a camera with a world space position of (5, 5, 5), orientation of (x=0, y=0, z=0), and center of interest = 1, this would yield a view matrix of

$\begin{bmatrix} 1 & 0 & 0 & -5 \\ 0 & 1 & 0 & -5 \\ 0 & 0 & 1 & -5 \\ 0 & 0 & 0 & 1 \end{bmatrix}$

and a rotation pivot of $\begin{bmatrix}5 & 5 & 4 & 1\end{bmatrix}^T$.

The tumble operation has two parameters, an azimuth and an elevation. Azimuth is an amount to rotate about the world-space Y axis. Elevation is an amount to rotate about the view-space X axis.

The formula given in the reference pdf for this is

$\begin{split} \mathbf{A} &= \mathscr{T}(P_c) \mathbf{R}_y \mathscr{T}(-P_c) \\ \mathbf{B} &= \mathscr{T}(P_c^\prime) \mathbf{R}_x \mathscr{T}(-P_c^\prime) \\ \mathbf{V}_{new} &= \mathbf{B} \mathbf{V}_{old} \mathbf{A} \end{split}$

where $\mathscr{T}$ is a translation matrix, $P_c$ is the pivot point in world space coordinates, $P_c^\prime$ is the pivot point in view space coordinates, and $\mathbf{V}_{old}$ is the current view matrix.

I'm having two problems. The first is, given the way I have parameterized my camera, how do I find $P_c$ and $P_c^\prime$. With the view matrix, I can get the camera's world space position easily. But I'm not sure how to make the vector to add to the world space position. I'm trying to use the Z axis from the view matrix (column 2, assuming 0-based indexes), scale that by the center of interest distance, and right multiply by the inverse of the view's orientation to get the vector into world space. Then subtract that from the camera's world space position to get the pivot. This seems correct to me, but I'm not sure. It works fine when the camera's orientation is (0, 0, 0).

The second problem is that the rotations seem backwards. I'm referring to the result of Maya's tumble command. If I make a camera as described above in Maya, and run 'tumble -azimuthAngle 0 -elevationAngle 90 -pivotPoint 5 5 4', I get the same values for the position (x=5, y=6, z=4), but the Euler angle values I extract from the view matrix has x=90, whereas Maya has x=-90. Maya is a right-handed system, and I think my math should result in a right-handed system as well. Similarly, if I rotate in Maya by an azimuth of 90, I get y=-90 whereas Maya gives y=90.

I've implemented this using python and numpy:

#!/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 = amount.T
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 = rotateZ(z) * rotateY(y) * rotateX(x)

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 eulerAngles(matrix):
'Extract the Euler angles from an XYZ 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),)

class Camera(object):
def __init__(self, worldPos, rx, ry, rz, coi):
# Construct the world-to-view matrix.  The camera's rotation order is
# forced to XYZ.  orientation is the top 3x3 XYZ rotation matrix for
# the world-to-view matrix.
self.orientation = \
(rotateZ(rz) * rotateY(ry) * rotateX(rx)).T

# position a point representating the translation component of the view
# matrix (column 4)
self.position = -(self.orientation * worldPos)
self.position[3, 0] = 1

# Construct the view matrix.  Apply the orientation, then the position.
self.view = translate(self.position) * self.orientation

# 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.worldPos
diff = self.orientation.T * (self.view.T.T * self.coi)
pivotPos -= diff
print pivotPos

# Construct the A and B rotation matrices, as defined by the
# CoordinateSystems pdf
a = rotate(0, -azimuth, 0, pivotPos)
b = rotate(elevation, 0, 0, self.view * pivotPos)

# Get the new view matrix
self.view = b * self.view * a

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

# Now extract the new view position
negEye = self.orientation.T * self.view
# The pdf says this is the new eye position.  But it appears this is in
# world space.
worldPos = -negEye.T.T
worldPos[3, 0] = 1
# Find the new view space position for the eye.
self.position = -(self.orientation * worldPos)
self.position[3, 0] = 1

@property
def worldPos(self):
# To go from world->view is -(self.orientation * worldPos).  To go from
# view->world is the inverse, -(self.orientation.T * self.position)
worldPos = -(self.orientation.T * self.position)
worldPos[3, 0] = 1
return worldPos

np.set_printoptions(precision=3)

pos = np.matrix([[5, 5, 5, 1]]).T
orientation = 0, 0, 0
coi = 1

camera = Camera(pos, *orientation, coi=coi)
print np.round(np.degrees(eulerAngles(camera.orientation)), 3)
print np.round(camera.worldPos, 3)


I figured this out. The main problem where the rotations seemed to be backwards was because I was still extracting the Euler angles from the matrix based on the world-to-view matrix, when I really want to do it on the view-to-world matrix. Because the view-to-world matrix is the inverse of the world-to-view, the rotation order is reversed as well. So I needed to adapt the algorithm for extracting the Euler angles to the reverse order.

The other problem I was having was how to properly get the pivot point based on the current view matrix. This was because I was using the full view matrix, which includes a translation component, instead of just the camera's orientation.

Here's a fixed version of the code:

#!/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 = 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).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 = [0, 0, 0, 1]

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

np.set_printoptions(precision=3)

pos = np.matrix([[5.321, 5.866, 4.383, 1]]).T