CS 315 Homework 6 - Under a Street Lamp (Part 2)

Due Tues Nov 11 at 11:59pm

Overview

This assignment is a continuation of the last, as you will add further details to your street lamp scene. In particular, you will add textured objects and utilized advanced texturing techniques--specifically, shadow mapping.

You will be adding the following components to your rendered scene:

  1. A texture-mapped object that is lit by your street lamp. This can be the "ground" you established in the previous homework (e.g., to make it look like a street or cobblestones), or can be an entirely separate object. Or both!
  2. A texture-mapped object to act as the "background" for your scene.
  3. Objects in your scene will cast shadows when lit by (only one of the sun or the lamp needs to produce shadows).
    • This means that your objects will need to be positioned so that there are shadows--e.g., one object needs to block another's path to the light.

The first two of these tasks are fairly straight-forward, though see Part 1 for some details. The third task is much more involved, but I'll provide you with lots of guidance!

This assignment should be completed individually.

Objectives

Necessary Files

You will need to have a working copy of your previous homework (which should be complete). I recommend you take a short amount of time to "clean up" your code: refactor methods, make sure variables are named sensibly, etc. This will make it easier to modify your work!

You will need to download the cs315-hwk6.zip file which contains a updated pass.vert vertex shader (that will give you some support for texture and shadow mapping), as well as a shadow.frag fragment shader that you can use for doing shadow mapping. See below for details.

You will also probably want a copy of the lecture demo-code (for working with textures and buffers) on hand.

Assignment Details

Texturing Meshes

The easiest part of this assignment is to add texture to a model. Again, this can be the "ground" you established in the previous homework (e.g., to make it look like a street or cobblestones), or can be an entirely separate object. Or both!

Adding Shadows

A demonstration of shadow mapping

The hard part of this assignment will be to add shadows. This will involve working with multiple shaders, multiple framebuffers, and multiple renderings. But by the end, you will have mastered the shader pipeline used by OpenGL!

The basic idea: you will modify your program to perform multi-pass rendering: you'll render the scene once from the perspective of the light in order to determine which objects are in front of which, store that information in a texture buffer, then re-render the scene from the camera's utilizing the data in that buffer to figure out which fragments should be drawn in shadow.

There are a lot of different steps, detailed below:

  1. Shadow mapping involves two rendering passes: one to determine the depth of each fragment from the light, and one to draw the fragments. Each one will require a different kind of shading. Thus your program will need to have two different shader programs. ((While it is possible to do this in one program using sentinal uniforms, you should use two programs for practice!))
    • You'll need to create a second shaderProgram (e.g., shadowProgram). This program will need its own call to ShaderUtils.initShaders(), using the existing pass.vert and the provided shadow.frag.
      • Notice how we can mix and match shaders!
    • Your second program will also need its own set of handles (though you won't need all of the ones used by your primary Phong/texture shader!)
    • You can switch which shader is currently being used with the gl.useProgram() command.
      • HELPFUL HINT: I recommend refactoring your original shaderProgram when you reference it in the init() method and renaming it (e.g., phongProgram). Then you can assign whichever program you are currently using to the "shaderProgram" variable, and be able to call your drawMesh() method without making any changes!
      • Thus your application can "change" shaders via:
        gl.useProgram(shadowProgram);
        shaderProgram = shadowProgram;
  2. The shadow.frag and new pass.vert shaders will require one additional uniform to be passed in: mat4 uLightMVPMatrix. This mat4 is the model-view-projection matrix representing the scene as rendered from the point of view of the light!
    • The "model" part of this matrix is your object's model matrix, as normal.
    • The "view" part of the matrix should be a view matrix with the "eye" at the position of the light source (pick either your sun or your lamp). You can use the mat4.lookAt() function from glMatrix.js to generate this matrix.
    • The "projection" matrix can just be the same one you use in your normal rendering. However, if you're casting shadows from the sun, you may want to use an orthogonal projection instead, e.g.:
      mat4.ortho(mat4.create(), -10,10, -10,10, 2,30); //gave me good results

    You'll need to pass this value to your shader at the start of your rendering method. Note that the uniform will need its own handle (in both shaders!)

    • Remember: you are only required to create shadows from one of the sun OR the lamp. If you have the sun cast shadows, remember to update this variable every frame when the sun moves!
    • Once this is in place, you can test that everything still works by simply setting your application to use the shadowProgram and then seeing how things look. Note that the shadow depth shader will produce seemingly random colors, but you should be able to see the general structure of your objects!
  3. Next is the first tricky part: you'll need to set up your application so that instead of rendering to the normal framebuffer, you can instead render to a separate framebuffer whose output is stored in a texture. This process is called rendering to a texture, and is used for creating effects like mirrors, shadows, and animations that appear on virtual screens (e.g., if your scene showed a TV with a scene on it!)
    • This process is detailed in the textbook in section 7.12 (pg 378); or you can see the sample code (html). You can also check out this tutorial (or one of many others) for further information. Overall, the steps you will need to perform are (each taking possibly a few lines of code/method calls):
      1. Create and bind a framebuffer
      2. Create a texture to render to (e.g., shadowTexture), with appropriate parameters (and mipmaps). This is just like what you did when loading images as textures!
      3. Create and bind a render buffer
      4. Specify that the framebuffer should use your shadowTexture as a texture as a buffer for the output color
      5. Bind the null renderbuffer and framebuffer (so that you output to the screen again)
    • Note that you will need to do this once at the start of your program (e.g., from your init() method).
    • This setup is hard to test, though you can check that the buffer is "complete" and that everything still loads and renders!
  4. Now you have to do the next tricky part: set up your program to do two render passes: the first time through you should render from the point of view of the light (using the depth shader, and rendering to your texture-based framebuffer), and the second time you will render from the camera's view (using the phong shader, and rendering to the screen's framebuffer).
    • I recommend making a new method (e.g., multiPassRender()) that can encapsulate this process. You'll need to move your empty-mesh checking and animations into here. Think of it as the "master" rendering method.
      • Your render() method will continue to mean "render the scene" (with the current view and shader)--you could even rename it as such!
    • Your multi-pass rendering should proceed as follows:
      1. Set the viewMatrix so that the camera position is at the light's position (be sure and remember the old camera position so you can get back to it!)
      2. Bind the off-screen framebuffer
      3. Use the shadowProgram shader program
      4. Call render() to render your scene to the current buffer with the current shader
      5. Regenerate the mipmaps for your shadowTexture (bind it, call gl.generateMipmap, then unbind it).
      6. Unbind the off-screen framebuffer (by binding the null buffer instead)
      7. Use your regular shader program
      8. Set the viewMatrix so that the camera position is at the camera's normal position
      9. Call render() to render your scene to the current buffer with the current shader
  5. If all goes to plan... you should see absolutely nothing new. Your scene should render as normal. But you've also rendering a version of your scene to the shadowTexture using the shadowProgram!
    • The best way to test this is---instead of rendering your normal scene at the end---is to render a single rectangle with the shadowTexture texture-mapped onto it as if it were a .jpg you loaded! You should then see the rectangle showing a picture of your scene from an alternate view point! An example testing scene that I used is behind the spoiler (you may need to adjust some variable names):

      Show Test Render Method

        function renderTest()
        {
          gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //clear context
      
          gl.uniform3fv(shaderProgram.eyeDirectionHandle, [eye[0]-at[0], eye[1]-at[1], eye[2]-at[2]]); //calc eye direction
          gl.uniform3fv(shaderProgram.lightPosHandle, light); //light position
      
          //model transformations
          var model = mat4.create();
          var s = 10;
          mat4.translate(model,model,[0,0,-1*s]);
          mat4.scale(model, model, [s,s,s]);
          var mesh = meshes['plane'];
          drawMesh(mesh,model,true,shadowTexture); //draw the mesh with isTextured == true and shadowTexture as what to texture with
        }
    • Note that if you don't switch to the shadowProgram shader, you should simply see a mini version of your scene be rendered (from the point of view of the light); like you took a picture of it and drew that on the screen. If you do not get this, there are some errors somewhere!
  6. Finally, finally--you can add in the shadows! This will occur during your second rendering pass (using your regular shader).
    • You'll need to pass in the shadowTexture to your fragment shader so you can access it... so yes, you'll need another uniform and handle (and I recommend a second uniform for "isShadowed" so that you can turn it on and off for testing).
      • Remember to set and bind the shadowTexture to a texture unit! I recommend doing this in your multiPassRender() method before your final call to render the scene.
    • Now you can modify the shader to use this passed-in texture. There are a couple of steps:
      • You'll need to copy/paste the unpack() method found in the provided shadow.frag shader into your regular fragment shader (it goes above the main() method).
        • In order to store the 32-bits of depth information, we basically break out different pieces to "pack" them into each of the 8-bit components for a vec4. In order to recover this 32-bit float, we need to "unpack" the components of the vec4---a functionality provided by this method.
      • In order to apply a shadow, you'll need to get the position of the fragment in "light space" (e.g., relative to the light source). This value is passed in by the provided pass.vert shader as vLightProjPos ('varying light projection position').
        • However, this value is in homogenous coordinates (e.g., with a w != 1). You'll need to convert back to "actual" values by dividing the xyz components by the w component.
        • You'll then need to scale the x and y components so they range from 0 to 1, not from -1 to 1. You can find an example of how to do this in the shadow.frag shader.
      • Now that you know the xy location of the fragment (from the light's perspective), you can look up the depth of the minimum fragment at that location by sampling the shadow map! This sampling will give you a vec4, which you can then unpack to get the depth of the fragment closest to the light.
      • If the actual fragment has a distance greater than this value, then it should be in shadow. You can figure out the distance of the current fragment by looking at the z component of the fragments position (because in "light space", distance is measured on the z axis!)
      • If the actual fragment's distance is greater than the distance sampled from the shadow map, then that fragment is in shadow. A fragment in shadow should be colored black (or otherwise made significantly darker)

At this point you should have shadows cast from the light! Their may be artifacting due to their depth (which you can fix with a shadow bias). There are lots of options for producing cleaner, crisper shadows (as well as approximating "soft shadows"). See here and here for examples. Cleaning up the shadows is worth extra credit as an extension--but make sure you get the basics working first!

Extensions

There are plenty of options available as extra credit extensions (again, check with me first about any ideas not mentioned here):

Submitting

BEFORE YOU SUBMIT: make sure your code is fully functional! Grading will be based primarily on functionality. I cannot promise that I can easily find any errors in your code that keep it from running, so make sure it works.

Upload your entire project (including the assets/ and lib/ folders) to the Hwk6 submission folder on vhedwig (see the instructions if you need help). Finally, include a filled-in copy of the README.txt file in your project directory!

Important note: Be sure and modify the HTML file so that it the page includes sources for all models and textures you use, giving proper credit.

The homework is due at midnight on Tues Nov 11.

Grading

This assignment will be graded out of 20 points. Note that the only components of this assignment's grading that rely on the previous homework are the existance of your scene, the existence (and position) of a light source, and some basic phong shading.