高级OpenGL渲染管线之帧缓冲(三)

帧缓冲包括之前学的:用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件对齐特定片段的模板缓冲。他们都存储在内存中,我们也可以定义自己的帧缓冲。

在绑定到GL_FRAMEBUFFER目标之后,所有的读取写入帧缓冲的操作将会影响当前绑定的帧缓冲。我们也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。绑定到GL_READ_FRAMEBUFFER的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到GL_DRAW_FRAMEBUFFER的帧缓冲将会被用作渲染、清除等写入操作的目标。大部分情况你都不需要区分它们,通常都会使用GL_FRAMEBUFFER,绑定到两个上。

一个完整的帧缓冲需要满足以下的条件:

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

在完整性检查执行之前,我们需要给帧缓冲附加一个附件。附件是一个内存位置,它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像。当创建一个附件的时候我们有两个选项:纹理或渲染缓冲对象(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,使用纹理的新维度作为参数,否则只有一小部分的纹理或屏幕会被渲染到这个纹理上。

  1. 除了颜色附件(GL_COLOR_ATTACHMENT0)外,我们还可以附加深度(GL_DEPTH_ATTACHMENT)和模板缓冲(GL_STENCIL_ATTACHMENT)纹理到帧缓冲对象中,或者,我们可以将深度和模板放到一个附件(GL_DEPTH_STENCIL_ATTACHMENT)中。

  2. 对于颜色纹理附件,我们使用glTexImage2D来分配内存空间,对于深度和模板缓冲,我们使用glRenderbufferStorage来分配空间。

  3. 颜色纹理attach使用的是glFramebufferTexture2D, 深度和模板则使用glFramebufferRenderbuffer。

渲染到纹理步骤

  1. 生成纹理缓冲
  2. 绑定到当前纹理缓冲
  3. 生成纹理,并将该纹理attach到纹理缓冲
  4. 如果需要,生成深度和模板缓冲附件并attach
  5. 要绘制到该纹理缓冲时只需要激活该纹理缓冲即可 glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
  6. 执行绘制
  7. 我们访问绑定到该纹理缓冲上的纹理即可获得绘制得到的纹理。
  8. 因为东西只绘制在新建的纹理缓冲中,所以记得切换回默认帧缓冲,并将纹理绘制到默认缓冲上,glBindFramebuffer(GL_FRAMEBUFFER, 0);

后期处理

在上面的第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时,也就是不模糊了。

边缘检测可以理解为更加锐化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值