1. 程式人生 > >Cocos2d-x 渲染器Renderer與6種RenderCommand詳解

Cocos2d-x 渲染器Renderer與6種RenderCommand詳解

Cocos2d-x 渲染器Renderer

宣告:本文使用的是cocos2d-x-3.17的程式碼

在《Cocos場景遍歷與渲染》中已講解了Cocos2d 在渲染時會先遍歷場景,遍歷時會生成渲染命令,渲染器再處理渲染命令繪製出圖形。這篇文章主要是講解渲染器如何處理渲染命令。

渲染佇列RenderQueue

RenderQueue

遍歷場景時會生成渲染命令,生成的渲染命令儲存在渲染佇列中。渲染佇列RenderQueue會將命令分層5類儲存,分別是:GLOBALZ_NEG、OPAQUE_3D、TRANSPARENT_3D、GLOBALZ_ZERO、GLOBALZ_POS。這5種命令儲存的是:
GLOBALZ_NEG: globalZ< 0的命令
OPAQUE_3D:globalZ=0的不透明3D命令
TRANSPARENT_3D:globalZ=0的3D透明命令
GLOBALZ_ZERO:globalZ=0的2D命令
GLOBALZ_POS:globalZ>0的命令
其中globalZ在生成命令時使用的是節點的Node::getGlobalZOrder賦值,也就是節點的_globalZOrder。globalZ=0的命令分成了三種首先按3D、2D進行分類,然後3D還分成是否透明。這樣分類的原因是:2D、3D在顯示畫面先後順序時,2D一般由繪製的先後順序決定,而3D則會使用深度快取;3D 分成是否透明是因為透明物體顯示跟繪製命令的先後順序有關(具體參考《OpenGL程式設計指南(第八版)》11.4.1順序無關透明),所以3D透明佇列再繪製前需要按照繪製物體的深度進行排序。

命令排序:GLOBALZ_NEG、GLOBALZ_POS、TRANSPARENT_3D命令在繪製前會進行排序,GLOBALZ_NEG、GLOBALZ_POS會根據globalZ的值從小到大排序,TRANSPARENT_3D會根據繪製物體的深度(離攝像機的遠近)進行排序。

RenderGroups

Renderer類種並不是只有一個RenderQueue,而有一個RenderQueue的陣列_renderGroups,之所以這樣是因為渲染有時某些命令需要單獨設定一些狀態,例如使用模板快取實現“鏤空”繪製等,這時就需要一個單獨的RenderQueue去建立一個渲染分支實現,以防止影響全域性繪製。

渲染都是從_renderGroups[0]開始繪製,如果不使用渲染分支需要使用也就只會用到_renderGroups[0]一個RenderQueue。如果想使用其他的RenderQueue建立渲染分支,需要使用GroupCommand渲染命令。

處理渲染命令

處理步驟如下:

  1. 對_renderGroups的每個RenderQueue中的命令進行排序
  2. 使用函式visitRenderQueue訪問_renderGroups[0]中的命令,訪問順序為GLOBALZ_NEG、OPAQUE_3D、TRANSPARENT_3D、GLOBALZ_ZERO、GLOBALZ_POS。訪問每個渲染命令陣列訪問時會先設定好OpenGL的狀態,然後再用函式processRenderCommand處理渲染命令。
  3. processRenderCommand處理渲染,按渲染命令型別對麼每個命令進行渲染

渲染命令

一共有6種渲染命令,每種命令型別對應一個類,這些類都繼承至RenderCommand類,對應關係以及作用如下:
TRIANGLES_COMMAND:TrianglesCommand,渲染三角形,可以合併命令減少OpenGL的呼叫提高渲染效率
MESH_COMMAND:MeshCommand,渲染3D
GROUP_COMMAND:GroupCommand,建立渲染分支,使用_renderGroups[0]之外的RenderQueue
CUSTOM_COMMAND:CustomCommand,自定義渲染命令
BATCH_COMMAND:BatchCommand,同時渲染多個使用同一紋理的圖形,提高渲染效率
PRIMITIVE_COMMAND:PrimitiveCommand,渲染自定義圖元。

接下來由易到難對每種命令單獨講解,並用Node類實現相應的命令,因為渲染跟OpenGL緊密相關,這一塊需要OpenGL的知識才能完全理解。

CustomCommand

CustomCommand是所有命令中最簡單的一個,也是最靈活的一個,繪製的內容和方式完全交由我們自己決定。LayerColor、DrawNode、Skybox等待都是使用CustomCommand命令進行繪製的。

CustomCommand有一個std::function<void()> func變數,當Renderer處理CustomCommand時只是簡單的呼叫func函式,以下是Renderer中的程式碼:

    else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)

    {

        flush();

        auto cmd = static_cast<CustomCommand*>(command);

        CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_CUSTOM_COMMAND");

        cmd->execute();

    }

void CustomCommand::execute()

{

    if(func)

    {

        func();

    }

}

所以使用CustomCommand需要我們自己寫好繪製函式,賦值給CustomCommand.func。例,完整程式碼參考(CustomScene.cpp/CustomScene.h):

void CustomNode::draw(cocos2d::Renderer* renderer, const cocos2d::Mat4& transform, uint32_t flags)

{

    _customCommand.init(_globalZOrder);

    _customCommand.func = CC_CALLBACK_0(CustomNode::onDraw, this, transform);

    renderer->addCommand(&_customCommand);

}

void CustomNode::onDraw(const cocos2d::Mat4& transform)

{

    auto glProgramState = getGLProgramState();

    glProgramState->setUniformVec4("Color", _color);

    glProgramState->apply(transform);

    GL::bindVAO(_buffersVAO); //VAO中包含了矩形資料

    glDrawArrays(GL_TRIANGLES, 0, 6);//繪製一個顏色為_color的矩形

    GL::bindVAO(0);

    CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, 6);

}

PrimitiveCommand

Primitive圖元,指的是OpenGL圖元

PrimitiveCommand繪製主要由類Primitive實現,Renderer處理時,也只是執行Primitive::draw函式繪製。TMXLayer瓦片地圖類使用了PrimitiveCommand進行繪製。以下是Renderer中處理PrimitiveCommand的程式碼:

else if(RenderCommand::Type::PRIMITIVE_COMMAND == commandType)

{

    flush();

    auto cmd = static_cast<PrimitiveCommand*>(command);

    CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_PRIMITIVE_COMMAND");

    cmd->execute();

}

void PrimitiveCommand::execute() const

{

    //Set texture

    GL::bindTexture2D(_textureID);

    //set blend mode

    GL::blendFunc(_blendType.src, _blendType.dst);

   _glProgramState->apply(_mv);

    _primitive->draw();

    CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1,_primitive->getCount());

}

Primitive類實現了OpenGL圖元的繪製,繪製步驟也和OpenGL非常相似。

OpenGL中繪製圖形可以使用glDrawArrays繪製GL_ARRAY_BUFFER中的資料,也可以通過函式glDrawElements使用索引繪製,Primitive類同樣支援這兩種繪製方式。

Primitive類繪製步驟大致如下:

  1. 建立頂點資料快取
  2. 設定好頂點快取資料屬性格式
  3. 建立索引快取,如果不用索引繪製,去掉此步驟
  4. 使用頂點快取和索引快取建立Primitive類,並設定圖元型別
  5. 通過Primitive::draw函式繪製

建立頂點快取

VertexBuffer類實現了頂點快取的功能,可以直接使用該類建立頂點快取,並設定頂點快取資料。

設定頂點快取資料屬性

VertexStreamAttribute類為一個頂點屬性,對應頂點著色器中的一個輸入。每個VertexStreamAttribute物件對應一個VertexBuffer物件,繪製的時候會有多個頂點屬性,類VertexData儲存一組繪製的VertexStreamAttribute物件,VertexData類也就繪製是的頂點資料。可以建立VertexStreamAttribute物件並通過函式VertexData::setStream設定給VertexData物件。

索引快取

索引快取可以通過IndexBuffer類建立並設定索引資料

建立並初始化Primitive類

    V3F_C4B_T2F data[] = {

        { { 0,    0,0 },{ 255,  0,  0,255 },{ 0,1 } },

        { { 200,  0,0 },{ 0,  255,255,255 },{ 1,1 } },

        { { 200,200,0 },{ 255,255,  0,255 },{ 1,0 } },

        { { 0,  200,0 },{ 255,255,255,255 },{ 0,0 } },

    };

    uint16_t indices[] = { 0,1,2,  2,0,3 };

    static const int TOTAL_VERTS = sizeof(data) / sizeof(data[0]);

    static const int TOTAL_INDICES = TOTAL_VERTS * 6 / 4;

    //建立頂點快取並設定資料

    auto vertexBuffer = VertexBuffer::create(sizeof(V3F_C4B_T2F), TOTAL_VERTS);

    vertexBuffer->updateVertices(data, TOTAL_VERTS, 0);

    //設定頂點屬性資料

    auto vertsData = VertexData::create();

    vertsData->setStream(vertexBuffer, VertexStreamAttribute(0, GLProgram::VERTEX_ATTRIB_POSITION, GL_FLOAT, 3));

    vertsData->setStream(vertexBuffer, VertexStreamAttribute(offsetof(V3F_C4B_T2F, colors), GLProgram::VERTEX_ATTRIB_COLOR, GL_UNSIGNED_BYTE, 4, true));

    vertsData->setStream(vertexBuffer, VertexStreamAttribute(offsetof(V3F_C4B_T2F, texCoords), GLProgram::VERTEX_ATTRIB_TEX_COORD, GL_FLOAT, 2));

    //設定索引快取

    auto indexBuffer = IndexBuffer::create(IndexBuffer::IndexType::INDEX_TYPE_SHORT_16, TOTAL_INDICES);

    indexBuffer->updateIndices(indices, TOTAL_INDICES, 0);

    _primitive = Primitive::create(vertsData, indexBuffer, GL_TRIANGLES); //建立圖元類

    _primitive->setCount(TOTAL_INDICES);

    _primitive->setStart(0);

建立PrimitiveCommand

    auto glProgramState = getGLProgramState();

    glProgramState->setUniformInt("HasTex", _texture!=nullptr);

    GLuint tex = (_texture == nullptr) ? 0 : _texture->getName();

    _primitiveCommand.init(_globalZOrder, tex, glProgramState,

        BlendFunc::ALPHA_NON_PREMULTIPLIED,

        _primitive, transform, flags);

    renderer->addCommand(&_primitiveCommand);

BatchCommand

BatchCommand可以同時繪製同一紋理的多個小圖,用於2D繪製,類SpriteBatchNode和類ParticleBatchNode使用了BatchCommand減少OpenGL的呼叫。

BatchCommand繪製主要由類TextureAtlas實現,TextureAtlas::drawQuads可以一次繪製多個使用同一紋理的矩形,Renderer處理BatchCommand也只是執行TextureAtlas::drawQuads函式,以下是Renderer中的程式碼:

else if(RenderCommand::Type::BATCH_COMMAND == commandType)

{

    flush();

    auto cmd = static_cast<BatchCommand*>(command);

    CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_BATCH_COMMAND");

    cmd->execute();

}

void BatchCommand::execute()

{

    // Set material

    _shader->use();

    _shader->setUniformsForBuiltins(_mv);

    GL::bindTexture2D(_textureID);

    GL::blendFunc(_blendType.src, _blendType.dst);

    // Draw

    _textureAtlas->drawQuads();

}

類TextureAtlas的使用很簡單,只需要使用一個紋理初始化,然後設定好要繪製的矩形,TextureAtlas::drawQuads就可以繪製。例,完整程式碼參考(BatchScene.cpp/ BatchScene.h):

建立並初始化TextureAtlas物件

    _textureAtlas =::createWithTexture(tex, 30);

    float w = tex->getPixelsWide()*2;

    float h = tex->getPixelsHigh()*2;

    cocos2d::V3F_C4B_T2F_Quad quad = {

        { { 0,      h, 0 },{ 255, 255, 255, 255 },{ 0,0 } },

        { { 0,      0, 0 },{ 255, 255, 255, 255 },{ 0,1 } },

        { { w*0.5f,  h, 0 },{ 255, 255, 255, 255 },{ 0.5,0 } },

        { { w*0.5f,  0, 0 },{ 255, 255, 255, 255 },{ 0.5,1 } },

    };

    cocos2d::V3F_C4B_T2F_Quad quad2 = {

        { { w*0.5f +200,      h, 0 },{ 255, 255, 255, 255 },{ 0.5,1 } },

        { { w*0.5f + 200,      0, 0 },{ 255, 255, 255, 255 },{ 0.5,0 } },

        { { w + 200,  h, 0 },{ 255, 255, 255, 255 },{ 1,1 } },

        { { w + 200,  0, 0 },{ 255, 255, 255, 255 },{ 1,0 } },

    };

    _textureAtlas->insertQuad(&quad, 0);

    _textureAtlas->insertQuad(&quad2, 1);

    _textureAtlas->retain();

建立BatchCommand

    if (_textureAtlas->getTotalQuads() == 0)

    {

        return;

    }

    _batchCommand.init(_globalZOrder,  getGLProgram(),

        BlendFunc::ALPHA_NON_PREMULTIPLIED,

         _textureAtlas, transform, flags);

    renderer->addCommand(&_batchCommand);

GroupCommand

GroupCommand本身並不繪製任何東西,GroupCommand是用於建立渲染分支,使得某些特殊的繪製可以單獨設定繪製狀態,不影響主渲染分支。類ClippingNode、RenderTexture、NodeGrid等待使用了GroupCommand。

Renderer類中的std::vector<RenderQueue> _renderGroups陣列支援多個渲染佇列,_renderGroups[0]是主渲染佇列,其他為渲染分支。

GroupCommand有一個變數_renderQueueID記錄的就是渲染分支佇列在_renderGroups中的索引。Renderer處理GroupCommand也很簡單,只需要用函式Renderer::visitRenderQueue訪問 _renderGroups[_renderQueueID]渲染佇列。以下是Renderer中處理GroupCommand的程式碼:

else if(RenderCommand::Type::GROUP_COMMAND == commandType)

{

    flush();

    int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();

    CCGL_DEBUG_PUSH_GROUP_MARKER("RENDERER_GROUP_COMMAND");

    visitRenderQueue(_renderGroups[renderQueueID]);

    CCGL_DEBUG_POP_GROUP_MARKER();

}

GroupCommandManager

GroupCommandManager的作用是快取已有的渲染佇列,減少渲染佇列的重複建立。建立渲染分支時,GroupCommandManager會先檢查會先檢查是否有沒有使用的渲染佇列,如果有則不建立直接放回已有的,沒有則建立。

例,以下建立了一個渲染佇列使用剪下矩形glScissor限制繪製的矩形,是的繪製限制在矩形內,矩形外的被剪下掉,完整程式碼參考(GroupScene.cpp/ GroupScene.h):