cocos2dx渲染架構
2dx的時代UI樹便利和渲染是沒有分開的,遍歷UI樹的時候就渲染.3dx版本為了分離了ui樹的遍歷和渲染,先遍歷生成渲染命令發到渲染佇列,之後遍歷渲染命令佇列開始渲染.這樣做的好處是渲染命令可以重用,單獨的渲染可以做優化例如自動批繪製.本篇首先介紹cocos2D-X 3.x版本的渲染結構,之後會深入opengl es.
mainLoop
void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { //只有一種情況會呼叫到這裡來,就是導演類呼叫end函式 _purgeDirectorInNextLoop = false; //清除導演類 purgeDirector(); } else if (! _invalid) { //繪製 drawScene(); //清除記憶體 PoolManager::getInstance()->getCurrentPool()->clear(); } }
分析的起點是mainLoop函式,這是在主執行緒裡面會呼叫的迴圈,其中drawScene函式進行繪製。那麼就進一步來看drawScene函式。mainLoop實在opengl的ondrawframe呼叫過來的即平臺每幀渲染會呼叫.
drawScene
void Director::drawScene() { //計算間隔時間 calculateDeltaTime(); //如果間隔時間過小會被忽略 if(_deltaTime < FLT_EPSILON){ return;} //空函式,也許之後會有作用 if (_openGLView) { _openGLView->pollInputEvents(); } //非暫停狀態 if (! _paused) { //scheduler更新 會使actionmanager更新和相關的schedule更新 引擎物理模擬都是在繪製之前做的 _scheduler->update(_deltaTime); _eventDispatcher->dispatchEvent(_eventAfterUpdate); } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //切換下一場景,必須放在邏輯後繪製前,否則會出bug if (_nextScene) { setNextScene(); } kmGLPushMatrix(); //建立單位矩陣 kmMat4 identity; kmMat4Identity(&identity); //繪製場景 if (_runningScene) { //遞迴的遍歷scene中的每個node的visit生成渲染命令放入渲染佇列 _runningScene->visit(_renderer, identity, false); _eventDispatcher->dispatchEvent(_eventAfterVisit); } //繪製觀察節點,如果你需要在場景中設立觀察節點,請呼叫攝像機的setNotificationNode函式 if (_notificationNode) { _notificationNode->visit(_renderer, identity, false);//這是一個常駐節點 } //繪製螢幕左下角的狀態 if (_displayStats) { showStats(); } //渲染 _renderer->render(); //渲染後 _eventDispatcher->dispatchEvent(_eventAfterDraw); kmGLPopMatrix(); _totalFrames++; if (_openGLView) { _openGLView->swapBuffers(); //交換緩衝區 } //計算繪製時間 if (_displayStats) { calculateMPF(); } }
其中和繪製相關的是visit的呼叫和render的呼叫,其中visit函式會呼叫節點的draw函式,在3.x之前的版本中draw函式就會直接呼叫繪製程式碼,3.x版本是在draw函式中生成將繪製命令放入到renderer佇列中,然後renderer函式去進行真正的繪製,首先來看sprite的draw函式.
渲染命令
void Sprite::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) { //檢查是否超出邊界,自動裁剪 _insideBounds = transformUpdated ? renderer->checkVisibility(transform, _contentSize) : _insideBounds; if(_insideBounds) { //初始化 _quadCommand.init(_globalZOrder, _texture->getName(), _shaderProgram, _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 } }
這裡面用了兩種不同的繪製命令quadCommand初始化後就可以加入到繪製命令中,customDebugDrawCommand傳入了一個回撥函式,具體的命令種類會在後面介紹。其中自定義的customDebugDrawCommand命令在初始化的時候只傳入了全域性z軸座標,因為它的繪製函式全部都在傳入的回撥函式裡面,_quadCommand則需要傳入全域性z軸座標,貼圖名稱,shader,混合,座標點集合,座標點集個數,變換。
Render
void Renderer::render()
{
_isRendering = true;
if (_glViewAssigned)
{
//清除
_drawnBatches = _drawnVertices = 0;
//排序
for (auto &renderqueue : _renderGroups)
{
renderqueue.sort();
}
//繪製
visitRenderQueue(_renderGroups[0]);
flush();
}
clean();
_isRendering = false;
}
Render類中的render函式進行真正的繪製,首先排序,再進行繪製,從列表中的第一個組開始繪製。在visitRenderQueue函式中可以看到五種不同型別的繪製命令型別,分別對應五個類,這五個類都繼承自RenderCommand。
繪製命令
QUAD_COMMAND:
QuadCommand類繪製精靈等。所有繪製圖片的命令都會呼叫到這裡,處理這個型別命令的程式碼就是繪製貼圖的openGL程式碼,
CUSTOM_COMMAND:
自定義繪製,自己定義繪製函式,在呼叫繪製時只需呼叫已經傳進來的回撥函式就可以,裁剪節點,繪製圖形節點都採用這個繪製,把繪製函式定義在自己的類裡。這種型別的繪製命令不會在處理命令的時候呼叫任何一句openGL程式碼,而是呼叫你寫好並設定給func的繪製函式,並自己實現一個自定義的繪製。
BATCH_COMMAND:
批處理繪製,批處理精靈和粒子,其實它類似於自定義繪製,也不會再render函式中出現任何一句openGL函式,它呼叫一個固定的函式。
GROUP_COMMAND:
繪製組,一個節點包括兩個以上繪製命令的時候,把這個繪製命令儲存到另外一個renderGroups中的元素中,並把這個元素的指標作為一個節點儲存到renderGroups[0]中。
render流程
void Renderer::addCommand(RenderCommand* command)
{
//獲得棧頂的索引
int renderQueue =_commandGroupStack.top();
//呼叫真正的addCommand
addCommand(command, renderQueue);
}
void Renderer::addCommand(RenderCommand* command, int renderQueue)
{
//將命令加入到陣列中
_renderGroups[renderQueue].push_back(command);
}
addCommand它是獲得需要把命令加入到renderGroups位置中的索引,這個索引是從commandGroupStack獲得的,commandGroupStack是個棧,當我們建立一個GROUP_COMMAND時,需要呼叫pushGroup函式,它是把當前這個命令在_renderGroups的索引位置壓到棧頂,當addCommand時,呼叫top,獲得這個位置
groupCommand.init(globalZOrder);
renderer->addCommand(&_groupCommand);
renderer->pushGroup(_groupCommand.getRenderQueueID());
GROUP_COMMAND一般用於繪製的節點有一個以上的繪製命 令,把這些命令組織在一起,無需排定它們之間的順序,他們作為一個整體被呼叫,所以一定要記住,棧是push,pop對應的,關於這個節點的所有的繪製命令被新增完成後,請呼叫pop,將這個值從棧頂彈出,否則後面的命令也會被新增到這裡。
為什麼呼叫的起始只需呼叫為什麼只是0,其他的呢?
visitRenderQueue(_renderGroups[0]);
它們會在處理GROUP_COMMAND被呼叫
else if(RenderCommand::Type::GROUP_COMMAND == commandType) {
flush();
int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
visitRenderQueue(_renderGroups[renderQueueID]);
}