Dynamic 2D Shadows

Update:

The XNA 4.0 version for Windows Phone 7 is available here: HardShadowsWP7.zip

 

This sample shows an implementation for dynamic 2D shadows in XNA. The code is based on an article from GameDev.net, written by Orangy Tang, but only implements hard-edged shadows.

As exercises to further understand the sample, you could try some of the following:

  • Make some objects (ConvexHull) move around
  • Add other lights, and modify their parameters
  • Add player-world collisions. This shouldn’t be too hard, since you already have the ConvexHull class.
  • Switch on/off the light surrounding the wizard
  • Try various texture for light
  • Make the player drop shadows. (Hint: switch off player light, and make a ConvexHull object that surrounds the player, and moves together with him)

The wizard character is controlled by the directional keyboard keys. My wired GamePad was not available these days :)

I want to credit George for the wizard sprite, and Studio Evil for the ground texture.

The sample can be downloaded here: dynamicshadows.zip

DynamicShadows1 DynamicShadows2

DynamicShadows3

  • Pingback: Catalin Zima - XNA and HLSL blog » April Sample Online!

  • 62316e

    YO! Nice sample! I have some problems with adding soft shadows and unable to rotate convex hull. would u be so kind as to help me?

  • 62316e

    How to add the Height property to the convex hull class? :)

  • http://www.catalinzima.com Catalin Zima

    I haven’t yet tried doing soft shadows, so I can’t help you there. As for rotating the objects…. you could probably transform each vertex manually.

  • 62316e

    Yeap. 10x i will try

  • Pingback: XNA Game Development « XNA Scratch

  • Greg

    Excellent sample.

    Can this code be used in a commercial project?

  • http://www.catalinzima.com Catalin Zima

    @greg: It’s fine by me to use it however you want.

  • RoyalFraggy

    Just what I was looking for. Great stuff.

    Thanks, and keep up the good work!

  • http://rogue-eyebrow.com VengantMjolnir

    Hey Catalina, have a question for you if you have the time for a discussion. I downloaded and started up your sample and added a frame counter to it. With one added light to the player( just a different texture to simulate a cone light ) and the original 37 objects the framerate was running at 24 fps average. After profiling I discovered the called operations( and looking at the algorithm ) I discovered that the shadow generation algorithm was being called on each object for every light. This made sense to me as a sample so don’t think I’m knocking it in any way.

    So, I fixed that with a quick and dirty SAT test against a bounding box for the hull, and the bounding circle for the light. If it passes the test, then I do the light calculations. This brough the number of shadows being cast from 259 to 61 at the starting position. Additionally I wanted to speed up the shadow generation algorithm so I moved the edge creation code into the constructor. (I also added convex hull rotation and movement into it, but that is seperate)

    However, this only increased the frame rate from 22-24 up to 30-34. I know, around a 30 percent increase… but not good enough for a really robust system. It was apparent additional profiling was needed. That yielded the fact that calls were down to the shadow generation algorithm… but over 75% of the draw function was spent in draw lightmaps, and 52% of that was spent in the draw lightmaps function itself and not its callees.

    It seems the culprit might be the renderstate changes in between each light. There isn’t much more I can do to opt out of shadow generation, so my optomizations have to focus on the creation of the lightmap now.

    Here is where I was hoping you had an insight or a resource I have missed. Do you know of another technique for drawing the shadow areas besides using the alpha channel?

  • http://www.catalinzima.com Catalin Zima

    Yeah, those two are the bottlenecks at the moment. I tried to think of many ways to do the shadow generation in a vertex shader, but unfortunately didn’t find a solution.

    One possible solution could be replacing the usage of the alpha channel for lightmaps with the usage of the stencil buffer. This might cut down on the number of renderstates that need to be set for each light, and maybe it is faster. The idea is mainly the same. Draw the shadows and while doing that, the affected pixels are marked in the stencil buffer. And then, when drawing the light, a test is made and only unmarked pixels are written.

  • http://rogue-eyebrow.com VengantMjolnnir

    Actually, a guy named Michael Anderson figured that one out and put it on his MSDN blog. Manders vs. Machine…
    http://blogs.msdn.com/manders/archive/2007/03/14/shadows-in-2d.aspx
    He even has the source for you to browse. I was thinking of using his shader program to speed up the shadow generation but that still leaves me with the main problem of render state switches.

    I’ll have a look into the stencil test method. Only thing I’m having a hard time warpping my head around is how a stencil test will handle overlapping shadows. Reason the current system works is because we clear the alpha map for each light… And I’m not currently sure how to clear the stencil buffer. If I can get it to work though I think it would eliminate the renderstate changes. I could render the shadows to a seperate render target just to fill in the stencil buffer. Then use that to render the lights.

    Do you think this discussion should be conducted in the Creator’s club forum?

  • http://www.catalinzima.com Catalin Zima

    to clear the stencil buffer: GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0);

    By moving it to Creator’s Club Forums, it’s possible you’ll have other people look at the problem and come up with more ideas.

  • Rautapalli

    How could i implement a scrolling background with this?
    I tried messing around with the viewMatrix, but i have no idea how it’s supposed to work.

    Currently my spritebatch uses Matrix.CreateTranslation(new Vector3(-Position, 0f)) that formula to have a moving background, but i have no idea how i would apply it to the shadows too.

  • Pingback: Catalin’s XNA Experiments » Working on shadows…

  • william bechard

    Rautapalli:

    i have a camera that i apply to spritebatch so that i can use world coordinates for all my objects and just move the camera for movement.

    (i use World1.cam.get_transformation(GraphicsDevice)); in my spriteBatch.Begin())

    //I get all my hull objects from this call
    World1.GatherShadows();
    DrawLightmap();

    graphics.GraphicsDevice.Clear(Color.White);

    // TODO: This is where i do my map drawing spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState, World1.cam.get_transformation(GraphicsDevice)); /*Send the variable that has your graphic device here*/

    spriteBatch.End();

    //Now draw all shadows

    spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState, World1.cam.get_transformation(GraphicsDevice)); /*Send the variable that has your graphic device here*/

    //World1.Worldobjects are all my stored hull objects
    foreach (ConvexHull hull in World1.Worldobjects)
    {
    hull.Draw(gameTime, World1.cam.get_transformation(GraphicsDevice));
    }

    spriteBatch.End();

    //Now draw the actual light
    spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState); /*Send the variable that has your graphic device here*/

    GraphicsDevice.RenderState.SourceBlend = Blend.Zero;
    GraphicsDevice.RenderState.DestinationBlend = Blend.SourceColor;
    GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
    spriteBatch.Draw(lightMap.GetTexture(), Vector2.Zero + new Vector2(400,300), null, Color.White, 0.0f, new Vector2(400.0f, 300.0f), 1.0f, 0, 0);

    spriteBatch.End();

    base.Draw(gameTime);

    //———————————————————
    Now notice the changes in the class. I made no changes to the light class because it takes spritebatch in its draw and if you noticed above i already used the World1.cam.get_transformation(GraphicsDevice) in the spriteBatch.Begin(….

    however in the ConvexHull.cs
    concentrate on 2 subs

    //Draw
    public void Draw(GameTime gameTime, Matrix TransformWorld)
    {
    GraphicsDevice device = game.GraphicsDevice;
    device.RenderState.CullMode = CullMode.None;
    device.RenderState.AlphaBlendEnable = false;

    device.VertexDeclaration = vertexDecl;

    drawingEffect.World = Matrix.CreateTranslation(position.X, position.Y, 0) * TransformWorld;
    drawingEffect.Begin();

    foreach (EffectPass pass in drawingEffect.CurrentTechnique.Passes)
    {
    pass.Begin();
    device.DrawUserIndexedPrimitives(PrimitiveType.TriangleFan, vertices, 0, vertices.Length, indices, 0, vertexCount);
    pass.End();

    }
    drawingEffect.End();
    }

    //DrawShadow
    public void DrawShadows(LightSource lightSource, Matrix TransformWorld)
    {
    //compute facing of each edge, using N*L
    for (int i = 0; i 0)
    backFacing[i] = false;
    else
    backFacing[i] = true;
    }

    //find beginning and ending vertices which
    //belong to the shadow
    int startingIndex = 0;
    int endingIndex = 0;
    for (int i = 0; i startingIndex)
    shadowVertexCount = endingIndex – startingIndex + 1;
    else
    shadowVertexCount = vertexCount + 1 – startingIndex + endingIndex;

    shadowVertices = new VertexPositionColor[shadowVertexCount * 2];

    //create a triangle strip that has the shape of the shadow
    int currentIndex = startingIndex;
    int svCount = 0;
    while (svCount != shadowVertexCount * 2)
    {
    Vector3 vertexPos = vertices[currentIndex].Position + new Vector3(position, 0);

    //one vertex on the hull
    shadowVertices[svCount] = new VertexPositionColor();
    shadowVertices[svCount].Color = Color.TransparentBlack;
    shadowVertices[svCount].Position = vertexPos;

    //one extruded by the light direction
    shadowVertices[svCount + 1] = new VertexPositionColor();
    shadowVertices[svCount + 1].Color = Color.TransparentBlack;
    Vector3 L2P = vertexPos – new Vector3(lightSource.Position, 0);
    L2P.Normalize();
    shadowVertices[svCount + 1].Position = new Vector3(lightSource.Position, 0) + L2P * 9000;

    svCount += 2;
    currentIndex = (currentIndex + 1) % vertexCount;
    }
    Matrix NewTemp = Matrix.Identity;

    //draw the shadow geometry
    game.GraphicsDevice.VertexDeclaration = vertexDecl;
    drawingEffect.World = TransformWorld;
    drawingEffect.Begin();
    drawingEffect.CurrentTechnique.Passes[0].Begin();

    game.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, shadowVertices, 0, shadowVertexCount * 2 – 2);

    drawingEffect.CurrentTechnique.Passes[0].End();
    drawingEffect.End();

    }

    //hope this helps.

  • william bechard

    I do have a question though. With black the shadows dont cross past their hulls. However if i set the color to anythign else shadows show on the hulls. Is there a way to prevent this.

  • william bechard

    hmm well i figured it out.

    within the DrawLightmap method

    I moved the

    foreach (ConvexHull hull in World1.Worldobjects)
    {
    hull.Draw(gT, World1.cam.get_transformation(GraphicsDevice));
    }

    right after the

    foreach (ConvexHull ch in World1.Worldobjects)
    {
    //draw shadow
    if (ch != null)
    {

    ch.DrawShadows(light, World1.cam.get_transformation(GraphicsDevice));

    }
    }

    so to recap in DrawLightmap method it should look like the following:

    foreach (LightSource light in lights)
    {
    //clear alpha to 1
    ClearAlphaToOne();

    //draw all shadows
    //write only to the alpha channel, which sets alpha to 0
    GraphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.Alpha;
    GraphicsDevice.RenderState.CullMode = CullMode.None;
    GraphicsDevice.RenderState.AlphaBlendEnable = true;
    GraphicsDevice.RenderState.DestinationBlend = Blend.Zero;
    GraphicsDevice.RenderState.SourceBlend = Blend.One;

    foreach (ConvexHull ch in World1.Worldobjects)
    {
    //draw shadow
    if (ch != null)
    {

    ch.DrawShadows(light, World1.cam.get_transformation(GraphicsDevice));

    }
    }
    foreach (ConvexHull hull in World1.Worldobjects)
    {
    hull.Draw(gT, World1.cam.get_transformation(GraphicsDevice));
    }

    GraphicsDevice.RenderState.ColorWriteChannels = ColorWriteChannels.All;

    //draw the light shape
    //where Alpha is 0, nothing will be written
    //This is the light
    spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState, World1.cam.get_transformation(GraphicsDevice));
    GraphicsDevice.RenderState.DestinationBlend = Blend.One;
    GraphicsDevice.RenderState.SourceBlend = Blend.DestinationAlpha;
    GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add;
    GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = true;
    light.Draw(spriteBatch);
    spriteBatch.End();

    }

    Now this will have the hulls lit up (fully) when light touches it

    If you would prefer them without light then make sure your color value is passed with an alpha of 0.

    this is my line to create the object (hull)

    objects = new ConvexHull(game2, Points, new Color(0,0,0,0), new Vector2(X, Y));

    Well this seems to have worked for me anyway.

  • Pnikosis

    Hi Catalin, I’m stuck with something and I though maybe you can help me out (please). I’m using your dynamic 2D shadows code (can I mention you in the credits in the game by the way?), and everything’s working fine. I’ve made some optimizations and it’s looking quite good.

    But now I’m trying to add some static shadows to my game, and for that I’m trying to calculate all the static shadows during the level loading period, and then just draw a texture with the static lights and shadows. The problem is that I’m not very good at graphics programming, and although I’m storing all the precalculated shadows in a big texture (with RenderTarget2D.GetTexture()) my problem is when my camera scrolls. RenderTarget2D gives me a texture with the viewport’s size, so when I move around and the camera moves, the rectangle containing the shadows stays in place :S What I need (I guess) is a bigger RenderTarget2D, with the size of the whole level, but I don’t know how to do this (gives me error when I try this. Any ideas?

    With dynamic shadows I have no problem, as it calculates on each frame the lights and shadows position related to the camera position.

    Thanks, sorry for the huge question.

  • Pnikosis

    Sorry, me again :P

    What I want to do is only draw the cached “cutted” static lights (by cutted, I mean, the light source and removing the parts that are shadowed), as the shadows are already in the dynamic shadows’ RenderTarget2D.

  • Pingback: Blind « AwkwardGames – Quote: "AwkwardGames ftw!"

  • Stan

    I’m trying to get this to work with a camera and all is well except that the shadows will only draw correctly if the convex hull position is at (0,0). Any suggestions? Thanks.

  • Christopher Harris

    Is anyone interested in a GPU based shadowing method?

  • http://britonia-game.com John Hampson

    Hi Catalin,

    Awesome sample and thanks for sharing. I’m going to take a look at it and try to incorperate it into one of my mini 2d projects.

    -John

  • FlySoft

    Wow, thats real awsome! You also can use this for tile Based games (32×32 tiles, 800×600), and it’s still very fast!

    Thank you very much!
    FlySoft

  • Pingback: Jeff On Games » Posted Without (Too Much) Comment

  • http://Website Pedro

    Hey!! This sample is very nice!!!
    I still trying to understand it! but I am a beginner in XNA!
    Unfortunately I am trying to port it to the 4.0 version, withou success… Any tips???

    Thank you!

  • http://krypton.codeplex.com Chris Harris – cDub

    Hello!

    I’ve created an extremely fast (full hd ready, 100+ lights) vertex-based 2D lighting engine, optimised for both PC and the Xbox 360, running on the XNA 4.0 framework. If anyone is interested in a ready-to-use, opensource 2D engine, rather than implementing your own solution, you can check out http://Krypton.codeplex.com/ :)

    Props to OrangyTang and Catalin!