浅谈cocos2dx渲染方式

场景的渲染

浅谈cocos2dx渲染方式_第1张图片

Node:visit

其作用是遍历整个场景渲染树。

部分代码如下

if(!_children.empty())
{
    sortAllChildren();
    // draw children zOrder < 0
    for(auto size = _children.size(); i < size; ++i)
    {
        auto node = _children.at(i);

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

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

如果子节点不为空,那么就对子节点进行排序。排序算法如下:

static void sortNodes(cocos2d::Vector<_T*>& nodes)
{
    static_assert(std::is_base_of<Node, _T>::value, "Node::sortNodes: Only accept derived of Node!");
#if CC_64BITS
    std::sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
        return (n1->_localZOrderAndArrival < n2->_localZOrderAndArrival);
    });
#else
    std::stable_sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
        return n1->_localZOrder < n2->_localZOrder;
    });
#endif
}

我们对localZOrder 很熟悉,但是对localZOrderAndArrival就可能就会懵。实际上,localZOrderAndArrival在addChild的时候就会生成一个,在前面addChild的时候,生成的会小于后面addChild的。

排完序之后,对Node继续进行遍历,这里会优先遍历localZOrder小于0的子节点,然后调用visit函数递归遍历。

所以这里的遍历顺序就是,小于0的子节点 > 父节点本身 > 大于0的子节点

遍历完之后,调用draw函数。

Node的draw函数是空的。一般都是子类进行实现自己的draw函数。

Sprite::draw

举个简单的例子,Spirte的draw函数。

只看最重要的

_trianglesCommand.init(_globalZOrder,
                       _texture,
                       getGLProgramState(),
                       _blendFunc,
                       _polyInfo.triangles,
                       transform,
                       flags);

renderer->addCommand(&_trianglesCommand);

这是cocos2dx 3.x改变最大的地方,draw函数只进行RenderCommon的生成。将生成好的命令存储到render中。

Render:render()

还记得前面调用场景的visit函数之后,调用了render函数么?

它在 CCRender这个类中。

_isRendering = true;
    
if (_glViewAssigned)
{
    //Process render commands
    //1. Sort render commands based on ID
    for (auto &renderqueue : _renderGroups)
    {
        renderqueue.sort();
    }
    visitRenderQueue(_renderGroups[0]);
}
clean();
_isRendering = false;

_renderGroups中存储的就是需要发送给OpenGL进行渲染的指令集合。这里会先进行一次排序。(这个排序很关键,这也是决定了为啥有些不能合批的原因。)

void RenderQueue::sort()
{
    // Don't sort _queue0, it already comes sorted
    std::stable_sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
    std::stable_sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]), std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]), compareRenderCommand);
    std::stable_sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]), std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]), compareRenderCommand);
}

排序的方法很简单。

TRANSPARENT_3D是3D的,根据景深来,这里不谈。

主要是下面

static bool compareRenderCommand(RenderCommand* a, RenderCommand* b)
{
    return a->getGlobalOrder() < b->getGlobalOrder();
}

这里可以看得出,是根据globalZOrder来的。

globalZOrder是个好东西,也是个坏东西。他可以决定场景中的渲染先后顺序,也就决定了谁在前,谁在后。

可能有人会说,localZOrder不也是这样么?

localZOrder只是在同一个父节点上决定渲染先后顺序,它会受父节点的影响。

globalZOrder则是决定OpenGL的渲染先后顺序,也就是不管父节点是谁,它会凌驾于其他比他低的上面。

我们一般globalZOrder都是设置为0,那么设置为0,这里就不会进行排序,那么节点渲染的顺序就是之前加入RenderCommon的顺序,也就是之前对子节点排序的顺序。

Render:visitRenderQueue

遍历渲染队列。

怎么遍历的呢?

  1. RenderQueue::QUEUE_GROUP::GLOBALZ_NEG globalZOrder < 0
  2. RenderQueue::QUEUE_GROUP::OPAQUE_3D 3D不透明的对象
  3. RenderQueue::QUEUE_GROUP::TRANSPARENT_3D 3D对象透明的对象
  4. RenderQueue::QUEUE_GROUP::GLOBALZ_ZERO globalZOrder == 0
  5. RenderQueue::QUEUE_GROUP::GLOBALZ_POS globalZOrder > 0

Render:visitRenderQueue

根据上面的顺序,会依次进入processRenderCommand函数

在说这个函数之前,我们要了解cocos2dx的几种渲染命令。

  1. TRIANGLES_COMMAND:TrianglesCommand,渲染三角形,可以合并命令减少OpenGL的调用提高渲染效率
  2. MESH_COMMAND:MeshCommand,渲染3D
  3. GROUP_COMMAND:GroupCommand,创建渲染分支,使用_renderGroups[0]之外的RenderQueue。也是多个renderCOmmand的集合,里面的命令不参与全局排序。可用于子元素裁剪,绘制元素到纹理等。
  4. CUSTOM_COMMAND:CustomCommand,自定义渲染命令
  5. BATCH_COMMAND:BatchCommand,同时渲染多个使用同一纹理的图形,提高渲染效率
  6. PRIMITIVE_COMMAND:PrimitiveCommand,渲染自定义图元

TRIANGLES_COMMAND

其功能不是绘制三角形,比如我们的2D图片、Sprite类等都是用这个绘制。

这个命令有一个非常大的特点,也就是合批渲染。

合批渲染,本质上来说就是将符合要求的渲染命令合并成一个OpenGL Draw Call的调用。

每次渲染,都会将渲染命令发送给OpenGL进行渲染,每一次调用就会使得Draw Call +1。而Draw Call越高,画面掉帧越厉害。

if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
{
    // flush other queues
    flush3D();

    auto cmd = static_cast<TrianglesCommand*>(command);
    
    // flush own queue when buffer is full
    if(_filledVertex + cmd->getVertexCount() > VBO_SIZE || _filledIndex + cmd->getIndexCount() > INDEX_VBO_SIZE)
    {
        CCASSERT(cmd->getVertexCount()>= 0 && cmd->getVertexCount() < VBO_SIZE, "VBO for vertex is not big enough, please break the data down or use customized render command");
        CCASSERT(cmd->getIndexCount()>= 0 && cmd->getIndexCount() < INDEX_VBO_SIZE, "VBO for index is not big enough, please break the data down or use customized render command");
        drawBatchedTriangles();
    }
    
    // queue it
    _queuedTriangleCommands.push_back(cmd);
    _filledIndex += cmd->getIndexCount();
    _filledVertex += cmd->getVertexCount();
}

drawBatchedTriangles 函数有点多,先看主要的部分

 // in the same batch ?
if (batchable && (prevMaterialID == currentMaterialID || firstCommand))
{
    CC_ASSERT(firstCommand || _triBatchesToDraw[batchesTotal].cmd->getMaterialID() == cmd->getMaterialID() && "argh... error in logic");
    _triBatchesToDraw[batchesTotal].indicesToDraw += cmd->getIndexCount();
    _triBatchesToDraw[batchesTotal].cmd = cmd;
}
else
{
    // is this the first one?
    if (!firstCommand) {
        batchesTotal++;
        _triBatchesToDraw[batchesTotal].offset = _triBatchesToDraw[batchesTotal-1].offset + _triBatchesToDraw[batchesTotal-1].indicesToDraw;
    }

    _triBatchesToDraw[batchesTotal].cmd = cmd;
    _triBatchesToDraw[batchesTotal].indicesToDraw = (int) cmd->getIndexCount();

    // is this a single batch ? Prevent creating a batch group then
    if (!batchable)
        currentMaterialID = -1;
}

这里,就可以看的出,从队列中取出第一条指令,放入 _triBatchesToDraw 中。后续,会判断下一条的指令的ID是否和上一个相同,如果相同,那么就继续放入 _triBatchesToDraw中一个序列中。

/************** 3: Draw *************/
for (int i=0; i<batchesTotal; ++i)
{
    CC_ASSERT(_triBatchesToDraw[i].cmd && "Invalid batch");
    _triBatchesToDraw[i].cmd->useMaterial();
    glDrawElements(GL_TRIANGLES, (GLsizei) _triBatchesToDraw[i].indicesToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (_triBatchesToDraw[i].offset*sizeof(_indices[0])) );
    _drawnBatches++;
    _drawnVertices += _triBatchesToDraw[i].indicesToDraw;
}

这就是合批渲染的原理。

那么可以看得出,要合批渲染降低drawcall的条件是。

  1. 相邻的命令
  2. 有相同的ID

相邻的命令好理解,也就是localZOrder的顺序或者globalZOrder的顺序。

相同的ID呢?

void TrianglesCommand::generateMaterialID()
{
    // glProgramState is hashed because it contains:
    //  *  uniforms/values
    //  *  glProgram
    //
    // we safely can when the same glProgramState is being used then they share those states
    // if they don't have the same glProgramState, they might still have the same
    // uniforms/values and glProgram, but it would be too expensive to check the uniforms.
    struct {
        GLuint textureId;
        GLenum blendSrc;
        GLenum blendDst;
        void* glProgramState;
    } hashMe;

    hashMe.textureId = _textureID;
    hashMe.blendSrc = _blendType.src;
    hashMe.blendDst = _blendType.dst;
    hashMe.glProgramState = _glProgramState;
    _materialID = XXH32((const void*)&hashMe, sizeof(hashMe), 0);
}

该函数就是ID的生成函数,或者说获取函数。

从该函数可以得知

  1. 相同的纹理
  2. 相同的混合模式(blend)
  3. 相同的shader

所以,要想能合批渲染,那么就必须满足上面的条件。

MESH_COMMAND

MeshCommand用于3D网格绘制,类CCSprite3D、Particle3DquadRender等等使用了MeshCommand绘制。

MeshCommand绘制可以分为两种,一种是绘制建模生成的3D模型,另一种是直接使用glDrawElements直接绘制。

不作详细说明。有兴趣可以深入研究下。

GROUP_COMMAND

GroupCommand本身并不绘制任何东西,GroupCommand是用于创建渲染分支,使得某些特殊的绘制可以单独设置绘制状态,不影响主渲染分支。类ClippingNode、RenderTexture、NodeGrid等待使用了GroupCommand。

Renderer类中的 _renderGroups 数组支持多个渲染队列,_renderGroups[0]是主渲染队列,其他为渲染分支。

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

可以看得出,其实就是递归调用visitRenderQueue函数进行渲染。

CUSTOM_COMMAND

CustomCommand是所有命令中最简单的一个,也是最灵活的一个,绘制的内容和方式完全交由我们自己决定。LayerColor、DrawNode、Skybox等待都是使用CustomCommand命令进行绘制的。

else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
{
    flush();
    auto cmd = static_cast<CustomCommand*>(command);
    CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_CUSTOM_COMMAND");
    cmd->execute();
}

//std::function func;  execute

调用命令自己的execute函数进行渲染。

BATCH_COMMAND

BatchCommand可以同时绘制同一纹理的多个小图,用于2D绘制,类SpriteBatchNode和类ParticleBatchNode使用了BatchCommand减少OpenGL的调用

BatchCommand绘制主要由类TextureAtlas实现,TextureAtlas::drawQuads可以一次绘制多个使用同一纹理的矩形,Renderer处理BatchCommand也只是执行TextureAtlas::drawQuads函数

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

PRIMITIVE_COMMAND

TMXLayer瓦片地图类使用了PrimitiveCommand进行绘制

Primitive图元,指的是OpenGL图元。
浅谈cocos2dx渲染方式_第2张图片

你可能感兴趣的:(cocos2dx学习之路,cocos2d,游戏引擎)