Moving Around a 3D World

  CU_MDX_Camera.zip (736.9 KiB, 4,103 hits)


  CUnitFramework.zip (101.1 KiB, 20,514 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

In this tutorial, we’ll learn how to move around in a 3D world. The camera style that we will implement is a classic first person shooter camera. The user will be able to move forward and backwards and strafe left and right. We’ll also add mouse look so users can look around with the mouse.

A camera is really just composed of a single matrix: an inverted world transform matrix (a.k.a. a view matrix). The view matrix controls where in 3D space to place our viewpoint and what coordinates to look at, so it makes sense that a camera can be represented by a view matrix. Why is a view matrix an inverted world transform matrix? Say you want to move to the right. Believe it or not, the camera is always positioned at the origin of a 3D coordinate system, pointing down the positive Z-axis. So, to move to the right we have to move everything in our world to the left. This has the same effect as moving a “camera” to the right. Luckily, there is a method that creates this matrix for us: Matrix.LookAtLeftHanded. All we need to pass in is a position, a view target, and an up vector.

If you look at the matrix that Matrix.LookAtLeftHanded creates, you’ll notice that it is created with 4 different vectors: a right vector, an up vector, a look vector, and a position. These vectors respectively refer to the camera’s local X-axis, local Y-axis, local Z-axis, and position in world space. Therefore, it makes sense to store these vectors separately since we transform the camera with respect to these individual vectors. For example, to look up, the up and look vectors are rotated about the right vector. To move right, we just move in the direction of the right vector. To calculate the new view matrix, we just call Matrix.LookAtLeftHanded with the stored position and up vectors along with a target vector that can be calculated by adding the look vector to the position vector.

Moving and strafing the camera is really simple since we’re storing the camera’s right, up, and look vectors. To move forward and backward, we just move in the direction of the look vector. To strafe left and right, we move in the direction of the right vector.

In order to implement mouse look properly, we need to be able to rotate around an arbitrary axis in world space as shown below. The arbitrary axes that we will rotate about are the right, up, and look vectors of the camera. The Matrix class has a method that creates this rotation matrix for us: Matrix.RotationAxis. We just tell it what vector to rotate around and how many radians to rotate and we get a rotation matrix in return. Once we have this rotation matrix, we need to rotate the camera’s local axes. We’ll also need to rotate our vectors with Vector3.TransformNormal.

Arbitrary Axis

To implement the camera, we will create_ a new Camera class that encapsulates all the view data, which includes a view matrix and projection matrix.

using System;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace CUnit
{

/// First person camera
public class Camera
{
private Matrix m_view;
private Matrix m_projection;
private Vector3 m_right;
private Vector3 m_up;
private Vector3 m_look;
private Vector3 m_position;
private Vector3 m_lookAt;
private Vector3 m_velocity;
private Plane[] m_frustum;
private float m_yaw;
private float m_pitch;
private float m_maxPitch;
private float m_maxVelocity;
private float m_fov;
private float m_aspect;
private float m_nearPlane;
private float m_farPlane;
private bool m_invertY;
private bool m_enableYMovement;

///

Creates a new Camera
public Camera() Toggle
{
m_frustum = new Plane[6];
m_maxPitch = Geometry.DegreeToRadian( 89.0f );
m_maxVelocity = 1.0f;
m_invertY = false;
m_enableYMovement = true;
m_position = new Vector3();
m_velocity = new Vector3();
m_look = new Vector3( 0.0f, 0.0f, 1.0f );
CreateProjectionMatrix( (float)Math.PI / 3.0f, 1.3f, 1.0f, 1000.0f );
Update();

}

///

Creates the projection matrix.
/// Field of view
/// Aspect ratio
/// Near plane
/// Far plane
private void CreateProjectionMatrix( float fov, float aspect, float near, float far ) Toggle
{
m_fov = fov;
m_aspect = aspect;
m_nearPlane = near;
m_farPlane = far;
m_projection = Matrix.PerspectiveFieldOfViewLeftHanded( m_fov, m_aspect, m_nearPlane, m_farPlane );

}

///

Moves the camera forward and backward.
/// Amount to move
public void MoveForward( float units ) Toggle
{
if ( m_enableYMovement )
{
m_velocity += m_look * units;

}
else
{

Vector3 moveVector = new Vector3( m_look.X, 0.0f, m_look.Z );
moveVector.Normalize();
moveVector *= units;
m_velocity += moveVector;

}

}

///

Moves the camera left and right.
/// Amount to move
public void Strafe( float units ) Toggle
{
m_velocity += m_right * units;

}

///

Moves the camera up and down.
/// Amount to move.
public void MoveUp( float units ) Toggle
{
m_velocity.Y += units;

}

///

Yaw the camera around its Y-axis.
/// Radians to yaw.
public void Yaw( float radians ) Toggle
{
if ( radians == 0.0f )
{
// Don’t bother
return;

}
Matrix rotation = Matrix.RotationAxis( m_up, radians );
m_right = Vector3.TransformNormal( m_right, rotation );
m_look = Vector3.TransformNormal( m_look, rotation );

}

///

Pitch the camera around its X-axis.
/// Radians to pitch.
public void Pitch( float radians ) Toggle
{
if ( radians == 0.0f )
{
// Don’t bother
return;

}

radians = (m_invertY) ? -radians : radians;
m_pitch -= radians;

if ( m_pitch > m_maxPitch )
{

radians += m_pitch – m_maxPitch;

}
else if ( m_pitch < -m_maxPitch )
{

radians += m_pitch + m_maxPitch;

}

Matrix rotation = Matrix.RotationAxis( m_right, radians );
m_up = Vector3.TransformNormal( m_up, rotation );
m_look = Vector3.TransformNormal( m_look, rotation );

}

///

Roll the camera around its Z-axis.
/// Radians to roll.
public void Roll( float radians ) Toggle
{
if ( radians == 0.0f )
{
// Don’t bother
return;

}
Matrix rotation = Matrix.RotationAxis( m_look, radians );
m_right = Vector3.TransformNormal( m_right, rotation );
m_up = Vector3.TransformNormal( m_up, rotation );

}

///

Updates the camera and creates a new view matrix.
public void Update() Toggle
{
// Cap velocity to max velocity
if ( Vector3.Length( m_velocity ) > m_maxVelocity )
{
m_velocity = Vector3.Normalize( m_velocity ) * m_maxVelocity;

}

// Move the camera
m_position += m_velocity;
// Could decelerate here. I’ll just stop completely.
m_velocity = new Vector3();
m_lookAt = m_position + m_look;

// Calculate the new view matrix
Vector3 up = new Vector3( 0.0f, 1.0f, 0.0f );
m_view = Matrix.LookAtLeftHanded( Position, LookAt, up );

// Calculate new view frustum
BuildViewFrustum();

// Set the camera axes from the view matrix
m_right.X = m_view.M11;
m_right.Y = m_view.M21;
m_right.Z = m_view.M31;
m_up.X = m_view.M12;
m_up.Y = m_view.M22;
m_up.Z = m_view.M32;
m_look.X = m_view.M13;
m_look.Y = m_view.M23;
m_look.Z = m_view.M33;

// Calculate yaw and pitch
float lookLengthOnXZ = (float)Math.Sqrt( m_look.Z * m_look.Z + m_look.X * m_look.X );
m_pitch = (float)Math.Atan2( m_look.Y, lookLengthOnXZ );
m_yaw = (float)Math.Atan2( m_look.X, m_look.Z );

}

///


/// Build the view frustum planes using the current view/projection matrices
///

public void BuildViewFrustum() Toggle
{
Matrix viewProjection = m_view * m_projection;

// Left plane
m_frustum[0].A = viewProjection.M14 + viewProjection.M11;
m_frustum[0].B = viewProjection.M24 + viewProjection.M21;
m_frustum[0].C = viewProjection.M34 + viewProjection.M31;
m_frustum[0].D = viewProjection.M44 + viewProjection.M41;

// Right plane
m_frustum[1].A = viewProjection.M14 – viewProjection.M11;
m_frustum[1].B = viewProjection.M24 – viewProjection.M21;
m_frustum[1].C = viewProjection.M34 – viewProjection.M31;
m_frustum[1].D = viewProjection.M44 – viewProjection.M41;

// Top plane
m_frustum[2].A = viewProjection.M14 – viewProjection.M12;
m_frustum[2].B = viewProjection.M24 – viewProjection.M22;
m_frustum[2].C = viewProjection.M34 – viewProjection.M32;
m_frustum[2].D = viewProjection.M44 – viewProjection.M42;

// Bottom plane
m_frustum[3].A = viewProjection.M14 + viewProjection.M12;
m_frustum[3].B = viewProjection.M24 + viewProjection.M22;
m_frustum[3].C = viewProjection.M34 + viewProjection.M32;
m_frustum[3].D = viewProjection.M44 + viewProjection.M42;

// Near plane
m_frustum[4].A = viewProjection.M13;
m_frustum[4].B = viewProjection.M23;
m_frustum[4].C = viewProjection.M33;
m_frustum[4].D = viewProjection.M43;

// Far plane
m_frustum[5].A = viewProjection.M14 – viewProjection.M13;
m_frustum[5].B = viewProjection.M24 – viewProjection.M23;
m_frustum[5].C = viewProjection.M34 – viewProjection.M33;
m_frustum[5].D = viewProjection.M44 – viewProjection.M43;

// Normalize planes
for ( int i = 0; i < 6; i++ )
{

m_frustum[i] = Plane.Normalize( m_frustum[i] );

}

}

///

Checks whether a sphere is inside the camera’s view frustum.
/// Position of the sphere.
/// Radius of the sphere.
/// true if the sphere is in the frustum, false otherwise
public bool SphereInFrustum( Vector4 position, float radius ) Toggle
{
for ( int i = 0; i < 6; i++ )
{
if ( Plane.Dot( m_frustum[i], position ) + radius < 0 )
{
// Outside the frustum, reject it!
return false;

}

}
return true;

}

///

Gets the view matrix
public Matrix View Toggle
{
get { return m_view; }

}

///

Gets the projection matrix
public Matrix Projection Toggle
{
get { return m_projection; }

}

///

Gets and sets the position of the camera
public Vector3 Position Toggle
{
get { return m_position; }
set { m_position = value; }

}

///

Gets and sets the position of the camera
public Vector3 LookAt Toggle
{
get { return m_lookAt; }
set
{
m_lookAt = value;
m_look = Vector3.Normalize( m_lookAt – m_position );

}

}

///

Gets and sets the field of view
public float FOV Toggle
{
get { return m_fov; }
set { CreateProjectionMatrix( value, m_aspect, m_nearPlane, m_farPlane ); }

}

///

Gets and sets the aspect ratio
public float AspectRatio Toggle
{
get { return m_aspect; }
set { CreateProjectionMatrix( m_fov, value, m_nearPlane, m_farPlane ); }

}

///

Gets and sets the near plane
public float NearPlane Toggle
{
get { return m_nearPlane; }
set { CreateProjectionMatrix( m_fov, m_aspect, value, m_farPlane ); }

}

///

Gets and sets the far plane
public float FarPlane Toggle
{
get { return m_farPlane; }
set { CreateProjectionMatrix( m_fov, m_aspect, m_nearPlane, value ); }

}

///

Gets and sets the maximum camera velocity
public float MaxVelocity Toggle
{
get { return m_maxVelocity; }
set { m_maxVelocity = value; }

}

///

Gets and sets whether the y-axis is inverted.
public bool InvertY Toggle
{
get { return m_invertY; }
set { m_invertY = value; }

}

///

Gets the camera’s pitch
public float CameraPitch Toggle
{
get { return m_pitch; }

}

///

Gets the camera’s yaw
public float CameraYaw Toggle
{
get { return m_yaw; }

}

///

Gets and sets the maximum pitch in radians.
public float MaxPitch Toggle
{
get { return m_maxPitch; }
set { m_maxPitch = value; }

}

///

Gets and sets whether the camera can move along its Y-axis.
public bool EnableYMovement Toggle
{
get { return m_enableYMovement; }
set { m_enableYMovement = value; }

}

}

}

The Camera class performs all the calculations described at the beginning of the tutorial. To move, we increase a velocity vector in the direction that we want to move. When we want to rotate the camera, we create_ the rotation matrix with Matrix.RotationAxis and then rotate the corresponding vectors with Vector3.TransformNormal. Notice that the pitch is constrained to a range that prevents the camera from looking straight up or straight down. Since we are implementing a first person camera, we don’t want to be able to rotate all the way around when we look up or down.

The view matrix is created in the Camera.Update method. To move the camera, we simply add the velocity vector to the camera’s position. To create_ the view matrix we call Matrix.LookAtLeftHanded. Notice that the up vector we pass in to this method is the world up vector (0.0f, 1.0f, 0.0f), instead of the camera’s up vector. We do this because we are implementing a first person camera where up is always (0.0f, 1.0f, 0.0f). If we were to use the camera’s up axis, we would create_ a space ship type of camera where we could roll around wherever we wanted to.

After the view matrix is created we extract the new camera axes from it and calculate the camera’s pitch and yaw. The right, up, and look vectors are respectively located in the 1st, 2nd, and 3rd columns of the view matrix. The pitch value is calculated by projecting the look vector onto the XZ-plane and then using the Math.Atan2 method to get the angle between the XZ-plane and the Y value of the camera’s look vector. The yaw is calculated by getting the angle between the X and Z values of the camera’s look vector.

Since the projection matrix also affects how we view geometry, the Camera class contains its own projection matrix, which is formed with Matrix.PerspectiveFieldOfViewLeftHanded.

The Camera class also keeps track of its view frustum, but we’ll learn about that in a later tutorial. For a better understanding of all this math, I recommend reading Real-Time Rendering.