Chapter 40. Shadows (with GitHub)

Another (maybe the most) critical component of a realistic image is SHADOWS.

The common approach is "shadow mapping". The algorithm consists of two passes. First, the scene is rendered from the point of view of the light source. Only the depth of each fragment/pixel is calculated. The scene is then rendered as usual, but with an extra test to see if the current pixel is in shadow. If corresponding depth value from shadow map is less than our current pixel's distance to light, it means that something was closer to light than our pixel, so the pixel IS in shadow.

So, we have:

  • 2 new shaders in C:\CPP\engine\dt\shaders: depthmap_v.txt and depthmap_f.txt
  • Other shaders are modified to perform a shadow depth test
  • Also now shaders are using layouts
  • New class Shadows.h/cpp
  • A bunch of classes were extended to handle these changes, such as: DrawJob, Material, Shader, ModelLoader, MaterialAdjust, TheGame
  • For debugging purposes, to see how shadow map looks like, I added 2 new classes UISubj and Coords2D (subfolder ui, "UI" is for "User Interface")

Again, since so many files are involved, instead of guiding through every change, here is a new repository:

https://github.com/bkantemir/_wg40

  • In case of any doubts how to use it, please see Chapter 37.2. GitHub

Result:

By the way, on Android looks even cooler. Sharper.


How it works

Initialization, Shadows::init(): Creating shadow map buffer, setting up "shadow camera", building shadow view-projection matrix.

Unlike in most tutorials, since I am planning to modify depth values, I am creating not just a depth buffer, but a texture (with pixels as a single 16-bit floats instead of usual RGBA) AND with a depth buffer attached to this texture. The key command here is:

glTexStorage2D(GL_TEXTURE_2D, 1, GL_R16F, pTex->size[0], pTex->size[1]);

In TheGame::drawFrame() we are doing 2 passes now:

  • First pass refers to GameSubj::renderDepthMap(..) where we're building a depth map, which will be passed later to other shaders as an extra texture for reference.
  • Second pass refers to normal rendering, GameSubj::renderStandard(..). Almost the same as before, but now we're passing to shaders 2 new uniforms, the depth map texture and MVP matrix for light view (which was used to draw the shadow map). The difference between calculated pixel's z-position and the value from depth map texture will tell us if pixel is shadowed or not.

The result we want to achieve is:

However, in real life it looks more like this:

The outcome looks kind of awful because of so called "self-shadow acne".

Solution is to move depth map values a bit OUT of light source (so called "depth bias"), for example by 1 pixel:

As we can see, such straight approach helps, but not completely. Moving values out even further, like by 3 pixels in this sample, creates another problem - so called "Peter panning effect", when shadow is visually "detached" from the object:

Solution is to modify the bias according to the slope. Stiffer the slope - bigger the bias:

We can estimate slope's stiffness by pixel normal's z-value. Smaller normal's z-value - stiffer the slope - bigger the bias.

In my code, in Shadows::init(), I pre-calculate a table of 16 biases for a range of normals z-values from 0.0 to 1.0.

In the depthmap_f shader I'm taking pixel's normal's z-value, multiplying and rounding it to convert it to an integer in 0 to 15 range, then using corresponding bias value from uDepthBias table.


Another important change here:

GLSL Shaders Layouts

"Layouts" is a relatively new feature in GLSL. Their purpose was a mystery to me, so I just didn't use them. Until now.

The problem:

When working on this chapter, I faced a situation: We have a model with it's DrawJobs, VBOs, assigned shaders and VAOs.

Just a reminder: despite its name, a "VAO" (Vertex Array Object) is an array of attributes (not vertices).

Each VAO is VBO+shader-specific. It means that VAO generated for rendering model can NOT be used for rendering shadow map (even over the same VBO), because shadow shader can have different (reduced) set of attributes (incoming data). Accordingly attributes IDs (numbers) in the shader can be different from what we have in initially generated VAO.
Theoretically, we could generate own VAO for each shader, which would make things much more complicated… But fortunately, we shouldn't.

Solution:

Shader Layout concept allows to assign specific layout numbers to each critical attribute/variable.
For example, like in my case:

  • layout (location = 0) in vec3 aPos; // position attribute (x,y,z)
  • layout (location = 1) in vec2 aTuv; //attribute TUV (texture coordinates)
  • layout (location = 2) in vec2 aTuv2; //attribute TUV2 (for normal map)
  • layout (location = 3) in vec3 aNormal; // normal attribute (x,y,z)

These numbers are used across all our 3 vertex shaders: phong_v.txt, nm_v.txt and depthmap_v.txt. So now we can use the same VAO with different shaders regardless of their variables order, being sure that aPos' location will always be 0, and aNormal's location will always be 3, and so on.


Leave a Reply

Your email address will not be published. Required fields are marked *