OpenGL Programming/Post-Processing

Post-processing are effects applied after the main OpenGL scene is rendered.

Technical overviewEdit

To apply a global effect on the whole scene, we face a limitation: all the shaders work locally: vertex shaders only know about the current vertex, and fragment shaders only know about the current pixel.

The only exception is when working with textures: in this case, we can access any part of the texture using texture coordinates.

So the idea for post-processing is to first render the whole scene in a texture, and then render this single texture to screen with the post-processing.

Two main alternatives exist:

  • render the screen a first time, then copy the screen to a texture using glCopyTexSubImage2D
  • render directly to a texture through a framebuffer object

We'll use the second method, which should be more efficient, and can render on an area bigger than the physical screen if necessary.

(The first method may be necessary if you plan to use the stencil buffer as well.)

ObjectsEdit

FramebufferEdit

We will create:

  • a framebuffer object
  • with a depth buffer stored in a render buffer (necessary to render a 3D scene)
  • a color buffer stored in a texture (with GL_CLAMP_TO_EDGE to avoid default GL_REPEAT's border "warping" effect).
/* Global */
GLuint fbo, fbo_texture, rbo_depth;
/* init_resources */
  /* Create back-buffer, used for post-processing */
 
  /* Texture */
  glActiveTexture(GL_TEXTURE0);
  glGenTextures(1, &fbo_texture);
  glBindTexture(GL_TEXTURE_2D, fbo_texture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screen_width, screen_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
  glBindTexture(GL_TEXTURE_2D, 0);
 
  /* Depth buffer */
  glGenRenderbuffers(1, &rbo_depth);
  glBindRenderbuffer(GL_RENDERBUFFER, rbo_depth);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, screen_width, screen_height);
  glBindRenderbuffer(GL_RENDERBUFFER, 0);
 
  /* Framebuffer to link everything together */
  glGenFramebuffers(1, &fbo);
  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbo_texture, 0);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo_depth);
  GLenum status;
  if ((status = glCheckFramebufferStatus(GL_FRAMEBUFFER)) != GL_FRAMEBUFFER_COMPLETE) {
    fprintf(stderr, "glCheckFramebufferStatus: error %p", status);
    return 0;
  }
  glBindFramebuffer(GL_FRAMEBUFFER, 0);
/* onReshape */
  // Rescale FBO and RBO as well
  glBindTexture(GL_TEXTURE_2D, fbo_texture);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screen_width, screen_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
  glBindTexture(GL_TEXTURE_2D, 0);
 
  glBindRenderbuffer(GL_RENDERBUFFER, rbo_depth);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, screen_width, screen_height);
  glBindRenderbuffer(GL_RENDERBUFFER, 0);
/* free_resources */
  glDeleteRenderbuffers(1, &rbo_depth);
  glDeleteTextures(1, &fbo_texture);
  glDeleteFramebuffers(1, &fbo);

VerticesEdit

Then we'll need a basic set of vertices to display the resulting texture on screen. In this example we'll only use 2D coordinates because we plan to make a 2D effect, but feel free to use 3D coordinates for a 3D effect (mapping the texture on a rotating cube like Compiz, for instance):

/* Global */
GLuint vbo_fbo_vertices;
/* init_resources */
  GLfloat fbo_vertices[] = {
    -1, -1,
     1, -1,
    -1,  1,
     1,  1,
  };
  glGenBuffers(1, &vbo_fbo_vertices);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_fbo_vertices);
  glBufferData(GL_ARRAY_BUFFER, sizeof(fbo_vertices), fbo_vertices, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, 0);
/* free_resources */
  glDeleteBuffers(1, &vbo_fbo_vertices);

ProgramEdit

Now we'll need a separate program for our post-processing effect. It's a lot of code, but it's a mere copy/paste from the basic tutorials :)

/* Global */
GLuint program_postproc, attribute_v_coord_postproc, uniform_fbo_texture;
/* init_resources */
  /* Post-processing */
  if ((vs = create_shader("postproc.v.glsl", GL_VERTEX_SHADER))   == 0) return 0;
  if ((fs = create_shader("postproc.f.glsl", GL_FRAGMENT_SHADER)) == 0) return 0;
 
  program_postproc = glCreateProgram();
  glAttachShader(program_postproc, vs);
  glAttachShader(program_postproc, fs);
  glLinkProgram(program_postproc);
  glGetProgramiv(program_postproc, GL_LINK_STATUS, &link_ok);
  if (!link_ok) {
    fprintf(stderr, "glLinkProgram:");
    print_log(program_postproc);
    return 0;
  }
  glValidateProgram(program_postproc);
  glGetProgramiv(program_postproc, GL_VALIDATE_STATUS, &validate_ok); 
  if (!validate_ok) {
    fprintf(stderr, "glValidateProgram:");
    print_log(program_postproc);
  }
 
  attribute_name = "v_coord";
  attribute_v_coord_postproc = glGetAttribLocation(program_postproc, attribute_name);
  if (attribute_v_coord_postproc == -1) {
    fprintf(stderr, "Could not bind attribute %s\n", attribute_name);
    return 0;
  }
 
  uniform_name = "fbo_texture";
  uniform_fbo_texture = glGetUniformLocation(program_postproc, uniform_name);
  if (uniform_fbo_texture == -1) {
    fprintf(stderr, "Could not bind uniform %s\n", uniform_name);
    return 0;
  }
/* free_resources */
  glDeleteProgram(program_postproc);

DrawingEdit

We've got all our pre-requisites, so now how do we draw to the texture?

In onDisplay, let's add:

  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
  // draw (without glutSwapBuffers)
  glBindFramebuffer(GL_FRAMEBUFFER, 0);

We've changed the destination framebuffer to our own framebuffer, drawn the scene (to its texture), and then switched back to the physical screen's framebuffer (0).

Now we can display the texture on screen, using our new program:

  glClearColor(0.0, 0.0, 0.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
 
  glUseProgram(program_postproc);
  glBindTexture(GL_TEXTURE_2D, fbo_texture);
  glUniform1i(uniform_fbo_texture, /*GL_TEXTURE*/0);
  glEnableVertexAttribArray(attribute_v_coord_postproc);
 
  glBindBuffer(GL_ARRAY_BUFFER, vbo_fbo_vertices);
  glVertexAttribPointer(
    attribute_v_coord_postproc,  // attribute
    2,                  // number of elements per vertex, here (x,y)
    GL_FLOAT,           // the type of each element
    GL_FALSE,           // take our values as-is
    0,                  // no extra data between each position
    0                   // offset of first element
  );
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  glDisableVertexAttribArray(attribute_v_coord_postproc);
 
  glutSwapBuffers();

GotchasEdit

When manipulating multiple program, you need to make sure that all code that rely on the current program works on the right program. In particular, we added glUniform calls in onIdle, so make sure you have the right glUseProgram before setting the uniforms, or you'll get a blank screen due to a missing MVP matrix (and OpenGL won't tell you).

ShadersEdit

First, let's implement an identity (no-change) shader, we'll modify it later to create a first effect.

We chose not to pre-compute the texture coordinates, so the vertex shader will do it:

attribute vec2 v_coord;
uniform sampler2D fbo_texture;
varying vec2 f_texcoord;
 
void main(void) {
  gl_Position = vec4(v_coord, 0.0, 1.0);
  f_texcoord = (v_coord + 1.0) / 2.0;
}

Nothing fancy.

Now the fragment shader will be able to pick pixels anywhere we want in the texture - we're not restricted to the current pixel anymore!

uniform sampler2D fbo_texture;
varying vec2 f_texcoord;
 
void main(void) {
  gl_FragColor = texture2D(fbo_texture, f_texcoord);
}

A first effectEdit

Let's implement a very basic post-processing effect: a static wave on the screen, using the sin function. There is a similar (but more complex) effect in God of War III during the Poseidon Hippocamp's water breathing attack.

Suzanne takes a bath

The idea is to postpone the x axis regularly, changing progressively on the y axis:

uniform sampler2D fbo_texture;
uniform float offset;
varying vec2 f_texcoord;
 
void main(void) {
  vec2 texcoord = f_texcoord;
  texcoord.x += sin(texcoord.y * 4*2*3.14159 + offset) / 100;
  gl_FragColor = texture2D(fbo_texture, texcoord);
}

We have 4 vertical sin waves, and its amplitude is 1/100 of the screen width.

offset is used to animate, by changing the starting point of the sin function, we define this uniform as:

/* onIdle() */
  glUseProgram(program_postproc);
  GLfloat move = glutGet(GLUT_ELAPSED_TIME) / 1000.0 * 2*3.14159 * .75;  // 3/4 of a wave cycle per second
  glUniform1f(uniform_offset, move);

We've done our first post-processing effect!

LinksEdit

  • SFML (a 2D game library) provides a post-effect system implementing this technique
- Comment on this page

- Recent stats

< OpenGL Programming

Browse & download complete code
Last modified on 29 November 2012, at 08:50