Opengl ES之FBO

2022-09-29 12:01:22

FBO介紹

FBO幀緩衝物件,它的主要作用一般就是用作離屏渲染,例如做Camera相機影象採集進行後期處理時就可能會用到FBO。假如相機出圖的是OES紋理,為了方便後期處理,
一般先將OES紋理通過FBO轉換成普通的2D紋理,然後再通過FBO等增加美顏等其他各種特效濾鏡,最後將FBO一路流送進編碼器進行編碼,另外一路渲染到螢幕上進行預覽顯示。

FBO總結起來就是可以暫時將未處理完的幀不直接渲染到螢幕上,而是渲染到離屏Buffer中快取起來,在恰當的時機再取出來渲染到螢幕。

FBO(Frame Buffer Object)幀緩衝物件提供了與顏色緩衝區(color buffer)、深度緩衝區(depth buffer)和模版緩衝區(stencil buffer) ,但並不會直接為這些緩衝區分配空間,而只是為這些緩衝區提供一個或多個掛接點。我們需要分別為各個緩衝區建立物件,申請空間,然後掛接到相應的掛接點上。

從上圖可以看出FBO中包含了:

  1. 多個顏色附著點(GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1...)
  2. 一個深度附著點(GL_DEPTH_ATTACHMENT)
  3. 一個模板附著點(GL_STENCIL_ATTACHMENT)

所謂的顏色附著(紋理附著)就是用於將顏色渲染到紋理中去的意思。後面我們主要介紹FBO的顏色附著。

如何使用FBO

  1. 使用函數glGenFramebuffers生成一個FBO物件,儲存物件ID。
  2. 使用函數glBindFramebuffer繫結FBO。
  3. 使用函數glFramebufferTexture2D關聯紋理和FBO,並執行渲染步驟。後續如果需要使用FBO的效果時只需要操作與FBO繫結的紋理即可。
  4. 使用函數glBindFramebuffer解綁FBO,一般在Opengl中ID引數傳遞0就是解綁。
  5. 使用函數glDeleteFramebuffers刪除FBO。

當掛接完成之後,我們在執行FBO下面的操作之前,可以檢查一下FBO的狀態,使用函數GLenum glCheckFramebufferStatus(GLenum target)檢查。

本著學以致用的原則,我們將結合之前的文章,例如紋理貼圖、VBO/VAO、EBO等相關知識點,使用這些知識點結合FBO繪製做一個實踐的例子:首先將紋理渲染到FBO上去,然後再將FBO的紋理渲染到螢幕上。

插個話。。。總有人盜用不貼原文連結,看看是誰。。。

首先上程式碼,然後我們挑重要的稍微解讀一下:
FBOOpengl.h

class FBOOpengl:public BaseOpengl{

public:
    FBOOpengl();
    void onFboDraw();
    virtual ~FBOOpengl();
    // override要麼就都寫,要麼就都不寫,不要一個虛擬函式寫override,而另外一個虛擬函式不寫override,不然可能編譯不過
    virtual void onDraw() override;
    virtual void setPixel(void *data, int width, int height, int length) override;
private:
    void fboPrepare();
    GLint positionHandle{-1};
    GLint textureHandle{-1};
    GLuint vbo{0};
    GLuint vao{0};
    GLuint ebo{0};
    // 本身影象紋理id
    GLuint imageTextureId{0};
    // fbo紋理id
    GLuint fboTextureId{0};
    GLint textureSampler{-1};
    GLuint fboId{0};
    // 用於fbo的vbo和vao  也可以用陣列的形式,這裡為了方便理解先獨立開來
    GLuint fboVbo{0};
    GLuint fboVao{0};
    int imageWidth{0};
    int imageHeight{0};
};

注意:override作為現代C++的一個關鍵字,使用的時候需要注意一點,要麼就整個類的虛擬函式都用,要麼整個類的虛擬函式都不用,不要一個虛擬函式用override修飾,另外一個虛擬函式又不用override關鍵字修飾,不然很有可能會編譯不過的。

在FBOOpengl中為了區分螢幕渲染和FBO離屏渲染,我們宣告了兩套VAO和VBO。

FBOOpengl.cpp

#include "FBOOpengl.h"
#include "../utils/Log.h"

// 頂點著色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aPosition;\n"
                         "in vec2 aTexCoord;\n"
                         "out vec2 TexCoord;\n"
                         "void main() {\n"
                         "  TexCoord = aTexCoord;\n"
                         "  gl_Position = aPosition;\n"
                         "}";

// 片元著色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "out vec4 FragColor;\n"
                              "in vec2 TexCoord;\n"
                              "uniform sampler2D ourTexture;\n"
                              "void main()\n"
                              "{\n"
                              "    FragColor = texture(ourTexture, TexCoord);\n"
                              "}";

const static GLfloat VERTICES_AND_TEXTURE[] = {
        0.5f, -0.5f, // 右下
        // 紋理座標
        1.0f,1.0f,
        0.5f, 0.5f, // 右上
        // 紋理座標
        1.0f,0.0f,
        -0.5f, -0.5f, // 左下
        // 紋理座標
        0.0f,1.0f,
        -0.5f, 0.5f, // 左上
        // 紋理座標
        0.0f,0.0f
};

// 紋理座標原點在圖片的左上角    又是倒置的?什麼鬼?疑惑吧?
//const static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
//        1.0f, -1.0f, // 右下
//        // 紋理座標
//        1.0f,1.0f,
//        1.0f, 1.0f, // 右上
//        // 紋理座標
//        1.0f,0.0f,
//        -1.0f, -1.0f, // 左下
//        // 紋理座標
//        0.0f,1.0f,
//        -1.0f, 1.0f, // 左上
//        // 紋理座標
//        0.0f,0.0f
//};

// 真正的紋理座標在圖片的左下角
const static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
        1.0f, -1.0f, // 右下
        // 紋理座標
        1.0f,0.0f,
        1.0f, 1.0f, // 右上
        // 紋理座標
        1.0f,1.0f,
        -1.0f, -1.0f, // 左下
        // 紋理座標
        0.0f,0.0f,
        -1.0f, 1.0f, // 左上
        // 紋理座標
        0.0f,1.0f
};

// 使用byte型別比使用short或者int型別節約記憶體
const static uint8_t indices[] = {
        // 注意索引從0開始!
        // 此例的索引(0,1,2,3)就是頂點陣列vertices的下標,
        // 這樣可以由下標代表頂點組合成矩形
        0, 1, 2, // 第一個三角形
        1, 2, 3  // 第二個三角形
};

FBOOpengl::FBOOpengl() {
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    textureHandle = glGetAttribLocation(program,"aTexCoord");
    textureSampler = glGetUniformLocation(program,"ourTexture");
    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("textureHandle:%d",textureHandle);
    LOGD("textureSample:%d",textureSampler);
    // VAO
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // vbo
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES_AND_TEXTURE), VERTICES_AND_TEXTURE, GL_STATIC_DRAW);

    // stride 步長 每個頂點座標之間相隔4個資料點,資料型別是float
    glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
    // 啟用頂點資料
    glEnableVertexAttribArray(positionHandle);
    // stride 步長 每個顏色座標之間相隔4個資料點,資料型別是float,顏色座標索引從2開始
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
                          (void *) (2 * sizeof(float)));
    // 啟用紋理座標陣列
    glEnableVertexAttribArray(textureHandle);

    // EBO
    glGenBuffers(1,&ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);

    // 這個順序不能亂啊,先解除vao,再解除其他的,不然在繪製的時候可能會不起作用,需要重新glBindBuffer才生效
    // vao解除
    glBindVertexArray(0);
    // 解除繫結
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 解除繫結
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);

    LOGD("program:%d", program);
    LOGD("positionHandle:%d", positionHandle);
    LOGD("colorHandle:%d", textureHandle);
}

void FBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel");
    imageWidth = width;
    imageHeight = height;
    glGenTextures(1, &imageTextureId);

    // 啟用紋理,注意以下這個兩句是搭配的,glActiveTexture啟用的是那個紋理,就設定的sampler2D是那個
    // 預設是0,如果不是0的話,需要在onDraw的時候重新啟用一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一樣的
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);

    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);
    // 為當前繫結的紋理物件設定環繞、過濾方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    // 生成mip貼圖
    glGenerateMipmap(GL_TEXTURE_2D);

    // 解繫結
    glBindTexture(GL_TEXTURE_2D, 0);
}

void FBOOpengl::fboPrepare(){

    // VAO
    glGenVertexArrays(1, &fboVao);
    glBindVertexArray(fboVao);

    // vbo
    glGenBuffers(1, &fboVbo);
    glBindBuffer(GL_ARRAY_BUFFER, fboVbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(FBO_VERTICES_AND_TEXTURE), FBO_VERTICES_AND_TEXTURE, GL_STATIC_DRAW);

    // stride 步長 每個頂點座標之間相隔4個資料點,資料型別是float
    glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
    // 啟用頂點資料
    glEnableVertexAttribArray(positionHandle);
    // stride 步長 每個顏色座標之間相隔4個資料點,資料型別是float,顏色座標索引從2開始
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
                          (void *) (2 * sizeof(float)));
    // 啟用紋理座標陣列
    glEnableVertexAttribArray(textureHandle);

    // EBO
    glGenBuffers(1,&ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);

    // 這個順序不能亂啊,先解除vao,再解除其他的,不然在繪製的時候可能會不起作用,需要重新glBindBuffer才生效
    // vao解除
    glBindVertexArray(0);
    // 解除繫結
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 解除繫結
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);

    glGenTextures(1, &fboTextureId);
    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D, fboTextureId);
    // 為當前繫結的紋理物件設定環繞、過濾方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    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);

    glGenFramebuffers(1,&fboId);
    glBindFramebuffer(GL_FRAMEBUFFER,fboId);
    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D,fboTextureId);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTextureId, 0);
    // 這個紋理是多大的?
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // 檢查FBO狀態
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE) {
        LOGE("FBOSample::CreateFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE");
    }
    // 解綁
    glBindTexture(GL_TEXTURE_2D, GL_NONE);
    glBindFramebuffer(GL_FRAMEBUFFER, GL_NONE);
}

void FBOOpengl::onFboDraw() {
    fboPrepare();

    glBindFramebuffer(GL_FRAMEBUFFER, fboId);

    // 主要這個的大小要與FBO繫結時的紋理的glTexImage2D 設定的大小一致呀
    glViewport(0,0,imageWidth,imageHeight);

     // FBO繪製
    // 清屏
    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 啟用紋理
    glActiveTexture(GL_TEXTURE1);
    glUniform1i(textureSampler, 1);

    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    // VBO與VAO配合繪製
    // 使用vao
    glBindVertexArray(fboVao);
    // 使用EBO
// 使用byte型別節省記憶體
    glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_BYTE,(void *)0);
    glUseProgram(0);
    // vao解除繫結
    glBindVertexArray(0);

    if (nullptr != eglHelper) {
        eglHelper->swapBuffers();
    }
    glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void FBOOpengl::onDraw() {

    // 先在FBO離屏渲染
    onFboDraw();

    // 恢復繪製螢幕寬高
    glViewport(0,0,eglHelper->viewWidth,eglHelper->viewHeight);

    // 繪製到螢幕
    // 清屏
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 啟用紋理
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);

    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D, fboTextureId);

    // VBO與VAO配合繪製
    // 使用vao
    glBindVertexArray(vao);
    // 使用EBO
// 使用byte型別節省記憶體
    glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_BYTE,(void *)0);
    glUseProgram(0);
    // vao解除繫結
    glBindVertexArray(0);

    // 禁用頂點
    glDisableVertexAttribArray(positionHandle);
    if (nullptr != eglHelper) {
        eglHelper->swapBuffers();
    }

    glBindTexture(GL_TEXTURE_2D, 0);
}


FBOOpengl::~FBOOpengl() noexcept {
    glDeleteBuffers(1,&ebo);
    glDeleteBuffers(1,&vbo);
    glDeleteVertexArrays(1,&vao);
    // ... 刪除其他,例如fbo等
}

按照之前Opengl ES之紋理貼圖 一文所說的,在Opengl ES中進行紋理貼圖時直接以圖片的左上角為(0,0)原點進行貼圖,以糾正紋理貼圖倒置的問題,那麼這次在繫結FBO之後之後我們就這麼幹,
使用以下的頂點座標和紋理座標:

// 紋理座標原點在圖片的左上角    又是倒置的?什麼鬼?疑惑吧?
const static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
        1.0f, -1.0f, // 右下
        // 紋理座標
        1.0f,1.0f,
        1.0f, 1.0f, // 右上
        // 紋理座標
        1.0f,0.0f,
        -1.0f, -1.0f, // 左下
        // 紋理座標
        0.0f,1.0f,
        -1.0f, 1.0f, // 左上
        // 紋理座標
        0.0f,0.0f
};

一執行,我們驚喜地發現,實際情況居然和 Opengl ES之紋理貼圖 一文所說的不一樣了,經過FBO後的貼圖再渲染到螢幕時,居然圖片是倒置的,如下圖:

這是什麼為什麼呢?

預設情況下,OpenGL ES 通過繪製到視窗系統提供的幀緩衝區,也就是螢幕本身就是一個預設的FBO,而使用FBO進行紋理貼圖的時候需要以真正的紋理座標(原點0,0在圖片的左下角)為基準進行貼圖。因此如果直接使用螢幕進行紋理貼圖,其實是應該細分成兩個
過程的,先以左下角為紋理座標原點進行貼圖,然後將貼圖後的螢幕預設FBO旋轉繞X軸旋轉180度與螢幕座標(左上角是座標原點)重合,但是這兩個細分的過程可以做個取巧就是直接以左上角為紋理座標原點進行貼圖,得到的結果是一樣的。

但是我們在單獨使用FBO時,仍應該遵循以左下角為紋理座標原點的原則進行紋理貼圖。因此我們只需修改一下頂點座標和紋理座標,以左下角為紋理座標作為原點進行FBO貼圖,然後再將FBO旋繞到螢幕上即可:

// 真正的紋理座標在圖片的左下角
const static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
        1.0f, -1.0f, // 右下
        // 紋理座標
        1.0f,0.0f,
        1.0f, 1.0f, // 右上
        // 紋理座標
        1.0f,1.0f,
        -1.0f, -1.0f, // 左下
        // 紋理座標
        0.0f,0.0f,
        -1.0f, 1.0f, // 左上
        // 紋理座標
        0.0f,1.0f
};

執行結果如圖:

往期系列

Opengl ES之EGL環境搭建
Opengl ES之著色器
Opengl ES之三角形繪製
Opengl ES之四邊形繪製
Opengl ES之紋理貼圖
Opengl ES之VBO和VAO
Opengl ES之EBO

關注我,一起進步,人生不止coding!!!