OpenGL: One-pass rendering to a cube map

Rendering to a texture is a common trick one should be able to do in OpenGL in order to implement many magnificent effects like shadows or refracting surfaces. At this point cube maps supported by OpenGL are a way to enhance those effects. For example, omnidirectional shadows become possible.

How to implement it?

There are two basic approaches. The first approach is just to render the whole scene six times for each face of the cube map. OpenGL allows binding particular face of a cube map as a framebuffer's depth/color attachment. That is a simple trick, and there are lots of articles about it over the Internet.

The second approach is one-pass rendering. It uses geometry shaders to render each triangle six times for each face, so GPU is forced to do all the work. I think this way is preferable and I'm going to show how it can be accomplished.

Framebuffer

The first thing to do is initialization of a framebuffer. Here we should create a few cubemap textures and bind them.

const int cube_s = 1024;
GLuint framebuffer, *fbcubetextures;
int fbtextures_count;

// ...

void prepare_framebuffer(void) {
    glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &fbtextures_count);
    // how many textures to create? depends on complexity of your effects.
    fbcubetextures = new GLuint[fbtextures_count]; 
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glGenTextures(fbtextures_count, fbcubetextures);
    
    GLenum target = GL_TEXTURE_CUBE_MAP;

    // initializing depth map
    glBindTexture(target, fbcubetextures[0]);
    // setting up texture parameters (obligatory):
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(target, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glTexParameteri(target, GL_TEXTURE_COMPARE_MODE,
                            GL_COMPARE_REF_TO_TEXTURE);
    glTexParameteri(target, GL_TEXTURE_COMPARE_FUNC, GL_GREATER);
    
    for(int face = 0; face < 6; ++face)
        // initializing faces
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 
                     GL_DEPTH_COMPONENT24, cube_s, cube_s, 0,
                     GL_DEPTH_COMPONENT, GL_FLOAT, 0);
    
    // attaching to the framebuffer
    glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                         fbcubetextures[0], 0);
    
    // initializing color maps
    for(int i = 1; i < fbtextures_count; ++i) {
        glBindTexture(target, fbcubetextures[i]);
        glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(target, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
        
        for(int face = 0; face < 6; ++face)
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 
                         GL_RGBA32F, cube_s, cube_s, 0,
                         GL_RGBA, GL_FLOAT, 0);
        
        // attaching to the framebuffer
        glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i - 1,
                             fbcubetextures[i], 0);
    }
    
    GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
    glDrawBuffers(2, drawBuffers); // don't forget to do this!
    // OpenGL won't be filling attachments you haven't mentioned here.
    // binding default framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

Projection Matrices

We will need six projection matrices to project triangle on each of six cube faces. Here is some working code to create them.

glm::mat4 shadowMapProjections[6];

// ...

mat4 mat4FromArray(GLfloat *M) {
    return transpose(mat4(
        M[0], M[1], M[2], M[3],
        M[4], M[5], M[6], M[7],
        M[8], M[9], M[10],M[11],
        M[12],M[13],M[14],M[15]));
}

glm::mat4 glGetMatrix(GLenum which) {
    if(which == GL_PROJECTION)
        which = GL_PROJECTION_MATRIX;
    if(which == GL_MODELVIEW)
        which = GL_MODELVIEW_MATRIX;
    GLfloat buff[16];
    glGetFloatv(which, buff);
    return mat4FromArray(buff);
}

void generate_projections(float zmin, float zmax) {
    auto perspective = [=](void) {
        glLoadIdentity();
        gluPerspective(90, 1., zmin, zmax);
    };
    
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    
    perspective();
    gluLookAt(0.0, 0.0, 0.0,  1.0, 0.0, 0.0,  0.0,-1.0, 0.0); // +X
    shadowMapProjections[
        GL_TEXTURE_CUBE_MAP_POSITIVE_X - GL_TEXTURE_CUBE_MAP_POSITIVE_X
    ] = glGetMatrix(GL_PROJECTION);
    
    perspective();
    gluLookAt(0.0, 0.0, 0.0, -1.0, 0.0, 0.0,  0.0,-1.0, 0.0); // -X
    shadowMapProjections[
        GL_TEXTURE_CUBE_MAP_NEGATIVE_X - GL_TEXTURE_CUBE_MAP_POSITIVE_X
    ] = glGetMatrix(GL_PROJECTION);
    
    perspective();
    gluLookAt(0.0, 0.0, 0.0,  0.0, 1.0, 0.0,  0.0, 0.0, 1.0); // +Y
    shadowMapProjections[
        GL_TEXTURE_CUBE_MAP_POSITIVE_Y - GL_TEXTURE_CUBE_MAP_POSITIVE_X
    ] = glGetMatrix(GL_PROJECTION);
    
    perspective();
    gluLookAt(0.0, 0.0, 0.0,  0.0,-1.0, 0.0,  0.0, 0.0,-1.0); // -Y
    shadowMapProjections[
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Y - GL_TEXTURE_CUBE_MAP_POSITIVE_X
    ] = glGetMatrix(GL_PROJECTION);
    
    perspective();
    gluLookAt(0.0, 0.0, 0.0,  0.0, 0.0, 1.0,  0.0,-1.0, 0.0); // +Z
    shadowMapProjections[
        GL_TEXTURE_CUBE_MAP_POSITIVE_Z - GL_TEXTURE_CUBE_MAP_POSITIVE_X
    ] = glGetMatrix(GL_PROJECTION);
    
    perspective();
    gluLookAt(0.0, 0.0, 0.0,  0.0, 0.0,-1.0,  0.0,-1.0, 0.0); // -Z
    shadowMapProjections[
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Z - GL_TEXTURE_CUBE_MAP_POSITIVE_X
    ] = glGetMatrix(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
}

Modelview matrix is way more simple and depends on light source position (if you are building a shadow map)  and your personal taste.

glm::mat4 shadowMapModelview;

// . . .

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(lightpos[0], lightpos[1], lightpos[2], .1, .1, .1, 0, 0, 1);
    shadowMapModelview = glGetMatrix(GL_MODELVIEW_MATRIX);

 By the way, my examples are using OpenGL Mathematics library (glm:: namespace).

Shaders

Finally, we'll need some specific shaders. Especially geometry shader:

# version 420 core

uniform mat4 shadowMapProjections[6];

layout(triangles, invocations = 1) in;
layout(triangle_strip, max_vertices = 18) out;

in vec3 pos[];
out vec3 frag_pos;

void main() {
    
    for(int j = 0; j < 6; ++j) {
        gl_Layer = j;
        for(int i = 0; i < 3; ++i) {
            frag_pos = pos[i];
            gl_Position = shadowMapProjections[j] * vec4(pos[i], 1);
            EmitVertex();
        }
        EndPrimitive();
    }
}

It will duplicate each triangle for each of six faces ('layers') of the cube map. Vertex and fragment shaders stay the same as if there were no cube maps at all.

Usage

After rendering such a cube map you can bind it as a GL_TEXTURE_CUBE_MAP and access it in shaders like

uniform samplerCube myCubeMap;

in vec4 myCubeMap_ModelviewPos; // = myCubeMap_ModelviewMatrix * gl_Vertex

// . . .

    vec3 smapValue = textureCube(myCubeMap, myCubeMap_ModelviewPos.xyz);

 You'll probably experience problems with shadow maps, since it's quite difficult to compute depth in shadow map coords sweeping through six projection matrices, but there is a workaround:

    // pos = vec3(gl_ModelViewMatrix * gl_Vertex)
    int isHighlighted = int(length(pos) <= length(smapValue) * 1.01);

Conclusion

One-pass rendering to a cube map is possible and quite simple, although cube maps in general are not much comfortable in use IMHO. Keep calm and use it well!