首页 > 代码库 > 【Cocos2d-x 3.x】 3.0版本的全新绘制系

【Cocos2d-x 3.x】 3.0版本的全新绘制系

在Cocos2d-x 3.0的版本之前,Cocos2d-x的每个元素的绘制逻辑均分布于每个元素内部的draw()方法里,紧密依赖UI树的遍历;3.0开始,对绘制部分进行了重构,新的代码将绘制部分从UI树的遍历中分离出来,使得绘制系统设计更优雅、更灵活和易于扩展。


UI树的遍历

这是渲染系统比较重要的一个职责,遍历UI树中每一个元素,遍历的有两个重要的目的,一是遍历的顺序基本决定了元素被绘制的顺序,二是在遍历过程中实现元素的模型视图变换矩阵的计算,计算结果供OpenGL ES渲染管线计算顶点位置。
在3D渲染系统中,元素可以用任何顺序被绘制,最终图形惯性能够根据元素的Z轴,使用深度测试进行正确的绘制;在2D图形绘制中,各个元素在渲染管线中具有相同的Z深度,这些元素之间的层级以及绘制关系必须依赖同一个逻辑的深度,Cocos2d-x使用localZOrder来表示元素的逻辑深度,UI树的遍历采用中序的深度优先算法进行遍历。

遍历顺序及特点为:
遍历左边的子节点;
遍历根节点;
遍历右边子节点。

Cocos2d-x按元素的层级关系组织了一颗“”二叉树“”,左边的子节点表示逻辑深度小于0的子元素,右边的“”子节点“”表示逻辑深度大于0的子元素,这样,就能通过逻辑深度的顺序来表示元素被绘制的顺序,参见Node::visit()方法:

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->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    _director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);
    
    bool visibleByCamera = isVisitableByVisitingCamera();

    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
        if (visibleByCamera)
            this->draw(renderer, _modelViewTransform, flags);

        for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
            (*it)->visit(renderer, _modelViewTransform, flags);
    }
    else if (visibleByCamera)
    {
        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;
}

可见,Node先会将所有子节点排序,如果两个子节点的localZOrder值相同,则按照它们出现的顺序来表示绘制的顺序:

bool nodeComparisonLess(Node* n1, Node* n2)
{
    return( n1->getLocalZOrder() < n2->getLocalZOrder() ||
           ( n1->getLocalZOrder() == n2->getLocalZOrder() && n1->getOrderOfArrival() < n2->getOrderOfArrival() )
           );
}

void Node::sortAllChildren()
{
    if (_reorderChildDirty)
    {
        std::sort(std::begin(_children), std::end(_children), nodeComparisonLess);
        _reorderChildDirty = false;
    }
}

排序之后,Node就会按照小于0,根节点,然后是大于0的节点这样的顺序来一次绘制每个节点。


渲染命令和渲染队列


概述

新的绘制流程大致分为三步:生成绘制命令、对绘制命令进行排序、执行绘制命令。

生成绘制命令


在UI树的遍历的时候,对每一个元素生成一个绘制命令,RenderCommand表示一个绘制类型,它定义了如何去绘制一个元素,

class CC_DLL RenderCommand
{
public:

    enum class Type
    {
        UNKNOWN_COMMAND,
        QUAD_COMMAND,
        CUSTOM_COMMAND,
        BATCH_COMMAND,
        GROUP_COMMAND,
        MESH_COMMAND,
        PRIMITIVE_COMMAND,
        TRIANGLES_COMMAND
    };

    /**
     * init function, will be called by all the render commands
     */
    void init(float globalZOrder, const Mat4& modelViewTransform, uint32_t flags);
    
    /** Get Render Command Id */
    inline float getGlobalOrder() const { return _globalOrder; }
    //...其他定义省略 
};

Type这个enum class里定义了几种绘制的类型,一般情况下,每个UI元素会关联0个或1个RenderCommand,并重写基类Node::draw()方法,在draw方法中将绘制命令发送给render。

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

    if(_insideBounds)
#endif
    {
        _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform, flags);
        renderer->addCommand(&_quadCommand);
        
#if CC_SPRITE_DEBUG_DRAW
        _debugDrawNode->clear();
        Vec2 vertices[4] = {
            Vec2( _quad.bl.vertices.x, _quad.bl.vertices.y ),
            Vec2( _quad.br.vertices.x, _quad.br.vertices.y ),
            Vec2( _quad.tr.vertices.x, _quad.tr.vertices.y ),
            Vec2( _quad.tl.vertices.x, _quad.tl.vertices.y ),
        };
        _debugDrawNode->drawPoly(vertices, 4, true, Color4F(1.0, 1.0, 1.0, 1.0));
#endif //CC_SPRITE_DEBUG_DRAW
    }
}

Sprite::draw()方法示意了这样绘制分离的方式,它只负责将绘制命令发送给render,并不会执行任何的GL命令,render会将RenderCommand放入一个栈中,等所有的UI元素遍历结束,render才开始执行所有的RenderCommand。

绘制命令的排序


绘制命令被执行的顺序不一定是UI元素被遍历的顺序,Cocos2d-x使用一个新的globalZOrder直接设置元素的绘制顺序,因此,UI元素绘制的顺序首先由globalZOrder决定,
然后再由遍历的顺序决定。

绘制命令执行

最后,render对经过排序的绘制命令执行绘制。 对于一般的RenderCommand,按顺序执行;对于Sprite使用的QuadCommand,如果两个QuadCommand相邻且使用相同的纹理、着色器等,render会将它们组合合成一个QuadCommand,这种情况称为自动批绘制。自动批绘制减少了绘制次数,提升了绘制性能。


绘制命令、绘制队列和绘制类


RenderCommand


class CC_DLL RenderCommand
{
public:

    enum class Type
    {
        UNKNOWN_COMMAND,
        QUAD_COMMAND,
        CUSTOM_COMMAND,
        BATCH_COMMAND,
        GROUP_COMMAND,
        MESH_COMMAND,
        PRIMITIVE_COMMAND,
        TRIANGLES_COMMAND
    };

    /**
     * init function, will be called by all the render commands
     */
    void init(float globalZOrder, const Mat4& modelViewTransform, uint32_t flags);
    
    /** Get Render Command Id */
    inline float getGlobalOrder() const { return _globalOrder; }
};

每一个R enderCommand实例中,都包含一个globalZOrder属性,它是决定绘制顺序的重要属性。还有一个属性是Type,引擎内置了多个RenderCommand类型,其中QUAD_COMMAND用来绘制1个或多个矩形区域(比如说Sprite和ParticalSystem),相邻的QuadCommand如果使用相同的纹理,则可以实现自动批绘制。

BATCH_COMMAND用来绘制一个TextAtlas,如Label、TileMap等。 GROUP_COMMAND可以包装多个RenderCommand的集合,而且GroupCommand中的每一个RenderCommand都不会参与全局的排序。

RenderQueue

class RenderQueue {
public:
    enum QUEUE_GROUP
    {
        GLOBALZ_NEG = 0,
        OPAQUE_3D = 1,
        TRANSPARENT_3D = 2,
        GLOBALZ_ZERO = 3,
        GLOBALZ_POS = 4,
        QUEUE_COUNT = 5,
    };

public:
    RenderQueue()
    {
        clear();
    }
    void push_back(RenderCommand* command);
    ssize_t size() const;
    void sort();
    RenderCommand* operator[](ssize_t index) const;
    void clear();
    inline std::vector<RenderCommand*>& getSubQueue(QUEUE_GROUP group) { return _commands[group]; }
    inline ssize_t getSubQueueSize(QUEUE_GROUP group) const { return _commands[group].size();}

    void saveRenderState();
    void restoreRenderState();
    
protected:
    
    std::vector<std::vector<RenderCommand*>> _commands;
    
    //Render State related
    bool _isCullEnabled;
    bool _isDepthEnabled;
    GLboolean _isDepthWrite;
};

场景中每一个UI元素的绘制命令被发送到一个RenderQueue的绘制栈上,由类的定义可知,RenderQueue中存储着一组RenderCommand。而在QUEUE_GROUP中定义了每个RenderCommand应该添加到vector的哪个对应索引下:

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

static bool compare3DCommand(RenderCommand* a, RenderCommand* b)
{
    return  a->getDepth() > b->getDepth();
}

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

这是RenderQueue的排序方式,有sort()函数体的代码可以看到,QUEUE_GROUP::TRANSPARENT_3D表示的是3D的物体的绘制命令,将这些绘制命令排序时用到compare3DCommand,比较他们的Depth;而QUEUE_GROUP::GLOBALZ_NEG和QUEUE_GROUP::GLBALZ_POS分别表示globalZOrder小于0和大于0的绘制命令。

Render类实际上维护着一个RenderQueue的数组,每一个RenderQueue对应一组RenderCommand或者一个GroupCommand。

GroupCommand

class CC_DLL GroupCommand : public RenderCommand
{
public:
    GroupCommand();
    ~GroupCommand();
    
    void init(float depth);

    inline int getRenderQueueID() const {return _renderQueueID;}
    
protected:
    int _renderQueueID;
};

每个GroupCommand都对应着一个单独的RenderQueue,由_renderQueueID标识。

Renderer

class CC_DLL Renderer
{
public:
    static const int VBO_SIZE = 65536;
    static const int INDEX_VBO_SIZE = VBO_SIZE * 6 / 4;
    
    static const int BATCH_QUADCOMMAND_RESEVER_SIZE = 64;
    static const int MATERIAL_ID_DO_NOT_BATCH = 0;

    /** Adds a `RenderComamnd` into the renderer */
    void addCommand(RenderCommand* command);

    /** Adds a `RenderComamnd` into the renderer specifying a particular render queue ID */
    void addCommand(RenderCommand* command, int renderQueue);

    /** Pushes a group into the render queue */
    void pushGroup(int renderQueueID);

    /** Pops a group from the render queue */
    void popGroup();

    /** returns whether or not a rectangle is visible or not */
    bool checkVisibility(const Mat4& transform, const Size& size);
protected:

    void processRenderCommand(RenderCommand* command);
    void visitRenderQueue(RenderQueue& queue);

    std::stack<int> _commandGroupStack;
    
    std::vector<RenderQueue> _renderGroups;
    
    MeshCommand*              _lastBatchedMeshCommand;
    std::vector<TrianglesCommand*> _batchedCommands;
    std::vector<QuadCommand*> _batchQuadCommands;

    GroupCommandManager* _groupCommandManager;
};

Renderer类主要的部分如上,_commandGroupStack保留了一个RenderQueue的栈,开始一个GroupCommand时,会对应新建一个新的RenderQueue的Id入栈,默认情况下,addCommand会将RenderCommand添加到_commandGroupStack栈的最后一个元素所对应的RenderQueue中,这样就能将所有子元素的RenderCommand添加到单独一个RenderQueue中,当分组结束,GroupCommand从_commandGroupStack上移除自己,后续的RederCommand将继续加入之前的RederQueue中:

int GroupCommandManager::getGroupID()
{
    //Reuse old id
    for(auto it = _groupMapping.begin(); it != _groupMapping.end(); ++it)
    {
        if(!it->second)
        {
            _groupMapping[it->first] = true;
            return it->first;
        }
    }

    //Create new ID
//    int newID = _groupMapping.size();
    int newID = Director::getInstance()->getRenderer()->createRenderQueue();
    _groupMapping[newID] = true;

    return newID;
}

这是GroupCommand获取groupID的方法,在GroupCommandManager的init()方法中,说明了GroupCommand所在的_commandGroupStack索引不能是0 :

bool GroupCommandManager::init()
{
    //0 is the default render group
    _groupMapping[0] = true;
    return true;
}

而在Renderer::Renderer()中,说明了_commandGroupStack[0]表示默认的绘制队列:
_commandGroupStack.push(DEFAULT_RENDER_QUEUE); // DEFAULT_RENDER_QUEUE == 0

Renderer还有两个函数,当创建一个GroupCommand并将其作为一个普通的RenderCommand发送到当前的RenderQueue上,GroupCommand会在Renderer上创建新的RenderQueue,并调用pushGroup()方法将其renderQueueId添加到_commandGroupStack栈中,结束时,调用popGroup():

void Renderer::pushGroup(int renderQueueID)
{
    CCASSERT(!_isRendering, "Cannot change render queue while rendering");
    _commandGroupStack.push(renderQueueID);
}

void Renderer::popGroup()
{
    CCASSERT(!_isRendering, "Cannot change render queue while rendering");
    _commandGroupStack.pop();
}


个人的一些源码阅读理解,欢迎路过的各位大大指出错误~ 后续还会有更新~



【Cocos2d-x 3.x】 3.0版本的全新绘制系