Jump to content

OpenGL Programming/Post-Processing

From Wikibooks, open books for an open world

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

Technical overview

[edit | edit source]

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.)

Objects

[edit | edit source]

Framebuffer

[edit | edit source]

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);

Vertices

[edit | edit source]

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);

Program

[edit | edit source]

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);

Drawing

[edit | edit source]

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();

Gotchas

[edit | edit source]

When using multiple programs, make sure you set the rendering state to use the correct program using glUseProgram before setting your uniforms. In particular, in our onIdle routine below, we set the rendering state to use our program_postproc program, then added a call to glUniform, so in your rendering code, you will need set the rendering state to your program before setting your uniforms, or you'll get a blank screen due to a missing MVP matrix (and OpenGL won't tell you).

In case the resolution of your texture and your screen differ, adjust the viewport size accordingly, using glViewport.

Shaders

[edit | edit source]

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 effect

[edit | edit source]

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!

[edit | edit source]
  • SFML (a 2D game library) provides a post-effect system implementing this technique

< OpenGL Programming

Browse & download complete code