Contact

 
Google
Web www.alanphipps.com

 
   
 
   
www.alanphipps.com

.: The 3D Quaternion Camera

 
 

In this tutorial we will write our long-awaited quaternion camera, there will be lots of new code, some changes to existing code and a couple of bug-fixes. So, get the source code from the last tutorial and first I will give you a brief explanation of what a quaternion is and why we use it.

We all know from high school maths that a point in 3D space is given a unique xyz coordinate, such as (1,0,0), we also know that if we draw a line between this point and the origin (0,0,0) we now have an axis around which we can rotate. This is exactly like taking a pole and sticking it in the ground so that it points straight up, if we now walk round this pole we have just created a quaternion.

A quaternion has four parts, (X,Y,Z,W).

X,Y and Z are the 3D coordinates that join together with the origin to form an axis and W is the amount of rotation that we move round that axis. This is all very interesting I hear you ask but why don't we just use normal 3D vectors, the answer here is simple. There exists, a problem with using normal vectors and Eular angles; this problem, known as Gimbal Lock, can make your camera movement somewhat unpredictable and in some instances you camera will just stop moving in a particular direction.

So, now that we know what a quaternion is and why we will use it, rename a copy of the source code folder to 3DCamera and open the project, we will begin by correcting some errors in the skybox class.

There was a few changes in the skybox class that I wont show here, I had designed this class to use a coordinate system where +Z was in the direction I was facing, only to find that XNA uses +Z going the other way, so I basically went through the code and made the appropriate changes to reflect this. Another aspect of the skybox class was the ability for the skybox to move in synch with the camera position, here I'm afraid I forgot to add the update code that updates the skybox's vertices, so in the skybox class the Update sub becomes:

Public Sub Update(ByVal LockSkybox As Boolean)

'If LockSkybox = False then the skybox position does not need to updated.
If LockSkybox = False Then Exit Sub

'If the camera position has moved.
If Camera.LastCameraPosition <> Camera.CameraPosition Then
' calculate the new Skybox vertex positions
SkyUpVertexBackLeft = Vector3.Add(SkyUpVertexBackLeft, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyUpVertexFrontLeft = Vector3.Add(SkyUpVertexFrontLeft, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyUpVertexFrontRight = Vector3.Add(SkyUpVertexFrontRight, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyUpVertexBackRight = Vector3.Add(SkyUpVertexBackRight, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyDownVertexBackLeft = Vector3.Add(SkyDownVertexBackLeft, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyDownVertexFrontLeft = Vector3.Add(SkyDownVertexFrontLeft, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyDownVertexFrontRight = Vector3.Add(SkyDownVertexFrontRight, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))
SkyDownVertexBackRight = Vector3.Add(SkyDownVertexBackRight, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))

'Update the SkyBoxOrigin too
SkyBoxOrigin = Vector3.Add(SkyBoxOrigin, _
Vector3.Subtract(Camera.CameraPosition, Camera.LastCameraPosition))

'Update the Vertices array of each texture with the new skybox vertices
TextureClass.TempVertexArray = New Vector3() {SkyUpVertexBackLeft, SkyUpVertexFrontLeft, SkyUpVertexFrontRight, SkyUpVertexBackRight}
For x As Integer = 0 To 3
SkyUp.TheVertices(x).Position = TextureClass.TempVertexArray(x)
Next
TextureClass.TempVertexArray = New Vector3() {SkyDownVertexFrontLeft, SkyDownVertexBackLeft, SkyDownVertexBackRight, SkyDownVertexFrontRight}
For x As Integer = 0 To 3
SkyDown.TheVertices(x).Position = TextureClass.TempVertexArray(x)
Next
TextureClass.TempVertexArray = New Vector3() {SkyDownVertexBackRight, SkyUpVertexBackRight, SkyUpVertexFrontRight, SkyDownVertexFrontRight}
For x As Integer = 0 To 3
SkyRight.TheVertices(x).Position = TextureClass.TempVertexArray(x)
Next
TextureClass.TempVertexArray = New Vector3() {SkyDownVertexFrontLeft, SkyUpVertexFrontLeft, SkyUpVertexBackLeft, SkyDownVertexBackLeft}
For x As Integer = 0 To 3
SkyLeft.TheVertices(x).Position = TextureClass.TempVertexArray(x)
Next
TextureClass.TempVertexArray = New Vector3() {SkyDownVertexBackLeft, SkyUpVertexBackLeft, SkyUpVertexBackRight, SkyDownVertexBackRight}
For x As Integer = 0 To 3
SkyBack.TheVertices(x).Position = TextureClass.TempVertexArray(x)
Next
TextureClass.TempVertexArray = New Vector3() {SkyDownVertexFrontRight, SkyUpVertexFrontRight, SkyUpVertexFrontLeft, SkyDownVertexFrontLeft}
For x As Integer = 0 To 3
SkyFront.TheVertices(x).Position = TextureClass.TempVertexArray(x)
Next

'assign the last camera position
Camera.LastCameraPosition = Camera.CameraPosition
End If

End Sub

Here the skybox vertices are updated with their new positions by adding the difference of the camera's current and last positions. The new vertices are then assigned to the skybox's texture vertices. Next we will move onto the camera class. The camera class that already exists in the source code can be overwritten.

First, the declarations:

#Region "Objects and Variables"
Private Shared MatrixView As Matrix ' Used in the draw sub
Private Shared MatrixProjection As Matrix ' Used in the draw sub
Private Shared CameraPos As Vector3 = Vector3.Zero 'The world space coordinates of the camera
Private Shared LastCameraPos As Vector3 = CameraPos 'The position of the camera in the last update loop.
Private Shared CameraViewVector As Vector3 = New Vector3(0.0F, 0.0F, -50.0F) ' The world space direction that the camera faces
Private Shared CameraPitchSng As Single = 0.0F ' Moves the camera direction up and down
Private Shared CameraRollSng As Single = 0.0F ' Moves the camera direction round the z axis
Private Shared RightVector As Vector3 = Vector3.Right ' The camera space vector that points right, initially set to WorldSpace right
Private Shared UpVector As Vector3 = Vector3.Up ' The camera space vector that points up, initially set to WorldSpace up
Private Shared ForwardVector As Vector3 = Vector3.Forward ' The camera space vector that points forward, initially set to WorldSpace forward
Private Shared DirectionQuaternion As Quaternion ' The quaternion that represents the Camera Direction
Private Shared ResultQuaternion As Quaternion ' The resultant Quaternion created by the multiplication of the Direction and Rotation Quaternions
Private Shared CameraYawSng As Single = 0.0F ' Moves the camera direction Left and Right
Private Shared ViewAngleSng As Single = MathHelper.ToRadians(45.0F)
Private Shared AspectRatioSng As Single = XNAEngine.XNAGraphics.GraphicsDevice.Viewport.Width / XNAEngine.XNAGraphics.GraphicsDevice.Viewport.Height
Private Shared ClipNear As Single = 1.0F ' Objects closer than 1.0F to the camera will not be drawn
Private Shared ClipFar As Single = 2000.0F ' Objects further away than 2000.0F will not be drawn
Private Shared RotationQuaternion As Quaternion ' The quaternion used that represents the rotation of the camera direction
Private Shared MatrixRotation As Matrix ' the rotation matrix created from quatRotation and used to calculate the new camera direction
Private Shared LastMousePosX As Single = 0.0F ' The X position of the mouse in the last draw loop
Private Shared LastMousePosY As Single = 0.0F ' The Y position of the mouse in the last draw loop
Private Shared CurCameraType As New CameraTypeEnum ' The type of camera currently in Use

'used in the update sub to determine camera movement
Public Enum CameraTypeEnum As Integer
Freeview = 0
FirstPerson = 1
ThirdPerson = 2
End Enum


#End Region

 

The Initialize and Draw sub are easy enough too:

''' <summary>
''' Initializes the camera using the Projection and View Matrices
''' </summary>

Public Shared Sub Initialize()

'Setup the camera initial camera position and direction
MatrixProjection = Matrix.CreatePerspectiveFieldOfView(ViewAngleSng, AspectRatioSng, ClipNear, ClipFar)
MatrixView = Matrix.CreateLookAt(CameraPos, CameraViewVector, UpVector)
Mouse.SetPosition(Game1.Window.ClientBounds.Width / 2, Game1.Window.ClientBounds.Height / 2)

End Sub

 

Here, the camera's position and view set initially set and the mouse is positioned in the centre of the screen., The Draw Sub:

''' <summary>
''' Sets the Matrices used by the games draw sub.
''' </summary>

Public Shared Sub Draw()
MatrixProjection = Matrix.CreatePerspectiveFieldOfView(ViewAngleSng, AspectRatioSng, ClipNear, ClipFar)
MatrixView = Matrix.CreateLookAt(CameraPos, CameraViewVector, UpVector)
End Sub

The draw sub is basically the same as the initialize sub, except that the variables will have changed in the update sub. The update sub is where all the quaternion calculations occur and it looks like this:

 

''' <summary>
''' Updates the camera's view and position vectors using quaternion geometry.
''' </summary>
''' <param name="gametime ">The current gametime passed since it was last measured, can be taken in secs, millisecs, etc.</param>
''' <param name="GetKeys ">The current state of the keyboard, showing which keys are pressed.</param>
''' <param name="GetMouse ">The current state of the mouse, showing if it has moved.</param>

Public Shared Sub Update(ByVal GetKeys As KeyboardState, ByVal GetMouse As MouseState, ByVal gametime As GameTime)

'Keep Mouse in centre of screen so that it does not stop camera when mouse reaches screen edge.
If GetMouse.X <= 5 Then Mouse.SetPosition(Game1.Window.ClientBounds.Width / 2, GetMouse.Y)
If GetMouse.X >= Game1.Window.ClientBounds.Width - 5 Then Mouse.SetPosition(Game1.Window.ClientBounds.Width / 2, GetMouse.Y)
If GetMouse.Y <= 5 Then Mouse.SetPosition(GetMouse.X, Game1.Window.ClientBounds.Height / 2)
If GetMouse.Y >= Game1.Window.ClientBounds.Height - 5 Then Mouse.SetPosition(GetMouse.X, Game1.Window.ClientBounds.Height / 2)

Select Case TheCameraType ' The various types of camera
Case Is = CameraTypeEnum.Freeview ' The camera is not limited in movement

'Set the current Camera type
CurCameraType = CameraTypeEnum.Freeview

'Normalize the vectors, so they dont end up with enormous values
UpVector.Normalize()
RightVector.Normalize()
ForwardVector.Normalize()

'When the Up arrow is pressed
If GetKeys.IsKeyDown(Keys.Up) Then
' Move the camera position and direction forward
CameraPos = CameraPos + ForwardVector
CameraViewVector = CameraViewVector + ForwardVector
'When the Down arrow is pressed
ElseIf GetKeys.IsKeyDown(Keys.Down) Then
' Move the camera position and direction backward
CameraPos = CameraPos - ForwardVector
CameraViewVector = CameraViewVector + ForwardVector
End If

'When the Left arrow is pressed
If GetKeys.IsKeyDown(Keys.Left) Then
' Move the camera position and direction Left
CameraPos = CameraPos - RightVector
CameraViewVector = CameraViewVector - RightVector
'When the Right arrow is pressed
ElseIf GetKeys.IsKeyDown(Keys.Right) Then
' Move the camera position and direction Right
CameraPos = CameraPos + RightVector
CameraViewVector = CameraViewVector + RightVector
End If

'When the A key is pressed
If GetKeys.IsKeyDown(Keys.A) Then
' Increase the camera roll to the left
CameraRollSng -= MathHelper.ToRadians(1.0F)
'When the D key is pressed
ElseIf GetKeys.IsKeyDown(Keys.D) Then
' Increase the camera roll to the left
CameraRollSng += MathHelper.ToRadians(1.0F)
End If
'Keep the max speed of rotation between 0.05 and -0.05
If CameraRollSng > 0.05F Then
CameraRollSng = 0.05F
ElseIf CameraRollSng < -0.05F Then
CameraRollSng = -0.05F
End If

'If the mouse has moved up
If GetMouse.Y < LastMousePosY Then
'Rotate Up
CameraPitchSng += MathHelper.ToRadians(0.05F)
ElseIf GetMouse.Y > LastMousePosY Then
'Rotate Down
CameraPitchSng -= MathHelper.ToRadians(0.05F)
Else
'If mouse is moving, slow the mouse speed until it reaches 0
Select Case CameraPitchSng
Case Is < -0.002F
CameraPitchSng += 0.002F
Exit Select
Case -0.002F To 0.002F
CameraPitchSng = 0.0F
Exit Select
Case Is > 0.002F
CameraPitchSng -= 0.002F
Exit Select
End Select
End If
'Keep the max speed of rotation between 0.05 and -0.05
If CameraPitchSng > 0.05F Then CameraPitchSng = 0.05F
If CameraPitchSng < -0.05F Then CameraPitchSng = -0.05

'If the mouse has moved Left
If GetMouse.X < LastMousePosX Then
'rotate Left
CameraYawSng += MathHelper.ToRadians(0.05F)
ElseIf GetMouse.X > LastMousePosX Then
'rotate Right
CameraYawSng -= MathHelper.ToRadians(0.05F)
Else
'If mouse is not moving, slow the mouse speed until it reaches 0
Select Case CameraYawSng
Case Is < -0.002F
CameraYawSng += 0.002F
Exit Select
Case -0.002F To 0.002F
CameraYawSng = 0.0F
Exit Select
Case Is > 0.002F
CameraYawSng -= 0.002F
Exit Select
End Select
End If
'Keep the max speed of rotation between 0.05 and -0.05
If CameraYawSng > 0.05F Then CameraYawSng = 0.05F
If CameraYawSng < -0.05F Then CameraYawSng = -0.05F

'Only if a rotation has occured.
If CameraYawSng <> 0.0F Or CameraPitchSng <> 0.0F Or CameraRollSng <> 0.0F Then
'Work out the direction quaternion
DirectionQuaternion = Quaternion.CreateFromAxisAngle(CameraViewVector, 0.0F)
'Apply the cameraPitch and CameraYaw
RotationQuaternion = Quaternion.Multiply(Quaternion.CreateFromAxisAngle(RightVector, CameraPitchSng), _
Quaternion.CreateFromAxisAngle(UpVector, CameraYawSng))
'Apply the cameraRoll
RotationQuaternion = Quaternion.Multiply(Quaternion.CreateFromAxisAngle(ForwardVector, _
CameraRollSng), RotationQuaternion)
'Get the resultant quaternion
ResultQuaternion = RotationQuaternion * DirectionQuaternion
'Create teh roatation matrix
MatrixRotation = Matrix.CreateFromQuaternion(ResultQuaternion)

'Apply the rotation matrix to the vectors
CameraViewVector = Vector3.Transform(CameraViewVector, MatrixRotation)
RightVector = Vector3.Transform(RightVector, MatrixRotation)
UpVector = Vector3.Transform(UpVector, MatrixRotation)
ForwardVector = Vector3.Transform(ForwardVector, MatrixRotation)
End If

Exit Select

Case Is = CameraTypeEnum.FirstPerson
'Set the current Camera type
CurCameraType = CameraTypeEnum.FirstPerson

Exit Select

Case Is = CameraTypeEnum.ThirdPerson
'Set the current Camera type
CurCameraType = CameraTypeEnum.ThirdPerson

Exit Select

End Select

End Sub

The select case at the start of this sub determines which type of camera to use, so far i have only developed a freeview camera that is not restricted in any direction, but later I will develop both a first and third person camera. This sub checks the mouse position and if it is within 5 pixels of the screen boundaries, the mouse is positioned center-screen. The three camera space vectors are then normalized, to stop them from becoming too large.

This should be a good point to explain what camera space and world space are. Imagine a cube with centre (0,0,0), Up (0,1,0) Right (1,0,0) and Forward ( 0,0,1). The dimensions of this cube are static and never change, this is your world space, the skybox is basically your world space. Now, think of another cube which also has an up vector, a right vector and a forward vector, this second cube exists wthin the world space and at the centre of this cube is the camera, this cube is called camera space. Now, as I have said world space is static and does not change but camera space does, every time you rotate right, the right vector and the forward vectors rotate too, this also applies to rotating up and the up vector. So we use quaternion maths to work out the amount of rotation tat occurs and then assign new world space coordinates to the camera vectors, you see, easy.

So, once the vectors have been normalized, we check the getkeys object to see what key have been pressed, If up has been pressed then the camera position and direction move forward and backward if the down key is pressed. The same idea applies to the left and right arrow keys. After every update loop the mouse position is stored into the LastMousePosX and LastMousePosY variables, when the next loop occurs if the current mouse Y position is different to the last one then CameraPitch is incremented or decremented depending whether the mouse moved up or down, the same idea occurs for the X mouse coordinate. The values of CameraPitch and CameraYaw are kept between -0.05 and +0.05 so that the camera does not move too fast.

 

Select Case CameraYawSng
Case Is < -0.002F
CameraYawSng += 0.002F
Exit Select
Case -0.002F To 0.002F
CameraYawSng = 0.0F
Exit Select
Case Is > 0.002F
CameraYawSng -= 0.002F
Exit Select
End Select

The code above states that if no mouse movement has occurred between update loops then the slows the cmera speed gradually so that it doesn't just stop dead when you stop moving the mouse. Lastly we have the quaternion calculation:

'Only if a rotation has occured.
If CameraYawSng <> 0.0F Or CameraPitchSng <> 0.0F Or CameraRollSng <> 0.0F Then
'Work out the direction quaternion
DirectionQuaternion = Quaternion.CreateFromAxisAngle(CameraViewVector, 0.0F)
'Apply the cameraPitch and CameraYaw
RotationQuaternion = Quaternion.Multiply(Quaternion.CreateFromAxisAngle(RightVector, CameraPitchSng), _
Quaternion.CreateFromAxisAngle(UpVector, CameraYawSng))
'Apply the cameraRoll
RotationQuaternion = Quaternion.Multiply(Quaternion.CreateFromAxisAngle(ForwardVector, _
CameraRollSng), RotationQuaternion)
'Get the resultant quaternion
ResultQuaternion = RotationQuaternion * DirectionQuaternion
'Create teh roatation matrix
MatrixRotation = Matrix.CreateFromQuaternion(ResultQuaternion)

'Apply the rotation matrix to the vectors
CameraViewVector = Vector3.Transform(CameraViewVector, MatrixRotation)
RightVector = Vector3.Transform(RightVector, MatrixRotation)
UpVector = Vector3.Transform(UpVector, MatrixRotation)
ForwardVector = Vector3.Transform(ForwardVector, MatrixRotation)
End I

This code says, if the mouse has moved, create a direction (view) quaternion and a rotation quaternion, multiply them together to get a result quaternion and then calculate a rotation matrix from the result quaternion. This rotation matrix can then be applied to the view, right, forward and up camera vectors. These vectors will then be used in the next draw loop. Now, we will add the necessary function calls to the XNAEngine class code:

In the XNAEngine.Initialize sub the following code was added

Camera.Initialize()

The camera should be initialized first after MyBase.initialize. In the Update Sub:

'Update the Camera
Camera.Update(GetKeys, GetMouse, Camera.CameraTypeEnum.Freeview) ' Update the camera position, view, etc
'assign the previous mouse position
Camera.PreviousMousePosX = GetMouse.X
Camera.PreviousMousePosY = GetMouse.Y

This code is added just before the skybox.update sub and it passes in the the current mouse and keyboard state. It also passes in which type of camera to use and lastly in the draw loop

Camera.Draw()

goes first, before skybox.draw.

Now, if you run the game

XNA in VB.NET - 3D Quaternion Camera

and then, if you move the mouse:

XNA in VB.NET - 3D Quaternion Camera Moving

and then:

XNA in VB.NET - 3D Quaternion Camera Still Moving

Right, next I'm going to work on building some terrain to fill our world, this may take some time as I have absolutely no idea on how to do it. But as usual, when I'm done i will upload it to the tutorials page, so until then, enjoy.

 

3DCamera Source Code - 11,306Kb
Next Tutorial - BasicTerrain

 

     
 
 
     

 

Web site contents © Copyright Alan Phipps 2006, All rights reserved.
Website templates
 
   
 
 

 

__PayPal

PayPal - Any Amount is Welcome
 
Please Donate to the Nvidia Geforce Go 7950 GTX Fund, All donations welcome. Thanks.

 

XNA in C#

 
 

 

Games at Amazon