1. 程式人生 > >Cocos2d-x3.2與OpenGL渲染總結(一)Cocos2d-x3.2的渲染流程

Cocos2d-x3.2與OpenGL渲染總結(一)Cocos2d-x3.2的渲染流程

    最近幾天,我都在學習如何在Cocos2d-x3.2中使用OpenGL來實現對圖形的渲染。在網上也看到了很多好的文章,我在它們的基礎上做了這次的我個人認為比較完整的總結。當你瞭解了Cocos2d-x3.2中對圖形渲染的流程,你就會覺得要學會寫自己的shader才是最重要的。

    第一,渲染流程從2.x到3.x的變化。

    在2.x中,渲染過程是通過遞迴渲染樹(Rendering tree)這種圖關係來渲染關係圖。遞迴呼叫visit()函式,並且在visit()函式中呼叫該節點的draw函式渲染各個節點,此時draw函式的作用是直接呼叫OpenGL程式碼進行圖形的渲染。由於visit()和draw函式都是虛擬函式,所以要注意執行時的多型。那麼我們來看看2.x版本中CCSprite的draw函式,如程式碼1。

    程式碼1:

//這是cocos2d-2.0-x-2.0.4版本的CCSprite的draw函式
void CCSprite::draw(void)
{
    CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
    CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
    CC_NODE_DRAW_SETUP();
    ccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );
    if (m_pobTexture != NULL)
    {
        ccGLBindTexture2D( m_pobTexture->getName() );
    }
    else
    {
        ccGLBindTexture2D(0);
    }  
    //
    // Attributes
    //
    ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );
#define kQuadSize sizeof(m_sQuad.bl)
    long offset = (long)&m_sQuad;
    // vertex
    int diff = offsetof( ccV3F_C4B_T2F, vertices);
    glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));
    // texCoods
    diff = offsetof( ccV3F_C4B_T2F, texCoords);
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));
    // color
    diff = offsetof( ccV3F_C4B_T2F, colors);
    glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    CHECK_GL_ERROR_DEBUG();
#if CC_SPRITE_DEBUG_DRAW == 1
    // draw bounding box
    CCPoint vertices[4]={
        ccp(m_sQuad.tl.vertices.x,m_sQuad.tl.vertices.y),
        ccp(m_sQuad.bl.vertices.x,m_sQuad.bl.vertices.y),
        ccp(m_sQuad.br.vertices.x,m_sQuad.br.vertices.y),
        ccp(m_sQuad.tr.vertices.x,m_sQuad.tr.vertices.y),
    };
    ccDrawPoly(vertices, 4, true);
#elif CC_SPRITE_DEBUG_DRAW == 2
    // draw texture box
    CCSize s = this->getTextureRect().size;
    CCPoint offsetPix = this->getOffsetPosition();
    CCPoint vertices[4] = {
        ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
        ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
    };
    ccDrawPoly(vertices, 4, true);
#endif // CC_SPRITE_DEBUG_DRAW

    CC_INCREMENT_GL_DRAWS(1);

    CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
}
     那麼我們也看看3.x中Sprite的draw函式,如程式碼2。

        程式碼2:

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // Don't do calculate the culling if the transform was not updated
    _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
    if(_insideBounds)
    {
        _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
        renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
        _customDebugDrawCommand.init(_globalZOrder);
        _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
        renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
    }
}

    從程式碼1和程式碼2的對比中,我們很容易就發現2.x版本中的draw函式直接呼叫OpengGL程式碼進行圖形渲染,而3.x版本中draw的作用是把RenderCommand新增到CommandQueue中,至於這樣做的好處是,實際的渲染API進入其中一個與顯示卡直接交流的有獨立執行緒的RenderQueue。

    從Cocos2d-x3.0開始,Cocos2d-x引入了新的渲染流程,它不像2.x版本直接在每一個node中的draw函式中直接呼叫OpenGL程式碼進行圖形渲染,而是通過各種RenderCommand封裝起來,然後新增到一個CommandQueue佇列裡面去,而現在draw函式的作用就是在此函式中設定好相對應的RenderCommand引數,然後把此RenderCommand新增到CommandQueue中。最後在每一幀結束時呼叫renderer函式進行渲染,在renderer函式中會根據ID對RenderCommand進行排序,然後才進行渲染。

   下面我們來看看圖1、圖2,這兩個圖形象地表現了Cocos2d-x3.x下RenderCommand的封裝與傳遞與及RenderCommand的排序。

    圖1:

     

    圖2:

      

     上面所說的各個方面都有點零碎,下面就對渲染的整個流程來一個從頭到尾的梳理吧。下面是針對3.2版本的,對於2.x版本的梳理不做梳理,因為我用的是3.2版本。

   首先,我們Cocos2d-x的執行是通過Application::run()來開始的,如程式碼3,此程式碼目錄中在xx\cocos2d\cocos\platform\對應平臺的目錄下,這是與多平臺實現有關的類,關於如何實現多平臺的編譯,你可以參考《cocos2d-x3.2原始碼分析(一)類FileUtils--實現把資源放在Resources檔案目錄下達到多平臺的引用 》中我對平臺編譯的分析。以防篇幅過長,只截取了重要部分,如需詳解,可以直接檢視原始碼。

   程式碼3:

int Application::run()
{
  ...
  director->mainLoop();
  ...
 }
    從程式碼3中,它明顯的啟發著我們要繼續追尋Director::mainLoop()函式。在Director中mainLoop()為純函式,此子類DisplayLinkDirector才有其實現,如程式碼4。

   程式碼4:

void DisplayLinkDirector::mainLoop()
{   
    <span><span class="comment">//只有一種情況會呼叫到這裡來,就是導演類呼叫end函式</span><span>  </span></span>
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        <span><span class="comment">//清除導演類</span><span></span></span>
        purgeDirector();
    }
    else if (! _invalid)
    {   <span><span class="comment">//繪製</span><span> </span></span>
        drawScene(); 
        //清除當前記憶體池中物件,即池中每一個物件--_referenceCount
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}
   mainLoop是主執行緒呼叫的迴圈,其中drawScene()是繪製函式,接著我們繼續追尋它的程式碼,如程式碼5。

   程式碼5:

void Director::drawScene()
{
    <span><span class="comment">//計算間隔時間</span><span> </span></span>
    calculateDeltaTime();
    
    //忽略該幀如果時間間隔接近0
    if(_deltaTime < FLT_EPSILON)
    {
        return;
    }

    if (_openGLView)
    {
        _openGLView->pollInputEvents();
    }

    //tick before glClear: issue #533
    if (! _paused)
    {
        _scheduler->update(_deltaTime);
        _eventDispatcher->dispatchEvent(_eventAfterUpdate);
    }

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    /* to avoid flickr, nextScene MUST be here: after tick and before draw.
     XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
    if (_nextScene)
    {
        setNextScene();
    }

    pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

    // draw the scene
    if (_runningScene)
    {
        _runningScene->visit(_renderer, Mat4::IDENTITY, false);
        _eventDispatcher->dispatchEvent(_eventAfterVisit);
    }

    // draw the notifications node
    if (_notificationNode)
    {
        _notificationNode->visit(_renderer, Mat4::IDENTITY, false);
    }

    if (_displayStats)
    {
        showStats();
    }

    _renderer->render();
    _eventDispatcher->dispatchEvent(_eventAfterDraw);

    popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

    _totalFrames++;

    // swap buffers
    if (_openGLView)
    {
        _openGLView->swapBuffers();
    }

    if (_displayStats)
    {
        calculateMPF();
    }
}
      從程式碼5中,我們看見visit()和render()函式的呼叫。其中visit()函式會呼叫draw()函式來向RenderQueue中新增RenderCommand,那麼就繼續追尋visit()的程式碼,如程式碼6。

     程式碼6:   

void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
    // quick return if not visible. children won't be drawn.
    if (!_visible)
    {
        return;
    }

    uint32_t flags = processParentFlags(parentTransform, parentFlags);

    // IMPORTANT:
    // To ease the migration to v3.0, we still support the Mat4 stack,
    // but it is deprecated and your code should not rely on it
    Director* director = Director::getInstance();
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);
    int i = 0;
    if(!_children.empty())
    {
        sortAllChildren();
        // draw children zOrder < 0
        for( ; i < _children.size(); i++ )
        {
            auto node = _children.at(i);

            if ( node && node->_localZOrder < 0 )
                node->visit(renderer, _modelViewTransform, flags);
            else
                break;
        }
        // self draw
        this->draw(renderer, _modelViewTransform, flags);

        for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
            (*it)->visit(renderer, _modelViewTransform, flags);
    }
    else
    {
        this->draw(renderer, _modelViewTransform, flags);
    }

    director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    
    // FIX ME: Why need to set _orderOfArrival to 0??
    // Please refer to https://github.com/cocos2d/cocos2d-x/pull/6920
    // reset for next frame
    // _orderOfArrival = 0;
}
   從程式碼6中,我們可以看到“ auto node = _children.at(i);和node->visit(renderer, _modelViewTransform, flags);”,這段程式碼的意思是先獲取子節點,然後遞迴呼叫節點的visit()函式,到了沒有子節點的節點,開始呼叫draw()函式。那麼我們看看draw()函式程式碼,如程式碼7。

   程式碼7:

void Node::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags)
{
}
   好吧,從程式碼7中,我們看到Node的draw什麼都沒有做,是我們找錯地方?原來draw()是虛擬函式,所以它執行時執行的是該位元組類的draw()函式。確實是我們找錯地方了。那麼我們分別看DrawNode::draw()、Sprite::draw()。

   程式碼8:

void DrawNode::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    _customCommand.init(_globalZOrder);
    _customCommand.func = CC_CALLBACK_0(DrawNode::onDraw, this, transform, flags);
    renderer->addCommand(&_customCommand);
}

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // Don't do calculate the culling if the transform was not updated
    _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;

    if(_insideBounds)
    {
        _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
        renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
        _customDebugDrawCommand.init(_globalZOrder);
        _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
        renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
    }
}
     從程式碼8中,我們可以看到在draw()函式向RenderQueue中新增RenderCommand,當然有的類的draw()不是向RenderQueue中新增RenderCommand,而是直接使用OpenGL的API直接進行渲染,或者做一些其他的事情。

    那麼當draw()都遞迴呼叫完了,我們來看看最後進行渲染的Renderer::render() 函式,如程式碼9。

    程式碼9:

void Renderer::render()
{
    //Uncomment this once everything is rendered by new renderer
    //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //TODO setup camera or MVP
    _isRendering = true;
    
    if (_glViewAssigned)
    {
        // cleanup
        _drawnBatches = _drawnVertices = 0;

        //Process render commands
        //1. Sort render commands based on ID
        for (auto &renderqueue : _renderGroups)
        {
            renderqueue.sort();
        }
        visitRenderQueue(_renderGroups[0]);
        flush();
    }
    clean();
    _isRendering = false;
}
   從程式碼9中,我們看到“renderqueue.sort()",這是之前所說的對命令先排序,然後才進行渲染,“visitRenderQueue( _renderGroups[0])”就是來進行渲染的。那麼我們接著看看void Renderer::visitRenderQueue(const RenderQueue& queue)的程式碼,如程式碼10。

   程式碼10:

void Renderer::visitRenderQueue(const RenderQueue& queue)
{
    ssize_t size = queue.size();
    
    for (ssize_t index = 0; index < size; ++index)
    {
        auto command = queue[index];
        auto commandType = command->getType();
        if(RenderCommand::Type::QUAD_COMMAND == commandType)
        {
            flush3D();
            auto cmd = static_cast<QuadCommand*>(command);
            //Batch quads
            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");
                
                //Draw batched quads if VBO is full
                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();

        }
        else if(RenderCommand::Type::GROUP_COMMAND == commandType)
        {
            flush();
            int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
            visitRenderQueue(_renderGroups[renderQueueID]);
        }
        else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
        {
            flush();
            auto cmd = static_cast<CustomCommand*>(command);
            cmd->execute();
        }
        else if(RenderCommand::Type::BATCH_COMMAND == commandType)
        {
            flush();
            auto cmd = static_cast<BatchCommand*>(command);
            cmd->execute();
        }
        else if (RenderCommand::Type::MESH_COMMAND == commandType)
        {
            flush2D();
            auto cmd = static_cast<MeshCommand*>(command);
            if (_lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID())
            {
                flush3D();
                cmd->preBatchDraw();
                cmd->batchDraw();
                _lastBatchedMeshCommand = cmd;
            }
            else
            {
                cmd->batchDraw();
            }
        }
        else
        {
            CCLOGERROR("Unknown commands in renderQueue");
        }
    }
}
    從程式碼10中,我們看到RenderCommand型別有QUAD_COMMAND,CUSTOM_COMMAND,BATCH_COMMAND,

GROUP_COMMAND,MESH_COMMAND五種,這些型別的講解在下一節。

   從程式碼10中,好像沒有與OpenGL相關的程式碼,有點囧。其實這OpenGL的API呼叫是在Renderer::drawBatched

Quads()、BatchCommand::execute()中。在程式碼10中,我們也看到在QUAD_COMMAND型別中呼叫了drawBatchedQuads(),如程式碼11。在CUSTOM_COMMAND中呼叫了CustomCommand::execute(),如程式碼12。在BATCH_COMMAND中呼叫了BatchCommand::execute(),如程式碼13。在MESH_COMMAND型別中呼叫了MeshCommand::preBatchDraw()和MeshCommand::batchDraw()。至於GROUP_COMMAND型別,就遞迴它組裡的成員。

    程式碼11:

void Renderer::drawBatchedQuads()
{
    //TODO we can improve the draw performance by insert material switching command before hand.

    int quadsToDraw = 0;
    int startQuad = 0;

    //Upload buffer to VBO
    if(_numQuads <= 0 || _batchedQuadCommands.empty())
    {
        return;
    }

    if (Configuration::getInstance()->supportsShareableVAO())
    {
        //Set VBO data
        glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);

        // option 1: subdata
//        glBufferSubData(GL_ARRAY_BUFFER, sizeof(_quads[0])*start, sizeof(_quads[0]) * n , &_quads[start] );

        // option 2: data
//        glBufferData(GL_ARRAY_BUFFER, sizeof(quads_[0]) * (n-start), &quads_[start], GL_DYNAMIC_DRAW);

        // option 3: orphaning + glMapBuffer
        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);

        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);

        // vertices
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, vertices));

        // colors
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, colors));

        // tex coords
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, texCoords));

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
    }

    //Start drawing verties in batch
    for(const auto& cmd : _batchedQuadCommands)
    {
        auto newMaterialID = cmd->getMaterialID();
        if(_lastMaterialID != newMaterialID || newMaterialID == QuadCommand::MATERIAL_ID_DO_NOT_BATCH)
        {
            //Draw quads
            if(quadsToDraw > 0)
            {
                glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );
                _drawnBatches++;
                _drawnVertices += quadsToDraw*6;

                startQuad += quadsToDraw;
                quadsToDraw = 0;
            }

            //Use new material
            cmd->useMaterial();
            _lastMaterialID = newMaterialID;
        }

        quadsToDraw += cmd->getQuadCount();
    }

    //Draw any remaining quad
    if(quadsToDraw > 0)
    {
        glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );
        _drawnBatches++;
        _drawnVertices += quadsToDraw*6;
    }

    if (Configuration::getInstance()->supportsShareableVAO())
    {
        //Unbind VAO
        GL::bindVAO(0);
    }
    else
    {
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    _batchedQuadCommands.clear();
    _numQuads = 0;

    程式碼12:

void CustomCommand::execute()
{
    if(func)
    {
        func();
    }
}

    程式碼13:  

void BatchCommand::execute()
{
    // Set material
    _shader->use();
    _shader->setUniformsForBuiltins(_mv);
    GL::bindTexture2D(_textureID);
    GL::blendFunc(_blendType.src, _blendType.dst);

    // Draw
    _textureAtlas->drawQuads();
}

   從程式碼11、程式碼12、程式碼13中,我們都看到了這些函式中對OpenGl的API呼叫來進行渲染。其中特別提醒一下,在CustomCommand::execute()中直接呼叫的函式是我們設定的回撥函式。在這個函式中,我們可以自己使用OpenGL的API進行圖形的渲染。這就在第三節中講如何在Cocos2d-x中自己設定渲染功能中向_customCommand新增的函式。在這裡我先給出簡便的方式,_customCommand.func = CC_CALLBACK_0(HelloWorld::onDraw, this)。

   以上就是把一個完整的渲染的流程都梳理了一片,下面我給出了流程圖,如圖3。

   圖3:

  

   第二,RenderCommand的型別。

    這裡的型別講解主要參考這篇文章中關於RenderComman的型別講解。

QUAD_COMMAND:QuadCommand類繪製精靈等。
   所有繪製圖片的命令都會呼叫到這裡,處理這個型別命令的程式碼就是繪製貼圖的openGL程式碼,下一篇文章會詳細介紹這部分程式碼。
  CUSTOM_COMMAND:CustomCommand類自定義繪製,自己定義繪製函式,在呼叫繪製時只需呼叫已經傳進來的回撥函式就可以,裁剪節點,繪製圖形節點都採用這個繪製,把繪製函式定義在自己的類裡。這種型別的繪製命令不會在處理命令的時候呼叫任何一句openGL程式碼,而是呼叫你寫好並設定給func的繪製函式,後續文章會介紹引擎中的所有自定義繪製,並自己實現一個自定義的繪製。
  BATCH_COMMAND:BatchCommand類批處理繪製,批處理精靈和粒子
  其實它類似於自定義繪製,也不會再render函式中出現任何一句openGL函式,它呼叫一個固定的函式,這個函式會在下一篇文章中介紹。
  GROUP_COMMAND:GroupCommand類繪製組,一個節點包括兩個以上繪製命令的時候,把這個繪製命令儲存到另外一個_renderGroups中的元素中,並把這個元素的指標作為一個節點儲存到_renderGroups[0]中。

   第三,如何在Cocos2d-x中自己設定渲染功能。

   1.第一種方法針對的是整個圖層的渲染。

    重寫visit()函式,並且在visit()函式中直接向CommandQueue新增CustomCommand,設定好回撥函式,這個比較直接,如程式碼14,程式碼14是子龍山人《基於Cocos2d-x學習OpenGL ES 2.0》第一篇中的部分程式碼。或者重寫draw()函式,並且在draw()函式中向CommandQueue新增CustomCommand,設定好回撥函式,這個就比較按照正規的流程走。

    程式碼14:

void HelloWorld::visit(cocos2d::Renderer *renderer, const Mat4 &transform, bool transformUpdated)
{
    Layer::draw(renderer, transform, transformUpdated);
    
    //send custom command to tell the renderer to call opengl commands
    _customCommand.init(_globalZOrder);
    _customCommand.func = CC_CALLBACK_0(HelloWorld::onDraw, this);
    renderer->addCommand(&_customCommand);
    
    
}
void HelloWorld::onDraw()
{
    //question1: why the triangle goes to the up side
    //如果使用對等矩陣,則三角形繪製會在最前面
    Director::getInstance()->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    Director::getInstance()->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    Director::getInstance()->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    Director::getInstance()->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
   
    auto glProgram = getGLProgram();
    
    glProgram->use();
    
    //set uniform values, the order of the line is very important
    glProgram->setUniformsForBuiltins();
    auto size = Director::getInstance()->getWinSize();
    
    //use vao
    glBindVertexArray(vao);
    
    GLuint uColorLocation = glGetUniformLocation(glProgram->getProgram(), "u_color");
    
    float uColor[] = {1.0, 1.0, 1.0, 1.0};
    glUniform4fv(uColorLocation,1, uColor);   
//  glDrawArrays(GL_TRIANGLES, 0, 6);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE,(GLvoid*)0);  
    glBindVertexArray(0); 
    CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, 6);
    CHECK_GL_ERROR_DEBUG();  
    Director::getInstance()->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);   
    Director::getInstance()->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

}

         從程式碼14中,我們看到重寫visit()函式,在visit()函式中直接向RenderQueue新增RenderCommand,即“renderer->addCommand(&_customCommand);”,由於此RenderCommand型別為CustomCommand,所以要新增處理圖形渲染的回撥函式,即“_customCommand.func = CC_CALLBACK_0(HelloWorld::onDraw, this);”,這行程式碼就是添加回調函式的,onDraw()函式中呼叫OpengGL的API渲染圖形。關於func是如何被呼叫,可以參考上面的程式碼12上下文的分析。

     2.第二種方法針對個別精靈。

     有時候,我們只要對個別精靈進行特效的處理,這個精靈需要使用我們自己編寫的Shader,而圖層其他的元素按預設處理就行了。這時候就需要第二種方法了。設定好Shader,向精靈新增Shader,最後在重寫draw函式,在draw函式中進行特效的處理,如程式碼15,程式碼15是《捕魚達人3》教程第二節的程式碼。

     程式碼15:

bool FishLayer::init()
{
         ...省略了不相關的程式碼。
	// 將vsh與fsh裝配成一個完整的Shader檔案。
    auto glprogram = GLProgram::createWithFilenames("UVAnimation.vsh", "UVAnimation.fsh");
	// 由Shader檔案建立這個Shader
    auto glprogramstate = GLProgramState::getOrCreateWithGLProgram(glprogram);
	// 給精靈設定所用的Shader
    m_Sprite->setGLProgramState(glprogramstate);

	//建立海龜所用的貼圖。
    auto textrue1 = Director::getInstance()->getTextureCache()->addImage("tortoise.png");
	//將貼圖設定給Shader中的變數值u_texture1
    glprogramstate->setUniformTexture("u_texture1", textrue1);
    //建立波光貼圖。
    auto textrue2 = Director::getInstance()->getTextureCache()->addImage("caustics.png");
    //將貼圖設定給Shader中的變數值u_lightTexture
    glprogramstate->setUniformTexture("u_lightTexture", textrue2);

    //注意,對於波光貼圖,我們希望它在進行UV動畫時能產生四方連續效果,必須設定它的紋理UV定址方式為GL_REPEAT。
    Texture2D::TexParams	tRepeatParams;
    tRepeatParams.magFilter = GL_LINEAR_MIPMAP_LINEAR;
    tRepeatParams.minFilter = GL_LINEAR;
    tRepeatParams.wrapS = GL_REPEAT;
    tRepeatParams.wrapT = GL_REPEAT;
    textrue2->setTexParameters(tRepeatParams);
    //在這裡,我們設定一個波光的顏色,這裡設定為白色。
    Vec4  tLightColor(1.0,1.0,1.0,1.0);
    glprogramstate->setUniformVec4("v_LightColor",tLightColor);
    //下面這一段,是為了將我們自定義的Shader與我們的模型頂點組織方式進行匹配。模型的頂點資料一般包括位置,法線,色彩,紋理,以及骨骼繫結資訊。而Shader需要將內部相應的頂點屬性通道與模型相應的頂點屬性資料進行繫結才能正確顯示出頂點。
    long offset = 0;
    auto attributeCount = m_Sprite->getMesh()->getMeshVertexAttribCount();
    for (auto k = 0; k < attributeCount; k++) {
        auto meshattribute = m_Sprite->getMesh()->getMeshVertexAttribute(k);
        glprogramstate->setVertexAttribPointer(s_attributeNames[meshattribute.vertexAttrib],
                                             meshattribute.size,
                                             meshattribute.type,
                                             GL_FALSE,
                                             m_Sprite->getMesh()->getVertexSizeInBytes(),
                                             (GLvoid*)offset);
        offset += meshattribute.attribSizeBytes;
    }

	//uv滾動初始值設為0
	m_LightAni.x = m_LightAni.y = 0;
	return true;
}

void FishLayer::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags)
{
	if(m_Sprite)
	{
		//烏龜從右向左移動,移出屏幕後就回到最右邊
		auto s = Director::getInstance()->getWinSize();
		m_Sprite->setPositionX(m_Sprite->getPositionX()-1);
		if(m_Sprite->getPositionX() < -100)
		{
			m_Sprite->setPositionX(s.width + 10);
		}
		
		auto glprogramstate = m_Sprite->getGLProgramState();
		if(glprogramstate)
		{
			m_LightAni.x += 0.01;
			if(m_LightAni.x > 1.0)
			{
				m_LightAni.x-= 1.0;
			}
			m_LightAni.y += 0.01;
			if(m_LightAni.y > 1.0)
			{
				m_LightAni.y-= 1.0;
			}
			glprogramstate->setUniformVec2("v_animLight",m_LightAni);
		}
	}
	Node::draw(renderer,transform,flags);
}
    從程式碼15中,我們可以看到先使用OpengGL的API建立自己的Shader,然後再把m_sprite的Shader設定為自己的Shader即“m_Sprite->setGLProgramState(glprogramstate);,這是給精靈設定所用的Shader,這就是針對個別的精靈,而不是整個圖層。接著在draw()中,如果精靈已生成,每次呼叫draw()函式都改變Shader中引數,以達到特別的效果。
    以上都是我通過閱讀別人的程式碼總結的方法,不知道還有沒有其他的在Cocos2d-x中自己設定渲染功能的方法,如果有的話,請告訴我,直接在我的部落格留言就可以了。

   參考資料:

   1.http://cn.cocos2d-x.org/article/index?type=wiki&url=/doc/cocos-docs-master/manual/framework/native   /wiki/renderer/zh.md

    2.http://cocos2d-x.org/wiki/Cocos2d_v30_renderer_pipeline_roadmap

    3.http://cn.cocos2d-x.org/tutorial/show?id=1336

    4.http://blog.csdn.net/bill_man/article/details/35839499

    5.《Cocos2d-x高階開發教程》2.1.4節

    6.Cocos2d-x3.2和Cocos2d-x2.0.4原始碼