teaching machines

CS 455 Lecture 19 – Skyboxes

April 21, 2015 by . Filed under cs455, lectures, spring 2015.

Agenda

TODO

As a lab exercise:

  1. Sync with the class repo to get some tweaks to the Texture and ObjUtilities classes, a skybox starter project, a box model, and some skybox textures.
  2. Build and run skybox to make sure it works. You should see a heightmap with a solid blue background.
  3. In OnInitialize, load the skybox geometry in the models directory into attributes[1]. I’ve provided a convenience method ObjUtilities::Read(path) that returns a pointer to a VertexAttributes instance. Just feed it  MODELS_DIR "/skybox.obj".
  4. Create the skybox shader in shader_programs[1] using the vanilla shaders in the project directory:
    shader_programs[1] = ShaderProgram::FromFiles(SHADERS_DIR "/skybox.v.glsl", SHADERS_DIR "/skybox.f.glsl");
  5. Marry attributes[1] and shader_programs[1] into skybox, a VertexArray instance variable.
  6. In OnSizeChanged, uncomment the code to upload the projection matrix to shader_programs[1].
  7. In OnDraw, change the #if 0 to #if 1 to start drawing the skybox geometry. Run and locate the white cube in the scene.
  8. The next thing we want to do is make that cube follow the camera around wherever it goes. To do this, we must translate it from its model space position around (0, 0, 0) to a world space position around the camera. In particular, we want to translate it to the camera’s from position. Alter OnDraw to upload an appropriate modelview transformation to the shader.
  9. Now we texture the cube. We’ll need some texture coordinates. The cubemap texturing hardware expects a three-dimensional texture coordinates. The strongest component selects the face, and the other two select the pixel within the face. For example if the y-coordinate is 0.9, then the color will be pulled from the top texture. The xz-coordinates will identify the pixel within this top texture.

    So, where do we get these texture coordinates? Since the cube is centered around the origin in model space, the vertex positions double as vectors pointing at the cube. These positions are our texture coordinates. To get them in the fragment shader, create a varying and simply assign it the modelspace vertex position. The position will be interpolated across the faces and become input to the fragment shader. In the fragment shader, set gl_FragColor to this texture coordinate. We should see red on the right, green above, and blue behind us.

  10. Let’s add a texture. We start by loading in six images. You are encouraged to locate your own cubemap skyboxes (check out http://www.custommapmakers.org/skyboxes.php), but for now let’s use models/hell_*.ppm. All six must be squares and have the same resolution. Load them in OnInitialize with:
    #define SKYBOX "hell"
      Image *images[] = {
        new Image(MODELS_DIR "/" SKYBOX "_left.ppm"),
        new Image(MODELS_DIR "/" SKYBOX "_right.ppm"),
        new Image(MODELS_DIR "/" SKYBOX "_bottom.ppm"),
        new Image(MODELS_DIR "/" SKYBOX "_top.ppm"),
        new Image(MODELS_DIR "/" SKYBOX "_back.ppm"),
        new Image(MODELS_DIR "/" SKYBOX "_front.ppm")
      };
  11. Create a new texture object in the skybox_texture instance variable. Call its Upload method and pass the texture resolution and pixel data.
  12. Add a samplerCube uniform to the fragment shader. In OnDraw, bind the skybox texture to this uniform.
  13. Perform a texture lookup in the fragment shader using your uniform and texture coordinate. The textureCube function will invoke the cube mapping lookup hardware:
    vec3 rgb = textureCube(skybox_texture, ftexcoord).rgb;
  14. Run your program. How’s it look? First off, you might notice some seams. We can reduce their visibility with the following two commands:
    skybox_texture->Wrap(Texture::CLAMP_TO_EDGE);
    glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
  15. In the vertex shader, I temporarily multiplied the cube’s model coordinates by 50 so that we could locate it in the scene. Remove this. Run it again. What happens?
  16. We want the cube to appear behind everything else that’s been drawn. We could find a multiplier that’s as big as our scene, but there’s a simpler trick that requires less calculation. Given the perspective divide that is built in to the hardware, we know that vertex’s normalized device coordinate will be ( x/w, y/w, z/w). If z/w is 1, it will have the greatest depth possible. So, let’s make sure that z/w is 1 by hacking z in the vertex shader:
    gl_Position.z = gl_Position.w;
  17. Now how’s it look?