场景的渲染

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
遍历渲染队列。
怎么遍历的呢?
- RenderQueue::QUEUE_GROUP::GLOBALZ_NEG globalZOrder < 0
 - RenderQueue::QUEUE_GROUP::OPAQUE_3D 3D不透明的对象
 - RenderQueue::QUEUE_GROUP::TRANSPARENT_3D 3D对象透明的对象
 - RenderQueue::QUEUE_GROUP::GLOBALZ_ZERO globalZOrder == 0
 - RenderQueue::QUEUE_GROUP::GLOBALZ_POS globalZOrder > 0
 
Render:visitRenderQueue
根据上面的顺序,会依次进入processRenderCommand函数
在说这个函数之前,我们要了解cocos2dx的几种渲染命令。
- TRIANGLES_COMMAND:TrianglesCommand,渲染三角形,可以合并命令减少OpenGL的调用提高渲染效率
 - MESH_COMMAND:MeshCommand,渲染3D
 - GROUP_COMMAND:GroupCommand,创建渲染分支,使用_renderGroups[0]之外的RenderQueue。也是多个renderCOmmand的集合,里面的命令不参与全局排序。可用于子元素裁剪,绘制元素到纹理等。
 - CUSTOM_COMMAND:CustomCommand,自定义渲染命令
 - BATCH_COMMAND:BatchCommand,同时渲染多个使用同一纹理的图形,提高渲染效率
 - 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的条件是。
- 相邻的命令
 - 有相同的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的生成函数,或者说获取函数。
从该函数可以得知
- 相同的纹理
 - 相同的混合模式(blend)
 - 相同的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<void()> 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图元。
 


















