cocos2D-X源碼分析之從cocos2D-X學習OpenGL(2)----QUAD_COMMAND

        個人原創,歡迎轉載,轉載請註明原文地址http://blog.csdn.net/bill_man

        上一篇文章介紹了cocos2d-x的基本渲染結構,這篇順着之前的渲染結構介紹渲染命令QUAD_COMMAND命令的部分,通過這部分的函數,學習opengl處理圖片渲染的方法,首先介紹這節需要涉及到的基本概念VAO和VBO。

VAO和VBO:

    頂點數組對象(Vertex Array Object  即VAO)是一個包含一個或數個頂點緩衝區對象(Vertex Buffer Object, 即 VBO)的對象,一般存儲一個可渲染物體的所有信息。頂點緩衝區對象(VertexBuffer Object VBO)是你顯卡內存中的一塊高速內存緩衝區,用來存儲頂點的所有信息。

這些概念顯得很晦澀,簡而言之,一般我們繪製一些圖形需要將所有頂點的信息存儲在一個數組裏,但是經常會出現一些點是被重複使用的,這樣就會出現一個點的信息的存儲空間被重複使用的問題,這樣第一會造成存儲控件的浪費,第二就是如果我們要修改這個點的信息,需要改多次。所以我們採用索引的方式來描述圖形,這樣可以用一個數組存儲點的信息,另外一個數組存儲點的索引,這樣所有的點都是不同的,另外把頂點信息存儲在顯卡的內存中,減少了cpu向gpu傳輸數據的時間,提高了程序的渲染效率,這就是VBO,在OpenGL3.0中,出現了更進一步的VAO,VBO通過繪製上下文獲得繪製狀態,VAO可以擁有多個VBO,它記錄所有繪製狀態,它的代碼更簡潔,效率更高,在cocos2d-x的繪製中,我們會判斷底層是否支持VAO,如果支持VAO,那麼優先採用VAO繪製。二者的區別可以從初始化就可以看出來:

void Renderer::setupBuffer()
{
    if(Configuration::getInstance()->supportsShareableVAO())
    {
        //初始化VBO和VAO
        setupVBOAndVAO();
    }
    else
    {
        //不支持VAO,只初始化VBO
        setupVBO();
    }
}
void Renderer::setupVBOAndVAO()
{
    //一個VAO
    glGenVertexArrays(1, &_quadVAO);
    //綁定VAO
    GL::bindVAO(_quadVAO);
    //創建生成兩個VBO
    glGenBuffers(2, &_buffersVBO[0]);
    //頂點Buffer
    glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(_quads[0]) * VBO_SIZE, _quads, GL_DYNAMIC_DRAW);
    //這裏就是VAO和VBO的區別,VAO把這些放到初始化中,無論後面繪製多少次,只要他不被改變,這段代碼只會被調用一次,而VBO中,這個功能的代碼會在每次被繪製時調用,這樣就節約了效率
    //位置
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(V3F_C4B_T2F), (GLvoid*) offsetof( V3F_C4B_T2F, vertices));
    //顏色
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_COLOR);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(V3F_C4B_T2F), (GLvoid*) offsetof( V3F_C4B_T2F, colors));
    //紋理座標數據
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_TEX_COORDS);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORDS, 2, GL_FLOAT, GL_FALSE, sizeof(V3F_C4B_T2F), (GLvoid*) offsetof( V3F_C4B_T2F, texCoords));
    //索引Buffer
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices[0]) * VBO_SIZE * 6, _indices, GL_STATIC_DRAW);
    //取消VAO
    GL::bindVAO(0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    CHECK_GL_ERROR_DEBUG();
}
void Renderer::setupVBO()
{
    //創建生成兩個VBO
    glGenBuffers(2, &_buffersVBO[0]);
    //調用函數綁定buffer
    mapBuffers();
}
void Renderer::mapBuffers()
{
    //GL_ARRAY_BUFFER 表示頂點數據
    //GL_ELEMENT_ARRAY_BUFFER 表示索引數據
    //避免改變buffer元素
    GL::bindVAO(0);
    //綁定id 頂點數據
    glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);
    //爲改id制定一段內存區域
    glBufferData(GL_ARRAY_BUFFER, sizeof(_quads[0]) * VBO_SIZE, _quads, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    //第二個VBO 索引數據
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices[0]) * VBO_SIZE * 6, _indices, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    CHECK_GL_ERROR_DEBUG();
}

需要介紹的兩個關鍵的函數

glBindBuffer:它綁定緩衝區對象表示選擇未來的操作將影響哪個緩衝區對象。如果應用程序有多個緩衝區對象,就需要多次調用glBindBuffer()函數:一次用於初始化緩衝區對象以及它的數據,以後的調用要麼選擇用於渲染的緩衝區對象,要麼對緩衝區對象的數據進行更新。

當傳入的第二個參數第一次使用一個非零無符號整數時,創建一個新的緩衝區對象;當第二個參數是之前使用過的,這個緩衝區對象成爲活動緩衝區對象;如果第二個參數值爲0時,停止使用緩衝區對象

glBufferData:保留空間存儲數據,他分配一定大小的(第二個參數)的openGL服務端內存,用於存儲頂點數據或索引。這個被綁定的對象之前相關聯的數據都會被清除。

glBufferData參數介紹

參數1,目標GL_ARRAY_BUFFER或者GL_ELEMENT_ARRAY_BUFFER

參數2,內存容量

參數3,用於初始化緩衝區對象,可以使一個指針,也可以是空

參數4,如何讀寫,可以選擇如下幾種

    GL_DYNAMIC_DRAW:多次指定,多次作爲繪圖和圖像指定函數的源數據,緩衝區對象的數據不僅常常需要進行更新,而且使用頻率也非常高

    GL_STATIC_DRAW:數據只指定一次,多次作爲繪圖和圖像指定函數的源數據,緩衝區對象的數據只指定1次,但是這些數據被使用的頻率很高

    GL_STREAM_DRAW:數據只指定一次,最多隻有幾次作爲繪圖和圖像指定函數的源數據,緩衝區對象中的數據常常需要更新,但是在繪圖或其他操作中使用這些數據的次數較少

從初始化的代碼上,爲什麼VAO反倒複雜了呢?因爲他只是把繪製時需要做的一些事情提前放到初始化函數中,來看一下繪製流程。

    //當前的openGL是否支持VAO
    if (Configuration::getInstance()->supportsShareableVAO())
    {
        //綁定頂點數組
        glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);
        //向緩衝區申請空間並指定數據傳輸方式
        glBufferData(GL_ARRAY_BUFFER, sizeof(_quads[0]) * (_numQuads), nullptr, GL_DYNAMIC_DRAW);
        //提供緩衝區對象包含整個數據集合的更新
        void *buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
        memcpy(buf, _quads, sizeof(_quads[0])* (_numQuads));
        //緩衝區對象的更新完成
        glUnmapBuffer(GL_ARRAY_BUFFER);
        //爲了禁用緩衝區對象,可以用0作爲緩衝區對象的標識符來調用glBindBuffer()函數。這將把OpenGL切換爲默認的不使用緩衝區對象的模式。
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        //Bind VAO
        GL::bindVAO(_quadVAO);
    }
    else
    {
#define kQuadSize sizeof(_quads[0].bl)
        glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(_quads[0]) * _numQuads , _quads, GL_DYNAMIC_DRAW);
        //激活頂點顏色紋理座標的屬性
        GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX);
        //頂點
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, vertices));
        //顏色
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, colors));
        //紋理座標
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORDS, 2, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, texCoords));

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
    }


可以看到,這些設置屬性的函數放在了繪製函數裏,雖然看似是一樣的,但是繪製函數會被調用的更頻繁,所以把這些函數放到初始化函數中可以大幅提高程序的效率。

這裏介紹VAO的兩個函數:

glMapBuffer函數返回一個指針,指向與第一個參數相關聯的當前綁定緩衝區對象的數據存儲。第一個參數與glBufferData的第一個參數一致。第二個參數是GL_READ_ONLY、GL_WRITE_ONLY或GL_READ_WRITE之一,表示可以對數據進行的操作。

glUnmapBuffer表示對當前綁定緩衝區對象的更新已經完成,並且這個緩衝區可以釋放。

enableVertexAttribs激活相關屬性,激活的屬性可以調用glVertexAttribPointer指定數據源,可選的有VERTEX_ATTRIB_FLAG_POSITION,VERTEX_ATTRIB_FLAG_COLOR和VERTEX_ATTRIB_FLAG_TEX_COORDS,這裏這個參數是激活這三個。

glVertexAttribPointer指定了渲染時第一個參數代表的索引值的頂點屬性數組的數據格式和位置。

第一個參數指定要修改的頂點屬性的索引值,包括VERTEX_ATTRIB_POSITION(位置),VERTEX_ATTRIB_COLOR(顏色),VERTEX_ATTRIB_TEX_COORDS(紋理座標)。

第二個參數指定每個屬性值的組件數量且必須爲1、2、3、4之一。

第三個參數指定數組中每個組件的數據類型。可用的符號常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT,GL_FIXED, 和 GL_FLOAT,初始值爲GL_FLOAT。

第四個參數指定當被訪問時,固定點數據值是否應該被歸一化(GL_TRUE,意味着整數型的值會被映射至區間[-1,1](有符號整數),或者區間[0,1](無符號整數))或者直接轉換爲固定點值(GL_FALSE)。

第五個參數指定了一個屬性到下一個屬性之間的步長(這就允許屬性值被存儲在單一數組或者不同的數組中)。也就是連續頂點屬性之間的偏移量。如果爲0,那麼它們是緊密排列在一起的。初始值爲0。

第六個參數指定一個指針,指向數組中第一個頂點屬性的第一個組件。初始值爲0。

最後需要調用繪製元素函數,繪製這些信息

glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );

它根據索引繪圖(注意:頂點數據和索引各自使用不同的緩衝區)

    需要注意的是在Renderer的析構函數中要調用glDeleteBuffers來釋放它的資源,並使它的標識可以其他緩衝區對象使用。

上一篇中介紹的幾種渲染命令中的QUAD_COMMAND(這裏把它稱作四邊形繪製)命令回調用drawBatchedQuads調用繪製函數,處理這個邏輯的命令是這樣的:

if(commandType == RenderCommand::Type::QUAD_COMMAND)
{
    auto cmd = static_cast<QuadCommand*>(command);
    CCASSERT(nullptr!= cmd, "Illegal command for RenderCommand Taged as QUAD_COMMAND");
                    
    //如果Quad數據量超過VBO的大小,那麼調用繪製,將緩存的命令全部繪製
    if(_numQuads + cmd->getQuadCount() > VBO_SIZE)
    {
        CCASSERT(cmd->getQuadCount()>= 0 && cmd->getQuadCount() < VBO_SIZE, "VBO is not big enough for quad data, please break the quad data down or use customized render command");
        drawBatchedQuads();
    }
    //將命令緩存起來,先不調用繪製
    _batchedQuadCommands.push_back(cmd);
    memcpy(_quads + _numQuads, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
    //轉換成世界座標
    convertToWorldCoordinates(_quads + _numQuads, cmd->getQuadCount(), cmd->getModelView());
    //記錄下四邊形數量
    _numQuads += cmd->getQuadCount();
}

       

void Renderer::flush()
{
    //繪製
    drawBatchedQuads();
    //清空
    _lastMaterialID = 0;
}

這個處理主要是把命令存入_batchedQuadCommands中,如果如果Quad數據量超過VBO的大小,那麼調用繪製,將緩存的命令全部繪製

如果一直沒有超過VBO的大小,drawBatchedQuads繪製函數將在flush被調用時調用


如有錯誤,歡迎指出

下一篇介紹圖形渲染和批處理


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章