Friday, December 19, 2014

2D Tile Based Lighting

A demonstration of lighting in the game.

Recently I added lighting to the game and wanted to share a simplified overview of how it was done. Lighting is all done using the CPU and is done on a tile by tile location basis, meaning that the lighting has a certain blocky resolution to it. In the future I may look into adding shader support for lighting, but for now I'll use this until I have time to research into it more.

Lighting is implemented in the game using two variables, the base/ambient light value, and the light value of each unit on the screen. Lighting is implemented in a very fast and simple matter. First, a 2D array is made representing the visible area on the screen (with a resolution the size of each tile). The program loops through every visible unit on the screen, calculates the light emitted from that unit (based on distance from the unit and the unit's light intensity), and if the light calculated is higher than the ambient light, it updates the tile with that higher light value.

Going through the algorithm using pseudo-code,

 LightMap light_map = array[screen_tiles_x][screen_tiles_y];  
 light_map.fill(ambient_light_value);  
 For (unit : visible_units) { // Calculate lighting values for all visible sprites  
         for (coordinate : light_map) { // Go through each visible coordinate on the screen and calculate the lighting based on the distance from the unit  
                 double distance = coordinate - unit.position;  
                 double new_light_value = calculateLightValue(distance);  
                 if (new_light_value > coordinate.light_value) { // Update the light map for any coordinate that has a higher calculated brightness  
                         coordinate.light_value = new_light_value;  
                 }  
         }  
 }  

Then, when drawing all the sprites on the screen (both units and tiles), we adjust the darkness of each sprite based on the light map previous calculated,

 brightness = 255 * light_map[unit_x_position][unit_y_position]; // light_map values range from 0.0 for completely dark, to 1.0 for completely lit  
 color_filter = color(brightness, brightness, brightness);  
 sprite.setColor(color_filter);  

What happens is that for tile locations that are fully lit, the color is unmodified, while for tile locations with a low brightness value, the color is darkened.

An issue with with this lighting method is the limited resolution in brightness, causing this blocky gradient in lighting to occur.

An issue with this method of lighting is that the change in lighting can look very blocky, as you can see a discrete change in lighting from one tile location to the next. Another issue is that this method doesn't support colored lighting, although it should be possible if the lightmap stores color values instead of single floating point values at each location. Of course, this comes at a performance cost.

I've looked up alternative lighting methods, and while I haven't listed them, this is by far the simplest and lowest cost lighting method I could implement. The penalty on performance is negligible due to a few optimizations and the effect still looks relatively good. One thing I need to do now though is implement support for lighting that is blocked by walls. I can do this using collision detection, so that a straight line is drawn at each location calculated to see if a collision with a wall occurs, and only updating the light map at locations where no collisions occur.

Originally this algorithm was designed with parallelization in mind, since it appears to be a good candidate for it. However, to do this requires either mutexes (or atomic variables) on the light map, killing any performance gains from parallelization, or to use seperate light maps that are combined later, which also kills performance due to the heavy memory usage required.

Some optimizations used for calculating lighting include calculating the maximum distance before the light value of the unit goes below ambient light values, and only calculating light values for coordinates within that range. Additionally, if the unit has a light value at or below 0.0, it automatically excludes calculating for that unit.

If anyone has suggestions for a better way to implement the lighting for this game, please send me a message. Currently the lighting has roughly a 13% impact on the performance (frames/second) of the game at 1000 lit units while still appearing respectable, so for now I'm content with it. Time to move on to the user interface!

Note: Special thanks to http://codeformatter.blogspot.com/ for providing a great tool to format the above code.

No comments: