Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficient 2D Tile based lighting system

What is the most efficient way to do lighting for a tile based engine in Java?
Would it be putting a black background behind the tiles and changing the tiles' alpha?
Or putting a black foreground and changing alpha of that? Or anything else?

This is an example of the kind of lighting I want:
http://i.stack.imgur.com/F5Lzo.png

like image 999
Kyranstar Avatar asked Aug 30 '13 04:08

Kyranstar


2 Answers

There are many ways to achieve this. Take some time before making your final decision. I will briefly sum up some techiques you could choose to use and provide some code in the end.


Hard Lighting

If you want to create a hard-edge lighting effect (like your example image), some approaches come to my mind:

hard light example

Quick and dirty (as you suggested)

  • Use a black background
  • Set the tiles' alpha values according to their darkness value

A problem is, that you can neither make a tile brighter than it was before (highlights) nor change the color of the light. Both of these are aspects which usually make lighting in games look good.

A second set of tiles

  • Use a second set of (black/colored) tiles
  • Lay these over the main tiles
  • Set the new tiles' alpha value depending on how strong the new color should be there.

This approach has the same effect as the first one with the advantage, that you now may color the overlay tile in another color than black, which allows for both colored lights and doing highlights.

Example: hard light with black tiles example

Even though it is easy, a problem is, that this is indeed a very inefficent way. (Two rendered tiles per tile, constant recoloring, many render operations etc.)


More Efficient Approaches (Hard and/or Soft Lighting)

When looking at your example, I imagine the light always comes from a specific source tile (character, torch, etc.)

  • For every type of light (big torch, small torch, character lighting) you create an image that represents the specific lighting behaviour relative to the source tile (light mask). Maybe something like this for a torch (white being alpha):

centered light mask

  • For every tile which is a light source, you render this image at the position of the source as an overlay.
  • To add a bit of light color, you can use e.g. 10% opaque orange instead of full alpha.

Results

image mask hard light result

Adding soft light

Soft light is no big deal now, just use more detail in light mask compared to the tiles. By using only 15% alpha in the usually black region you can add a low sight effect when a tile is not lit:

soft light

You may even easily achieve more complex lighting forms (cones etc.) just by changing the mask image.

Multiple light sources

When combining multiple light sources, this approach leads to a problem: Drawing two masks, which intersect each other, might cancel themselves out:

mask cancellation

What we want to have is that they add their lights instead of subtracting them. Avoiding the problem:

  • Invert all light masks (with alpha being dark areas, opaque being light ones)
  • Render all these light masks into a temporary image which has the same dimensions as the viewport
  • Invert and render the new image (as if it was the only light mask) over the whole scenery.

This would result in something similar to this: result image

Code for the mask invert method

Assuming you render all the tiles in a BufferedImage first, I'll provide some guidance code which resembles the last shown method (only grayscale support).

Multiple light masks for e.g. a torch and a player can be combined like this:

public BufferedImage combineMasks(BufferedImage[] images)
{
    // create the new image, canvas size is the max. of all image sizes
    int w, h;

    for (BufferedImage img : images)
    {
        w = img.getWidth() > w ? img.getWidth() : w;
        h = img.getHeight() > h ? img.getHeight() : h;
    }

    BufferedImage combined = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);

    // paint all images, preserving the alpha channels
    Graphics g = combined.getGraphics();

    for (BufferedImage img : images)
        g.drawImage(img, 0, 0, null);

    return combined;
}

The final mask is created and applied with this method:

public void applyGrayscaleMaskToAlpha(BufferedImage image, BufferedImage mask)
{
    int width = image.getWidth();
    int height = image.getHeight();

    int[] imagePixels = image.getRGB(0, 0, width, height, null, 0, width);
    int[] maskPixels = mask.getRGB(0, 0, width, height, null, 0, width);

    for (int i = 0; i < imagePixels.length; i++)
    {
        int color = imagePixels[i] & 0x00ffffff; // Mask preexisting alpha

        // get alpha from color int
        // be careful, an alpha mask works the other way round, so we have to subtract this from 255
        int alpha = (maskPixels[i] >> 24) & 0xff;
        imagePixels[i] = color | alpha;
    }

    image.setRGB(0, 0, width, height, imagePixels, 0, width);
}

As noted, this is a primitive example. Implementing color blending might be a bit more work.

like image 79
bricklore Avatar answered Sep 21 '22 23:09

bricklore


I've been into indie game development for about three years right now. The way I would do this is first of all by using OpenGL so you can get all the benefits of the graphical computing power of the GPU (hopefully you are already doing that). Suppose we start off with all tiles in a VBO, entirely lit. Now, there are several options of achieving what you want. Depending on how complex your lighting system is, you can choose a different approach.

  • If your light is going to be circular around the player, no matter the fact if obstacles would block the light in real life, you could choose for a lighting algorithm implemented in the vertex shader. In the vertex shader, you could compute the distance of the vertex to the player and apply some function that defines how bright things should be in function of the computed distance. Do not use alpha, but just multiply the color of the texture/tile by the lighting value.

  • If you want to use a custom lightmap (which is more likely), I would suggest to add an extra vertex attribute that specifies the brightness of the tile. Update the VBO if needed. Same approach goes here: multiply the pixel of the texture by the light value. If you are filling light recursively with the player position as starting point, then you would update the VBO every time the player moves.

  • If your lightmap depends on where the sunlight hits your level, you could combine two sort of lighting techniques. Create one vertex attribute for the sun brightness and another vertex attribute for the light emitted by light points (like a torch held by the player). Now you can combine those two values in the vertex shader. Suppose the your sun comes up and goes down like the day and night pattern. Let's say the sun brightness is sun, which is a value between 0 and 1. This value can be passed to the vertex shader as a uniform. The vertex attribute that represents the sun brightness is s and the one for light, emitted by light points is l. Then you could compute the total light for that tile like this:

    tileBrightness = max(s * sun, l + flicker);
    

    Where flicker (also a vertex shader uniform) is some kind of waving function that represents the little variants in the brightness of your light points.
    This approach makes the scene dynamic without having to recreate continuously VBO's. I implemented this approach in a proof-of-concept project. It works great. You can check out what it looks like here: http://www.youtube.com/watch?v=jTcNitp_IIo. Note how the torchlight is flickering at 0:40 in the video. That is done by what I explained here.

like image 23
Martijn Courteaux Avatar answered Sep 18 '22 23:09

Martijn Courteaux