Dungeon Illumination

I’m a big fan of Phillip Reed and have backed over a dozen of his Kickstarter projects. (He currently has 76!) He writes a lot of system-less supplements for RPG gamemasters. They’re primarily about adding ‘flair’ to a campaign and are fun to read.

His recent project, Dungeon Illumination, made me think about Fancy lighting for texture and effect. I missed the window to pledge but I’ll pick it up when it comes to DriveThuRPG. How a dungeon is illuminated sounds trivial but I put an enormous amount of effort into getting the lighting right because it really enhanced the aesthetic of the game. For the moment I’ve only implemented torches. At some point perhaps I’ll get inspired to add other types of light sources with different colors and properties.

Fancy lighting for texture and effect

When I put together tiles as I started building the game, it was very exciting. Initially there were only a few (stone walls, stone floors, torches and orcs), but it was amazing to see them come together on the screen. After the initial euphoria faded, however, I was left feeling that the experience was a bit flat. I randomly rotated the tiles to create a little variety in the layouts, but the lighting left things feeling a little too uniform. The solution was to add some variation in intensity with a little flickers to simulate illumination from a torch.

How is it done? Every tile is an object with a local illumination variable which range from 0 (pitch dark) to 100 (maximum light). Every source of light, such as a torch, will then increment the values of the tiles within its range (lumensRange) by an amount relative to the inverse of the square of the distance to that tile, as stipulated by the Inverse Square Law. To make things feel a little more dynamic and “real,” I added a random amount of illumination (randomLumens) to simulate a flicker. This effect is primarily visible near the edge of the illumination radius.

for (int x = -lumensRange; x <= lumensRange; x++) {
	for (int y = -lumensRange; y <= lumensRange; y++) {
		int randomLumens = (flickerAmt != 0) ? rand.nextInt(flickerAmt) : 0;
		if (x == 0 && y == 0) lumensFlickerCache[x + lumensRange][y + lumensRange] = (lumens + randomLumens) / 4;
		else lumensFlickerCache[x + lumensRange][y + lumensRange] = (lumens + randomLumens) / 4 / ((x*x) + (y*y));
	}
}

To save compute, as well as control the frequency of the flicker, these values are not computed at every update, hence the lumensFlickerCache.

The next step is, of course, is to adjust the color effects (brightness, hue, contrast, saturation) of the tiles as a function of the level of illumination. As is frequently the case, someone on Stack Overflow put together a class with a collection of methods to accomplish all of this. To get the desired effect, I experimented with adjusting all of the effects as a function of the level of illumination, but in the end settled on brightness and contrast. As it turned out, adjusting the brightness was not sufficient to dim the tiles with low illumination.

// method from ColorFilterGenerator class
public static ColorFilter adjustColor(int brightness, int contrast, int saturation, int hue){
    cm.reset();
    adjustHue(cm, hue);
    adjustContrast(cm, contrast);
    adjustBrightness(cm, brightness);
    adjustSaturation(cm, saturation);
    return new ColorMatrixColorFilter(cm);
}

// method that adjusts the tile's color filter during rendering
public OverlayImage(Context context, Entities e) {
    super(context);
    entity = e;
    if (!entity.equals(hero)) { // normal brightness for hero
        int brightness = map.getBrightness(entity.getPos());
        p.setColorFilter(ColorFilterGenerator.adjustColor(brightness, brightness, 0, 0));
    }
}

Here is a short video from the early days of development. You can see the illumination radius and, with careful attention, should be able to see the flicker at the edges. It’s worth noting that I prioritized doing this over building the mechanics of the game, such as combat. At the end of the video you can notice that the orc does not hit back.

While this may seem rudimentary, it’s actually an aspect of the game for which I am the most proud. Not only do I love the way it looks, and the feeling it gives to the experience, but it actually took a lot of time and experimentation to get it to the point where it worked and looked right.