Cocos2d-x 学习----通过OpenGL绘图过程学习场景、层、与精灵的关系。

本文深入探讨Cocos2d-x中场景、层、精灵的关系,解析坐标系变换、节点层级与渲染树结构。通过3.2版本的渲染优化机制,理解如何减少GPU负荷,提高性能,包括Batch渲染和RenderQueue的使用。
说明:
1、以前学习过Android UI 布局
2、使用Java编写过动画效果demo
学习之前:我之前简单的认为,场景、层、精灵在图画绘制的关系就是由大到小的包含布局关系,并没有深入地了解这三者从无到有的绘制过程。
学习目的:理解cocos2d-x如何使用OpenGL来渲染成像的。

参考博客:
OpenGL–3D世界(视图变换,模型变换,投影变换,视口变换)
http://blog.csdn.net/u010223072/article/details/44620871
OpenGL 的渲染流水线
http://blog.csdn.net/pizi0475/article/details/50605972
Cocos场景遍历与渲染
http://blog.csdn.net/wlk1229/article/details/55005282
渲染流水线
http://www.cnblogs.com/suimeng/p/4655834.html
cocos2dx的渲染机制
http://blog.csdn.net/han1558249222/article/details/48093931
贴图、纹理、材质的区别
https://www.zhihu.com/question/25745472/answer/31462038
cocos2d-x3.2渲染机制的优化
http://blog.csdn.net/mydreamremindme/article/details/47091601
浅谈Draw Call和Batch的区别
http://blog.csdn.net/molti/article/details/42495993
参考书籍:《Cocos2d-x高级开发教程》

基础知识:

1、导演director、场景scene、层layer、精灵sprite 的基本概念和继承关系树。
http://www.cocos.com/docs/native/

2、世界坐标系、绘图坐标系、模型坐标系。
说说我现在的理解,世界坐标系是不动的,在cocos2d-x里面,世界坐标系的原点是在可视屏幕的左下角,x轴往右,Y轴往上。绘图坐标系顾名思义,是OpenGL绘制图形的时候使用的坐标系,cocos2d-x也参照了OpenGL,套用上了这个绘图坐标系,在没有设计任何坐标变换时(绘图坐标系具有可变性),绘图坐标系和世界坐标系的原点相同。模型坐标系,模型坐标系就是以纹理自身的锚点作为坐标系的原点,X轴和Y轴方向与世界坐标系相同。

3、坐标系变换及坐标系变换矩阵。

坐标系的变换,主要的关键再与绘图坐标系与世界坐标系的关系。前面说到绘图坐标系是具备可变性的,它的原点和X轴Y轴的方向是可变,但纹理在绘图坐标系中的坐标是不会改变的。比如绘制一个精灵,通过平移200PX,那么就会通过当前绘图坐标系原点所在的世界坐标系的值,通过平移矩阵的变换,得到绘图坐标系原点新的世界坐标系的值,然后再绘制这个精灵。变换矩阵通过《Cocos2d-x高级开发教程》中学习。

4、节点层级

节点是场景、层和精灵共同的父类,在这里将它们统称节点。我把节点分为组合节点和独立节点。组合节点由“自身节点+组合节点or独立节点”构成,是一个树的结构。
从生活体验可以知道,画家在绘制同一幅画时,都是有先后顺序的,OpenGL的绘制也是如此。这个优先顺序称为节点层级。在组合节点的子节点中,节点层级通过三个值globalZOrder、localZOrder、orderOfArrival 进行比较,都是值越大越靠前。三个值的对层级影响的优先顺序为globalZOrder、localZOrder、orderOfArrival。当globalZOrder的值大时,则会忽略localZOrder和orderOfArrival,globalZOrder值大则排在前面。我们发现了一个词叫ZOrder,Z轴坐标,由于OpenGL绘制成像是有过程的,cocos2d-x中,通过提供ZOrder属性,来对图形的绘制顺序形象立体化,来应对组合节点的绘制过程以及图像的重叠等问题。

orderOfArrival

orderOfArrival是节点添加到父节点时会被设置,

child->setOrderOfArrival(s_globalOrderOfArrival++)。

所有节点的orderOfArriva都是通过 s_ globalOrderOfArrival值累加得到的。s_ globalOrderOfArrival是Node节点的一个静态变量,初始值为1。

int Node::s_globalOrderOfArrival = 1;

每次有调用addChild函数,则该值会加1,所以所有节点的该值都不一样。

localZOrder

localZOrder在节点的构造函数中设置为0,默认值为0。可以通过函数
voidNode::_setLocalZOrder(intz)设置。
localZOrder和orderOfArrival都只是在遍历子节点时排序使用,排序比较时会先比价LocalZOrder的值,如过不相等则返回相应的比较结果,如果相等则会比较orderOfArrival的值。因为两个值都只是用在子节点排序上,所以localZOrder和orderOfArrival只会影响子节点之间的排序,所以如果两个节点的父节点不同则无法通过localZOrder和orderOfArrival控制层级。

对于子节点和父节点的遍历顺序:
LocalZOrder小于0的子节点>父节点自身>LocalZOrder大于等于0的子节点。
所以LocalZOrder小于0会绘制在父节点下面。

globalZOrder

globalZOrder在节点的构造函数中设置为0,默认值为0。
globalZOrder可以通过函数voidNode::setGlobalZOrder(floatglobalZOrder)设置。
globalZOrder是在场景绘制命令生成后,会根据globalZOrder对节点的绘制命令排序。所以globalZOrder是对场景全局节点的层级进行控制的,不管节点的父节点是谁,只要globalZOrder的值大,则绘制会更靠前。globalZOrder只是控制一个场景的节点,不可跨场景。

5、渲染树结构

编程中有一个结构叫做递归。渲染树结构就是利用了递归的结构。在上文中提及,Node节点是父类,在Node类中定义了两个重要的方法,visit 和 draw 方法,其中draw方法在cocos2d-x 3.x版本之后,定义为虚函数,draw方法就是绘制节点自身的函数。在Node类中,将draw方法的调用放到了visit的方法体中,通过调用visit方法来调用draw。visit方法,顾名思义,探望探亲,通过visit方法来寻找与自身节点相关的子节点。visit方法则使用了函数的递归结构。visit方法中,首先判断是不是组合节点,如果是组合节点,那么会根据其子节点的节点层级进行一次排序,根据排序的结果,优先渲染localZOrder小于零的子节点,再渲染localZOrder大于零的子节点。在渲染子节点时,会调用子节点的visit方法,继续查找是不是组合节点;如果发现不是组合节点,则执行此独立节点的Draw方法,进行绘制。如此类推,整个cocos图像成型的体系就像一个树一样。

渲染树

6、cocos2d-x 3.2 渲染优化机制。

从第五点可知,我们要绘制一个完整的cocos图像,就要绘制里面每一个节点,这样会给GPU带来很大的负荷。从cocos2d-x 3.2 开始,对Node类中的draw方法进行了优化改动,不再在draw方法中直接调用OpenGL的绘图函数。而是新增加了一个Render渲染类,通过Render类中的函数来调用OpenGL的渲染函数,Render类对象作为visit函数和draw函数的一个参数进行传递。当这一帧中所有的Node对象都被visit遍历过之后,再按照RenderQueue队列对图形进行渲染。

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
        //渲染自身,只有是在渲染自身的时候,才调用draw方法,子节点都是通过调用visit方法来调用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;
}
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
    {
        _trianglesCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, _polyInfo.triangles, transform, flags);
        renderer->addCommand(&_trianglesCommand);
    }
}

在draw方法中,Render类的实例对象调用了addCommand方法,这个方法是因为Render渲染器具备一个RenderQueue,addCommand将渲染该节点的任务Command添加到队列中。
渲染命令RenderCommand分成以下几种:
TrianglesComman,
QuadCommand,
MeshCommand,
GroupCommand,
BatchCommand,
PrimitiveCommand精灵的绘制主要用QuadCommand这种渲染命令。
Render类中定义了一个drawBatchQuads的方法,下面是方法内部分代码:

void Renderer::drawBatchedQuads(){}
  //Start drawing vertices in batch
    for(const auto& cmd : _batchQuadCommands)
    {
        bool commandQueued = true;
        auto newMaterialID = cmd->getMaterialID();
        if(_lastMaterialID != newMaterialID || newMaterialID == MATERIAL_ID_DO_NOT_BATCH)
        {
            // flush buffer
            if(indexToDraw > 0)
            {
                glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) );
                _drawnBatches++;
                _drawnVertices += indexToDraw;

                startIndex += indexToDraw;
                indexToDraw = 0;
            }

            //Use new material
            _lastMaterialID = newMaterialID;

            cmd->useMaterial();
        }

        if (commandQueued)
        {
            indexToDraw += cmd->getQuadCount() * 6;
        }
    }

重点看_materialID 的构成。

void QuadCommand::generateMaterialID()
{
    _skipBatching = false;

    if(_glProgramState->getUniformCount() == 0)
    {
        int glProgram = (int)_glProgramState->getGLProgram()->getProgram();
        int intArray[4] = { glProgram, (int)_textureID, (int)_blendType.src, (int)_blendType.dst};

        _materialID = XXH32((const void*)intArray, sizeof(intArray), 0);
    }
    else
    {
        _materialID = Renderer::MATERIAL_ID_DO_NOT_BATCH;
        _skipBatching = true;
    }
}

OpenGL全写Open Graphics Library,开发的绘制图形库。OpenGL被大家俗称为状态机,也就是具备很多个开关的机器(关于OpenGL详情可查看OpenGL渲染流水线)。我们绘制不同的图像时打开的开关种类和数量不同,那么如果我们绘制的图像基本相同,并且打开开光的种类和数量相同,是不是就可以把多个图像一起绘制?
_materialID 就是作为这样的一个判断,如果 当前加入RenderQueue的精灵具备与上一个加入RenderQueue的精灵相同的 _materialID,那么就可以将这两个精灵结合在一起,给Render传入两个精灵的顶点,让它一起绘制,从而节省了一次调用GPU渲染的过程。

渲染优化机制应用

cocos2d-x游戏开发中,一般使用plist文件将多个图片集合在一起,plist文件中的所有图片都是具备同一种纹理的,也就是可以说 它们的_materialID 都是相同的。那么就可以如下进行,绘制多个图片,传递多个图片的顶点数据,最后只调用一次GPU。

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("close.plist");   
for(int i=0;i<100;i++){  
Sprite* sprite1 = Sprite::createWithSpriteFrameName("CloseNormal.png");  
Sprite* sprite2 = Sprite::createWithSpriteFrameName("CloseSelected.png");  
Sprite* spriteN = Sprite::createWithSpriteFrameName("name.png");  
sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+sprite1->getContentSize().height/2*(i%10));  
sprite2->setPosition(origin.x+sprite2->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-sprite2->getContentSize().height/2*(i%10));  
spriteN->setPosition(origin.x+spriteN->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-spriteN->getContentSize().height/2*(i%10));
this->addChild(spriteN);  
this->addChild(sprite2);  
this->addChild(sprite1);  
}  

不过由于渲染机制的Queue的队列方式,只能够通过最外层的RenderCommand的_lastMaterialID 进行对比,所以,局限性是被渲染树的结构所限制。

总结

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值