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