Cocos2d-x 渲染器Renderer與6種RenderCommand詳解
Cocos2d-x 渲染器Renderer
宣告:本文使用的是cocos2d-x-3.17的程式碼
在《Cocos場景遍歷與渲染》中已講解了Cocos2d 在渲染時會先遍歷場景,遍歷時會生成渲染命令,渲染器再處理渲染命令繪製出圖形。這篇文章主要是講解渲染器如何處理渲染命令。
渲染佇列RenderQueue
RenderQueue
遍歷場景時會生成渲染命令,生成的渲染命令儲存在渲染佇列中。渲染佇列RenderQueue會將命令分層5類儲存,分別是:GLOBALZ_NEG、OPAQUE_3D、TRANSPARENT_3D、GLOBALZ_ZERO、GLOBALZ_POS。這5種命令儲存的是:
GLOBALZ_NEG: globalZ< 0的命令
OPAQUE_3D:globalZ=0的不透明3D命令
TRANSPARENT_3D:globalZ=0的3D透明命令
GLOBALZ_ZERO:globalZ=0的2D命令
GLOBALZ_POS:globalZ>0的命令
其中globalZ在生成命令時使用的是節點的Node::getGlobalZOrder賦值,也就是節點的_globalZOrder。globalZ=0的命令分成了三種首先按3D、2D進行分類,然後3D還分成是否透明。這樣分類的原因是:2D、3D在顯示畫面先後順序時,2D一般由繪製的先後順序決定,而3D則會使用深度快取;3D 分成是否透明是因為透明物體顯示跟繪製命令的先後順序有關(具體參考《OpenGL程式設計指南(第八版)》11.4.1順序無關透明),所以3D透明佇列再繪製前需要按照繪製物體的深度進行排序。
命令排序:GLOBALZ_NEG、GLOBALZ_POS、TRANSPARENT_3D命令在繪製前會進行排序,GLOBALZ_NEG、GLOBALZ_POS會根據globalZ的值從小到大排序,TRANSPARENT_3D會根據繪製物體的深度(離攝像機的遠近)進行排序。
RenderGroups
Renderer類種並不是只有一個RenderQueue,而有一個RenderQueue的陣列_renderGroups,之所以這樣是因為渲染有時某些命令需要單獨設定一些狀態,例如使用模板快取實現“鏤空”繪製等,這時就需要一個單獨的RenderQueue去建立一個渲染分支實現,以防止影響全域性繪製。
渲染都是從_renderGroups[0]開始繪製,如果不使用渲染分支需要使用也就只會用到_renderGroups[0]一個RenderQueue。如果想使用其他的RenderQueue建立渲染分支,需要使用GroupCommand渲染命令。
處理渲染命令
處理步驟如下:
- 對_renderGroups的每個RenderQueue中的命令進行排序
- 使用函式visitRenderQueue訪問_renderGroups[0]中的命令,訪問順序為GLOBALZ_NEG、OPAQUE_3D、TRANSPARENT_3D、GLOBALZ_ZERO、GLOBALZ_POS。訪問每個渲染命令陣列訪問時會先設定好OpenGL的狀態,然後再用函式processRenderCommand處理渲染命令。
- processRenderCommand處理渲染,按渲染命令型別對麼每個命令進行渲染
渲染命令
一共有6種渲染命令,每種命令型別對應一個類,這些類都繼承至RenderCommand類,對應關係以及作用如下:
TRIANGLES_COMMAND:TrianglesCommand,渲染三角形,可以合併命令減少OpenGL的呼叫提高渲染效率
MESH_COMMAND:MeshCommand,渲染3D
GROUP_COMMAND:GroupCommand,建立渲染分支,使用_renderGroups[0]之外的RenderQueue
CUSTOM_COMMAND:CustomCommand,自定義渲染命令
BATCH_COMMAND:BatchCommand,同時渲染多個使用同一紋理的圖形,提高渲染效率
PRIMITIVE_COMMAND:PrimitiveCommand,渲染自定義圖元。
接下來由易到難對每種命令單獨講解,並用Node類實現相應的命令,因為渲染跟OpenGL緊密相關,這一塊需要OpenGL的知識才能完全理解。
CustomCommand
CustomCommand是所有命令中最簡單的一個,也是最靈活的一個,繪製的內容和方式完全交由我們自己決定。LayerColor、DrawNode、Skybox等待都是使用CustomCommand命令進行繪製的。
CustomCommand有一個std::function<void()> func變數,當Renderer處理CustomCommand時只是簡單的呼叫func函式,以下是Renderer中的程式碼:
else if(RenderCommand::Type::CUSTOM_COMMAND == commandType) { flush(); auto cmd = static_cast<CustomCommand*>(command); CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_CUSTOM_COMMAND"); cmd->execute(); } void CustomCommand::execute() { if(func) { func(); } } |
所以使用CustomCommand需要我們自己寫好繪製函式,賦值給CustomCommand.func。例,完整程式碼參考(CustomScene.cpp/CustomScene.h):
void CustomNode::draw(cocos2d::Renderer* renderer, const cocos2d::Mat4& transform, uint32_t flags) { _customCommand.init(_globalZOrder); _customCommand.func = CC_CALLBACK_0(CustomNode::onDraw, this, transform); renderer->addCommand(&_customCommand); } void CustomNode::onDraw(const cocos2d::Mat4& transform) { auto glProgramState = getGLProgramState(); glProgramState->setUniformVec4("Color", _color); glProgramState->apply(transform); GL::bindVAO(_buffersVAO); //VAO中包含了矩形資料 glDrawArrays(GL_TRIANGLES, 0, 6);//繪製一個顏色為_color的矩形 GL::bindVAO(0); CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, 6); } |
PrimitiveCommand
Primitive圖元,指的是OpenGL圖元
PrimitiveCommand繪製主要由類Primitive實現,Renderer處理時,也只是執行Primitive::draw函式繪製。TMXLayer瓦片地圖類使用了PrimitiveCommand進行繪製。以下是Renderer中處理PrimitiveCommand的程式碼:
else if(RenderCommand::Type::PRIMITIVE_COMMAND == commandType) { flush(); auto cmd = static_cast<PrimitiveCommand*>(command); CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_PRIMITIVE_COMMAND"); cmd->execute(); } void PrimitiveCommand::execute() const { //Set texture GL::bindTexture2D(_textureID); //set blend mode GL::blendFunc(_blendType.src, _blendType.dst); _glProgramState->apply(_mv); _primitive->draw(); CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1,_primitive->getCount()); } |
Primitive類實現了OpenGL圖元的繪製,繪製步驟也和OpenGL非常相似。
OpenGL中繪製圖形可以使用glDrawArrays繪製GL_ARRAY_BUFFER中的資料,也可以通過函式glDrawElements使用索引繪製,Primitive類同樣支援這兩種繪製方式。
Primitive類繪製步驟大致如下:
- 建立頂點資料快取
- 設定好頂點快取資料屬性格式
- 建立索引快取,如果不用索引繪製,去掉此步驟
- 使用頂點快取和索引快取建立Primitive類,並設定圖元型別
- 通過Primitive::draw函式繪製
建立頂點快取
VertexBuffer類實現了頂點快取的功能,可以直接使用該類建立頂點快取,並設定頂點快取資料。
設定頂點快取資料屬性
VertexStreamAttribute類為一個頂點屬性,對應頂點著色器中的一個輸入。每個VertexStreamAttribute物件對應一個VertexBuffer物件,繪製的時候會有多個頂點屬性,類VertexData儲存一組繪製的VertexStreamAttribute物件,VertexData類也就繪製是的頂點資料。可以建立VertexStreamAttribute物件並通過函式VertexData::setStream設定給VertexData物件。
索引快取
索引快取可以通過IndexBuffer類建立並設定索引資料
建立並初始化Primitive類
V3F_C4B_T2F data[] = { { { 0, 0,0 },{ 255, 0, 0,255 },{ 0,1 } }, { { 200, 0,0 },{ 0, 255,255,255 },{ 1,1 } }, { { 200,200,0 },{ 255,255, 0,255 },{ 1,0 } }, { { 0, 200,0 },{ 255,255,255,255 },{ 0,0 } }, }; uint16_t indices[] = { 0,1,2, 2,0,3 }; static const int TOTAL_VERTS = sizeof(data) / sizeof(data[0]); static const int TOTAL_INDICES = TOTAL_VERTS * 6 / 4; //建立頂點快取並設定資料 auto vertexBuffer = VertexBuffer::create(sizeof(V3F_C4B_T2F), TOTAL_VERTS); vertexBuffer->updateVertices(data, TOTAL_VERTS, 0); //設定頂點屬性資料 auto vertsData = VertexData::create(); vertsData->setStream(vertexBuffer, VertexStreamAttribute(0, GLProgram::VERTEX_ATTRIB_POSITION, GL_FLOAT, 3)); vertsData->setStream(vertexBuffer, VertexStreamAttribute(offsetof(V3F_C4B_T2F, colors), GLProgram::VERTEX_ATTRIB_COLOR, GL_UNSIGNED_BYTE, 4, true)); vertsData->setStream(vertexBuffer, VertexStreamAttribute(offsetof(V3F_C4B_T2F, texCoords), GLProgram::VERTEX_ATTRIB_TEX_COORD, GL_FLOAT, 2)); //設定索引快取 auto indexBuffer = IndexBuffer::create(IndexBuffer::IndexType::INDEX_TYPE_SHORT_16, TOTAL_INDICES); indexBuffer->updateIndices(indices, TOTAL_INDICES, 0); _primitive = Primitive::create(vertsData, indexBuffer, GL_TRIANGLES); //建立圖元類 _primitive->setCount(TOTAL_INDICES); _primitive->setStart(0); |
建立PrimitiveCommand
auto glProgramState = getGLProgramState(); glProgramState->setUniformInt("HasTex", _texture!=nullptr); GLuint tex = (_texture == nullptr) ? 0 : _texture->getName(); _primitiveCommand.init(_globalZOrder, tex, glProgramState, BlendFunc::ALPHA_NON_PREMULTIPLIED, _primitive, transform, flags); renderer->addCommand(&_primitiveCommand); |
BatchCommand
BatchCommand可以同時繪製同一紋理的多個小圖,用於2D繪製,類SpriteBatchNode和類ParticleBatchNode使用了BatchCommand減少OpenGL的呼叫。
BatchCommand繪製主要由類TextureAtlas實現,TextureAtlas::drawQuads可以一次繪製多個使用同一紋理的矩形,Renderer處理BatchCommand也只是執行TextureAtlas::drawQuads函式,以下是Renderer中的程式碼:
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(); } |
類TextureAtlas的使用很簡單,只需要使用一個紋理初始化,然後設定好要繪製的矩形,TextureAtlas::drawQuads就可以繪製。例,完整程式碼參考(BatchScene.cpp/ BatchScene.h):
建立並初始化TextureAtlas物件
_textureAtlas =::createWithTexture(tex, 30); float w = tex->getPixelsWide()*2; float h = tex->getPixelsHigh()*2; cocos2d::V3F_C4B_T2F_Quad quad = { { { 0, h, 0 },{ 255, 255, 255, 255 },{ 0,0 } }, { { 0, 0, 0 },{ 255, 255, 255, 255 },{ 0,1 } }, { { w*0.5f, h, 0 },{ 255, 255, 255, 255 },{ 0.5,0 } }, { { w*0.5f, 0, 0 },{ 255, 255, 255, 255 },{ 0.5,1 } }, }; cocos2d::V3F_C4B_T2F_Quad quad2 = { { { w*0.5f +200, h, 0 },{ 255, 255, 255, 255 },{ 0.5,1 } }, { { w*0.5f + 200, 0, 0 },{ 255, 255, 255, 255 },{ 0.5,0 } }, { { w + 200, h, 0 },{ 255, 255, 255, 255 },{ 1,1 } }, { { w + 200, 0, 0 },{ 255, 255, 255, 255 },{ 1,0 } }, }; _textureAtlas->insertQuad(&quad, 0); _textureAtlas->insertQuad(&quad2, 1); _textureAtlas->retain(); |
建立BatchCommand
if (_textureAtlas->getTotalQuads() == 0) { return; } _batchCommand.init(_globalZOrder, getGLProgram(), BlendFunc::ALPHA_NON_PREMULTIPLIED, _textureAtlas, transform, flags); renderer->addCommand(&_batchCommand); |
GroupCommand
GroupCommand本身並不繪製任何東西,GroupCommand是用於建立渲染分支,使得某些特殊的繪製可以單獨設定繪製狀態,不影響主渲染分支。類ClippingNode、RenderTexture、NodeGrid等待使用了GroupCommand。
Renderer類中的std::vector<RenderQueue> _renderGroups陣列支援多個渲染佇列,_renderGroups[0]是主渲染佇列,其他為渲染分支。
GroupCommand有一個變數_renderQueueID記錄的就是渲染分支佇列在_renderGroups中的索引。Renderer處理GroupCommand也很簡單,只需要用函式Renderer::visitRenderQueue訪問 _renderGroups[_renderQueueID]渲染佇列。以下是Renderer中處理GroupCommand的程式碼:
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(); } |
GroupCommandManager
GroupCommandManager的作用是快取已有的渲染佇列,減少渲染佇列的重複建立。建立渲染分支時,GroupCommandManager會先檢查會先檢查是否有沒有使用的渲染佇列,如果有則不建立直接放回已有的,沒有則建立。
例,以下建立了一個渲染佇列使用剪下矩形glScissor限制繪製的矩形,是的繪製限制在矩形內,矩形外的被剪下掉,完整程式碼參考(GroupScene.cpp/ GroupScene.h):
|