1.2.02_COCOS2D-X渲染之绘图原理

96
希希爸爸
2017.07.04 17:54* 字数 1296

精灵的绘制

打开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) | 获取触摸点在节点坐标系中的坐标(相对于锚点)

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

202.Cocos2d-x(3.+)
Web note ad 1