New lighting system

Over the next couple of days, I’ll be revisiting the lighting system in order to get a more complete system which is easy to use, and also implement new lights like Spot Lights. Here is the new hemispheric light:

Got the kettle on?

Introducing the Light Manager…

I have finally managed to get round to implementing the LightManager. This post will explain how this in implemented into the engine and how to use the new class.

Back in the earlier version of this project, I wrote a simple Light Manager. This new LightManager class is based on the old one, but with a few new modifications. Start by creating a class in the Core folder called LightManager. Add the following namespaces:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ProjectVanquish.Renderers;
using ProjectVanquish.Cameras;

Declare the class as public and then add the following fields:

Effect directionalLightEffect, hemisphericLightEffect, pointLightEffect;
Model sphereModel;
QuadRenderer fullscreenQuad;
Vector2 halfPixel;
Texture2D hemisphericColorMap;
static Lights.DirectionalLight light;
static IList<Lights.PointLight> pointLights;

Here we are taking some of the fields from the DeferredRenderer class, so we can delete them from there. We can now add the constructor:

public LightManager(Game game)
{
    // Load Effects, Models and Quad Renderer
    directionalLightEffect = game.Content.Load("Shaders/Lights/DirectionalLight");
    pointLightEffect = game.Content.Load("Shaders/Lights/PointLight");
    hemisphericLightEffect = game.Content.Load("Shaders/Lights/HemisphericLight");
    sphereModel = game.Content.Load("Models/Sphere");
    fullscreenQuad = new QuadRenderer(game);
    game.Components.Add(fullscreenQuad);
    halfPixel = new Vector2()
    {
        X = 0.5f / (float)game.GraphicsDevice.PresentationParameters.BackBufferWidth,
        Y = 0.5f / (float)game.GraphicsDevice.PresentationParameters.BackBufferHeight
    };

    // Load the Color Map for the Hemispheric Light
    hemisphericColorMap = game.Content.Load("Textures/ColorMap");

    // Make our directional light source
    light = new Lights.DirectionalLight();
    light.Direction = new Vector3(-1, -1, -1);
    light.Color = new Vector3(0.7f, 0.7f, 0.7f);

    // Instantiate the PointLights List
    pointLights = new List();
} 

Back in the DeferredRenderer class, we had all of the old DrawLights code. We’ll start adding this into this class.

void DrawDirectionalLight(RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, Camera camera)
{
    // Set all parameters
    directionalLightEffect.Parameters["colorMap"].SetValue(colorRT);
    directionalLightEffect.Parameters["normalMap"].SetValue(normalRT);
    directionalLightEffect.Parameters["depthMap"].SetValue(depthRT);
    directionalLightEffect.Parameters["lightDirection"].SetValue(light.Direction);
    directionalLightEffect.Parameters["Color"].SetValue(light.Color);
    directionalLightEffect.Parameters["cameraPosition"].SetValue(camera.Position);
    directionalLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(camera.ViewMatrix * camera.ProjectionMatrix));
    directionalLightEffect.Parameters["halfPixel"].SetValue(halfPixel);

    // Apply the Effect
    directionalLightEffect.Techniques[0].Passes[0].Apply();

    // Draw a FullscreenQuad
    fullscreenQuad.Render(Vector2.One * -1, Vector2.One);
}

You’ll notice that the method declaration is different from the old one. This is because we don’t have direct access to the required fields like we did before, and I’d like to keep it that way. We’ll be passing them through the modified DrawLights method, which we’ll get to shortly. The next method to move is the DrawHemisphericLight:

void DrawHemisphericLight(GraphicsDevice device, SceneManager scene, Camera camera)
{
    device.BlendState = BlendState.Opaque;

    // Only apply the effect to those models in the Frustum
    foreach (Models.Actor actor in scene.Models.Where(a => camera.BoundingFrustum.Intersects(a.BoundingSphere)))
    {
        foreach (ModelMesh mesh in actor.Model.Meshes)
        {
            foreach (ModelMeshPart part in mesh.MeshParts)
            {
                // Set the Effect Parameters
                hemisphericLightEffect.Parameters["matWorldViewProj"].SetValue(actor.World * camera.ViewMatrix * camera.ProjectionMatrix);
                hemisphericLightEffect.Parameters["matInverseWorld"].SetValue(actor.World);
                hemisphericLightEffect.Parameters["vLightDirection"].SetValue(new Vector4(light.Direction, 1));
                hemisphericLightEffect.Parameters["ColorMap"].SetValue(hemisphericColorMap);
                hemisphericLightEffect.Parameters["LightIntensity"].SetValue(0.4f);
                hemisphericLightEffect.Parameters["SkyColor"].SetValue(new Vector4(light.Color, 1));

                // Apply the Effect
                hemisphericLightEffect.Techniques[0].Passes[0].Apply();

                // Render the Primitives
                device.SetVertexBuffer(part.VertexBuffer, part.VertexOffset);
                device.Indices = part.IndexBuffer;
                device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, part.NumVertices, part.StartIndex, part.PrimitiveCount);
            }
        }
    }

    device.BlendState = BlendState.AlphaBlend;
}

Before moving onto the DrawLights method, we’ll implement the DrawPointLights method:

void DrawPointLight(GraphicsDevice device, RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, Camera camera, Lights.PointLight pointLight)
{
    // Set the G-Buffer parameters
    pointLightEffect.Parameters["colorMap"].SetValue(colorRT);
    pointLightEffect.Parameters["normalMap"].SetValue(normalRT);
    pointLightEffect.Parameters["depthMap"].SetValue(depthRT);

    // Compute the light world matrix
    // scale according to light radius, and translate it to light position
    Matrix sphereWorldMatrix = Matrix.CreateScale(pointLight.Range) * Matrix.CreateTranslation(pointLight.Position);
    pointLightEffect.Parameters["World"].SetValue(sphereWorldMatrix);
    pointLightEffect.Parameters["View"].SetValue(camera.ViewMatrix);
    pointLightEffect.Parameters["Projection"].SetValue(camera.ProjectionMatrix);

    // Light position
    pointLightEffect.Parameters["lightPosition"].SetValue(pointLight.Position);

    // Set the color, radius and Intensity
    pointLightEffect.Parameters["Color"].SetValue(pointLight.Color);
    pointLightEffect.Parameters["lightRadius"].SetValue(pointLight.Range);
    pointLightEffect.Parameters["lightIntensity"].SetValue(pointLight.Intensity);

    // Parameters for specular computations
    pointLightEffect.Parameters["cameraPosition"].SetValue(camera.Position);
    pointLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(camera.ViewMatrix * camera.ProjectionMatrix));

    // Size of a halfpixel, for texture coordinates alignment
    pointLightEffect.Parameters["halfPixel"].SetValue(halfPixel);

    // Calculate the distance between the camera and light center
    float cameraToCenter = Vector3.Distance(camera.Position, pointLight.Position);

    // If we are inside the light volume, draw the sphere's inside face
    if (cameraToCenter < pointLight.Range)
        device.RasterizerState = RasterizerState.CullClockwise;
    else
        device.RasterizerState = RasterizerState.CullCounterClockwise;

    // Reset DepthStencilState
    device.DepthStencilState = DepthStencilState.None;

    // Apply the Effect
    pointLightEffect.Techniques[0].Passes[0].Apply();

    // Draw the Sphere mesh
    foreach (ModelMesh mesh in sphereModel.Meshes)
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
            device.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
            device.Indices = meshPart.IndexBuffer;
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
        }

    // Reset RenderStates
    device.RasterizerState = RasterizerState.CullCounterClockwise;
    device.DepthStencilState = DepthStencilState.Default;
} 

You may have noticed some changes to this method. We are now using a pointLight object. We’ll need to modify the PointLight class to allow for the new properties, but we’ll finish this class first. Lastly, we’ll add the modified DrawLights method:

public void DrawLights(GraphicsDevice device, RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, RenderTarget2D lightRT, Camera camera, SceneManager scene)
{
    // Set the Light RenderTarget
    device.SetRenderTarget(lightRT);

    // Clear all components to 0
    device.Clear(Color.Transparent);
    device.BlendState = BlendState.AlphaBlend;
    device.DepthStencilState = DepthStencilState.None;

    // Render either the Directional or Hemispheric light
    if (UseHemisphericLight)
        DrawHemisphericLight(device, scene, camera);
    else
        DrawDirectionalLight(colorRT, normalRT, depthRT, camera);

    // Render each PointLight
    foreach (Lights.PointLight pointLight in pointLights)
        DrawPointLight(device, colorRT, normalRT, depthRT, camera, pointLight);

    // Reset RenderStates
    device.BlendState = BlendState.Opaque;
    device.DepthStencilState = DepthStencilState.None;
    device.RasterizerState = RasterizerState.CullCounterClockwise;

    // Reset the RenderTarget
    device.SetRenderTarget(null);
}

We have now got our new LightManager class ready, so we can plug this into the DeferredRenderer class. In the DeferredRenderer class, add a new field:

private LightManager lightManager;

In the LoadContent method, we’ll instantiate the LightManager:

// Instantiate the LightManager
lightManager = new LightManager(Game);

We are reaching the final goal posts. There is one big change to the DeferredRenderer class. As we have modified the DrawLights call, we need to build a new method:

void CombineFinal(RenderTarget2D shadowOcclusion)
{
    // If SSAO is enabled, set the RenderTarget
    if (SSAORenderer.Enabled)
        GraphicsDevice.SetRenderTarget(sceneRT);

    // Set the effect parameters
    finalCombineEffect.Parameters["colorMap"].SetValue(colorRT);
    finalCombineEffect.Parameters["lightMap"].SetValue(lightRT);
    finalCombineEffect.Parameters["halfPixel"].SetValue(halfPixel);
    finalCombineEffect.Parameters["shadowOcclusion"].SetValue(shadowOcclusion);

    // Apply the Effect
    finalCombineEffect.Techniques[0].Passes[0].Apply();

    // Render a full-screen quad
    quadRenderer.Render(Vector2.One * -1, Vector2.One);
}

This will create our final scene. In the Draw method, we need to alter it to use the new LightManager.DrawLights method, and also include the new method. Place the following after the Render Shadows call:

// Draw Lights
lightManager.DrawLights(GraphicsDevice, colorRT, normalRT, depthRT, lightRT, camera, scene);

// Combine the Final scene
CombineFinal(shadowOcclusion);

Before we can compile this, we need to modify the PointLight class. Below is the complete listing for the PointLight class:

protected float intensity = 0.0f;
protected float range = 0.0f; 

public PointLight(Vector3 position, Vector3 color, float range, float intensity)
    : base()
{
    Position = position;
    Color = color;
    Range = range;
    Intensity = intensity;
} 

public float Intensity
{
    get { return intensity; }
    set { intensity = value;}
}

public Vector3 Position
{
    get { return worldMatrix.Translation; }
    set { worldMatrix.Translation = value; }
}

public float Range
{
    get { return range; }
    set { range = value; }
} 

Because the SpotLight class is inherited from the PointLight class, we’ll need to modify the constructor of that too. Here is the modified version:

public SpotLight(Vector3 position, Vector3 color, float range, float intensity)
    : base(position, color, range, intensity)

Now, compiling the project shouldn’t give you any errors and you’ll be ok to run it, albeit without Point Lights as these are not yet implemented into the LightManager class yet. The full sourcecode can be downloaded from the Codeplex page, and the source code is found here.

What’s next inline?

Well, after resolving the issue of the PointLight, it highlighted the fact that there is no real way of adding a new PointLight.  So, I’m going to start work on a LightManager class that will make this a lot easier.  As soon as I’ve done this, I’ll create a post explaining what has been implemented with a step-by-step guide so you can implement it yourselves.

Adding a basic Light Manager – Part 1

In the last post we implemented Point lights, but it was hard coded into the “DeferredRenderer” class. In this post, we’ll implement a very basic Light Manager that will store the lights and render them. This is a very simple implementation. I’d really like you, the community, to help implement a decent Light Manager, but for the time being we’ll use this.

In the “ProjectVanquish” project, add a new Class under the “Core” folder called “LightManager”. Add the following namespaces:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using ProjectVanquish.Cameras;
using ProjectVanquish.Renderers;

Add the following variables:

ContentManager content;
GraphicsDevice device;
Effect directionalLightEffect, pointLightEffect;
QuadRenderer fullscreenQuad;
Vector2 halfPixel;
Model sphere;

Now we can create our constructor:

public LightManager(GraphicsDevice device, ContentManager content)
{
    this.content = content;
    this.device = device;
    directionalLightEffect = content.Load<Effect>("Shaders/Lights/DirectionalLight");
    pointLightEffect = content.Load<Effect>("Shaders/Lights/PointLight");
    fullscreenQuad = new QuadRenderer(device);
    halfPixel = new Vector2()
    {
        X = 0.5f / (float)device.PresentationParameters.BackBufferWidth,
        Y = 0.5f / (float)device.PresentationParameters.BackBufferHeight
    };
    sphere = content.Load<Model>("Models/sphere");
}

As you can see, we are now loading the “Sphere” Model in our “LightManager” constructor, so we should remove this from the “DeferredRenderer” class as well.

We now have duplicated code as we are loading the light Effects in the “DeferredRenderer” class and the new “LightManager” class. Remove the instantiation code from the “DeferredRenderer” class, but not the rendering methods yet. We need to move these methods into the “LightManager” class, so we’ll start with the “DrawLights” method. Cut the method and paste it into the “LightManager” class. We’ll get some errors now, because the RenderTargets don’t exist in this class. Alter the “DrawLights” method declaration to the following:

public void DrawLights(RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, RenderTarget2D lightRT, FreeCamera camera)

Find the “DrawDirectionalLight” and “DrawPointLight” methods in the “DeferredRenderer” class and cut and paste into the “LightManager” class. Both method declarations will now need to change in order to work. We’ll start with the new declaration of the “DrawDirectionalLight” method:

void DrawDirectionalLight(RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, FreeCamera camera, Vector3 lightDirection, Color color)

And the new “DrawPointLight” method:

void DrawPointLight(RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, FreeCamera camera, Vector3 lightPosition, Color color, float lightRadius, float lightIntensity)

The last thing left to do is to find sceneManager.Camera instances and change them to camera.

Back in our “DeferredRenderer” class, let’s instantiate this new “LightManager” class. Add a variable:

private LightManager lightManager;

In the constructor, instantiate it:

lightManager = new LightManager(device, content);

The last thing left to do is to alter the “DrawLights” method in the “Draw” method. This now becomes:

lightManager.DrawLights(colorRT, normalRT, depthRT, lightRT, sceneManager.Camera);

Build the solution and you should get no errors. If you run the code you should still see our scene from the last post. In the next part, we’ll extend this “LightManager” class by creating a “PointLight” class which we can instantiate and control it’s position, direction etc. from the “DeferredRenderer” class.