Raytracing Lighting on a 2D Tilemap

Example source code


<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
var ctx = null;
var tileW = 40, tileH = 40;
var mapW = 20, mapH = 10;

var mapData = [
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0,
	0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0,
	0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
	0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0,
	0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
	0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
	0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0,
	0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0,
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var lightMap = null;
var allLights = [];

// The minimum lighting for the map
var baseLighting = 0.1;

function Light(x, y, radius, intensity)
{
	this.position		= [x,y];
	this.radius			= radius;
	this.intensity		= intensity;
	this.area			= {};
}
Light.prototype.calculate = function()
{
	this.area = {};
	var edges = pointsOnCircumference(this.position[0], this.position[1],
		this.radius);
	
	for(var e in edges)
	{
		var line = bresenhamsLine(this.position[0], this.position[1],
			edges[e][0], edges[e][1]);
		var mult = this.intensity / line.length;
		
		for(var l in line)
		{
			if(line[0] < 0 || line[0]>=mapW ||
				line[1] < 0 || line[1]>=mapH) { break; }
				
			var idx = ((line[l][1]*mapW)+line[l][0]);
			var strength = mult * (line.length - l);
			
			if(!(idx in this.area) || this.area[idx]<strength)
			{
				this.area[idx] = (strength > 1 ? 1 : strength);
			}
			
			if(mapData[idx]==0) { break; }
		}
	}
};

function rebuildLightMap()
{
	lightMap = new Array();
	for(var i = 0; i < (mapW*mapH); i++) { lightMap[i] = baseLighting; }
	
	for(var l in allLights)
	{
		for(var a in allLights[l].area)
		{
			if(lightMap[a] < allLights[l].area[a])
			{
				lightMap[a] = allLights[l].area[a];
			}
		}
	}
}

function resetLights()
{
	// Remove existing lights
	allLights.length = 0;
	
	// Create 5 random lights
	while(allLights.length < 5)
	{
		// Create random light attributes
		var intensity = 0.3+Math.random();
		var radius = Math.floor(intensity / 0.05);
		var x = Math.floor(Math.random()*mapW);
		var y = Math.floor(Math.random()*mapH);
		
		// Check the destination tile of this light
		// is not solid (or else it will only illuminate
		// the one tile on which it is placed)
		if(mapData[((y*mapW)+x)]==0) { continue; }
		
		// Check another light does not already exist
		// at these coordinates (otherwise it's not really
		// a useful example!)
		placeHere = true;
		
		for(l in allLights)
		{
			if(allLights[l].position[0]==x &&
				allLights[l].position[1]==y)
			{
				placeHere = false;
				break;
			}
		}
		
		if(!placeHere) { continue; }
		
		// Create this light and calculate its
		// illumated area
		var l = new Light(x, y, radius, intensity);
		l.calculate();

		// Add to the allLights array
		allLights.push(l);
	}
	// Rebuild the light map!
	rebuildLightMap();
}

window.onload = function() {
	ctx = document.getElementById('game').getContext('2d');
	ctx.font = "bold 10pt sans-serif";
	
	// Add some random lights
	resetLights();
	
	// If our reset link is clicked, remove existing
	// lights, create new ones, and rebuild the lightMap
	document.getElementById('changeLights').addEventListener('mouseup',
		function() {
		resetLights();
	});
	
	requestAnimationFrame(drawGame);
};

function drawGame()
{
	if(ctx==null) { return; }

	// Clear the Canvas
	ctx.fillStyle = "#000000";
	ctx.fillRect(0, 0, 800, 400);

	for(var y = 0; y < mapH; ++y)
	{
		for(var x = 0; x < mapW; ++x)
		{
			var idx = ((y * mapW) + x);
			
			if(lightMap[idx]==0) { continue; }
			
			ctx.globalAlpha = lightMap[idx];
			
			ctx.fillStyle = (mapData[idx]==0 ? "#0000cc" : "#ffddcc");
			ctx.fillRect(x * tileW, y * tileH, tileW, tileH);
		}
	}

	// Mark light sources
	ctx.globalAlpha = 1;
	ctx.strokeStyle = "#ff0000";
	for(var l in allLights)
	{
		ctx.beginPath();
		ctx.arc(allLights[l].position[0]*tileW + (tileW / 2),
			allLights[l].position[1]*tileH + (tileH / 2),
			5, 0, Math.PI*2);
		ctx.closePath();
		ctx.stroke();
	}
	
	// Ask for the next animation frame
	requestAnimationFrame(drawGame);
}

function bresenhamsLine(x1, y1, x2, y2)
{
	line = new Array();
	
	var dx = Math.abs(x2 - x1);
	var dy = Math.abs(y2 - y1);
	
	var sx = (x1 < x2 ? 1 : -1);
	var sy = (y1 < y2 ? 1 : -1);
	
	var error = dx - dy;
	
	var x = x1, y = y1;
	
	while(1)
	{
		line.push([x, y]);
		
		if(x==x2 && y==y2) { break; }
		
		var e2 = 2 * error;
		
		if(e2 >-dy) { error-= dy; x+= sx; }
		if(e2 < dx) { error+= dx; y+= sy; }
	}
	
	return line;
}

function pointsOnCircumference(cx, cy, cr)
{
	var list = new Array();
	
	var x = cr;
	var y = 0;
	var o2 = Math.floor(1 - x);

	while(y <= x)
	{
		list.push([ x + cx,  y + cy]);
		list.push([ y + cx,  x + cy]);
		list.push([-x + cx,  y + cy]);
		list.push([-y + cx,  x + cy]);
		list.push([-x + cx, -y + cy]);
		list.push([-y + cx, -x + cy]);
		list.push([ x + cx, -y + cy]);
		list.push([ y + cx, -x + cy]);

		y+= 1;

		if(o2 <= 0) { o2+= (2 * y) + 1; }
		else
		{
			x-= 1;
			o2+= (2 * (y - x)) + 1;
		}
	}

	return list;
}	
</script>

</head>
<body>
<p>Random lights with differing intensities are placed on the map below.  <a id="changeLights" style="color:#00a; font-weight:bold; cursor:pointer;">Click here to change the random lights</a>.  If you cannot see the example below, please ensure you have Javascript enabled and that your browser supports the Canvas element.</p>
<canvas id="game" width="800" height="400"></canvas>

</body>
</html>

Page loaded in 0.01 second(s).