CMSC 23700 Lab 5: Shadow Mapping

Lab 5 Objectives

In this week's lab, you will learn about the mechanisms needed to perform shadow mapping in OpenGL, in particular:


Background

The Pproject 3 description gives background information about the math and rendering proccess assoicated with implementing shadow mapping. Please look over this information to make sure you fully understand before trying to implement it in lab

  1. Project 3 Description (Look at the "Shadow Mapping" section).

Framebuffers & Depth Buffers

As discussed at the beginning of lab and in the project 3 description, shadow mapping is done in two passes. In this section, we discuss mechanisms you will need in order to complete the first pass, which entails creating a depth buffer.

Note: the depth buffer that we render to in the first pass is called the shadow map.

The results of the rendering process end up in something which is called a framebuffer object (FBO). Inside this object, lives the color buffer (which is displayed on screen), the depth buffer, and a few other buffers for additional usages. The GLFW windowing system is responsible for creating and managing the default framebuffer, which cannot be deleted by OpenGL. In addition to the default framebuffer, an application can create FBOs of its own. These objects can be manipulated and used for various techniques under the control of the application. Once this object is created and configured properly we can change framebuffers by simply binding to a different FBO. Note that only the default framebuffer can be used to display something on the screen. The framebuffers created by the application can only be used for offscreen rendering (we'll see more examples of off-screen rendering in Project 4). This can be an intermediate rendering pass (e.g., our shadow mapping buffer) which can later be used for the actual rendering pass that goes to the screen.

Luckily for you, we have provided you with a class that handles creating a FBO to save depth values and writing these values into a depth texture, which you then can use during the second pass. This class lives inside the depth-buffer.hxx and depth-buffer.cxx files. Let us look at the three important definitions from the depth buffer class:

// 1 
  DepthBuffer (uint32_t winWidth, uint32_t winHeight);

// 2
  void BindForWriting ();

//3
  void BindForReading (GLenum texUnit);
  1. The constructor for the depth buffer allocates and initializes a frame-buffer object and a associated depth texture, given the window's width and height. Make sure to choose a width and height that is a power of 2. You may need to increase the width and height to the next power of two if the window dimensions are not a multiple of a power of 2.
  2. We will need to toggle between rendering into the depth buffer and rendering into the default framebuffer. This function makes the depth buffer be the current render target (i.e. framebuffer). We will call it before the first render pass.
  3. This function will be used before the second render pass to bind the shadow map for reading. This function takes the texture unit to which the shadow map will be bound. The texture unit index must be synchronized with the shader (since the shader has a sampler2D uniform variable to access the texture).

Depth Shader Program

During the first rendering pass, you'll need to create a shader program that saves the depth values (i.e. the z component) to the FBO's depth buffer. How should this work?


Part 1: Rendering the Cube and Floor

In this lab, you will need to draw the shadow of a cube that is placed on a plane (i.e., the floor). The lab4 directory provides and setups all the mesh information for both the floor and cube. The main difference from the previous labs is that both the floor and cube are sub-structs of the struct Drawable. You can think of Drawable as the base struct for all objects that can be rendered to the screen, similiar to various renderers you have created in the projects (e.g., FlatShadingRenderer, WireFrameRenderer, etc.,) are subclasses of the "Renderer" base class. The View's constructor setups the drawables (cube and floor) for you and places them in a std::vector filled with Drawables. The only thing you will need to do is call the Draw() function on a drawable when you are ready to render them (i.e., cube and floor). Make sure to take a look at drawable.hxx to fully understand what's inside one.

Note: Since the drawables live inside a std::vector, you will want to use iterators to look through the collection, just like you did in the projects when iterating through the scene objects. Make sure to use the begin() and end() functions to retrieve the beginning and end iterators.

Using the code from your project 2, try to render both the cube and floor. This is what it should look like:

screen.png

Here are some additional tips and the lighting values to setup lighting:


Part 2: Shadow Mapping the Cube

Here are some additional tips on getting shadow mapping working: