I said I would write some more about using custom renderstates with SpriteBatch, and being a man of my word, here it comes…
The SpriteBlendMode enum has built in support for a couple of the most common blending modes. AlphaBlend is good for most translucent objects, and Additive works well for explosions and glowing magic effects. There are many other interesting blend modes, however, which you can only access by setting renderstates directly on the graphics device.
Here I’m going to talk about multiplicative blending, which is good for lighting effects. Lighting for 3D games is usually done in a custom vertex or pixel shader, but there are many cool 2D effects that can be achieved using simple sprite blending.
Imagine we’ve finished drawing a 2D scene (or maybe just a part of a scene), and now want to draw some lighting effects over the top of it. Perhaps we want to render dynamic light effects by brightening the areas near explosions, or a headlamp effect by brightening the area in front of our car sprite, or a shadow by darkening a blob underneath our player sprite.
Multiplicative blending can achieve all these effects. If we multiply the color already in the framebuffer with the color of the sprite being drawn, this will have no effect where that sprite contains a value of one. Where the sprite contains a value less than one it will darken the scene to create a shadow, or if the sprite is brighter than one it will lighten it.
The only problem is, standard texture formats only contain values ranging from 0 to 1 (although paint programs often present this scaled so the range appears to be 0 to 255). This makes it impossible to ever lighten the scene!
The solution requires a bit of trickery using the source and destination blend factors. Here’s the code to set this up:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.None); graphicsDevice.RenderState.SourceBlend = Blend.DestinationColor; graphicsDevice.RenderState.DestinationBlend = Blend.SourceColor;
Remember that the blend equation is SourceColor * SourceBlend + DestinationColor * DestinationBlend. With these blend factor settings, the resulting color expands to:
SourceColor * DestinationColor + DestinationColor * SourceColor
Algebra to the rescue! If you refactor this, you will see it is the same thing as:
SourceColor * DestinationColor * 2
In other words, where our sprite contains a color value of 0.5 (usually shown as 128 in a paint program), that will have no effect. Brighter values add light, and darker values create shadows.
An example might help to visualize this. Let’s say we start off with this scene:
(hey, what can I say, I’m a programmer, not an artist
We can create a shadow sprite using an upside down, squashed, and blurred version of the cat:
Using multiplicative blending, this shadow can be blended over the top of the original scene:
But wait! This is no ordinary cat. His demon cyborg eyes need to cast rays of laser light in order to incinerate any passing camels. For this we need a sprite containing the shape of the light:
With multiplicative blending, I can draw a couple of rotated copies of this sprite over my scene. Note how the light isn’t just drawn as white, but actually brightens up whatever scenery lies behind it:
If you have a lot of overlapping lights in your scene, an interesting extension of this technique is to accumulate all the lights separately before combining them with your main scene:
- Create a RenderTarget2D the same size as your screen
- Draw the regular scene as normal
- GraphicsDevice.SetRenderTarget(0, lightRenderTarget)
- Draw all the light shapes using SpriteBlendMode.Additive
- GraphicsDevice.SetRenderTarget(0, null)
- Using SpriteSortMode.Immediate, set renderstates for multiply blend mode
- SpriteBatch.Draw(lightRenderTarget.GetTexture()), covering the entire screen
Many years ago I wrote a demo that used a similar technique to do fake 2D lighting entirely in software. How technology has improved since then! Using the XNA SpriteBatch class, it is easy to handle hundreds of overlapping lights at a great framerate, and without any of the banding problems I ran into trying to do this with just 256 VGA colors.