Recommended Reading:
What are dynamic 2D shadows
Monaco: What's Yours is Mine |
Of course, this aspect does not need to be used just for lighting specifically. Monaco is a game which uses this concept for the player's field of view, such that you can only see the parts of the level that your character actually has line of sight with.
How you could implement it
There are two main aspects to consider when using this approach. The first is the concept of Render to Texture, where instead of rendering to the usual backbuffer you render to a separate texture; and the second is Texture Blending, where you control how multiple textures are blended together for different visual effects. It's important that you have a basic grasp of these techniques, so I'd recommend reading up on them if you're not too sure what they mean.Our game rendering code is broken up into three phases. First we render what's known as a lightmap to a separate render texture, then we render our level geometry to the backbuffer and finally we then blend the backbuffer with the lightmap to create our final scene. Since creating the lightmap requires the most work, it will be the focus for this article.
Creating a lightmap
A lightmap is simply a map of the light (and consequently dark) areas in a scene. In our case, it's simply a coloured texture which we combine with our scene, where the dark areas of the lightmap are rendered in ambient lighting and the bright areas are rendered according to their colour.Creating shadow primitives |
If we were to simply loop through each light, render a simple feathered circle texture for each and then render a black primitive behind each of our objects for shadows, we wouldn't be able to blend multiple light sources together (as the black would override any other colour already there). As such, in order to create our lightmap, we're going to use a little trick to control what parts of the light texture are rendered.
First, we render the shadow primitives to the texture's alpha channel as 0. The alpha channel controls how transparent a texture is, and since the lightmap will always be completely opaque we can use it as a form of temporary storage instead. To do this in DirectX 9, we set the following render state parameter before drawing our primitives.
device->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_ALPHA);
Then we render our light texture using a blending operation where the amount of light texture added depends on the target's alpha. For DirectX 9, the way we achieve this is by setting the device's render state blending parameters before drawing our light texture. We also reset the ColorWriteEnable parameter so we can render the light texture's RGB values.
device->SetRenderState(D3DRS_COLORWRITEENABLE, 0x0000000F); device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTALPHA); device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
A lightmap created from 6 lights |
If we repeat this process for each light we can create a lightmap containing all of our lights, correctly shadowed and blended where two lights meet. Don't forget to clear the alpha channel between drawing each light so that our shadow data doesn't carry across into the following lights. Unfortunately both the Clear() and ColorFill() methods in DirectX ignore the ColorWriteEnable parameter, so you have to manually render a full-screen quad if you want to clear the lightmap's alpha channel.
Combining the lightmap with the scene
After rendering your scene geometry, we can then combine it with the lightmap to form the final lit scene. To do this, we use another blending operation to control how much of the scene colour we render. Specifically, we render our target texture where the amount of target texture used depends on the lightmap's colour or RGB values. To this in DirectX 9, we set the device's render state blending parameters to the following values.device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO); device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);
Scene combined with lightmap |
An interesting thing to note is the effects of multiple light sources. In the picture shown, you can see how some of the shadows from the blue light are rendered as more pink since they're still receiving light from the pink source. Light is also correctly blended where the two lights meet, forming a purple area.
Source code and comments
While this method does produce very impressive visual results, it can also be quite taxing for the framerate. Every frame we have to compare every light to every object in the scene and calculate shadows for each of them. In my sample, adding just one extra hull caused the FPS to drop by 60 frames. If you consider how sparse the scene is, you can already begin to imagine the problems you'd have with using this solution in a complex game.I've attempted to optimise the code by batching all of the shadows into one draw call for each light, but even then the performance save was only some tens of frames per second. There may be other ways to increase performance though, so if you have any ideas then do feel free to share them. You should find an executable in the Release folder, which will need the latest DirectX 9 redistributable or SDK to run.
Credits:
George - Wizard Sprite
Studio Evil - Ground Texture
Source:
Sample Archive
DirectX 9 Redistributable
No comments:
Post a Comment