渲染顺序
Cocos2dx的渲染过程可以类比于油画的绘制,后渲染的总是会盖在先渲染的上层。所以顺序的重要性不言而喻。
根节点是Director 中的 _runningScene,由Scene::Visit()开始 收集整个场景的绘制命令,visit()的代码实现是在基类Node中的,核心代码是:
int i = 0;
if(!_children.empty())
{
//有子节点时,先对所有的子节点排序,先绘制_localZOrder 小于0的子节点 然后绘制自身,最后绘制其他节点
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);
}
这里的绘制顺序类似于2叉树的中序遍历。
_localZOrder 的初始值为0,也就是说默认情况下,先绘制父节点,后绘制子节点。深挖一下sortAllChildren()调用的排序逻辑,会发现的排序中还用到一个_orderOfArrival :
std::sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
return (n1->_localZOrder == n2->_localZOrder && n1->_orderOfArrival < n2->_orderOfArrival) || n1->_localZOrder < n2->_localZOrder;
});
上述逻辑表示子节点的排序是优先按照_localZOrder 进行升序排列,在_localZOrder相同时再按照_orderOfArrival 进行升序排列。
_orderOfArrival 的值是通过调用下边这个函数来赋值,其中s_globalOrderOfArrival是一个全局自增变量。
void Node::updateOrderOfArrival() {
_orderOfArrival = (++s_globalOrderOfArrival);
}
这个函数主要是在自身添加到父节点上调用的(还有一处是reorderChild())。
做一下总结:
- 默认先绘制自身,然后绘制子节点,先添加的子节点先绘制
- 可以通过修改_localZOrder 来改变自身的绘制顺序,值越小越先绘制,当值小于0时会比父节点要先绘制即在父节点下层。
绘制命令
cocos先遍历所有的节点,将绘制命令收集在一个 RenderQueue 中。绘制命令的类型有以下几种:
/**Enum the type of render command. */
enum class Type
{
/** Reserved type.*/
UNKNOWN_COMMAND,
/** Quad command, used for draw quad.*/
QUAD_COMMAND,
/**Custom command, used for calling callback for rendering.*/
CUSTOM_COMMAND,
/**Batch command, used for draw batches in texture atlas.*/
BATCH_COMMAND,
/**Group command, which can group command in a tree hierarchy.*/
GROUP_COMMAND,
/**Mesh command, used to draw 3D meshes.*/
MESH_COMMAND,
/**Primitive command, used to draw primitives such as lines, points and triangles.*/
PRIMITIVE_COMMAND,
/**Triangles command, used to draw triangles.*/
TRIANGLES_COMMAND
};
像常用的Sprite 使用的是TRIANGLES_COMMAND,对应的类是 TrianglesCommand。在对cocos的性能优化中很重要的一条就是DrawCall的优化。减少DrawCall常用且最有效的手段就是合批,即将一些满足一定条件的绘制命令的数据收集起来,然后再调用绘制的接口。还是以TrianglesCommand为例, 它会根据自身的部分数据生成一个_materialID,_materialID相同的TrianglesCommand能够合批渲染
void TrianglesCommand::generateMaterialID()
{
// glProgramState is hashed because it contains:
// * uniforms/values
// * glProgram
//
// we safely can when the same glProgramState is being used then they share those states
// if they don't have the same glProgramState, they might still have the same
// uniforms/values and glProgram, but it would be too expensive to check the uniforms.
struct {
void* glProgramState;
GLuint textureId;
GLenum blendSrc;
GLenum blendDst;
} hashMe;
// NOTE: Initialize hashMe struct to make the value of padding bytes be filled with zero.
// It's important since XXH32 below will also consider the padding bytes which probably
// are set to random values by different compilers.
memset(&hashMe, 0, sizeof(hashMe));
hashMe.textureId = _textureID;
hashMe.blendSrc = _blendType.src;
hashMe.blendDst = _blendType.dst;
hashMe.glProgramState = _glProgramState;
_materialID = XXH32((const void*)&hashMe, sizeof(hashMe), 0);
}
cocos2dx 的 Renderer 在处理TrianglesCommand时,会先将连续的TrianglesCommand缓存起来,自动对其中 _materialID 连续相同的命令进行合批 (主要代码位于Renderer::drawBatchedTriangles()函数中)。注意这里的两个粗体的连续,举个例子:
RenderQueue = [ //原始的队列
1 TrianglesCommand(_materialID1,data1),
2 TrianglesCommand(_materialID2,data2),
3 TrianglesCommand(_materialID1,data3),
4 OtherCommand(data4),
5 TrianglesCommand(_materialID1,data5),
6 TrianglesCommand(_materialID1,data6),
7 TrianglesCommand(_materialID1,data7),
8 TrianglesCommand(_materialID3,data8),
9 OtherCommand(data9),
... ...
]
TrianglesCacheQueue = [] //三角形绘制命令的缓存
TrianglesDrawQueue = [] //三角形实际的绘制命令
开始遍历RenderQueue:
1、2、3 是TrianglesCommand 按顺序放入缓存
到4时发现不再是TrianglesCommand,此时先处理 TrianglesCacheQueue =[
1 TrianglesCommand(_materialID1,data1),
2 TrianglesCommand(_materialID2,data2),
3 TrianglesCommand(_materialID1,data3),
]
遍历发现没有连续相同的_materialID,即不能合批
TrianglesDrawQueue = TrianglesCacheQueue
逐条执行TrianglesDrawQueue中的绘制命令
执行4 OtherCommand的绘制
继续往后,5、6、7、8 是TrianglesCommand 按顺序放入缓存
到9时发现不再是TrianglesCommand,此时先处理 TrianglesCacheQueue =[
1 TrianglesCommand(_materialID1,data5),
2 TrianglesCommand(_materialID1,data6),
3 TrianglesCommand(_materialID1,data7),
4 TrianglesCommand(_materialID3,data8),
]
遍历发现前三条可以合批
TrianglesDrawQueue = [
1 TrianglesCommand(_materialID1,data5 + data6 + data7),
2 TrianglesCommand(_materialID3,data8),
]
继续处理后续的命令 ... ...
DrawCall优化
了解上文这些逻辑后,我们知道可以利用自动合批来降低DrawCall,除了在开发中刻意避免打断合批外,使用合图(将同一图层中的精灵图像打包成一个大的图集)是一个常用的技巧。
此外,我们还可以在游戏开发中针对性的进行手动合批:
类背包界面的 DrawCall优化 TODO