目的:在之前,所有的物体都是中规中矩的显示的,只考虑了光照对物体的影响,那假设想要显示特殊的效果该怎么操作呢?例如马赛克风、将所有的物体都显示为黑白色,就像上世纪80年代的灰白电视一样,又或者说将整个场景渲染到一张泛黄的纸上以体现出年代感……当然是修改着色器,事实上,很多地方都是这么做的,不过有些情况下,场景中的物体和对应的着色器都不少,若是想要整个场景(视口)体现出某个效果,就需要借助别的方法了。
概况:想办法把当前的场景作为一张纹理存起来,然后再去全屏渲染这张“纹理”,在这种情况下,我们只需要给这张“纹理”编写一个独一无二的着色器就可以了。
几种不同类型的屏幕缓冲,这几种缓冲结合起来叫做帧缓冲(Framebuffer)。
- 用于写入颜色值的颜色缓冲。
- 用于写入深度信息的深度缓冲。
- 以及允许我们基于一些条件丢弃指定片段的模板缓冲。
我们目前所做的渲染操作都是是在默认的帧缓冲之上进行的,当你创建了你的窗口的时候默认帧缓冲就被创建和配置好了(GLFW为我们做了这件事)。是的,GLFW已经为我们把这份代码写了,只不过我们是要自己再“客制化”一个想要的结果,OpenGL给了我们自己定义帧缓冲的自由,我们可以选择性的定义自己的颜色缓冲、深度和模板缓冲。和VBO、EBO一样,也可以用同样的方法创建和绑定一个FBO(帧缓冲)变量:
GLuint FBO;
glGenFramebuffers(1, &FBO);
glBindFramebuffer(GL_FRAMEBUFFER, FBO);
绑定的目标有以下3种,一般都是第一种:
- GL_FRAMEBUFFER:绑定到目标后,接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。
- GL_READ_FRAMEBUFFER:允许读取操作。
- GL_DRAW_FRAMEBUFFER:允许进行渲染、清空和其他的写入操作。
构建一个完整的帧缓冲必须满足以下条件,如果不满足条件,则属于无效构建:
- 必须往里面加入至少一个附件(颜色、深度、模板缓冲)。
- 必须要有至少一个为颜色附件。
- 所有的附件都应该已经存储在内存中的。
- 每个缓冲都应该有同样数目的样本。
样本是什么暂时不管,后面会有讲到,总而言之,因为帧缓冲的构建需要条件,所以我们在后面需要用 glCheckFramebufferStatus 函数来检查是否真的满足所有的条件,如果返回的值是 GL_FRAMEBUFFER_COMPLETE 就说明创建成功了。
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
- On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作。
- Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
......
- 回到上面的帧缓冲代码,通过glBindFramebuffer方法,就相当于切换了当时的缓冲区,后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中(即离屏渲染),如果当前激活的帧缓冲不是默认的帧缓冲,渲染命令对窗口的视频输出就不会产生任何影响。
- 因此后面若是要渲染到主窗口,就必须要通过 glBindFramebuffer(GL_FRAMEBUFFER, 0) 来使默认帧缓冲被激活。
当我们做完所有帧缓冲操作后,不要忘记删除帧缓冲对象:
glDeleteFramebuffers(1, &fbo);
好了,现在回到最初的目的:
想办法把当前的场景作为一张纹理存起来,然后再去全屏渲染这张“纹理”,在这种情况下,我们只需要给这张“纹理”编写一个独一无二的着色器就可以了。
在此,目的就变成了:
- 自定义一个帧缓冲并且启用,然后走之前正常的渲染逻辑,这个时候,本应该在屏幕中展示的图像就被渲染到了自定义的帧缓冲中。
- 接下来,我们再想办法将自定义帧缓冲中的数据转变为纹理,并且对这张纹理在默认帧缓冲(主窗口)中进行渲染显示。
接下来,就是想办法往帧缓冲里添加附件,一个附件就是一个内存地址,这个内存地址里面包含一个为帧缓冲准备的缓冲,它可以是个图像。当创建一个附件的时候我们有两种方式可以采用:
- 纹理。
- 渲染缓冲(renderbuffer)对象。
先创建一个帧缓冲的纹理:
GLuint getAttachmentTexture()
{
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); // 与普通纹理不同,data处传递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);
return textureID;
}
这里和之前的定义纹理有三点不同:
- 纹理的大小为屏幕的大小(测试是800 x 600)。
- 不需要传递data参数,只分配内存而不去填充,纹理填充会在渲染到帧缓冲的时候去做。
- 不再关心环绕方式或者Mipmap。
创建完纹理后,紧接着就是把它附加到帧缓冲上:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);
glFramebufferTexture2D 函数参数:
- target:帧缓冲类型的目标
- attachment:附加的附件的类型,目前附加的是一个颜色附件
- textarget:你希望附加的纹理类型,目前总是GL_TEXTURE_2D
- texture:附加的实际纹理,用上面生成的texture
- level:Mipmap level
扩展:如果想要附加深度缓冲和模板缓冲,就需要在生成纹理时是用 GL_DEPTH24_STENCIL8 的纹理格式和内部类型,这样的纹理每32位数值就包含了24位的深度信息和8位的模板信息,不过这里暂时不需要考虑,大致用法如下:
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT, 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),渲染缓存是为离线渲染而新引进的,它容许将一个场景直接渲染到一个渲染缓存对象中,而不是渲染到纹理对象中。RBO并不能单独使用,必须配合FBO,与opengl缓冲区对应,RBO可以存放颜色、深度、模板数据。
- 和纹理图像一样,渲染缓冲对象也是一个缓冲,它可以是一堆字节、整数、像素或者其他东西。渲染缓冲对象的一大优点是,它以OpenGL原生渲染格式储存它的数据,因此在离屏渲染到帧缓冲的时候,这些数据时已经被优化过的了,这样的话,写入或把它们的数据简单地到其他缓冲的时候非常快,之前提到的函数glfwSwapBuffers交换缓冲区,同样以渲染缓冲对象实现。
glGenRenderbuffers(1, &RBO);
glBindRenderbuffer(GL_RENDERBUFFER, RBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);
- 由于渲染缓冲对象通常是只写的,它们经常作为深度和模板附件来使用,因为大多数时候并不需要从深度和模板缓冲中读取数据。
glRenderbufferStorage:创建一个深度和模板渲染缓冲对象,第2个参数指定渲染缓冲区的颜色可渲染、深度可渲染或模板可渲染格式,后面两个参数为指定缓冲区的宽和高。
glFramebufferRenderbuffer:和glFramebufferTexture2D目的一样,将渲染缓冲区附加到当前帧缓冲区上。
到现在帧缓冲就做好了:绑定了颜色缓冲,也有了深度和模板缓冲。如果遗漏了某个缓冲,那么对应的测试就不会工作,因为当前绑定的帧缓冲里没有对应的缓冲。
好了,现在又回到之前的目的:
1)定义一个帧缓冲并且启用,然后走之前正常的渲染逻辑,这个时候,本应该在屏幕中展示的图像就被渲染到了自己定义的帧缓冲中。
2)接下来,我们再想办法将帧缓冲中的数据转变为纹理,并且对这张纹理在默认帧缓冲中进行渲染显示。
在此,目的就变成了:绑定为上面自己定义的帧缓冲,像往常那样渲染场景,之后绑定回默认帧缓冲,简单的绘制一个全屏四边形,用自己的帧缓冲的颜色缓冲作为他的纹理。(详情见OpenGL帧缓冲代码示例:framebuffers.cpp)
根据上文的目的,整体的渲染流程就是这样的:
while (!glfwWindowShouldClose(window))
{
//……
glBindFramebuffer(GL_FRAMEBUFFER, FBO); // glBindFramebuffer绑成自定义FBO
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
//正常渲染流程……
glBindFramebuffer(GL_FRAMEBUFFER, 0); // glBindFramebuffer绑成“主窗口”
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
shaderScreen.Use();
glBindVertexArray(quadVAO);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer); // 借机在主窗口输出“自定义shader的屏幕大小的纹理”
glDrawArrays(GL_TRIANGLES, 0, 6);
//……
}
也可以在渲染最后的“全场景”纹理时,关闭深度测试和模板测试,毕竟没有必要了。不需要考虑包括矩阵变换的很多东西,毕竟就是最简单的绘制四边形,如果代码是没问题的,那么就会出现和之前章节中一摸一样的场景,只不过处理的方式不一样
好了,既然这个时候整个场景就是一个纹理,并且有着专属的着色器,那么就可以很容易的实现下面的效果(图片变黑白):
着色器代码如下:
// .vs
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texture;
out vec2 texIn;
void main()
{
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
texIn = texture;
}
// .fs
#version 330 core
in vec2 texIn;
out vec4 color;
uniform sampler2D screenTexture;
void main()
{
color = texture(screenTexture, texIn);
float average = (color.r + color.g + color.b) / 3.0;
// 人眼趋向于对绿色更敏感,对蓝色感知比较弱,所以为了获得更精确的符合人体物理的结果,往往使用下面的计算方法:
// float average = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
color = vec4(average, average, average, 1.0); // 三通道变单通道,即成为灰度图
}
黑白反射和模糊的区别:
- 对于黑白和反色,我们只需要考虑当前的像素点,并且通过一定的运算获得。
- 而对于模糊和锐化,就不止需要考虑当前这一像素点了,还需要考虑其周围像素点的颜色,也就是说,需要从当前纹理值的周围采样多个纹理值,创造性地把它们结合起来参与计算,这个时候就需要用到Kernel矩阵。
一个简单的模糊Kernel矩阵,它代表着当前像素值 =(斜邻近4个像素的值 + 上下左右4个像素的值 * 2 + 自身像素的值 * 4)/ 16:
至于这个邻近的像素怎么取,主要是通过纹理坐标+纹理坐标的差获得,对应的片段着色器代码如下:
#version 330 core
in vec2 texIn;
out vec4 color;
uniform sampler2D screenTexture;
const float offset = 1.0 / 500;
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.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
);
vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture(screenTexture, texIn.st + offsets[i]));
}
vec3 col;
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
color = vec4(col, 1.0);
}
可以看到,代码中设置了偏移量为0.002,逻辑很容易看懂。如果没问题的话,最终就可以得到下面的效果:
对应图片锐化的Kernel矩阵和效果如下:
可能会觉得效果和想象中的不太一样,毕竟这是最简单的Kernel矩阵,也有更多更大的Kernel矩阵可以得到一些更有意思的效果。就拿模糊来说,还有“运动模糊”、“醉酒模糊”等,它们都有对应的Kernel矩阵,在一些像Photoshop这样的软件中也是使用这些kernel作为图像操作工具的,对图像和kernel矩阵进行逐个元素相乘再求和的操作,就是类卷积的过程,一般来讲:
- 绝大多数的kernel矩阵,它们所有元素的和加起来等于1。
- kernel矩阵不一定是3 x 3,还可以是 5 x 5、9 x 9 或更大。
(4条消息) OpenGL基础33:帧缓冲(上)之离屏渲染_Jaihk662的博客-CSDN博客_opengl离屏渲染
(4条消息) OpenGL基础34:帧缓冲(中)之附件_Jaihk662的博客-CSDN博客
(4条消息) OpenGL基础35:帧缓冲(下)之简单图像处理_Jaihk662的博客-CSDN博客_opengl 图像处理