OpenGL Programming/Post-Processing
Post-processing are effects applied after the main OpenGL scene is rendered.
Technical overview
editTo 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
editFramebuffer
editWe 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
editThen 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
editNow 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
editWe'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
editWhen 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
editFirst, 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
editLet'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.
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!
Links
edit- SFML (a 2D game library) provides a post-effect system implementing this technique