環境: 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<_T*>& nodes) { static_assert(std::is_base_of<Node, _T>::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<CustomCommand*>(command); cmd->execute(); } else if(RenderCommand::Type::BATCH_COMMAND == commandType) { flush(); auto cmd = static_cast<BatchCommand*>(command); cmd->execute(); } else if(RenderCommand::Type::PRIMITIVE_COMMAND == commandType) { flush(); auto cmd = static_cast<PrimitiveCommand*>(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 };
...