Lighting Bloom on a Tilemap
tilemaptutorialgraphicsray-tracinglightingjavascriptarchivedLighting Bloom
Bloom for lighting can be an involved topic also, but we'll be looking at it from a simple perspective. If your lights are in an atmosphere, you can expect some degree of bloom. This is where light is scattered as it passes through air, smoke, noxious gasses - whatever makes your game world "atmospheric" (hoho).
This is also to emulate light bouncing off surfaces, and the other ways in which light moves about in the real world. It goes around corners, to some degree, in reality, so that's what we're looking at here.
We'll add a new global setting that determines how much light we'll keep from a tiles light intensity to the tiles it blooms to. For this example, I'm using the value 0.75, or 75% of the lights intensity will be used on the tiles it blooms to.
var bloom = 0.75;
Most of our code is the same as our previous Raytracing Lighting on Tilemaps tutorial, but we'll expand the calculate method. We start with the complete code from the previous tutorial:
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; }
}
}
But before we close this function, we're adding our bloom loops. We'll create an empty array called openList, and add all of the points in the area array touched by this light which are not solid (mapData value of 0 for solid, 1 for not solid in our example) to the list.
var openList = [];
for(var a in this.area)
{
if(mapData[a]==1) { openList.push(a); }
}
Now we begin our main bloom loop. While we've got entries in the openList array, we'll pop the value off the end of the array and store it in a variable called idx - this will be an index in the mapData array.
while(openList.length > 0)
{
var idx = parseInt(openList.pop());
We use parseInt() to ensure the browsers Javascript engine treats this value as an Integer.
The bloom that scatters from this tile is the intensity from the area property of this light at the idx position, multiplied by our global bloom value (0.75):
var bloomAmt = this.area[idx] * bloom;
If the amount of bloom generated by this tile is less than the baseLighting for our map, there's no point in spreading it any further. If your baseLighting is very low, I recommend you replace this with a bottom-level cut-off, so your light doesn't bloom over the whole map (replace the reference to baseLighting with 0.2, for example); while there's no problem with this per se, it can add a lot of processing demand on big maps:
if(bloomAmt <= baseLighting) { continue; }
We now create a list of neighbours - tiles that are next to the current tile in each of the cardinal directions. We only add neighbours that fall within map bounds.
var neighbours = [];
if((idx - mapW) > 0) { neighbours.push(idx - mapW); }
if((idx + mapW) < (mapW * mapH)) { neighbours.push(idx + mapW); }
if((idx % mapW) > 0) { neighbours.push(idx - 1); }
if((idx % mapW) < (mapW - 1)) { neighbours.push(idx + 1); }
We now loop through the neighbouring tiles that were within map bounds...
for(var n in neighbours)
{
If this tile is either not already in the lights area, or the current illumination value for this tile in the area is less than bloomAmt, and the tile is of a type that light can pass through (a value of 1 in our mapData example array), we add it to the openList:
if((!(neighbours[n] in this.area) ||
this.area[neighbours[n]] < bloomAmt) &&
mapData[neighbours[n]]==1)
{
openList.push(neighbours[n]);
}
If the neighbour is already in the area lit by this light and the current illumination strength on this tile is less than the bloomAmt we've calculated for these tiles, or this tile is not yet in the illumination area at all, we set the illumination value for this tile:
if((neighbours[n] in this.area &&
this.area[neighbours[n]] < bloomAmt) ||
!(neighbours[n] in this.area))
{
this.area[neighbours[n]] = bloomAmt;
}
...and we can close our loops and this function is done!
}
}
};
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;
// The amount of "bloom" to expand lighting by
// (relative to previous tile)
var bloom = 0.75;
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; }
}
}
var openList = [];
for(var a in this.area)
{
if(mapData[a]==1) { openList.push(a); }
}
while(openList.length > 0)
{
var idx = parseInt(openList.pop());
var bloomAmt = this.area[idx] * bloom;
if(bloomAmt <= baseLighting) { continue; }
var neighbours = new Array();
if((idx - mapW) > 0) { neighbours.push(idx - mapW); }
if((idx + mapW) < (mapW * mapH)) { neighbours.push(idx + mapW); }
if((idx % mapW) > 0) { neighbours.push(idx - 1); }
if((idx % mapW) < (mapW - 1)) { neighbours.push(idx + 1); }
for(var n in neighbours)
{
if((!(neighbours[n] in this.area) ||
this.area[neighbours[n]] < bloomAmt) &&
mapData[neighbours[n]]==1)
{
openList.push(neighbours[n]);
}
if((neighbours[n] in this.area &&
this.area[neighbours[n]] < bloomAmt) ||
!(neighbours[n] in this.area))
{
this.area[neighbours[n]] = bloomAmt;
}
}
}
};
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 3 random lights
while(allLights.length < 3)
{
// 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>