top of page
Kyle and Rich

Lighting Much Dungeon


This post is going to take a deep dive into how we handle lighting in Much Dungeon. Just like our posts on world generation, there are many ways to solve this problem. This is the solution we found that gave us results we liked. If you haven’t checked out the generation posts, you may want to do that first to have some background on what elements go into our worlds.

First off, we define our light sources as a color, max tile range, and brightness value. Based on these values, we can determine what color would be added to a given tile based on the distance from a light source.

We split our lighting into a static and dynamic layer that is then merged at render time. This lets us skip some recalculations for light sources that don’t change their light. Also worth noting, we’re handling the calculation of lights on the cpu and storing light values per tile. Much Dungeon’s world is a flat plane. So, gpu based lighting that took our ‘wall’ tiles into account didn’t make sense to try and pull off.

What is a Light?

We’ve limited what game elements can emit light to what we call ‘decorators’ and ‘ephemera’. A ‘decorator’ is a non-instanced graphic in a specific tile, and an ‘ephemera’ is an instanced graphic at a world position. Decorators are generally much more common, these make up doors, chests, pools of water, etc. Ephemera handle projectiles, floating popup icons, and any other temporary graphic.

Lighting storage is separate from decorator or ephemera storage, but both types of elements are queried when calculating the light values. Unlike our other storage, which is stored in 64x64 tile chunks, we operate on lights at the sub chunk level (8x8 tiles). This allows for denser light sources in the world without having to recalculate hundreds of elements to determine the lighting within a small area.

Static Lighting

Static lights are just that, static. These light sources only need to ever be calculated once and have values that never change. They still need to know what tiles are visible and how far from the light those tiles are, but the light’s color, max range, and intensity don’t change from cycle to cycle.

Examples of a static light would be some unchanging glow around an object or a fixed spot light.

Static Lighting

Dynamic Lighting

Dynamic lights are much more interesting. These lights are actually two different light sources that we blend between. Using a transition type and a duration time, we can determine how much of light source A and how much of light source B should be in the resulting light.

A ‘smooth’ transition is our most used type. This just finds the light source between A and B that is equivalent to how far through the transition we are based on duration. Also, if we’re over the transition duration, we smoothly move back down from B to A.

Examples of a dynamic light would be any pulsing, changing, or flickering light. This is almost every light in the game. Torches, fires, magic effects, etc, etc.

Dynamic Lighting

Putting it Together

The process for both light source types is similar: For a given source, find what tiles are visible from its position, find the distance from source to target tile, and then calculate and store the light that would reach that tile. This process is repeated for all nearby light sources, combining the overall light at a given tile. To find the light for any given tile, the light sub chunk it’s contained in must have been calculated as well as all adjacent light sub chunks.

The visible tile set for each light source is retained to save some time, only being recalculated if a new light is added or if the world has changed (fully destructible environment). The calculated per tile light is kept as well until a recalculation is called.

Stuff That Didn’t Work

At first we didn’t really like the end result of the light blending. We had used an alpha blending style of painter’s algorithm (wiki). This ended up making the far edges of where a light could reach darker than if the light wasn’t there at all. This was really a color blend instead of a light addition. That looked something like this:

float endRatio = (other.a + this.a * (1- other.a));

this.r = other.r * other.a + this.r * this.a * (1 - other.r) / endRatio;

this.g = other.g * other.a + this.g * this.a * (1 - other.g) / endRatio;

this.b = other.b * other.a + this.b * this.a * (1 - other.b) / endRatio;

this.a = a + other.a * (1 - this.a));

So, instead we changed to something that was really adding light rather than trying to do some form of blending that could end up darker than the original. The final version looks something like this:

float mult = (1 - this.a) * other.a;

this.r = this.r + other.r * mult;

this.g = this.g + other.g * mult;

this.b = this.b + other.b * mult;

this.a = this.a + other.a * mult;

Performance was an issue at first as well. The first version used full chunk storage (64x64 tiles) for lights, tried to update dynamic lights every cycle, and didn’t share much info between lights. We moved to the sub chunk (8x8 tiles) storage to reduce the total numbers of lights that could update at once, changed dynamic lights to only update every few cycles, and changed light storage to re-use and share much more information. All of this ended up giving us a more playable experience.

Hope you enjoyed the post - Check back for more updates

Love Always,

Much Software

68 views0 comments

Recent Posts

See All
bottom of page