帧缓冲

到目前为止,我们已经掌握了很多缓冲,比如用来储存顶点的GL_ARRAY_BUFFER,用来储存顶点索引的GL_ELEMENT_ARRAY_BUFFER以及储存颜色的颜色缓冲,储存深度信息的深度缓冲等等,这些诸多缓冲其实就是构成每一帧画面的诸多要素,所以故而便存在一个储存这些所有缓冲的缓冲,便是帧缓冲。

和其他缓冲一样,帧缓冲的创建也是先创建,然后再绑定。glGenFramebuffers用来创建一个帧缓冲,再将帧缓冲用glBindFramebuffer绑定到GL_FRAMEBUFFER之后,便可以开始对其进行修改。刚创建好的帧缓冲其实是不完整的,一个完整的帧缓冲需要满足以下的条件:

  • 附加至少一个缓冲(颜色、深度或模板缓冲)
  • 至少有一个颜色缓冲附件(color-attachment)
  • 所有的附件都必须是完整的(保留了内存)
  • 每个缓冲都应该有相同的样本数。

附件

附件其实可以理解为一段内存,也即是在计算机内存中真实的内存空间,一个帧缓冲只有拥有附件的时候才是有效的。附件也分为纹理附件和缓冲对象附件。

  1. 纹理附件

纹理附件顾名思义,就是利用纹理的格式作为帧缓冲的内存,它可以将帧缓冲里面储存的东西以纹理的形式保存下来,这样使用起来也可以按照纹理的格式去使用。首先需要创建一个空的纹理,我们之前创建的纹理都是先用stbi_image导入图像后再将图像导入进纹理,但是这里由于我们事先没有纹理数据,所以只能创建一个空的纹理glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,widht,height,GL_RGB,GL_UNSIGNED_BYTE,NULL)。创建完成后再设置纹理必要参数,例如环绕模式,纹理过滤器,多级渐远纹理等等。之后我们就可以将纹理绑定在我们创建好的framebuffer上了,glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture,0)

  • target:帧缓冲的目标(一般都是GL_FRAMEBUFFER)
  • attachment:我们想要附加的附件类型,颜色附件就是GL_COLOR_ATTACHMENTN,注意这里的N表示附加很多个颜色纹理附件,就和纹理中的GL_TEXUTURE0(1,2,3…)一样,可以有很多个。
  • textarget:希望附加的纹理类型,一般都是2D纹理,所以就是GL_TEXTURE_2D。
  • texuture:要附加的纹理本身。
  • level:多级渐远纹理级别,这里设置为最近一级也即是0即可。

当然除了颜色可以用纹理的格式存储外,深度和模板都是可以的,如果要存储深度,那么纹理格式和内部格式就要改成GL_DEPTH_COMPONENT,并且绑定时的附件类型就要变成GL_DEPTH_ATTACHMENT;如果要存储模板缓冲,那么纹理格式就要改成GL_STENCIL_INDEX绑定时附件类型改为GL_STENCIL_ATTACHMENT。同时深度与模板可以合并一个纹理附加到帧缓冲中,纹理格式为GL_DEPTH24_STENCIL8,格式的含义是用24位来存储深度信息,用8位来存储模板信息。同时附件类型变为GL_DEPTH_STENCIL_ATTACHMENT

  1. 渲染缓冲对象附件(Renderbuffer Object)

在引入渲染缓冲对象之前,纹理是唯一可用的附件,所以所有的信息都需要转换位纹理再使用。但是纹理并不是针对帧缓冲设计的,所以转换起来需要耗费一定时间。所以渲染缓冲便是针对帧缓冲而设计的。渲染缓冲直接将所有的渲染数据存储到它里面,所以它可以很快速的被写入或者复制到其他缓冲中去,所以像交换缓冲(即每次最后进行的glfwSwapBuffers)便可以用渲染缓冲对象实现。但是渲染缓冲对象是只写的,所以一般用于只需要写操作的缓冲,例如深度缓存和模板缓存。

渲染缓冲对象用glGenRenderbuffers创建,然后glBindRenderbuffer之后,便可以创建它真正的缓冲,即内存中的位置glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH24_STENCIL8,width,height),最后也是用glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DPETH_STENCIL_ATTACHMENT,GL_RENDERBUFFER,rbo)来将渲染缓冲对象绑定到帧缓冲上面,这里注意到第三个参数变为了GL_RENDERBUFFER,这表明附件是使用渲染缓冲作为存储的(其实从函数名已经可以看出来了)。

开始渲染

有了缓冲附件,那么我们的帧缓冲就已经完整了,当然在使用前还可以用glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE来检查帧缓冲是否完整。首先我们要先创建一个帧缓冲:

1
2
unsigned int FBO;
glGenFramebuffers(1,&FBO);

之后就可以开始补充附件了,我们先创建颜色缓冲所需单独附件,这里我们选择纹理附件,因为写入纹理之后我们还需要将纹理渲染到其他图形上去。

1
2
3
4
5
6
7
unsigned int texture_pic;
glGenTextures(1,&texture_pic);
glBindTexture(GL_TEXTURE_2D,texture_pic);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,1200,1200,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);
glBindTexture(GL_TEXTURE_2D,0);

这里我们选择1200X1200的纹理尺寸,然后只设置纹理过滤,纹理环绕没有设置。之后便要设置深度缓冲和模板缓冲了,由于深度缓冲和模板缓冲我们不需要再使用,只要openGL自动进行模板测试和深度测试就可以了,所以我们选择缓冲对象附件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned int RBO;
glGenRenderbuffers(1,&RBO);
glBindRenderbuffer(GL_RENDERBUFFER,RBO);
glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH24_STENCIL8,1200,1200);
glBindRenderbuffer(GL_RENDERBUFFER,0);
~~~~
这里设置的缓冲对象大小也是1200X1200和颜色缓冲对应,之后我们只需要将纹理附件和缓冲对象附件一并绑定到帧缓冲即可,最后还可以检查帧缓冲是否完整。
~~~C++
glBindFramebuffer(GL_FRAMEBUFFER,FBO);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture_pic,0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_STENCIL_ATTACHMENT,GL_RENDERBUFFER,RBO);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE){
cout<<"FrameBuffer is not complete!"<<endl;
}
glBindFramebuffer(GL_FRAMEBUFFER,0);

之后的阶段其实和之前渲染是完全一样的,只不过由于绑定了新的帧缓冲,所以渲染的最终结果不会显示到屏幕上而是被储存到了帧缓冲里面,所以这也叫离屏缓冲。这里还需要注意的是,在开始渲染之前还需要使用glViewport(0,0,1200,1200)来调整视口变换,因为我们这里的帧缓冲大小设置的是1200X1200,也就是相当于把屏幕画面放缩到了1200X1200,但是我们之前屏幕画面是800X600,所以如果不进行新的视口变化调整,渲染出来的画面会出问题。同样的,调整了视口变化,那么投影矩阵也要相对应进行调整Projection=perspective(radians(45.f),1200*1.f/1200*1.f,0.1f,1000.f)这样画面才不会被拉伸。
最后我们将离屏渲染出来的纹理绑定到一个矩形上,然后在默认的屏幕帧缓冲上渲染出这个矩形,那么将会看到这样的画面,就好像是投影在了一块屏幕上的感觉。