Commit ae9d7c6e authored by Nathan H. Bean's avatar Nathan H. Bean

Added lighting and cameras section

parent 9283a532
---
title: "Introduction"
pre: "1. "
weight: 1
date: 2020-03-24T10:00:00-05:00
---
You've now seen how vertices are grouped into triangles and rendered using accelerated hardware, how we can use a mesh of triangles to represent more complex objects, and how we can apply a texture to that mesh to provide visual detail. Now we need to add light sources that can add shading to our models, and a camera which can be shared by all objects in a scene to provide a common view and projection matrix.
We'll once again be working from a starter project, which provides our needed content resources. You can clone the starter from GitHub here: [https://github.com/ksu-cis/lighting-and-cameras-starter](https://github.com/ksu-cis/lighting-and-cameras-starter)
This diff is collapsed.
---
title: "Adding Lights"
pre: "3. "
weight: 3
date: 2020-03-24T10:00:00-05:00
---
Well, we have a crate. Let's make it more interesting by adding some lights. To start with, we'll use the `BasicEffect`'s default lights. Add the line:
```csharp
effect.EnableDefaultLighting();
```
Into your `Crate.IntializeEffect()` method. Then run the program again. Notice a difference?
![Side-by-side comparison of a lit and unlit crate]({{<static "images/lighting-and-cameras-3.1.png">}}).
The default lighting is useful to quickly see what our object will look like illuminated, but ultimately, we'll want to define our own lights and how they interact with our objects.
## Lighting Calculations
The `BasicEffect` uses the [Phong shading model](https://en.wikipedia.org/wiki/Phong_shading) (named after its inventor, Bui Tuong Phong). This model approximates shading accounting for the smoothness of the object. It uses an equation to calculate the color of each pixel. This equation appears in the image below:
![Phong equation]({{<static "images/phong-equation.png">}})
Essentailly, the Phong approach calculates three different lighting values, and combines them into shading values to apply to a model. Each of these is based on the behavior of light, which is a particle (and a wave) that travels in (largely) stright lines. We can think of these lines as rays.
The first is _ambient_ light, which reprsents light that has been bouncing around the scene so much that it is hitting our object from all directions. Rather than try to capture that chaos, the Phong model simply substitutes a single flat value that is applied to all surfaces in the scene. In a brightly lit scene, this might be a high value; for a creepy night scene, we would use a very low value to provide only dim illumination away from light sources.
The second is _diffuse_ light, which is the light that strikes a surface and scatters. We choose the strength of this light based on the characteristics of the material. Rough materials have more diffuse light, as the light striking the surface bounces off in all directions, so only some of it is toward the observer.
The third is _specular_ light, which is _also_ light that strikes a surface and bounces off, and is chosen by the properties of the material. However, high specular light corresponds to smooth surfaces - because they are smooth, light rays that strike near one another tend to bounce the same direction. Hence, light that is striking at the right angle will all bounce towards the veiwer, creating "hot spots" of very bright color.
These calculations are based on the angle between the surface and the viewer - this is why we need to provide a normal, as well as a direction the camera is looking and a direction the light is coming from; the angles between these vectors are used in calculating these lighting components.
The `BasicEffect` uses the [DirectionalLight class](https://www.monogame.net/documentation/?page=T_Microsoft_Xna_Framework_Graphics_DirectionalLight) to represent lights. You define the diffuse and specular color as `Vector3` objects (where the x,y,and z correspond to rgb values, within the range [0..1] where 0 is no light, and 1 is full light). You also define a direction the light is coming from as a `Vector3`. Since ambient light doesn't have a direction, you simply represent it with a color `Vector3`. When the object is rendered, the shader combines those color contributions of each light additively with the colors sampled from the texture(s) that are being applied. We can define up to three directional light sources with the `BasicEffect`.
## Customizing our Crate Lighting
Let's see this in action. Delete the `effect.EnableDefaultLighting()` line in your `Crate.InitializeEffect()` and replace it with:
```csharp
// Turn on lighting
effect.LightingEnabled = true;
// Set up light 0
effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight0.Direction = new Vector3(1f, 0, 1f);
effect.DirectionalLight0.DiffuseColor = new Vector3(0.8f, 0, 0);
effect.DirectionalLight0.SpecularColor = new Vector3(1f, 0.4f, 0.4f);
```
Notice the difference? We're shining a red light onto our crate from an oblique angle, above and to the left.
![The Illuminated Crate]({{<static "images/lighting-and-cameras-3.2.png">}})
Notice how one face of the crate is in complete shadow? Let's add some ambient light with the command:
```csharp
effect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
```
![The crate with ambient light]({{<static "images/lighting-and-cameras-3.3.png">}})
Notice how the shadowed face is now somewhat visible?
Go ahead and try tweaking the values for `AmbientLightColor` and `DirectionalLight0`, and see how that changes the way your crate looks. You can also set the properties of `DirectionalLight1` and `DirectionalLight2`.
---
title: "Adding a Camera"
pre: "4. "
weight: 4
date: 2020-03-24T10:00:00-05:00
---
So far we've set the World, View, and Transform matrix of each 3D object within that object. That works fine for these little demo projects, but once we start building a full-fledged game, we expect to look at everything in the world _from the same perspective_. This effectively means we want to use the _same_ view and perspective matrices for all objects in a scene. Moreover, we want to move that perspective around in a well-defined manner.
What we want is a _camera_ - an object that maintains a position and derives a view matrix from that position. Our camera also should provide a projection matrix, as we may want to tweak it in response to game activity - i.e. we might swap it for another matrix when the player uses a sniper rifle.
In fact, we may want _multiple_ cameras in a game. We might want to change from a first-person camera to an overhead camera when the player gets into a vehicle, or we may want to present a flythrough of the level before the player starts playing. Since each of these may work in very different ways, let's start by defining an interface of thier common aspects.
## The ICamera Interface
Those commonalities are our two matrices - the view and the perspective. Let's expose them with read-only properties (properties with only a getter):
```csharp
/// <summary>
/// An interface defining a camera
/// </summary>
public interface ICamera
{
/// <summary>
/// The view matrix
/// </summary>
Matrix View { get; }
/// <summary>
/// The projection matrix
/// </summary>
Matrix Projection { get; }
}
```
Now let's define some cameras.
## CirclingCamera
To start with, let's duplicate something we've already done. Let's create a camera that just spins around the origin. We'll call it `CirclingCamera`:
```csharp
/// <summary>
/// A camera that circles the origin
/// </summary>
public class CirclingCamera : ICamera
{
}
```
We know from our previous work, we'll need to keep track of the angle:
```csharp
// The camera's angle
float angle;
```
We might also hold a vector for the camera's position:
```csharp
// The camera's position
Vector3 position;
```
And a rotation speed:
```csharp
// The camera's speed
float speed;
```
And the `Game` (which we need to determine the aspect ratio of the screen):
```csharp
// The game this camera belongs to
Game game;
```
We'll also define private backing variables for our view and perspective matrices:
```csharp
// The view matrix
Matrix view;
// The projection matrix
Matrix projection;
```
And fulfill our interface by making them accessible as properties:
```csharp
/// <summary>
/// The camera's view matrix
/// </summary>
public Matrix View => view;
/// <summary>
/// The camera's projection matrix
/// </summary>
public Matrix Projection => projection;
```
Then we can add our constructor:
```csharp
/// <summary>
/// Constructs a new camera that circles the origin
/// </summary>
/// <param name="game">The game this camera belongs to</param>
/// <param name="position">The initial position of the camera</param>
/// <param name="speed">The speed of the camera</param>
public CirclingCamera(Game game, Vector3 position, float speed)
{
this.game = game;
this.position = position;
this.speed = speed;
this.projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4,
game.GraphicsDevice.Viewport.AspectRatio,
1,
1000
);
this.view = Matrix.CreateLookAt(
position,
Vector3.Zero,
Vector3.Up
);
}
```
This just sets our inital variables. Finally, we can write our update method:
```csharp
/// <summary>
/// Updates the camera's positon
/// </summary>
/// <param name="gameTime">The GameTime object</param>
public void Update(GameTime gameTime)
{
// update the angle based on the elapsed time and speed
angle += speed * (float)gameTime.ElapsedGameTime.TotalSeconds;
// Calculate a new view matrix
this.view =
Matrix.CreateRotationY(angle) *
Matrix.CreateLookAt(position, Vector3.Zero, Vector3.Up);
}
```
Since our rotation is around the origin, we can simply multiply a lookat matrix by a rotation matrix representing the incremental change.
## Refactoring Game1
Finally, we'll need to add our camera to the `Game1` class:
```csharp
// The camera
CirclingCamera camera;
```
Initialize it in the `Game.LoadContent()` method:
```csharp
// Initialize the camera
camera = new CirclingCamera(this, new Vector3(0, 5, 10), 0.5f);
```
Update it in the `Game1.Update()` method:
```csharp
// Update the camera
camera.Update(gameTime);
```
And in our draw method, we'll need to supply this camera to our `crate`. Replace the line `crate.Draw()` with:
```csharp
crate.Draw(camera);
```
### Refactoring Crate
This of course means we'll need to tweak the `Draw` method in `Crate`. Change it to this:
```csharp
/// <summary>
/// Draws the crate
/// </summary>
/// <param name="camera">The camera to use to draw the crate</param>
public void Draw(ICamera camera)
{
// set the view and projection matrices
effect.View = camera.View;
effect.Projection = camera.Projection;
// apply the effect
effect.CurrentTechnique.Passes[0].Apply();
// set the vertex buffer
game.GraphicsDevice.SetVertexBuffer(vertexBuffer);
// set the index buffer
game.GraphicsDevice.Indices = indexBuffer;
// Draw the triangles
game.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList, // Tye type to draw
0, // The first vertex to use
0, // The first index to use
12 // the number of triangles to draw
);
}
```
Now if you run your code, you should find yourself circling the lit crate.
---
title: "More Crates!"
pre: "5. "
weight: 5
date: 2020-03-24T10:00:00-05:00
---
Let's up the ante a bit, and add _multiple_ crates to the game.
## Refactor Crate
We don't want all of our crates in the same spot, so it's time to change our world matrix. Let's refactor our `Crate` so we can pass a matrix in through the constructor:
```csharp
/// <summary>
/// Creates a new crate instance
/// </summary>
/// <param name="game">The game this crate belongs to</param>
/// <param name="type">The type of crate to use</param>
/// <param name="world">The position and orientation of the crate in the world</param>
public Crate(Game game, CrateType type, Matrix world)
{
this.game = game;
this.texture = game.Content.Load<Texture2D>($"crate{(int)type}_diffuse");
InitializeVertices();
InitializeIndices();
InitializeEffect();
effect.World = world;
}
```
It is important that we set the `effect.World` only _after_ we have constructed it in `InitializeEffect()`.
## Refactor Game1
Let's use our refactored `Crate` by changing the variable `crate` in your `Game1` class to an array:
```csharp
// A collection of crates
Crate[] crates;
```
And initialize them in the `Game1.LoadContent()` method:
```csharp
// Make some crates
crates = new Crate[] {
new Crate(this, CrateType.DarkCross, Matrix.Identity),
new Crate(this, CrateType.Slats, Matrix.CreateTranslation(4, 0, 5)),
new Crate(this, CrateType.Cross, Matrix.CreateTranslation(-8, 0, 3)),
new Crate(this, CrateType.DarkCross, Matrix.CreateRotationY(MathHelper.PiOver4) * Matrix.CreateTranslation(1, 0, 7)),
new Crate(this, CrateType.Slats, Matrix.CreateTranslation(3, 0, -3)),
new Crate(this, CrateType.Cross, Matrix.CreateRotationY(3) * Matrix.CreateTranslation(3, 2, -3))
};
```
And draw the collection in `Game1.Draw()`:
```csharp
// Draw some crates
foreach(Crate crate in crates)
{
crate.Draw(camera);
}
```
Try running your code now - you should see a collection of crates.
![Crates]({{<static "images/lighting-and-cameras-5.1.png">}})
\ No newline at end of file
---
title: "FPS Camera"
pre: "6. "
weight: 6
date: 2020-03-24T10:00:00-05:00
---
Let's go ahead and create a camera that the player can actually control. This time, we'll adopt a camera made popular by PC first-person shooters, where the player's looking direction is controlled by the mouse, and the WASD keys move forward and back and strife side-to-side.
## The FPS Camera Class
Let's start by defining our class, `FPSCamera`:
```csharp
/// <summary>
/// A camera controlled by WASD + Mouse
/// </summary>
public class FPSCamera : ICamera
{
}
```
### Private Fields
This camera is somewhat unique in it partially the splits vertical from horizontal axes; the vertical axis _only_ controls the angle the player is looking along, while the horizontal axis informs both looking and the direction of the player's movement. Thus, we'll need to track these angles separately, and combine them when needed:
```csharp
// The angle of rotation about the Y-axis
float horizontalAngle;
// The angle of rotation about the X-axis
float verticalAngle;
```
We also need to keep track of the position of the camera in the world:
```csharp
// The camera's position in the world
Vector3 position;
```
And we need to know what the previous state of the mouse was:
```csharp
// The state of the mouse in the prior frame
MouseState oldMouseState;
```
And an instance of the `Game` class:
```csharp
// The Game this camera belongs to
Game game;
```
### Public Properties
We need to define the `View` and `Projection` matrices to meet our `ICamera` inteface requirements:
```csharp
/// <summary>
/// The view matrix for this camera
/// </summary>
public Matrix View { get; protected set; }
/// <summary>
/// The projection matrix for this camera
/// </summary>
public Matrix Projection { get; protected set; }
```
We'll keep the setters protected, as they should only be set from within the camera (or a derived camera).
We also will provide a `Sensitivity` value for fine-tuning the mouse sensitivity; this would likely be adjusted from a menu, so it needs to be public:
```csharp
/// <summary>
/// The sensitivity of the mouse when aiming
/// </summary>
public float Sensitivity { get; set; } = 0.0018f;
```
We'll likewise expose the speed property, as it may be changed in-game to respond to powerups or special modes:
```csharp
/// <summary>
/// The speed of the player while moving
/// </summary>
public float Speed { get; set; } = 0.5f;
```
### The Constructor
Constructing the `FPSCamera` requires a `Game` instance, and an initial position:
```csharp
/// <summary>
/// Constructs a new FPS Camera
/// </summary>
/// <param name="game">The game this camera belongs to</param>
/// <param name="position">The player's initial position</param>
public FPSCamera(Game game, Vector3 position)
{
this.game = game;
this.position = position;
}
```
Inside the constructor, we'll initialize our angles to `0` (alternatively, you might also add a facing angle to the constructor so you can control both where the player starts and the direction they face):
```csharp
this.horizontalAngle = 0;
this.verticalAngle = 0;
```
We'll also set up our projection matrix:
```csharp
this.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, game.GraphicsDevice.Viewport.AspectRatio, 1, 1000);
```
And finally, we'll center the mouse in the window, and save its state:
```csharp
Mouse.SetPosition(game.Window.ClientBounds.Width / 2, game.Window.ClientBounds.Height / 2);
oldMouseState = Mouse.GetState();
```
### The Update Method
The `Update()` method is where the heavy lifting of the class occurs, updating the camera position and calculating the view matrix. There's a lot going on here, so we'll assemble it line-by-line, discusing each as we add it:
```csharp
/// <summary>
/// Updates the camera
/// </summary>
/// <param name="gameTime">The current GameTime</param>
public void Update(GameTime gameTime)
{
}
```
First up, we'll grab current input states:
```csharp
var keyboard = Keyboard.GetState();
var newMouseState = Mouse.GetState();
```
Then we'll want to handle movement. Before we move the camera, we need to know what direction it is currenlty facing. We can represent this with a `Vector3` in that direction, which we calculate by rotating a forward vector by the horizontal angle:
```csharp
// Get the direction the player is currently facing
var facing = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(horizontalAngle));
```
Then we can apply forward and backward movement along this vector when the W or S keys are pressed:
```csharp
// Forward and backward movement
if (keyboard.IsKeyDown(Keys.W)) position += facing * Speed;
if (keyboard.IsKeyDown(Keys.S)) position -= facing * Speed;
```
The A and D keys provide _strifing_ movement, movement _perpendicular_ to the forward vector. We can find this perpendicular vector by calculating the cross product of the facing and up vectors:
```csharp
// Strifing movement
if (keyboard.IsKeyDown(Keys.A)) position += Vector3.Cross(Vector3.Up, facing) * Speed;
if (keyboard.IsKeyDown(Keys.D)) position -= Vector3.Cross(Vector3.Up, facing) * Speed;
```
That wraps up moving the camera's position in the world. Now we need to tackle where the camera is looking. This means adusting the vertical and horizontal angles based on mouse movement this frame (which we caculate by subtracing the new mouse position from the old):
```csharp
// Adjust horizontal angle
horizontalAngle += Sensitivity * (oldMouseState.X - newMouseState.X);
// Adjust vertical angle
verticalAngle += Sensitivity * (oldMouseState.Y - newMouseState.Y);
```
From these angles, we can calculate the direction the camera is facing, by rotating a forward-facing vector in both the horizontal and vertical axes:
```csharp
direction = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationX(verticalAngle) * Matrix.CreateRotationY(horizontalAngle));
```
With that direction, we can now calculate the view matrix using `Matrix.CreateLookAt()`. The target vector is the direction vector added to the position:
```csharp
// create the veiw matrix
View = Matrix.CreateLookAt(position, position + direction, Vector3.Up);
```
Lastly, we reset the mouse state. First we re-center the mouse, and then we save its new centered state as our old mouse state. This centering is important in Windowed mode, as it keeps our mouse within the window even as the player spins 360 degrees or more. Otherwise, our mouse would pop out of the window, and could interact with other windows while the player is trying to play our game.
```csharp
// Reset mouse state
Mouse.SetPosition(game.Window.ClientBounds.Width / 2, game.Window.ClientBounds.Height / 2);
oldMouseState = Mouse.GetState();
```
This does mean that you can no longer use the mouse to close the window, so it is important to have a means to exit the game. By default, the `Game1` class uses hitting the escape key to do this. In full games you'll probably replace that functionality with a menu that contains an exit option.
## Refactoring the Game Class
Of course, to use this camera, you'll need to replace the `CirclingCamera` references in `Game1` with our `FPSCamera` implementation. So you'll define a private `FPSCamera` reference:
```csharp
// The game camera
FPSCamera camera;
```
Initialize it with its starting position in the `LoadContent()` method:
```csharp
// Initialize the camera
camera = new FPSCamera(this, new Vector3(0, 3, 10));
```
Update it in the `Update()` method (which isn't really a change):
```csharp
// Update the camera
camera.Update(gameTime);
```
And provide it to the crates in the `Draw()` method (again, this shouldn't be a change from the `CirclingCamera` implementation):
```csharp
// Draw some crates
foreach(Crate crate in crates)
{
crate.Draw(camera);
}
```
Now if you run the game, you should be able to move around the scene using WASD keys and the mouse.
---
title: "Summary"
pre: "7. "
weight: 7
date: 2020-03-24T10:00:00-05:00
---
In this lesson, we've seen how to apply Phong lighting using the BasicEffect, and how to set up cameras. Armed with this knowledge, you're ready to start building explorable game environments.
A good next step is to think about what other kinds of cameras you can create. What about an over-the-shoulder camera that follows the player? Or a first-person camera that uses GamePad input? As you now know, a game camera is nothing more than code to determine where the camera is in a scene, and where it is pointed. From that, you can create a View matrix. You might also try expanding the options for the Perspective matrix from the default implementation we've been using.
\ No newline at end of file
+++
title = "Lights and Cameras"
date = 2020-03-20T10:53:05-05:00
weight = 10
chapter = true
+++
### Game Development Techniques
# Lights and Cameras
Lights, Camera, Action!
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment