learnOpenGL 4.5 帧缓冲

244 阅读4分钟

到目前为止,我们已经使用了很多屏幕缓冲了:用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲(Framebuffer)。

实际上我们平时做的一些操作都是在默认帧缓冲中的渲染缓冲(Renderbuffer)中进行的,这里默认的帧缓冲是在创建窗口的时候GLFW生成和配置的。

创建帧缓冲

第一步创建和绑定与其他缓冲类似。

    unsigned int framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

纹理附件

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

创建过程和普通纹理类似,主要的区别就是,我们将维度设置为了屏幕大小,并且我们给纹理的data参数传递了NULL。对于这个纹理,我们仅仅分配了内存而没有填充它。
之所以这样,是因为希望用它来装载整个屏幕的渲染结果。

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

创建后,我们把它附加到帧缓冲上

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

这个函数的第二个参数表示纹理为颜色附件(通常如此),在有需要时也可以设为深度或模板附件。

渲染缓冲

渲染缓冲对象通常都是只写的,它们会经常用于深度和模板附件,因为大部分时间我们都不需要从深度和模板缓冲中读取值,只关心深度和模板测试。我们需要深度和模板值用于测试,但不需要对它们进行采样,所以渲染缓冲对象非常适合它们。

创建代码如下,

	unsigned int rbo;
    glGenRenderbuffers(1, &rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

glRenderbufferStorage分配了一片存储空间,其中我们选择GL_DEPTH24_STENCIL8作为内部格式,它封装了24位的深度和8位的模板缓冲。
我们将渲染缓冲对象附加到帧缓冲的深度和模板附件上。

我们希望检查帧缓冲是否是完整的,如果不是,我们将打印错误信息。
最后,激活它。

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

渲染到纹理

我们希望最终把整个场景渲染到一个和屏幕一样大的平面纹理上。所以我们要提前创建一个屏幕大小的平面quadVAO。

同时,我们也要为它另外编写一套shader,在不添加其他效果的情况,它只需要渲染一个平面的着色器。

下面是实际渲染循环中的代码。

// 第一处理阶段(Pass)
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); // 返回默认
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);  

后期效果

现在在着色器中整个场景只是一个纹理,很容易进行各种处理,下面举例一些效果。

  • 反相
void main()
{
    FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}

在这里插入图片描述

  • 灰度
void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
    FragColor = vec4(average, average, average, 1.0);
}

在这里插入图片描述

  • 核效果:锐化

核(Kernel)(或卷积矩阵(Convolution Matrix))是一个类矩阵的数值数组(如下图),它的中心为当前的像素,它会用它的核值乘以周围的像素值,并将结果相加变成一个值。
通过利用和改变核,我们可以做出多种有趣的效果。
在这里插入图片描述

const float offset = 1.0 / 300.0;  

void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // 左上
        vec2( 0.0f,    offset), // 正上
        vec2( offset,  offset), // 右上
        vec2(-offset,  0.0f),   // 左
        vec2( 0.0f,    0.0f),   // 中
        vec2( offset,  0.0f),   // 右
        vec2(-offset, -offset), // 左下
        vec2( 0.0f,   -offset), // 正下
        vec2( offset, -offset)  // 右下
    );

    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 = vec3(0.0);
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];

    FragColor = vec4(col, 1.0);
}

在这里插入图片描述

  • 核效果:模糊
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  
);

在这里插入图片描述

  • 核效果:边缘检测
    float kernel[9] = float[](
    1.0f,1.0f, 1.0f,
    1.0f, -8.0f, 1.0f,
    1.0f, 1.0f, 1.0f  
);

在这里插入图片描述