OpenGL ES 入门 -- 帧缓存

2,612 阅读6分钟

如果不想安逸的被淘汰,那就奋不顾身的去努力

OpenGL ES 入门 -- 绘制图形

OpenGL ES 入门 -- 渲染图片

OpenGL ES 入门 -- 变换

OpenGL ES 入门 -- 立方体贴图

前言

这段时间一直在LearnOpenGL CN学习,在学习帧缓冲的时候,碰到了一点小问题,找了好久好久,后来终于在落影大佬的博客找到了一丝线索,

帧缓存概念

帧缓存区对象是一组颜色、深度和模板纹理或者渲染目标,各种2D图像可以连接到帧缓冲区对象中的颜色附着点,这些附着点包括一个渲染缓冲区对象,它保存颜色值、2D纹理或者立方图面的mip级别、2D数组纹理的层次,甚至3D纹理中一个2D切片的mip级别,同样,包含深度值的各种2D图像可以连接到FBO的深度附着点,可以连接到FBO模板附着点的唯一2D图像是保存模板值的渲染缓冲区对象。一个帧缓冲区对象中只能有一个颜色、深度、模板缓冲区

我们之前所做的所有操作都是在默认帧缓冲的渲染缓冲上进行的,默认的帧缓冲是在你创建和配置GLKView的时候,默认帮我们做了这些,有了我们自己的帧缓冲,我们做出很酷的效果。

创建绑定帧缓冲

glGenFramebuffers(1, &_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, _fbo);

和OpenGL其他对象一样,我们会使用glGenFramebuffers函数来创建一个帧缓冲对象,这种创建和使用对象的方式我们已经见过很多次了,所以它的使用函数和其他对象也很类似,创建以后我们需要添加对应的附件,我们得确保得到一个完整的帧缓冲才能使用,得满足下面几个条件:

  • 附加至少一个缓冲(颜色、深度或者模板缓冲)
  • 至少有一个颜色附件
  • 所有的附件都必须是完整的
  • 每个缓冲都应该有相同的样本数

在完成所有的条件以后,我们可以使用glCheckFramebufferStatus函数检测帧缓冲是否完整,如果返回的状态status == GL_FRAMEBUFFER_COMPLETE,帧缓冲就是完整的了。

if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"ERROR::FRAMEBUFFER:: Framebuffer is not complete! = %d",glCheckFramebufferStatus(GL_FRAMEBUFFER));
}

之后所有的渲染操作都会渲染在当前绑定的渲染缓冲的附件中,由于我们创建的帧缓冲不是默认帧缓冲,所以渲染指令不会对当前窗口的视觉输出造成任何影响,要保证所有的渲染操作在主窗口有效,我们需要再次激活默认的帧缓冲,glBindFramebuffer(GL_FRAMEBUFFER, _fbo);在完成所有的帧缓存操作后,记住删除这个帧缓冲对象,glDeleteFramebuffers(1, &fbo);

当创建一个附件的时候我们可以选择纹理或者渲染缓冲对象

纹理附件

当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写到这个纹理中,使用纹理的优点是,所有的渲染结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便的使用它。

为帧缓冲创建一个纹理和创建一个普通纹理差不多~

如上图,在生成纹理的时候,不同的是将维度设置成了屏幕的大小(这不是必须的),并且对于Data参数传递了Null,我们仅仅是申请分配了内存而没有进行填充,填充这个纹理将在我们渲染帧缓冲之后。

如果你想将你的屏幕渲染到一个更大或者更小的纹理上,则需要在渲染到帧缓冲之前,
通过调用glViewport,使用纹理的新维度作为参数,否则只有一部分的纹理渲染到屏幕上。

现在我们已经创建好一个纹理,然后我们需要将纹理附加到帧缓冲,

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

glFrameBufferTexture2D有以下的参数:

  • target:帧缓冲的目标
  • attachment:附件类型,可以添加多个颜色附件。
  • textarget:附加的纹理类型
  • texture:附加的纹理对象
  • level:多级减员纹理的级别,保留为0

除了颜色附件之外,我们还可以添加深度和模板缓冲纹理到帧缓冲对象中去,如果要附加深度缓冲的话,要将附件类型设置为GL_DEPTH_ATTACHMENT,注意纹理的格式和内部格式类型将变为GL_DEPTH_COMPONENT,来反映深度缓冲的储存格式,要附加模板缓冲的话,要将附件类型设置为GL_STENCIL_ATTACHMENT,并将纹理的格式设定为GL_STENCIL_INDEX

渲染缓冲对象附件

渲染缓冲对象是在纹理以后引入到OpenGL中,渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。

创建一个渲染缓冲对象:

创建一个深度和模板渲染缓冲对象可以通过glRenderbufferStorage函数来完成:

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

最后一件事就是附加这个渲染缓冲对象:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

通常如果你不需要从一个缓冲中采样数据,那么对这个缓冲使用渲染缓冲对象是明智的选择,如果你需要从缓冲采样颜色或者深度值等数据,那么你应该选择纹理附件,性能方面不会产生非常大的影响。

渲染

当使用glCheckFramebufferStatus函数检查帧缓冲的完整性后,在渲染的时候我们只需要绑定这个帧缓冲对象,让渲染到帧缓冲的缓冲中而不是默认的缓冲中,之后渲染指令将会影响当前绑定的帧缓冲,所有的深度和模板操作都会从当前绑定的帧缓冲的深度和模板附件中读取,如果你忽略了深度缓冲,那么所有的深度测试操作将不再工作,因为当前绑定的帧缓冲不存在深度缓冲。

所以想要绘制场景到一个纹理上,我们需要采取以下的步骤:

  • 将新的帧缓冲绑定为激活的帧缓冲,和往常一样渲染场景。
  • 绑定默认的帧缓冲
  • 绘制一个填满整个屏幕的四边形,将帧缓冲的颜色作为它的纹理

大致流程如下:

// 第一处理阶段(渲染自定义的帧缓冲)
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 我们现在不使用模板缓冲
glEnable(GL_DEPTH_TEST);
DrawScene();    

// 第二处理阶段
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 返回默认
[_glkView bindDrawable];
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);

glUseProgram(_program) 
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);  

大致流程如上,是一个关于帧缓存的Demo,还可以自己动手实现更多更酷的效果,欢迎大家自己下载动手玩玩~

Demo下载地址,欢迎交流

遇到的问题

其中文章开头中提到的问题困扰了很久,什么问题呢? 参考的是LearnOpenGLES官网的案例,然后怎么实现屏幕都是黑色,再三检查着色器、帧缓冲的创建代码等等,实在找不到问题,最后在落影大佬的Demo里找到[xxxxx bindDrawable],最终才解决问题,bindDrawable API是GLKView将基础 framebuffer 对象重新绑定到 OpenGL ES。