Raytracing Lighting on a 2D Tilemap

Tilemap lighting tutorial

Another great use of Raytracing is lighting. This subject can get pretty complex and is an accomplished discipline in its own right, but we're going to be starting with the basics, so anyone with a limited programming knowledge should be comfortable engaging in this lesson.

View example

We'll be looking at how to add light sources and per-tile lighting on a tilemap. First, let's look at our Light object - we'll create our lights with four initial values: the x, y coordinates of the light on the map, the radius, which is the distance light spreads from this light source, and the lights intensity:

function Light(x, y, radius, intensity)
{
	this.position		= [x,y];
	this.radius		= radius;
	this.intensity		= intensity;
	this.area		= {};
}

Our lighting will be used to determine the opacity at which we draw each tile. A value of 0 is completely invisible (so we'll just show the black background), and 1 is completely visible, at full brightness.

A higher value than 1 is fine, but nothing will be drawn brighter than full opacity (1). A higher value can let the brightness of the light spread further before dimming.

The lights also have a area attribute, which we'll look at next. The Light object has its own method, which we'll call calculate. When this method is executed, we'll begin by resetting the area attribute to an empty object (this object will be used as a HashMap or associative array, depending on which languages you're familiar with).

Light.prototype.calculate = function()
{
	this.area = {};

Now a call is made to Bresenhams circle algorithm to find all of the furthest points from the light source's centre:

	var edges = pointsOnCircumference(this.position[0], this.position[1],
		this.radius);

...and we'll loop through all of the points that are returned and get the points to make a line using Bresenhams line algorithm from the lights position to the current point in the edges list.

	for(var e in edges)
	{
		var line = bresenhamsLine(this.position[0], this.position[1],
			edges[e][0], edges[e][1]);

To find how intense the lighting will be at each step along the line, we'll divide the lights intensity by the number of steps along the line:

		var mult = this.intensity / line.length;

...and then step through the points in the current line. If the current point in the current line falls outside of the maps bounds, we'll break from the loop as there's no point in calculating the rest of the line.

		for(var l in line)
		{
			if(line[0] < 0 || line[0]>=mapW ||
				line[1] < 0 || line[1]>=mapH) { break; }

We'll create a temporary variable, idx, that converts the x, y position of the current point on this line to its coordinate in the mapData array.

			var idx = ((line[l][1]*mapW)+line[l][0]);

The light intensity at this point is equivalent to [total intensity] - ( ([total intensity] / [line length]) x [distance from centre]). As we've already calculated our mult variable, we can get the strength at the current line step like this:

			var strength = mult * (line.length - l);

Now, we check that the current line point is either not currently illuminated by this light, or the light intensity is less than the strength we've currently calculated for this point.

If either of these conditions is true, we set the area entry for this points mapData index (idx) to the value of strength, or 1 if strength is greater than 1.

			if(!(idx in this.area) || this.area[idx]<strength)
			{
				this.area[idx] = (strength > 1 ? 1 : strength);
			}

Additionaly, if the current tile is solid and light cannot pass through it (a value of 0 in our example mapData, we do not continue any further along this line by breaking from the loop. The solid tile is illuminated, but light has not passed through it.

We can also go ahead and close the loops and the method, as we're done for this light!

			if(mapData[idx]==0) { break; }
		}
	}
};
Page loaded in 0.01 second(s).