片段处理与帧缓存 - 离屏渲染、反混叠

这篇文章介绍片段着色器相关的两个部分,离屏渲染和反混叠。离屏渲染将内容渲染到用户自定义格式的缓存中。反混叠可以使渲染的物体更加平滑。

离屏渲染

到目前位置,我们程序中的窗体创建工作都是程序框架做的,其中封装了很多细节。虽然针对特定操作系统如何绑定窗体缓存的步骤,我们不得而知。但是我们可以通过这节内容,了解如何自行设置帧缓存,进而侧面感受一下窗体缓存的概念。

首先,我们需要创建一个帧缓存对象。可以通过 glCreateFramebuffers() 函数创建,其原型为:

  • void glCreateFramebuffers(GLsizei n, GLuint *framebuffers);

可以看到创建帧缓存对象的参数和我们之前创建缓冲对象是一样的。同样,后续我们也需要将帧缓存对象绑定到目标点,需要使用 glBindFramebuffer() 函数。其原型为:

  • void glBindFramebuffer(GLenum target, GLuint framebuffer);

其中,目标点可以是 GL_DRAW_FRAMEBUFFER、GL_READ_FRAMEBUFFER 或 GL_FRAMEBUFFER。我们一般选 GL_FRAMEBUFFER,表示将对象同时绑定到读取和绘制目标点。

需要注意的是,将 target 参数设置为 0,可以恢复到默认帧缓存,通常就是应用程序窗口对应的帧缓存。

target 参数设置为 0,可以恢复到默认帧缓存。

当我们创建和绑定好帧缓存对象后,我们需要将纹理对象附加到帧缓存对象,以作为要进行渲染的存储空间。帧缓存支持 3 种附加类型 —— 深度、模板和颜色附加,分别作为深度、模板和颜色缓存。附加可以通过 glFramebufferTexture() 函数完成,其原型为:

  • void glFramebufferTexture(GLenum target,
  •                           GLenum attachment,
  •                           GLuint texture,
  •                           GLint level);

其中 attachment 需要说明一下,就是用它指定附加类型。GL_DEPTH_ATTACHMENT 表示深度缓存;GL_STENCIL_ATTACHMENT 表示模板缓存;GL_COLOR_ATTACHMENTi 表示颜色缓存,因为颜色输出可以有多个,我们在 《片段处理与帧缓冲 - 片段着色器、单片段测试、颜色输出》 中已经了解过。

参数 level 指的是 mipmap 的 level。

因为颜色缓冲会有多个,所以还需要特别指定片段着色器需要输出值到哪些颜色缓冲。需要使用 glDrawBuffers() 函数指定,其原型为:

  • void glDrawBuffers(GLsizei n, const GLenum *bufs);

其中 bufs 参数是包含指定缓冲的列表。常用的值为 GL_COLOR_ATTACHMENTn,含义和 glFramebufferTexture 中的颜色缓存是对应上的。值还可能是 GL_BACK_LEFT、GL_BACK_RIGHT 等,用于立体渲染。

立体渲染一节的实验没有做成功,窗体创建失败。应该是本地环境不支持立体渲染这种模式。

至此,我们目前需要用到的函数就都介绍完毕了。我们再看到代码清单 1.1 的帧缓存设置代码,就容易理解了:首先我们创建并绑定了帧缓存对象。接着我们创建了两个纹理对象,之后将这两个纹理对象分别当作颜色缓存和深度缓存,绑定到帧缓存对象。最后通过 glDrawBuffers() 指定颜色缓存输出。

代码清单 1.1 设置帧缓存对象
  • glCreateFramebuffers(1, &fbo);
  • glBindFramebuffer(GL_FRAMEBUFFER, fbo);
  •  
  • glCreateTextures(GL_TEXTURE_2D, 1, &color_texture);
  • glBindTexture(GL_TEXTURE_2D, color_texture);
  • glTexStorage2D(GL_TEXTURE_2D, 9, GL_RGBA8, 512, 512);
  • glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  • glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  •  
  • glCreateTextures(GL_TEXTURE_2D, 1, &depth_texture);
  • glBindTexture(GL_TEXTURE_2D, depth_texture);
  • glTexStorage2D(GL_TEXTURE_2D, 9, GL_DEPTH_COMPONENT32F, 512, 512);
  •  
  • glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color_texture, 0);
  • glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth_texture, 0);
  •  
  • static const GLenum draw_buffers[] = { GL_COLOR_ATTACHMENT0 };
  • glDrawBuffers(1, draw_buffers);

我们接下来做的实验操作是,先渲染上述自定义的帧缓存对象,然后我们将渲染的输出结果当作另一个程序的纹理输入。

代码清单 1.2 是自定义帧缓存的片段着色器,它输出条纹状的颜色。代码清单 1.3 是需要渲染到窗体上的片段着色器,它的颜色从纹理中获取。

代码清单 1.2 自定义帧缓存的片段着色器
  • #version 410 core
  •  
  • in VS_OUT
  • {
  •     vec4 color;
  •     vec2 texcoord;
  • } fs_in;
  •  
  • out vec4 color;
  •  
  • void main(void)
  • {
  •     color = sin(fs_in.color * vec4(40.0, 20.0, 30.0, 1.0)) * 0.5 + vec4(0.5);
  • }
代码清单 1.3 窗体渲染片段着色器
  • #version 420 core
  •  
  • uniform sampler2D tex;
  •  
  • out vec4 color;
  •  
  • in VS_OUT
  • {
  •     vec4 color;
  •     vec2 texcoord;
  • } fs_in;
  •  
  • void main(void)
  • {
  •     color = mix(fs_in.color, texture(tex, fs_in.texcoord), 0.7);
  • }

渲染过程如代码清单 1.4 所示。第 1 行绑定到自定义的帧缓存。第 3 至 10 行执行完就可以在帧缓存中得到渲染结果。接着第 12 行切换回默认窗体缓冲。注意第 17 行将绑定到帧缓存的颜色纹理作为下一个渲染程序的纹理输入。

代码清单 1.4 渲染到纹理
  1. glBindFramebuffer(GL_FRAMEBUFFER, fbo);
  2.  
  3. glViewport(0, 0, 512, 512);
  4. glClearBufferfv(GL_COLOR, 0, sb7::color::Green);
  5. glClearBufferfi(GL_DEPTH_STENCIL, 0, 1.0f, 0);
  6.  
  7. glUseProgram(program1);
  8. glUniformMatrix4fv(proj_location, 1, GL_FALSE, proj_matrix);
  9. glUniformMatrix4fv(mv_location, 1, GL_FALSE, mv_matrix);
  10. glDrawArrays(GL_TRIANGLES, 0, 36);
  11.  
  12. glBindFramebuffer(GL_FRAMEBUFFER, 0);
  13. glViewport(0, 0, info.windowWidth, info.windowHeight);
  14. glClearBufferfv(GL_COLOR, 0, blue);
  15. glClearBufferfv(GL_DEPTH, 0, &one);
  16.  
  17. glBindTexture(GL_TEXTURE_2D, color_texture);
  18.  
  19. glUseProgram(program2);
  20. glUniformMatrix4fv(proj_location2, 1, GL_FALSE, proj_matrix);
  21. glUniformMatrix4fv(mv_location2, 1, GL_FALSE, mv_matrix);
  22.  
  23. glDrawArrays(GL_TRIANGLES, 0, 36);
  24.  
  25. glBindTexture(GL_TEXTURE_2D, 0);

图 1 是渲染的结果,可以看到转动的大立方体的各个面上,会有转动的条纹状颜色的小立方体。

图1 渲染到纹理的结果

看着是旋转的立方体,其实还是对应纹理的二维图像。

分层渲染

分层渲染是指将渲染结果分层输出到可索引的 2D 纹理上,这和之前学习的数组纹理是一个概念。因为我们之前已经学习过数组纹理,所以分层渲染非常容易理解,我们直接看到代码。

帧缓存的设置如代码清单 1.5 所示,主要差异点就是需要创建数组纹理 —— GL_TEXTURE_2D_ARRAY。

代码清单 1.5 设置分层帧缓存
  • glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &array_texture);
  • glBindTexture(GL_TEXTURE_2D_ARRAY, array_texture);
  • glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 256, 256, 16);
  •  
  • glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &array_depth);
  • glBindTexture(GL_TEXTURE_2D_ARRAY, array_depth);
  • glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT32, 256, 256, 16);
  •  
  • glCreateFramebuffers(1, &layered_fbo);
  • glBindFramebuffer(GL_FRAMEBUFFER, layered_fbo);
  • glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, array_texture, 0);
  • glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, array_depth, 0);

着色器中使用内置变量 gl_Layer 来指定渲染的输出层数。如代码清单 1.6 所示,我们在几何着色器中实现分层渲染。几何着色器生成 16 层颜色、旋转角度各不相同的渲染结果。

代码清单 1.6 使用几何着色器的分层渲染
  • #version 430 core
  •  
  • layout (invocations = 16, triangles) in;
  • layout (triangle_strip, max_vertices = 3) out;
  •  
  • in VS_OUT
  • {
  •     vec4 color;
  •     vec3 normal;
  • } gs_in[];
  •  
  • out GS_OUT
  • {
  •     vec4 color;
  •     vec3 normal;
  • } gs_out;
  •  
  • layout (binding = 0) uniform BLOCK
  • {
  •     mat4 proj_matrix;
  •     mat4 mv_matrix[16];
  • };
  •  
  • void main(void)
  • {
  •     int i;
  •  
  •     const vec4 colors[16] = vec4[16](
  •         vec4(0.0, 0.0, 1.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0),
  •         vec4(0.0, 1.0, 1.0, 1.0), vec4(1.0, 0.0, 1.0, 1.0),
  •         vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 1.0, 1.0, 1.0),
  •         vec4(0.0, 0.0, 0.5, 1.0), vec4(0.0, 0.5, 0.0, 1.0),
  •         vec4(0.0, 0.5, 0.5, 1.0), vec4(0.5, 0.0, 0.0, 1.0),
  •         vec4(0.5, 0.0, 0.5, 1.0), vec4(0.5, 0.5, 0.0, 1.0),
  •         vec4(0.5, 0.5, 0.5, 1.0), vec4(1.0, 0.5, 0.5, 1.0),
  •         vec4(0.5, 1.0, 0.5, 1.0), vec4(0.5, 0.5, 1.0, 1.0)
  •     );
  •  
  •     for (i = 0; i < gl_in.length(); i++)
  •     {
  •         gs_out.color = colors[gl_InvocationID];
  •         gs_out.normal = mat3(mv_matrix[gl_InvocationID]) * gs_in[i].normal;
  •         gl_Position = proj_matrix * mv_matrix[gl_InvocationID] * gl_in[i].gl_Position;
  •         gl_Layer = gl_InvocationID;
  •         EmitVertex();
  •     }
  •  
  •     EndPrimitive();
  • }

最后同样是将各层渲染结果以数组纹理的方式显示到窗体上。结果如图 2 所示,有 12 个颜色、旋转角度各不相同的圆环。

图2 分层渲染示例的结果

反混叠

信号采样速率(采样率)不满足信号内容要求时会发生混叠。自己通俗的理解就是渲染的内容有锯齿、不平滑。这节介绍 OpenGL 的反混叠操作,代码不多,自己实验下来也只是前后对比,了解感受一下反混叠的效果。

过滤法反混叠

过滤法反混叠是去除信号中的高频内容。它在颜色混合的基础上开启平滑,比如以下代码片段可以开启线平滑:

  • glEnable(GL_BLEND);
  • glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  • glEnable(GL_LINE_SMOOTH);

使用线平滑前后的效果如图 3 所示,因为图片宽高较小,且图片压缩的原因,这边将细节放大。图 3 上方是没有开启线平滑的效果,下方是开启了线平滑的效果,可以看出反混叠效果显著。

图3 使用线平滑的反混叠

如图 4 上方所示,当以纯白渲染立方体时,外部边缘的锯齿严重。这时候可以将线平滑换成多边形平滑:

  • glEnable(GL_POLYGON_SMOOTH);

如图 4 下方所示,多边形平滑还存在一个问题,虽然外部边缘的锯齿消失了,但是内部边缘却变得更加明显了。

图4 使用多边形平滑的反混叠

多样本反混叠

多样本反混叠采取的手段是提高采样率。可使用目前框架代码开启多采用,如下是选择了八样本反混叠:

  • virtual void init()
  • {
  •     sb7::application::init();
  •     info.samples = 8;
  • }

多采样默认是开启的,可以使用 glDisable 关闭,glEnable 启用:

  • glEnable(GL_MULTISAMPLE);

图 5 是开启八样本反混叠的结果,可以看出不仅外部边缘锯齿消失,内部边缘的分界也变成不可见的了。

图5 八样本反混叠

采样率着色

之前我们了解到着色的时候是会进行插值计算的,所以也会存在欠采样的情况。如代码清单 2 所示,可以在片段着色器中生成高频输出。

代码清单 2 生成高频输出的片段着色器
  • #version 420 core
  •  
  • out vec4 color;
  •  
  • in VS_OUT
  • {
  •     vec2 tc;
  • } fs_in;
  •  
  • void main(void)
  • {
  •     float val = abs(fs_in.tc.x + fs_in.tc.y) * 30.0f;
  •     color = vec4(fract(val) >= 0.5 ? 1.0 : 0.2);
  • }

图 6 上方是没有开启采样率着色的效果,可以看出着色有明显的锯齿。我们可以按如下开启采样率着色:

  • glEnable(GL_SAMPLE_SHADING);

并且使用 glMinSampleShading() 函数指定多少部分采用样本率着色。其函数原型为:

  • void glMinSampleShading(GLfloat value);

参数 value 指定至少 value 比率的样本运行着色器。比如 value 为 0.5 时,表示至少一半的样本运行着色器;value 为 1.0 时,表示各个样本都进行单独着色。

图6 高频着色器输出的反混叠

总结

这两节内容,书上介绍的内容很多,但是对应的代码样例比较少,这边也是基于代码样例进行“筛选”学习。遗留的一些知识点感觉专业性很强,今后有空再回过头来学习。