# 2D TileMap smooth lighting

I want to implement (a smooth) lighting into my 2D Maps. I want to have the basic idea. What I thought of is 2 components.

The Scene's Ambient light:
The Tiles and Sprites are tinted into this colour first. For example if it's nighttime, the ambient color is very dark so you can barely see anything.

The Scene's Light sources: The Light Sources have a Radius, intensity and a falloff. The more a tile or sprite is near a light source, the more it gets illuminated by it.

What I thought about the soft lights is that a tile/ sprite has a bounding. Since Tiles are always quadratic, I assume I can interpolate between all four edges of the bounding.The Edges of two neighbouring tiles have the same illumination,similar to vertex lighting in 3D. Does someone have an approach how to start this?

One method you could use is lightmaps. It depends on how big your tile maps get I guess but a good thing about lightmaps is that they dont have to be the same resolution as your tilemaps.

So say you have a 2d tilemap, each tile is 16*16 and you have 10X10 tiles ( to make the maths easy ). This is 160*160 pixels.

You create a 160X160 bitmap ( rgb if you want coloured lights ) and plot your lighting onto it only.

You then take the corner positions of each of your tiles and use that as coordinates into the lightmap.

So tile at coord 8X3 is gets the rgb at coord (8*16), (3*16) from the lightmap. You then apply this to the tile corner.

Sprites would use the same method.

A good thing about lightmaps is that you can also bake shadows into them.

You can set spotlights and simulate light by a raycast algorithm and interpolate to have the light intensity, here's an approximation in javascript that i wrote to test something similar some time ago, read it carefully, it's not clean as it could be but it can help you to find out your own solution.

var canvas = document.getElementsByTagName("canvas")[0];
canvas.height = 300;
canvas.width = 300;
var ctx = canvas.getContext("2d");

var vec2 = function(_x,_y){
this.x = _x || 0;
this.y = _y || 0;

var t = new vec2;
t.x = this.x + v.x;
t.y = this.y + v.y;
return t;
}
}

var Player = function(){
var fov = 15;
var direction;
var speed = 2;
var acceleration = new vec2(0,0);
var pos = new vec2(0,50*world.ts);

this.pos = function(){
return pos;
}
this.Draw = function(){
ctx.save();
ctx.fillStyle = "red";
ctx.fillRect(pos.x, pos.y, world.ts , world.ts);
ctx.restore();
}

this.Move = function(kc){
pos.x++;
if(pos.x > canvas.width)pos.x = 0;
}

this.Update = function(){
// if()
}

this.RayCast = function(){
world.tileVisible("clear");
var rays = 3600;
var visible = [];
var angle = 360/rays;

for(var r = 0; r < rays; r++){
var p = new vec2(pos.x + world.ts/2, pos.y - 1 + world.ts/2);
var ang = (angle * r) * (Math.PI / 180);

for(var f = 0; f < fov; f++){
var pointing = new vec2(p.x + (world.ts*f) * Math.cos(ang), p.y + (world.ts*f) * Math.sin(ang));
if(world.getTileByCoord(pointing)){
world.tileVisible(pointing);
}else{
world.tileVisible(pointing);
break;
}
}
}

}
}

var WorldMap = function(){
var size = 100;
var ts = 3;
var visible = {};

var tiles = (function(){
var temp = [];
for(var y = 0; y < size; y++){
var row = [];
for(var x = 0; x < size; x++){
(y != 50 && Math.floor(Math.random()*100) > 90)? row.push(0) : row.push(1);
}
temp.push(row);
}
return temp;
})();

this.Draw = function(){
for(var y = 0; y < tiles.length; y++){
for(var x = 0; x < tiles[0].length; x++){

if(this.isVisible(x,y)){
if(tiles[y][x]){
// ctx.strokeRect(0 + (ts * x), 0 + (ts * y), ts, ts);
}else{
ctx.fillRect(0 + (ts * x), 0 + (ts * y), ts, ts);
}
}else{
ctx.save();
ctx.fillStyle = "grey";
ctx.fillRect(0 + (ts * x), 0 + (ts * y), ts, ts);
ctx.restore();
}
}
}

}

this.getPortion = function(cpos, fov, cb){
var temp = [];
var ini = new vec2(Math.round(cpos.x - fov.x/2), Math.round(cpos.y - fov.y/2));

for(var y = ini.y; y < ini.y + fov.y; y++){
for(var x = ini.x; x < ini.x + fov.x; x++){
if(tiles[y] && typeof tiles[y][x] == "number"){
if(cb) cb(new vec2(x,y));
else temp.push(new vec2(x,y));
}
}
}

if(!cb) return temp;
}

this.getTileByCoord = function(tile){
return tiles[Math.round(tile.y/ts)][Math.round(tile.x/ts)];
}

this.tileVisible = function(tile){
if(tile == "clear"){ delete visible; visible = {};}
if(!visible[Math.round(tile.x/ts)])
visible[Math.round(tile.x/ts)] = {};

visible[Math.round(tile.x/ts)][Math.round(tile.y/ts)] = true;
}

this.isVisible = function(tile, y){
if(!y){
if(visible[Math.round(tile.x/ts)] && visible[Math.round(tile.x/ts)][Math.round(tile.y/ts)])
return true;
return false;
}else{
if(visible[tile] && visible[tile][y])
return true
return false;
}
}

this.setVisibles = function(vis){
visible = vis;
}

this.map = tiles;
this.ts = ts;
}

function Raycast(args){
var map = args.map;
var center = args.center || {x: map.length/2,y: map[0].length/2};
var los = args.lengthOfSight || map.length/2;
var ts = args.tileSize || 5;
var prec = args.precision || 100;
var block = args.vBlock || [0];

var visibles = {};

var rays = 3600 * ((prec || 100)/100);
var los = los || map.length;
var visible = [];
var angle = 360/rays;

var getTileByCoord = function(tile){
return map[Math.round(tile.y/ts)][Math.round(tile.x/ts)];
}

var tileVisible_1 = function(tile){
if(!visibles[Math.round(tile.x / ts)])
visibles[Math.round(tile.x / ts)] = {};

visibles[Math.round(tile.x / ts)][Math.round(tile.y / ts)] = true;
}

var tileVisible = function(tile){
if(!visibles[Math.round(tile.x)])
visibles[Math.round(tile.x)] = {};

visibles[Math.round(tile.x)][Math.round(tile.y)] = true;
}

var getTilesCrossed = function(ini, end){
if(!window.first){
console.log(ini,end);
window.first = 1;
}
var steep = Math.abs(ini.y - end.x) > Math.abs(ini.y - end.x);
if (steep) {
ini = new vec2(ini.y, ini.x);
end = new vec2(end.y, end.x);
}
if (ini.x > end.x) {
ini = new vec2(ini.y, ini.x);
end = new vec2(end.y, end.x);
}

var deltax = end.x - ini.x;
var deltay = Math.abs(end.y - ini.y);
var error = 0;
var ystep;
var y = ini.y;

if(ini.y < end.y){
ystep = 1;
}else{
ystep = -1;
}

for (var x = ini.x; x <= end.x; x++) {
if(typeof map[y][x] == "number"){
var t = map[y][x];
if(steep)
tileVisible(new vec2(y,x));
else
tileVisible(new vec2(x,y));

if(block.indexOf(t) >= 0) break;

if(!steep){
error += deltay;
}

if (2 * error >= deltax) {
y += ystep;
error -= deltax;
}
}
}
}

var ini = new Date().getTime();

for(var r = 0; r < rays; r++){
var p = new vec2(center.x, center.y);
var ang = (angle * r) * (Math.PI / 180);

for(var f = 0; f < los; f++){
var pointing = new vec2(p.x + (ts*f) * Math.cos(ang), p.y -1  + (ts*f) * Math.sin(ang));
var t = parseInt(getTileByCoord(pointing));
if(typeof t == "number" && block.indexOf(t) < 0){
tileVisible_1(pointing);
}else{
tileVisible_1(pointing);
break;
}
}
}

// console.log("raycast in: ",new Date().getTime() - ini);
return visibles;
}

world = new WorldMap();
pj = new Player();

window.timer = setInterval(function(){
ctx.clearRect(0,0,canvas.width,canvas.height);
world.setVisibles(
Raycast({
map: world.map,
center: {x: pj.pos().x,y: pj.pos().y},
tileSize: world.ts,
lengthOfSight: 10,
precision: 100,
vBlock: [0]
})
);
world.Draw();
pj.Draw();
pj.Move();
},1000/60);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<canvas></canvas>

• Whoa. Not sure on the actual value of this answer regarding its usefulness, but I gotta say that it's one hell of a cool demo!
– user35344
Commented Jan 13, 2016 at 13:57