高级OpenGL-帧缓冲

OpenGL-framebuffers

1.创建一个帧缓冲

到目前为止,我们使用了用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲、用于丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲。我们目前的工作都是在默认帧缓冲的渲染缓冲上进行,GLFW为我们提供的,但是opengl还允许我们定义自己的缓冲,帮助我们来实现更多的效果。

同其他对象一样,我们通过glGenFramebuffers来创建帧缓冲对象。

unsigned int fbo;
glGenFramebuffers(1, &fbo);

它的使用方法也和其他对象类似,首先创建帧缓冲对象,把它绑定为active,在使用过后再解绑帧缓冲。我们通过glBindFramebuffer来绑定帧缓冲。

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

其中GL_FRAMEBUFFER其实包含了两个目标,它们分别是GL_READ_FRAMEBUFFERGL_DRAW_FRAMEBUFFER即读取目标和写入目标。绑定到读取目标的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到写入目标的帧缓冲将会被用作渲染、清除等写入操作的目标。当然我们也可以分别绑定,但是在大部分情况下不需要区分它们。

一个完整的帧缓冲需要满足:

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

因此我们还需要为我们刚刚创建的帧缓冲准备一些附件,随后还可以检查帧缓冲是否完全

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
	// 执行胜利的舞蹈
else
	// 干

之后所有的渲染操作都会渲染到当前绑定帧缓冲的附件中。但是由于我们的帧缓冲不是默认帧缓冲,因此我们还需要再次激活默认帧缓冲,绑定到0,这种渲染到不同的帧缓冲叫做离屏渲染

glBindFramebuffer(GL_FRAMEBUFFER, 0);

最后还要记得删除我们的帧缓冲对象(注意在创建和删除帧对象时,由于我们可以同时操作多个帧对象,在函数名中Framebuffers用的也是复数形式)

glDeleteFramebuffers(1, &fbo);

下面我们来讨论如何为创建的帧对象添加附件。

2.纹理附件

为帧缓冲创建纹理和创建普通纹理差不多,这样做的优点在于,所有的渲染操作结果都会被存储在一个纹理图像中,我们可以在着色器中很方便地使用它。

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

3.渲染缓冲对象组件

渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。它会将数据储存为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);

因此我们需要知道什么时候使用渲染缓冲对象,什么时候使用纹理对象。通常规则是,如果你不需要从一个缓冲中采样数据,那么采用渲染缓冲对象更好一些,如果你需要从缓冲中采样颜色或深度值等数据,那么你应该选择纹理附件。性能方面他不会产生非常大的影响。

4.渲染到纹理

现在我们开始实践,将场景渲染到一个附加到帧缓冲对象上的颜色纹理中,然后在一个横跨整个屏幕的四边形上绘制这个纹理。这样便于我们对整个画面进行进一步的操作,也称画面后处理。

使用前:

高级OpenGL-帧缓冲_第1张图片

使用后:

高级OpenGL-帧缓冲_第2张图片

其实我在这里卡了很久都没有做出来,因为我想要把前面教程里的深度测试模板测试混合这些都加进来,就导致最后在屏幕上绘制texture的时候无法正常读取。上面是把模板测试关掉之后的效果。

不过总而言之,我们的帧缓冲的确是成功了

通过在这个地方找bug的这些时间,我对帧缓冲实现的方法也有了更为深刻的认识。虽然最后深度缓冲还是没能成功实现,但是重点在于对概念的理解吧,就先搁置,等有时间再回过头来处理这个问题。

采用帧缓冲对象,其实就是把我们原本采用默认帧缓冲直接绘制到屏幕的画面,通过我们新创建的帧缓冲事先绘制在一个texture上,再用默认的帧缓冲将texture绘制到屏幕上。从而达到离屏渲染的目的,这样的优点在于我们可以直接对得到的texture进行操作,也就是画面后处理。

5.后期处理

拿到预先绘制好的texture之后,就可以为所欲为了!

反相:
void main()
{
    vec3 tex = texture(screenTexture, TexCoords).rgb;
    vec3 col = 1 - tex;
    FragColor = vec4(col, 1.0);
}

高级OpenGL-帧缓冲_第3张图片

灰度:
void main()
{
    vec3 tex = texture(screenTexture, TexCoords).rgb;
    vec3 col = vec3((tex.x + tex.y + tex.z) / 3.0);
    FragColor = vec4(col, 1.0);
}

高级OpenGL-帧缓冲_第4张图片

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

高级OpenGL-帧缓冲_第5张图片

核效果:
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);
}

高级OpenGL-帧缓冲_第6张图片

float kernel[9] = float[] (
         2,  2,  2,
         2,-15,  2,
         2,  2,  2
    );

高级OpenGL-帧缓冲_第7张图片

模糊:
float kernel[9] = float[] (
         1,  2,  1,
         2,  4,  2,
         1,  2,  1
    );
    
    ...
    
    col += sampleTex[i] * kernel[i] / 16.0f;

高级OpenGL-帧缓冲_第8张图片

边缘检测:
float kernel[9] = float[] (
         1,  1,  1,
         1, -8,  1,
         1,  1,  1
    );

高级OpenGL-帧缓冲_第9张图片

float kernel[9] = float[] (
        -2, -2, -2,
        -2, 15, -2,
        -2, -2, -2
    );

高级OpenGL-帧缓冲_第10张图片

我也不知道为什么这两个核最后绘制出来的效果倒是蛮相近的

后话:

上面有几个效果会发现屏幕的边缘会出现奇怪的条纹,这里是因为取样的时候取到了屏幕另一边的数据。这是我们可以将屏幕纹理的环绕方式设置为GL_CLAMP_TO_EDGE,这样在边缘的时候就可以重复取边缘的数据,看起来就会更加自然。

你可能感兴趣的:(OpenGL,学习笔记)