It’s been a long time since the last post here. There are two reasons for that :

  • I’ve been quite busy with real life stuff like building a house. This will probably keep me busy for a few years…
  • The new house I live in appears to be in some place not eligible for ADSL… I didn’t even know this was possible in 21st century… Anyway I’ll probably be without net access for a few more months.

Anyway here are some thoughts about light. Nothing really groundbreaking, but it’s good to have it here for people starting with lighting. There’s also a glimpse of a new experimentation I’m doing with lights. I hope to be able to bring this new system to pyromancer! sometime this year. Enjoy the reading 😉

So long I’ve always used a very basic light model in my prototypes. For a point light at position lx,ly, the light reaching position x,y is :
int squaredDist = (lx-x)*(lx-x)+(ly-y)*(ly-y);
double intensityCoef = 1.0-squaredDist/(radius*radius);

The formula makes the intensity value 1.0 at light position and 0.0 at distance radius from the light.

It’s very fast (especially since 1.0/(radius*radius) can be precomputed) but doesn’t produce very good looking lights :

The intensity decreases linearly and seems to end abruptly at the light range.

A more realistic approach is to use the inverse square law. The light intensity decreases proportionnally to the inverse of square distance :

int squaredDist = (lx-x)*(lx-x)+(ly-y)*(ly-y);
double intensityCoef = 1.0/(1.0+squaredDist);

There are several issues with this naive approach :

  • The intensity decreases very rapidely. This forces us to have huge light radius, which impacts performances
  • The intensity doesn’t reach 0. At the light radius, it’s still 1.0/(1.0+radius*radius) and it drop abruptly to 0 after.

We can easily fix both issues. First we scale down the distance to keep the intensity from dropping too fast :
double intensityCoef1 = 1.0/(1.0+squaredDist/20);

Then we subtract the remaining intensity at radius range so that it reaches 0 :
double intensityCoef2 = intensityCoef1 - 1.0/(1.0+radius*radius);

The issue is that intensity at light position is no more 1.0 but 1.0 – 1.0/(1.0+radius*radius), which results is less bright lights.
To fix that, we scale the final intensity :
double intensityCoef3 = intensityCoef2 / (1.0 - 1.0/(1.0+radius*radius));

This is a pretty good looking light, but I’m still annoyed by light interaction with fov blocking elements. Using any FOV algorithm to compute the part of the map reached by the light, we end up with hard shadow edges like this :

This kind of shadows has two flaws : it’s does not look very good, especially when the light source moves (think about a hero wielding a torch) and it’s not realistic. When light hits the ground of a room through a door, you don’t expect the rest of the room to be in total darkness. Diffuse lighting will increase the light in the whole room. I’ve always been thinking about implementing some 2D radiosity algorithm to light dungeons since I started working on roguelikes 5 years ago. I finally found some time to work on this and have some tech demo running. I still have to make it work inside pyromancer! to check if it can be fast enough for a real game…

Not sure a lot of people will be interested in such technology, but I’ll probably release the demo on the demo page soon.

17 comments so far

Add Your Comment
  1. Nice read! Glad to see you are still here. Keep up the good work.

  2. Hey, it’s good to hear from you at least! Interesting article, too.

  3. Well, i would be one of the “few” interested 😉

    Could you share some more details on the 2d radiosity algorithm?

  4. Wow, will this handle colours as well as luminosity? You know, like, for my glowing mushrooms cavern?

  5. Cool! Two more thoughts about lights:

    1. I don’t know if this is needed but gamma-correction would make it so that a light 1/2 as bright in the game looks 1/2 as bright to the human eye. This would be an adjustment per pixel.
    2. The human eye adjusts to brightness in somewhat of a logarithmic scale. Something with 1/100th as much light doesn’t look 1/100th as bright to us. I think this adjustment is per scene, not per pixel. Also, it doesn’t occur immediately (think of going outside after being in a movie theatre).

  6. Thanks for your support 🙂

    @Worthstream : in its current form, it’s a very naive implementation : light the dungeon with a standard algorithm, then consider each lit cell as a light source and procede iteratively. I’m using 3 iterations in the post’s screenshot (including the initial shading phase). This implementation has a lot of drawbacks and limitations and of course, each iteration makes the thing slower…

    @Mingos : the current implementation works with struct { float r,g,b } colors instead of intensity value, so yeah, it should work with colored lights. I’ll give it a try.

    @Amit : good suggestion. I’ll dig the gamma correction

  7. Re diffuse/ambient lighting, have you thought of using “Dikjstra maps” instead of a line-of-sight algorithm? The amount of light falling on a tile depends on the length of the path to the tile from the root node(s). Obviously not perfect but a reasonable approximation. You could add this light to “radiant” light based on line of sight.

  8. Awesome post! I struggled with the light functions too; I went with the realistic formula but had to up the brightness a lot. It still has a nasty sudden cut-off at the edge of the radius though. It only shows in outdoors maps, dungeons are often too cramped to notice. I’ll give your new formula a try, it looks pretty good 🙂

    Also, the radiosity effect is fantastic! I wonder if it can be faked using less computation? A blur filter with a variable radius could kinda work (small blur near lightsource and large blur away from it) but then light could bleed through walls. Maybe using artificial light sources at every lit cells really is unavoidable 😉

  9. It should be noted that while light does obey the inverse-square law, the amount of light actually hitting a surface also depends on the angle between the light source and the surface. The intensity coefficient needs to be further multiplied by 1/sqrt(1+r^2), if you’re aiming for true accuracy.

  10. Interesting, I never thought of that! I wonder what the curve would look like. Also, walls have a different angle to the light source than the floor so would probably look different. Maybe this is the nudge towards realism that results in a much more aesthetically pleasing image?

  11. Actually, if you’re shooting for physical accuracy, you should also take monitor gamma into account. On computer displays, RGB (128, 128, 128) does not mean 50% perceived brightness.

    Ideally you’d want to perform lighting calculations in “linear light” floating point format, then apply gamma correction before you convert the results to 24 bit RGB for display. This kind of setup would also make HDR lighting possible.

  12. This would make a roguelike based on the Thief series possible. Awesome!

  13. Good to see you’re still around Jice 🙂

    Horror of horrors, I don’t have any any variation in my light model, it’s either lit or it’s unlit! I might try a few of these and see how I like the look of them.

  14. That looks great. I just started on my first roguelike (well, just the lighting engine really see: http://etcet.github.com/Chrogue/) and all this info is great. I’m definitely going to try out the penumbra shading and luminosity equation you describe. But first I’m going to figure out sub-cell shading (just played pyromancer – damn it looks good!)…

  15. Very nice ! First I thought it was just a screenshot, then I saw the animated lights, then I discovered it was an actual game!! keep up the good work !
    I’m glad you find the article useful and you like pyro 🙂

  16. What… is that an online Javascript roguelike with dynamic lights? o.O Freaking awesome!!

  17. yes it is ! 😀