【OpenGL】笔记二十三、帧缓冲

1. 流程

达成了各种渲染效果以后,现在我们想要更进一步,对屏幕的每一个像素进行精准着色,或者对渲染后的图像再进行全局效果处理(比如镜子、模糊等效果),这些做法在之前是没有办法实现的,我们该怎么办?

其实OpenGL给出了一种数据缓冲——帧缓冲(Framebuffer),我们之前用的颜色缓冲、深度缓冲和模板缓冲就是包含在帧缓冲之中的,我们之前所做的渲染也都是在默认的帧缓冲中做的,完成一切渲染后,OpenGL就会从帧缓冲中读取数据,然后渲染到屏幕上的每一个像素

当然,想要做出自己的效果,我们肯定需要定义一个自己的帧缓冲,一个完整的帧缓冲包含:

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

附件其实就是相当于一个缓冲,可以是图像之类的,图像的一个像素就可以看作缓冲的一个元素,目前,OpenGL给出了两种附件,分别是纹理和渲染缓冲对象

1.1 纹理附件

创建纹理附件的过程和我们之前创建纹理的过程大同小异,只是我们这次是把它当作一个缓冲,而暂时不向其中写入任何数据

	unsigned int textureColorbuffer;
    glGenTextures(1, &textureColorbuffer);
    glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCR_WIDTH, SCR_HEIGHT, 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, textureColorbuffer, 0);

可以看到,在glTexImage2D这句代码中,我们把宽和高设置为了屏幕大小,这样它就能覆盖屏幕的每个像素,然后我们把数据的参数设置为了NULL,这就使得它暂时是一个空的缓冲,然后设置完纹理,我们又利用glFramebufferTexture2D将这个附件附加到了帧缓冲上,其中第二个参数表明这个附件是个颜色类附件(代表我们将这个纹理作为我们帧缓冲中的颜色缓冲)

当然,我们也可以将这个纹理作为深度和模板缓冲:

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

因为缓冲的数据每一位是32位,所以这里将深度缓冲和模板缓冲的存取格式设置为了深度24位,模板8位,这就使得它们可以在一个缓冲内读取

1.2 渲染缓冲对象

渲染缓冲对象是在纹理之后引入到OpenGL中的,它和纹理的区别就是它的数据都是原生的,也就是存取和处理都要比纹理附件要快,但它通常都是只写的,所以你不能读取它们(比如使用纹理访问)。当然你仍然还是能够使用glReadPixels来读取它,这会从当前绑定的帧缓冲,而不是附件本身,中返回特定区域的像素。

渲染缓冲对象的创建和纹理附件差不多,接下来我们就将深度和模板数据写入它

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

1.3 帧缓冲

之前我们完成了两个附件的创建,将颜色、深度和模板缓冲都附加到了帧缓冲上,但是一直没有对帧缓冲的创建做说明,其实它的创建也和其他缓冲差不多:

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

完成帧缓冲创建之后,我们就接着完成之前提到的附件创建,并把它们附加到我们自己的帧缓冲上

接着我们检测一下这个帧缓冲完不完整:

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

如果完整的话,我们就可以进行下面的渲染环节了

1.4 渲染

其实利用我们自己的帧缓冲渲染是一件很简单的事,具体的基本渲染和我们之前的做法都相同,只是在做渲染之前,我们需要激活我们自己的帧缓冲:

    while (!glfwWindowShouldClose(window))
    {
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

做完各种场景渲染之后,颜色、深度和模板都被写入了我们的帧缓冲之中,但是这时候我们会发现没有渲染出来画面,这是怎么回事呢?

这是因为我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做离屏渲染(Off-screen Rendering)。要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到0。

glBindFramebuffer(GL_FRAMEBUFFER, 0);

绑定完之后,我们接下来做什么呢?还记得我们之前将颜色缓冲绑定在了纹理附件上吗?我们在渲染时将所有屏幕的像素都存入了这个颜色缓冲,没错,接下来我们用画一个填充屏幕的矩形的方法,将之前已经写入的纹理画在这个矩形上,

首先当然是创建这个矩形的VAO,VBO:

		float quadVertices[] = { // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.
		    // positions   // texCoords
		    -1.0f,  1.0f,  0.0f, 1.0f,
		    -1.0f, -1.0f,  0.0f, 0.0f,
		     1.0f, -1.0f,  1.0f, 0.0f,
		
		    -1.0f,  1.0f,  0.0f, 1.0f,
		     1.0f, -1.0f,  1.0f, 0.0f,
		     1.0f,  1.0f,  1.0f, 1.0f
		};
	    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(float), (void*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));

然后我们创建一个简单的输出纹理的着色器:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

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

in vec2 TexCoords;

uniform sampler2D screenTexture;

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

最后我们在完成场景渲染后绑定默认帧缓冲,画出这个矩形:

    while (!glfwWindowShouldClose(window))
    {
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
		...
		...
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glDisable(GL_DEPTH_TEST); // disable depth test so screen-space quad isn't discarded due to depth test.
        // clear all relevant buffers
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // set clear color to white (not really necessary actually, since we won't be able to see behind the quad anyways)
        glClear(GL_COLOR_BUFFER_BIT);

        shader.use();
        shader.setInt("screenTexture", 0);
        glActiveTexture(GL_TEXTURE0);
        glBindVertexArray(quadVAO);
        glBindTexture(GL_TEXTURE_2D, textureColorbuffer);	// use the color attachment texture as the texture of the quad plane
        glDrawArrays(GL_TRIANGLES, 0, 6);

效果:
在这里插入图片描述
和之前一样,没有区别,但是如果我们使用线框模式的话:
在这里插入图片描述
就会发现,我们其实画的是一个矩形,只是我们把之前渲染的画面作为纹理附加给这个矩形了而已

1.5 后期处理

1.5.1 反相

有了自己的帧缓冲,我们添加更多后期效果就更方便了,比如我们对颜色反相:

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

在这里插入图片描述

1.5.2 灰度图

比如我们做成灰度图:

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

在这里插入图片描述
人眼会对绿色更加敏感一些,而对蓝色不那么敏感,所以为了获取物理上更精确的效果,我们还可以使用加权的(Weighted)通道:

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

在这里插入图片描述

1.5.3 核效果

我们还可以在当前纹理坐标的周围取一小块区域,对当前纹理值周围的多个纹理值进行采样。

1.5.3.1 核卷积

比如核卷积,它是一个类矩阵的数值数组,它的中心为当前的像素,它会用它的核值乘以周围的像素值,并将结果相加变成一个值。所以,基本上我们是在对当前像素周围的纹理坐标添加一个小的偏移量,并根据核将结果合并。下面是核的一个例子:

在这里插入图片描述

这个核取了8个周围像素值,将它们乘以2,而把当前的像素乘以-15。这个核的例子将周围的像素乘上了一个权重,并将当前像素乘以一个比较大的负权重来平衡结果。

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[](
        -2, -2, -2,
        -2,  15, -2,
        -2, -2, -2
    );

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

在片段着色器中,我们首先为周围的纹理坐标创建了一个9个vec2偏移量的数组。偏移量是一个常量,你可以按照你的喜好自定义它。之后我们定义一个核,在这个例子中是一个锐化(Sharpen)核,它会采样周围的所有像素,锐化每个颜色值。最后,在采样时我们将每个偏移量加到当前纹理坐标上,获取需要采样的纹理,之后将这些纹理值乘以加权的核值,并将它们加到一起。

因为权重和是-1,所以大部分是黑色的:
在这里插入图片描述
可以做一个权重和为1的效果
在这里插入图片描述

1.5.3.2 模糊

创建模糊(Blur)效果的核是这样的:
在这里插入图片描述
因为是模糊,所以不要忘了取加权平均:

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

在这里插入图片描述

1.5.3.3 边缘检测

下面的边缘检测(Edge-detection)核和锐化核非常相似:
在这里插入图片描述
这个核高亮了所有的边缘,而暗化了其它部分,且权重和为0,在我们只关心图像的边角的时候是非常有用的。
在这里插入图片描述
(注意,核在对屏幕纹理的边缘进行采样的时候,由于还会对中心像素周围的8个像素进行采样,其实会取到纹理之外的像素。由于环绕方式默认是GL_REPEAT,所以在没有设置的情况下取到的是屏幕另一边的像素,而另一边的像素本不应该对中心像素产生影响,这就可能会在屏幕边缘产生很奇怪的条纹。为了消除这一问题,我们可以将屏幕纹理的环绕方式都设置为GL_CLAMP_TO_EDGE。这样子在取到纹理外的像素时,就能够重复边缘的像素来更精确地估计最终的值了。)

1.5.4 马赛克像素风

实现马赛克风的原理很简单,马赛克就是将一块像素的颜色都设置为中心像素的颜色,当然,有一种简单的方法,那就是在渲染场景到我们的帧缓冲之前,先按比例缩小窗口,然后将我们自己的帧缓冲也设置为缩小的尺寸,接下来渲染场景到帧缓冲上,这一步相当于手动减小分辨率,然后还原视口大小,渲染帧缓冲
在这里插入图片描述
(注意,因为是像素风,所以帧缓冲的纹理采样必须是NEAREST,不能线性插值)
在这里插入图片描述

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ycr的帐号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值