Monday 30 January 2012

Simple Shadows

Nice shadows? What do you think?


Will its actually a quick hack and simple to do. So lets look at it.

The basics
How did I do it?  Well take a suitable transparent looking texture.  Then you just stick on onto the ground like this:
Suddenly it becomes obvious what I did, doesn't it?

What do we need:

  1. A suitable transparent texture (you can do this yourself)
  2. A textured quad to draw on the ground
  3. A way to make sure it appears on the ground but not in the ground

So, lets get to it...

The Textured Quad
This is using DirectX 9, but the idea is the same for DirectX 10, or even XNA.  We just need to an array of positions and UV coordinates.  The code will probably be something like this:
// the structure for feeding into DX9
struct ShadowVertex
{
  D3DXVECTOR3 pos;
  D3DXVECTOR2 uv;
};
// the FVF
const DWORD ShadowVertex_FVF=D3DFVF_XYZ | D3DFVF_TEX1;

// sets up the UV coordinates, which only needs to be done once
void InitShadowQuad(ShadowVertex quad[4])
{
  quad[0].uv=D3DXVECTOR2(1,1);
  quad[1].uv=D3DXVECTOR2(0,1);
  quad[2].uv=D3DXVECTOR2(1,0);
  quad[3].uv=D3DXVECTOR2(0,0);
}

// sets up the XZ coordinates for the quad
// you will need to adjust the Y coordinates to make sure they are
// neatly on the ground
void UpdateShadowQuad(ShadowVertex quad[4],D3DXVECTOR3 pos,float size)
{
  quad[0].pos=pos+D3DXVECTOR3(size,0,size);
  quad[1].pos=pos+D3DXVECTOR3(size,0,-size);
  quad[2].pos=pos+D3DXVECTOR3(-size,0,size);
  quad[3].pos=pos+D3DXVECTOR3(-size,0,-size);
}

I really should have a vertex buffer to hold these in, but I'm going for the simplest possible solution and just sticking with an array of data.

The other task you will need to do is to make sure that the Y coordinates of the quad are neatly on the ground.  For flat terrain that's just a matter of quad[0].pos.y=0, but it you are doing it on 3D terrain, it will be a little more complex.


The Rendering
For those who know their render states, this should be simple.  But for those who don't, it could be a bit more complex, so here are the main issues to consider.
  • Must have blending: to blend nicely with the terrain under the shadow
  • No lights: the shadow affect the terrains appearance, we don't want apply lighting twice
  • Z-Buffering must be dealt with: this is the tricky part 
The most obvious solution would be to just draw the shadow on the ground.  The trouble is you get something like this:
Some bits of the quad are above the ground, some bits of the quad are in the ground.  Its caused by rounding errors in the Z-buffer.  We can try to fix this by moving the quad a little bit above the ground, but this then asks the question how much? 0.1 units? 0.01units? 0.00001?  Depending on various factors in your game this may (or may not) look good.

I experimented with disabling the Z-buffer write or just turning the whole Z-buffer off, and I found that turning the Z-buffer off worked better.  You just have to be careful in your draw order (my first attempt had the shadow drawn on top of my model).

So here is the quick summary in english:
draw the ground
turn off Z-buffer
turn off lights
turn on blend
draw the quad with the shadow on it
turn off blend
turn on lights
turn on Z-buffer
NOW draw the model over the shadow

In code it looks like this:
// draw the shadow quad
void DrawShadowQuad(IDirect3DDevice9* pDev,IDirect3DTexture9* pTex,
                      ShadowVertex quad[4])
{
 pDev->SetRenderState(D3DRS_LIGHTING,false); // lights off
 pDev->SetRenderState(D3DRS_ZENABLE,false); // Z-buffer off
 pDev->SetRenderState(D3DRS_ALPHABLENDENABLE,true); // blend on 
 pDev->SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
 pDev->SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);
 pDev->SetTransform(D3DTS_WORLD,&IDENTITY_MAT); // world set with identity matrix
 pDev->SetFVF(ShadowVertex_FVF);
 pDev->SetTexture(0,pTex);
 pDev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP,2,quad,sizeof(ShadowVertex)); // draw
 pDev->SetRenderState(D3DRS_LIGHTING,true); // lights back on
 pDev->SetRenderState(D3DRS_ZENABLE,true); // Z-buffer on
 pDev->SetRenderState(D3DRS_ALPHABLENDENABLE,false); // blend off
}

(Note on DrawPrimitiveUP: Yes, I KNOW it has a horrible performance penalty, but its the simplest way to get the code up.  It should certainly be done with a vertex buffer.  However, you try and make the vertex buffer code this simple!  I went for simplicity, not efficiency)


Final Code
The main code looks something like this (but it will vary depending upon your ground):
// draw the shadow quad
// draw ground...
D3DXVECTOR3 pos=players pos
float radius=players radius
UpdateShadowQuad(verts,pos,radius); // update the vertex position
// put the vertexes on the ground
for(int i=0;i<4;i++) verts[i].pos.y=0;
DrawShadowQuad(GetDevice(),mpTex,verts); // draw the shadow
// draw the player...

And there we have it.

Improvements
Nope: its not a true shadow, and it is just a circle, and its not very realistic.  But it was very simple.  To do this properly, we need to start messing around with stencil buffers and possibly shaders too.  Frank Luna (http://www.d3dcoder.net/) has some good books on the topic and there are probably some other references on the net somewhere else.

This is quick and it works, and I have not found any articles explaining this simple trick. So I hope it helps.

Happy Coding,
Mark

No comments:

Post a Comment