cocos2d-x 渲染系统 (二)

cocos2d-x 渲染系统 (一)中讲到引擎主循环中遍历UI节点树,生成渲染命令RenderCommand,并放入渲染队列中,在遍历结束后按照顺序执行渲染命令。本文以TrianglesCommand为例,解析渲染命令的执行过程,文章依旧基于cocos2d-x v3.13。

在研究TrianglesCommand之前,需要了解一些OpenGL以及cocos2d-x中的概念,首先来看应用程序如何把需要渲染的顶点数据发送给OpenGL服务端。

VBO和VAO

VBO和VAO是OpenGL中非常重要的概念。

VBO即顶点缓存对象(Vertex Buffer Object, VBO),是显存中的一块高速内存缓冲区,用来存储顶点的所有信息。每个VBO对象有一个ID,并且OpenGL在GPU中保存有VBO的ID和其对应的显存地址或地址偏移。在初始化时,VBO自身并不区分其存储的是何种信息(位置坐标、颜色或纹理坐标等),而是在渲染时确定。

VAO即顶点数组对象(Vertex Array Object, VAO),用于存储顶点绘制信息的对象。对应一个或数个VBO,记录关联了哪些VBO、及每个VBO中有哪些数据、这些数据的格式是怎样的。

以下是OpenGL中涉及到的一些函数:

涉及到的OpenGL函数

更多更详细的内容可以参考官网API,此处列出一些主要的:

glGenBuffers

1
void glGenBuffers(	GLsizei n, GLuint * buffers);

用于VBO的ID,n表示创建的VBO的ID的个数,buffers表示用于存放VBO的ID的数组。生成的VBO的ID会存放在buffers中,生成的ID未必是连续的整数,但都是未被使用的整数。只有在调用glBindBuffer之后,才会有VBO与生成的ID相关联。

glBindBuffer

1
void glBindBuffer( GLenum target, GLuint buffer);

target表示要绑定的VBO的类型,可以是GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFERbuffer是要绑定的VBO的ID。

glBindBuffer将创建或使用一个已命名的VBO,将VBO绑定到一个target类型,则对应该target的旧的绑定将会解除。VBO的ID是无符号的整数,其中0是保留数,且对应不同类型的target,不存在默认的VBO。将target绑定为0将会解除与对应target绑定的VBO,并释放对应于该target的存储空间。VBO首次绑定后,VBO的状态是大小为0的使用方式为GL_STATIC_DRAW 的存储缓冲区。

当非0的VBO绑定到targetGL_ARRAY_BUFFER时,VBO将存储顶点数组,其内容可以为顶点位置坐标、颜色、纹理坐标等。当非0的VBO绑定到targetGL_ELEMENT_ARRAY_BUFFER时,VBO将存储顶点的索引。

glBufferData

1
void glBufferData( GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);

创建并初始化VBO的数据。target指定对应target的VBO。size指定VBO的新数据的存储空间的大小。data指向的数据是将要复制到缓冲区用于初始化的数据,如果没有要存储的数据则用NULLusage指定数据将要以何种方式来使用,是一个枚举量,可以选的值有:GL_STREAM_DRAWGL_STATIC_DRAWGL_DYNAMIC_DRAW,各值的含义如下:

  • GL_DYNAMIC_DRAW:多次指定,多次作为绘图和图像指定函数的源数据,缓冲区对象的数据不仅常常需要进行更新,而且使用频率也非常高
  • GL_STATIC_DRAW:数据只指定一次,多次作为绘图和图像指定函数的源数据,缓冲区对象的数据只指定1次,但是这些数据被使用的频率很高
  • GL_STREAM_DRAW:数据只指定一次,最多只有几次作为绘图和图像指定函数的源数据,缓冲区对象中的数据常常需要更新,但是在绘图或其他操作中使用这些数据的次数较少

glGenVertexArrays

这是OpenGL ES的扩展函数,在ios平台将使用以下函数:

1
GLvoid glGenVertexArraysOES(GLsizei n, GLuint *arrays)

n指定创建的VAO的个数,arrays指向用于存放VAO的ID的数组。存放于arrays的数组会被标记为已使用,此标记仅供glGenVertexArrays使用,只有在完成与VAO的绑定之后才可以查询其状态或类型。

GL::bindVAO

是cocos2d-x中实现的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void bindVAO(GLuint vaoId)
{
if (Configuration::getInstance()->supportsShareableVAO())
{

#if CC_ENABLE_GL_STATE_CACHE
if (s_VAO != vaoId)
{
s_VAO = vaoId;
glBindVertexArray(vaoId);
}
#else
glBindVertexArray(vaoId);
#endif // CC_ENABLE_GL_STATE_CACHE

}
}

对是否支持VAO进行判断,实际上调用的是glBindVertexArray

glBindVertexArray

这是OpenGL ES的扩展函数,在ios平台将使用以下函数:

1
GLvoid glBindVertexArrayOES(GLuint array)

array是将要绑定的VAO的ID。如果是0则表示对当前的VAO解除绑定。如果对应于array没有已存在的VAO则会在第一次绑定时创建一个。

glVertexAttribPointer

1
void glVertexAttribPointer(	GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);

用于指定渲染时索引值为 index的顶点attribute数组的数据格式和位置。

index是要修改的顶点attribute的索引,size指定每个顶点attribute中分量的个数,可以为1、2、3或4,初始值为4。type指定了数组中数据类型,可以取的值有GL_BYTEGL_UNSIGNED_BYTEGL_SHORTGL_UNSIGNED_SHORTGL_FIXEDGL_FLOAT,初始值是GL_FLOATnormalized是一个布尔值,若为真(GL_TRUE)则在获取数据时会将定点数据(fixed-point data)的值正交化(映射到[-1,1]或[0,1]),否则(GL_FALSE)仅直接转化为定点数据。stride指定连续顶点attribute之间的偏移量。如果为0,那么认为顶点attribute是紧密排列在一起的,初始值为0。pointer是指向数组中首个顶点attribute的第一个分量的指针,初始值为0。

如果一个名称非0的VBO被绑定至targetGL_ARRAY_BUFFER且此时指定了一个顶点attribute数组,那么pointer被当做该VBO数据存储区的字节偏移量。并且,缓冲对象绑定(GL_ARRAY_BUFFER_BINDING)被存为索引为index的顶点attribute数组客户端状态(GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING)。

当指定一个顶点attribute数组时,除了当前的顶点数组缓冲对象绑定,sizetypenormalizedstridepointer 也会被存为客户端状态。

要启用或者禁用顶点attribute数组,调用glEnableVertexAttribArrayglDisableVertexAttribArray传入参数index。在启用后,当glDrawArrays或者glDrawElements被调用时,顶点attribute数组会被使用。

glVertexAttribPointer通常是由GL客户端(应用程序)来实现。

glEnableVertexAttribArray

1
void glEnableVertexAttribArray(	GLuint index);

启用一个通用的顶点attribute数组,index是对应的顶点attribute的索引。默认状态下所有的attribute数组都是未启用的,在启用之后则可使用glDrawArraysglDrawElements等访问顶点attribute数组中的数值。
与之相对应的还有glDisableVertexAttribArray,功能相反,不再赘述。

glMapBuffer

将VBO映射到指定的内存。在ios平台将使用以下函数:

1
GLvoid glMapBufferOES (GLenum target, GLenum access);

target 指定VBO的target类型, access: 指定缓冲区对象中数据映射后的读写策略,可选值为:GL_READ_ONLYGL_WRITE_ONLYGL_READ_WRITE。如果映射成功,返回指向的缓存的地址,失败则返回NULL

与此相对应的还有glUnmapBuffer,用于释放VBO与GL客户端地址空间的关系。

glDrawElements

1
void glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoid * indices);

绘制单元。mode指定要绘制图元的类型,可选值为GL_POINTSGL_LINE_STRIPGL_LINE_LOOPGL_LINESGL_TRIANGLE_STRIPGL_TRIANGLE_FANGL_TRIANGLEScount将要绘制的元素个数。type为索引值的类型,可以是GL_UNSIGNED_BYTEGL_UNSIGNED_SHORTindices是指向索引的指针。

glDrawElements函数能够通过较少的函数调用绘制多个几何图元,调用此函数前需要使用glVertexAttribPointer来预先定义顶点attribute数组。

在调用glDrawElements时,会从一个启用的数组中从indices开始,按次序使用count个元素,组建一系列的几何图元。组建何种图元或将数组中的元素按何种形式组建则由mode决定。如果启用的有一个以上的数组,则每个都会使用。启用或禁用数组使用glEnableVertexAttribArrayglDisableVertexAttribArray

VBO和VAO的初始化

参看cocos2d-x源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void Renderer::setupBuffer()
{
if(Configuration::getInstance()->supportsShareableVAO())
{
setupVBOAndVAO();
}
else
{
setupVBO();
}
}

void Renderer::setupVBOAndVAO()
{
//generate vbo and vao for trianglesCommand
glGenVertexArrays(1, &_buffersVAO);
GL::bindVAO(_buffersVAO);

glGenBuffers(2, &_buffersVBO[0]);

glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(_verts[0]) * VBO_SIZE, _verts, GL_DYNAMIC_DRAW);

// vertices
glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(V3F_C4B_T2F), (GLvoid*) offsetof( V3F_C4B_T2F, vertices));

// colors
glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_COLOR);
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(V3F_C4B_T2F), (GLvoid*) offsetof( V3F_C4B_T2F, colors));

// tex coords
glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_TEX_COORD);
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, sizeof(V3F_C4B_T2F), (GLvoid*) offsetof( V3F_C4B_T2F, texCoords));

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices[0]) * INDEX_VBO_SIZE, _indices, GL_STATIC_DRAW);

// Must unbind the VAO before changing the element buffer.
GL::bindVAO(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);

CHECK_GL_ERROR_DEBUG();
}

void Renderer::setupVBO()
{
glGenBuffers(2, &_buffersVBO[0]);
mapBuffers();
}

void Renderer::mapBuffers()
{
// Avoid changing the element buffer for whatever VAO might be bound.
GL::bindVAO(0);

glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(_verts[0]) * VBO_SIZE, _verts, GL_DYNAMIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER, 0);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices[0]) * INDEX_VBO_SIZE, _indices, GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

CHECK_GL_ERROR_DEBUG();
}

在以上代码中,在Renderer初始化Buffer时,根据实现是否支持VAO,采用不同策略。

  • 如果支持VAO:创建一个VAO和一个VBO,将VBO分别绑定到targetGL_ARRAY_BUFFER并用_verts初始化。启用三个attribute数组,分别对应的是顶点的位置坐标、颜色、纹理坐标,指定这些attribute在VBO中的存储位置及偏移。创建一个VBO,绑定GL_ELEMENT_ARRAY_BUFFER并用_indices初始化。最后将VAO和VBO解除绑定;
  • 如果不支持VAO:创建两个VBO,绑定VBO到两个target(GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFER),分别使用_verts_indices来初始化两个VBO(与使用VAO不同,VBO中保存的数据是什么内容则会在渲染时指定)

在每帧的渲染过程中,也会根据是否支持VAO采用不同的策略。以TrianglesCommand为例,分析RenderCommand执行过程。

TrianglesCommand

cocos2d-x v3.13源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/** 
Command used to render one or more Triangles, which is similar to QuadCommand.
Every TrianglesCommand will have generate material ID by give textureID, glProgramState, Blend function
if the material id is the same, these TrianglesCommands could be batched to save draw call.
*/
class CC_DLL TrianglesCommand : public RenderCommand
{
public:
/**The structure of Triangles. */
struct Triangles
{
/**Vertex data pointer.*/
V3F_C4B_T2F* verts;
/**Index data pointer.*/
unsigned short* indices;
/**The number of vertices.*/
int vertCount;
/**The number of indices.*/
int indexCount;
};
/**Constructor.*/
TrianglesCommand();
/**Destructor.*/
~TrianglesCommand();

/** Initializes the command.
@param globalOrder GlobalZOrder of the command.
@param textureID The openGL handle of the used texture.
@param glProgramState The specified glProgram and its uniform.
@param blendType Blend function for the command.
@param triangles Rendered triangles for the command.
@param mv ModelView matrix for the command.
@param flags to indicate that the command is using 3D rendering or not.
*/
void init(float globalOrder, GLuint textureID, GLProgramState* glProgramState, BlendFunc blendType, const Triangles& triangles,const Mat4& mv, uint32_t flags);
void init(float globalOrder, Texture2D* textureID, GLProgramState* glProgramState, BlendFunc blendType, const Triangles& triangles, const Mat4& mv, uint32_t flags);
/**Apply the texture, shaders, programs, blend functions to GPU pipeline.*/
void useMaterial() const;
/**Get the material id of command.*/
inline uint32_t getMaterialID() const { return _materialID; }
/**Get the openGL texture handle.*/
inline GLuint getTextureID() const { return _textureID; }
/**Get a const reference of triangles.*/
inline const Triangles& getTriangles() const { return _triangles; }
/**Get the vertex count in the triangles.*/
inline ssize_t getVertexCount() const { return _triangles.vertCount; }
/**Get the index count of the triangles.*/
inline ssize_t getIndexCount() const { return _triangles.indexCount; }
/**Get the vertex data pointer.*/
inline const V3F_C4B_T2F* getVertices() const { return _triangles.verts; }
/**Get the index data pointer.*/
inline const unsigned short* getIndices() const { return _triangles.indices; }
/**Get the glprogramstate.*/
inline GLProgramState* getGLProgramState() const { return _glProgramState; }
/**Get the blend function.*/
inline BlendFunc getBlendType() const { return _blendType; }
/**Get the model view matrix.*/
inline const Mat4& getModelView() const { return _mv; }

protected:
/**Generate the material ID by textureID, glProgramState, and blend function.*/
void generateMaterialID();

/**Generated material id.*/
uint32_t _materialID;
/**OpenGL handle for texture.*/
GLuint _textureID;
/**GLprogramstate for the command. encapsulate shaders and uniforms.*/
GLProgramState* _glProgramState;
/**The GLProgram used by GLProgramState*/
GLProgram* _glProgram;
/**Blend function when rendering the triangles.*/
BlendFunc _blendType;
/**Rendered triangles.*/
Triangles _triangles;
/**Model view matrix when rendering the triangles.*/
Mat4 _mv;

GLuint _alphaTextureID; // ANDROID ETC1 ALPHA supports.
};

包含了Triangles结构体的定义,贴图_textureID,_glProgram,混合函数_blendType,模型视图矩阵_mv,使用贴图、着色程序状态、混合函数、顶点数据、变换矩阵等参数的初始化函数,材质ID(MaterialID),访问各私有成员的inline函数等,其中涉及到的几个概念:

Triangles结构体

1
2
3
4
5
6
7
8
9
10
11
12
/**The structure of Triangles. */
struct Triangles
{
/**Vertex data pointer.*/
V3F_C4B_T2F* verts;
/**Index data pointer.*/
unsigned short* indices;
/**The number of vertices.*/
int vertCount;
/**The number of indices.*/
int indexCount;
};

存储绘制的顶点的信息,包括存储顶点attribute数据的数组、存储顶点索引的数组、顶点的个数、顶点索引的个数。其中顶点的attribute数据使用V3F_C4B_T2F结构体来存储。

V3F_C4B_T2F结构体

如下边的代码所示,V3F_C4B_T2F结构体用来存储顶点的信息,包括顶点坐标、颜色、纹理坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @struct V3F_C4B_T2F
* 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个浮点型数值:

1
2
3
4
5
6
7
8
9
10
11
12
/** @struct Tex2F
* A TEXCOORD composed of 2 floats: u, y
* @since v3.0
*/
struct CC_DLL Tex2F {
Tex2F(float _u, float _v): u(_u), v(_v) {}

Tex2F(): u(0.f), v(0.f) {}

GLfloat u;
GLfloat v;
};

V3F_C4B_T2F结构体的形式可以看做是一个数组,其定义了应用程序中顶点数据的格式,同时也是向OpenGL传递以及在显存中存储的顶点attribute数据的格式:

vertices colors texCoords
x y z r g b a s t

索引 indices

VBO有两个target,GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFER,即顶点的信息和顶点的索引是分开存储(为了更高效利用存储空间),在Triangles结构体中,indices是一个unsigned short型的数组,直接存储索引信息,并用indexCount记录数组的长度。

MaterialID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void TrianglesCommand::generateMaterialID()
{
// do not batch if using custom uniforms (since we cannot batch) it
if(_glProgramState->getUniformCount() > 0)
{
_materialID = Renderer::MATERIAL_ID_DO_NOT_BATCH;
setSkipBatching(true);
}
else
{
int glProgram = (int)_glProgram->getProgram();
int intArray[4] = { glProgram, (int)_textureID, (int)_blendType.src, (int)_blendType.dst};
_materialID = XXH32((const void*)intArray, sizeof(intArray), 0);
}
}

TrianglesCommand在初始化时会调用generateMaterialID生成材质ID。生成材质ID保存到_materialID。在生成材质ID时,如果包含有自定义的着色器全局uniform,则此TrianglesCommand不参与批量绘制。否则生成材质ID,U32 XXH32(const void* input, int len, U32 seed)是一个哈希函数,用于根据不同的着色程序、纹理、混合函数生成的独特的材质ID。

两个TrianglesCommand当且仅当着色程序、纹理、混合函数都相同的时候,材质ID才会相等。如果两个对象渲染顺序相邻,且材质ID相同,就可以合并批量渲染。

处理RenderCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void Renderer::processRenderCommand(RenderCommand* command)
{
auto commandType = command->getType();
if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
{
// ...略去无关代码

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)
{
CCASSERT(cmd->getVertexCount()>= 0 && cmd->getVertexCount() < VBO_SIZE, "VBO for vertex is not big enough, please break the data down or use customized render command");
CCASSERT(cmd->getIndexCount()>= 0 && cmd->getIndexCount() < INDEX_VBO_SIZE, "VBO for index is not big enough, please break the data down or use customized render command");
drawBatchedTriangles();
}

// queue it
_queuedTriangleCommands.push_back(cmd);
_filledIndex += cmd->getIndexCount();
_filledVertex += cmd->getVertexCount();
}
else if (RenderCommand::Type::MESH_COMMAND == commandType)
{
flush2D();

// ...略去无关代码

}
else if(RenderCommand::Type::GROUP_COMMAND == commandType)
{
flush();

// ...略去无关代码

}

// ...略去无关代码

else
{
CCLOGERROR("Unknown commands in renderQueue");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Renderer::flush()
{
flush2D();
flush3D();
}

void Renderer::flush2D()
{
flushTriangles();
}

void Renderer::flushTriangles()
{
drawBatchedTriangles();
}

由以上代码易知,在两种情况下会调用drawBatchedTriangles():VBO的buffer写满或新接收到的RenderCommand类型不为TrianglesCommand

处理TrianglesCommand

调用Renderer::drawBatchedTriangles(),此时在_queuedTriangleCommands中应存储有待渲染的TrianglesCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
void Renderer::drawBatchedTriangles()
{
if(_queuedTriangleCommands.empty())
return;

CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_BATCH_TRIANGLES");

_filledVertex = 0;
_filledIndex = 0;

/************** 1: Setup up vertices/indices *************/

_triBatchesToDraw[0].offset = 0;
_triBatchesToDraw[0].indicesToDraw = 0;
_triBatchesToDraw[0].cmd = nullptr;

int batchesTotal = 0;
int prevMaterialID = -1;
bool firstCommand = true;

for(auto it = std::begin(_queuedTriangleCommands); it != std::end(_queuedTriangleCommands); ++it)
{
const auto& cmd = *it;
auto currentMaterialID = cmd->getMaterialID();
const bool batchable = !cmd->isSkipBatching();

fillVerticesAndIndices(cmd);

// in the same batch ?
if (batchable && (prevMaterialID == currentMaterialID || firstCommand))
{
CC_ASSERT(firstCommand || _triBatchesToDraw[batchesTotal].cmd->getMaterialID() == cmd->getMaterialID() && "argh... error in logic");
_triBatchesToDraw[batchesTotal].indicesToDraw += cmd->getIndexCount();
_triBatchesToDraw[batchesTotal].cmd = cmd;
}
else
{
// is this the first one?
if (!firstCommand) {
batchesTotal++;
_triBatchesToDraw[batchesTotal].offset = _triBatchesToDraw[batchesTotal-1].offset + _triBatchesToDraw[batchesTotal-1].indicesToDraw;
}

_triBatchesToDraw[batchesTotal].cmd = cmd;
_triBatchesToDraw[batchesTotal].indicesToDraw = (int) cmd->getIndexCount();

// is this a single batch ? Prevent creating a batch group then
if (!batchable)
currentMaterialID = -1;
}

// capacity full ?
if (batchesTotal + 1 >= _triBatchesToDrawCapacity) {
_triBatchesToDrawCapacity *= 1.4;
_triBatchesToDraw = (TriBatchToDraw*) realloc(_triBatchesToDraw, sizeof(_triBatchesToDraw[0]) * _triBatchesToDrawCapacity);
}

prevMaterialID = currentMaterialID;
firstCommand = false;
}
batchesTotal++;

/************** 2: Copy vertices/indices to GL objects *************/
auto conf = Configuration::getInstance();
if (conf->supportsShareableVAO() && conf->supportsMapBuffer())
{
//Bind VAO
GL::bindVAO(_buffersVAO);
//Set VBO data
glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);

glBufferData(GL_ARRAY_BUFFER, sizeof(_verts[0]) * _filledVertex, nullptr, GL_STATIC_DRAW);
void *buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
memcpy(buf, _verts, sizeof(_verts[0]) * _filledVertex);
glUnmapBuffer(GL_ARRAY_BUFFER);

glBindBuffer(GL_ARRAY_BUFFER, 0);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices[0]) * _filledIndex, _indices, GL_STATIC_DRAW);
}
else
{
// Client Side Arrays
#define kQuadSize sizeof(_verts[0])
glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);

glBufferData(GL_ARRAY_BUFFER, sizeof(_verts[0]) * _filledVertex , _verts, GL_DYNAMIC_DRAW);

GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX);

// vertices
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, vertices));

// colors
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, colors));

// tex coords
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, texCoords));

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices[0]) * _filledIndex, _indices, GL_STATIC_DRAW);
}

/************** 3: Draw *************/
for (int i=0; i<batchesTotal; ++i)
{
CC_ASSERT(_triBatchesToDraw[i].cmd && "Invalid batch");
_triBatchesToDraw[i].cmd->useMaterial();
glDrawElements(GL_TRIANGLES, (GLsizei) _triBatchesToDraw[i].indicesToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (_triBatchesToDraw[i].offset*sizeof(_indices[0])) );
_drawnBatches++;
_drawnVertices += _triBatchesToDraw[i].indicesToDraw;
}

/************** 4: Cleanup *************/
if (Configuration::getInstance()->supportsShareableVAO())
{
//Unbind VAO
GL::bindVAO(0);
}
else
{
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

_queuedTriangleCommands.clear();
_filledVertex = 0;
_filledIndex = 0;
}

由以上代码不难看出,整个渲染过程可以划分为四个阶段:

[1]准备顶点和索引信息 -> [2]将顶点和索引信息传给GL对象 -> [3]绘制 -> [4]清理

准备顶点和索引信息

遍历_queuedTriangleCommands中的TrianglesCommand,调用fillVerticesAndIndices,将所有的顶点信息和索引的信息复制到_verts[]_indices[],并累加记录顶点和索引的个数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Renderer::fillVerticesAndIndices(const TrianglesCommand* cmd)
{
memcpy(&_verts[_filledVertex], cmd->getVertices(), sizeof(V3F_C4B_T2F) * cmd->getVertexCount());

// fill vertex, and convert them to world coordinates
const Mat4& modelView = cmd->getModelView();
for(ssize_t i=0; i < cmd->getVertexCount(); ++i)
{
modelView.transformPoint(&(_verts[i + _filledVertex].vertices));
}

// fill index
const unsigned short* indices = cmd->getIndices();
for(ssize_t i=0; i< cmd->getIndexCount(); ++i)
{
_indices[_filledIndex + i] = _filledVertex + indices[i];
}

_filledVertex += cmd->getVertexCount();
_filledIndex += cmd->getIndexCount();
}

根据材质ID对TrianglesCommand进行判断,如果符合批量绘制的条件则存入_triBatchesToDraw[]

将顶点和索引信息传给GL对象

传递顶点和索引信息,根据实现是否支持VAO,采用不同策略:

  • 如果支持VAO:绑定VAO,绑定GL_ARRAY_BUFFER的VBO,为VBO申请空间并指定数据传输方式为GL_STATIC_DRAW,将_verts[]的值写给VBO对应的内存;绑定GL_ELEMENT_ARRAY_BUFFER的VBO,并将索引信息_indices[]写入;
  • 如果不支持VAO:绑定GL_ARRAY_BUFFER的VBO,将_verts[]中的顶点信息写入,开启用于存储位置坐标、颜色及纹理坐标的顶点attribute的数组,绑定attribute数组中对应位置坐标、颜色及纹理坐标的存储位置并制定数据格式;绑定GL_ELEMENT_ARRAY_BUFFER的VBO,并将索引信息_indices[]写入;

可见当使用VAO时,使用glVertexAttribPointer绑定数组及制定数据格式的相关操作放在初始化过程中,而不是绘制阶段。VAO中保存了绘制状态信息(GL-context),从而减少了绘制时的花销,提高了渲染的效率。

绘制

_triBatchesToDraw[]中存储的TrianglesCommand,逐个使用glDrawElements绘制。

清理

结束VAO和VBO的绑定,清除_queuedTriangleCommands[]

REFERENCE

cocos2d-x v3.13 source code
我所理解的cocos2d-x
https://www.khronos.org/opengles/sdk/docs/man/xhtml/
https://www.khronos.org/opengles/sdk/docs/man3/
http://www.zwqxin.com/archives/opengl/vao-and-vbo-stuff.html
http://blog.csdn.net/bill_man/article/details/38314077
http://blog.csdn.net/h1051760124/article/details/41776931