Cubemaps

Cubemap textures are a mechanism provided by GPUs that allows us to develop several rendering techniques such as skyboxes, skylight illumination, dynamic reflection of the environment, etc. With them we can map the visible environment onto the 6 faces of a cube and later easily query this information from a shader program.

Cubemap 3d

In the previous image we can see the cubemap texture represented in 3D.

Cubemap textures are able to store each one of the images corresponding to every face of the cube into a certain sub-texture. In order to do that, OpenGL provides functions to upload independent textures for each cubemap face according to the following scheme:

Cubemap unfold

Notice that this image is the result of unfolding the first 3D cubemap representation. The textures expected by OpenGL are the images of the environment projected onto the faces of the cubemap as seen from the outside (I remark this because I’ve found myself trying to understand what was wrong because I uploaded the images as seen from within the cube, just because it seems kinda intuitive when implementing skyboxes).

OpenGL code to generate a cubemap texture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Create and bind a new cubemap texture
GLuint tex;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_CUBE_MAP, tex);

// Texture parameters (don't forget R coordinate)
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
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);

// Pixels
GLint w, h; // Width and height
void *texelsPX = readPixels("cubemap_positive_x.png", &w, &h);
void *texelsNX = readPixels("cubemap_negative_x.png", &w, &h);
void *texelsPY = readPixels("cubemap_positive_y.png", &w, &h);
void *texelsNY = readPixels("cubemap_negative_y.png", &w, &h);
void *texelsPZ = readPixels("cubemap_positive_z.png", &w, &h);
void *texelsNZ = readPixels("cubemap_negative_z.png", &w, &h);

// Fixed format (could be different...)
GLenum ifmt = GL_RGB; // Internal format
GLenum dfmt = GL_RGBA; // Data format
GLenum dtype = GL_UNSIGNED_BYTE; // Data type

// Upload texels for each cube face
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, ifmt, w, h, 0, dfmt, dtype, texelsPX);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, ifmt, w, h, 0, dfmt, dtype, texelsNX);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, ifmt, w, h, 0, dfmt, dtype, texelsPY);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, ifmt, w, h, 0, dfmt, dtype, texelsNY);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, ifmt, w, h, 0, dfmt, dtype, texelsPZ);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, ifmt, w, h, 0, dfmt, dtype, texelsNZ);

OpenGL code to send it to a skybox shader

The procedure to send the cubemap texture to a shader program is exactly the same as sending a regular 2D texture:

1
2
3
4
5
6
7
8
9
10
11
12
// Bind the texture
const int textureSlot = 0;
glActiveTexture(GL_TEXTURE0 + textureSlot);
glBindTexture(GL_TEXTURE_CUBE_MAP, tex);

// Configure program
glUseProgram(program);
GLint skyboxLocation = glGetUniformLocation(program, "skybox");
glUniform1i(skyboxLocation, textureSlot);

// Send some geometry to draw
// ...

GLSL skybox vertex shader

It actually does nothing. Although no vertices need to be processed, one vertex must be sent to the rendering pipeline (no matter its value or attributes) in order to launch the program.

1
2
#version 400
void main() { }

GLSL skybox geometry shader

We generate the whole cube geometry here. Notice that it expects no input from the vertex shader (it was empty). It constructs the whole cube by means of generating two triangle strip primitives from the scratch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#version 400

uniform mat4 projectionMatrix;
uniform mat4 orientationMatrix;

layout(points) in;
layout(triangle_strip, max_vertices = 16) out;

// Corner position(and texture coordinate for the next stage)
out vec3 pos;

void main()
{
  // Orientation-projection matrix
  mat4 m  = projectionMatrix * orientationMatrix;

  // Macro to emit a vertex
#define EmitVertex3f(x,y,z) \
  pos = vec3(x,y,z); \
  gl_Position = m * vec4(pos,1.0); \
  EmitVertex();


  // Horizontal triangle strip (-X -Z +X)
  EmitVertex3f(-1.0, 1.0, 1.0);
  EmitVertex3f(-1.0,-1.0, 1.0);
  EmitVertex3f(-1.0, 1.0,-1.0);
  EmitVertex3f(-1.0,-1.0,-1.0);
  EmitVertex3f( 1.0, 1.0,-1.0);
  EmitVertex3f( 1.0,-1.0,-1.0);
  EmitVertex3f( 1.0, 1.0, 1.0);
  EmitVertex3f( 1.0,-1.0, 1.0);
  EndPrimitive();

  // Vertical triangle strip (-Y +Z +Y)
  EmitVertex3f( 1.0,-1.0,-1.0);
  EmitVertex3f(-1.0,-1.0,-1.0);
  EmitVertex3f( 1.0,-1.0, 1.0);
  EmitVertex3f(-1.0,-1.0, 1.0);
  EmitVertex3f( 1.0, 1.0, 1.0);
  EmitVertex3f(-1.0, 1.0, 1.0);
  EmitVertex3f( 1.0, 1.0,-1.0);
  EmitVertex3f(-1.0, 1.0,-1.0);
  EndPrimitive();
}

GLSL skybox fragment shader

Finally, the fragments are coloured with the value retrieved from the cubemap sampler. In order to query the sampler, we use the interpolated position between the cube corners output from the geometry shader stage. As they are the corners of a cube centered at the origin (0,0,0), they perfectly match the directions to be used as 3D cubemap texture coordinates.

1
2
3
4
5
6
7
8
9
10
#version 400

uniform samplerCube skybox; // Cubemap sampler

in vec3 pos; // Texture coordinate
out vec4 out_Color; // Output color

void main() {    
  out_Color = texture(skybox, pos);
}

Leave a Reply

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

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>