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 游戏库)提供了一个 后期效果 系统来实现这种技术