Pyramid Shadow Mapping ( 180 degree point light source )
========================================================
Idea
====
Using one single square shape texture and splitting it into 4 areas like this:
+-----+
|\ 1 /|
| \ / |
|4 * 2|
| / \ |
|/ 3 \|
+-----+
Each triangular area is rendered in one pass similar to cube mapping. This way
a light source can be considered a pyramid ( hence the name ) where the light
source sits at the base and the light direction goes through the tip. This
looks roughly like this from the side:
+---L---+
\ /
\ /
\ /
v
The main difference to cube mapping is that the cameras rendering each side of
the pyramid have to be tilted downwards by 45 degrees to capture properly the
scene depths around the light source.
Characteristics
===============
- During mapping conventional 2D texture shadow mapping techniques like PCF can
be used
- Instead of 5 ( or even 6 ) faces as with a cube map only 4 have to be rendered
- In contrary to paraboloid shadow maps all involved mappings are linear which
prevents the problem of straight lines ending up bend in the render
- In contrary to paraboloid shadow maps the entire texture space is used instead
of only the circle fitting inside the square texture
- In contrary to cube mapping 6 times less memory foot print. Cube maps have 6
faces with the same resolution as a pyramid shadow map but due to the 180
degree nature only half the texels are actually used. The 3 times more pixel
information can be improved by pyramid shadow maps using double the resolution
which gives 4 times the pixel information instead of 3 times as with cube maps
- As with the other shadow mapping techniques the pyramid shadow map has higher
detail along the light direction and decreases resolution towards the border
- Since the entire texture space is used GL_CLAMP can be used safely to get
correct PCF results at around 180 degree
Creating the shadow map
=======================
Requires 4 passes. In each turn the model view and projection camera has to be
altered as well as the viewport. The projection camera requires specific field
of view values to render the map properly. A bit of math is required there.
// calculate the projection matrix. there is a little catch with the fov
// ratio which has to be taken into account as otherwise the result is
// incorrect. the fov in y direction has to be 90 degree but due to the
// 45 degree rotation of the camera the fov in x direction has to be
// larger than 90 degree. precisely the fov in x direction has to be
// 2 * atan( sqrt(2) ) = 109.47122 * ONE_PI. therefore:
// - fovX = 109.47122 * ONE_PI = 1.91063323624
// - fovY = 90 * ONE_PI = 1.57079632679
// - fovRatio = fovY / fovX = 0.822133885769
//
// now there exists a second catch and this is that for the left and
// right render the fov values has to be exchanged. this gives the second
// set of camera values:
// - fovX = 90 * ONE_PI = 1.57079632679
// - fovY = 109.47122 * ONE_PI = 1.91063323624
// - fovRatio = fovY / fovX = 1.21634689594
For each turn the model view matrix has to be set like this where baseMatrix is
the light world matrix:
// part 1: rotation=(45,0,0) viewport(0,0,1,0.5)
// cameraMatrix = decDMatrix::CreateRotation( 45.0 * ONE_PI, 0.0, 0.0 ) * baseMatrix;
// glViewport( 0, halfShadowSize, shadowSize, halfShadowSize );
// projectionCamera = plan.CreateProjectionMatrix( 1, 1, camFov1, camFovRatio1, 0.01f, light.GetCutOffDistance() );
//
// part 2: rotation=(0,-45,0) viewport(0.5,0,1,1)
// cameraMatrix = decDMatrix::CreateRotation( 0.0, -45.0 * ONE_PI, 0.0 ) * baseMatrix;
// glViewport( halfShadowSize, 0, halfShadowSize, shadowSize );
// projectionCamera = plan.CreateProjectionMatrix( 1, 1, camFov2, camFovRatio2, 0.01f, light.GetCutOffDistance() );
//
// part 3: rotation=(-45,0,0) viewport(0,0.5,1,1)
// cameraMatrix = decDMatrix::CreateRotation( -45.0 * ONE_PI, 0.0, 0.0 ) * baseMatrix;
// glViewport( 0, 0, shadowSize, halfShadowSize );
// projectionCamera = plan.CreateProjectionMatrix( 1, 1, camFov1, camFovRatio1, 0.01f, light.GetCutOffDistance() );
//
// part 4: rotation(0,45,0) viewport(0,0,0.5,1)
// cameraMatrix = decDMatrix::CreateRotation( 0.0, 45.0 * ONE_PI, 0.0 ) * baseMatrix;
// glViewport( 0, 0, halfShadowSize, shadowSize );
// projectionCamera = plan.CreateProjectionMatrix( 1, 1, camFov2, camFovRatio2, 0.01f, light.GetCutOffDistance() );
Inside the shadow shader pixels have to be rejected for each turn. This can be
either done using stenciling or a discard statement in the fragement program.
The appropriate statements are simple as they require only determing the major
axis and if it matches the turn. In the shadow map itself the distance of the
point to the light source is stored similar to cube shadow mapping.
Lighting with the shadow map
============================
Lighting works similar to cube shadow mapping in that the transformation from
camera space into light space has to be provided to the light shader usually
through a texture matrix. The same shadow casting routines as for spot shadow
mapping can be used just that the shadow coordinates are first processed a
bit. For this the point is send through the transformation matrix in the
texture matrix and then mapped to the pyramid. This shadow code is basic and
does the right mapping which can still be optmized. Shapos are the shadow
coordinates after the matrix transformation. Shadist is the distance of the
point to the light source. Stc are the texture coordinates ( vec3 ) ready to
be send into shadow2D.
if( shapos.y >= abs( shapos.x ) ){
stc = vec3( shapos.x, shapos.y - shapos.z, shadist );
stc.st *= vec2( 1.0 / ( shapos.y + shapos.z ) );
stc.st = stc.st * vec2( 0.5, 0.25 ) + vec2( 0.5, 0.75 );
}else if( shapos.x >= abs( shapos.y ) ){
stc = vec3( shapos.x - shapos.z, shapos.y, shadist );
stc.st *= vec2( 1.0 / ( shapos.x + shapos.z ) );
stc.st = stc.st * vec2( 0.25, 0.5 ) + vec2( 0.75, 0.5 );
}else if( -shapos.y >= abs( shapos.x ) ){
stc = vec3( shapos.x, shapos.y + shapos.z, shadist );
stc.st *= vec2( 1.0 / ( shapos.z - shapos.y ) );
stc.st = stc.st * vec2( 0.5, 0.25 ) + vec2( 0.5, 0.25 );
}else{ // -shapos.x >= abs( shapos.y )
stc = vec3( shapos.x + shapos.z, shapos.y, shadist );
stc.st *= vec2( 1.0 / ( shapos.z - shapos.x ) );
stc.st = stc.st * vec2( 0.25, 0.5 ) + vec2( 0.25, 0.5 );
}
All other shadow techniques can be used in conjunction with a pyramid shadow
map as only the initial shadow coordinates are calculated differently.
Potential Issues
================
At the boundaries of areas exist discontinuities. They do usually though not
cause troubles since shadow mapping taps usually in a small area around the
actual center shadow coordinates. Should though still troubles occur the
above mapping can be conducted for each sample to tap to get a correct
result. An indirection texture could be used for such a case. In various test
scenarios though the area boundaries are not visible at all.