OpenGL 程式設計/後期處理
後期處理是在主 OpenGL 場景渲染完成後應用的效果。
為了在整個場景上應用全域性效果,我們面臨一個限制:所有著色器都區域性工作:頂點著色器只知道當前頂點,片段著色器只知道當前畫素。
唯一的例外是在使用紋理時:在這種情況下,我們可以使用紋理座標訪問紋理的任何部分。
因此,後期處理的思路是先將整個場景渲染到紋理中,然後用後期處理將這個單一紋理渲染到螢幕上。
存在兩種主要方法
- 第一次渲染螢幕,然後使用
glCopyTexSubImage2D將螢幕複製到紋理 - 透過幀緩衝區物件直接渲染到紋理
我們將使用第二種方法,它應該更高效,如果需要,可以渲染到比物理螢幕更大的區域。
(如果您計劃使用 模板緩衝區,則可能需要第一種方法。)
我們將建立
- 一個幀緩衝區物件
- 帶有儲存在渲染緩衝區中的深度緩衝區(渲染 3D 場景所必需)
- 一個儲存在紋理中的顏色緩衝區(使用 GL_CLAMP_TO_EDGE 以避免預設 GL_REPEAT 的邊界“扭曲”效果)。
/* 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);
然後我們需要一組基本的頂點來在螢幕上顯示生成的紋理。在這個例子中,我們只使用 2D 座標,因為我們計劃製作 2D 效果,但您可以隨意使用 3D 座標來製作 3D 效果(例如,將紋理對映到像 Compiz 這樣的旋轉立方體上)
/* 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);
現在我們需要一個單獨的程式來處理我們的後期處理效果。程式碼很多,但它只是從基本教程中複製貼上而已 :)
/* 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);
我們已經具備了所有先決條件,那麼我們如何繪製到紋理上呢?
在 onDisplay 中,讓我們新增
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// draw (without glutSwapBuffers)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
我們將目標幀緩衝區更改為我們自己的幀緩衝區,繪製了場景(到它的紋理),然後切換回物理螢幕的幀緩衝區(0)。
現在我們可以使用我們的新程式在螢幕上顯示紋理
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();
在使用多個程式時,確保您在設定制服之前使用 glUseProgram 將渲染狀態設定為使用正確的程式。特別是,在我們下面的 onIdle 例程中,我們將渲染狀態設定為使用我們的 program_postproc 程式,然後添加了對 glUniform 的呼叫,因此在您的渲染程式碼中,您需要在設定制服之前將渲染狀態設定為您的程式,否則您將獲得空白螢幕,因為缺少 MVP 矩陣(並且 OpenGL 不會告訴您)。
如果紋理的解析度與螢幕的解析度不同,請使用 glViewport 調整視口大小。
首先,讓我們實現一個身份(無變化)著色器,我們稍後會修改它來建立第一個效果。
我們選擇不預先計算紋理座標,因此頂點著色器會這樣做
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;
}
沒什麼特別的。
現在片段著色器可以從我們想要的任何地方在紋理中選擇畫素——我們不再侷限於當前畫素!
uniform sampler2D fbo_texture;
varying vec2 f_texcoord;
void main(void) {
gl_FragColor = texture2D(fbo_texture, f_texcoord);
}
讓我們實現一個非常基本的後期處理效果:使用 sin 函式在螢幕上建立靜止波浪。在戰神 3 中,波塞冬海馬的水呼吸攻擊中存在類似(但更復雜)的效果。

思路是在 y 軸上逐漸改變 x 軸的定期延遲
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);
}
我們有 4 個垂直正弦波,其振幅為螢幕寬度的一百分之一。
offset 用於動畫,透過改變 sin 函式的起點,我們定義這個制服為
/* 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);
我們已經完成了我們的第一個後期處理效果!
- SFML(一個 2D 遊戲庫)提供了一個 後期效果 系統來實現這種技術