Part of me wishes I hadn't. It isn't that shadows are difficult but in the state our engine currently is in, we're duplicating a few things. I really need to find time to add a pre-processor into our shader loading code. But we'll make do. Note that for our write up I'll only do things once so where code currently needs to be duplicated, have a look at the finished source code.
Rendering shadows requires knowing whether there is anything between the surface you are rendering and the lightsource that illuminates it.
It gets increasingly complex when more light sources are involved though that is something I won't get into now.
Shadowmaps are a bit of a cheat to allow us to quickly find out if light is being blocked out by another object. With a shadow map we render our scene from the perspective of the light source. As we render our scene the Z-buffer builds up and will eventually paint a picture of what are the closest objects that block out our light.
When we render our real scene besides projecting our vertices to screen space, we also project them using the same mvp we used when rendering it to the shadow map. That allows us to check the Z value for each fragment against our shadow map. If it's larger, we're behind something and we're thus in shadow.
This does require us to render our scene twice (or more if we have more light sources). This adds overhead but we've got a few things going for us:
- we're only interested in our depth buffer, so we can create very simple and quick shaders that do as little calculations as possible
- we can be more conservative with what we render, for spotlights we often need to only render a fragment of our scene, only for our sunlight we include a lot
- we may not need to render everything, for instance it makes little sense to render our terrain into our shadow map, nothings beneath our terrain so there is nothing for it to cast its shadow on.
- when stereo rendering we can reuse our shadow maps for both eyes, we don't need to render them twice
We're just going to render a shadow map for our sunlight. Because the sun is very very far away and light rays hit our surfaces pretty much parallel we're going to use an orthographical projection for this.
When we'll eventually add spotlights we'll use a perspective projection to create proper shadows.
Creating our shadow map
Now here one of our previous posts comes in very handy. For our shadow map we're going to render to texture, it's just that our texture is a depth buffer :)So we start by adding a handy function for this to our texture map library that is a simplified version of our render to texture function we added in our LOD tutorial:
// Prepare our texture as a shadow map (if needed) and makes our // shadow map frame buffer active bool tmapRenderToShadowMap(texturemap * pTMap, int pWidth, int pHeight) { if (pTMap == NULL) { return false; }; // check if we can reuse what we have... if ((pTMap->width != pWidth) || (pTMap->height != pHeight)) { // chuck our current frame buffer JIC. tmapFreeFrameBuffers(pTMap); };Note that we'll decide to rebuild our shadow map if its size changes. Not something we'll use today but it can be handy sometimes. We just need to be conservative as rebuilding our buffers will introduce a fair amount of overhead.
// create our frame buffer if we haven't already if (pTMap->frameBufferId == 0) { GLenum status; pTMap->filter = GL_LINEAR; pTMap->wrap = GL_CLAMP; pTMap->width = pWidth; pTMap->height = pHeight;Obviously we're assuming no texture is loaded so we set our values as we need them to be.
glGenFramebuffers(1, &pTMap->frameBufferId); glBindFramebuffer(GL_FRAMEBUFFER, pTMap->frameBufferId); // init our depth buffer glBindTexture(GL_TEXTURE_2D, pTMap->textureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, pTMap->width, pTMap->height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pTMap->filter); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, pTMap->filter); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, pTMap->wrap); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, pTMap->wrap); // bind our depth texture to our frame buffer glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, pTMap->textureId, 0);So this bit is nearly identical to how we created our frame buffer and added our Z-buffer in our render to texture example. The difference is that we're not adding any color buffers. I'm also using our textureId as we've already generated a texture object when we construct our object and it seems wasteful to create a second one just to use our depth buffer.
// and make sure our framebuffer knows we draw nothing else... glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE);Now here is a bit of magic, these two commands ensure our frame buffer knows there are no color buffers to write to or read from. We're just writing to our Z-buffer.
// and check if all went well status = glCheckFramebufferStatus(GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) { errorlog(status, "Couldn't init framebuffer (errno = %i)", status); tmapFreeFrameBuffers(pTMap); return false; } else { errorlog(0, "Created shadow map %i,%i", pWidth, pHeight); }; } else { // reactivate our framebuffer glBindFramebuffer(GL_FRAMEBUFFER, pTMap->frameBufferId); }; return true; };And the last bit again is the same as our render to texture function, we check if we've successfully created our frame buffer and reuse our frame buffer next time we call our function.
Our shadow shaders
As I mentioned we need simplified shaders to render our objects to our shadow maps. We have one vertex shader and two fragment shaders. We need an extra fragment shader to deal with texture maps that have an alpha such as those that we use for our leaves or our leaves would cast square shadows. We don't want that overhead if we don't need it.Here is our vertex shader:
#version 330 layout (location=0) in vec3 positions; layout (location=2) in vec2 texcoords; uniform mat4 mvp; // our model-view-projection matrix out vec2 T; // coordinates for this fragment within our texture map void main(void) { // load up our values vec4 V = vec4(positions, 1.0); T = texcoords; // our on screen position by applying our model-view-projection matrix gl_Position = mvp * V; }And our normal fragment shader:
#version 330 out vec4 fragcolor; void main() { // this does nothing, we're only interested in our Z fragcolor = vec4(1.0, 1.0, 1.0, 1.0); }And our texture shadow shader:
#version 330 uniform sampler2D textureMap; // our texture map in vec2 T; // coordinates for this fragment within our texture map out vec4 fragcolor; void main() { fragcolor = texture(textureMap, T); if (fragcolor.a < 0.2) { discard; }; }By now these should be pretty self explanatory. Even though we do output a fragment color that output is ignored
Finally in our load_shaders we actually load these shaders:
solidShadow = newShader("solidshadow", "shadow.vs", NULL, NULL, NULL, "solidshadow.fs"); textureShadow = newShader("textureshadow", "shadow.vs", NULL, NULL, NULL, "textureshadow.fs");It starts to get interesting once we start using our shaders. I've modified our material library to record both the normal shader and the shadow shader for each material. If no shadow shaders is set the material doesn't cast a shadow. For this to work we've added two functions to our material library:
- matSetShadowShader assigns a shadow shader to our material
- matSelectShadow selects that shader and set it up
Finally we assign our shadow shaders to our materials in our load_objects function. Note that I have moved a few things around where I didn't want materials to get a shadow shader:)
... // assign shaders to our materials lnode = materials->first; while (lnode != NULL) { mat = (material * ) lnode->data; // assign both solid and shadow shaders, note that our shadow shader will be ignored for transparent shadows if (mat->reflectMap != NULL) { matSetShader(mat, reflectShader); matSetShadowShader(mat, solidShadow); } else if (mat->diffuseMap != NULL) { matSetShader(mat, texturedShader); matSetShadowShader(mat, textureShadow); } else { matSetShader(mat, colorShader); matSetShadowShader(mat, solidShadow); }; lnode = lnode->next; }; ...
Rendering our shadow map
Now it's time to render our shadow map. First I've enhanced our meshnode library and added a meshNodeShadowMap function to it that renders our node using the shadow shaders. It's a dumbed down version of our meshNodeRender function that only renders non transparent objects for which a shadow shader is available.// render suitable objects to a shadow map void meshNodeShadowMap(meshNode *pNode, shaderMatrices * pMatrices) { dynarray * meshesWithoutAlpha = newDynArray(sizeof(renderMesh)); mat4 model; int i; // prepare our array with things to render, we ignore meshes with alpha.... mat4Identity(&model); meshNodeBuildRenderList(pNode, &model, pMatrices, meshesWithoutAlpha, NULL); // we should sort our meshesWithoutAlpha list by material here and then only select our material // if we're switching material for (i = 0; i < meshesWithoutAlpha->numEntries; i++) { bool selected = true; renderMesh * render = dynArrayDataAtIndex(meshesWithoutAlpha, i); shdMatSetModel(pMatrices, &render->model); if (render->mesh->material != NULL) { selected = matSelectShadow(render->mesh->material, pMatrices); if (selected) { meshRender(render->mesh); }; }; }; dynArrayFree(meshesWithoutAlpha); };There are a few small tweaks to meshNodeBuildRenderList that allow for a NULL pointer to be used for the dynarrays and prevent rendering our bounding boxes to our depth buffer.
Now it's time to enhance our rendering loop. At the start of engineRender I've added this snippit of code:
// only render our shadow maps once per frame, we can reuse them if we're doing our right eye as well if (pMode != 2) { renderShadowMapForSun(); };It calls the renderShadowMapForSun function unless we're rendering our right eye (as we're reusing our left eyes map).
The renderShadowMapForSun function is where the magic happens, lets look at it in detail:
// 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() { // we'll initialize a 4096x4096 shadow map for our sun if (tmapRenderToShadowMap(sun.shadowMap, 4096, 4096)) { mat4 tmpmatrix; vec3 tmpvector; shaderMatrices matrices; GLint wasviewport[4]; // remember our current viewport glGetIntegerv(GL_VIEWPORT, &wasviewport[0]); // set our viewport glViewport(0, 0, 4096, 4096);So above we've called tmapRenderToShadowMap to create our shadowMap (first time round) and select our frame buffer. We then set our viewport to match. Now here I was a little surprised to find out the viewport is not bound to the framebuffer so this overwrites the viewport configuration we had set in our main.c code. We thus store this before hand.
We've created a 4096x4096 map which should provide us with enough detail to get started.
// clear our depth buffer glClear(GL_DEPTH_BUFFER_BIT); // enable and configure our backface culling, note that here we cull our front facing polygons // to minimize shading artifacts glEnable(GL_CULL_FACE); // enable culling glFrontFace(GL_CW); // clockwise glCullFace(GL_FRONT); // frontface culling // enable our depth test glEnable(GL_DEPTH_TEST); // disable alpha blending glDisable(GL_BLEND); // solid polygons glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);This should look pretty familiar, we clear our depth buffer, enable what we need to but there is one strange little tidbit here. We're culling our front faces instead of our back faces.
Assuming our objects are all solid this prevents objects to throw shadows onto themselves.
// need to create our projection matrix first // for our sun we need an orthographic projection as rays of sunlight pretty much are parallel to each other. // if this was a spotlight a perspective projection gives the best result mat4Identity(&tmpmatrix); mat4Ortho(&tmpmatrix, -10000.0, 10000.0, -10000.0, 10000.0, -50000.0, 50000.0); shdMatSetProjection(&matrices, &tmpmatrix);As mentioned, we use an orthographic projection for our sun. Note that our near place is -50000. Our orthographic projection maps our Z buffer to -1.0 => 1.0, if we set our near plane to 0 any objects between our "eye" and halfway through our scene would fall behind the clipping point. Oops.
The our map spans is important. The larger our area the further down in the scene we'll be able to render shadows but at the cost of precision. Our map spans an area of 20000x20000 which is enough for our scene to get shadows far away enough without sacrificing too much precision but you'll see it isn't perfect. We'll be looking at way to improve this in the next two posts.
// We are going to adjust our sun's position based on our camera position. // We position the sun such that our camera location would be at Z = 0. // Our near plane is actually behind our 'sun' which gives us some wiggleroom. vec3Copy(&sun.adjPosition, &sun.position); vec3Normalise(&sun.adjPosition); // normalize our sun position vector vec3Mult(&sun.adjPosition, 10000.0); // move the sun far enough away vec3Add(&sun.adjPosition, &camera_eye); // position in relation to our cameraWe readjust our position of our sun so it's not too far away as it becomes the position of our camera.
// Now we can create our view matrix, here we use a lookat matrix from our sun looking towards our camera position. // There is an argument to use our lookat point instead as in worst case scenarios half our of shadowmap could // relate to what is behind our camera but using our lookat point risks not covering enough with our shadowmap. // // Note that for our 'up-vector' we're using an Z-axis aligned vector. This is because our sun will be straight // up at noon and we'd get an unusable view matrix. An Z-axis aligned vector assumes that our sun goes from east // to west along the X/Y axis and the Z of our sun will be 0. Our 'up-vector' thus points due north (or south // depending on your definition). // If you do not align your coordinate system to a compass you'll have to calculate an up-vector that points to your // north or south mat4Identity(&tmpmatrix); mat4LookAt(&tmpmatrix, &sun.adjPosition, &camera_eye, vec3Set(&tmpvector, 0.0, 0.0, 1.0)); shdMatSetView(&matrices, &tmpmatrix);And we use our good old mat4LookAt function to set our view matrix. I won't repeat what I mention about the up-vector in the comments in the code, just read them:)
// now we override our eye position to be at our camera position, this is important for our LOD calculations shdMatSetEyePos(&matrices, &camera_eye);This is an important small change we added to our matrices object. We can override our eye position which is important here because our LOD calculations would otherwise be incorrect.
// now remember our view-projection matrix, we need it later on when rendering our scene mat4Copy(&sun.shadowMat, shdMatGetViewProjection(&matrices));We also need to remember our view-projection matrix because we need it later on when rendering our scene.
// and now render our scene for shadow maps (note that we only render materials that have a shadow shader and we ignore transparent objects) if (scene != NULL) { meshNodeShadowMap(scene, &matrices); };Last but not least, we call our meshNodeShadowMap render function
// and output back to screen glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(wasviewport[0],wasviewport[1],wasviewport[2],wasviewport[3]); }; };And as part of our cleanup we reset our viewport back to what it was before. At the end of this we have our shadow map, but we're not using it yet.
Applying our shadows
Now we're ready to actually cast some shadows in our end result. This has become fairly simple at this point in time. First off we need to make sure our shaders know what shadowmap to use and what our shadow view-projection matrix is. Luckily these are both stored in our lightSource structure so we simply need to add a small code fragment to matSelectProgram:
... if (pMat->matShader->shadowMapId >= 0) { glActiveTexture(GL_TEXTURE0 + texture); if (pLight->shadowMap == NULL) { glBindTexture(GL_TEXTURE_2D, 0); } else { glBindTexture(GL_TEXTURE_2D, pLight->shadowMap->textureId); } glUniform1i(pMat->matShader->shadowMapId, texture); texture++; }; if (pMat->matShader->shadowMatId >= 0) { glUniformMatrix4fv(pMat->matShader->shadowMatId, 1, false, (const GLfloat *) pLight->shadowMat.m); }; ...
So now we need to update our shaders. Now these changes at this point in time need to be applied to multiple shaders. I've added them to our terrain shader, our flatshader, our textured shader and our reflection shader. We'll only discuss the changes to our textured shader here.
First we start with our standard.vs vertex shader, I'll just highlight the changes:
... uniform mat4 shadowMat; // our shadows view-projection matrix ... // shadow map out vec4 Vs; // our shadow map coordinates void main(void) { ... // our shadow map coordinates Vs = shadowMat * model * V; ... }So we've added our shadow view-projection matrix as a uniform and added an output called Vs. Then we calculate Vs by projecting our vertex position.
In our fragment shaders we add a new uniform for our shadowMap and an input for Vs at the start:
uniform sampler2D shadowMap; // our shadow map in vec4 Vs; // our shadow map coordinatesAfter that we add two helper functions that use our Vs input to perform our lookup in our shadowMap and return a factor between 0.0 (fully in shadow) and 1.0 (not in shadow). That seems a bit like overkill right now but in part two of this write up we'll expand on this:
// sample our shadow map float sampleShadowMap(float pZ, vec2 pCoords) { float bias = 0.00005; float depth = texture(shadowMap, pCoords).x; if (pZ - bias > depth) { return 0.0; } else { return 1.0; }; } // check if we're in shadow.. float shadow(vec4 pVs) { float factor; vec3 Proj = pVs.xyz / pVs.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 = sampleShadowMap(0.5 * Proj.z + 0.5, vec2(0.5 * Proj.x + 0.5, 0.5 * Proj.y + 0.5)); } else { factor = 1.0; }; return factor; }And in our main function we'll call shadow to obtain our shadow factor and apply it:
void main() { ... // Check our shadow map float shadowFactor = shadow(Vs); ... // and calculate our color after lighting is applied vec3 diffuseColor = fragcolor.rgb * lightcol * (1.0 - ambient) * NdotL * shadowFactor; ... specColor = lightcol * matSpecColor * specPower * shadowFactor; ... }Note how we simply add our shadow factor into our diffuse and specular colour calculation.
And we have shadows:
Download the source here
What's next?
This is only a start. When you move the camera around you'll see we've got plenty of things that need to improve. Very simply put, we don't have enough detail in our shadow map. We're also sacrificing half our shadow map as part of the shadow maps relates to shadows that are behind our camera.
In the next part we'll start looking at "Percentage Close Filtering" which will be a small post on smoothing out our shadow maps. We'll also look at ways to improve our projection matrix so we sacrifice less detail.
After that we'll look at cascaded shadow maps, we basically render more then 1 shadow map for our light source so we can use a higher detail one for shadows that are close to our camera.
No comments:
Post a Comment