Orientation Matrices for Cube Mapping

I may well be totally wrong, but I don’t think I’ve ever successfully Googled a useful set of orientation matrices for cube mapping. As you already know, a cube map is a set of six images which, when projected on to a cube, provide a decent simulation of a spherical texture map. Two of the most common modes of usage are to provide real-time reflections on the outside of an object (by repeatedly making a map of the environment surrounding that object and then projecting that map back on to the object, as in the reflections you see on e.g. the cars in racing games), and to provide a skybox.

Skyboxes are usually either pre-rendered (pretty but boring), or done through rendering atmospheric scattering for your scene and then projecting some celestial bodies like the moon, stars etc (pretty but computationally intensive). An additional bonus of drawing your own skyboxes is that you can then use them for doing environment/ambient lighting for objects in your scene, either by working out the spherical harmonics (neat but I’m far too dumb to have ever wrapped my head around it) or by techniques which involve downsampling the cube map. This gives you ambient light which changes colour depending on the angle of the sun basically for free.

Therefore cube maps have multiple advantages for rendering your skybox and lighting:
1) render once and reuse (you can make this once per frame, or less often depending on how dynamic the sun is.)
2) you can do atmospheric scattering at a surprisingly low resolution and still have a decent looking result. You basically have to do your atmospheric scattering in the fragment shader if you want to use a “spotlight” effect to render the sun, which gets very expensive in terms of fragment power. I actually do both the sun and the moon, which is even more expensive, so lowering resolution is a major speed-up here.
3) basically free specular and ambient environment mapping of the sky on to everything in your scene. You can either go the very expensive route for downsampling, or just mipmap the thing and get 90% of the quality for 10% of the effort, and hardware acceleration.
4) if you’re blending your scene into the sky for a distance fogging effect – well, you just got the source for that as well!

This is where you usually run into a brick wall because figuring out the correct orientation matrices for rendering the cube map is a pain in the backside. What you’re going to be doing in the end is rendering a box around your camera and texture mapping the cube map on to the inside of the box, which will then act as the skybox. You can simplify this by not applying any rotation to the skybox, so that it’s aligned with the x, y and z axes. Therefore what you need to do is figure out how to make the camera look in six directions: +x, -x, +y, -y, +z and -z. You could do this with gluLookAt, but that’s a whole heck of a lot of lines of code just to look in the direction of an axis. Better to just know what matrices to use: see below. (I’m weird and use +x = east, +y = north, -z = up i.e. inverted right-handed axes.)

So, the process is:

1) set up your cube map as an attachment to a frame buffer so that you can draw to it.
2) your camera matrix is going to consist of projectionMatrix * orientationMatrix * worldSpaceCameraPositionMatrix. The projectionMatrix should have an FOV of 90˚ because you’re going to be rendering the whole of one of the cube faces at a time.
3) render six times, once to each face of the cube with each of the six orientation matrices in turn.
4) you now have a cube map which is properly orientated so that you can just map it onto your skybox and the texture coordinates will match up. You can optionally downscale or mipmap this texture.

Here’s an example of setting up a frame buffer with a cube map attached:

 
#define SKY_CUBE_FACE_SIZE 256 //or whatever
 
GLuint skyCubeMapTargetFBOId;
GLuint skyCubeMapTexture;
 
- (void)createSkyCubeMapFBO {
    glGenFramebuffers(1, &skyCubeMapTargetFBOId);
    glBindFramebuffer(GL_FRAMEBUFFER, skyCubeMapTargetFBOId);
 
    glGenTextures(1, &skyCubeMapTexture);
    glBindTexture(GL_TEXTURE_CUBE_MAP, skyCubeMapTexture);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 0);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 8);
 
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+0, 0, GL_RGBA16F, SKY_CUBE_FACE_SIZE, SKY_CUBE_FACE_SIZE, 0, GL_BGRA, GL_FLOAT, NULL);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+1, 0, GL_RGBA16F, SKY_CUBE_FACE_SIZE, SKY_CUBE_FACE_SIZE, 0, GL_BGRA, GL_FLOAT, NULL);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+2, 0, GL_RGBA16F, SKY_CUBE_FACE_SIZE, SKY_CUBE_FACE_SIZE, 0, GL_BGRA, GL_FLOAT, NULL);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+3, 0, GL_RGBA16F, SKY_CUBE_FACE_SIZE, SKY_CUBE_FACE_SIZE, 0, GL_BGRA, GL_FLOAT, NULL);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+4, 0, GL_RGBA16F, SKY_CUBE_FACE_SIZE, SKY_CUBE_FACE_SIZE, 0, GL_BGRA, GL_FLOAT, NULL);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+5, 0, GL_RGBA16F, SKY_CUBE_FACE_SIZE, SKY_CUBE_FACE_SIZE, 0, GL_BGRA, GL_FLOAT, NULL);
 
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X, skyCubeMapTexture, 0);
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
 
    GLenum buffers[] = { GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, buffers);
 
    GLuint status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    switch (status) {
        case GL_FRAMEBUFFER_COMPLETE:	printf("SKY_CUBE_FRAMEBUFFER_COMPLETE\n");break;
        case 0x8CDB:	        	printf("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT\n");break;
        case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: printf("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT\n");break;
        case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: printf("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT\n");break;
        case GL_FRAMEBUFFER_UNSUPPORTED:printf("GL_FRAMEBUFFER_UNSUPPORTED\n");break;
        default:			printf("Unknown issue (%x).\n",status);break;
    }
}

And here’s an example of rendering to the cube map and then mipmapping it:

- (void)renderSkyCube:(float)cameraZ X:(float)cameraX Y:(float)cameraY {
    Matrix4x4 cubeProjection = PerspectiveMatrix(90.0f, 1.0, MAIN_FRAMEBUFFER_NEAR_PLANE, MAIN_FRAMEBUFFER_FAR_PLANE);
 
    Matrix4x4 rotationMatrices[6];
 
    //please never delete me!//
    rotationMatrices[0] = Matrix4x4Make(0.0, 0.0, -1.0, 0.0,
                                        0.0, -1.0, 0.0, 0.0,
                                        -1.0, 0.0, 0.0, 0.0,
                                        0.0, 0.0, 0.0, 1.0);
 
 
    rotationMatrices[1] = Matrix4x4Make(0.0, 0.0, 1.0, 0.0,
                                        0.0, -1.0, 0.0, 0.0,
                                        1.0, 0.0, 0.0, 0.0,
                                        0.0, 0.0, 0.0, 1.0);
 
    rotationMatrices[2] = Matrix4x4Make(1.0, 0.0, 0.0, 0.0,
                                        0.0, 0.0, 1.0, 0.0,
                                        0.0, -1.0, 0.0, 0.0,
                                        0.0, 0.0, 0.0, 1.0);
 
    rotationMatrices[3] = Matrix4x4Make(1.0, 0.0, 0.0, 0.0,
                                        0.0, 0.0, -1.0, 0.0,
                                        0.0, 1.0, 0.0, 0.0,
                                        0.0, 0.0, 0.0, 1.0);
 
    rotationMatrices[4] = Matrix4x4Make(1.0, 0.0, 0.0, 0.0,
                                        0.0, -1.0, 0.0, 0.0,
                                        0.0, 0.0, -1.0, 0.0,
                                        0.0, 0.0, 0.0, 1.0);
 
    rotationMatrices[5] = Matrix4x4Make(-1.0, 0.0, 0.0, 0.0,
                                        0.0, -1.0, 0.0, 0.0,
                                        0.0, 0.0, 1.0, 0.0,
                                        0.0, 0.0, 0.0, 1.0);
 
    glUseProgram(skyShaderProgram);
    glUniformMatrix4fv(SkyShaderUniforms[kSKYProjectionMatrixUniform], 1, GL_FALSE, (const GLfloat *) &cubeProjection);
    glBindFramebuffer(GL_FRAMEBUFFER, skyCubeMapTargetFBOId);
    GLint frameBufferSize = SKY_CUBE_FACE_SIZE;
    glUniform2f(SkyShaderUniforms[kSKYViewportDimensions], (GLfloat)frameBufferSize, frameBufferSize);
    glViewport(0.0, 0.0, frameBufferSize, frameBufferSize);
    glBindVertexArray(quadVertexArrayObject); // this is a VAO with 2 triangles forming a quad
 
    for (int i = 0; i < 6; i ++) {
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, skyCubeMapTexture, 0); // chose the face of the cube map to draw
        Matrix4x4 skyInverseProjRot = gluInvertMatrix(Matrix4x4Mult(cubeProjection, rotationMatrices[i]));
        glUniformMatrix4fv(SkyShaderUniforms[kSKYInverseProjectionMatrix], 1, GL_FALSE, (const GLfloat *) &skyInverseProjRot);
 
        // the above two lines are a bit complicated.  I'm setting the inverse of the orientation matrix
        // multiplied by the current projection matrix because this will allow me to get the correct
        // orientation to get the direction from the camera to each fragment in world space in the shader,
        // which is used in the atmospheric scattering shader.  See also the article on getting 
        // fragment position in world space
 
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
    }
    glBindTexture(GL_TEXTURE_CUBE_MAP, skyCubeMapTexture);
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
}

Acknowledgements: I have used the matrix definitions from Tom Davie’s awesome tutorial on setting up modern OpenGL using Cocoa, and the ideas re: environment mapping were “inspired by” (i.e. totally ripped off from) Florian Bösch’s webGL tutorials and the awesome Morgan McGuire

Leave a Reply

Your email address will not be published. Required fields are marked *