Rabid Lion Games

 

Well, it’s been a long haul, but we’ve finally got here! In the last part we finished writing our Lighting system and got it to build. Now we can finally start using it. This is also the part where, if you just want to use the system, not understand how it works, then you’ll see how to do that.

Once we’ve written the code to create the demo we saw way back in the first part of the series we’ll discuss ways that the system could be improved, both in optimisation to improve performance/ resource usage, and in extra features that we could add to improve the image quality of our final scene.

 

First up, Game code!

Fire up our Visual Studio solution one last time and open up the Game1 class. This currently has some auto-generated XNA code in it. Leave that there.

We’ll start by adding the fields we’re going to need. Add the following to the top of the class:

 

Texture2D Background;
Texture2D MidGround;

List<PointLight> pointLightHandles;
List<SpotLight> spotLightHandles;

public LightRenderer lightSystem;

 

These are just the textures we’ll be using to draw, a list to hold each of our types of light (we’ll only be creating spot lights in this example, but you should create both to make sure both shaders are working), and our lighting system itself. Next up we head to the Game1() constructor and add the following code after the code that’s already there:

 

graphics.PreferredBackBufferWidth = 1280;
graphics.PreferredBackBufferHeight = 720;

spotLightHandles = new List<SpotLight>();
pointLightHandles = new List<PointLight>();

lightSystem = new LightRenderer(graphics);

 

All we’re doing is setting the screen resolution (feel free to set it to whatever resolution you prefer), and creating our light lists and lighting system. Next we go to Initialize(). Add this at the top of the method:

 

lightSystem.Initialize();
lightSystem.minLight = 0.5f;
lightSystem.lightBias = 3f;

spotLightHandles.Add(new SpotLight(
    new Vector2(GraphicsDevice.Viewport.Width/2f, GraphicsDevice.Viewport.Height / 2f), 
    Vector2.UnitY * 1.0001f, 0.8f, 1.5f, 1.25f, 500f, Color.Blue));
spotLightHandles.Add(new SpotLight(
    new Vector2((GraphicsDevice.Viewport.Width/2f)+100, 
    (GraphicsDevice.Viewport.Height / 2f) + 100),
    Vector2.UnitY * -1.0001f, 0.8f, 1.5f, 1.25f, 500f, Color.Red));

lightSystem.spotLights.Add(spotLightHandles[0]);
lightSystem.spotLights.Add(spotLightHandles[1]);

 

Here we’re calling initialize() on the lightSystem to set up all the internals of the system, setting a couple of the parameters that we need to specify, creating two spot lights 100 pixels apart, one blue one pointing down, and one red one pointing up. The other parameters we’ve set to values that seem to work. Feel free to play around with them to get different effects.

Next we head to LoadContent() and add the following after the auto-generated code:

 

Background = Content.Load<Texture2D>(@"Textures\Background");
MidGround = Content.Load<Texture2D>(@"Textures\Midground");

lightSystem.LoadContent(Content);

 

Here all we’re doing is loading our two textures, and calling LoadContent() on our lighting system so that it can load the effect files that it needs from the Content project.

Now we get to the meatier stuff. First, the Update() method. In this we add some code to make our spot lights move according to the left and right thumbsticks, and rotate based on the A and B buttons. This code is really just showing how to manipulate spot lights rather than how to interact with our Lighting system. Add the following code before the call to base.Update():

 

if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.X < -0.25f)
{
    spotLightHandles[0].Position.X -= 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[0].Position.X < 0)
        spotLightHandles[0].Position.X = 0;
}
else if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.X > 0.25f)
{
    spotLightHandles[0].Position.X += 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[0].Position.X > GraphicsDevice.Viewport.Width)
        spotLightHandles[0].Position.X = GraphicsDevice.Viewport.Width;
}
else if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.Y > 0.25f)
{
    spotLightHandles[0].Position.Y -= 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[0].Position.Y < 0)
        spotLightHandles[0].Position.Y = 0;
}
else if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.Y < -0.25f)
{
    spotLightHandles[0].Position.Y += 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[0].Position.Y > GraphicsDevice.Viewport.Height)
        spotLightHandles[0].Position.Y = GraphicsDevice.Viewport.Height;
}

if (GamePad.GetState(PlayerIndex.One).Buttons.A == ButtonState.Pressed)
{
    spotLightHandles[0].direction = Vector2.Transform(spotLightHandles[0].direction,
         Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 
             0.0015f * gameTime.ElapsedGameTime.Milliseconds));
}

if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Right.X < -0.25f)
{
    spotLightHandles[1].Position.X -= 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[1].Position.X < 0)
        spotLightHandles[1].Position.X = 0;
}
else if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Right.X > 0.25f)
{
    spotLightHandles[1].Position.X += 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[1].Position.X > GraphicsDevice.Viewport.Width)
        spotLightHandles[1].Position.X = GraphicsDevice.Viewport.Width;
}
else if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Right.Y < -0.25f)
{
    spotLightHandles[1].Position.Y += 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[1].Position.Y > GraphicsDevice.Viewport.Height)
        spotLightHandles[1].Position.Y = GraphicsDevice.Viewport.Height;
}
else if (GamePad.GetState(PlayerIndex.One).ThumbSticks.Right.Y > 0.25f)
{
    spotLightHandles[1].Position.Y -= 0.25f * gameTime.ElapsedGameTime.Milliseconds;
    if (spotLightHandles[1].Position.Y < 0)
        spotLightHandles[1].Position.Y = 0;
}

if (GamePad.GetState(PlayerIndex.One).Buttons.B == ButtonState.Pressed)
{
    spotLightHandles[1].direction = Vector2.Transform(spotLightHandles[1].direction,
        Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 
            0.0015f * gameTime.ElapsedGameTime.Milliseconds));
}

 

This should all look familiar if you’re used to moving objects around in XNA. You might not be completely familiar with Quaternion rotation, but all we do is specify the angle we want to rotate by and the axis that we rotate around.

Finally we get to the part of the API that we designed way back in part 3. Find the Draw method, delete the auto-generated call to GraphicsDevice.Clear() and add the following code to the beginning of the method:

 

lightSystem.BeginDrawBackground();
spriteBatch.Begin();
spriteBatch.Draw(Background, 
    new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), 
    Color.White);
spriteBatch.End();
lightSystem.EndDrawBackground();

lightSystem.BeginDrawShadowCasters();
spriteBatch.Begin();
spriteBatch.Draw(MidGround, 
    new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), 
    Color.White);
spriteBatch.End();
lightSystem.EndDrawShadowCasters();

lightSystem.DrawLitScene();

 

This is basically exactly the API we described back in part 3. If any foreground sprites need to be drawn they should be drawn after the call to DrawLitScene(). And that’s it, we’ve written our last line of code. Now for the moment of truth. Hit F5 or Build & Run and see your light system in action!

The code for the finished solution is at the link below:

 

Finished Solution

 

Optimisations and Improvments

 

So we have our basic light system working. The question is then, now what. There are several optimisations we could make to improve performance if you’re already pushing your PC/ Xbox to it’s limits. For starters, the code could use a tidy up. Because a lot of the system was exposed to game code while I was developing it, many of the fields are public that don’t need to be.

The biggest single performance optimisation we could make involves combining the Unwrap stage with the creation of the occlusion map. We would do this in the following way. At the moment we are creating a full texture with all of rays unwrapped on it and then we draw each row of that texture on top of each other using our special blend state. Instead, we could create those rows from scratch using a special version of our unwrap shader and draw them on top of each other using our blend state. This would save us one draw call per light, and the memory of our large unwrap texture.

There main issue with this is that we need to tell the new version of our unwrap shader which row we are currently drawing. If we do this using a parameter then we would effectively be using a separate draw call for each row of the texture, which would be several orders of magnitude worse than our current method. So we need to find another way to pass this to the shader. The way I was intending to do this was to encode the row number inside the Color parameter for spriteBatch. There are several ways we could do this, but I won’t go into them here. There is plenty of material on the internet about this problem.

You may also need to learn a bit about Vertex Shaders, as when I attempted this technique it didn’t quite work with the normal spriteBatch Vertex shader. I’ll leave it for the reader as an exercise 🙂

 

Other micro-optimisations can be made, such as moving some of the shader parameters out of the Game loop, since we only really need to set them once at the beginning.

 

As for improvements, there are many. The most obvious is to allow the lighting (but not shadows) to affect the shadow casters. This would give the scene a bit more realism, as at the moment the light behaves as though it is on the same level as the shadow caster, but we might want it to appear as though it is slightly closer to the gamer i.e. a bit further ‘out of the screen’. This is not as simple as it sounds, and as far as I can tell would require an extra pass over the scene, or else it would require splitting the pointLight/ spotLight shaders into two steps, one to create the lights as though there were no shadow casters, and the second to black out those pixels that are obscured from the light by shadow casters. Then the non-blacked out map could be used to light the shadow casters themselves.

The second big enhancement would be to adapt the pointLight and spotLight to use normal maps, making it seem like the background (and shadow casters) have depth and texture. There’s lots of material out there on normal mapping, and I don’t think this should be too difficult.

These are just a few suggestions for optimisation and improvements. Hopefully this series has started to give you the tools you need to start reading more on lighting techniques, and, more importantly, the confidence to start experimenting with shaders to create your own effects.

And if you come up with something cool, then share it with the community! We all learn from each other and we’re all better developers for it in the end.

 

That’s it for the series. I really hope you’ve enjoyed working through these tutorials, and that you’ve maybe learnt something along the way. I’d love to hear about projects people use the system in, or about your own systems that you’ve built based on what you’ve learnt here.

12 Responses to “2D Lighting System tutorial series: Part 9 – Conclusion: Game code & Optimisations for the future”

  1. DarkPrisma

    Hi!

    very, very nice work. Its exactly that, what I need. But I have problem with the lights. On some positions, the light get cuttet: http://217.160.176.162/lightBug.png

    I have taken the 5th example to reproduce it. Is still in the 9th.
    http://217.160.176.162/2DLightingSystemP5.zip

    How I can fix that? And is it possible to use a texture as light instead of that calculated pointlight? a texture like: http://217.160.176.162/SpriteFile00002.png

  2. trwoodward

    Hi, I’m glad you’re finding the series useful!

    I ran into a lot of odd bugs while developing the technique/ tutorials, so I’ll have a look at your repo over the weekend and see if I can spot where it’s going wrong.

    On whether you can use a texture rather than calculating a light, the answer is yes and no. Using a texture with the LightBlend shader from part 2 on a scene will make it look ‘lit’, and will work fine if you don’t need dynamic shadows (e.g. you just want to turn a light on or off in your scene and don’t want moving objects to cast shadows).

    If you want moving shadows though, the light maps (i.e. the texture you use at the end) have to be recalculated every frame, so using a pre-prepared texture wouldn’t work there.

    I’ll comment back here when I’ve had a chance to look at the your repo project 🙂

  3. trwoodward

    Ok, this was due to a bug that crept into the Pointlight and Unwrap shaders while I was refactoring.

    To fix this, in the PointLight shader, in the line “float dist = (screenCoords.x < LightPos.x) ? .... ;" change the < for a >. Then, in the unwrap shader, in the return statement, swap the references to sample1 and sample2 with each other.

    Let me know if that works for you! If it does I’ll update the tutorials accordingly.

  4. DarkPrisma

    wonderful, realy wonderful. I had searched so long time for one simple and worked example. I could cry, so happy I’m 😀

    thank u very much!

  5. Jean

    Hi,
    First, thank you very very much for this tutorial. I really can imagine how much work it was to, first, make the code work and easy to understand, and second, to write this tutorial. I think the whole technical level was just perfect and easy to understand !

    I’m working in VB.Net and I’ve “translated” everything and it works fine. However, I can’t make PointLights to work, either in my code (VB) or in your solution (in case I made a typo or a translation error). Nothing is showing on the screen… The PointLight I created is pos = (10,10), power=0.5 and radius = 50 . The unwraptarget gets generated (The image i get is a deformed midground image in green and red, taking the left half of the image), but the occlusion map is all black… I can’t figure what’s going wrong… Any insight ???

    Thank you very much !

  6. Jean

    Oups… forgot to mention that color = color.white…

    Thank you again !

  7. trwoodward

    Hi! Thanks for reading, and it’s awesome that you’re porting it to VB!

    There are a few bugs in the point light code – there’s a fix in the comments of one of the installments (I think week 5?) but I haven’t gotten around to fixing the code in the tutorials or the example projects – I’m on holiday at the moment, so I’ll try and get it done in the next couple of days.

    Thanks again!

    Travis

  8. Jean

    Hi Travis,
    I really I’ll need your help on this one when you’ll have some time. I’ve went through the comments as you proposed but didn’t find anything on how to fix the point light problem. However, in the meantime, I’ll try to look deeper in the code and debug it to see if I can find the problem.

    So, if in the meantime you can just point me out the exact comment you’re refering to, I’d be glad.

    Thank you for your help, and hope you have great holidays !

    Jean

  9. trwoodward

    Hi Jean,

    I’ve tested creating a pointlight with the properties you’ve given, and there are two points – first as I mentioned there is a bug in the pointlight shader, and also one in the unwrap shader. The fixes are actually in the comments on this post – see my comment of 9th December 2012.

    The second point is that, if you’re using the textures from the example solution, then (10, 10) will put your pointlight inside the wall that runs around the edge of the image – so the light system will think you’re inside a wall and so all light from the point light will be blocked. Try placing the pointlight in the middle of the screen (GraphicsDevice.Viewport.Width / 2f, GraphicsDevice.Viewport.Height / 2f) and see if the light appears then.

    Let me know if that helps!

    Travis

  10. JJ

    Hey,

    I just wanted to say thanks a bunch for this tutorial, it’s the most detailed and easy to follow tutorial on the subject that I’ve found.

    Now to just figure out how to make the light affect the casters too..

  11. JJ

    Hi again,

    Do you have any ideas on how to make the shadow casters themselves be affected by the other shadows, but not their own? Can’t get my head around this 🙂 Render the lightmap with only the shadows, and not the caster, and use that to blend perhaps?

  12. trwoodward

    Hi,

    Glad you like the series, and apologies for the delay in replying. I take it you mean you want a sprite (say, a lightbulb) that isn’t effected by it’s own light, but still casts a shadow from other lights?

    One way would be to split the creation of the shadow casters render target into two steps:

    1) Create a shadow casters render target with all shadow casters EXCEPT for your light source sprites (e.g. your lightbulb sprite), and then
    2) Have a second render target which you clear, and then before you create the light map for each light render the shadow casters texture from (1) onto it, and then render all of your light source sprites EXCEPT the light source sprite for the current light onto it – then use the new shadow caster texture instead of the normal shadow caster texture in your light calculations.

    That way each light can ‘see’ the other light source sprites, but not it’s own.

    Hope that helps!

Leave a Reply


× nine = 81

Proudly powered by WordPress. Theme developed with WordPress Theme Generator.
Copyright © Rabid Lion Games. All rights reserved.