原文:http://www.2cto.com/kf/201409/336234.html
最近几天,我都在学习如何在Cocos2d-x3.2中使用OpenGL来实现对图形的渲染。在网上也看到了很多好的文章,我在它们的基础上做了这次的我个人认为比较完整的总结。当你了解了Cocos2d-x3.2中对图形渲染的流程,你就会觉得要学会写自己的shader才是最重要的。
在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"
);
}
|
代码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:
图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();
...
}
|
代码4:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void
DisplayLinkDirector::mainLoop()
{
if
(_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop =
false
;
purgeDirector();
}
else
if
(! _invalid)
{
drawScene();
//清除当前内存池中对象,即池中每一个对象--_referenceCount
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
|
代码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()
{
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();
}
}
|
代码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;
}
|
代码7:
1
2
3
|
void
Node::draw(Renderer* renderer,
const
Mat4 &transform, uint32_t flags)
{
}
|
代码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
}
}
|
那么当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
;
}
|
代码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
//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
cmd->execute();
}
else
if
(RenderCommand::Type::BATCH_COMMAND == commandType)
{
flush();
auto cmd = static_cast
cmd->execute();
}
else
if
(RenderCommand::Type::MESH_COMMAND == commandType)
{
flush2D();
auto cmd = static_cast
if
(_lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID())
{
flush3D();
cmd->preBatchDraw();
cmd->batchDraw();
_lastBatchedMeshCommand = cmd;
}
else
{
cmd->batchDraw();
}
}
else
{
CCLOGERROR(
"Unknown commands in renderQueue"
);
}
}
}
|
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 =
|