环境: cocos3.10 Xcode
UI元素的渲染流程图示:
1. 从main进入到Application:run中,该方法下有个while循环,用于处理设定的每帧(FPS)刷新相关
{// ...
glview->retain();
while (!glview->windowShouldClose()) {
director->mainLoop();
}
return 0;
}
2. mainLoop做的事情如下:
void Director::mainLoop()
{
if (! _invalid)
{
// 绘制场景
drawScene();
// 自动内存释放相关,每帧结束后,检测释放掉内存池中引用计数为1的节点
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
3.绘制场景drawScene的主要代码:
void Director::drawScene()
{
// 渲染当前运行场景
if (_runningScene)
{
//clear draw stats
_renderer->clearDrawStats();
//render the scene
_openGLView->renderScene(_runningScene, _renderer);
_eventDispatcher->dispatchEvent(_eventAfterVisit);
}
}
4. 通过renderScene会进入到Scene::render()中,它是处理渲染相关的主要接口:
void Scene::render(Renderer* renderer, const Mat4* eyeTransforms, const Mat4* eyeProjections, unsigned int multiViewCount)
{
auto director = Director::getInstance();
Camera* defaultCamera = nullptr;
// 转换坐标系,将ui元素相对坐标转换为世界坐标,原因在于OpenGL ES坐标系与cocos世界坐标系一致
const auto& transform = getNodeToParentTransform();
for (const auto& camera : getCameras())
{
if (!camera->isVisible())
continue;
Camera::_visitingCamera = camera;
if (Camera::_visitingCamera->getCameraFlag() == CameraFlag::DEFAULT)
{
defaultCamera = Camera::_visitingCamera;
}
// 遍历场景中UI树,会进入到Node::visit中,进行UI树遍历
visit(renderer, transform, 0);
// 绘制,会进入到Renderer::render中,执行绘制
renderer->render();
//
camera->restore();
//
for (unsigned int i = 0; i < multiViewCount; ++i)
director->popProjectionMatrix(i);
}
Camera::_visitingCamera = nullptr;
}
5. visit主要用于UI树的遍历,遍历的规则使用的是中序(in-order)深度优先算法。即:
1. 遍历左边的子节点(小于0)
2. 遍历根节点(等于0)
3. 遍历右边的子节点(大于0)
void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
// 如果不可见,则不再绘制
if (!_visible)
{
return;
}
bool visibleByCamera = isVisitableByVisitingCamera();
int i = 0;
if(!_children.empty())
{
// 按照localZOrder排序,若相同则按照UI树顺序排序
sortAllChildren();
// 遍历 localZOrder < 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;
}
// 遍历自身
if (visibleByCamera)
this->draw(renderer, _modelViewTransform, flags);
// 遍历localZOrder > 0
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);
}
而遍历的方法主要通过sortAllChildren方法实现,比较的是localZOrder。
// sortAllChildren
void Node::sortAllChildren()
{
if (_reorderChildDirty)
{
sortNodes(_children);
_reorderChildDirty = false;
}
}
// sortNodes
static void sortNodes(cocos2d::Vector& nodes)
{
static_assert(std::is_base_of::value, "Node::sortNodes: Only accept derived of Node!");
// 按照localZOrder排序
std::stable_sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
return n1->_localZOrder < n2->_localZOrder;
});
}
6. 在visit排序遍历中,会依次根据localZOrder依次执行draw, 我们以Sprite为例:
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绘制栈RenderQueue中
renderer->addCommand(&_trianglesCommand);
}
}
7. draw并未执行绘制,而是生成了RenderCommand绘制命令,放置到了RenderQueue中。
void Renderer::addCommand(RenderCommand* command)
{
int renderQueue =_commandGroupStack.top();
addCommand(command, renderQueue);
}
8. UI树遍历结束后,会生成一系列的绘制命令,此时Renderer::render()开始对绘制命令进行排序,绘制。
这样做的目地主要使得渲染系统可以对绘制做一些优化,比如:使用相同纹理的Command可执行自动批绘制。
void Renderer::render()
{
_isRendering = true;
if (_glViewAssigned)
{
for (auto &renderqueue : _renderGroups)
{
// 执行遍历,遍历的是globalZOrder相关
renderqueue.sort();
}
visitRenderQueue(_renderGroups[0]);
}
clean();
_isRendering = false;
}
关于其遍历sort的实现看下:
void RenderQueue::sort()
{
// 3D相关
std::sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
// GLOBALZ_NEG 表示globalZOrder < 0
std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]), std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]), compareRenderCommand);
// GLOBALZ_POS 表示globalZOrder > 0
std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]), std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]), compareRenderCommand);
}
static bool compareRenderCommand(RenderCommand* a, RenderCommand* b)
{
return a->getGlobalOrder() < b->getGlobalOrder();
}
在这里,我们可以明白,RenderQueue对RenderCommand下globalZOrder不为0的,又执行了排序。我们可以总结到:
1. 过多的使用globalZOrder会影响UI元素的绘制性能
2. UI元素的绘制顺序并非仅受localZOrder的影响,也会受到globalZOrder的影响
9. 通过 visitRenderQueue 这一步会进入到渲染流程,而最终执行到的代码为:processRenderCommand
void Renderer::processRenderCommand(RenderCommand* command)
{
auto commandType = command->getType();
if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
{
// ...
}
else if (RenderCommand::Type::MESH_COMMAND == commandType)
{
flush2D();
// ...
}
else if(RenderCommand::Type::GROUP_COMMAND == commandType)
{
flush();
int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
CCGL_DEBUG_POP_GROUP_MARKER();
}
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::PRIMITIVE_COMMAND == commandType)
{
flush();
auto cmd = static_cast(command);
cmd->execute();
}
}
可以看到,更具渲染类型不同,会调用不同的渲染方法等。其相关命令主要有:
enum class Type
{
/** Reserved type.*/
UNKNOWN_COMMAND,
// 用于绘制1个或多个矩形区域,比如Sprite, ParticleSystem
QUAD_COMMAND,
/**Custom command, used for calling callback for rendering.*/
CUSTOM_COMMAND,
/*
用来绘制一个TextureAtlas, 比如Label, TileMap等
而对于TextureAtlas来说,它主要用于对同一纹理下多个精灵的封装
*/
BATCH_COMMAND,
// 用来包装多个RenderCommand,而其中的RenderCommand不会参与全局排序,多用于ClippingNode,RenderTexture等
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
};
