跳转到内容

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

< OpenGL 编程

浏览并下载 完整代码
华夏公益教科书