跳轉到內容

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 程式設計

瀏覽並下載 完整程式碼
華夏公益教科書