OpenGL学习总结(七)

高级OpenGL总结(三)

五、帧缓冲

渲染过程中,图形加速器一般都有一个预先分配好的内存区域来维护显示列表内容(译者:注意,不一定是主内存)。它由显示内存和脱屏内存组成。随着OpenGL的渲染而改变内容的那一部分图形内存区域叫做帧缓存(frame buffer)。在窗口系统里,OpenGL通过帧缓存与窗口通信。窗口系统为OpenGL提供了一组工具来为窗口选择帧缓存特性,而这组工具,通常是系统相关的。

一个支持OpenGL渲染的窗口 (即帧缓存) 可能包含以下的组合:
· 至多4个颜色缓存
· 一个深度缓存
· 一个模板缓存
· 一个积累缓存
· 一个多重采样缓存

  为了能够执行双缓存构架,大多数图形硬件同时支持前后缓存。这将允许应用程序在显示前缓存(可见的)的时候渲染到后缓存(离屏缓存)。当渲染结束的时候,这两个缓存进行交换,以便已经完成渲染的缓存像前缓存一样进行显示,这样渲染就能在后缓存重新开始了。一旦使用双缓存,在绘制过程当中用户将不能看到图像。这种技术通常被用来实现实时交互的平滑动画。

  如果为左眼和右眼各实现一个颜色缓存的话,那么就可以支持立体视觉效果了。双缓存技术由前后缓存来支持。因此一个双缓存的立体视觉将会有4各颜色缓存:前左,前右,后左,后右。一个普通的(非立体的)双缓存窗口将会仅仅有前后两个缓存。一个单缓存的窗口将会只有一个缓存。

  如果绘制3D对象时需要剔除隐藏表面的话,深度缓存是必要的。这个缓存在每个象素上存储了显示对象的深度值。当绘制附加对象的时候,会在每个象素上进行深度比较,这样就能决定新的对象是否可见。

  模板缓存用来进行复杂的掩模(masking)操作。一个复杂的形状可以存储在模板缓存里,然后绘制子序列操作可以使用模板缓存里的内容来决定是否更新象素。

  积累缓存是一个颜色缓存,不过典型地它有比颜色缓存更高的精度。这就允许一些图像通过积累产生一些合成的图像。比如说一个作用就是可以在积累缓存里对一个对象随着他的运动绘制一些帧数。在积累缓存中的象素除以帧数以后,结果图像就展现出了运动模糊效果。相似的技巧也可以用来模拟景深效果以及高质量的全屏抗锯齿。

  而通常的,当一个对象被绘制的时候,对于某个图元是否影像屏幕上的象素,会做一个单独的决议。多重采样缓存正是这样一个缓存,它允许每个渲染的对象在象素内被采样多次,以进行高质量的全屏抗锯齿,而不必对这个对象渲染多次。每个象素内的采样包括:颜色,深度,模板信息。每个象素采样的次数当然是必须的。当窗口包含多重采样缓存的时候,它将不回包括单独的深度或者是模板缓存。随着对象的渲染,颜色样本会被组合生成一个单一的颜色值,然后这个颜色值被传递,并写入到颜色缓存里。因为他们包括窗口中每个象素的多个颜色、深度以及模板样本(通常是4,8或者是16),因此多重采样缓存会消耗相当数量的离屏缓存。

1、创建一个帧缓冲
就像OpenGL中其他对象一样,我们可以使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象(简称FBO):

GLuint fbo;
glGenFramebuffers(1, &fbo);

这种对象的创建和使用的方式我们已经见过不少了,因此它们的使用方式也和之前我们见过的其他对象的使用方式相似。首先我们要创建一个帧缓冲对象,把它绑定到当前帧缓冲,做一些操作,然后解绑帧缓冲。我们使用glBindFramebuffer来绑定帧缓冲:

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

绑定到GL_FRAMEBUFFER目标后,接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。也可以把帧缓冲分开绑定到读或写目标上,分别使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER来做这件事。如果绑定到了GL_READ_FRAMEBUFFER,就能执行所有读取操作,像glReadPixels这样的函数使用了;绑定到GL_DRAW_FRAMEBUFFER上,就允许进行渲染、清空和其他的写入操作。大多数时候你不必分开用,通常把两个都绑定到GL_FRAMEBUFFER上就行。

2、填充帧缓冲


    1、我们必须往里面加入至少一个附件(颜色、深度、模板缓冲)。
    2、其中至少有一个是颜色附件。
    3、所有的附件都应该是已经完全做好的(已经存储在内存之中)。
    4、每个缓冲都应该有同样数目的样本。

从上面的需求中你可以看到,我们需要为帧缓冲创建一些附件(Attachment),还需要把这些附件附加到帧缓冲上。当我们做完所有上面提到的条件的时候我们就可以用 glCheckFramebufferStatus 带上 GL_FRAMEBUFFER 这个参数来检查是否真的成功做到了。然后检查当前绑定的帧缓冲,返回了这些规范中的哪个值。如果返回的是 GL_FRAMEBUFFER_COMPLETE就对了:

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)

后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中,由于我们的帧缓冲不是默认的帧缓冲,渲染命令对窗口的视频输出不会产生任何影响。出于这个原因,它被称为离屏渲染(off-screen rendering),就是渲染到一个另外的缓冲中。为了让所有的渲染操作对主窗口产生影响我们必须通过绑定为0来使默认帧缓冲被激活:

glBindFramebuffer(GL_FRAMEBUFFER, 0);

当我们做完所有帧缓冲操作,不要忘记删除帧缓冲对象:

glDeleteFramebuffers(1, &fbo);

现在在执行完成检测前,我们需要把一个或更多的附件附加到帧缓冲上。一个附件就是一个内存地址,这个内存地址里面包含一个为帧缓冲准备的缓冲,它可以是个图像。当创建一个附件的时候我们有两种方式可以采用:纹理或渲染缓冲(renderbuffer)对象。

3、纹理附件
当把一个纹理附加到帧缓冲上的时候,所有渲染命令会写入到纹理上,就像它是一个普通的颜色/深度或者模板缓冲一样。使用纹理的好处是,所有渲染操作的结果都会被储存为一个纹理图像,这样我们就可以简单的在着色器中使用了。
a、创建纹理附件

GLuint 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);

b、附加到帧缓冲

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

glFramebufferTexture2D函数需要传入下列参数:
target:我们所创建的帧缓冲类型的目标(绘制、读取或两者都有)。
attachment:我们所附加的附件的类型。现在我们附加的是一个颜色附件。需要注意,最后的那个0是暗示我们可以附加1个以上颜色的附件。我们会在后面的教程中谈到。
textarget:你希望附加的纹理类型。
texture:附加的实际纹理。
level:Mipmap level。我们设置为0

除颜色附件以外,我们还可以附加一个深度和一个模板纹理到帧缓冲对象上。为了附加一个深度缓冲,我们可以用GL_DEPTH_ATTACHMENT作为附件类型。记住,这时纹理格式和内部格式类型(internalformat)就成了 GL_DEPTH_COMPONENT深度缓冲的存储格式。附加一个模板缓冲,你要使用 GL_STENCIL_ATTACHMENT作为第二个参数,把纹理格式指定为 GL_STENCIL_INDEX。

也可以同时附加一个深度缓冲和一个模板缓冲为一个单独的纹理。这样纹理的每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、缓冲对象附件
和纹理图像一样,渲染缓冲对象也是一个缓冲,它可以是一堆字节、整数、像素或者其他东西。渲染缓冲对象的一大优点是,它以OpenGL原生渲染格式储存它的数据,因此在离屏渲染到帧缓冲的时候,这些数据就相当于被优化过的了。

渲染缓冲对象将所有渲染数据直接储存到它们的缓冲里,而不会进行针对特定纹理格式的任何转换,这样它们就成了一种快速可写的存储介质了。然而,渲染缓冲对象通常是只写的,不能修改它们(就像获取纹理,不能写入纹理一样)。可以用glReadPixels函数去读取,函数返回一个当前绑定的帧缓冲的特定像素区域,而不是直接返回附件本身。

因为它们的数据已经是原生格式了,在写入或把它们的数据简单地到其他缓冲的时候非常快。当使用渲染缓冲对象时,像切换缓冲这种操作变得异常高速。我们在每个渲染迭代末尾使用的那个glfwSwapBuffers函数,同样以渲染缓冲对象实现:我们简单地写入到一个渲染缓冲图像,最后交换到另一个里。渲染缓冲对象对于这种操作来说很完美。

a、创建一个渲染缓冲对象和创建帧缓冲代码差不多:

GLuint rbo;
glGenRenderbuffers(1, &rbo);

b、把渲染缓冲对象绑定,这样所有后续渲染缓冲操作都会影响到当前的渲染缓冲对象:

glBindRenderbuffer(GL_RENDERBUFFER, rbo);

c、调用glRenderbufferStorage函数可以创建一个深度和模板渲染缓冲对象:

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

d、最后一件还要做的事情是把帧缓冲对象附加上:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

注:在帧缓冲项目中,渲染缓冲对象可以提供一些优化,但更重要的是知道何时使用渲染缓冲对象,何时使用纹理。通常的规则是,如果你永远都不需要从特定的缓冲中进行采样,渲染缓冲对象对特定缓冲是更明智的选择。如果哪天需要从比如颜色或深度值这样的特定缓冲采样数据的话,你最好还是使用纹理附件。从执行效率角度考虑,它不会对效率有太大影响。

5、将场景渲染到纹理
a、第一件要做的事情是创建一个帧缓冲对象,并绑定它,这比较明了:

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

b、我们创建一个纹理图像,这是我们将要附加到帧缓冲的颜色附件。我们把纹理的尺寸设置为窗口的宽度和高度,并保持数据未初始化:

GLuint texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
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);
glBindTexture(GL_TEXTURE_2D, 0);

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

c、创建渲染缓冲对象——模板、深度缓冲对象附件

GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);
//我们为渲染缓冲对象分配了足够的内存空间以后,我们可以解绑渲染缓冲。

d、我们把渲染缓冲对象附加到帧缓冲的深度和模板附件上:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

e、查找错误并解绑帧缓冲

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
 cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

现在帧缓冲做好了,我们要做的全部就是渲染到帧缓冲上,而不是绑定到帧缓冲对象的默认缓冲。余下所有命令会影响到当前绑定的帧缓冲上。所有深度和模板操作同样会从当前绑定的帧缓冲的深度和模板附件中读取,当然,得是在它们可用的情况下。如果你遗漏了比如深度缓冲,所有深度测试就不会工作,因为当前绑定的帧缓冲里没有深度缓冲。

所以,为把场景绘制到一个单独的纹理,我们必须以下面步骤来做:

1、使用新的绑定为激活帧缓冲的帧缓冲,像往常那样渲染场景。
2、绑定到默认帧缓冲。
3、绘制一个四边形,让它平铺到整个屏幕上,用新的帧缓冲的颜色缓冲作为他的纹理。

f、为了绘制四边形我们将会创建新的着色器。我们不打算引入任何花哨的变换矩阵,因为我们只提供已经是标准化设备坐标的顶点坐标,所以我们可以直接把它们作为顶点着色器的输出。顶点着色器看起来像这样:

#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
    TexCoords = texCoords;
}

g、片段着色器更简洁,因为我们做的唯一一件事是从纹理采样:

#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D screenTexture;

void main()
{
    color = texture(screenTexture, TexCoords);
}

h、接着需要你为屏幕上的四边形创建和配置一个VAO。渲染迭代中帧缓冲处理会有下面的结构:

// First pass
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // We're not using stencil buffer now
glEnable(GL_DEPTH_TEST);
DrawScene();

// Second pass
glBindFramebuffer(GL_FRAMEBUFFER, 0); // back to default
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

screenShader.Use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);

你可能感兴趣的:(OpenGL学习总结,opengl,图形)