Having seen how we can render the shadow map for each split in the last post, it’s time to look at the final step: map the shadow atlas onto the scene. So, basically, for each pixel in the scene we need to determine if it lies in shadow or not. To do so we first need to determine the appropriate shadow split for that pixel. This can be done in a couple different ways, for example, by determining the split based on the distance to the viewer or by picking the split based on the look up texture coordinates. I chose the latter, as this makes better usage of the higher resolution maps. Check out this article for a nice comparison.

So in order to determine the shadow split we basically transform the current fragment’s world position by each shadow transform and then pick the first shadow map where the coordinates fall within the range of the shadow atlas partition. Let’s define some helper functions first:

#define NumSplits 4

// the "world to shadow atlas partition" transforms

// the bounding rectangle (in texture coordinates) for each split
float4 TileBounds[NumSplits];

{
magfilter = POINT;    minfilter = POINT;    mipfilter = POINT;
};

{
float4 TexCoords_0_1;
float4 TexCoords_2_3;
float4 LightSpaceDepth;
};

// compute shadow parameters (tex coords and depth)
// for a given world space position
{
float4 texCoords[NumSplits];
float lightSpaceDepth[NumSplits];

for( int i=0; i<NumSplits; ++i )
{
float4 lightSpacePosition = mul( worldPosition, ShadowTransform[i] );
texCoords[i] = lightSpacePosition / lightSpacePosition.w;
lightSpaceDepth[i] = texCoords[i].z;
}

result.TexCoords_0_1 = float4(texCoords[0].xy, texCoords[1].xy);
result.TexCoords_2_3 = float4(texCoords[2].xy, texCoords[3].xy);
result.LightSpaceDepth = float4( lightSpaceDepth[0],
lightSpaceDepth[1],
lightSpaceDepth[2],
lightSpaceDepth[3] );

return result;
}

{
float2 TexCoords;
float  LightSpaceDepth;
int    SplitIndex;
};

// find split index, texcoords and light space depth for given shadow data
{
{
};

float lightSpaceDepth[NumSplits] =
{
};

for( int splitIndex=0; splitIndex < NumSplits; splitIndex++ )
{
{
result.LightSpaceDepth = lightSpaceDepth[splitIndex];
result.SplitIndex = splitIndex;

return result;
}
}

ShadowSplitInfo result = { float2(0,0), 0, NumSplits };
return result;
}

{
float storedDepth = tex2Dlod( ShadowMapSampler, float4( splitInfo.TexCoords, 0, 0)).r;

return (splitInfo.LightSpaceDepth <  storedDepth);
}


Armed with these definitions we can now add shadowing to any shader. All we need to do is call GetShadowData in order to convert a given world position into shadow atlas texture coordinates and then GetShadowFactor to do the lookup. Note that since the quantities in ShadowData are linear, I would suggest calling GetShadowFactor in the vertex shader in order to save fragment shader instructions. I collected these functions in a header file Shadow.h, which can be included by any shader.

What’s with ShadowTransform[i] though? The matrix is assumed to transform a vertex’s world space position into the texture coordinates of it’s corresponding shadow split $i$. Actually nothing new, the combined shadow view and projection matrix – if it weren’t for the nasty coordinate space differences: After projection, a vertex ends up in clip space which is defined as (ignoring the Z coordinate) $[-1, \dots, 1] \times [-1, \dots, 1]$. But we need texture coordinates for the shadow map lookup, which range from $[0, \dots, 1] \times [0, \dots, 1]$. Even more specific: we need to index into the sub rectangle of the corresponding shadow map in the shadow atlas. And to make things even worse: The y-axis of clip space points upwards while the y axis in texture space points downwards: top left in clip space is $(-1,1)$ whereas top left in texture coordinates is $(0,0)$. Luckily we can extend the combined shadow view and projection matrix to do the remapping:

// compute block index into shadow atlas
int tileX = i % 2;
int tileY = i / 2;

// tile matrix: maps from clip space to shadow atlas block
var tileMatrix = Matrix.Identity;
tileMatrix.M11 = 0.25f;
tileMatrix.M22 = -0.25f;
tileMatrix.Translation = new Vector3(0.25f + tileX * 0.5f, 0.25f + tileY * 0.5f, 0);

// now combine with shadow view and projection

So the first two diagonal elements of tileMatrix scale the clip space $x$ and $y$ coordinates down to $[-\frac{1}{4}, \dots, \frac{1}{4}]$ and the translation component shifts the coordinates to $[0, \dots, \frac{1}{2}]$. The tileX * 0.5f and tileY * 0.5f part then offsets the coordinates into the proper shadow atlas partition. Simple as that. Beware though, this only works for orthographic shadow projections.