LearnOpenGL笔记——四、高级OpenGL:“面剔除”与“帧缓冲”

四、高级OpenGL:“面剔除”与“帧缓冲”

4.4 面剔除

  • 不绘制我们看不到的面
  • 如何只绘制面向观察者的面呢?
  • OpenGL使用了一个很聪明的技巧,分析顶点数据的环绕顺序(Winding Order)!
  • 环绕顺序
    • 简而言之,对于每个面,定义时面向观察者,顶点顺序为逆时针,这样在光栅化时任何顶点顺序为顺时针的面都是背向观察者的,可以剔除!
  • 面剔除
    • 确认在每个三角形中它们都是以逆时针定义的,这是一个很好的习惯。
    • 要想启用面剔除,我们只需要启用OpenGL的GL_CULL_FACE选项:
      glEnable(GL_CULL_FACE);
      
    • 从这一句代码之后,所有背向面都将被丢弃(尝试飞进立方体内部,看看所有的内面是不是都被丢弃了)。目前我们在渲染片段的时候能够节省50%以上的性能,但注意这只对像立方体这样的封闭形状有效。
    • 当我们想要绘制上一节中的草时,我们必须要再次禁用面剔除,因为它们的正向面和背向面都应该是可见的。
    • OpenGL允许我们改变需要剔除的面的类型。如果我们只想剔除正向面而不是背向面会怎么样?我们可以调用glCullFace来定义这一行为:
      glCullFace(GL_FRONT);
      
    在这里插入图片描述
    • glCullFace的初始值是GL_BACK。除了需要剔除的面之外,我们也可以通过调用glFrontFace,告诉OpenGL我们希望将顺时针的面(而不是逆时针的面)定义为正向面:
      glFrontFace(GL_CCW);
      
    • 默认值是GL_CCW,它代表的是逆时针的环绕顺序,另一个选项是GL_CW,它(显然)代表的是顺时针顺序。
    • 我们可以来做一个实验,告诉OpenGL现在顺时针顺序代表的是正向面,这样的结果是只有背向面被渲染了:
      glEnable(GL_CULL_FACE);
      glCullFace(GL_BACK);
      glFrontFace(GL_CW);
      
    • 注意你可以仍使用默认的逆时针环绕顺序,但剔除正向面,来达到相同的效果:
      glEnable(GL_CULL_FACE);
      glCullFace(GL_FRONT);
      
  • 可以看到,面剔除是一个提高OpenGL程序性能的很棒的工具。但你需要记住哪些物体能够从面剔除中获益,而哪些物体不应该被剔除。

4.5 帧缓冲

  • 到目前为止,我们已经使用了很多屏幕缓冲了:用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。
  • 这些缓冲结合起来叫做帧缓冲(Framebuffer),它被储存在内存中。
  • OpenGL允许我们定义我们自己的帧缓冲,也就是说我们能够定义我们自己的颜色缓冲,甚至是深度缓冲和模板缓冲。
  • 我们目前所做的所有操作都是在默认帧缓冲的渲染缓冲上进行的。默认的帧缓冲是在你创建窗口的时候生成和配置的(GLFW帮我们做了这些)。有了我们自己的帧缓冲,我们就能够有更多方式来渲染了。
  • 你可能不能很快理解帧缓冲的应用,但渲染你的场景到不同的帧缓冲能够让我们在场景中加入类似镜子的东西,或者做出很酷的后期处理效果。

4.5.1 创建一个帧缓冲

  • 和OpenGL中的其它对象一样,我们会使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象(Framebuffer Object, FBO):
    unsigned int fbo;
    glGenFramebuffers(1, &fbo);
    
  • 首先我们创建一个帧缓冲对象,将它绑定为激活的(Active)帧缓冲,做一些操作,之后解绑帧缓冲。我们使用
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    
  • 在绑定到GL_FRAMEBUFFER目标之后,所有的读取写入帧缓冲的操作将会影响当前绑定的帧缓冲。我们也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。绑定到GL_READ_FRAMEBUFFER的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到GL_DRAW_FRAMEBUFFER的帧缓冲将会被用作渲染、清除等写入操作的目标。大部分情况你都不需要区分它们,通常都会使用GL_FRAMEBUFFER,绑定到两个上。
  • 不幸的是,我们现在还不能使用我们的帧缓冲,因为它还不完整(Complete),一个完整的帧缓冲需要满足以下的条件:
    • 附加至少一个缓冲(颜色、深度或模板缓冲)。
    • 至少有一个颜色附件(Attachment)。
    • 所有的附件都必须是完整的(保留了内存)。
    • 每个缓冲都应该有相同的样本数
  • 在完成所有的条件之后,我们可以以GL_FRAMEBUFFER为参数调用glCheckFramebufferStatus,检查帧缓冲是否完整。它将会检测当前绑定的帧缓冲,并返回规范中这些值的其中之一。如果它返回的是GL_FRAMEBUFFER_COMPLETE,帧缓冲就是完整的了。
    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
      // 执行胜利的舞蹈
    
  • 之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做离屏渲染(Off-screen Rendering)要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到0。
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
  • 在完成所有的帧缓冲操作之后,不要忘记删除这个帧缓冲对象:
    glDeleteFramebuffers(1, &fbo);
    
  • 在完整性检查执行之前,我们需要给帧缓冲附加一个附件。附件是一个内存位置,它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像。当创建一个附件的时候我们有两个选项:纹理渲染缓冲对象(Renderbuffer Object)
4.5.1.1 纹理附件
  • 当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入到这个纹理中,就像它是一个普通的颜色/深度或模板缓冲一样。
  • 使用纹理的优点是,所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。
  • 为帧缓冲创建一个纹理和创建一个普通的纹理差不多:
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
  • 主要的区别就是,我们将维度设置为了屏幕大小(尽管这不是必须的),并且我们给纹理的data参数传递了NULL。对于这个纹理,我们仅仅分配了内存而没有填充它。填充这个纹理将会在我们渲染到帧缓冲之后来进行。同样注意我们并不关心环绕方式或多级渐远纹理,我们在大多数情况下都不会需要它们。
  • 现在我们已经创建好一个纹理了,要做的最后一件事就是将它附加到帧缓冲上了:
    在这里插入图片描述
  • 也可以将深度缓冲和模板缓冲附加为一个单独的纹理。纹理的每32位数值将包含24位的深度信息和8位的模板信息。要将深度和模板缓冲附加为一个纹理的话,我们使用GL_DEPTH_STENCIL_ATTACHMENT类型,并配置纹理的格式,让它包含合并的深度和模板值。将一个深度和模板缓冲附加为一个纹理到帧缓冲的例子可以在下面找到:
    glTexImage2D(
      GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, 
      GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
    );
    
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
    
4.5.1.2 渲染缓冲对象附件
  • 渲染缓冲对象(Renderbuffer Object)是在纹理之后引入到OpenGL中,作为一个可用的帧缓冲附件类型的,所以在过去纹理是唯一可用的附件。
  • 和纹理图像一样,渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。
  • 渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。
  • 因为它的数据已经是原生的格式了,当写入或者复制它的数据到其它缓冲中时是非常快的。所以,交换缓冲这样的操作在使用渲染缓冲对象时会非常快。
  • 我们在每个渲染迭代最后使用的glfwSwapBuffers,也可以通过渲染缓冲对象实现:只需要写入一个渲染缓冲图像,并在最后交换到另外一个渲染缓冲就可以了。渲染缓冲对象对这种操作非常完美。
  • 创建一个渲染缓冲对象的代码和帧缓冲的代码很类似:
    unsigned int rbo;
    glGenRenderbuffers(1, &rbo);
    
  • 类似,我们需要绑定这个渲染缓冲对象,让之后所有的渲染缓冲操作影响当前的rbo:
    glBindRenderbuffer(GL_RENDERBUFFER, rbo);
    
  • 由于渲染缓冲对象通常都是只写的,它们会经常用于深度和模板附件。我们需要深度和模板值用于测试,但不需要对它们进行采样,所以渲染缓冲对象非常适合它们。当我们不需要从这些缓冲中采样的时候,通常都会选择渲染缓冲对象,因为它会更优化一点。
  • 创建一个深度和模板渲染缓冲对象可以通过调用glRenderbufferStorage函数来完成:
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
    
  • 创建一个渲染缓冲对象和纹理对象类似,不同的是这个对象是专门被设计作为帧缓冲附件使用的,而不是纹理那样的通用数据缓冲(General Purpose Data Buffer)。这里我们选择GL_DEPTH24_STENCIL8作为内部格式,它封装了24位的深度和8位的模板缓冲。
  • 最后一件事就是附加这个渲染缓冲对象:
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
    
  • 如果你不需要从一个缓冲中采样数据,那么对这个缓冲使用渲染缓冲对象会是明智的选择。如果你需要从缓冲中采样颜色或深度值等数据,那么你应该选择纹理附件。性能方面它不会产生非常大的影响的。

4.5.2 渲染到纹理

  • 此部分即为以上内容整合的全流程,看官网与代码即可
  • 要注意一些事情。
    • 第一,由于我们使用的每个帧缓冲都有它自己一套缓冲,我们希望设置合适的位,调用glClear,清除这些缓冲。
    • 第二,当绘制四边形时,我们将禁用深度测试,因为我们是在绘制一个简单的四边形,并不需要关系深度测试。在绘制普通场景的时候我们将会重新启用深度测试。
      在这里插入图片描述

4.5.3 后期处理

  • 通过屏幕的像素着色器进行
  • 反相
  • 灰度
  • 核效果
    • 锐化
    • 模糊
    • 边缘检测