【安卓OpenGLES】 开发入门(九):离屏渲染FBO

5,801 阅读6分钟

本文正在参加「金石计划」

FBO离屏渲染

到目前为止,我们已经使用过了几个buffer:可写入颜色值的颜色缓冲区,可以写入和测试深度的深度缓冲区以及模板缓冲区。这些缓冲区被存放在GPU的某内存中,这块内存就是一个帧缓冲

之前我们使用的都是屏幕提供的默认缓冲区,默认缓冲区绘制后的颜色和深度等都会呈现在屏幕上

OpenGL还提供了一个更加灵活的API,可以让开发者自定义帧缓冲区,然后写入到对应的纹理或者Render缓冲区对象中(RBO):这就是我们要说的离屏渲染

顾名思义,离屏渲染就是不会绘制在屏幕上的绘制操作,而是在GPU后台进行绘制,并写入到纹理或者RBO中

绘制后的纹理或者RBO又可以用来绘制到屏幕上,相当于做了一个中间层的转换

什么是RBO?

RBO(Render Buffer Object)即渲染缓冲区对象,是一个由应用程序分配的 2D 图像缓冲区。渲染缓冲区可以用于分配和存储深度或者模板值,可以用作 FBO 中的深度或者模板附着。

帧缓冲区对象,渲染缓冲区对象和纹理

为什么要使用FBO?

首先,在默认情况下,OpenGL通过绘制到窗口的默认帧缓冲区,实现纹理的渲染,但是这种情况下只能适配纹理尺寸小余帧缓冲区的情况,如果纹理太大就没法正常绘制。

其次:我们知道GPU的图像处理能力是很强大的,在绘制的过程中,也不是所有的场景都需要绘制到屏幕上的,如图像的前期处理,缩放,滤镜等耗时操作都可以使用FBO来做预处理,这样可以大大提高GPU在后台的利用率。

FBO如何使用?

FBO的使用过程一般有以下几个:

  • 1.创建并初始化FBO

    • 1.1 : 创建2D纹理用于fbo的颜色依附:glGenTextures->glBindTexture
    • 1.2:创建FBO:glGenFramebuffers
    • 1.3:绑定FBO:glBindFramebuffer(GL_FRAMEBUFFER,fbo_id);
    • 1.4 : 绑定FBO纹理:glBindTexture->glFramebufferTexture2D->glTexImage2D
    • 1.5 : 检测FBO的完整性:glCheckFramebufferStatus
    • 1.6:解绑纹理:glBindTexture,解绑FBO:glBindFramebuffer
    //1.1:创建2D纹理用于fbo的颜色依附
    glGenTextures(1,&m_FboTextureId);
    glBindTexture(GL_TEXTURE_2D,m_FboTextureId);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, GL_NONE);
    ​
    // 1.2:创建FBO:glGenFramebuffers
    glGenFramebuffers(1,&m_FboId);
    // 1.3:绑定FBO:glBindFramebuffer(GL_FRAMEBUFFER,fbo_id);
    glBindFramebuffer(GL_FRAMEBUFFER,m_FboId);
    // 1.4: 绑定FBO纹理:glBindTexture->glFramebufferTexture2D->glTexImage2D
    glBindTexture(GL_TEXTURE_2D,m_FboTextureId);
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,m_FboTextureId,0);
    glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,imageWidth,imageHeight,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr);
    // 1.5: 检测FBO的完整性:glCheckFramebufferStatus
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE)
    {
        LOGCATE("FBOSample::CreateFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE");
        return ;
    }
    // 1.6:解绑纹理:glBindTexture,解绑FBO:glBindFramebuffer
    glBindTexture(GL_TEXTURE_2D,GL_NONE);
    glBindFramebuffer(GL_FRAMEBUFFER, GL_NONE);
    ​
    
  • 2.使用FBO对象创建的纹理或者RBO对象进行渲染

    • 2.1:离屏渲染
    • 2.1.1:绑定FBO并使用离屏渲染的着色器对象:glBindFramebuffer->glUseProgram(fboShaderObj);
    • 2.1.2:绑定VAO并设置纹理对象:glBindVertexArray->glUniform1i
    • 2.1.3:开始离屏渲染纹理:glDrawElements
    • 2.1.4:解绑VAO:glBindVertexArray->glBindFramebuffer
    • 2.1.5:解绑FBO:glBindFramebuffer(GL_FRAMEBUFFER, 0);
    //2.1:离屏渲染
    {
        glPixelStorei(GL_UNPACK_ALIGNMENT,1);
        glViewport(0,0,imageWidth,imageHeight);
        //2.1.1:绑定FBO并使用离屏渲染的着色器对象:glBindFramebuffer,glUseProgram(fboShaderObj);
        glBindFramebuffer(GL_FRAMEBUFFER,m_FboId);
        glUseProgram(fbo_ProgramObj);
        //2.1.2:绑定VAO并设置纹理对象:glBindVertexArray->glUniform1i
        glBindVertexArray(m_fboVAO);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D,textureID);
        glUniform1i(glGetUniformLocation(fbo_ProgramObj,"s_TextureMap"),0);
        //2.1.3:开始离屏渲染纹理:glDrawElements
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void *)0);
        //2.1.4:解绑VAO:glBindVertexArray->glBindFramebuffer
        glBindTexture(GL_TEXTURE_2D,GL_NONE);
        glBindVertexArray(GL_NONE);
        //2.1.5:解绑FBO:glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glBindFramebuffer(GL_FRAMEBUFFER,0);
    }
    

    离屏渲染会得到一个新的纹理对象,可以提供给普通渲染使用。

    注意:在离屏渲染的时候我们将渲染的视口(glViewport)设置为了原纹理贴图的宽高,然后将离屏渲染顶点数据设置为1为单位,这样就可以渲染出一个和原纹理贴图一样尺寸的纹理对象

    • 2.2:普通渲染
    • 2.2.1:使用普通着色器对象:glUseProgram(commonShaderObj);
    • 2.2.2:绑定VAO并设置纹理。使用2.1中离屏渲染的FboTextureId纹理id
    • 2.2.3:开始绘制:glDrawElements
    • 2.2.4:解绑VAO
    //2.2:普通渲染
    {
        glViewport(0, 0, mScreenWidth, mScreenHeight);
        //2.2.1:使用普通着色器对象:glUseProgram(commonShaderObj);
        glUseProgram(programObj);
        //2.2.2:绑定VAO并设置纹理。使用2.1中离屏渲染的FboTextureId纹理id
        glBindVertexArray(VAO);
    ​
        glUniform1i(glGetUniformLocation(programObj, "s_TextureMap"), 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, m_FboTextureId);
        //2.2.3:开始绘制:glDrawElements
        glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
        // 2.2.4:解绑VAO
        glBindTexture(GL_TEXTURE_2D,GL_NONE);
        glBindVertexArray(GL_NONE);
    }
    
  • 顶点数据:

//0.普通渲染顶点数据
GLfloat vertices[] = {
        //顶点             //纹理坐标
        -0.5f, -0.5f, 0.0f,0.0f, 0.0f,//V0
        0.5f,-0.5f, 0.0f,1.0f,0.0f,//V1
        0.5f, 0.5f, 0.0f, 1.0f, 1.0f,//V2
        -0.5f, 0.5f, 0.0f,0.0f, 1.0f//V3
};
//0.离屏渲染顶点数据:设置以1位单位距离
GLfloat fbo_vertices[] = {
        //顶点             //纹理坐标
        -1.0f, -1.0f, 0.0f,0.0f, 0.0f,//V0
        1.0f,-1.0f, 0.0f,1.0f,0.0f,//V1
        1.0f, 1.0f, 0.0f, 1.0f, 1.0f,//V2
        -1.0f, 1.0f, 0.0f,0.0f, 1.0f//V3
};
//确定三角形索引
GLint indices[] = {
        0,1,2,
        0,2,3
};

这里小余对离屏渲染使用不同的顶点数据,配合前面说的视口变化操作,得到一个新的和原纹理贴图尺寸一样的纹理对象。

  • 着色器程序
//1.创建着色器程序,此处将着色器程序创建封装到一个工具类中
char vShaderStr[] =
        "#version 300 es                          \n"
        "layout(location = 0) in vec4 vPosition;  \n"
        "layout(location = 1) in vec2 texCords;  \n"
        "out vec2 v_texCoord;  \n"
        "void main()                              \n"
        "{                                        \n"
        "   gl_Position = vPosition;              \n"
        "   v_texCoord = texCords;              \n"
        "}                                        \n";
​
char fShaderStr[] =
        "#version 300 es                              \n"
        "precision mediump float;                     \n"
        "in vec2 v_texCoord;                          \n"
        "out vec4 fragColor;                          \n"
        "uniform sampler2D s_TextureMap;                          \n"
        "void main()                                  \n"
        "{                                            \n"
        "   fragColor = texture(s_TextureMap,v_texCoord);  \n"
        "}                                            \n";
// 用于离屏渲染的片段着色器脚本,取每个像素的灰度值
    char fbo_fShaderStr[] =
            "#version 300 es\n"
            "precision mediump float;\n"
            "in vec2 v_texCoord;\n"
            "layout(location = 0) out vec4 outColor;\n"
            "uniform sampler2D s_TextureMap;\n"
            "void main()\n"
            "{\n"
            "    vec4 tempColor = texture(s_TextureMap, v_texCoord);\n"
            "    //   RGB转YUV:\n"
            "    //   Y = 0.299 * R + 0.587 * G + 0.114*B\n"
            "    //   U = - 0.1687 * R - 0.3313 * G + 0.5* B + 128    注: +128 的含义是让UV的范围处于整数区间(0-255)\n"
            "    //   V = 0.5 * R - 0.4187 * G - 0.0813 * B + 128     注: +128 的含义是让UV的范围处于整数区间(0-255)\n"
            "    //  下面的luminance 只取了YUV的Y,所以是黑白的照片"
            "    float luminance = tempColor.r * 0.299 + tempColor.g * 0.587 + tempColor.b * 0.114;\n"
            "    outColor = vec4(vec3(luminance), tempColor.a);\n"
            "}"; // 输出灰度图

这里要显示灰度图,涉及到RGB和YUV的转换:

RGBYUV:
Y = 0.299 * R + 0.587 * G + 0.114*B"
U = - 0.1687 * R - 0.3313 * G + 0.5* B + 128    注: +128 的含义是让UV的范围处于整数区间(0-255V = 0.5 * R - 0.4187 * G - 0.0813 * B + 128     注: +128 的含义是让UV的范围处于整数区间(0-255

效果图:

image-20230413001001895

可以看到正确显示出了灰度值,说明我们离屏渲染操作是成功了的。

总结

离屏渲染这块可能大家第一次学的话比较难看懂,只要理解:离屏渲染可以在GPU后台对纹理做一些预处理操作,记住是预处理,且这中间可以经过多次离屏渲染,然后最终发送到进行屏幕绘制的默认帧缓冲中

离屏渲染可以做的东西很多:只要是对图片进行处理的,比如图片缩放,裁剪,旋转,模糊,滤镜等等

以后工作中做的大部分是对图片的处理操作,也就是FBO。所以要入门图像处理,FBO也是一个必会技能。