Wednesday, March 20, 2019

MonoGame: Static Camera Tutorial and code


Some of you may have noticed my series on openGL with openTK in C# that I wrote some years ago. Since then my life has been quite full of stuff happening so I never got around to look at lightning or other stuff to make sure that I would end up with a working engine.

Lately I found MonoGame and started to experiment with it, turns out that it has much of the stuff that I want to use so I ended up writing some stuff with it.

Here I thought that I would share a basic static camera class and its usage as it took some time for me to understand how to get it working together with the BasicEffect class provided by MonoGame.

Some code. Let's start to look at the interface that we would want to use with out cameras.

public interface ICamera
{
    Matrix ViewMatrix { get; }
    Matrix ProjectionMatrix { get; }
    void Update(GameTime gameTime);
}

We want our cameras to expose their View and Projection matrices as they will be used to integrate the camera with MonoGame BasicEffect.
Also, as we will be using this interface with all of our cameras we may want to update it in each frame, hence the Update(GameTime gametTime) method. GameTime is provided by MonoGame and contains two timespans, time since game start and time since last frame.


Next we look at the actual StaticCamera, a camera that once created will remain static in the world. I.e. you place it at a position and point it towards something interesting and it will look at that position until you remove the camera. Useful for some scenarios and a good stepping point for creating more advanced camera classes.

We want our camera to have a Position in in the game world and a Direction.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
public class StaticCamera : ICamera
{
    public Vector3 Position { get; }
    public float FieldOfView { get; }
    public float NearPlane { get; }
    public float FarPlane { get; }
    public Vector3 Direction { get; }
    public Matrix ViewMatrix { get; }
    public Matrix ProjectionMatrix { get; }

    public StaticCamera(GraphicsDevice graphicsDevice, float fieldOfViewDegrees, float nearPlane, float farPlane)
        : this(graphicsDevice, fieldOfViewDegrees, nearPlane, farPlane, Vector3.Zero, -Vector3.UnitZ)
    { }

    public StaticCamera(GraphicsDevice graphicsDevice, float fieldOfViewDegrees, float nearPlane, float farPlane, Vector3 position, Vector3 target)
    {
        FieldOfView = fieldOfViewDegrees * 0.0174532925f;
        Position = position;
        Direction = Vector3.Normalize(target - position);
        ViewMatrix = Matrix.CreateLookAt(Position, Direction, Vector3.Up);
        ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(
            FieldOfView,
            graphicsDevice.Viewport.AspectRatio,
            nearPlane,
            farPlane);
    }
    public void Update(GameTime gameTime)
    { }
}

We have two constructors, the first one takes a GraphicsDevice reference that we need to figure out what resolution and especially what aspect ration our current view port has. (i.e. screen resolution/window size)
Also, we want to specify the lense angle in degrees. I.e. 60 degree lense.
The near and far clipping planes, i.e. we will only want to render objects that are between those two distances from the camera.
The first constructor creates a camera located at coordinates (0, 0, 0) looking inward (0, -1, 0)

The second constructor takes also the position and target to look at.
We calculate the radians of the camera lense by multiplying the field of view degrees with 0.01745...
After that we figure out the direction that the camera points at by subtracting the camera location from the target and normalizing the result.

To get the ViewMatrix we use the Matrix.CreateLookAt function that takes Position, Direction and an up vector
To get the ProjectionMatrix we use the Matrix.CreatePerspectiveFieldOfView function that takes the field of view, aspect ration and near/far planes.
This is pretty much all for a simple static camera using the MonoGame framework.

Next step is to use the camera.

private ICamera _camera;
protected override void LoadContent()
{
 _camera = new StaticCamera(GraphicsDevice, 60, 1, 200, new Vector3(1, -10, 0), Vector3.Zero);
}

I put the above code in my Game1.cs file. As the default Game1 class inherits from Game, it will have GraphicsDevice provided and we just need to send it into the constructor together with field of view angle, near and far planes, position and target coordinates to look at.

All of my game objects inherit from the following abstract GameObject class
public abstract class AGameObject
{
 public Vector3 Coordinates;
 public Vector3 Rotation;
 public Vector3 Velocity;
 public float Scale = 1f;
}
So each game object has a position in the game world that is stored in Coordinates, it also has an rotation, velocity and scale.


Whenever we want to render a game object in a scene, we wrap it in a Renderable object that also has a model and a Draw method:
public class Renderable
{
 public VertexPositionNormalTexture[] Model;
 public BasicEffect BasicEffect;
 public AGameObject GameObject;

 public Renderable(AGameObject gameObject, GraphicsDevice graphicsDevice)
 {
  GameObject = gameObject;
  BasicEffect = new BasicEffect(graphicsDevice)
  {
   AmbientLightColor = Vector3.One,
   LightingEnabled = true,
   DiffuseColor = Vector3.One,
  };
 }

 public void Draw(GraphicsDevice graphicsDevice, ICamera camera)
 {
  var t2 = Matrix.CreateTranslation(GameObject.Coordinates.X, GameObject.Coordinates.Y, GameObject.Coordinates.Z);
  var r1 = Matrix.CreateRotationX(GameObject.Rotation.X);
  var r2 = Matrix.CreateRotationY(GameObject.Rotation.Y);
  var r3 = Matrix.CreateRotationZ(GameObject.Rotation.Z);
  var s = Matrix.CreateScale(GameObject.Scale);

  BasicEffect.World = r1 * r2 * r3 * s * t2;

  BasicEffect.View = camera.ViewMatrix;
  BasicEffect.Projection = camera.ProjectionMatrix;
  
  BasicEffect.EnableDefaultLighting();

  foreach (var pass in BasicEffect.CurrentTechnique.Passes)
  {
   pass.Apply();

   graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, Model, 0, Model.Length / 3);
  }

 }
}

Here we can see that the render method takes a graphicsDevice and camera. Here we take the ViewMatrix from the camera and put it in the BasicEffect.View, and the same goes for the ProjectionMatrix that is put in BasicEffect.Projection.
The BasicEffect.World receives the rotated, scaled and translated matrix generated from the GameObject.

Now we need to look up our Draw method in the main game file (Game1.cs) and place calls to render our Renderables there
private List<Renderable> _scene = new List<Renderable>();
private readonly RasterizerState _rasterizerState = new RasterizerState
{
    CullMode = CullMode.CullClockwiseFace
};

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.RasterizerState = _rasterizerState;
    GraphicsDevice.Clear(Color.CornflowerBlue);
    foreach(var x in _scene)
    {
        x.Draw(GraphicsDevice, _camera);
    }
    base.Draw(gameTime);
}
First we tell the graphics device to save some time by culling triangles that are backwards facing, we then clear the scene to a background color.
For simplicity we store all our Renderables in a list called _scene and iterate through it and call Draw on each element.


I hope this helps someone out there to get unstuck when starting to use MonoGame.

All code provided as-is. This is copied from my own code-base, May need some additional programming to work. Use for whatever you want, how you want! If you find this helpful, please leave a comment or share on social media, not required but much appreciated! :)

3 comments:

  1. I am between MonoGame or OpenTK

    ReplyDelete
    Replies
    1. It is a hard choice, both give you full control and ability to do stuff with code. Never fell in love with Unity or other engines that force you into to do things their way... *stubborn* :)

      Delete
    2. Yes, it's hard to choice, now I am reading and watching videos about C++ because I want to learn how to create video games but I think I need to focus and I will choose MonoGame.

      Delete