3. The Game Class
To draw a 3D scene a developer needs to set up a coordinate system and create a camera to view the rendered scene.
3.1. The Camera
We need to create a camera that will be a view into our 3D world of triangles that is rendered by the GPU. Right-click on the XNA3DPrimitives project and select Add => New Item... => click on XNA Game Studio 4.0 and select Game Component. Name the new game component Camera and click Add. Remember from our discussion above that the Camera
object has a position within the model world, looks in a particular
direction, and has a view defined by the Frustum. We modify the
constructor to take three parameters: cameraUpVector, cameraPosition, and cameraDirection all of type Vector3..
The cameraPosition and cameraDirection parameters orient the Camera. As an example, if flying an airplane in the first person, you bank right to turn right, so it would make sense to have the CameraUp Vector
oriented 45 degrees to the right through the turn. The camera direction
defines the viewing Frustum and will be oriented to look towards our
triangles when drawn to the screen.
The Camera GameComponent object will need two public properties: View and Projection of type Matrix. Don't worry about what it means to define these terms as a Matrix type – it isn't critical. It is more important to think of the concepts above as we define them programmatically.
The Matrix class has a static method called CreatePerspectiveFieldOfView that defines the frustrum for the GPU. This method call takes four parameters:
fieldOfView
aspectRatio
nearPlaneDistance
farPlaneDistance
The fieldOfView parameter defines the angle
width of the field of view for the Frustrum in radians, not degrees.
Remember from geometry that 360 degrees equals 2pi Radians, 180 degrees
equals pi Radians, and 90 degrees equals pi/2 radians. The MathHelper class defines these values for you so you can pass in MathHelper.PiOver4 to define a field of view of 45 degrees, which is a good starting point for most games.
The aspectRatio parameter is defined by the
ratio of the screen width divided by screen height. The Game object has
these values in this format: Game.Window.ClientBounds.Width /Game.Window.ClientBounds.Height.
The nearPlaneDistance parameter defines how far in front of the Camera object the near plane from Figure 8-4
is defined. A good starting point is one for our example, but in a real
game you may not want the near plane to include every object in front
of the Camera object. Lastly, the farPlaneDistance parameter defines the far plane, i.e., the how far into the distance the Camera can "see" past the Near Plane.
Now that we have defined the Frustrum box, we need to
orient the frustrum box in a particular direction to look at our
triangles that we are going to draw in just a bit. The handy Matrix class defines a static method named CreateLookAt that defines the direction that the Camera is facing. It takes three parameters:
Camera Position
Camera Direction
Camera Up Vector
The CreateLookAt parametersmatch the three parameters that are passed into the Camera GameComponent object that we defined above so we pass them into the method call as shown in Listing 2.
Example 2. The Camera GameComponent Class
using Microsoft.Xna.Framework;
namespace XNA3DPrimitives
{
public class Camera : Microsoft.Xna.Framework.GameComponent
{
public Camera(Game game, Vector3 position, Vector3 direction, Vector3 upVector)
: base(game)
{
// Initialize frustum matrix
Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4,
this.Game.GraphicsDevice.Viewport.AspectRatio,
1, 20);
// Initialize "look at" matrix
View = Matrix.CreateLookAt(position, direction, upVector);
}
public Matrix View { get; private set; }
public Matrix Projection { get; private set; }
public override void Initialize()
{
base.Initialize();
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
}
}
|
We next update the Game1 class to include our new Camera object and use it to call the cube related methods to render the cube in a scene.
3.2. Rendering the Cube Scene
To leverage the Camera object we add it to Game1.cs in the Game1.Initialize method:
protected override void Initialize()
{ // Initialize camera
camera = new Camera(this, cameraPosition,
Vector3.Zero, Vector3.Up);
Components.Add(camera);
base.Initialize();
}
We update the DrawCubePrimitive method to leverage the Camera
object. The method now just takes two parameters. Here is a snippet of
the first part of the method where the changes are in effect:
private void DrawCubePrimitive (Matrix world, Color color)
{
// Set BasicEffect parameters.
basicEffect.World = world;
basicEffect.View = camera.View;
basicEffect.Projection = camera.Projection;
basicEffect.DiffuseColor = color.ToVector3();
basicEffect.Alpha = color.A / 255.0f;
...
The basicEffect object now gets its View and Projection properties from the Camera object in the DrawCubePrimitive method. Here is the Game1.Draw method that renders the cube:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
Matrix world = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);
DrawCubePrimitive (world, Color.Orange);
// Reset the fill mode renderstate.
GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
base.Draw(gameTime);
}
The World coordinate is obtained from configured,
yaw, pitch, and role values at the top of Game1.cs resulting in the
rendered cube shown in Figure 7.
float yaw = .5f;
float pitch = .5f;
float roll = .5f;
The scene is static, which is a bit boring. Let's add some movement to the scene in the next section via yaw, pitch, and roll.
3.3. Adding Movement
In the previous section, the cube object is positioned using world Matrix object, which is created using the Matrix.CreateFromYawPitchRoll(yaw, pitch, roll) static method call. The world Matrix objectis passed to DrawCubePrimitive method and assigned to the basicEffect.World property to render the object with the configured yaw, pitch, and roll applied to its coordinates. Figure 8 describes these parameters visually.
We apply a simple way to manipulate the object by updating the yaw, pitch, and roll members in the Update method shown here:
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
float time = (float)gameTime.TotalGameTime.TotalSeconds;
yaw = time * 0.5f;
pitch = time * 0.5f;
roll = time * 0.5f;
base.Update(gameTime);
}
The gameTime.TotalGameTime.TotalSeconds value is fairly small as it is the elapsed time between frames. Applying the modification on each call to Update
results in smooth animation. A screenshot doesn't do it justice, so run
the sample code to observe the lighting provided by the BasicEffect instance when rendering the object as it rotates.
Now that we covered 3D programming basics
and rendering 3D objects generated from triangle primitives in code, we
can move on to rendering rich 3D models, building on the 3D game
development concepts presented so far.