当前位置: 代码网 > it编程>App开发>Android > LearnOpenGL - Android OpenGL ES 3.0 使用 FBO 进行离屏渲染

LearnOpenGL - Android OpenGL ES 3.0 使用 FBO 进行离屏渲染

2024年08月03日 Android 我要评论
利用 FBO(Framebuffer Object),我们可以实现离屏渲染。在前面的章节中,当我们调用 glDrawElements 后,手机屏幕上就会显示出绘制的图像。这意味着 OpenGL 将数据直接渲染到了手机屏幕上。通过使用 FBO,我们可以将数据渲染到纹理上,而不是直接渲染到屏幕,这个过程称为离屏渲染。通过离屏渲染,我们可以在最终显示之前对图像进行复杂的处理。这种方法非常有用,比如在后期处理效果(如模糊、HDR、阴影等)中,或者在渲染多个场景以进行纹理贴图、环境映射等操作时。

系列文章目录

一、前言

利用 fbo(framebuffer object),我们可以实现离屏渲染。在前面的章节中,当我们调用 gldrawelements 后,手机屏幕上就会显示出绘制的图像。这意味着 opengl 将数据直接渲染到了手机屏幕上。通过使用 fbo,我们可以将数据渲染到纹理上,而不是直接渲染到屏幕,这个过程称为离屏渲染。

通过离屏渲染,我们可以在最终显示之前对图像进行复杂的处理。这种方法非常有用,比如在后期处理效果(如模糊、hdr、阴影等)中,或者在渲染多个场景以进行纹理贴图、环境映射等操作时。

假设你在开发一款图片处理软件,包含美颜、滤镜等功能。用户可以同时应用多种滤镜,如瘦脸、美白、长腿等,每种滤镜都通过 opengl shader 进行处理和渲染。为实现这种功能,你可以设计一个图片渲染链。

一种直观的方法是为每种滤镜创建一个独立的模块,通过组合不同的模块实现多种滤镜的处理链。在处理链完成之前,我们无法将结果渲染到屏幕上。模块与模块之间的处理结果应该通过某种介质进行传递,这里使用的介质就是纹理。这也解释了我们为什么需要使用 fbo。

通过 fbo,我们可以在离屏状态下将渲染结果存储到纹理中,然后将该纹理作为输入传递给下一个滤镜模块。这样,整个处理链就可以逐步处理图像,直到应用所有滤镜后,将最终结果渲染到屏幕上。

在这里插入图片描述
本文所有代码在 fbodrawer.kt

二、fbo 简介

在这里插入图片描述
上图显示了帧缓冲区对象的结构,它提供了颜色缓冲区和深度缓冲区的替代品。如你所见,绘制操作并不是直接发生在帧缓冲区中的,而是发生在帧缓冲区所关联的对象(attachment)上。一个帧缓冲区有多个关联对象:颜色关联对象(color attachment)、深度关联对象(depth attachment)和模板关联对象(stencil attachment),分别用来替代颜色缓冲区、深度缓冲区和模板缓冲区。经过一些设置,opengl 就可以向帧缓冲区的关联对象中写入数据,就像写入颜色缓冲区或深度缓冲区一样。我们目前只关注颜色关联对象即可

每个关联对象又可以是两种类型的:纹理对象或渲染缓冲区对象(renderbuffer object)。当我们把纹理对象作为颜色关联对象关联到帧缓冲区对象后,opengl 就可以在纹理对象中绘图。渲染缓冲区对象表示一种更加通用的绘图区域,可以向其中写入多种类型的数据。

2.1 渲染缓冲对象

渲染缓冲区对象(renderbuffer object)是 opengl 和 opengl es 中的一种缓冲区类型,用于离屏渲染。它提供了一种高效的方式来存储图像数据,特别适用于深度缓冲区和模板缓冲区。

渲染缓冲区对象的特点:

  1. 高效存储

    • 渲染缓冲区对象在实现上通常比纹理对象更高效,特别是用于深度和模板数据的存储。
    • 它不需要纹理过滤、mip 贴图等特性,因此在某些场景下可以提供更好的性能。
  2. 不可直接采样

    • 与纹理对象不同,渲染缓冲区对象不能直接被着色器采样。
    • 这意味着你不能在着色器中直接访问渲染缓冲区对象中的数据,只能用于渲染过程。
  3. 用途广泛

    • 渲染缓冲区对象可以用作颜色、深度或模板缓冲区。
    • 在使用 fbo 进行离屏渲染时,渲染缓冲区对象可以作为这些附件类型附加到 fbo 上。

渲染缓冲区对象的使用步骤:

  1. 创建渲染缓冲区对象

    gluint rbo;
    glgenrenderbuffers(1, &rbo);
    glbindrenderbuffer(gl_renderbuffer, rbo);
    
  2. 分配存储

    • 根据用途分配存储,比如深度缓冲区、颜色缓冲区等。
    glrenderbufferstorage(gl_renderbuffer, gl_depth_component16, width, height);
    // 或者为颜色缓冲区分配存储
    // glrenderbufferstorage(gl_renderbuffer, gl_rgba8, width, height);
    
  3. 附加到 fbo

    • 将渲染缓冲区对象附加到 fbo 作为附件。
    glframebufferrenderbuffer(gl_framebuffer, gl_depth_attachment, gl_renderbuffer, rbo);
    // 如果是颜色缓冲区
    // glframebufferrenderbuffer(gl_framebuffer, gl_color_attachment0, gl_renderbuffer, rbo);
    

纹理对象与渲染缓冲区对象的对比:

  • 纹理对象

    • 可以在着色器中采样,用于更灵活的图像处理。
    • 适用于需要在多个渲染步骤中反复使用和处理的图像数据。
  • 渲染缓冲区对象

    • 高效的存储和写入,但不能在着色器中采样。
    • 适用于深度缓冲区和模板缓冲区,或者不需要在着色器中采样的颜色缓冲区。

结合使用:

在实际应用中,常常将纹理对象和渲染缓冲区对象结合使用。比如:

  • 使用渲染缓冲区对象存储深度和模板数据,以获得更高的性能。
  • 使用纹理对象存储颜色数据,以便在后续渲染步骤中进行采样和处理。

例子:

假设我们在开发一个图片处理软件,通过 fbo 进行多重滤镜处理。每个滤镜模块会产生一个中间结果,这些中间结果通常存储在纹理对象中,因为它们需要被后续的滤镜模块采样和处理。然而,为了提高性能,我们可以使用渲染缓冲区对象来存储深度数据,因为这些数据通常不需要在滤镜处理中直接访问。

// 创建并绑定 fbo
gluint fbo;
glgenframebuffers(1, &fbo);
glbindframebuffer(gl_framebuffer, fbo);

// 创建并附加颜色附件(纹理对象)
gluint colortex;
glgentextures(1, &colortex);
glbindtexture(gl_texture_2d, colortex);
glteximage2d(gl_texture_2d, 0, gl_rgba, width, height, 0, gl_rgba, 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, colortex, 0);

// 创建并附加深度附件(渲染缓冲区对象)
gluint depthrbo;
glgenrenderbuffers(1, &depthrbo);
glbindrenderbuffer(gl_renderbuffer, depthrbo);
glrenderbufferstorage(gl_renderbuffer, gl_depth_component16, width, height);
glframebufferrenderbuffer(gl_framebuffer, gl_depth_attachment, gl_renderbuffer, depthrbo);

// 检查 fbo 完整性
if (glcheckframebufferstatus(gl_framebuffer) != gl_framebuffer_complete) {
    // 处理错误
}

// 解绑 fbo 以恢复默认帧缓冲区
glbindframebuffer(gl_framebuffer, 0);

通过这种方式,我们可以高效地实现图像的离屏渲染和多重滤镜处理。

三、fbo 使用流程

gles30.glgentextures(1, fbotexids)
gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0])
gles30.gltexparameteri(gles30.gl_texture_2d, gles30.gl_texture_min_filter, gles30.gl_linear)
gles30.gltexparameteri(gles30.gl_texture_2d, gles30.gl_texture_mag_filter, gles30.gl_linear)
gles30.glbindtexture(gles30.gl_texture_2d, gles30.gl_none)

// generate fbo id and config fbo
// 创建 fbo
gles30.glgenframebuffers(1, fbo);
gles30.glbindframebuffer(gles30.gl_framebuffer, fbo[0])
gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0])
gles30.glframebuffertexture2d(gles30.gl_framebuffer, gles30.gl_color_attachment0, gles30.gl_texture_2d, fbotexids[0], 0)
gles30.glteximage2d(gles30.gl_texture_2d, 0, gles30.gl_rgba, imagewidth, imageheight, 0, gles30.gl_rgba, gles30.gl_unsigned_byte, null)
gles30.glbindtexture(gles30.gl_texture_2d, gles30.gl_none)
gles30.glbindframebuffer(gles30.gl_framebuffer, gles30.gl_none)

这段代码用于在 opengl es 3.0 中创建并配置一个帧缓冲区对象(fbo),并将一个纹理对象附加到这个帧缓冲区对象上作为颜色附件,以便进行离屏渲染。下面是对每行代码的详细解释:

创建和配置纹理对象

// 生成一个纹理对象,并将其id存储在 fbotexids 数组中
gles30.glgentextures(1, fbotexids);

// 绑定生成的纹理对象
gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]);

// 设置纹理过滤参数,线性过滤
gles30.gltexparameteri(gles30.gl_texture_2d, gles30.gl_texture_min_filter, gles30.gl_linear);
gles30.gltexparameteri(gles30.gl_texture_2d, gles30.gl_texture_mag_filter, gles30.gl_linear);

// 解除纹理绑定
gles30.glbindtexture(gles30.gl_texture_2d, gles30.gl_none);
  1. 生成纹理对象

    • gles30.glgentextures(1, fbotexids);:生成一个纹理对象,并将其id存储在 fbotexids 数组中。
  2. 绑定纹理对象

    • gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]);:将生成的纹理对象绑定到目标 gl_texture_2d
  3. 设置纹理参数

    • gles30.gltexparameteri(gles30.gl_texture_2d, gles30.gl_texture_min_filter, gles30.gl_linear);:设置纹理的缩小过滤为线性过滤。
    • gles30.gltexparameteri(gles30.gl_texture_2d, gles30.gl_texture_mag_filter, gles30.gl_linear);:设置纹理的放大过滤为线性过滤。
  4. 解除纹理绑定

    • gles30.glbindtexture(gles30.gl_texture_2d, gles30.gl_none);:解除当前绑定的纹理对象。

创建和配置帧缓冲区对象

// 生成一个帧缓冲区对象,并将其id存储在 fbo 数组中
gles30.glgenframebuffers(1, fbo);

// 绑定生成的帧缓冲区对象
gles30.glbindframebuffer(gles30.gl_framebuffer, fbo[0]);

// 重新绑定之前创建的纹理对象
gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]);

// 将纹理对象附加到帧缓冲区对象的颜色附件上
gles30.glframebuffertexture2d(gles30.gl_framebuffer, gles30.gl_color_attachment0, gles30.gl_texture_2d, fbotexids[0], 0);

// 为纹理对象分配存储空间
gles30.glteximage2d(gles30.gl_texture_2d, 0, gles30.gl_rgba, imagewidth, imageheight, 0, gles30.gl_rgba, gles30.gl_unsigned_byte, null);

// 解除纹理绑定
gles30.glbindtexture(gles30.gl_texture_2d, gles30.gl_none);

// 解除帧缓冲区对象的绑定
gles30.glbindframebuffer(gles30.gl_framebuffer, gles30.gl_none);
  1. 生成帧缓冲区对象

    • gles30.glgenframebuffers(1, fbo);:生成一个帧缓冲区对象,并将其id存储在 fbo 数组中。
  2. 绑定帧缓冲区对象

    • gles30.glbindframebuffer(gles30.gl_framebuffer, fbo[0]);:将生成的帧缓冲区对象绑定到目标 gl_framebuffer
  3. 重新绑定纹理对象

    • gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]);:将之前创建的纹理对象重新绑定到目标 gl_texture_2d
  4. 附加纹理对象到帧缓冲区对象

    • gles30.glframebuffertexture2d(gles30.gl_framebuffer, gles30.gl_color_attachment0, gles30.gl_texture_2d, fbotexids[0], 0);:将纹理对象作为颜色附件附加到帧缓冲区对象上。
  5. 为纹理对象分配存储空间

    • gles30.glteximage2d(gles30.gl_texture_2d, 0, gles30.gl_rgba, imagewidth, imageheight, 0, gles30.gl_rgba, gles30.gl_unsigned_byte, null);:为纹理对象分配存储空间,并指定其格式和尺寸。
  6. 解除纹理绑定

    • gles30.glbindtexture(gles30.gl_texture_2d, gles30.gl_none);:解除当前绑定的纹理对象。
  7. 解除帧缓冲区对象的绑定

    • gles30.glbindframebuffer(gles30.gl_framebuffer, gles30.gl_none);:解除当前绑定的帧缓冲区对象。

四、fbo 离屏渲染

为了演示 fbo 离屏渲染,我在 fbodrawer.kt 构建了两个 shader,第一个 shader 将 rgb 图片转换为灰度图,第二个 shader 则将纹理渲染到屏幕上。

companion object {
    val vertexshadersource =
        """
        #version 300 es
        layout(location = 0) in vec3 a_posit
        layout(location = 1) in vec2 a_texco
        
        out vec2 v_texcoord;
        
        void main()
        {
            gl_position = vec4(a_position, 1
            v_texcoord = a_texcoord;
        }
        """.trimindent()
    val fragmentshadersource =
        """
        #version 300 es
        precision mediump float;
        
        uniform sampler2d texture0;
        in vec2 v_texcoord;
        out vec4 fragcolor;
        void main(void)
        {
            fragcolor = texture(texture0, v_
        }
        """.trimindent()
    val fbofragmentshadersource =
        """
        #version 300 es
        precision mediump float;
        
        uniform sampler2d texture0;
        in vec2 v_texcoord;
        out vec4 fragcolor;
        void main(void)
        void main(void)
		{
    		vec4 tempcolor = texture(texture0, v_texcoord);
    		float gray = 0.299*tempcolor.a + 0.587*tempcolor.g + 0.114*tempcolor.b;
    		fragcolor = vec4(vec3(gray), 1.0);
		}
        """.trimindent()
}
private val shader = shader(
    vertexshadersource,
    fragmentshadersource
)
private val fboshader = shader(
    vertexshadersource,
    fbofragmentshadersource
)

因此我们需要调用两次 draw 方法:

  1. 第一次,我们的 shader 输入是 rgb 图片的纹理,输出是灰度图纹理
  2. 第二次,我们的 shader 输入是灰度图纹理,然后直接绘制到纹理上
override fun draw() {
    // first, fbo off screen rendering
    gles30.glviewport(0, 0, imagewidth, imageheight)
    gles30.glbindframebuffer(gles30.gl_framebuffer, fbo[0])
    fboshader.use()
    fboshader.setint("texture0", 0)
    gles30.glbindvertexarray(vaos[0])
    gles30.glactivetexture(gles30.gl_texture0)
    gles30.glbindtexture(gles30.gl_texture_2d, imagetexids[0])
    gles30.gldrawelements(gles30.gl_triangles, indices.size, gles30.gl_unsigned_int, 0)
    gles30.glbindtexture(gles30.gl_texture_2d, 0)
    gles30.glbindvertexarray(0)
    gles30.glbindframebuffer(gles30.gl_framebuffer, 0)
    // second, draw texture to screen
    gles30.glviewport(0, 0, screenwidth, screenheight)
    shader.use()
    shader.setint("texture0", 0)
    gles30.glclear(gles30.gl_color_buffer_bit)
    gles30.glbindvertexarray(vaos[0])
    gles30.glactivetexture(gles30.gl_texture0)
    gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]) // 用 fbo 渲染的结果作为纹理的输入
    gles30.gldrawelements(gles30.gl_triangles, indices.size, gles30.gl_unsigned_int, 0)
    gles30.glbindvertexarray(0)
}

这段代码展示了如何使用帧缓冲区对象(fbo)进行离屏渲染,然后将离屏渲染的结果绘制到屏幕上。具体分为两个步骤:第一步是将场景渲染到 fbo,第二步是将 fbo 的内容作为纹理绘制到屏幕上。

第一步:离屏渲染到 fbo

// 设置视口为 fbo 的尺寸
gles30.glviewport(0, 0, imagewidth, imageheight);

// 绑定 fbo
gles30.glbindframebuffer(gles30.gl_framebuffer, fbo[0]);

// 使用离屏渲染的着色器程序
fboshader.use();

// 设置着色器程序中纹理单元的位置
fboshader.setint("texture0", 0);

// 绑定 vao
gles30.glbindvertexarray(vaos[0]);

// 激活纹理单元并绑定需要渲染的纹理
gles30.glactivetexture(gles30.gl_texture0);
gles30.glbindtexture(gles30.gl_texture_2d, imagetexids[0]);

// 绘制元素
gles30.gldrawelements(gles30.gl_triangles, indices.size, gles30.gl_unsigned_int, 0);

// 解除纹理绑定
gles30.glbindtexture(gles30.gl_texture_2d, 0);

// 解除 vao 绑定
gles30.glbindvertexarray(0);

// 解除 fbo 绑定,恢复默认帧缓冲区
gles30.glbindframebuffer(gles30.gl_framebuffer, 0);
  1. 设置视口gles30.glviewport(0, 0, imagewidth, imageheight) 设置渲染区域为 fbo 的尺寸。
  2. 绑定 fbogles30.glbindframebuffer(gles30.gl_framebuffer, fbo[0]) 绑定帧缓冲区对象。
  3. 使用着色器程序fboshader.use() 使用用于离屏渲染的着色器程序。
  4. 设置纹理单元fboshader.setint("texture0", 0) 设置着色器程序中的纹理单元。
  5. 绑定 vaogles30.glbindvertexarray(vaos[0]) 绑定顶点数组对象(vao)。
  6. 激活并绑定纹理gles30.glactivetexture(gles30.gl_texture0)gles30.glbindtexture(gles30.gl_texture_2d, imagetexids[0]) 激活并绑定需要渲染的纹理。
  7. 绘制元素gles30.gldrawelements(gles30.gl_triangles, indices.size, gles30.gl_unsigned_int, 0) 使用索引数组绘制三角形。
  8. 解除绑定:解除纹理和 vao 的绑定,以及 fbo 的绑定,恢复默认帧缓冲区。

第二步:将 fbo 的内容绘制到屏幕上

// 设置视口为屏幕尺寸
gles30.glviewport(0, 0, screenwidth, screenheight);

// 使用屏幕渲染的着色器程序
shader.use();

// 设置着色器程序中纹理单元的位置
shader.setint("texture0", 0);

// 清除颜色缓冲区
gles30.glclear(gles30.gl_color_buffer_bit);

// 绑定 vao
gles30.glbindvertexarray(vaos[0]);

// 激活纹理单元并绑定 fbo 的纹理
gles30.glactivetexture(gles30.gl_texture0);
gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]);

// 绘制元素
gles30.gldrawelements(gles30.gl_triangles, indices.size, gles30.gl_unsigned_int, 0);

// 解除 vao 绑定
gles30.glbindvertexarray(0);
  1. 设置视口gles30.glviewport(0, 0, screenwidth, screenheight) 设置渲染区域为屏幕的尺寸。
  2. 使用着色器程序shader.use() 使用用于屏幕渲染的着色器程序。
  3. 设置纹理单元shader.setint("texture0", 0) 设置着色器程序中的纹理单元。
  4. 清除颜色缓冲区gles30.glclear(gles30.gl_color_buffer_bit) 清除颜色缓冲区。
  5. 绑定 vaogles30.glbindvertexarray(vaos[0]) 绑定顶点数组对象(vao)。
  6. 激活并绑定纹理gles30.glactivetexture(gles30.gl_texture0)gles30.glbindtexture(gles30.gl_texture_2d, fbotexids[0]) 激活并绑定 fbo 的纹理(即离屏渲染的结果)。
  7. 绘制元素gles30.gldrawelements(gles30.gl_triangles, indices.size, gles30.gl_unsigned_int, 0) 使用索引数组绘制三角形。
  8. 解除绑定:解除 vao 的绑定。

总结

  • 第一步:在 fbo 中进行离屏渲染,将结果存储在一个纹理对象中。
  • 第二步:将 fbo 中的纹理对象作为输入,绘制到屏幕上。

这种方法在图形应用程序中非常常见,特别是在实现多重渲染效果(如后期处理、反射、阴影映射等)时。

参考

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com