Thursday 13 October 2011

More On Shadows

I think I might finally have a solution that works for shadows.

I tried ID shadow maps and they worked well and in combination with the baked in ambient occlusion (AO) the scenes looked good...     in most places!

There is always a catch.  Objects that went underground shadowed the ground incorrectly!  As I had slopes it would have been very difficult to avoid some models that had to be partially underground on at least one side.

To cut a lot of time and trial and error out of this story, the solution was a combination of percentage closer filtering (PCF) and ID shadow maps combined.  I tried variance shadow maps (VSM) with ID but the limitations of the surface formats available on the Xbox meant I could not get enough depth precision at the same time as storing the object ID.  I might still be able to do it with VSM by packing the three variables in to the available formats but that is for another day.  PCF with ID only needs two variables so the Vector2 surface format works well for that.

The sampling method first checks the ID and avoids self shadowing models.  Only then does it resort to the common PCF method using 4 samples.  Anything much more than four samples on the Xbox results in a sudden massive halving of the frame rate which I put down to predicated tiling.

The following is the most significant part of the shader.

float PID_Sample(
        float2 vTexCoord, 
        float fLightDepth, 
        float entityID)
    float lit = 1.0f;

    float2 fSample = 
        SAMPLE_TEXTURE(ShadowMap, vTexCoord);
    float casterID = fSample.y;
    // The render target is initialised to white 
    // GraphicsDevice.Clear(Color.White)
    if (casterID < 1.0f && 
        casterID != entityID && 
        fLightDepth >= fSample.x)
        lit = 0.0f;
    return lit;

float PID_BoxSampleLightFactor(float4 shadowTexCoord)
    float fLightDepth = 
        shadowTexCoord.z - ShadowDepthBias;
    float texelStepSize = 1.0f / ShadowSize;
    // Work in floats
    float entityID = IntToFloat(ShadowEntityID);
    // Sample round the position
    float shadow[4];
    shadow[0] = PID_Sample(shadowTexCoord, 
        fLightDepth, entityID);
    shadow[1] = PID_Sample(shadowTexCoord + 
        float2(texelStepSize, 0), 
        fLightDepth, entityID);
    shadow[2] = PID_Sample(shadowTexCoord + 
        float2(0, texelStepSize), 
        fLightDepth, entityID);
    shadow[3] = PID_Sample(shadowTexCoord + 
        float2(texelStepSize, texelStepSize), 
        fLightDepth, entityID);

 float2 lerpFactor = frac(ShadowSize * shadowTexCoord);
 // Linear extrapolate between the samples
 return lerp(lerp(shadow[0], shadow[1], lerpFactor.x),
    lerp(shadow[2], shadow[3], lerpFactor.x),

The shadows fade out after about 40m from the camera.  I have found that anything less is a bit distracting in game but 40m is hard to see.  Even with a 2048x2048 shadow map the shadows are a tiny bit more pixelated than my ideal but it is the best I can manage at the moment.

The most important thing is that it works on the Xbox 360 and keeps a solid 60 frames per second with room to spare.

I can now move on to other things... again.

No comments: