Cocos2d-x 渲染器Renderer
声明:本文使用的是cocos2d-x-3.17的代码
文章中的提到的测试代码下载地址https://gitee.com/Kyle12/Cocos2dRenderStudy
在《Cocos场景遍历与渲染》中已讲解了Cocos2d 在渲染时会先遍历场景,遍历时会生成渲染命令,渲染器再处理渲染命令绘制出图形。这篇文章主要是讲解渲染器如何处理渲染命令。
本文的代码下载地址:https://gitee.com/Kyle12/Cocos2dRenderStudy
渲染队列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对应节点的_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类创建并设置索引数据
例,完整代码参考(PrimitiveScene.cpp/PrimitiveScene.h):
创建并初始化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 = 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):
void GroupNode::draw(cocos2d::Renderer* renderer, const cocos2d::Mat4& transform, uint32_t flags) { // Optimization: Fast Dispatch if (_textureAtlas->getTotalQuads() == 0) { return; } _groupCommand.init(_globalZOrder); renderer->addCommand(&_groupCommand); //renderer->pushGroup(_groupCommand.getRenderQueueID());
//设置剪切矩形 _beforeCommand.init(-1); _beforeCommand.func = CC_CALLBACK_0(GroupNode::BeforeDraw, this, transform, this->getPositionZ()); //将命令加入到_groupCommand渲染分支中 renderer->addCommand(&_beforeCommand, _groupCommand.getRenderQueueID());
_batchCommand.init(0, getGLProgram(),BlendFunc::ALPHA_NON_PREMULTIPLIED, _textureAtlas,transform,flags); renderer->addCommand(&_batchCommand, _groupCommand.getRenderQueueID());
//还原剪切矩形 _afterCommand.init(1); _afterCommand.func = CC_CALLBACK_0(GroupNode::AfterDraw, this); renderer->addCommand(&_afterCommand, _groupCommand.getRenderQueueID()); //renderer->popGroup(); }
void GroupNode::BeforeDraw(Mat4 &transform, float z) { glGetIntegerv(GL_SCISSOR_BOX, _scissorBox); Vec3 orgin(0, 0, z); transform.transformPoint(&orgin); glEnable(GL_SCISSOR_TEST); glScissor(orgin.x, orgin.y, SIZE_X, SIZE_Y); }
void GroupNode::AfterDraw() { glScissor(_scissorBox[0], _scissorBox[1], _scissorBox[2], _scissorBox[3]); glDisable(GL_SCISSOR_TEST); } |
MeshCommand
MeshCommand用于3D网格绘制,类CCSprite3D、Particle3DquadRender等等使用了MeshCommand绘制。
MeshCommand绘制可以分为两种,一种是绘制建模生成的3D模型,另一种是直接使用glDrawElements直接绘制。
绘制3D模型
3D模型的创建以及初始化内容比较多,这里不详细讲解,可以能后面有单独的文章讲解。MeshCommand绘制3D模型时,需要在初始化的时候设置Material* _material指针,该指针代表了3D模型的材质。绘制时也需要使用该指针绘制。
glDrawElements直接绘制
MeshCommand绘制只支持索引绘制,不支持glDrawArrays绘制。绘制的步骤如下:
- 创建顶点数据缓存
- 创建索引缓存
- 设置绘制使用GLProgramState的顶点属性
- 通过执行MeshCommand绘制
和PrimitiveCommand中Primitive类的创建比较,可以发现这里的MeshCommand绘制需要设置GLProgramState的顶点属性。这是因为MeshCommand并没有使用VertexData类,只能用GLProgramState的顶点属性来设置顶点缓存的数据格式。
处理MeshCommand
以下是Renderer类中的处理代码
if (cmd->isSkipBatching() || _lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID()) { flush3D(); CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_MESH_COMMAND"); if(cmd->isSkipBatching()) { cmd->execute(); } else { cmd->preBatchDraw(); cmd->batchDraw(); _lastBatchedMeshCommand = cmd; } } else { CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_MESH_COMMAND"); cmd->batchDraw(); } |
Renderer中处理MeshCommand并没有和前面几种那样简单,cmd->isSkipBatching判断是否掉过“Batch”。这里的“Batch”和前面BatchCommand中的“Batch”意义不一样,BatchCommand中“Batch”是将多个绘制合并成一个OpenGL Draw Call调用,这里的“Batch”是指使用合并使用VAO对象绘制,并不会减少OpenGL Draw Call调用。
如果cmd->isSkipBatching为true会直接绘制命令,如果为false第一次会调用cmd->preBatchDraw创建并绑定VAO对象,然后调用cmd->batchDraw绘制,如果下一个绘制的MeshCommand命令顶点缓存、索引缓存、纹理等等数据是一样的,则不会创建和绑定VAO对象,而是直接调用cmd->batchDraw绘制。
例,完整代码参考(MeshScene.cpp/ MeshScene.h):
创建并初始化顶点缓存、索引缓存、GLProgramState
float data[] = { ... }; uint32_t indices2[] = { ... }; static const int TOTAL_VERTS = sizeof(data) / sizeof(data[0])/6; static const int TOTAL_INDICES = sizeof(indices2) / sizeof(indices2[0]);
_vertexBuffer = VertexBuffer::create(sizeof(float)*6, TOTAL_VERTS); _vertexBuffer->updateVertices(data, TOTAL_VERTS, 0); _vertexBuffer->retain();
_indexBuffer = IndexBuffer::create(IndexBuffer::IndexType::INDEX_TYPE_UINT_32, TOTAL_INDICES); _indexBuffer->updateIndices(indices2, TOTAL_INDICES, 0); _indexBuffer->retain();
auto glprogram = GLProgram::createWithByteArrays(_vertShader.c_str(), _fragShader.c_str()); auto glprogramstate = GLProgramState::getOrCreateWithGLProgram(glprogram); setGLProgramState(glprogramstate);
GLsizei stride = sizeof(float) * 6; glprogramstate->setVertexAttribPointer("a_position", 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)0); glprogramstate->setVertexAttribPointer("a_normal", 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(sizeof(float)*3)); |
初始化MeshCommand命令
_meshCommand.init(getGlobalZOrder(), 0, glProgramState, _stateBlock, _vertexBuffer->getVBO(), _indexBuffer->getVBO(), GL_TRIANGLES, GL_UNSIGNED_INT, _indexBuffer->getIndexNumber(), transform, flags);
_meshCommand.setSkipBatching(true); _meshCommand.setTransparent(false); _meshCommand.set3D(true);
renderer->addCommand(&_meshCommand); |
TrianglesCommand
TrianglesCommand看命名似乎是绘制三角形,实际上这个命令主要功能并不是绘制三角形,这个命令主要是用来会2D的图像,精灵类Sprite就是使用该命令绘制。这个命令有一个特性就是可以将使用相同纹理和融合函数的命令合并成一个OpenGL Draw Call调用,如果从这个特性命名名字应该叫AutoBatchCommand。
TrianglesCommand并不能执行,其主要是存储绘制命令需要的数据,纹理、顶点缓存、索引缓存等等,Renderer类在处理时会用这些数据进行绘制。在Render中有一个数组std::vector<TrianglesCommand*> _queuedTriangleCommands会将连续的TrianglesCommand缓存起来,集中一起处理。
处理TrianglesCommand
- 在Renderer::processRenderCommand函数中,使用render类的_queuedTriangleCommands缓存TrianglesCommand命令。代码如下:
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) { drawBatchedTriangles(); } // queue it _queuedTriangleCommands.push_back(cmd); _filledIndex += cmd->getIndexCount(); _filledVertex += cmd->getVertexCount(); } |
- 当遇到其他类型的绘制命令或没有其他的命令时,调用函数Renderer::drawBatchedTriangles开始绘制
- Renderer::drawBatchedTriangles函数先遍历_queuedTriangleCommands数组,对每个TrianglesCommand调用Renderer::fillVerticesAndIndices将顶点数据缓存到数组_verts,将索引数据缓存到数组_indices中。在缓存顶点数据之前,有一个重要的步骤就是将顶点位置到世界坐标系下,因为每个TrianglesCommand的局部坐标可能都不一样,需要统一到世界坐标系下。
- drawBatchedTriangles函数在历_queuedTriangleCommands数组时还会生成TriBatchToDraw,存放在_triBatchesToDraw数组中。每个TriBatchToDraw会对应一个OpenGL Draw Call调用,TriBatchToDraw一次绘制多个矩形。生成TriBatchToDraw如果发现两个TrianglesCommand使用相同纹理和融合函数则会合并两个TrianglesCommand。
- 将顶点数据缓存的数组_verts和索引缓存数组_indices的数据复制到已有GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER缓存。
- 根据_triBatchesToDraw数组绘制出图像。
例,请参考TrianglesScene.cpp/ TrianglesScene.h。