1.2.02_COCOS2D-X渲染之绘图原理

精灵的绘制

打开CCSprite的代码文件,其中draw方法负责绘制精灵,其实现代码如下:

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    if (_texture == nullptr)
    {
        return;
    }

#if CC_USE_CULLING
    // Don't calculate the culling if the transform was not updated
    auto visitingCamera = Camera::getVisitingCamera();
    auto defaultCamera = Camera::getDefaultCamera();
    if (visitingCamera == defaultCamera) {
        _insideBounds = ((flags & FLAGS_TRANSFORM_DIRTY) 
          || visitingCamera->isViewProjectionUpdated()) 
          ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
    }
    else
    {
        // XXX: this always return true since
        _insideBounds = renderer->checkVisibility(transform, _contentSize);
    }

    if(_insideBounds)
#endif
    {
        _trianglesCommand.init(_globalZOrder,
                               _texture,
                               getGLProgramState(),
                               _blendFunc,
                               _polyInfo.triangles,
                               transform,
                               flags);

        renderer->addCommand(&_trianglesCommand);

#if CC_SPRITE_DEBUG_DRAW
        _debugDrawNode->clear();
        auto count = _polyInfo.triangles.indexCount/3;
        auto indices = _polyInfo.triangles.indices;
        auto verts = _polyInfo.triangles.verts;
        for(ssize_t i = 0; i < count; i++)
        {
            //draw 3 lines
            Vec3 from =verts[indices[i*3]].vertices;
            Vec3 to = verts[indices[i*3+1]].vertices;
            _debugDrawNode->drawLine(Vec2(from.x, from.y), 
              Vec2(to.x,to.y), Color4F::WHITE);

            from =verts[indices[i*3+1]].vertices;
            to = verts[indices[i*3+2]].vertices;
            _debugDrawNode->drawLine(Vec2(from.x, from.y), 
              Vec2(to.x,to.y), Color4F::WHITE);

            from =verts[indices[i*3+2]].vertices;
            to = verts[indices[i*3]].vertices;
            _debugDrawNode->drawLine(Vec2(from.x, from.y), 
              Vec2(to.x,to.y), Color4F::WHITE);
        }
#endif //CC_SPRITE_DEBUG_DRAW
    }
}
void TrianglesCommand::init(float globalOrder, GLuint textureID, 
GLProgramState* glProgramState, BlendFunc blendType,
const Triangles& triangles,const Mat4& mv, uint32_t flags)
{
    CCASSERT(glProgramState, "Invalid GLProgramState");
    CCASSERT(glProgramState->getVertexAttribsFlags() == 0, \
      "No custom attributes are supported in QuadCommand");

    RenderCommand::init(globalOrder, mv, flags);

    _triangles = triangles;
    if(_triangles.indexCount % 3 != 0)
    {
        int count = _triangles.indexCount;
        _triangles.indexCount = count / 3 * 3;
        CCLOGERROR("Resize indexCount from %zd to %zd, \
          size must be multiple times of 3", count, _triangles.indexCount);
    }
    _mv = mv;
    
    if( _textureID != textureID || _blendType.src != blendType.src
    || _blendType.dst != blendType.dst ||
       _glProgramState != glProgramState)
    {
        _textureID = textureID;
        _blendType = blendType;
        _glProgramState = glProgramState;

        generateMaterialID();
    }
}
  • 采用"服务器端"(负责具体的绘制渲染)+"客户端"(负责向服务器端发送绘图指令)的方式进行渲染。

  • 初始化 渲染指令TrianglesCommand-_trianglesCommand,设置ZOrder、纹理、OpenGL状态、颜色混合模式、贴图渲染的方式、顶点坐标、纹理坐标以及顶点颜色等。

  • renderer->addCommand(&_trianglesCommand);将渲染指令加入render渲染列表,等待下一帧主循环mainLoop调用绘制Director::drawScene()来渲染_renderer->render();

</br>

渲染树的绘制

无论如何复杂的游戏场景也都是精灵通过不同的层次、位置组合构成的,因此只要可以把精灵按照前后层次,在不同的位置绘制出来就完成了游戏场景的绘制。

Cocos2d-x的渲染树结构,渲染树是由各种游戏元素按照层次关系构成的树结构,它展示了Cocos2d-x游戏的绘制层次,因此游戏的渲染顺序就是由渲染树决定的。

回顾Cocos2d-x游戏的层次:导演类CCDirector直接控制渲染树的根节点--场景(CCScene),场景包含多个层(CCLayer),层中包含多个精灵(CCSprite)。实际上,每一个上述的游戏元素都在渲染树中表示为节点(CCNode),游戏元素的归属关系就转换为了节点间的归属关系,进而形成树结构。

CCNode的visit方法实现了对一棵渲染树的绘制。为了绘制树中的一个节点,就需要绘制自己的子节点,直到没有子节点可以绘制时再结束这个过程。因此,为了每一帧都绘制一次渲染树,就需要调用渲染树的根节点。换句话说,当前场景的visit方法在每一帧都会被调用一次。这个调用是由游戏主循环完成的。

绘制父节点时会引起子节点的绘制,同时,子节点的绘制方式与父节点的属性也有关。例如,父节点设置了放大比例,则子节点也会随之放大;父节点移动一段距离,则子节点会随之移动并保持相对位置不变。显而易见,绘制渲染树是一个递归的过程,下面我们来详细探讨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(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);
    }

    _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;
}

</br>

坐标变换

在绘制渲染树中,最关键的步骤之一就是进行坐标系的变换。没有坐标系的变换,则无法在正确的位置绘制出纹理。同时,坐标系的变换在其他的场合(例如碰撞检测中)也起着十分重要的作用。因此在这一节中,我们将介绍Cocos2d-x中的坐标变换功能。
首先,我们来看一下transform方法,其代码如下所示:

Mat4 Node::transform(const Mat4& parentTransform)
{
    return parentTransform * this->getNodeToParentTransform();
}
const Mat4& Node::getNodeToParentTransform() const
{
    if (_transformDirty)
    {
        // Translate values
        float x = _position.x;
        float y = _position.y;
        float z = _positionZ;
        
        if (_ignoreAnchorPointForPosition)
        {
            x += _anchorPointInPoints.x;
            y += _anchorPointInPoints.y;
        }
        
        bool needsSkewMatrix = ( _skewX || _skewY );

        // Build Transform Matrix = translation * rotation * scale
        Mat4 translation;
        //move to anchor point first, then rotate
        Mat4::createTranslation(x, y, z, &translation);
        
        Mat4::createRotation(_rotationQuat, &_transform);
        
        if (_rotationZ_X != _rotationZ_Y)
        {
            // Rotation values
            // Change rotation code to handle X and Y
            // If we skew with the exact same value for both x and y then we're simply just rotating
            float radiansX = -CC_DEGREES_TO_RADIANS(_rotationZ_X);
            float radiansY = -CC_DEGREES_TO_RADIANS(_rotationZ_Y);
            float cx = cosf(radiansX);
            float sx = sinf(radiansX);
            float cy = cosf(radiansY);
            float sy = sinf(radiansY);
            
            float m0 = _transform.m[0], m1 = _transform.m[1], m4 = _transform.m[4], m5 = _transform.m[5], m8 = _transform.m[8], m9 = _transform.m[9];
            _transform.m[0] = cy * m0 - sx * m1, _transform.m[4] = cy * m4 - sx * m5, _transform.m[8] = cy * m8 - sx * m9;
            _transform.m[1] = sy * m0 + cx * m1, _transform.m[5] = sy * m4 + cx * m5, _transform.m[9] = sy * m8 + cx * m9;
        }
        _transform = translation * _transform;

        if (_scaleX != 1.f)
        {
            _transform.m[0] *= _scaleX, _transform.m[1] *= _scaleX, _transform.m[2] *= _scaleX;
        }
        if (_scaleY != 1.f)
        {
            _transform.m[4] *= _scaleY, _transform.m[5] *= _scaleY, _transform.m[6] *= _scaleY;
        }
        if (_scaleZ != 1.f)
        {
            _transform.m[8] *= _scaleZ, _transform.m[9] *= _scaleZ, _transform.m[10] *= _scaleZ;
        }
        
        // FIXME:: Try to inline skew
        // If skew is needed, apply skew and then anchor point
        if (needsSkewMatrix)
        {
            float skewMatArray[16] =
            {
                1, (float)tanf(CC_DEGREES_TO_RADIANS(_skewY)), 0, 0,
                (float)tanf(CC_DEGREES_TO_RADIANS(_skewX)), 1, 0, 0,
                0,  0,  1, 0,
                0,  0,  0, 1
            };
            Mat4 skewMatrix(skewMatArray);
            
            _transform = _transform * skewMatrix;
        }

        // adjust anchor point
        if (!_anchorPointInPoints.isZero())
        {
            // FIXME:: Argh, Mat4 needs a "translate" method.
            // FIXME:: Although this is faster than multiplying a vec4 * mat4
            _transform.m[12] += _transform.m[0] * -_anchorPointInPoints.x + _transform.m[4] * -_anchorPointInPoints.y;
            _transform.m[13] += _transform.m[1] * -_anchorPointInPoints.x + _transform.m[5] * -_anchorPointInPoints.y;
            _transform.m[14] += _transform.m[2] * -_anchorPointInPoints.x + _transform.m[6] * -_anchorPointInPoints.y;
        }
    }

    if (_additionalTransform)
    {
        // This is needed to support both Node::setNodeToParentTransform() and Node::setAdditionalTransform()
        // at the same time. The scenario is this:
        // at some point setNodeToParentTransform() is called.
        // and later setAdditionalTransform() is called every time. And since _transform
        // is being overwritten everyframe, _additionalTransform[1] is used to have a copy
        // of the last "_transform without _additionalTransform"
        if (_transformDirty)
            _additionalTransform[1] = _transform;

        if (_transformUpdated)
            _transform = _additionalTransform[1] * _additionalTransform[0];
    }

    _transformDirty = _additionalTransformDirty = false;

    return _transform;
}

首先通过getNodeToParentTransform()方法获取此节点相对于父节点的变换矩阵,然后把它转换为OpenGL格式的矩阵并右乘在当前绘图矩阵之上,最后进行了一些摄像机与Gird特效相关的操作。

把此节点相对于父节点的变换矩阵与当前节点相连,也就意味着在当前坐标系的基础上进行坐标系变换,得到新的合适的坐标系。

形象地讲,transform方法的任务就是根据当前节点的属性计算出如何把绘图坐标系变换为新坐标系的矩阵。图10-8形象地描述了这一操作。

变换相关的方法

方法 | 描述
-- |
CCAffineTransform nodeToParentTransform() | 获取节点相对于父节点的变换矩阵
CCAffineTransform parentToNodeTransform() | 获取父节点相对于节点的变换矩阵
CCAffineTransform nodeToWorldTransform() | 获取节点相对于世界坐标系的变换矩阵
CCAffineTransform worldToNodeTransform() | 获取世界坐标系相对于节点的变换矩阵
CCPoint convertToNodeSpace(const CCPoint& worldPoint) | 把世界坐标系中的点坐标转换到节点坐标系
CCPoint convertToWorldSpace(const CCPoint& nodePoint) | 把节点坐标系中的点坐标转换到世界坐标系
CCPoint convertToNodeSpaceAR(const CCPoint& worldPoint) | 把世界坐标系中的点坐标转换到节点坐标系(相对于锚点)
CCPoint convertToWorldSpaceAR(const CCPoint& nodePoint) | 把节点坐标系中的点坐标(相对于锚点)转换到世界坐标系
CCPoint convertTouchToNodeSpace(CCTouch * touch) | 获取触摸点在节点坐标系中的坐标
CCPoint convertTouchToNodeSpaceAR(CCTouch * touch) | 获取触摸点在节点坐标系中的坐标(相对于锚点)

"节点坐标系"指的是以一个节点作为参考而产生的坐标系,换句话说,它的任何一个子节点的坐标值都是由这个坐标系确定的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 摘自:Cocos2d-x 高级开发教程-第10章OpenGL基础 OpenGL简介(1) OpenGL是一个基于C...
    资深小夏阅读 1,575评论 0 0
  • 渲染是一个游戏的重要组成部分,给与玩家最直接的体验。渲染做的好,游戏不仅要华丽,还要运行流畅。接下来,我们来看看c...
    鱼鸟fly阅读 1,003评论 0 2
  • 前言 我选择开发一个游戏有很多原因。我觉得自己是“核心”玩家,过去的大部分时间我都花在玩游戏,自己制作、阅读和游戏...
    月影檀香阅读 11,687评论 1 27
  • 在一个风和日丽的上午,案主悠闲地靠在树下看着书,爸爸妈妈在落地窗前吹口琴、饮茶,树上的小鸟在欢快地歌唱,身边的小兔...
    皮皮爸爸时代阅读 149评论 0 2
  • 上班没多久,就被告知,工作室的助理A月底就要离开这里,开始下一个冒险挑战,化妆助理B因意外情况也突然离开,助...
    小伊__er阅读 444评论 3 0