OpenGL核心技术之帧缓冲

笔者介绍:姜雪伟。IT公司技术合伙人,IT高级讲师。CSDN社区专家。特邀编辑。畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术具体解释》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

本篇博文主要是给读者解密关于游戏后处理渲染效果的原理,后处理渲染效果在Unity,UE4虚幻引擎等商业引擎 使用的许多。

比方Bloom,Blur。SSAO。PSSM。HDR等等都属于后处理渲染效果。它们的实现事实上就是应用帧缓冲技术实现的。本篇博文主要是

环绕帧缓冲给读者介绍事实上现原理以及应用案例。

在前面给读者介绍了几种不同的屏幕缓冲:用于写入颜色值的颜色缓冲,用于写入深度信息的深度缓冲。以及同意我们基于一些条件丢弃指定片段的模板缓冲。本篇博客主要是给读者介绍帧缓冲。什么是帧缓冲?事实上就是把前面介绍的这几种缓冲结合起来叫做帧缓冲(Framebuffer),它被储存于内存中。

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上,就同意进行渲染、清空和其它的写入操作。在此给读者总结一下构建一个完整的帧

缓冲满足的条件:

建构一个完整的帧缓冲必须满足以下条件:

  • 我们必须往里面加入至少一个附件(颜色、深度、模板缓冲)。
  • 当中至少有一个是颜色附件。
  • 全部的附件都应该是已经全然做好的(已经存储在内存之中)。

  • 每一个缓冲都应该有相同数目的样本。

上面的条件提到了样本。假设你不知道什么是样本也不用操心,我们会在后面的博文中讲到。

我们须要为帧缓冲创建一些附件(Attachment),还须要把这些附件附加到帧缓冲上。当我们做全然部上面提到的条件的时候我们就能够用 glCheckFramebufferStatus 带上 GL_FRAMEBUFFER 这个參数来检查是否真的成功做到了。

然后检查当前绑定的帧缓冲。返回了这些规范中的哪个值。

假设返回的是 GL_FRAMEBUFFER_COMPLETE就对了:

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
  // Execute victory dance
兴许全部渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中,由于我们的帧缓冲不是默认的帧缓冲。渲染命令对窗体的视频输出不会产生不论什么影响。出于这个原因,它被称为离屏渲染(off-screen rendering),就是渲染到一个另外的缓冲中。为了让全部的渲染操作对主窗体产生影响我们必须通过绑定为0来使默认帧缓冲被激活:

glBindFramebuffer(GL_FRAMEBUFFER, 0);
当我们做全然部帧缓冲操作,不要忘记删除帧缓冲对象:

glDeleteFramebuffers(1, &fbo);

如今在运行完毕检測前,我们须要把一个也许多其它的附件附加到帧缓冲上。

一个附件就是一个内存地址,这个内存地址里面包括一个为帧缓冲准备的缓冲,它能够是个图像。当创建一个附件的时候我们有两种方式能够採用:纹理或渲染缓冲(renderbuffer)对象。

接下来介绍纹理当把一个纹理附加到帧缓冲上的时候,全部渲染命令会写入到纹理上,就像它是一个普通的颜色/深度或者模板缓冲一样。使用纹理的优点是,全部渲染操作的结果都会被储存为一个纹理图像,这样我们就能够简单的在着色器中使用了。

创建一个帧缓冲的纹理和创建普通纹理几乎相同:

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);
这里基本的差别是我们把纹理的维度设置为屏幕大小(虽然不是必须的)。我们还传递NULL作为纹理的data參数。

对于这个纹理。

我们仅仅分配内存,而不去填充它。纹理填充会在渲染到帧缓冲的时候去做。

假设你打算把整个屏幕渲染到一个或大或小的纹理上。你须要用新的纹理的尺寸作为參数再次调用glViewport(要在渲染到你的帧缓冲之前

做好),否则仅仅有一小部分纹理或屏幕能够绘制到纹理上。

如今我们已经创建了一个纹理。最后一件要做的事情是把它附加到帧缓冲上:

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);
再介绍渲染缓冲对象OpenGL引进了渲染缓冲对象(Renderbuffer objects)。所以在过去那些美好时光里纹理是附件的唯一可用的类型。和纹理图像一样。渲染缓冲对象也是一个缓冲。它能够是一堆字节、整数、像素或者其它东西。渲染缓冲对象的一大优点是,它以OpenGL原生渲染格式储存它的数据,因此在离屏渲染到帧缓冲的时候,这些数据就相当于被优化过的了。

渲染缓冲对象将全部渲染数据直接储存到它们的缓冲里。而不会进行针对特定纹理格式的不论什么转换。这样它们就成了一种快速可写的存储介质了。

然而,渲染缓冲对象一般是仅仅写的,不能改动它们(就像获取纹理,不能写入纹理一样)。能够用glReadPixels函数去读取,函数返回一个当前绑定的帧缓冲的特定像素区域,而不是直接返回附件本身。

由于它们的数据已经是原生格式了,在写入或把它们的数据简单地到其它缓冲的时候很快。

当使用渲染缓冲对象时。像切换缓冲这种操作变得异常快速。我们在每一个渲染迭代末尾使用的那个glfwSwapBuffers函数,相同以渲染缓冲对象实现:我们简单地写入到一个渲染缓冲图像。最后交换到还有一个里。渲染缓冲对象对于这种操作来说很完美。

创建一个渲染缓冲对象和创建帧缓冲代码几乎相同:

GLuint rbo;
glGenRenderbuffers(1, &rbo);

类似地,我们打算把渲染缓冲对象绑定,这样全部兴许渲染缓冲操作都会影响到当前的渲染缓冲对象:
glBindRenderbuffer(GL_RENDERBUFFER, rbo);

由于渲染缓冲对象一般是仅仅写的。它们常常作为深度和模板附件来使用。由于大多数时候,我们不须要从深度和模板缓冲中读取数据,但仍关心深度和模板測试。我们就须要有深度和模板值提供给測试,但不须要对这些值进行採样(sample)。所以深度缓冲对象是全然符合的。

当我们不去从这些缓冲中採样的时候。渲染缓冲对象通常很合适。由于它们等于是被优化过的。

调用glRenderbufferStorage函数能够创建一个深度和模板渲染缓冲对象:

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

创建一个渲染缓冲对象与创建纹理对象类似。不同之处在于这个对象是专门被设计用于图像的,而不是通用目的的数据缓冲,比方纹理。这里我们选择GL_DEPTH24_STENCIL8作为内部格式。它同一时候代表24位的深度和8位的模板缓冲。

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

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

在帧缓冲项目中,渲染缓冲对象能够提供一些优化。但更重要的是知道何时使用渲染缓冲对象,何时使用纹理。

通常的规则是,假设你永远都不须要从特定的缓冲中进行採样,渲染缓冲对象对特定缓冲是更明智的选择。

假设哪天须要从比方颜色或深度值这种特定缓冲採样数据的话。你不妨使用纹理附件。从运行效率角度考虑,它不会对效率有太大影响。



以下通过案例的方式介绍怎样使用帧缓存,我们会把场景渲染到一个颜色纹理上,这个纹理附加到一个我们创建的帧缓冲上。然后把纹

理绘制到一个简单的四边形上,这个四边形铺满整个屏幕。

输出的图像看似和没用帧缓冲一样,可是这次,它事实上是直接打印到了一个单独的

四边形上面。

为什么这很实用呢?下一部分我们会看到原因。

第一件要做的事情是创建一个帧缓冲对象,并绑定它,这比較明了:

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
第二件要做的事情是我们创建一个纹理图像,这是我们将要附加到帧缓冲的颜色附件。我们把纹理的尺寸设置为窗体的宽度和高度,并保持数据未初始化:

// Generate texture
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);

// Attach it to currently bound framebuffer object
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
我们相同打算要让OpenGL确定能够进行深度測试(模板測试,假设你用的话)所以我们必须还要确保向帧缓冲中加入一个深度(和模板)

附件。

由于我们仅仅採样颜色缓冲,并不採样其它缓冲,我们能够创建一个渲染缓冲对象来达到这个目的。

创建一个渲染缓冲对象不太难。

唯一一件要记住的事情是,我们正在创建的是一个渲染缓冲对象的深度和模板附件。

我们把它的内部给事设置

GL_DEPTH24_STENCIL8,对于我们的目的来说这个准确度已经足够了。


GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

我们为渲染缓冲对象分配了足够的内存空间以后,我们能够解绑渲染缓冲。

接着。在做好帧缓冲之前。还有最后一步,我们把渲染缓冲对象附加到帧缓冲的深度和模板附件上:


glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

然后我们要检查帧缓冲是否真的做好了。假设没有。我们就打印一个错误消息。

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

还要保证解绑帧缓冲,这样我们才不会意外渲染到错误的帧缓冲上。

如今帧缓冲做好了,我们要做的全部就是渲染到帧缓冲上。而不是绑定到帧缓冲对象的默认缓冲。余下全部命令会影响到当前绑定的帧缓冲上。

全部深度和模板操作相同会从当前绑定的帧缓冲的深度和模板附件中读取。当然,得是在它们可用的情况下。假设你遗漏了比方深度缓冲。全部深度測试就不会工作。由于当前绑定的帧缓冲里没有深度缓冲。

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

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

为了绘制四边形我们将会创建新的着色器。我们不打算引入不论什么花哨的变换矩阵。由于我们仅仅提供已经是标准化设备坐标的 顶点坐标 ,所以我们能够直接把它们作为顶点着色器的输出。

顶点着色器看起来像这样:


#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;
}

片段着色器更简洁,由于我们做的唯一一件事是从纹理採样:

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

uniform sampler2D screenTexture;

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


接着须要你为屏幕上的四边形创建和配置一个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);

第一。由于我们用的每一个帧缓冲都有自己的一系列缓冲。我们打算使用glClear设置的合适的位(bits)来清空这些缓冲。

第二,当渲染四边形的时候,我们关闭深度測试,由于我们不关系深度測试,我们绘制的是一个简单的四边形;当我们绘制普通场景时我们必须再次开启深度測试。实现效果例如以下所看到的:



上述案例实现得出的结果是能够自由的获取渲染场景中的不论什么像素。事实上就是把它作为一个纹理图像。

接下来利用帧缓冲实现我们游戏中常常使用的后处理效果,比方游戏中颜色的反相处理。就是把颜色值取反。

这个在片段着色器中处理就可以,在片段着色器里返回这些颜色的反色(Inversion)并不难。我们得到屏幕纹理的颜色,然后用1.0减去它:

void main()
{
    color = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
反相是一种相对简单的后处理特效。实现的效果例如以下所看到的:



这样把整个场景都处理了,这也是后处理渲染实现的效果,这样仅仅须要在上述片段着色器中改动一行代码就可以实现。

以下再继续深入探讨,在单独纹理图像上进行后处理的还有一个优点是我们能够从纹理的其它部分进行採样。

比方我们能够从当前纹理值的周围採样多个纹理值。

创造性地把它们结合起来就能创造出有趣的效果了。

kernel是一个长得有点像一个小矩阵的数值数组。它中间的值中心能够映射到一个像素上,这个像素和这个像素周围的值再乘以kernel,最后再把结果相加就能得到一个值。所以,我们基本上就是给当前纹理坐标加上一个它四周的偏移量,然后基于kernel把它们结合起来。以下是一个kernel的样例:


这个kernel表示一个像素周围八个像素乘以2。它自己乘以-15。

这个样例基本上就是把周围像素乘上2,中间像素去乘以一个比較大的负数来进行平衡。kernel对于后处理来说很管用。由于用起来简单。网上能找到有许多实例。为了能用上kernel我们还得改改片段着色器。

这里假设每一个kernel都是3×3(实际上大多数都是3×3):

const float offset = 1.0 / 300;  

void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset, offset),  // top-left
        vec2(0.0f,    offset),  // top-center
        vec2(offset,  offset),  // top-right
        vec2(-offset, 0.0f),    // center-left
        vec2(0.0f,    0.0f),    // center-center
        vec2(offset,  0.0f),    // center-right
        vec2(-offset, -offset), // bottom-left
        vec2(0.0f,    -offset), // bottom-center
        vec2(offset,  -offset)  // bottom-right
    );

    float kernel[9] = float[](
        -1, -1, -1,
        -1,  9, -1,
        -1, -1, -1
    );

    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
    }
    vec3 col;
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];

    color = vec4(col, 1.0);
}

在片段着色器中我们先为每一个四周的纹理坐标创建一个9个vec2偏移量的数组。偏移量是一个简单的常数。你能够设置为自己喜欢的。接着我们定义kernel,这里应该是一个锐化kernel。它通过一种有趣的方式从全部周边的像素採样。对每一个颜色值进行锐化。最后,在採样的时候我们把每一个偏移量加到当前纹理坐标上,然后用加在一起的kernel的值乘以这些纹理值。

这个锐化的kernel看起来像这样:


再举个样例关于模糊(Blur)效果的Kernel定义例如以下:


由于全部数值加起来的总和为16,简单返回结合起来的採样颜色是很亮的,所以我们必须将kernel的每一个值除以16.终于的kernel数组会是这种:

float kernel[9] = float[](
    1.0 / 16, 2.0 / 16, 1.0 / 16,
    2.0 / 16, 4.0 / 16, 2.0 / 16,
    1.0 / 16, 2.0 / 16, 1.0 / 16  
);
通过在像素着色器中改变kernel的float数组,我们就全然改变了之后的后处理效果.如今看起来会像是这样:



这种模糊效果具有创建许多有趣效果的潜力,模糊效果在后处理中使用的许多。它会结合着Bloom后处理渲染使用。

模糊也能为我们在后面的教程中提供都颜色值进行平滑处理的能力。

最后把关于帧缓冲的顶点着色器和片段着色器代码分别给读者展演示样例如以下:

顶点着色器代码:

#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;
} 

片段着色器代码例如以下所看到的:

#version 330 core
in vec2 TexCoords;

out vec4 color;

uniform sampler2D screenTexture;

const float offset = 1.0 / 300;  

void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset, offset),  // top-left
        vec2(0.0f,    offset),  // top-center
        vec2(offset,  offset),  // top-right
        vec2(-offset, 0.0f),    // center-left
        vec2(0.0f,    0.0f),    // center-center
        vec2(offset,  0.0f),    // center-right
        vec2(-offset, -offset), // bottom-left
        vec2(0.0f,    -offset), // bottom-center
        vec2(offset,  -offset)  // bottom-right    
    );

    float kernel[9] = float[](
        -1, -1, -1,
        -1,  9, -1,
        -1, -1, -1
    );
    
    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
    }
    vec3 col;
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];
    
    color = vec4(col, 1.0);
} 
另外把在C++中关于处理帧缓存的核心代码给读者展演示样例如以下:

// Setup cube VAO
    GLuint cubeVAO, cubeVBO;
    glGenVertexArrays(1, &cubeVAO);
    glGenBuffers(1, &cubeVBO);
    glBindVertexArray(cubeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glBindVertexArray(0);
    // Setup plane VAO
    GLuint floorVAO, floorVBO;
    glGenVertexArrays(1, &floorVAO);
    glGenBuffers(1, &floorVBO);
    glBindVertexArray(floorVAO);
    glBindBuffer(GL_ARRAY_BUFFER, floorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(floorVertices), &floorVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glBindVertexArray(0);
    // Setup screen VAO
    GLuint quadVAO, quadVBO;
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)(2 * sizeof(GLfloat)));
    glBindVertexArray(0);

    // Load textures
    GLuint cubeTexture = loadTexture(FileSystem::getPath("resources/textures/container.jpg").c_str());
    GLuint floorTexture = loadTexture(FileSystem::getPath("resources/textures/metal.png").c_str());
    #pragma endregion

    // Framebuffers
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);  
    // Create a color attachment texture
    GLuint textureColorbuffer = generateAttachmentTexture(false, false);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);
    // Create a renderbuffer object for depth and stencil attachment (we won't be sampling these)
    GLuint rbo;
    glGenRenderbuffers(1, &rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo); 
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, screenWidth, screenHeight); // Use a single renderbuffer object for both a depth AND stencil buffer.
    glBindRenderbuffer(GL_RENDERBUFFER, 0);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); // Now actually attach it
    // Now that we actually created the framebuffer and added all attachments we want to check if it is actually complete now
    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

每一帧处理的代码例如以下所看到的:

        /////////////////////////////////////////////////////
        // Bind to framebuffer and draw to color texture 
        // as we normally would.
        // //////////////////////////////////////////////////
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        // Clear all attached buffers        
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // We're not using stencil buffer so why bother with clearing?

glEnable(GL_DEPTH_TEST); // Set uniforms shader.Use(); glm::mat4 model; glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // Floor glBindVertexArray(floorVAO); glBindTexture(GL_TEXTURE_2D, floorTexture); model = glm::mat4(); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); // Cubes glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_2D, cubeTexture); model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 36); model = glm::mat4(); model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); ///////////////////////////////////////////////////// // Bind to default framebuffer again and draw the // quad plane with attched screen texture. // ////////////////////////////////////////////////// glBindFramebuffer(GL_FRAMEBUFFER, 0); // Clear all relevant buffers glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // Set clear color to white (not really necessery actually, since we won't be able to see behind the quad anyways) glClear(GL_COLOR_BUFFER_BIT); glDisable(GL_DEPTH_TEST); // We don't care about depth information when rendering a single quad // Draw Screen screenShader.Use(); glBindVertexArray(quadVAO); glBindTexture(GL_TEXTURE_2D, textureColorbuffer); // Use the color attachment texture as the texture of the quad plane glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); // Swap the buffers glfwSwapBuffers(window);


总结:
以上就是关于帧缓冲的介绍。它基本的作用是能够获取到场景像素,后处理就是对场景像素作渲染处理的。所以该技术广泛的被应用在后处理开发中,这也是为读者揭示后处理渲染的本质,希望对大家有所帮助。。

。。



你可能感兴趣的:(OpenGL核心技术之帧缓冲)