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.