The most important phenomenon in this scenario is light scattering. Light scattering takes place in every direction, not only downwards as you modelled it. You can calculate the lighting with a big linear system of equations, but this is potentially slow (see radiosity). Here is an approximative alternative:
The basic idea is that a block distributes its light to all neighbouring blocks. We start with a 1
for unfilled blocks and 0
for filled blocks (note that I use 0
for "no light" and 1
for "full light"; this seems to be more intuitive"). After we have initialized all blocks with their respective values, we can start the following distribution pass (I omitted range checks; that's up to you):
float[, ] incomingLight; // should have the same size as the map
// and saves the additional light for each block
for each i,j in the map
float myLight = (map[i, j] == null ? 1 : map[i, j].Light)
incomingLight[i - 1, j ] += 0.10f * myLight //Add light to the left neighbour
incomingLight[i - 1, j - 1] += 0.07f * myLight //top left neighbour
incomingLight[i , j - 1] += 0.10f * myLight //top neighbour
//do the same for all remaining neighbours
incomingLight[i, j] -= (sumOfWeights) * myLight //preserve overall light intensity
// ...
next
for each i,j in the map
if(map[i, j] != null)
map[i, j].Light = min(1, map[i, j].Light + incomingLight[i, j])
incomingLight[i, j] = 0
next
Now we have illuminated all blocks that are adjacent to a light block. You might need to adjust the weights for the distribution.
We can repeat this pass to create a more realistic lighting. The more often the pass is executed, the deeper the light will scatter. Note that each pass will brighten up the entire scene because unfilled blocks have basically an infinite light intensity.
Another approach, which should be faster but slightly more imprecise, is to calculate the distance of each block to the nearest unblocked tile and use this as a coefficient for the lighting. There are very efficient algorithms to calculate the distance (see distance transform). Calculating the block distance (Manhattan distance) is quite simple and should suffice in your case:
Initialize filled blocks with infinity or a very big number and unfilled blocks with zero. Then do four passes:
float [,] distances //initialized with 0 or infinity
//from top left corner
for y from 0 to height - 1
for x from 0 to width - 1
distances[x, y] = min(distances[x, y], distances[x - 1, y] + 1, distances[x, y - 1] + 1)
//from top right corner
for y from 0 to height - 1
for x width - 1 to 0
distances[x, y] = min(distances[x, y], distances[x + 1, y] + 1, distances[x, y - 1] + 1)
//from bottom left corner
for y from height - 1 to 0
for x 0 to width - 1
distances[x, y] = min(distances[x, y], distances[x - 1, y] + 1, distances[x, y + 1] + 1)
//from bottom rightcorner
for y from height - 1 to 0
for x width - 1 to 0
distances[x, y] = min(distances[x, y], distances[x + 1, y] + 1, distances[x, y + 1] + 1)
After those four passes you have the block distance to the nearest free block in the distances
array.