Adding a basic Light Manager – Part 2

We’ll start by creating a new folder in our “ProjectVanquish” project. Locate the “Core” folder and create a new folder called “Lights”. Create a class called “PointLight” in this folder and once created, add the following namespace:

using Microsoft.Xna.Framework;

Now, alter the class declaration so that it is public.

public class PointLight

And declare the following variables:

Vector3 position;
Color color;
float lightRadius, lightIntensity;

Pretty simple in what we are trying to achieve here. Just a simple class to store the position, color, radius and intensity of the light. Let’s add a constructor:

public PointLight(Vector3 position, Color color, float radius, float intensity)
{
    this.position = position;
    this.color = color;
    lightRadius = radius;
    lightIntensity = intensity;
}

And to finish this class off, we just need to declare some properties the the “LightManager” class can use:

public Color Color { get { return color; } }

public float Radius { get { return lightRadius; } }

public float Intensity { get { return lightIntensity; } }

public Vector3 Position { get { return position; } }

We need to make some big modifications to our “LightManager” class now. In our “DrawLights” method, where we have our “DrawPointLight” call, we need to change this to the following:

foreach (PointLight light in pointLights)
    DrawPointLight(light, colorRT, normalRT, depthRT, camera);

There is a new variable there, called “pointLights”. This is a list of all of the instantiated Point lights. Let’s add this variable:

IList<PointLight> pointLights = new List<PointLight>();

We will also need to include the new namespace, else we can’t use the “PointLight” class:

using ProjectVanquish.Core.Lights;

Going back to our “DrawPointLight” method, our declaration has changed some what. We are now passing in a “PointLight” object. Amend the “DrawPointLight” method to:

void DrawPointLight(PointLight light, RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, FreeCamera camera)

The last thing to change in this code is all of the parameters where we used to have them passed in. These are now part of the “PointLight” object. I’ve included the final version of the “DrawPointLight” to save time:

void DrawPointLight(PointLight light, RenderTarget2D colorRT, RenderTarget2D normalRT, RenderTarget2D depthRT, FreeCamera camera)
{
    // 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(light.Radius) * Matrix.CreateTranslation(light.Position);
    pointLightEffect.Parameters["World"].SetValue(sphereWorldMatrix);
    pointLightEffect.Parameters["View"].SetValue(camera.View);
    pointLightEffect.Parameters["Projection"].SetValue(camera.Projection);
    // Light position
    pointLightEffect.Parameters["lightPosition"].SetValue(light.Position);

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

    // Parameters for specular computations
    pointLightEffect.Parameters["cameraPosition"].SetValue(camera.Position);
    pointLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(camera.View * camera.Projection));
    // 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, light.Position);
    // If we are inside the light volume, draw the sphere's inside face
    if (cameraToCenter < light.Radius)
        device.RasterizerState = RasterizerState.CullClockwise;
    else
        device.RasterizerState = RasterizerState.CullCounterClockwise;

    device.DepthStencilState = DepthStencilState.None;

    pointLightEffect.Techniques[0].Passes[0].Apply();
    foreach (ModelMesh mesh in sphere.Meshes)
    {
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
            device.Indices = meshPart.IndexBuffer;
            device.SetVertexBuffer(meshPart.VertexBuffer);

            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
        }
    }

    device.RasterizerState = RasterizerState.CullCounterClockwise;
    device.DepthStencilState = DepthStencilState.Default;
}

All being well, the project will compile without any issues. Let’s add the final method to the “LightManager” class:

public void AddLight(PointLight light)
{
    pointLights.Add(light);
}

This method will allow the “DeferredRenderer” class to be able to add new Point lights, but we can’t access this method from outside of the “DeferredRenderer” class. So, in our “DeferredRenderer” class, we add the following method:

public void AddLight(PointLight light)
{
    lightManager.AddLight(light);
}

Build the solution and once it’s finished, we’ll be able to add Point lights from our “Game1” class. To do so, in the “Game1” class, we’ll need to add the new “Lights” namespace:

using ProjectVanquish.Core.Lights;

Locate the “LoadContent” method. Under the renderer.AddModel() line, add:

renderer.AddLight(new PointLight(new Vector3(-30, 1, -70), Color.Red, 30, 5));
renderer.AddLight(new PointLight(new Vector3(0, 1, -70), Color.Green, 30, 5));
renderer.AddLight(new PointLight(new Vector3(30, 1, -70), Color.Blue, 30, 5));

Here we are creating 3 Point lights (Red, Green and Blue), positioned in a line which should produce the following output:

In the next post, we’ll add a simple Frames Per Second counter so we can see how well the engine is performing.

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.

Implementing Point lights

So, I thought it would be a good point to release the source code that we have been building throughout the project. Here is the Source Code.

If you haven’t been typing the code yourself (or copying/pasting), download the code and then let’s move on with implementing Point lights 🙂

Roy’s source code contains a “Sphere” model. Extract this model to our “Models” directory in the “ProjectVanquishTestContent” project. We don’t need to change the “Content Processor” for this Model, so we’ll just keep it as the default values. Add new a “Effect” file to the “Lights” folder under the “Shaders” directory and call it “PointLight”. Clear out the default content and add in the following parameter declarations:

float4x4 World;
float4x4 View;
float4x4 Projection;
// Color of the light 
float3 Color; 
// Position of the camera, for specular light
float3 cameraPosition; 
// This is used to compute the world-position
float4x4 InvertViewProjection; 
// This is the position of the light
float3 lightPosition;
// How far does this light reach
float lightRadius;
// Control the brightness of the light
float lightIntensity = 1.0f;
float2 halfPixel;

Next we’ll add our Sampler States:

// Diffuse color, and SpecularIntensity in the alpha channel
texture colorMap; 
sampler colorSampler = sampler_state
{
    Texture = (colorMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = LINEAR;
    MinFilter = LINEAR;
    Mipfilter = LINEAR;
};

// Depth
texture depthMap;
sampler depthSampler = sampler_state
{
    Texture = (depthMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = POINT;
    MinFilter = POINT;
    Mipfilter = POINT;
};

// Normals, and SpecularPower in the alpha channel
texture normalMap;
sampler normalSampler = sampler_state
{
    Texture = (normalMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = POINT;
    MinFilter = POINT;
    Mipfilter = POINT;
};

Create our Vertex Shader Input, Output and Function:

struct VertexShaderInput
{
    float3 Position : POSITION0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 ScreenPosition : TEXCOORD0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    // Processing geometry coordinates
    float4 worldPosition = mul(float4(input.Position,1), World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    output.ScreenPosition = output.Position;
    return output;
}

We’ll add our Pixel Shader and Technique:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // Obtain screen position
    input.ScreenPosition.xy /= input.ScreenPosition.w;

    // Obtain textureCoordinates corresponding to the current pixel
    // The screen coordinates are in [-1,1]*[1,-1]
    // The texture coordinates need to be in [0,1]*[0,1]
    float2 texCoord = 0.5f * (float2(input.ScreenPosition.x,-input.ScreenPosition.y) + 1);
    // Align texels to pixels
    texCoord -=halfPixel;

    // Get normal data from the normalMap
    float4 normalData = tex2D(normalSampler,texCoord);
    // Transform normal back into [-1,1] range
    float3 normal = 2.0f * normalData.xyz - 1.0f;
    // Get specular power
    float specularPower = normalData.a * 255;
    // Get specular intensity from the colorMap
    float specularIntensity = tex2D(colorSampler, texCoord).a;

    // Read depth
    float depthVal = tex2D(depthSampler,texCoord).r;

    // Compute screen-space position
    float4 position;
    position.xy = input.ScreenPosition.xy;
    position.z = depthVal;
    position.w = 1.0f;
    // Transform to world space
    position = mul(position, InvertViewProjection);
    position /= position.w;

    // Surface-to-light vector
    float3 lightVector = lightPosition - position;

    // Compute attenuation based on distance - linear attenuation
    float attenuation = saturate(1.0f - length(lightVector)/lightRadius); 

    // Normalize light vector
    lightVector = normalize(lightVector); 

    // Compute diffuse light
    float NdL = max(0,dot(normal,lightVector));
    float3 diffuseLight = NdL * Color.rgb;

    // Reflection vector
    float3 reflectionVector = normalize(reflect(-lightVector, normal));
    // Camera-to-surface vector
    float3 directionToCamera = normalize(cameraPosition - position);
    // Compute specular light
    float specularLight = specularIntensity * pow( saturate(dot(reflectionVector, directionToCamera)), specularPower);

    // Take into account attenuation and lightIntensity.
    return attenuation * lightIntensity * float4(diffuseLight.rgb,specularLight);
}

technique PointLight
{
    pass Pass0
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Build the project and we should receive no errors. Now we need to implement this new Shader into our “DeferredRenderer” class. Open the “DeferredRenderer” class and add a new variable:

private Effect pointLightEffect;

In the constructor, we’ll instantiate it:

pointLightEffect = content.Load<Effect>("Shaders/Lights/PointLight");

Excellent. We now have our Effect initalized, now we need to add the “Sphere” Model. Create a new variable:

private Model sphere;

And back in the constructor, we’ll instantiate it:

sphere = content.Load<Model>("Models/sphere");

We now have the Effect and Model ready to go. The last steps are to create a method that will render them. In our “DrawLights” method, after we the “DrawDirectionalLight” method, add:

DrawPointLight(new Vector3(0, 1, -70), Color.Red, 30, 5);

What the above will do is create a Point light at a position of Y: 1, Z: -70 and set the colour to Red. It will assign a radius to the Point light as well as the intensity. Comment out the “DrawDirectionalLight” method call.

Let’s create the method and populate our PointLight Effect parameters:

void DrawPointLight(Vector3 lightPosition, Color color, float lightRadius, float lightIntensity)
{
    // 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(lightRadius) * Matrix.CreateTranslation(lightPosition);
    pointLightEffect.Parameters["World"].SetValue(sphereWorldMatrix);
    pointLightEffect.Parameters["View"].SetValue(sceneManager.Camera.View);
    pointLightEffect.Parameters["Projection"].SetValue(sceneManager.Camera.Projection);
    // Light position
    pointLightEffect.Parameters["lightPosition"].SetValue(lightPosition);

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

    // Parameters for specular computations
    pointLightEffect.Parameters["cameraPosition"].SetValue(sceneManager.Camera.Position);
    pointLightEffect.Parameters["InvertViewProjection"].SetValue(
                                                        Matrix.Invert(
                                                           sceneManager.Camera.View * 
                                                           sceneManager.Camera.Projection));
    // 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(sceneManager.Camera.Position, lightPosition);
    // If we are inside the light volume, draw the sphere's inside face
    if (cameraToCenter < lightRadius)
        device.RasterizerState = RasterizerState.CullClockwise;                
    else
        device.RasterizerState = RasterizerState.CullCounterClockwise;

    device.DepthStencilState = DepthStencilState.None;

    pointLightEffect.Techniques[0].Passes[0].Apply();
    foreach (ModelMesh mesh in sphere.Meshes)
    {
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
            device.Indices = meshPart.IndexBuffer;
            device.SetVertexBuffer(meshPart.VertexBuffer);
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
        }
    }            
            
    device.RasterizerState = RasterizerState.CullCounterClockwise;
    device.DepthStencilState = DepthStencilState.Default;
}

Build the solution and then run the project. Fingers crossed, you should be seeing:

In the next post we’ll look at creating a basic Light Manager so we can add new lights from our “ProjectVanquishTest” project, rather than hardcoding values in the “DeferredRenderer” class.

Directional lighting

In this post we’ll add a Directional light to our engine. We’ll start by creating a new folder in the “ProjectVanquishTestContent” project under the “Shaders” folder called “Lights”. We’ll add all of our lighting shaders in here. Create a new Effect file called “DirectionalLight” and remove all of the files content. We’ll start the shader off with some parameter declarations:

// Direction of the light
float3 lightDirection;
// Color of the light 
float3 Color; 
// Position of the camera, for specular light
float3 cameraPosition; 
// This is used to compute the world-position
float4x4 InvertViewProjection; 
// Diffuse color, and SpecularIntensity in the alpha channel
texture colorMap; 
// Normals, and SpecularPower in the alpha channel
texture normalMap;
// Depth
texture depthMap;
float2 halfPixel;

Next we’ll define our Sampler States:

sampler colorSampler = sampler_state
{
    Texture = (colorMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = LINEAR;
    MinFilter = LINEAR;
    Mipfilter = LINEAR;
};

sampler depthSampler = sampler_state
{
    Texture = (depthMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = POINT;
    MinFilter = POINT;
    Mipfilter = POINT;
};

sampler normalSampler = sampler_state
{
    Texture = (normalMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = POINT;
    MinFilter = POINT;
    Mipfilter = POINT;
};

We’ll create our Vertex Shader Input and Output structs, plus our VertexShaderFunction:

struct VertexShaderInput
{
    float3 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    output.Position = float4(input.Position,1);
    // Align texture coordinates
    output.TexCoord = input.TexCoord - halfPixel;
    return output;
}

The only things left to do is create our Pixel Shader Function and to add our technique:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // Get normal data from the normalMap
    float4 normalData = tex2D(normalSampler,input.TexCoord);
    // Transform normal back into [-1,1] range
    float3 normal = 2.0f * normalData.xyz - 1.0f;
    // Get specular power, and get it into [0,255] range]
    float specularPower = normalData.a * 255;
    // Get specular intensity from the colorMap
    float specularIntensity = tex2D(colorSampler, input.TexCoord).a;
    
    // Read depth
    float depthVal = tex2D(depthSampler,input.TexCoord).r;

    // Compute screen-space position
    float4 position;
    position.x = input.TexCoord.x * 2.0f - 1.0f;
    position.y = -(input.TexCoord.x * 2.0f - 1.0f);
    position.z = depthVal;
    position.w = 1.0f;
    // Transform to world space
    position = mul(position, InvertViewProjection);
    position /= position.w;
    
    // Surface-to-light vector
    float3 lightVector = -normalize(lightDirection);

    // Compute diffuse light
    float NdL = max(0,dot(normal,lightVector));
    float3 diffuseLight = NdL * Color.rgb;

    // Reflection vector
    float3 reflectionVector = normalize(reflect(-lightVector, normal));
    // Camera-to-surface vector
    float3 directionToCamera = normalize(cameraPosition - position);
    // Compute specular light
    float specularLight = specularIntensity * pow( saturate(dot(reflectionVector, directionToCamera)), specularPower);

    // Output the two lights
    return float4(diffuseLight.rgb, specularLight) ;
}

technique DirectionalLight
{
    pass Pass0
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Great! Build the solution to make sure that you don’t have any errors. Back in our “DeferredRenderer” class, let’s define a new variable:

private Effect directionalLightEffect;

In the constructor, let’s instantiate it:

directionalLightEffect = content.Load<Effect>("Shaders/Lights/DirectionalLight");

In the “Draw” method, we have one comment left, and that is to do with the lights. Change the comment too:

DrawLights();

Now, let us create this new method:

void DrawLights()
{
}

In this method, we’ll set the Light RenderTarget, the BlendState and the DepthStencilState, draw the lights and reset the BlendState, DepthStencilState and the Light RenderTarget.

void DrawLights()
{
    device.SetRenderTarget(lightRT);
    device.Clear(Color.Transparent);
    device.BlendState = BlendState.AlphaBlend;
    device.DepthStencilState = DepthStencilState.None;

    // Draw lights
    DrawDirectionalLight(new Vector3(0, -1, 0), Color.Blue);

    device.BlendState = BlendState.Opaque;
    device.DepthStencilState = DepthStencilState.None;
    device.RasterizerState = RasterizerState.CullCounterClockwise;
    device.SetRenderTarget(null);
}

There is a new method in there which we’ll need to create:

void DrawDirectionalLight(Vector3 lightDirection, Color color)
{
}

This is really a nasty hack in order to create a Directional light for testing purposes. Outside of the “DeferredRenderer” class, you cannot modify the Color or Position of this light. This is where a Light Manager will come in handy and all of the Light rendering will move in there, much like the Scene Manager. Anyway, let’s continue with the code. In this method we’ll be assigning the parameter values in the “DirectionalLight” effect:

void DrawDirectionalLight(Vector3 lightDirection, Color color)
{
    directionalLightEffect.Parameters["colorMap"].SetValue(colorRT);
    directionalLightEffect.Parameters["normalMap"].SetValue(normalRT);
    directionalLightEffect.Parameters["depthMap"].SetValue(depthRT);
    directionalLightEffect.Parameters["lightDirection"].SetValue(lightDirection);
    directionalLightEffect.Parameters["Color"].SetValue(color.ToVector3());
    directionalLightEffect.Parameters["cameraPosition"].SetValue(sceneManager.Camera.Position);
    directionalLightEffect.Parameters["InvertViewProjection"].SetValue(
                                           Matrix.Invert(sceneManager.Camera.View * 
                                                         sceneManager.Camera.Projection));
    directionalLightEffect.Parameters["halfPixel"].SetValue(halfPixel);
    directionalLightEffect.Techniques[0].Passes[0].Apply();
    fullscreenQuad.Draw();
}

Now, if I haven’t missed anything out, you’ll be able to build this without any errors. If you run the application, you should see the following:

If you feel like changing the colour of the light, change the line in the “DrawLights” method:

DrawDirectionalLight(new Vector3(0, -1, 0), Color.Blue);

In the next part, we’ll look at implement Point lights.