3
\$\begingroup\$

I am currently working on a 2d platformer and have the general idea of how to apply 'lighting' to my level, but I am not sure at all how to make it smooth, similar to Terraria, as you can see here. enter image description here

I apply a tint to my tiles based on how many lights or open tiles there are nearby. This tint varies from Color.White to a gray to Color.Black.enter image description here

My question is how to 'smoothen' this out and if there is generally a better method to do this. I'm not entirely a fan of shaders at the moment, as I am still learning about them.

Thanks!

\$\endgroup\$

3 Answers 3

2
\$\begingroup\$

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();
}
\$\endgroup\$
2
  • \$\begingroup\$ Quick question- how large would the initial rendertarget be then? \$\endgroup\$ Commented Jan 14, 2015 at 2:56
  • \$\begingroup\$ @TheUnrealMegashark Edited my answer with information on your question. Basically it depends on how your rendering and the important thing is to have the same width/height for the rendertarget as tiles you are drawing on the scene. \$\endgroup\$
    – Saliken
    Commented Jan 14, 2015 at 4:34
1
\$\begingroup\$

Okay, you've done the right thing. To get smooth lighting, all that's necessary is to have a separate brightness for each tile vertex rather than each tile. Then, render the tiles using a series of textured quads. The brightness of each vertex is just the average of the brightness of its neighboring tiles.

However, you are using XNA, so in order to do this, you will have to make use of a custom shader passed in to your spritebatch so that it can use vertex coloring. It is not too hard to do this. However, if you want it to have good performance, you will need to abandon spritebatch and draw textured quads manually.

\$\endgroup\$
2
  • \$\begingroup\$ vertex coloring isn't too bad an idea to avoid using a 2nd texture. It can be faster on some GPUs. But there's issues with interpolation so tiles will have to be made of 4 triangles / 5 vertices (like a club-sandwich cut) to look nice. If the GPU is capable of render-to-texture and we want dynamic lighting then a lighting texture is the way to go. \$\endgroup\$ Commented Jan 13, 2015 at 6:46
  • \$\begingroup\$ Vertex lighting is good enough if you are fine with angled linear gradients. The caveat is that you have to flip the triangles in some of the quads for the lighting to look correct. See this article, in the section Details Regarding Meshing for an explanation. \$\endgroup\$
    – ChrisC
    Commented Jan 13, 2015 at 23:32
1
\$\begingroup\$

You draw the lighting values into a small gray scale (or color) texture with 1 texel per map tile, enable interpolation, then sample this interpolated texture to tint your tiles.

This will smooth out the lighting.

Note: doing per-vertex interpolation rather than texture sampling will cause to interpolate over the 2 triangles that makes up the quad and you'll see the 2 triangle gradient split for the tile like on the left cube:

enter image description here

\$\endgroup\$
3
  • \$\begingroup\$ Hmm, that actually makes more sense than my solution! \$\endgroup\$
    – mklingen
    Commented Jan 13, 2015 at 6:38
  • \$\begingroup\$ I'm not sure how to go about "enabling interpolation" or sampling the texture to light my tiles. Is there a little more information you could provide me with to point me in the right direction? \$\endgroup\$ Commented Jan 13, 2015 at 7:11
  • 1
    \$\begingroup\$ You need to use multitexturing (2 textures) where both textures multiply one another to give the final color (color = tex_tile * tex_light). google "XNA multi texturing" for tutorials, here's one tutorial (iloveshaders.blogspot.ca/2011/04/multiple-textures.html) that is a bit more complex than what you need but it has the info. \$\endgroup\$ Commented Jan 13, 2015 at 7:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .