Shadow Mapping Shaders - Brian Beuken - 04-08-2018
Shadow Mapping requires 2 sets of shaders, one to gather the shadows and one to render them
The principles are explained in the Fundamentals book, so I won't go into them here, and the shaders are also available on downloadable demo's but I thought I'd put them here in isolation for you as well. These are optimized to run reasonably well on OpenGLES2.0 giving the client side (CPU) the responsibility to create the MVP for light and Models. Normally on a PC you'd let the Shader caclulate them, but here its better to let the CPU pass these as a uniform. (because even though PC is far slower to do the calculations, its a one time thing, but on a limited core GPU you don't gain the performance boost you would on a 512core unit)
1st thing to remember is that you need to do a gathering pass, where you render your height map into an FBO so you need to have an FBO 1st. This is easy to set up like so.
Code: // only used when creating dynamic shadows.
// Create and return a frame buffer object
GLuint Graphics::CreateShadowBuffer(int width, int height)
{
GLuint FBO, RenderBuffer;
glActiveTexture(GL_TEXTURE1); // use an unused texture
// make a texture the size of the screen
glGenTextures(1, &ShadowTexture);
glBindTexture(GL_TEXTURE_2D, ShadowTexture); // its bound to texture 1
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// once the texture is set up, we can turn to the Framebuffer
glGenFramebuffers(1, &FBO); // allocate a FrameBuffer
glGenRenderbuffers(1, &RenderBuffer); // allocate a render buffer
glBindRenderbuffer(GL_RENDERBUFFER, RenderBuffer); // bind it
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width,height); //give some parameters for it
glBindTexture(GL_TEXTURE_2D, ShadowTexture); // bind the texture
glBindFramebuffer(GL_FRAMEBUFFER, FBO); // and the frame buffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, RenderBuffer); // they are now attached
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ShadowTexture, 0); // and so is the texture that will be created
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
printf("oh bugger framebuffer not properly set up");
glActiveTexture(GL_TEXTURE0); // careful to not damage this
return FBO;
}
Height and width should be the same (512x512 or 1024*1024 work well), but you can have any size you want up to the max size of textures, but the bigger the better, though there is a speed penalty the bigger the buffer is. If you have a very large scene, it might be better to use a number of different lights and switch between them with different view areas to cover the space.
Now that you have your framebuffer, you only need to switch it on and clear it before running a gathering pass
Code: // if we are going to have shadows, we need to do a pass through all the objects and do the draw shadow routines for anything that casts
glBindFramebuffer(GL_FRAMEBUFFER, MyGraphics->ShadowFB);
glViewport(0, 0, ShadowBufferSize, ShadowBufferSize);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // clear
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
for (int i = 0; i < MyObjects.size(); i++)
{
MyObjects[i]->DrawShadow(); // most objects will do a dummy call
} // for
The DrawShadow method is a much more simplified drawing system, which has no need of textures or light effects, it only needs to do a simple draw with these shaders, 1st the vertex.
Code: #version 100
precision mediump float;
// simple shader to gather the depth, its a 1st pass shader designed to render into a bound FBO
attribute vec4 a_position;
uniform mat4 u_lightMVP; // light MVP is the casters Model, the lights view and Projection
varying vec4 v_texCoords; // we have to pass coords to the frag shader
void main()
{
gl_Position = u_lightMVP * a_position;
v_texCoords = gl_Position; // pass this to the fragment which can't access gl_Position
}
And now the fragment
Code: #version 100
precision mediump float;
varying vec4 v_texCoords;
/*
Generate shadow map
We need to gather the depth values, bring them into range
and then encode them into the x and y values of the
colour value we create
This shader should be linked with ShadowMap.vsh and used with a bound FBO
*/
void main()
{
float value = 10.0 - v_texCoords.z; // 10 is the most usual range
float num = floor(value);
float f = value - num; // get the floating point value
float vn = num * 0.1;
gl_FragColor = vec4(vn, f, 0.0, 1.0);
}
any model which casts a shadow needs to set up these shaders, and then simply run a simplified DrawShadow Method.
Once thats done, you will have a nice FBO filled with shadow info, actually height map info, and you can switch off your FBO and render back to screen with a simple
Code: // release the shadow framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glEnable(GL_DEPTH_TEST);
glCullFace(GL_BACK);
// Setup the viewport
glViewport(0, 0, MyGraphics->p_state->width, MyGraphics->p_state->height);
Now we're ready to render normally, Your Vertex Shader looks like this
Code: #version 100
precision mediump float;
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord;
uniform mat4 MVP; // our models MVP
uniform mat4 u_LightsMVP; // our lights MVP (using the objects model)
varying vec4 v_ShadowTexCoord; // the texture coords for the shadow
varying vec2 v_texCoord; // the usual texture coords for the texture
// we need to bring our values into range so this matrix will do that
const mat4 biasMatrix = mat4(0.5, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.5, 0.5, 0.5, 1.0);
void main()
{
// Calculate vertex position, seen from the light view point and Normalize texture coords from -1<>1
v_ShadowTexCoord = biasMatrix * (u_LightsMVP * a_position); // this result gets passed to the frag shader as its a varying
// do our normal position calculations
gl_Position = MVP * a_position; // use the models normal camera based MVP
// pass the texture coord to the frag and our lit flag
v_texCoord = a_texCoord;
}
and your fragment
Code: #version 100
precision mediump float;
/*
This is a base level system for adding shadows to your scene, it uses a simple
FBO as a texture to gain access to a shadow map.
The result is an effective but potentially low resolution hard shadow (depending on scene size and light spread)
Softer shadows can be done using PCF or other methods but there is a very significant
drop in performance on limited core GPU's and you need a better resolution to get good results, on phones/SBC's this may be too much.
*/
uniform sampler2D s_texture; // texture0 our normal diffuse texture
uniform sampler2D u_s2dShadowMap; // texture1 our FBO shadows
uniform vec4 Ambient; // not used yet
varying vec4 v_ShadowTexCoord; // the coordinates passed from the vertex shader
varying vec2 v_texCoord; // passed by our normal textures
void main()
{
float fLight = 1.0; // supply a transparent value as defaut
/* we need to get the packed light distance data from the shadow texture, texture1. */
vec2 Depth = texture2DProj(u_s2dShadowMap, v_ShadowTexCoord).xy;
// its in 2 values scaled down so resize them
float fDepth = (Depth.x * 10.0 + Depth.y);
// reconstitute the depth
float ActualDepth = (10.0-v_ShadowTexCoord.z) + 0.1 - fDepth ;
if (fDepth> 0.0 && ActualDepth <0.0)
{
fLight = 0.6; // set lower values for a darker shadow
}
gl_FragColor = texture2D( s_texture, v_texCoord )*fLight; // we can also use ambient and other light factors here
gl_FragColor.w = 1.0;
}
And thats it... Assuming you set up the light to an overhead location that is not too far away from the casters, and your draw routine provides the appropriate uniforms and attribute pointers you will get a nice set of shadows like this
Shadow mapping is an effective and relatively fast way to do shadows, but softer edge shadows are preferred, trouble is the Pi GPU and most other SBC GPU's will struggle to do too much of the sampling needed to do them, so for now this is probably the best option. But....there are ways to make it better, I'll let you explore them.
You will note that the shadow has rather chunky pixels, this can be improved with a decent sample system to give anti aliasing, but as noted you will find there is a massive speed hit for that.
Also a factor, is the resolution of our FBO, whatever size it is it will stretch over the view of our lights coverage of the terrain so a smaller FBO stretches further and has chunkier pixels.
Separate lights swapped as needed and focusing on areas that are close to the size of the texture will provide better results. Larger resolution FBO's are also good, but do come at a cost of speed.
Of course these shaders don't do any lighting or Lerping, which you will need to add if you want them, but remember, the more you make your shaders do, the slower they get.
This very simple shadow technique has its flaws, but it will work on pretty much any system. Culling systems to prevent rendering when not needed will also make a massive difference.
|