Microsoft XNA Framework; Creating a freelook camera

Before we continue with some real exciting stuff, we'll need to create a more interactive environment for us to navigate through. Right now, every 3d model in the world space is transformed to the screen using its own View and Projection matrices. These matrices define the Viewing Frustum of the scene (projection matrix), and the remaining transformations of the perspective (view matrix). It would be much more efficient and versatile to create a controllable camera component that has its own view and projection.

A Viewing Frustum can best be described as a region in the world space that appears on the screen. The shape of this region resembles that of a pyramid with the top and base cut off. The following image visualizes a Viewing Frustum. The planes that cut the frustum are called the near plane and far plane. Objects located closer to the camera than the near plane or farther from the camera as the far plane are not drawn. When defining a Projection matrix, the slope of the pyramid is determent by the Field of View argument, and the width and height of the near plane are considered to be the aspect ratio.

 

 

Let’s start by defining a simple Camera GameComponent that has the two matrices we need.  I’ve set the View matrix to an Identity matrix because we don’t have any logic that determents the view yet. 

C#:
  1. public class Camera
  2. {
  3.     protected float FOV = MathHelper.Pi / 3;
  4.     protected float aspectRatio = 1;
  5.     protected float nearClip = 1.0f;
  6.     protected float farClip = 10000000.0f;
  7.  
  8.     protected Quaternion cameraRotation;
  9.     protected Vector3 cameraPosition;
  10.  
  11.     public Quaternion Rotation
  12.     {
  13.         get
  14.         {
  15.             return this.cameraRotation;
  16.         }
  17.         set
  18.         {
  19.             this.cameraRotation = value;
  20.         }
  21.     }
  22.     public Vector3 Position
  23.     {
  24.         get
  25.         {
  26.             return this.cameraPosition;
  27.         }
  28.         set
  29.         {
  30.             this.cameraPosition = value;
  31.         }
  32.     }
  33.     public Matrix Projection
  34.     {
  35.         get
  36.         {
  37.             return Matrix.CreatePerspectiveFieldOfView(this.FOV, this.aspectRatio, this.nearClip, this.farClip);
  38.         }
  39.     }
  40.     public Matrix View
  41.     {
  42.         get
  43.         {
  44.             return Matrix.Identity;
  45.         }
  46.     }
  47.  
  48.     public Camera()
  49.     {
  50.         this.cameraRotation = new Quaternion();
  51.         this.cameraPosition = Vector3.Zero;
  52.     }
  53. }

 

As you can see I’ve mapped four local members to the arguments of the Matrix.CreatePerspectiveFieldOfView method. I’ve placed the far plane really far from the camera to ensure every object in the 3d world is rendered. Since we want to be able to control our camera, we need a couple of methods to rotate and move it. I’ve chosen to let the rotations rely on Quaternions. Quaternions are basically matrices that store the computational members in the W member instead of directly transforming to coordinates. This is very convenient because it means the quaternion multiplication order isn’t relevant, and it prevents Gimble lock, a problem that occurs when trying to rotate multiple axis that interfere.

C#:
  1. protected float yaw;
  2. protected float pitch;
  3. protected float roll;
  4.  
  5. public Camera()
  6. {
  7.     this.cameraRotation = new Quaternion();
  8.     this.cameraPosition = Vector3.Zero;
  9.  
  10.     this.yaw = 0;
  11.     this.pitch = 0;
  12.     this.roll = 0;
  13. }
  14.  
  15. public void Rotate(float xRotation, float yRotation, float zRotation)
  16. {
  17.     this.yaw += xRotation;
  18.     this.pitch += yRotation;
  19.     this.roll += zRotation;
  20.  
  21.     Quaternion q1 = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), this.yaw);
  22.     Quaternion q2 = Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), this.pitch);
  23.     Quaternion q3 = Quaternion.CreateFromAxisAngle(new Vector3(0, 0, 1), this.roll);
  24.   �
  25.     this.cameraRotation = q1 * q2 * q3;
  26. }
  27.  
  28. public void Translate(Vector3 distance)
  29. {
  30.     Vector3 diff = Vector3.Transform(distance, Matrix.CreateFromQuaternion(this.cameraRotation));
  31.     this.cameraPosition += diff;
  32. }

The Rotate method creates 3 quaternions for each axis, and rotates each quaternion depending on the locally stored Yaw, Pitch and Roll members. Yaw, Pitch and Roll typically define rotations around the X, Y and Z axis in flight dynamics and computer graphics.

The Translate method uses the Vector3.Transform method to create a normal vector directing towards the rotation of the given quaternion. The position matrix can then be translated through world space in the right direction.

With that in place, we still need to update the View property to return a Matrix that has the rotation and translation, and we need a camera manager that listens to our input device and transforms the camera accordingly. The following snippet contains the updated view property, followed by the Camera Manager class.

C#:
  1. public Matrix View
  2. {
  3.     get
  4.     {
  5.         return Matrix.Invert(Matrix.CreateFromQuaternion(this.Rotation) * Matrix.CreateTranslation(this.Position));
  6.     }
  7. }

C#:
  1. public class CameraManager : Microsoft.Xna.Framework.GameComponent, ICameraManagerService
  2. {
  3.     protected Vector2 offMousePos;
  4.     protected Vector2 preMousePos;
  5.     protected Camera camera;
  6.  
  7.     public CameraManager(Game game)
  8.         : base(game)
  9.     {
  10.         this.camera = new Camera();
  11.         {
  12.             this.camera.Rotation = new Quaternion(0, 0, 0, 0);
  13.             this.camera.Position = new Vector3(0.0f, 50.0f, 5000.0f);
  14.         }
  15.  
  16.         this.Game.Services.AddService(typeof(ICameraManagerService), this);
  17.     }
  18.  
  19.     public Camera Camera
  20.     {
  21.         get
  22.         {
  23.             return this.camera;
  24.         }
  25.     }
  26.  
  27.     public override void Initialize()
  28.     {
  29.         // TODO: Add your initialization code here
  30.         IInputManagerService inputManagerService = (IInputManagerService)this.Game.Services.GetService(typeof(IInputManagerService));
  31.         {
  32.             inputManagerService.OnKeyDown += new InputManagerHandler(inputManagerService_OnKeyDown);
  33.         }
  34.  
  35.         base.Initialize();
  36.     }
  37.  
  38.     protected void inputManagerService_OnKeyDown(InputManager sender, List keys)
  39.     {
  40.         // Variable that controls the speed
  41.         float speed = 35f;
  42.  
  43.         if (keys.Contains(Keys.LeftShift))
  44.             speed *= 2;
  45.  
  46.         // Check for specified key presses
  47.         if (keys.Contains(Keys.W))
  48.             this.camera.Translate(new Vector3(0, 0, -1) * speed);
  49.  
  50.         if (keys.Contains(Keys.S))
  51.             this.camera.Translate(new Vector3(0, 0, 1) * speed);
  52.  
  53.         if (keys.Contains(Keys.A))
  54.             this.camera.Translate(new Vector3(-1, 0, 0) * speed);
  55.  
  56.         if (keys.Contains(Keys.D))
  57.             this.camera.Translate(new Vector3(1, 0, 0) * speed);
  58.     }
  59.  
  60.     public override void Update(GameTime gameTime)
  61.     {
  62.         // Retrieve the mousestate
  63.         MouseState ms = Mouse.GetState();
  64.  
  65.         // Check if pressed the left mouse button
  66.         if (ms.LeftButton == ButtonState.Pressed)
  67.         {
  68.             // Rotate camera
  69.             this.camera.Rotate(offMousePos.X * 0.005f, offMousePos.Y * 0.005f, 0);
  70.         }
  71.         // Save the offset between mousecoordinates, and the current mouse pos
  72.         offMousePos = preMousePos - new Vector2(ms.X, ms.Y);
  73.         preMousePos = new Vector2(ms.X, ms.Y);
  74.  
  75.         // TODO: Add your update code here
  76.         base.Update(gameTime);
  77.     }
  78. }

Notice that the CameraManager implements an interface that allows access to the Camera itself, and that I’ve registered the service in the constructor of the class. That way, our 3d objects can retrieve the CameraManager instance and use the Camera property to read the View and Projection matrices. Also, in the Initialize method, I retrieved the previously created InputManager component and used the OnKeyDown event to listen to the keystrokes. The Update method contains logic to rotate the camera depending on the movement of the mouse. I’m not going to dive into this code since it should be pretty easy for you to understand. Add the CameraManager component to the components collection of our game, and run the application.

Oh oww! Nothing different seems to happen? How could this be? Well, we haven’t updated the code of our models yet to incorporate the View and Projection matrices of our camera. Update the code in the MySpaceWarShip component to retrieve the camera component through the interface type on the services collection, and use the correct matrices.

C#:
  1. ICameraManagerService cameraManagerService = (ICameraManagerService)this.Game.Services.GetService(typeof(ICameraManagerService));

C#:
  1. foreach (BasicEffect effect in mesh.Effects)
  2. {
  3.     effect.World = Matrix.CreateRotationY(this.modelRotation) * Matrix.CreateTranslation(this.modelPosition) ;
  4.     effect.View = cameraManagerService.Camera.View;
  5.     effect.Projection = cameraManagerService.Camera.Projection;
  6.     effect.EnableDefaultLighting();
  7. }

Congratulations, we now have a free look camera component that allows us the use the keyboard and mouse to ‘fly’ through the world space of our XNA project. The sources can be downloaded here; 05 - Creating a freelook camera

WordPress database error: [Table 'feg_wordpress.wp_comments' doesn't exist]
SELECT * FROM wp_comments WHERE comment_post_ID = '18' AND comment_approved = '1' ORDER BY comment_date

Leave a Reply