Hello there! So a couple of days ago I got really curious about how I could simulate some 2D lights in SFML. After a bit of work, this is what I came up with.
(I didn't realize fraps recorded my background music too)
I kinda cheated because the scene itself was not made by me, it's just some screenshot I found on the internet. (which you can see by clicking
here)
This is far from finished but I think it looks kinda cool. I know there are lots of better projects, such as Let there be light, that are probably far more optimized and work better overall.
Here you can see the walls I set for this specific scene.
I'm pretty sure there are things that could be done better so let me know if you have any suggestions/criticisms.
So how does it work? There are three steps to generate a lightmap (which is basically more or less a texture generated depending on the lights), then we take the lightmap and draw it over our scene using the multiply blending mode. By using this blending mode each pixel that is already draw will be multiplied with the pixel from the lightmap that is directly above it. You can try using Photoshop or some other picture editing software to get a feeling of how this blendmode works.
To generate the lightmap we have to:
1. Generate/calculate the light meshFirst of all we need some kind of vertex array that describes the span of our light. To do this we have a couple of options. This is the hardest part you'll have to think about, but don't worry, it's not nearly as hard as you might think.
We could for example take a circle with a radius of how far we want the light to reach and do something called "polygon clipping" to clip away any object that is not supposed to be illuminated.
My approach however uses something called raytracing. In a nutshell, we imagine firing a ray in a certain direction to see if it hits anything on the way. I imagine this could get resource intensive really fast but I was too lay to figure out polygon clipping and I ways already familiarized with raytracing.
To learn how you can use raytracing to generate your mesh you can follow
THIS awesome interactive article that shows you everything you need to know, step by step, with interactive animations!
At the end, we're left with something like this:
Kinda ugly, but it's the exact same lightmap you saw drawn in the video (except the light controlled by my cursor).
Notice that the lights have a maximum range after which they stop propagating.
To draw colored light all you have to do is draw the polygon a certain color. In a scene with multiple lights I recommend using the add blend mode when you're drawing the light meshes. You can see in the picture above that where 2 lights intersect the colors get blended rather than overlapping.
If you want, you can add a bit of ambient light by clearing the screen with something other than black when rendering the lightmap. If you leave it completely black only what's illuminated by your lights will be visible. Choosing a gray color will make the entire scene visible, but darker where the lights don't illuminate. Finally choosing a different color will give your scene a slight tint. You can use this to make a scene feel warmer or colder or whatever your creativity demands.
2. Adjust the intensity over distance
No light in the real world would look like that because the intensity (the color) of the mesh is the same everywhere. Naturally you would expect a light to be at maximum intensity at the source and then slowly lose intensity as it spreads out more and more into space. Since we're going to multiply the lightmap with the rest of the scene at the end, intensity is pretty much the same as color at this point.
To do this you can either set different color for each vertex in the mesh or you could use shaders and let your graphics card work a bit.
I chose to use a fragment shader just because it felt easier to write at the time. It's very simple and you can write it in less than 10 lines of code. Basically you have to slowly fade out the color of your light the further you get from the light source.
Just by doing this your lightmap should start looking like this:
It kinda looks a bit soft and fuzzy because of the way I coded it, this can be improved a lot.
3. BlurRight now our light map has the right shape and colors but it still looks kinda rough so I blur them a bit with another shader that applies some gaussian blur.
Congratulations! Your lightmap texture is complete. Now all you have to do is draw it over your scene, using the multiply blending mode.
PenumbraIf you looked really carefully at the screenshots/video you may have noticed that the main light I'm controlling with my cursor produces penumbra. Usually a point light only produces an umbra (shadow) while something like the sun also produces penumbra. (if you have no ieda what the heck I'm rambling about, just google 'penumbra' and the first or second image should be self explanatory). I managed to get this effect by spawning a ring of point lights, this kinda approximates the way light would be emitted from the surface of an object.
Performance issues and considerationsThe part that will stress your computer the most is generating the mesh for each light. To avoid this you can generate the mesh only when needed (when the light is created, moved, has changed colors, etc) and when some object that is in the rage of the light has changed (has moved, rotated, etc).
I would say updating lots of lights in the same frame is a bad idea with this method and should be avoided. One cheap way I got away with it was decreasing the number of light updates per frame and scheduling the rest of them for the next few frames.
In my experience, since my code is not really optimized and it's single threaded, updating 10 lights every frame in the scene you saw above caused a noticeable FPS dip and while restricting the number of light updates per frame helped a lot, I would say there's huge space for improvements.
I'm sure this is caused by my sloppy code and you can probably get away without any performance issues if you optimize it enough.
Multi threading also might solve this problem but I'll have to rewrite a bit of the code for that to work so maybe someday I'll do a followup.
The codeI'm not going to share the code I wrote because in my opinion it's not ready to be used. It's more or less just a demo I made to see what kind of effect I can get without much work or complicated maths. I'm not proud of the performance side either and I'm sure it can be improved a lot.
If you have any suggestions or any kid of comments about my approach feel free to leave a reply below and I'll check it out tomorrow.