Cocos2d-x3.2与OpenGL渲染总结(一)Cocos2d-x3.2的渲染流程

原文:http://www.2cto.com/kf/201409/336234.html

最近几天,我都在学习如何在Cocos2d-x3.2中使用OpenGL来实现对图形的渲染。在网上也看到了很多好的文章,我在它们的基础上做了这次的我个人认为比较完整的总结。当你了解了Cocos2d-x3.2中对图形渲染的流程,你就会觉得要学会写自己的shader才是最重要的。

第一,渲染流程从2.x到3.x的变化。

在2.x中,渲染过程是通过递归渲染树(Rendering tree)这种图关系来渲染关系图。递归调用visit()函数,并且在visit()函数中调用该节点的draw函数渲染各个节点,此时draw函数的作用是直接调用OpenGL代码进行图形的渲染。由于visit()和draw函数都是虚函数,所以要注意执行时的多态。那么我们来看看2.x版本中CCSprite的draw函数,如代码1。

代码1:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//这是cocos2d-2.0-x-2.0.4版本的CCSprite的draw函数
void CCSprite::draw( void )
{
     CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw" );
     CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called" );
     CC_NODE_DRAW_SETUP();
     ccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );
     if (m_pobTexture != NULL)
     {
         ccGLBindTexture2D( m_pobTexture->getName() );
     }
     else
     {
         ccGLBindTexture2D( 0 );
    
     //
     // Attributes
     //
     ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );
#define kQuadSize sizeof(m_sQuad.bl)
     long offset = ( long )&m_sQuad;
     // vertex
     int diff = offsetof( ccV3F_C4B_T2F, vertices);
     glVertexAttribPointer(kCCVertexAttrib_Position, 3 , GL_FLOAT, GL_FALSE, kQuadSize, ( void *) (offset + diff));
     // texCoods
     diff = offsetof( ccV3F_C4B_T2F, texCoords);
     glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2 , GL_FLOAT, GL_FALSE, kQuadSize, ( void *)(offset + diff));
     // color
     diff = offsetof( ccV3F_C4B_T2F, colors);
     glVertexAttribPointer(kCCVertexAttrib_Color, 4 , GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, ( void *)(offset + diff));
     glDrawArrays(GL_TRIANGLE_STRIP, 0 , 4 );
     CHECK_GL_ERROR_DEBUG();
# if CC_SPRITE_DEBUG_DRAW == 1
     // draw bounding box
     CCPoint vertices[ 4 ]={
         ccp(m_sQuad.tl.vertices.x,m_sQuad.tl.vertices.y),
         ccp(m_sQuad.bl.vertices.x,m_sQuad.bl.vertices.y),
         ccp(m_sQuad.br.vertices.x,m_sQuad.br.vertices.y),
         ccp(m_sQuad.tr.vertices.x,m_sQuad.tr.vertices.y),
     };
     ccDrawPoly(vertices, 4 , true );
#elif CC_SPRITE_DEBUG_DRAW == 2
     // draw texture box
     CCSize s = this ->getTextureRect().size;
     CCPoint offsetPix = this ->getOffsetPosition();
     CCPoint vertices[ 4 ] = {
         ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
         ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
     };
     ccDrawPoly(vertices, 4 , true );
#endif // CC_SPRITE_DEBUG_DRAW
 
     CC_INCREMENT_GL_DRAWS( 1 );
 
     CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw" );
}
那么我们也看看3.x中Sprite的draw函数,如代码2。

代码2:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
     // Don't do calculate the culling if the transform was not updated
     _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
     if (_insideBounds)
     {
         _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1 , transform);
         renderer->addCommand(&_quadCommand);
# if CC_SPRITE_DEBUG_DRAW
         _customDebugDrawCommand.init(_globalZOrder);
         _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this );
         renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
     }
}

从代码1和代码2的对比中,我们很容易就发现2.x版本中的draw函数直接调用OpengGL代码进行图形渲染,而3.x版本中draw的作用是把RenderCommand添加到CommandQueue中,至于这样做的好处是,实际的渲染API进入其中一个与显卡直接交流的有独立线程的RenderQueue。

从Cocos2d-x3.0开始,Cocos2d-x引入了新的渲染流程,它不像2.x版本直接在每一个node中的draw函数中直接调用OpenGL代码进行图形渲染,而是通过各种RenderCommand封装起来,然后添加到一个CommandQueue队列里面去,而现在draw函数的作用就是在此函数中设置好相对应的RenderCommand参数,然后把此RenderCommand添加到CommandQueue中。最后在每一帧结束时调用renderer函数进行渲染,在renderer函数中会根据ID对RenderCommand进行排序,然后才进行渲染。

下面我们来看看图1、图2,这两个图形象地表现了Cocos2d-x3.x下RenderCommand的封装与传递与及RenderCommand的排序。

图1:

Cocos2d-x3.2与OpenGL渲染总结(一)Cocos2d-x3.2的渲染流程_第1张图片


图2:

Cocos2d-x3.2与OpenGL渲染总结(一)Cocos2d-x3.2的渲染流程_第2张图片

上面所说的各个方面都有点零碎,下面就对渲染的整个流程来一个从头到尾的梳理吧。下面是针对3.2版本的,对于2.x版本的梳理不做梳理,因为我用的是3.2版本。

首先,我们Cocos2d-x的执行是通过Application::run()来开始的,如代码3,此代码目录中在xx\cocos2d\cocos\platform\对应平台的目录下,这是与多平台实现有关的类,关于如何实现多平台的编译,你可以参考《cocos2d-x3.2源码分析(一)类FileUtils--实现把资源放在Resources文件目录下达到多平台的引用 》中我对平台编译的分析。以防篇幅过长,只截取了重要部分,如需详解,可以直接查看源码。 

代码3:

?
1
2
3
4
5
6
int Application::run()
{
   ...
   director->mainLoop();
   ...
  }
从代码3中,它明显的启发着我们要继续追寻Director::mainLoop()函数。在Director中mainLoop()为纯函数,此子类DisplayLinkDirector才有其实现,如代码4。

代码4:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void DisplayLinkDirector::mainLoop()
{  
     class = "comment" > //只有一种情况会调用到这里来,就是导演类调用end函数 
     if (_purgeDirectorInNextLoop)
     {
         _purgeDirectorInNextLoop = false ;
         class = "comment" > //清除导演类
         purgeDirector();
     }
     else if (! _invalid)
     {   class = "comment" > //绘制
         drawScene();
         //清除当前内存池中对象,即池中每一个对象--_referenceCount
         PoolManager::getInstance()->getCurrentPool()->clear();
     }
}
mainLoop是主线程调用的循环,其中drawScene()是绘制函数,接着我们继续追寻它的代码,如代码5。

代码5:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
void Director::drawScene()
{
     class = "comment" > //计算间隔时间
     calculateDeltaTime();
     
     //忽略该帧如果时间间隔接近0
     if (_deltaTime < FLT_EPSILON)
     {
         return ;
     }
 
     if (_openGLView)
     {
         _openGLView->pollInputEvents();
     }
 
     //tick before glClear: issue #533
     if (! _paused)
     {
         _scheduler->update(_deltaTime);
         _eventDispatcher->dispatchEvent(_eventAfterUpdate);
     }
 
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
     /* to avoid flickr, nextScene MUST be here: after tick and before draw.
      XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
     if (_nextScene)
     {
         setNextScene();
     }
 
     pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
 
     // draw the scene
     if (_runningScene)
     {
         _runningScene->visit(_renderer, Mat4::IDENTITY, false );
         _eventDispatcher->dispatchEvent(_eventAfterVisit);
     }
 
     // draw the notifications node
     if (_notificationNode)
     {
         _notificationNode->visit(_renderer, Mat4::IDENTITY, false );
     }
 
     if (_displayStats)
     {
         showStats();
     }
 
     _renderer->render();
     _eventDispatcher->dispatchEvent(_eventAfterDraw);
 
     popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
 
     _totalFrames++;
 
     // swap buffers
     if (_openGLView)
     {
         _openGLView->swapBuffers();
     }
 
     if (_displayStats)
     {
         calculateMPF();
     }
}
从代码5中,我们看见visit()和render()函数的调用。其中visit()函数会调用draw()函数来向RenderQueue中添加RenderCommand,那么就继续追寻visit()的代码,如代码6。

代码6:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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* director = Director::getInstance();
     director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
     director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);
     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
         this ->draw(renderer, _modelViewTransform, flags);
 
         for (auto it=_children.cbegin()+i; it != _children.cend(); ++it)
             (*it)->visit(renderer, _modelViewTransform, flags);
     }
     else
     {
         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;
}
从代码6中,我们可以看到“ auto node = _children.at(i);和node->visit(renderer, _modelViewTransform, flags);”,这段代码的意思是先获取子节点,然后递归调用节点的visit()函数,到了没有子节点的节点,开始调用draw()函数。那么我们看看draw()函数代码,如代码7。

代码7:

?
1
2
3
void Node::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags)
{
}
好吧,从代码7中,我们看到Node的draw什么都没有做,是我们找错地方?原来draw()是虚函数,所以它执行时执行的是该字节类的draw()函数。确实是我们找错地方了。那么我们分别看DrawNode::draw()、Sprite::draw()。

代码8:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void DrawNode::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
     _customCommand.init(_globalZOrder);
     _customCommand.func = CC_CALLBACK_0(DrawNode::onDraw, this , transform, flags);
     renderer->addCommand(&_customCommand);
}
 
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
     // Don't do calculate the culling if the transform was not updated
     _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
 
     if (_insideBounds)
     {
         _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1 , transform);
         renderer->addCommand(&_quadCommand);
# if CC_SPRITE_DEBUG_DRAW
         _customDebugDrawCommand.init(_globalZOrder);
         _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this );
         renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
     }
}
从代码8中,我们可以看到在draw()函数向RenderQueue中添加RenderCommand,当然有的类的draw()不是向RenderQueue中添加RenderCommand,而是直接使用OpenGL的API直接进行渲染,或者做一些其他的事情。

那么当draw()都递归调用完了,我们来看看最后进行渲染的Renderer::render() 函数,如代码9。

代码9:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Renderer::render()
{
     //Uncomment this once everything is rendered by new renderer
     //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
     //TODO setup camera or MVP
     _isRendering = true ;
     
     if (_glViewAssigned)
     {
         // cleanup
         _drawnBatches = _drawnVertices = 0 ;
 
         //Process render commands
         //1. Sort render commands based on ID
         for (auto &renderqueue : _renderGroups)
         {
             renderqueue.sort();
         }
         visitRenderQueue(_renderGroups[ 0 ]);
         flush();
     }
     clean();
     _isRendering = false ;
}
从代码9中,我们看到“renderqueue.sort()",这是之前所说的对命令先排序,然后才进行渲染,“visitRenderQueue( _renderGroups[0])”就是来进行渲染的。那么我们接着看看void Renderer::visitRenderQueue(const RenderQueue& queue)的代码,如代码10。

代码10:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void Renderer::visitRenderQueue( const RenderQueue& queue)
{
     ssize_t size = queue.size();
     
     for (ssize_t index = 0 ; index < size; ++index)
     {
         auto command = queue[index];
         auto commandType = command->getType();
         if (RenderCommand::Type::QUAD_COMMAND == commandType)
         {
             flush3D();
             auto cmd = static_cast(command);
             //Batch quads
             if (_numQuads + cmd->getQuadCount() > VBO_SIZE)
             {
                 CCASSERT(cmd->getQuadCount()>= 0 && cmd->getQuadCount() < VBO_SIZE, "VBO is not big enough for quad data, please break the quad data down or use customized render command" );
                 
                 //Draw batched quads if VBO is full
                 drawBatchedQuads();
             }
             
             _batchedQuadCommands.push_back(cmd);
             
             memcpy(_quads + _numQuads, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
             convertToWorldCoordinates(_quads + _numQuads, cmd->getQuadCount(), cmd->getModelView());
             
             _numQuads += cmd->getQuadCount();
 
         }
         else if (RenderCommand::Type::GROUP_COMMAND == commandType)
         {
             flush();
             int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
             visitRenderQueue(_renderGroups[renderQueueID]);
         }
         else if (RenderCommand::Type::CUSTOM_COMMAND == commandType)
         {
             flush();
             auto cmd = static_cast(command);
             cmd->execute();
         }
         else if (RenderCommand::Type::BATCH_COMMAND == commandType)
         {
             flush();
             auto cmd = static_cast(command);
             cmd->execute();
         }
         else if (RenderCommand::Type::MESH_COMMAND == commandType)
         {
             flush2D();
             auto cmd = static_cast(command);
             if (_lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID())
             {
                 flush3D();
                 cmd->preBatchDraw();
                 cmd->batchDraw();
                 _lastBatchedMeshCommand = cmd;
             }
             else
             {
                 cmd->batchDraw();
             }
         }
         else
         {
             CCLOGERROR( "Unknown commands in renderQueue" );
         }
     }
}
从代码10中,我们看到RenderCommand类型有QUAD_COMMAND,CUSTOM_COMMAND,BATCH_COMMAND,

GROUP_COMMAND,MESH_COMMAND五种,这些类型的讲解在下一节。

从代码10中,好像没有与OpenGL相关的代码,有点囧。其实这OpenGL的API调用是在Renderer::drawBatched

Quads()、BatchCommand::execute()中。在代码10中,我们也看到在QUAD_COMMAND类型中调用了drawBatchedQuads(),如代码11。在CUSTOM_COMMAND中调用了CustomCommand::execute(),如代码12。在BATCH_COMMAND中调用了BatchCommand::execute(),如代码13。在MESH_COMMAND类型中调用了MeshCommand::preBatchDraw()和MeshCommand::batchDraw()。至于GROUP_COMMAND类型,就递归它组里的成员。

代码11:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
void Renderer::drawBatchedQuads()
{
     //TODO we can improve the draw performance by insert material switching command before hand.
 
     int quadsToDraw = 0 ;
     int startQuad =

你可能感兴趣的:(Cocos2d-x3.2与OpenGL渲染总结(一)Cocos2d-x3.2的渲染流程)