TRING
A Modified Carom Billiards Game
Dynamic Cube Mapping

We have implemented two levels of dynamicism using cube maps. No matter which level is chosen, the OpenGL implementation must support the GL_ARB_texture_cube_map extension in order for the game to run with cube maps turned on.

With the first level of dynamicism, we generate a cube map by placing the eye in the middle of the table and taking 6 pictures using a 90 degree FOV camera in each direction. This cube map contains only the table and scene (no balls), and is not updated throughout the game. The cube map is simply rotated, rather than regenerated, to handle the proper viewpoint.

The second level of dynamicism is a GPU hog. I'd recommend it only for fast cards (it runs on Brian's card at roughly 35 fps, but it runs on mine at less than 10 fps). The idea is to generate cubemaps for the balls from their locations for each frame before the render stage. This works nicely, and if you have a fast enough card to make the game playable, you'll certainly notice the difference when you can see the reflections of the balls in each other. The following is a decent screenshot of the dynamic cube map effect:



Some Implementation Details

The first step is creating the cubemap texture. We essentially create a texture for each of the faces of the cube and fill it with garbage from the framebuffer. This is done in the Ball class inside of the constructor somewhere:

    /* Cube texture won't be defined until the first rendering pass */
	if (useCubemaps)
    {
        glEnable(GL_TEXTURE_CUBE_MAP_ARB);
        glGenTextures(1, &cubeTex);
        glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, cubeTex);
        glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S,
            GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T,
            GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MAG_FILTER,
            GL_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MIN_FILTER,
            GL_LINEAR);
    
        for (int ii = 0; ii < 6; ii++)
        {
            // Fill the texture with whatever is on the framebuffer
            // right now... we don't really care until the first
            // rendering pass.
            glCopyTexImage2D(cubeSurfaces[ii], 0, GL_RGB8, 0, 0,
                CUBEMAP_SIZE, CUBEMAP_SIZE, 0);
        }
        glDisable(GL_TEXTURE_CUBE_MAP_ARB);
    }

Then, when it comes time for a frame to be rendered, the following function is called 3 times (once for each ball) to update the cube map textures. If dynamic cube mapping is on (see "Level 2" as discussed earlier), the cube mapped textures are generated from the ball's point of view. Otherwise, they are generated (and the function is only called once per ball) from the center of the table, with no balls on the table.

void Tring::updateCubeMaps()
{
    static int ii = 0;
    
    renderState = CUBEMAP_UPDATE;
    
    glReadBuffer(GL_BACK);
    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective (90, 1.0f, ballRadius / 2.0f, 500);
    glViewport(0, 0, CUBEMAP_SIZE, CUBEMAP_SIZE);
    glMatrixMode(GL_MODELVIEW);
    
    Ball *curBall = balls[ii];
    Vector ballPos = Vector(curBall->getX(), curBall->getY(), 0.0f);
    GLuint cubeTex = curBall->getCubeTextureNum();
    glPushMatrix();
        for (int s = 0; s < 6; s++)
        {
            glPushMatrix();
                glClear(GL_DEPTH_BUFFER_BIT);
                glEnable(GL_DEPTH_TEST);
                glDepthMask(GL_TRUE);
                glEnable(GL_LIGHTING);
                glLoadIdentity();
                glRotatef(rot[s][0], rot[s][1], rot[s][2], rot[s][3]);
                if (s == 0 || s == 1)
                    glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
                
                glTranslatef(0, 0, -ballRadius);
                if (dynamicCubemaps)
                    glTranslatef(-ballPos.getX(), -ballPos.getY(), 0);
                skyBox->display(1.0f);
                tringTable->display(1.0f, false, true, true);
                
                // don't draw the balls in static cubemaps... they're ugly
                if (dynamicCubemaps)
                {
                    glTranslatef(0, 0, ballRadius);
                    for (int b = 0; b < 3; b++)
                    {
                        if (b != ii)
                            balls[b]->display();
                    }
                }
            glPopMatrix();
            
            glFinish();
            
            glEnable(GL_TEXTURE_CUBE_MAP_ARB);
            glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, cubeTex);
            glCopyTexSubImage2D(cubeSurfaces[s], 0, 0, 0, 0, 0,
                CUBEMAP_SIZE, CUBEMAP_SIZE);
            
            glDisable(GL_TEXTURE_CUBE_MAP_ARB);
            
            //glutSwapBuffers();
            //sleep(1);
        }
    glPopMatrix();
    
    glViewport(0, 0, windowWidth, windowHeight);
    switchTo3D();
    glClear(GL_DEPTH_BUFFER_BIT);	// we're drawing over it all
    
    ii = (ii + 1) % 3; // we'll update the next ball next frame

}

Then, the only trick left is rendering the cube maps. The following function is the portion of the ball rendering code which draws the cube map over the current ball. It is essentially drawing a translucent ball over the current ball, offset slightly in the depth buffer so that they do not overlap (hence the glPolygonOffset calls). The tricky part is getting the cube map to align properly with the camera. To do so, we switched the matrix mode to GL_TEXTURE and un-did all of the rotations necessary to get to this point so that the cube map was screen-aligned again. This could be abstracted out into another function that could reverse the rotations of any modelview matrix, but since our camera model was so simple, we were okay doing it the way we did.

void Ball::drawShinyBall()
{
    #ifndef christmas
        glPushAttrib(GL_TEXTURE_BIT | GL_DEPTH_BUFFER_BIT);
    #else
        glActiveTextureARB(GL_TEXTURE1_ARB);
    #endif
    
    //Enable cube mapping.
    glEnable(GL_TEXTURE_CUBE_MAP_ARB);
    #ifdef christmas
        glDisable(GL_TEXTURE_2D);
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    #endif
        
    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_ARB);
    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_ARB);
    glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_ARB);
    
    //Enable auto generation of tex-coords
    glEnable(GL_TEXTURE_GEN_S);
    glEnable(GL_TEXTURE_GEN_T);
    glEnable(GL_TEXTURE_GEN_R);
    
    glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, cubeTex);
    
    glMatrixMode(GL_TEXTURE);
    glPushMatrix();
        if (tring->getRenderState() == MAIN_VIEW)
        {
            /* We must do viewpoint correction on the cubemap
               if the camera moves in the Main View */
            glRotatef(-tring->getEyeAngle(), 1.0f, 0.0f, 0.0f);
        }
        else if (tring->getRenderState() == SHOT_VIEW)
        {
            /* Viewpoint correction for the shot view */
            glRotatef(-tring->getShotAngle(), 0.0f, 0.0f, 1.0f);
            glRotatef(180, 0, 0, 1);
            glRotatef(90, 1, 0, 0);
        }
        
        #ifndef christmas
            glEnable(GL_POLYGON_OFFSET_FILL);
            glPolygonOffset(-1.0f, -1.0f);
            glDepthMask(GL_FALSE);
            glDepthFunc(GL_LEQUAL);
        #endif
        gluSphere(quadric, radius, 30, 30);
        #ifndef christmas
            glDisable(GL_POLYGON_OFFSET_FILL);
        #endif
    glPopMatrix();
    
    glMatrixMode(GL_MODELVIEW);
    
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_TEXTURE_CUBE_MAP_ARB);
    
    #ifdef christmas
        glActiveTextureARB(GL_TEXTURE0_ARB);
        glDisable(GL_TEXTURE_2D);
        glDisable(GL_TEXTURE_CUBE_MAP_ARB);
    #endif

    // Reset the texture and depth info
    glPopAttrib();
}

navigate
about us
links