到目前为止,我们使用了用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲、用于丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲。我们目前的工作都是在默认帧缓冲的渲染缓冲上进行,GLFW为我们提供的,但是opengl还允许我们定义自己的缓冲,帮助我们来实现更多的效果。
同其他对象一样,我们通过glGenFramebuffers
来创建帧缓冲对象。
unsigned int fbo;
glGenFramebuffers(1, &fbo);
它的使用方法也和其他对象类似,首先创建帧缓冲对象,把它绑定为active,在使用过后再解绑帧缓冲。我们通过glBindFramebuffer
来绑定帧缓冲。
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
其中GL_FRAMEBUFFER
其实包含了两个目标,它们分别是GL_READ_FRAMEBUFFER
和GL_DRAW_FRAMEBUFFER
即读取目标和写入目标。绑定到读取目标的帧缓冲将会使用在所有像是glReadPixels
的读取操作中,而绑定到写入目标的帧缓冲将会被用作渲染、清除等写入操作的目标。当然我们也可以分别绑定,但是在大部分情况下不需要区分它们。
一个完整的帧缓冲需要满足:
因此我们还需要为我们刚刚创建的帧缓冲准备一些附件,随后还可以检查帧缓冲是否完全
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
// 执行胜利的舞蹈
else
// 干
之后所有的渲染操作都会渲染到当前绑定帧缓冲的附件中。但是由于我们的帧缓冲不是默认帧缓冲,因此我们还需要再次激活默认帧缓冲,绑定到0
,这种渲染到不同的帧缓冲叫做离屏渲染
glBindFramebuffer(GL_FRAMEBUFFER, 0);
最后还要记得删除我们的帧缓冲对象(注意在创建和删除帧对象时,由于我们可以同时操作多个帧对象,在函数名中Framebuffers
用的也是复数形式)
glDeleteFramebuffers(1, &fbo);
下面我们来讨论如何为创建的帧对象添加附件。
为帧缓冲创建纹理和创建普通纹理差不多,这样做的优点在于,所有的渲染操作结果都会被存储在一个纹理图像中,我们可以在着色器中很方便地使用它。
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
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);
主要区别在于,我们将维度设置为了屏幕大小(不必须),并给纹理的data
参数传递了NULL
。现在我们只是分配了内存,我们等渲染到帧缓冲之后再来进行填充。
下面我们需要将这个纹理添加到帧缓冲上:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
其中关于这些参数代表的意义:
target
:帧缓冲的目标。(绘制、读取或者两者都有)attachment
:添加的附件类型。现在我们正在添加一个颜色附件,最后的0
表示这是我们附加的第一个颜色附件textarget
:希望附加的纹理类型。texture
:希望附加的纹理。level
:多级渐远纹理的级别,我们将它保留为0
。除了颜色附件以外,我们还可以附加深度和模板缓冲纹理到帧缓冲对象中。要附加深度缓冲的话,我们需要将附件类型设置为GL_DEPTH_ATTACHMENT
,纹理的格式将变成GL_DEPTH_COMPONENT
。要附加模板缓冲的话,附件类型则应该为GL_STENCIL_ATTACHMENT
,并将纹理的格式修改为GL_STENCIL_INDEX
。
我们也可以同时把深度缓冲和模板缓冲附加为一个纹理,纹理的每32位数据中将用24位表示深度信息8位表示模板信息(这样的精度足够我们平常使用)。此时我们需要使用GL_DEPTH_STENCIL_ATTACHMENT
类型,并配置纹理的格式,举个栗子。
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。它会将数据储存为opengl原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。
渲染缓冲对象通常都是只写的,但是我们仍然可以使用glReadPixels
来读取它,这会从当前绑定的帧缓冲中返回特定区域的像素。
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
类似的,我们还需要绑定这个渲染缓冲对象,让之后所有的渲染缓冲操作影响当前的rbo
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
我们通过调用glRenderbufferStorage
函数来创建深度和模板渲染缓冲对象:
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
创建渲染缓冲对象和纹理对象类似,但是不同点在于这个对象是专门被设计作为图像使用的,而不是纹理那样的通用数据缓冲。我们选择GL_DEPTH24_STENCIL8
作为内部格式,它封装了24位深度和8位模板缓冲。
最后要做的一件事就是附加这个渲染缓冲对象:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
因此我们需要知道什么时候使用渲染缓冲对象,什么时候使用纹理对象。通常规则是,如果你不需要从一个缓冲中采样数据,那么采用渲染缓冲对象更好一些,如果你需要从缓冲中采样颜色或深度值等数据,那么你应该选择纹理附件。性能方面他不会产生非常大的影响。
现在我们开始实践,将场景渲染到一个附加到帧缓冲对象上的颜色纹理中,然后在一个横跨整个屏幕的四边形上绘制这个纹理。这样便于我们对整个画面进行进一步的操作,也称画面后处理。
使用前:
使用后:
其实我在这里卡了很久都没有做出来,因为我想要把前面教程里的深度测试模板测试混合这些都加进来,就导致最后在屏幕上绘制texture的时候无法正常读取。上面是把模板测试关掉之后的效果。
不过总而言之,我们的帧缓冲的确是成功了
通过在这个地方找bug的这些时间,我对帧缓冲实现的方法也有了更为深刻的认识。虽然最后深度缓冲还是没能成功实现,但是重点在于对概念的理解吧,就先搁置,等有时间再回过头来处理这个问题。
采用帧缓冲对象,其实就是把我们原本采用默认帧缓冲直接绘制到屏幕的画面,通过我们新创建的帧缓冲事先绘制在一个texture上,再用默认的帧缓冲将texture绘制到屏幕上。从而达到离屏渲染的目的,这样的优点在于我们可以直接对得到的texture进行操作,也就是画面后处理。
拿到预先绘制好的texture之后,就可以为所欲为了!
void main()
{
vec3 tex = texture(screenTexture, TexCoords).rgb;
vec3 col = 1 - tex;
FragColor = vec4(col, 1.0);
}
void main()
{
vec3 tex = texture(screenTexture, TexCoords).rgb;
vec3 col = vec3((tex.x + tex.y + tex.z) / 3.0);
FragColor = vec4(col, 1.0);
}
void main()
{
vec3 tex = texture(screenTexture, TexCoords).rgb;
vec3 col = vec3((0.2126 * tex.x + 0.7152 * tex.y + 0.0722 * tex.z) / 3.0);
FragColor = vec4(col, 1.0);
}
const float offset = 1.0f / 300.0f;
void main()
{
vec2 offsets[9] = vec2[] (
vec2 (-offset, offset), // left top
vec2 (0.0f, offset), // top middle
vec2 (offset, offset), // right top
vec2 (-offset, 0.0f), // left
vec2 (0.0f, 0.0f), // middle
vec2 (offset, 0.0f), // right
vec2 (-offset, -offset), // left bottom
vec2 (0.0f, -offset), // bottom middle
vec2 (offset, -offset) // right bottm
);
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] = texture(screenTexture, TexCoords.st + offsets[i]).rgb;
}
vec3 col = vec3(0.0f);
for (int i= 0; i < 9; i++) {
col += sampleTex[i] * kernel[i];
}
FragColor = vec4(col, 1.f0);
}
float kernel[9] = float[] (
2, 2, 2,
2,-15, 2,
2, 2, 2
);
float kernel[9] = float[] (
1, 2, 1,
2, 4, 2,
1, 2, 1
);
...
col += sampleTex[i] * kernel[i] / 16.0f;
float kernel[9] = float[] (
1, 1, 1,
1, -8, 1,
1, 1, 1
);
float kernel[9] = float[] (
-2, -2, -2,
-2, 15, -2,
-2, -2, -2
);
我也不知道为什么这两个核最后绘制出来的效果倒是蛮相近的
上面有几个效果会发现屏幕的边缘会出现奇怪的条纹,这里是因为取样的时候取到了屏幕另一边的数据。这是我们可以将屏幕纹理的环绕方式设置为GL_CLAMP_TO_EDGE
,这样在边缘的时候就可以重复取边缘的数据,看起来就会更加自然。