dynamic soft shadows in 2D

This implementation of dynamic shadows with soft edges in 2D using OpenGL was part of a game concept I experimented with. In the end I decided not to go ahead with the game, but as the shadowing itself was pretty fun to do and as the concepts (and possibly the code) can easily be used for other games, I will detail my work here.

I had just finished implementing hard-edged shadows for rectangles when I found a GameDev.net article by Orangy Tang, where a technique for rendering soft shadow edges is described. The article is very thorough and I ended up using most of the ideas described there. Instead of starting from scratch, I will only explain what was done differently.

But first, here’s an animation of the result in action:
Dynamic soft shadows animation

Click for a (2.5 MB) full-size version. The limited colors of the gif animation don’t do it justice, but I hope you can get a good impression of the shadow behaviour. Also notice the neat (and correct) colored lighting.

The main difference between the procedure presented in the article and the approach I chose is using a render to texture operation to accumulate the light. In the article, it is proposed to build lit and unlit areas in the alpha channel and then render the geometry modulated by the alpha value for every light.

The shadowed areas, including the ‘shadow fin’ regions, are essentially calculated in the same way and written to the alpha channel of a texture. Then, the light’s texture, modulated by the new alpha values, is rendered additively to the color channels. This is repeated for every light, until finally the color values at each pixel represent the amount of light of a given color at that location.

Now it is time to render the regular geometry. In contrast to the method from the article, any blend mode can be used. Thus it is fully possible to use the usual modes for transparency and similar. In a final step, the light color values from the texture are multiplied with the scene color values.

The advantages of the original approach are retained. In particular, it is still possible to have any shape of light simply by changing the light’s texture. Additionally, colored lighting works more intuitively and the presence of light and shadow does not affect the regular rendering of the scene at all.

It comes at the cost of requiring a render to texture type operation. On modern graphics chips, however, this feature is fast and well supported using framebuffer objects. Unfortunately, my notebook’s onboard graphics adapter does not provide the necessary extensions and so the downloadable code uses a slower copy of the light data to a texture instead.

For the implementation, I used the D programming language along with small parts of Arc 0.2 for Phobos. As said, the code was taken from a game prototype and as such was written to get things done quickly and not for extensibility or efficiency. Nevertheless, I have cleaned it and tried to add comments where they were missing, so it should be quite understandable.

There are a lot of possible enhancements: Spot lights and using the depth buffer to allow some geometry to be drawn over shadows instead of under (see the article) would be two worthwile additions. Significant improvements of the efficiency of shadow calculations are possible. Calculating shadows only for light blockers that are in the light’s radius, using the framebuffer objects extension, if available, and caching shadows for immobile lights and blockers should all improve performance significantly.

Finally, here are the sources.

Comments 26

  1. felix wrote:

    Yay for shadows! I’m almost at the point where I can implement it into my graphics engine… let’s see how python performs there ;)

    Posted 13 Sep 2007 at 11:49
  2. Clay Smith wrote:

    Can’t wait to see this integrated with physics :)

    Posted 21 Sep 2007 at 3:09
  3. Damian wrote:

    Any chance for a C++ port of the code?

    Posted 05 Oct 2007 at 14:08
  4. Christian wrote:

    No. But the code really isn’t very D dependent – most of the work happens in OpenGL. Porting to C++ should be simple.

    Posted 05 Oct 2007 at 17:14
  5. Damian wrote:

    Well, I’m trying to do such a port and I’m telling you it’s really not that simple. The shadows just won’t work as expected.

    Posted 05 Oct 2007 at 20:28
  6. Christian wrote:

    I could take a look at what you have so far, if you like. Just send me a mail with your code.

    Posted 05 Oct 2007 at 21:56
  7. Damian wrote:

    That would be great! I couldn’t find your email on the website though, but you should have mine through this comment form?

    Posted 06 Oct 2007 at 2:34
  8. Christian wrote:

    Right, I sent you a short mail. My address can, for example, be found in the license text of shadow.d.

    Posted 06 Oct 2007 at 10:29
  9. Damian wrote:

    I sent you an email with the code at hopefully the correct address, as I haven’t got any email from you.

    Posted 06 Oct 2007 at 19:54
  10. Christian wrote:

    I had already sent you two emails. I’ve now send another one to both of your source addresses.

    Posted 06 Oct 2007 at 20:53
  11. Alex wrote:

    Hi, just found this after doing some searching. However i cant get your program to work, it either segfaults or seem to be stuck in an infinite loop. Well, ill try port it to c++ when i get time. :)

    Posted 25 Oct 2007 at 22:05
  12. Christian wrote:

    Since I don’t do any error checking in the prototype, the most likely case of start up crashes are missing libraries. If you’re on windows, you can download dlls which supposedly work from http://svn.dsource.org/projects/arclib/downloads/dll/ and drop them into the directory the executable resides in.

    Damian’s C++ port has been coming along nicely. I don’t think he’s made it public yet though. You might want to talk to him instead of porting it all over again.

    Posted 26 Oct 2007 at 21:10
  13. Bastian wrote:

    Hello. This is a great effect. I can`t create this effect in my demo in c++ and opengl). Do you send to me this code in c++ (or send me email to Damian).

    Posted 24 Oct 2008 at 14:59
  14. Christian wrote:

    Bastian: Done.

    Posted 24 Oct 2008 at 21:14
  15. Bastian wrote:

    Ok. Thanks for your response and interest to my request.

    Posted 24 Oct 2008 at 22:12
  16. EiNDouble wrote:

    Hello, where can I get the c++ port of the code?
    I really wanted to try it out.

    Best regards and excellent work!

    Posted 16 Mar 2009 at 20:58
  17. Christian wrote:

    I’m sorry, but I do not have a working version of the C++ port Damian did. If someone ports it again and releases the source, please drop me a line so I can add a link!

    Posted 18 Mar 2009 at 18:49
  18. Jim wrote:

    Hi, Im just wondering if youre still active or not. I am currently in the process of porting the prototype to c++, but im having trouble in getting the blending to work, thus the shadows dont work.

    Would be great if you can help.

    FYI, great article. Helped me a lot :)

    Posted 14 Dec 2009 at 22:17
  19. Christian wrote:

    Jim: While porting the prototype can be a fun exercise, a lot has changed in the last years. You could probably achieve the same effect much more efficiently on modern graphics cards.

    Unfortunately I don’t have the time to help debug your problem. Start small, build things step by step and test everything along the way.

    Posted 18 Dec 2009 at 20:15
  20. Jim wrote:

    Christian: Thx for the reply :). Dont worry, i got it working just enough to achieve what i want.

    Thank you. Your sample codes really helped me in trying to achieve the effect. I would have no idea how/where to start by just reading orangy’s article.

    Cheers :)

    Posted 20 Dec 2009 at 11:32
  21. Manuel Bua wrote:

    Cool stuff, any chance to download c++ code for it?

    Posted 04 May 2010 at 15:25
  22. David Amador wrote:

    Hi, great tutorial.
    But I’m having some troubles converting it to C++.

    Would you mind converting this line to me, I can’t understand the loop, begins where and finishes where?

    foreach(ref vert; blockerLine[rightpenumbra.sections.length-1..$-leftpenumbra.sections.length+1])
    umbra.sections ~= Umbra.Section(vert, extendDir(0.5 * (leftpenumbra.sections[$-1].direction + rightpenumbra.sections[$-1].direction)));

    I’m also having some troubles on other parts but for now this would help a bunch. I’ve placed my email on the form.

    Posted 26 Nov 2011 at 1:17
  23. Christian wrote:

    David: It’s equivalent to this C++ code:

    for (size_t i = rightpenumbra.sections.size() - 1; i < blockerLine.size() - leftpenumbra.sections.size() + 1; ++i) {
        Vert &vert = blockerLine[i];
        umbra.sections.push_back(Umbra::Section(vert, extendDir(0.5 * (leftpenumbra.sections.last().direction + rightpenumbra.sections.last().direction)));

    Posted 28 Nov 2011 at 8:26
  24. Max wrote:

    I implemented this in c++ and it works, but I am interested in learning what you mean by “a lot has changed in the last years. You could probably achieve the same effect much more efficiently on modern graphics cards.” Optimizations I can think of are:
    - using a Quadtree to determine which lights/shadowblockers are visible
    - use vertex buffers instead of immediate mode
    - maybe put the shadow casting stuff in a geometry shader

    Also I think there are two limitations right now with this method. First, you can’t get overbrighting effects as described in the original article. I solved this by rendering the whole scene in a seperate fbo and blend it over each light, but I think this is not the most optimal way of doing it. It would be ideal if you could somehow save color values greater than 1 in a fbo, but I don’t know how.(maybe floating point textures or something)
    Second, this currently only works if the lights size is smaller than the shadowblockers. I think this can be solved by rendering both the penumbras and umbra in full darkness and then subtracting the areas of the penumbras appropriately.

    Anyways, this helped me a lot, I am only interested in knowing what can be improved to make it even better. This is how my implementation looks right know, you can also see the mentioned overbrightening effects:

    Posted 16 Aug 2012 at 15:19
  25. Christian wrote:

    I’m glad to hear that you got it working. Looks great! :)

    What I was thinking about when writing the statement about modern gpus was that the algorithm described here was mainly constrained by the limited number of blend modes available in the old fixed function pipeline. I haven’t thought about improving it in detail. Using vertex buffers and some sort of spacial tree data structure for the lights would definitely make sense though.

    As for overbrightening: floating point textures would work. You can also continue to use ARGB32 and change the pixel shader used for blending the lights onto the scene to do a scaled multiplication (something like result_color = 2 * unlit_color * light_amount) . You’d lose some accuracy and would have a maximum amount of overbrightening, but that’s probably ok.

    If you release your code somewhere, let me know! People have been asking for a C++ implementation quite a bit.

    Posted 18 Aug 2012 at 15:51
  26. Max wrote:

    Thanks for the answer! I will probably try something like what you suggested for the overbrightening before I look into floating point textures.
    I will also release it in the future, but it is not finished yet. There are no comments and I still want to do some of the things I mentioned. Right now I want to look into how Box2D does its spatial partitioning. I found this, but understanding the code might take a while: http://gamedev.stackexchange.com/questions/15126/2d-spatial-partitioning-alternatives-to-spatial-hashes-and-quadtrees/15150#15150

    Posted 19 Aug 2012 at 2:17

Trackbacks & Pingbacks 1

  1. From Jeff On Games » Posted Without (Too Much) Comment on 30 Jul 2010 at 3:35

    [...] shadows working in XNA using the GameDev article in combination with Catalin's implementation and Christian's implementation in D. However, trying to write the post, I wasn't sure if I could make it any clearer, as I'm not [...]