OpenGl L17帧缓冲

一.帧缓冲

写入颜色的叫做颜色缓冲,写入深度信息的叫做深度缓冲,允许丢弃特定片段的叫做模板缓冲。把这些缓冲结合起来叫做帧缓冲,OpenGl允许我们定义自己的帧缓冲。
我们目前的操作都是在默认帧缓冲的渲染缓冲上进行的(在创建窗口的时候生成和配置的),而有了我们的帧缓冲之后,就可以有更多方式来渲染了。

1.创建一个帧缓存

  • glGenFramebuffers:创建一个帧缓冲对象
//和我们其他的GL对象一样
unsigned int fbo;
glGenFramebuffers(1, &fbo);

//绑定帧缓冲
glBindFrambuffer(GL_FRAMEBUFFER,fbo);

注意,要想使用帧缓冲,必须定义一个完整的帧缓存,完整的帧缓冲条件如下:

  • 附加至少一个缓冲(颜色,深度或模板)
  • 至少有一个颜色附件
  • 所有的附件必须完整(保留了内存)
  • 每个缓冲都应该有相同的样本数
//检查帧缓冲是否完整,如果返回 `GL_FRAMEBUFFER_COMPLETE`,帧缓冲就是完整的了
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)

之后所有的渲染操作将会渲染到当前绑定的附件中,而这时渲染到的是非窗口帧缓冲(离屏渲染),要让所有帧缓冲的渲染在主窗口上有效果,需要我们再次激活帧缓冲,绑定为0

glBindFramebuffer(GL_FRAMEBUFFER, 0);
//完成了所有帧缓冲后,别忘了删除帧缓冲对象
glDeleteFramebuffers(1, &fbo);

在完整性检查执行之前,需要给帧缓冲附加一个附件。附件是一个内存位置,能作为帧缓冲的一个缓冲,可以想象为一个图像。创建一个附件有两个选项:纹理或渲染缓冲对象

2.纹理附件

使用一个纹理附加到帧缓冲的时候,所有渲染指令都会写入这个纹理中,和普通的缓冲是一个的,其实都是一些数据信息。

  • 为帧缓冲创建一个纹理
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);

和我们默认创建一个普通的纹理的主要区别在于,我们的纹理数据data参数传递给了NULL。对于这个纹理,当下只完成了分配内存操作,填充这个纹理我们将放在渲染到帧缓冲之后来进行,同时纹理环绕方式这里是不重要的。但是我们需要将屏幕渲染到一个更小的或更大的纹理的时候,需要再次调用glViewport

  • 附加纹理到帧缓冲
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

依次的参数如下:

  • target:帧缓冲的目标(绘制、读取或者两者皆有)
  • attachment:我们想要附加的附件类型。当前我们正在附加一个颜色附件。注意最后的0意味着我们可以附加多个颜色附件。我们将在之后的教程中提到。
  • textarget:你希望附加的纹理类型
  • texture:要附加的纹理本身
  • level:多级渐远纹理的级别。我们将它保留为0。

除了颜色附件之外,我们还可以附加一个深度和模板缓冲纹理到帧缓冲对象中。要附加深度缓冲的话,我们将附件类型设置为GL_DEPTH_ATTACHMENT。注意纹理的格式(Format)和内部格式(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);

3.渲染缓冲对象附件

渲染缓冲对象是在纹理之后引入的,是一个帧缓冲附件类型。渲染缓冲是一个真正的缓冲,会将数据存储为OpenGl原生的渲染格式,比离屏渲染优化了一些。
渲染缓冲对象直接对渲染数据进行存储,不进行任何转换,所以读写速度很快。在之前每次渲染循环的时候最后使用glfwSwapBuffers也可以通过渲染缓冲对象的交换实现。

  • 创建一个渲染缓冲对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
  • 绑定这个渲染缓冲对象
glBindRenderbuffer(GL_RENDERBUFFER, rbo);

注意:由于渲染缓冲对象都是只写的,所以在我们不需要对深度和模板附件进行采样的时候使用尤佳,比如说深度测试和模板测试。

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

总结:渲染缓冲对象能为你的帧缓冲对象提供一些优化,但知道什么时候使用渲染缓冲对象,什么时候使用纹理是很重要的。通常的规则是,如果你不需要从一个缓冲中采样数据,那么对这个缓冲使用渲染缓冲对象会是明智的选择。如果你需要从缓冲中采样颜色或深度值等数据,那么你应该选择纹理附件。

4.渲染到纹理

现在我们来实战将一个场景附加到帧缓冲对象上的颜色纹理中,之后在一个横跨整个屏幕的四边形上绘制这个纹理。

  • 创建一个帧缓冲对象,并绑定
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
  • 接下来创建一个纹理图像,并作为一个纹理附件附加到帧缓冲上,并将纹理维度设置为窗口的宽和高。
// 生成纹理
unsigned int 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);  
  • 然后除了创建一个渲染对象外,我们还希望进行深度测试,因此还需要添加一个深度附件到帧缓冲中,而这里除了对颜色缓冲要进行采样外,其他的都不会进行采样,所以我们创建一个深度和模板附件渲染缓冲对象,并将内部格式的精度设置为 GL_DEPTH24_STENCIL8
unsigned int 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)
    std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

好,现在我们就可以绑定这个帧缓冲了,后面的渲染指令是在这个帧缓冲里而不是默认的帧缓冲中。所有的深度和模板操作都会在当前绑定的帧缓冲中找对应的附件信息,而不是在一个深度缓冲中找。

总结:绘制场景到一个纹理步骤

  1. 将新的帧缓冲绑定为激活的帧缓冲
  2. 绑定默认的帧缓冲
  3. 绘制一个整屏的四边形,将帧缓冲的颜色缓冲作为纹理。

由于现在我们绘制屏幕的纹理是直接帧缓冲上进行采样的,所以我们不需要进行MVP变换,直接将帧缓冲中的数据作为顶点着色器的输出。

//顶点着色器
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    TexCoords = aTexCoords;
}
//片段着色器
#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;

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

为屏幕四边形创建并配置一个VAO。而帧缓冲的渲染迭代结构为:

// 第一处理阶段(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);  

注意,这里仅仅是对一个四边形进行颜色采样得到的渲染结果。

在这里插入图片描述

二.后期处理

既然整个场景都渲染到了一个纹理(图像)上,所以我们可以用图像处理的方法得到一些有意思的效果,这中对渲染结果的处理也叫做后处理。

1.反相

在片段着色器中我们可以得到最后图像中的每一个颜色,所以我们也可以直接在片段着色器中对颜色取反相。也就是用1.0减去颜色值,改变最后屏幕上的纹理。

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

在这里插入图片描述

2.灰度

注意,平时我们的颜色对应的是(R,G,B)三个分量信息,不同的比例对应了不同的颜色,而当RGB三个分量的值一样的时候,最后会呈现出的就是一个灰色图像,所以我们可以对(R,G,B)进行取平均。

void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;
    FragColor = vec4(average, average, average, 1.0);
}

在这里插入图片描述

3.核操作

将核(卷积矩阵)中心对齐当前像素,并将对应的核值和对应的像素值进行乘积的和赋给中心像素的值,比如说下面的一个核:
在这里插入图片描述
这个核取了8个周围像素值,将它们乘以2,而把当前的像素乘以-15。这个核的例子将周围的像素乘上了一个权重,并将当前像素乘以一个比较大的负权重来平衡结果。

注意,如果加权的和不为1的话,那么最后的纹理颜色的亮度就会发生变换。

例如:

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

不同的核我们可以得到不同的图像处理效果:

  • 模糊
    在这里插入图片描述
    在这里插入图片描述
  • 边缘检测
    在这里插入图片描述
    在这里插入图片描述

注意,在边缘进行核操作的时候,把纹理的环绕方式设置为GL_CLAMP_TO_EDGE,这样子在取到纹理外的像素时,就能够重复边缘的像素来更精确地估计最终的值了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值