I recently attempted this very thing and the question and answer here got me through it - How can I acheive a smooth 2D lighting effect?.
I'll explain it for you though:
What this method does is instead of 'tinting' your tiles directly, it draws that 'tinting' color to a new texture (called the LightMap), scaled down to 1 pixel for each tile. Then you draw all the tiles normally without any color tinting at all. After that you draw the LightMap to the screen and blend the LightMap colors with the tile colors. By scaling the LightMap up to the screen size before drawing, XNA will handle interpolating the lighting values for you.
Let's begin:
According to the answer linked above you will need a custom BlendState
so put one somewhere in your lighting engine and initialize it like this:
BlendStateMultiply = new BlendState()
{
AlphaSourceBlend = Blend.DestinationAlpha,
AlphaDestinationBlend = Blend.Zero,
AlphaBlendFunction = BlendFunction.Add,
ColorSourceBlend = Blend.DestinationColor,
ColorDestinationBlend = Blend.Zero,
ColorBlendFunction = BlendFunction.Add
};
You are going to use this BlendState
to draw your RenderTarget2D
with the lighting values onto the scene and have them light the tiles appropriately.
So first thing first you are going to need to do is draw your lighting values to the RenderTarget2D
. Place a RenderTarget2D
somewhere in your lighting engine and then add the following to the beginning of your Draw()
method:
spriteBatch.GraphicsDevice.SetRenderTarget(renderTarget);
spriteBatch.GraphicsDevice.Clear(Color.Transparent); //or Color.White.
spriteBatch.Begin();
EDIT: --------------------------------
To figure out the size of the RenderTarget2D you need to examine how you are drawing your tiles. If you are drawing your entire map of tiles then it is simply the amount of tiles going horizontally as the width, and vertically as the height. If you are being clever and using a 2D camera and culling tiles that are not on the screen then you need to work around that.
For example if your rendering looks something like this:
for(int x = tileStartX; x < tileEndX; x++)
{
for(int y = tileStartY; y < tileEndY; y++)
{
spriteBatch.Draw(tiles[x][y].texture, new Vector2(x, y), Color.White);
}
}
Where tileStartX
& tileStartY
is the top left most tile that should be drawn and tileEndX
& tileEndY
is the bottom right most tile, then you should initialize the RenderTarget2D
to the distance between tileStartX
& tileEndX
and between tileStartY
& tileEndY
. This is generally your screenSize plus a single tile buffer. So for example with a screen size of 1280x720 and a tile size of 32 you would end up with 42 tiles for the width (1280 / 32 = 40 + 1 tile buffer on each side) and 25 for the height (720 / 32 = 22.5 rounded up to 23 + a 1 tile buffer on each side) which are the dimensions for the RenderTarget2D
.
End Edit: ---------------------------
Now you need a Texture2D
that is a single, white, pixel so go ahead and make one in whatever photo editing software you've got and add it to your project. Then you are going to use this texture to draw your lighting colors to the RenderTarget2D
like so:
for(int x = 0; x < tiles.count; x++)
{
for(int y = 0; y < tiles[x].count; y++)
{
spriteBatch.Draw(whitePixelTexture, new Vector2(x, y), tiles[x][y].tintingcolor);
}
}
I'm assuming all your tiles are in a 2D list or array. Basically you need to loop through all the tiles you want to draw and then draw their tinting color value with the single pixel texture. The end result is that the RenderTarget2D
is now a LightMap, containing all the Color
values you want to blend over your tiles.
Now you need to switch back to drawing to the screen so add this:
spriteBatch.End();
spriteBatch.GraphicsDevice.SetRenderTarget(null);
spriteBatch.Begin();
Right after that line is where you need to draw your tiles, but make sure that instead of drawing them with the 'tinting' color you just draw them with Color.White
. Also make sure to spriteBatch.End();
after drawing your tiles.
There's one final thing to do in order to light your tiles and that is to draw the LightMap to the screen. When you do this you need to stretch it to the correct width/height. You can find this value by multiplying the size of the texture by the size of your tiles. The complete example at the bottom will explain better.
Before drawing your LightMap you need to set a few things in the spriteBatch.Begin()
method. The first is the SpriteSortMode
(which doesn't matter at this point) and the second is the BlendState
which is where you will use the custom BlendState we created earlier.
spriteBatch.Begin(SpriteSortMode.Deferred, BlendStateMultiply);
spriteBatch.Draw(renderTarget, new Rectangle(0, 0, renderTarget.Width * tileWidth, renderTarget.Height * tileHeight, Color.White);
spriteBatch.End();
If all was done correctly then you should have a properly lit scene with a mild amount of interpolation between the lighting on your tiles. Though this effect may not be exactly what you desired, it is definitely a good step towards learning about lighting and finding your own method later on.
Here is the complete Draw() method in your lighting engine.
public void Draw(SpriteBatch spriteBatch)
{
//Swap drawing to target the RenderTarget2D and Begin()
spriteBatch.GraphicsDevice.SetRenderTarget(renderTarget);
spriteBatch.GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin();
//Draw the lighting values to the RenderTarget2D
//This loop needs to be the exact same one you use to Draw your tiles later.
for(int x = 0; x < tiles.count; x++)
{
for(int y = 0; y < tiles[x].count; y++)
{
spriteBatch.Draw(whitePixelTexture, new Vector2(x, y), tiles[x][y].tintingColor);
}
}
//End the current batch and switch to drawing to the screen
spriteBatch.End();
spriteBatch.GraphicsDevice.SetRenderTarget(null);
spriteBatch.Begin();
//Draw the tiles normally, with a Color.White parameter
for(int x = 0; x < tiles.count; x++)
{
for(int y = 0; y < tiles[x].count; y++)
{
spriteBatch.Draw(tiles[x][y].texture, new Vector2(x * tileWidth, y * tileHeight), Color.White);
}
}
//End and Begin the spriteBatch with our custom BlendState
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred, Multiply);
//Draw the RenderTarget2D (LightMap) over the scene, stretching to the correct values..
//..where tileWidth is the width of your tiles and tileHeight the height.
spriteBatch.Draw(renderTarget, new Rectangle(0, 0, renderTarget.Width * tileWidth, renderTarget.Height * tileHeight), Color.White);
spriteBatch.End();
}