帧缓冲包括之前学的:用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件对齐特定片段的模板缓冲。他们都存储在内存中,我们也可以定义自己的帧缓冲。
在绑定到GL_FRAMEBUFFER目标之后,所有的读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲。我们也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。绑定到GL_READ_FRAMEBUFFER的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到GL_DRAW_FRAMEBUFFER的帧缓冲将会被用作渲染、清除等写入操作的目标。大部分情况你都不需要区分它们,通常都会使用GL_FRAMEBUFFER,绑定到两个上。
一个完整的帧缓冲需要满足以下的条件:
在完整性检查执行之前,我们需要给帧缓冲附加一个附件。附件是一个内存位置,它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像。当创建一个附件的时候我们有两个选项:纹理或渲染缓冲对象(Renderbuffer Object)。
当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入这个纹理中,可以认为她就是一个普通的颜色/深度或模板缓冲。为帧缓冲创建一个纹理和创建一个普通的纹理差不多:
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);
主要的区别就是,我们将维度设置为了屏幕大小(尽管这不是必须的),并且我们给纹理的data
参数传递了NULL
。对于这个纹理,我们仅仅分配了内存而没有填充它。填充这个纹理将会在我们渲染到帧缓冲之后来进行。同样注意我们并不关心环绕方式或多级渐远纹理,我们在大多数情况下都不会需要它们。
如果你想将你的屏幕渲染到一个更小或更大的纹理上,你需要(在渲染到你的帧缓冲之前)再次调用glViewport,使用纹理的新维度作为参数,否则只有一小部分的纹理或屏幕会被渲染到这个纹理上。
除了颜色附件(GL_COLOR_ATTACHMENT0)外,我们还可以附加深度(GL_DEPTH_ATTACHMENT)和模板缓冲(GL_STENCIL_ATTACHMENT)纹理到帧缓冲对象中,或者,我们可以将深度和模板放到一个附件(GL_DEPTH_STENCIL_ATTACHMENT)中。
对于颜色纹理附件,我们使用glTexImage2D来分配内存空间,对于深度和模板缓冲,我们使用glRenderbufferStorage来分配空间。
颜色纹理attach使用的是glFramebufferTexture2D, 深度和模板则使用glFramebufferRenderbuffer。
在上面的第6步后,我们就得到了屏幕截图纹理,有了这个纹理,我们可以做很多单纯使用shader无法完成的事情。因为shader中我们只关注单个顶点或但个片元(像素)的位置与颜色,我们并不关心他与周围顶点或片元之前的关系。基于此,我们可以完成例如灰度、模糊、锐化、边缘检测等很多操作。当然,灰度也可以在shader里完成,不同的是,我们可以单独写一个灰度的shader,来对整个屏幕的场景进行灰度,而避免了在所有需要灰度的物体的shader里加上灰度的代码。
我们这里暂时只关注像素与紧邻的像素的关系,我们将其的乘因子的矩阵单独列出来,称作核,它有下面的这种形式。
但是需要注意的是,我们保证所有因子的和为1,否则可能造成变亮和变暗,这个和我们的提高亮度shader和减小亮度shader是类似的。
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);
}
我们只是单纯地把核用来存储乘因子,然后将每个元素乘以乘因子累加得到最终的结果。我们当然可以使用其他更复杂的算法,只不过,上面这种简单的算法已经能够充分表现各种效果了,而我们只需要调整核中的各个因子的比例即可。
上面的例子是锐化,从核中个因子的比例,我们可以看出,使用该的目的是为了加大目标像素与周围像素的不同,而我们称呼这个不同为锐化。同理,如果要制作模糊的核,因为模糊的本质是目标像素和周围的像素差别变小,那么我们可以写出下面的核。
上面的模糊核没有负数,也即只是单纯求平均。我们可以改变中间因子与周围因子的比例,来控制模糊的系数,当中间为1,周围均为0时,也就是不模糊了。
边缘检测可以理解为更加锐化。