其作用是遍历整个场景渲染树。
部分代码如下
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);
}
如果子节点不为空,那么就对子节点进行排序。排序算法如下:
static void sortNodes(cocos2d::Vector<_T*>& nodes)
{
static_assert(std::is_base_of<Node, _T>::value, "Node::sortNodes: Only accept derived of Node!");
#if CC_64BITS
std::sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
return (n1->_localZOrderAndArrival < n2->_localZOrderAndArrival);
});
#else
std::stable_sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
return n1->_localZOrder < n2->_localZOrder;
});
#endif
}
我们对localZOrder 很熟悉,但是对localZOrderAndArrival就可能就会懵。实际上,localZOrderAndArrival在addChild的时候就会生成一个,在前面addChild的时候,生成的会小于后面addChild的。
排完序之后,对Node继续进行遍历,这里会优先遍历localZOrder小于0的子节点,然后调用visit函数递归遍历。
所以这里的遍历顺序就是,小于0的子节点 > 父节点本身 > 大于0的子节点
遍历完之后,调用draw函数。
Node的draw函数是空的。一般都是子类进行实现自己的draw函数。
举个简单的例子,Spirte的draw函数。
只看最重要的
_trianglesCommand.init(_globalZOrder,
_texture,
getGLProgramState(),
_blendFunc,
_polyInfo.triangles,
transform,
flags);
renderer->addCommand(&_trianglesCommand);
这是cocos2dx 3.x改变最大的地方,draw函数只进行RenderCommon的生成。将生成好的命令存储到render中。
还记得前面调用场景的visit函数之后,调用了render函数么?
它在 CCRender这个类中。
_isRendering = true;
if (_glViewAssigned)
{
//Process render commands
//1. Sort render commands based on ID
for (auto &renderqueue : _renderGroups)
{
renderqueue.sort();
}
visitRenderQueue(_renderGroups[0]);
}
clean();
_isRendering = false;
_renderGroups中存储的就是需要发送给OpenGL进行渲染的指令集合。这里会先进行一次排序。(这个排序很关键,这也是决定了为啥有些不能合批的原因。)
void RenderQueue::sort()
{
// Don't sort _queue0, it already comes sorted
std::stable_sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
std::stable_sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]), std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]), compareRenderCommand);
std::stable_sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]), std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]), compareRenderCommand);
}
排序的方法很简单。
TRANSPARENT_3D是3D的,根据景深来,这里不谈。
主要是下面
static bool compareRenderCommand(RenderCommand* a, RenderCommand* b)
{
return a->getGlobalOrder() < b->getGlobalOrder();
}
这里可以看得出,是根据globalZOrder来的。
globalZOrder是个好东西,也是个坏东西。他可以决定场景中的渲染先后顺序,也就决定了谁在前,谁在后。
可能有人会说,localZOrder不也是这样么?
localZOrder只是在同一个父节点上决定渲染先后顺序,它会受父节点的影响。
globalZOrder则是决定OpenGL的渲染先后顺序,也就是不管父节点是谁,它会凌驾于其他比他低的上面。
我们一般globalZOrder都是设置为0,那么设置为0,这里就不会进行排序,那么节点渲染的顺序就是之前加入RenderCommon的顺序,也就是之前对子节点排序的顺序。
遍历渲染队列。
怎么遍历的呢?
根据上面的顺序,会依次进入processRenderCommand函数
在说这个函数之前,我们要了解cocos2dx的几种渲染命令。
其功能不是绘制三角形,比如我们的2D图片、Sprite类等都是用这个绘制。
这个命令有一个非常大的特点,也就是合批渲染。
合批渲染,本质上来说就是将符合要求的渲染命令合并成一个OpenGL Draw Call的调用。
每次渲染,都会将渲染命令发送给OpenGL进行渲染,每一次调用就会使得Draw Call +1。而Draw Call越高,画面掉帧越厉害。
if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
{
// flush other queues
flush3D();
auto cmd = static_cast<TrianglesCommand*>(command);
// flush own queue when buffer is full
if(_filledVertex + cmd->getVertexCount() > VBO_SIZE || _filledIndex + cmd->getIndexCount() > INDEX_VBO_SIZE)
{
CCASSERT(cmd->getVertexCount()>= 0 && cmd->getVertexCount() < VBO_SIZE, "VBO for vertex is not big enough, please break the data down or use customized render command");
CCASSERT(cmd->getIndexCount()>= 0 && cmd->getIndexCount() < INDEX_VBO_SIZE, "VBO for index is not big enough, please break the data down or use customized render command");
drawBatchedTriangles();
}
// queue it
_queuedTriangleCommands.push_back(cmd);
_filledIndex += cmd->getIndexCount();
_filledVertex += cmd->getVertexCount();
}
drawBatchedTriangles 函数有点多,先看主要的部分
// in the same batch ?
if (batchable && (prevMaterialID == currentMaterialID || firstCommand))
{
CC_ASSERT(firstCommand || _triBatchesToDraw[batchesTotal].cmd->getMaterialID() == cmd->getMaterialID() && "argh... error in logic");
_triBatchesToDraw[batchesTotal].indicesToDraw += cmd->getIndexCount();
_triBatchesToDraw[batchesTotal].cmd = cmd;
}
else
{
// is this the first one?
if (!firstCommand) {
batchesTotal++;
_triBatchesToDraw[batchesTotal].offset = _triBatchesToDraw[batchesTotal-1].offset + _triBatchesToDraw[batchesTotal-1].indicesToDraw;
}
_triBatchesToDraw[batchesTotal].cmd = cmd;
_triBatchesToDraw[batchesTotal].indicesToDraw = (int) cmd->getIndexCount();
// is this a single batch ? Prevent creating a batch group then
if (!batchable)
currentMaterialID = -1;
}
这里,就可以看的出,从队列中取出第一条指令,放入 _triBatchesToDraw 中。后续,会判断下一条的指令的ID是否和上一个相同,如果相同,那么就继续放入 _triBatchesToDraw中一个序列中。
/************** 3: Draw *************/
for (int i=0; i<batchesTotal; ++i)
{
CC_ASSERT(_triBatchesToDraw[i].cmd && "Invalid batch");
_triBatchesToDraw[i].cmd->useMaterial();
glDrawElements(GL_TRIANGLES, (GLsizei) _triBatchesToDraw[i].indicesToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (_triBatchesToDraw[i].offset*sizeof(_indices[0])) );
_drawnBatches++;
_drawnVertices += _triBatchesToDraw[i].indicesToDraw;
}
这就是合批渲染的原理。
那么可以看得出,要合批渲染降低drawcall的条件是。
相邻的命令好理解,也就是localZOrder的顺序或者globalZOrder的顺序。
相同的ID呢?
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 {
GLuint textureId;
GLenum blendSrc;
GLenum blendDst;
void* glProgramState;
} hashMe;
hashMe.textureId = _textureID;
hashMe.blendSrc = _blendType.src;
hashMe.blendDst = _blendType.dst;
hashMe.glProgramState = _glProgramState;
_materialID = XXH32((const void*)&hashMe, sizeof(hashMe), 0);
}
该函数就是ID的生成函数,或者说获取函数。
从该函数可以得知
所以,要想能合批渲染,那么就必须满足上面的条件。
MeshCommand用于3D网格绘制,类CCSprite3D、Particle3DquadRender等等使用了MeshCommand绘制。
MeshCommand绘制可以分为两种,一种是绘制建模生成的3D模型,另一种是直接使用glDrawElements直接绘制。
不作详细说明。有兴趣可以深入研究下。
GroupCommand本身并不绘制任何东西,GroupCommand是用于创建渲染分支,使得某些特殊的绘制可以单独设置绘制状态,不影响主渲染分支。类ClippingNode、RenderTexture、NodeGrid等待使用了GroupCommand。
Renderer类中的 _renderGroups 数组支持多个渲染队列,_renderGroups[0]是主渲染队列,其他为渲染分支。
else if(RenderCommand::Type::GROUP_COMMAND == commandType)
{
flush();
int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
CCGL_DEBUG_PUSH_GROUP_MARKER("RENDERER_GROUP_COMMAND");
visitRenderQueue(_renderGroups[renderQueueID]);
CCGL_DEBUG_POP_GROUP_MARKER();
}
可以看得出,其实就是递归调用visitRenderQueue函数进行渲染。
CustomCommand是所有命令中最简单的一个,也是最灵活的一个,绘制的内容和方式完全交由我们自己决定。LayerColor、DrawNode、Skybox等待都是使用CustomCommand命令进行绘制的。
else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
{
flush();
auto cmd = static_cast<CustomCommand*>(command);
CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_CUSTOM_COMMAND");
cmd->execute();
}
//std::function func; execute
调用命令自己的execute函数进行渲染。
BatchCommand可以同时绘制同一纹理的多个小图,用于2D绘制,类SpriteBatchNode和类ParticleBatchNode使用了BatchCommand减少OpenGL的调用
BatchCommand绘制主要由类TextureAtlas实现,TextureAtlas::drawQuads可以一次绘制多个使用同一纹理的矩形,Renderer处理BatchCommand也只是执行TextureAtlas::drawQuads函数
else if(RenderCommand::Type::BATCH_COMMAND == commandType)
{
flush();
auto cmd = static_cast<BatchCommand*>(command);
CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_BATCH_COMMAND");
cmd->execute();
}
void BatchCommand::execute()
{
// Set material
_shader->use();
_shader->setUniformsForBuiltins(_mv);
GL::bindTexture2D(_textureID);
GL::blendFunc(_blendType.src, _blendType.dst);
// Draw
_textureAtlas->drawQuads();
}
TMXLayer瓦片地图类使用了PrimitiveCommand进行绘制