Saturday 9 April 2016

Shadow maps #3 (part 28)

Ok, time for the last part of implementing basic shadow maps. The technique we're going to look at in this post is called cascading shadow maps. This is a technique mostly used to improve the quality for lights that effect large areas such as our sunlight. The problem we've had so far is that a high quality shadow map will only produce shadows in a small area while a large area shadow maps will be of such low quality things get very blocky.

While using a smoothing technique like we did in our last post does improve this somewhat up close shadows do not look very good. The lower quality shadow maps are fine for things that are further away.

Now there are several techniques that can improve this each with their own strong points and weakpoints. One alternative I'd like to mention is altering the projection matrix so the projection is skewed based on the distance to the camera ensuring we have higher detail shadow maps closer to the camera in a single map.
I'd also like to point to a completely different technique called shadow volumes, I've not implemented those myself but reading about them I'm interested to try some day. They seem to give incredible results but they may be more difficult to implement if you have loads of moving objects. I'm no expert in them yet so I'll refrain from commenting too much.

The technique we'll be using is where we simply render multiple shadow maps and pick the one best suited to what we're rendering. So we have a high quality shadow map for shadows cast close to the camera, we have a medium quality shadow map for things further away, and we have a low quality shadow map for things even further out. The screenshot below shows the three maps where I've changed the color of the shadow cast to highlight the transitions between the shadow maps (I left the code that produces this in our terrain fragment shader but disabled it, if you want to play around with it):


Now I'm keeping things simple here and as a result we're adding more overhead then we need.
First off, where there is overlap in the shadow maps we're rendering a bunch of detail into the lower quality shadow maps that will never be used. We could use a stencil buffer to cut that out but I'm not sure how much that would improve things as we're really not doing anything in the fragment shader anyway. Another improvement I've thought about is using our bounding box checking logic to exclude anything that falls fully within the overlap space, that might make a noticeable difference.
Second, depending on our camera position and the angle of our sun we may not need the other shadow maps at all.
Third, I already mentioned this in my previous posts and this ties into the second point, we're centering our shadow maps on the camera position so in worse case half our our shadow maps will never be used. Adjusting our lookat point for our shadows may allow us to cover a greater area with our higher detail shadow map.

These are all issues for later to deal with. It's worth noting though that with the changes we're making today on my little MacBook Pro the frame rate has suffered and while we were rendering at a comfortable 60fps unless we move to high in our scene, it's dropped to 30 to 40fps at the moment.
I have added one small enhancement and that is that I only re-render our shadow maps if our lighting direction has changed (which usually is a static) or if our lookat point has moved more then a set distance (we do this by rounding our look at position).

Last but not least, I've added a small bit of code to react to the - and = (+) keys and move the position of the sun. There is no protection for "night time" so we actually end up lighting the scene from below.

Going from 1 to 3 shadow maps


Obviously we need to add support for our 3 levels of shadow maps first. This starts with adjusting our lightsource structure:
// and a structure to hold information about a light (temporarily moved here)
typedef struct lightSource {
  float             ambient;          // ambient factor for our light
  vec3              position;         // position of our light
  vec3              adjPosition;      // position of our light with view matrix applied
  bool              shadowRebuild[3]; // do we need to rebuild our shadow map?
  vec3              shadowLA[3];      // remembering our lookat point for our shadow map
  texturemap *      shadowMap[3];     // shadowmaps for this light
  mat4              shadowMat[3];     // view-projection matrices for this light
} lightSource;
Note that as we're not yet dealing with anything but our sun as a lightsource I'm not putting any code in yet to support a flexible number of shadow maps.

So we now have 3 shadow maps and three shadow matrices to go with them. There is also a set of flags that determine if shadow maps need to be rebuild and a set of look at coordinates that we can use to check if we've moved our camera enough in order to need to rebuild our shadow maps.

It is important to realise at this point that this won't be enough once we start moving objects around. The easiest is to update our rebuild flags but we may as well remove this all together once things start moving around. A better solution would be to render our shadow maps with all the static objects only, and either overlay or add in objects that move around as we render our scenes. Thats something for much later however.

Similarly our shader library is enhanced to support the 3 shadow maps as well:
...
// structure for encapsulating a shader, note that not all ids need to be present (would be logical to call this struct shader but it's already used in some of the support libraries...)
typedef struct shaderInfo {
  ...
  GLint   shadowMapId[3];           // ID of our shadow maps
  GLint   shadowMatId[3];           // ID for our shadow matrices
  ...
} shaderInfo;
...
void shaderSetProgram(shaderInfo * pShader, GLuint pProgram) {
  ...
  for (i = 0; i < 3; i++) {
    sprintf(uName, "shadowMap[%d]", i);
    pShader->shadowMapId[i] = glGetUniformLocation(pShader->program, uName);
    if (pShader->shadowMapId[i] < 0) {
      errorlog(pShader->shadowMapId[i], "Unknown uniform %s:%s", pShader->name, uName);
    };
    sprintf(uName, "shadowMat[%d]", i);
    pShader->shadowMatId[i] = glGetUniformLocation(pShader->program, uName);
    if (pShader->shadowMatId[i] < 0) {
      errorlog(pShader->shadowMatId[i], "Unknown uniform %s:%s", pShader->name, uName);
    };
  };
  ...

And we need a similar change to our materials library to inform our shaders of the 3 shadow maps:
...
bool matSelectProgram(material * pMat, shaderMatrices * pMatrices, lightSource * pLight) {
  ...
  for (i = 0; i < 3; i++) {
    if (pMat->matShader->shadowMapId[i] >= 0) {
      glActiveTexture(GL_TEXTURE0 + texture);
      if (pLight->shadowMap[i] == NULL) {
        glBindTexture(GL_TEXTURE_2D, 0);      
      } else {
        glBindTexture(GL_TEXTURE_2D, pLight->shadowMap[i]->textureId);
      }
      glUniform1i(pMat->matShader->shadowMapId[i], texture); 
      texture++;   
    };
    if (pMat->matShader->shadowMatId[i] >= 0) {
      glUniformMatrix4fv(pMat->matShader->shadowMatId[i], 1, false, (const GLfloat *) pLight->shadowMat[i].m);
    };
  };
  ...
These changes should all be pretty straight forward so far.

Rendering our 3 shadow maps


Rendering 3 maps instead of 1 is simply a matter of calling our shadow map code 3 times. For this to work I've changed our renderShadowMapForSun function so I can parse it parameters to let it know which shadow map we're rendering and at what level of detail we want it. I'm just adding the start of the code here as most of the function has stayed the same from our first part. Have a look at the full source on github to see that other changes needed:
...
// render our shadow map
// we'll place this in our engine.h for now but we'll soon make this part of our lighting library
void renderShadowMapForSun(bool * pRebuild, texturemap * pShadowMap, vec3 * pLookat, mat4 * pShadowMat, int pResolution, float pSize) {
  vec3 newLookat;

  // prevent rebuilds if we only move a tiny bit....
  newLookat.x = camera_eye.x - fmod(camera_eye.x, pSize/100.0);
  newLookat.y = camera_eye.y - fmod(camera_eye.x, pSize/100.0);
  newLookat.z = camera_eye.z - fmod(camera_eye.x, pSize/100.0);

  if ((pLookat->x != newLookat.x) || (pLookat->y != newLookat.y) || (pLookat->z != newLookat.z)) {
    vec3Copy(pLookat, &newLookat);
    *pRebuild = true;
  };

  // we'll initialize a shadow map for our sun
  if (*pRebuild == false) {
    // reuse it as is...
  } else if (tmapRenderToShadowMap(pShadowMap, pResolution, pResolution)) {
  ...
I'm highlighting this part of the code because it shows the changes we made to limit the number of times we rebuild our shadow maps. We round our lookat position based on the level of detail we want in our shadow map. For our closest shadow map we may only move our camera 15 units before we need to rebuild our shadow maps while for our higher detail map it will be 100 units. Obviously if our light position changes we set our rebuild flags to true and we rebuild all shadow maps.

Finally we need to call this method 3 times which we do in our engineRender method:
  ...
  if (pMode != 2) {
    renderShadowMapForSun(&sun.shadowRebuild[0], sun.shadowMap[0], &sun.shadowLA[0], &sun.shadowMat[0], 4096, 1500);
    renderShadowMapForSun(&sun.shadowRebuild[1], sun.shadowMap[1], &sun.shadowLA[1], &sun.shadowMat[1], 4096, 3000);
    renderShadowMapForSun(&sun.shadowRebuild[2], sun.shadowMap[2], &sun.shadowLA[2], &sun.shadowMat[2], 2048, 10000);
  };
  ...
So our highest quality shadow map is a 4096x4096 map that covers an area of 3000x3000 units (2*1500).
Our lowest quality shadow map is a 2048x2048 map that covers an area of 20000x20000 units.

Note that this is where the color coded rendering of the shadow maps does come in handy for tweaking what works well as the size of our maps depend a lot on the sizes of your objects and what you consider to be close or far.

Changing our shaders

The final ingredient is changing our shaders. Again at this stage we need to update all our shaders but I'm only going to look at the changes once.

In our vertex shader (and in our tessellation evaluation shader for our terrain) we now need to calculate 3 vertices projected for our shadow maps. In this case I'm fully writing them out as its faster then looping:
...
uniform mat4      shadowMat[3];   // our shadows view-projection matrix
...
// shadow map
out vec4          Vs[3];          // our shadow map coordinates

void main(void) {
  ...
  // our shadow map coordinates
  Vs[0] = shadowMat[0] * model * V;
  Vs[1] = shadowMat[1] * model * V;
  Vs[2] = shadowMat[2] * model * V;

  ...
Our fragment shaders need to be adjusted as well. First off we need to change our samplePCF function so it checks a specific shadow map:
...
uniform sampler2D shadowMap[3];                     // our shadow map
in vec4           Vs[3];                            // our shadow map coordinates
...
float samplePCF(float pZ, vec2 pCoords, int pMap, int pSamples) {
  float bias = 0.0000005; // our bias
  float result = 1.0; // our result
  float deduct = 0.8 / float(pSamples); // deduct if we're in shadow

  for (int i = 0; i < pSamples; i++) {
    float Depth = texture(shadowMap[pMap], pCoords + offsets[i]).x;
    if (pZ - bias > Depth) {
      result -= deduct;
    };  
  };
    
  return result;
}
...
And finally we need to change our shadow function to figure out which shadow map to use.

We simply start with our highest quality shadow map and if our projection coordinates are within bounds we use it, else we check a level up:
...
// check if we're in shadow..
float shadow(vec4 pVs0, vec4 pVs1, vec4 pVs2) {
  float factor;
  
  vec3 Proj = pVs0.xyz / pVs0.w;
  if ((abs(Proj.x) < 0.99) && (abs(Proj.y) < 0.99) && (abs(Proj.z) < 0.99)) {
    // bring it into the range of 0.0 to 1.0 instead of -1.0 to 1.0
    factor = samplePCF(0.5 * Proj.z + 0.5, vec2(0.5 * Proj.x + 0.5, 0.5 * Proj.y + 0.5), 0, 9);
  } else {
    vec3 Proj = pVs1.xyz / pVs1.w;
    if ((abs(Proj.x) < 0.99) && (abs(Proj.y) < 0.99) && (abs(Proj.z) < 0.99)) {
      // bring it into the range of 0.0 to 1.0 instead of -1.0 to 1.0
      factor = samplePCF(0.5 * Proj.z + 0.5, vec2(0.5 * Proj.x + 0.5, 0.5 * Proj.y + 0.5), 1, 4);
    } else {
      vec3 Proj = pVs2.xyz / pVs2.w;
      if ((abs(Proj.x) < 0.99) && (abs(Proj.y) < 0.99) && (abs(Proj.z) < 0.99)) {
        // bring it into the range of 0.0 to 1.0 instead of -1.0 to 1.0
        factor = samplePCF(0.5 * Proj.z + 0.5, vec2(0.5 * Proj.x + 0.5, 0.5 * Proj.y + 0.5), 2, 1);
      } else {
        factor = 1.0;
      };
    };
  };

  return factor;
}

void main() {
  ...
  // Check our shadow map
  float shadowFactor = shadow(Vs[0], Vs[1], Vs[2]);
  ...
And that's it.

For this part I've created a Tag in Github instead of a branch. We'll see which works better.
Download the source code

And a quick video showing the end result:


What's next


I think I've gone as far as I want with shadows for now. The next part may take while before I get it done as there is a lot involved rewriting our code so far to a deferred lighting model but that's what we'll be doing next.

After that we'll start looking at adding additional lights and looking at other shading techniques.
Somewhere in the middle we'll also start looking at adding a simple pre-processor to our shaders so we can start reusing some code and make our shaders easier to put together.


No comments:

Post a Comment