As useful as model and world space offsetting is, there is one more space that it might be useful to orient from. Camera-space.
This is primarily useful in modelling applications, but it can have other applications as well. In such programs, as the user spins the camera around to different angles, the user may want to transform the object relative to the direction of the view.
In order to understand the solution to doing this, let's frame the problem explicitly. We have positions (p) in model space. These positions will be transformed by a current model-to-world orientation (O), and then by a final camera matrix (C). Thus, our transform equation is C*O*p .
We want to apply an orientation offset (R), which takes points in camera-space. If we wanted to apply this to the camera matrix, it would simply be multiplied by the camera matrix: R*C*O*p . That's nice and all, but we want to apply a transform to O, not to C.
Therefore, we need to use our transforms to generate a new orientation offset (N), which will produce the same effect:
In order to solve this, we need to introduce a new concept. Given a matrix M, there may be a matrix N such that MN = I , where I is the identity matrix. If there is such a matrix, the matrix N is called the inverse matrix of M. The notation for the inverse matrix of M is M-1. The symbol “-1” does not mean to raise the matrix to the -1 power; it means to invert it.
The matrix inverse can be analogized to the scalar multiplicative inverse (ie: reciprocal). The scalar multiplicative inverse of X is the number Y such that XY = 1 .
In the case of the scalar inverse, this is very easy to solve for: Y = 1/X . Easy though this may be, there are values of X for which there is no multiplicative inverse. OK, there's one such real value of X: 0.
The case of the inverse matrix is much more complicated. Just as with the scalar inverse, there are matrices that have no inverse. Unlike the scalar case, there are a lot of matrices with no inverse. Also, computing the inverse matrix is a lot more complicated than simply taking the reciprocal of a value.
Most common transformation matrices do have an inverse. And for the basic transformation matrices, the inverse matrix is very easy to compute. For a pure rotation matrix, simply compute a new rotation matrix by negating the angle that the old one was generated with. For a translation matrix, negate the origin value in the matrix. For a scale matrix, take the reciprocal of the scale along each axis.
To take the inverse of a sequence of matrices, you can take the inverse of each of the component matrices. But you have to do the matrix multiplication in reverse order. So if we have M = TRS , then M-1 = S-1R-1T-1 .
Quaternions, like matrices, have a multiplicative inverse. The inverse of a pure rotation matrix, which quaternions represent, is a rotation about the same axis with the negative of the angle. For any angle θ, it is the case that sin(-θ) = -sin(θ) . It is also the case that cos(-θ) = cos(θ) . Since the vector part of a quaternion is built by multiplying the axis by the sine of the half-angle, and the scalar part is the cosine of the half-angle, the inverse of any quaternion is just the negation of the vector part.
You can also infer this to mean that, if you negate the axis of rotation, you are effectively rotating about the old axis but negating the angle. Which is true, since the direction of the axis of rotation defines what direction the rotation angle moves the points in. Negate the axis's direction, and you're rotating in the opposite direction.
In quaternion lingo, the inverse quaternion is more correctly called the conjugate quaternion. We use the same inverse notation, “-1,” to denote conjugate quaternions.
Incidentally, the identity quaternion is a quaternion who's rotation angle is zero. The cosine of 0 is one, and the sine of 0 is zero, so the vector part of the identity quaternion is zero and the scalar part is one.
Given our new knowledge of inverse matrices, we can solve our problem.
We can right-multiply both sides of this equation by the inverse transform of O.
The I is the identity transform. From here, we can left-multiply both sides by the inverse transform of C:
Therefore, given an offset that is in camera space, we can generate the world-space equivalent by multiplying it between the camera and inverse camera transforms.
It turns out that this is a generalized operation. It can be used for much more than just orientation changes.
Consider a scale operation. Scales apply along the main axes of their space. But if you want to scale something along a different axis, how do you do that?
You rotate the object into a coordinate system where the axis you want to scale is one of the basis axes, perform your scale, then rotate it back with the inverse of the previous rotation.
Effectively, what we are doing is transforming, not positions, but other transformation matrices into different spaces. A transformation matrix has some input space and defines an output space. If we want to apply that transformation in a different space, we perform this operation.
The general form of this sequence is as follows. Suppose you have a transformation matrix T, which operates on points in a space called F. We have some positions in the space P. What we want is to create a matrix that applies T's transformation operation, except that it needs to operate on points in the space of P. Given a matrix M that transforms from P space to F space, that matrix is M-1*T*M .
Let's look at how this all works out in code, with the Camera Relative tutorial. This works very similarly to the last tutorial, but with a few differences.
Since we are doing camera-relative rotation, we need to have an actual camera that can move independently of the world. So we incorporate our camera code from our world space into this one. As before, the I and K keys will move the camera up and down, relative to a center point. The J and K keys will move the camera left and right around the center point. Holding Shift with these keys will move the camera in smaller increments.
The SpaceBar will toggle between three transforms: model-relative (yaw/pitch/roll-style), world-relative, and camera-relative.
Our scene also includes a ground plane as a reference.
The display
function only changed where needed to deal with
drawing the ground plane and to handle the camera. Either way, it's nothing that
has not been seen elsewhere.
The substantive changes were in the OffsetOrientation
function:
Example 8.4. Camera Relative OffsetOrientation
void OffsetOrientation(const glm::vec3 &_axis, float fAngDeg) { float fAngRad = Framework::DegToRad(fAngDeg); glm::vec3 axis = glm::normalize(_axis); axis = axis * sinf(fAngRad / 2.0f); float scalar = cosf(fAngRad / 2.0f); glm::fquat offset(scalar, axis.x, axis.y, axis.z); switch(g_iOffset) { case MODEL_RELATIVE: g_orientation = g_orientation * offset; break; case WORLD_RELATIVE: g_orientation = offset * g_orientation; break; case CAMERA_RELATIVE: { const glm::vec3 &camPos = ResolveCamPosition(); const glm::mat4 &camMat = CalcLookAtMatrix(camPos, g_camTarget, glm::vec3(0.0f, 1.0f, 0.0f)); glm::fquat viewQuat = glm::quat_cast(camMat); glm::fquat invViewQuat = glm::conjugate(viewQuat); const glm::fquat &worldQuat = (invViewQuat * offset * viewQuat); g_orientation = worldQuat * g_orientation; } break; } g_orientation = glm::normalize(g_orientation); }
The change here is the addition of the camera-relative condition. To do this in
quaternion math, we must first convert the world-to-camera matrix into a quaternion
representing that orientation. This is done here using
glm::quat_cast
.
The conjugate quaternion is computed with GLM. Then, we simply multiply them with the offset to compute the world-space offset orientation. This gets left-multiplied into the current orientation, and we're done.