Tom Ward

3D Camera Controls

In my spare time I’ve started working on making my own little model viewer in OpenGL (more on this later) and I’ve recently been working on creating some nice camera controls. Although I’ve had to delve into cameras a few times over the years, this is the first time I’d had to create one from scratch and was quite an experience! I also found the help online to be a bit sparse, with various ways of doing it but none I really liked.

Here’s what I learnt and snippets of my final solution, I haven’t gone into the nitty gritty of all the math functions, but there’s plenty of places to find that stuff.

First Things First, Looking at a Point

Firstly I wanted to make a camera that I could position in world space, looking at a particular point which I could later use to orbit around. This was relatively simple to do by writing a LookAt function that would take a camera position, target position and up direction (which in this case would be positive Y) to produce a view matrix. This is what I came up with which I basically nicked from here:

inline
Matrix44 Matrix44FromLookAt(
    const Vector3& lvEyePosition,
    const Vector3& lvTargetPosition,
    const Vector3& lvUp )
{
    Vector3 lvAt = Normalize( lvTargetPosition - lvEyePosition );
    M_ASSERT( IsValid( lvAt ) );
    Vector3 lvRight = Normalize( Cross( lvAt, lvUp ) );
    M_ASSERT( IsValid( lvRight ) );
    Vector3 lvPlane = Cross( lvRight, lvAt );
    M_ASSERT( IsValid( lvPlane ) );

    Matrix44 lLookAt;
    lLookAt.SetIdentity();
    lLookAt.SetRowX( Vector4( lvRight, -Dot( lvRight, lvEyePosition ) ) );
    lLookAt.SetRowY( Vector4( lvPlane, -Dot( lvPlane, lvEyePosition ) ) );
    lLookAt.SetRowZ( Vector4( -lvAt, Dot(lvAt, lvEyePosition ) ) );
    return lLookAt;
}

That worked pretty well for setting up the camera to look at an object, so I began by storing my position, target and up direction in my camera class, then calculating the resulting matrix on demand using the above code.

Zooming the Camera

With my camera setup, I then proceeded to add the ability to zoom the camera. I did this first basically because I thought it would be the easiest to do! This was indeed very simple, basically all I did was get the vector between the camera position and the target position, then multiply this by a “zoom factor” (how quickly I want to zoom by) and add this vector to the camera position to move it closer/farther from the target pos. I did start by using a set distance to zoom by (so say 1m) but this caused it to feel like it was zooming a lot when close up and not much at all when far away, so instead I went for a percentage. Here’s the code:

void
Camera::ZoomCamera(
    const float32_t& lfZoomFactor )
{
    Vector3 lvCameraDirection = GetTargetPosition() - GetPosition();

    // if we don't have a direction, we can't zoom
    if( !IsZero( lvCameraDirection ) )
    {
        lvCameraDirection *= lvfZoomFactor;

        SetPosition( GetPosition() + lvCameraDirection );
    }
}

Really simple stuff, one thing I should probably add is a minimum/maximum distance to zoom to prevent getting stuck at the extents, but I haven’t managed to look at that yet.

Next, Panning

With the simple movement out of the way, I looked at being able to pan the camera. The first thing that became apparent here is that I needed not only to move the camera position, but also the target position in order to retain the same look direction, and to maintain the same orbit “feel”.

I began this by trying to use the “right” vector of the camera in world space (calculated the cross product of the look at direction by the camera up), as I figured this would allow me to move perpendicular to the target position a certain amount. This didn’t really work though because the direction I was moving in was screen space and not world space, so instead I had to get the right vector for the viewmatrix instead. Once I had the correct direction, I could then multiply this by a panning amount in both the X and Y to get the pan translation in world space. Adding this to both the camera position and target produced a nice smooth camera pan in both the X and Y, whilst still retaining my looking direction. Here’s the code:

void
Camera::PanCamera(
const Vector2& lvScreenPanAmount )
{
    Matrix44 lViewMatrix = GetViewMatrix();

    const Vector3 lvCameraUp = Normalize( GetCameraUp() );

    Vector3 lvWorldPanAmount;
    lvWorldPanAmount += GetXYZ( lViewMatrix.GetRowY() ) * lvScreenPanAmount.GetY();
    lvWorldPanAmount -= GetXYZ( lViewMatrix.GetRowX() ) * lvScreenPanAmount.GetX(); // reversed as it makes more sense!

    Vector3 lvNewPosition = GetPosition() + lvWorldPanAmount;
    Vector3 lvNewTarget = GetTargetPosition() + lvWorldPanAmount;

    SetPosition(lvNewPosition);
    SetTargetPosition(lvNewTarget);
}

The ellusive orbit

With panning and zooming sorted, the final movement I wanted was to be able to orbit around the target position. Recall I’m currently storing the target position, camera position and up direction for the camera and computing the lookat matrix from these components. Therefore all I needed to do was to rotate about the target position a certain amount (in radians) and translate by the current position to target distance in order to obtain my new look at position. I began by rotating just around the world up direction, which I did using the following to great success:

    Math::VPU::Matrix44 lRotationMatrix;
    lRotationMatrix.SetIdentity();
    lRotationMatrix.RotateYAxis( Math::ToRadians( lvfRotationAmount ) );

    // Get the direction from target position to the camera. This might seem a little backward, but we want to rotate
    // around the target and not the camera position
    Math::VPU::Vector3 lvTargetToCameraDirection = lpCamera->GetCameraPosition() - lpCamera->GetTargetPosition();

    // rotate the target to camera direction
    lvTargetToCameraDirection = (lvTargetToCameraDirection * lRotationMatrix);

    lpCamera->SetCameraPosition( lvTargetToCameraDirection + lpCamera->GetTargetPosition() );

This worked pretty well as I’m only rotating in one axis (the XZ plane). The problems started happening when I began trying to rotate in all three directions. The problem was I was trying to rotate a matrix around a given axis a certain amount, which was done using euler angles. I don’t want to get bogged down explaining quaternions (take a look on google!), but basically I ended up getting into gimbal lock and all kinds of things. The solution was instead of using the lookat function above for calculating my view matrix, I instead stored my direction as a quaternion and created my view matrix from this. After I’d done this I completely fixed my rotation woes, ensuring a nice smoooth orbit. There was one final thing that didn’t feel quite right, and that was how I rotated in the XZ plane. I found it just felt better doing so always around the world up rather than the camera up, but this is kind of down to personal preference (it’s more “correct” to rotate around the camera up).

Here’s the code I ended up with, which produced a nice orbit:

void
Camera::OrbitCamera(
    const Vector2& lvOrbitAmount )
{
    // Get the direction from target position to the camera. This might seem a little backward, but we want to rotate
    // around the target and not the camera position
    Vector3 lvRelativePos = GetPosition() - GetTargetPosition();

    Vector3 lvAxis = Normalize( Cross( GetCameraUp(), lvRelativePos ) );

    Quaternion lRotationAmount = QuaternionFromAxisRotationAngle( lvAxis, ToRadians( lvOrbitAmount.GetY() ) );
    lRotationAmount *= QuaternionFromAxisRotationAngle( Vector3(0.0f, 1.0f, 0.0f), ToRadians( lvOrbitAmount.GetX() ) );

    Matrix44 lRotationMatrix = lRotationMatrix = Matrix44FromQuaternion( lRotationAmount );

    SetPosition( GetTargetPosition() + ( lvRelativePos * lRotationMatrix ) );
    SetRotation( GetRotation() * lRotationAmount );
    SetCameraUp( Normalize( GetCameraUp() * lRotationMatrix ) );
}

Summary

I never really found any great examples of doing this stuff, so hopefully this little post will help some poor soul out in the future. I found it infuriating at times trying to work out where in my matrix math code I was going wrong, but it was pretty rewarding once I finally figured it all out.