渲染,感覺這個挺重要了,這里代入一個簡單的例子 Sprite 建立及到最后的畫在屏幕上, 我們描述一下這個渲染的流程:
1 sprite 初始化(紋理, 坐標,及當前元素的坐標大小信息)
2 主循環調用sprite的draw(), 把繪制命令發送到系統的render的渲染隊列中。
3 Render拿到渲染隊列中的渲染命令, 分別對每個命令進行處理, 我們這里的QUAD_COMMAND, 把這個命令中的坐標信息復制到自己的渲染緩沖中, 之后通過調用OpenGL命令對當前的矩形進行繪制。
1 Sprite初始化:

如:
Size visibleSize = Director::getInstance()->getVisibleSize(); Sprite *sp = Sprite::create("HelloWorld.png"); sp->setPosition(Point(visibleSize.width * .5, visibleSize.height * .5)); this->addChild(sp, 1);
1.1 簡述:
1> 初始化Sprite, 從紋理緩存中讀取紋理的大小及相關信息。
2> 初始化當前對象中的_quad屬性,Sprite對OpenGL來說就是一個矩形的繪制,要繪制它, 就需要它的頂點信息,紋理信息, 這些信息都在Sprite初始化時被添加到了_quad屬性中。 具體看一下下面的這個屬性的數據結構。
1.2 詳細:
1> 初始化Sprite
做了一堆的初始化工作這里我們注意一下_quad. 這個對象記錄了當前的Sprite, 矩形的坐標,及紋理相關的信息。我們之后的繪制就需要這個對象。
void Sprite::setTextureRect(const Rect& rect, bool rotated, const Size& untrimmedSize)
1. _quad
V3F_C4B_T2F_Quad _quad;
V3F_C4B_T2F_Quad, 這個數據結構記錄了我們的左上,左下,右上, 右下,這個矩形的4個頂點信息, 每個頂點是由一個V3F_C4B_T2F結構組成的, 可以看到,其中有坐標, 顏色, 紋理的數據存儲。
//! 4 Vertex3FTex2FColor4B struct CC_DLL V3F_C4B_T2F_Quad { //! top left V3F_C4B_T2F tl; //! bottom left V3F_C4B_T2F bl; //! top right V3F_C4B_T2F tr; //! bottom right V3F_C4B_T2F br; }; //! a Vec2 with a vertex point, a tex coord point and a color 4B struct CC_DLL V3F_C4B_T2F { //! vertices (3F) Vec3 vertices; // 12 bytes //! colors (4B) Color4B colors; // 4 bytes // tex coords (2F) Tex2F texCoords; // 8 bytes };
2. 紋理相關的初始化 setTextureCoords()
_quad.bl.texCoords.u = left; _quad.bl.texCoords.v = bottom; _quad.br.texCoords.u = right; _quad.br.texCoords.v = bottom; _quad.tl.texCoords.u = left; _quad.tl.texCoords.v = top; _quad.tr.texCoords.u = right; _quad.tr.texCoords.v = top;
3. 坐標相關初始化
// Atlas: Vertex float x1 = 0 + _offsetPosition.x; float y1 = 0 + _offsetPosition.y; float x2 = x1 + _rect.size.width; float y2 = y1 + _rect.size.height; // Don't update Z. _quad.bl.vertices = Vec3(x1, y1, 0); _quad.br.vertices = Vec3(x2, y1, 0); _quad.tl.vertices = Vec3(x1, y2, 0); _quad.tr.vertices = Vec3(x2, y2, 0);
2 調用Sprite draw()
還記得之前一篇的主循環嗎?
Director 每幀都調用drawScene(), drawScene()中會調用當前Scene->render() , Scene->render() 又會遞歸遍歷其子元素,分別調用子元素的visit() 或 draw() , 當有子元素時, 用visit() , 沒有子元素時直接調用其draw. 我們這里的Sprite被調用了, 這里調用的是draw().
這個函數, 主要做了一件事,初始化一個矩形繪制命令, 把當前繪制命令發送到當前的系統 Render 的繪制隊列中。
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) { // Don't do calculate the culling if the transform was not updated _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds; if(_insideBounds) { _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform); renderer->addCommand(&_quadCommand); }
3 Render:render()
Render內部保存了一個命令的繪制隊列,cocos2dx的所有繪制都會被封裝成為命令發送到當前的繪制隊列中。
當整個的子節點遍歷后, 即所有的繪制命令加入到繪制隊列上時, 這里調用 render->render()
這個函數的作用是繪制當前繪制隊列中的繪制信息, 最終是調用OpenGL的命令完成了,繪制。
下面看看它是怎么做的。

3.1 排序, 對繪制命令進行排序
這里默認使用的遍歷順序排序. 當然也提供了可以改變這個順序的方法, 這里不展開。
3.2 遍歷當前的繪制隊列
命令的類型:
TRIANGLES_COMMAND
QUAD_COMMAND
GROUP_COMMAND
CUSTOM_COMMAND
BATCH_COMMAND
PRIMITIVE_COMMAND
MESH_COMMAND
1》根據不同的命令類型來對命令進行處理, 這里我們只關注,QUAD_COMMAND, 之前我們的Sprite就發送的這種類型的繪制命令。
else if ( RenderCommand::Type::QUAD_COMMAND == commandType ) { flush3D(); if(_filledIndex > 0) { drawBatchedTriangles(); _lastMaterialID = 0; } auto cmd = static_cast<QuadCommand*>(command); //Batch quads if( (_numberQuads + cmd->getQuadCount()) * 4 > VBO_SIZE ) { CCASSERT(cmd->getQuadCount()>= 0 && cmd->getQuadCount() * 4 < VBO_SIZE, "VBO for vertex is not big enough, please break the data down or use customized render command"); //Draw batched quads if VBO is full drawBatchedQuads(); } _batchQuadCommands.push_back(cmd); fillQuads(cmd); }
有2個關鍵點,
1 把繪制命令加到到批繪制列表中。_batchQuadCommands.push_back(cmd)
2 處理當前的命令中的頂點數據, fillQuads(cmd)
void Renderer::fillQuads(const QuadCommand *cmd) { memcpy(_quadVerts + _numberQuads * 4, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount()); const Mat4& modelView = cmd->getModelView(); for(ssize_t i=0; i< cmd->getQuadCount() * 4; ++i) { V3F_C4B_T2F *q = &_quadVerts[i + _numberQuads * 4]; Vec3 *vec1 = (Vec3*)&q->vertices; modelView.transformPoint(vec1); } _numberQuads += cmd->getQuadCount(); }
1》首先, Render在初始化時, 初始化了一個的很大的頂點信息的緩存區, 我們的QuadCommand中保存的節點信息, 會先被復制到這個緩存區中。
memcpy(_quadVerts + _numberQuads * 4, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
2》對當前的頂點坐標信息, 做頂點坐標變化, 也就是模型坐標到世界坐標的變換
for(ssize_t i=0; i< cmd->getQuadCount() * 4; ++i) { V3F_C4B_T2F *q = &_quadVerts[i + _numberQuads * 4]; Vec3 *vec1 = (Vec3*)&q->vertices; modelView.transformPoint(vec1); }
3 記錄當前Render中有多少個這種矩形信息。
_numberQuads += cmd->getQuadCount();
3.3 繪制
flush() 關於這塊的OpenGL相關的函數,(VAO, VBO,glDrawBuffer) 參考下面的附錄:
1 綁定VAO, 這里的VAO在 setupVBOAndVAO里已經初始化過了。初始化時,對頂點的索引值進行了初始化。
GL::bindVAO(_quadVAO);
2 設置VBO數據
//Set VBO data glBindBuffer(GL_ARRAY_BUFFER, _quadbuffersVBO[0]); // option 3: orphaning + glMapBuffer glBufferData(GL_ARRAY_BUFFER, sizeof(_quadVerts[0]) * _numberQuads * 4, nullptr, GL_DYNAMIC_DRAW); void *buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); memcpy(buf, _quadVerts, sizeof(_quadVerts[0])* _numberQuads * 4); glUnmapBuffer(GL_ARRAY_BUFFER);
3 綁定頂點索引數據 , 這里的索引數據, 詳細看 setupVBOAndVAO()
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _quadbuffersVBO[1]);
4 遍歷之前的批繪制列表,(這個有一個優化,就是當相鄰的矩形的材質相同時,統一在一個繪制命令中繪制)
for(const auto& cmd : _batchQuadCommands)
5 繪制
glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) );
源碼:
void Renderer::drawBatchedQuads() { //TODO: we can improve the draw performance by insert material switching command before hand. int indexToDraw = 0; int startIndex = 0; //Upload buffer to VBO if(_numberQuads <= 0 || _batchQuadCommands.empty()) { return; } if (Configuration::getInstance()->supportsShareableVAO()) { //Bind VAO GL::bindVAO(_quadVAO); //Set VBO data glBindBuffer(GL_ARRAY_BUFFER, _quadbuffersVBO[0]); // option 3: orphaning + glMapBuffer glBufferData(GL_ARRAY_BUFFER, sizeof(_quadVerts[0]) * _numberQuads * 4, nullptr, GL_DYNAMIC_DRAW); void *buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); memcpy(buf, _quadVerts, sizeof(_quadVerts[0])* _numberQuads * 4); glUnmapBuffer(GL_ARRAY_BUFFER); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _quadbuffersVBO[1]); } else { } //Start drawing verties in batch for(const auto& cmd : _batchQuadCommands) { auto newMaterialID = cmd->getMaterialID(); if(_lastMaterialID != newMaterialID || newMaterialID == MATERIAL_ID_DO_NOT_BATCH) { //Draw quads if(indexToDraw > 0) { glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) ); _drawnBatches++; _drawnVertices += indexToDraw; startIndex += indexToDraw; indexToDraw = 0; } //Use new material cmd->useMaterial(); _lastMaterialID = newMaterialID; } indexToDraw += cmd->getQuadCount() * 6; } //Draw any remaining quad if(indexToDraw > 0) { glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) ); _drawnBatches++; _drawnVertices += indexToDraw; } if (Configuration::getInstance()->supportsShareableVAO()) { //Unbind VAO GL::bindVAO(0); } else { glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } _batchQuadCommands.clear(); _numberQuads = 0; }
參考:
VAO&VBO
OpenGL ES 2.0
cocos2D-X源碼分析之從cocos2D-X學習OpenGL(2)----QUAD_COMMAND