【OPENGL ES 3.0 学习笔记】第十一天:glDrawArrays和glDrawElements

glDrawArrays与glDrawElements
在OpenGL ES渲染管线中,glDrawArrays和glDrawElements是触发图元渲染的两大核心函数。
二者的核心差异在于顶点数据的使用方式:glDrawArrays直接按顺序读取顶点缓冲区数据,无法复用顶点;glDrawElements通过索引缓冲区(IBO/EBO)指定顶点使用顺序,支持顶点复用。
理解这种差异是优化渲染性能、适配复杂模型的关键。
glDrawArrays
glDrawArrays是最简单的渲染调用方式,它从顶点缓冲区的指定位置开始,按连续顺序读取顶点数据,组装成图元。
无需额外索引,适合结构简单、顶点复用少的场景。
1.1 基本定义
函数原型(OpenGL ES 2.0/3.x通用):
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
作用:根据mode指定的图元类型(如GL_TRIANGLES),从顶点缓冲区的第first个顶点开始,连续读取count个顶点,完成渲染。
1.2 关键参数解析
| 参数名 | 类型 | 核心作用 | 示例 |
|---|---|---|---|
mode | GLenum | 指定图元类型,如GL_POINTS、GL_LINES、GL_TRIANGLES | 渲染三角形用GL_TRIANGLES |
first | GLint | 顶点缓冲区中起始顶点的索引(从0开始) | 从第0个顶点开始取数据用0 |
count | GLsizei | 需读取的顶点总数 | 渲染2个三角形(6个顶点)用6 |
1.3 工作流程
- 绑定顶点缓冲区:通过
glBindBuffer(GL_ARRAY_BUFFER, vboId)绑定存储顶点数据的VBO; - 配置顶点属性:通过
glVertexAttribPointer指定顶点数据的格式(如每个顶点3个位置分量); - 触发渲染:调用
glDrawArrays,GPU按顺序从VBO的first位置读取count个顶点; - 组装图元:根据
mode的规则(如GL_TRIANGLES每3个顶点组成一个三角形),完成图元装配并光栅化。
1.4 代码示例:用glDrawArrays画2个相邻三角形
需求:绘制两个共用一条边的三角形(组成一个矩形),需6个顶点(其中2个顶点重复存储)。
// 1. 定义顶点数据(2个三角形,6个顶点,重复存储顶点1和2)
float[] vertices = {// 三角形1(0,1,2)-0.5f, 0.5f, 0.0f, // 0: 左上-0.5f, -0.5f, 0.0f, // 1: 左下(重复)0.5f, -0.5f, 0.0f, // 2: 右下(重复)// 三角形2(0,2,3)-0.5f, 0.5f, 0.0f, // 0: 左上(重复)0.5f, -0.5f, 0.0f, // 2: 右下(重复)0.5f, 0.5f, 0.0f // 3: 右上
};// 2. 创建并绑定VBO
int[] vbo = new int[1];
GLES20.glGenBuffers(1, vbo, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]);
// 将顶点数据写入VBO
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertices.length * 4, createFloatBuffer(vertices), GLES20.GL_STATIC_DRAW);// 3. 配置顶点属性(位置分量)
int aPositionLoc = GLES20.glGetAttribLocation(programId, "aPosition");
GLES20.glEnableVertexAttribArray(aPositionLoc);
GLES20.glVertexAttribPointer(aPositionLoc, 3, GLES20.GL_FLOAT, false, 3 * 4, 0 // 每个顶点3个float,步长12字节,偏移0
);// 4. 调用glDrawArrays渲染(6个顶点,图元类型GL_TRIANGLES)
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);// 5. 清理状态
GLES20.glDisableVertexAttribArray(aPositionLoc);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
关键问题:顶点1(左下)和顶点2(右下)被两个三角形重复使用,但需在VBO中存储2次,造成内存冗余。
glDrawElements
glDrawElements是更高效的渲染方式,它通过索引缓冲区(IBO/EBO) 指定顶点的使用顺序,实现顶点复用——相同顶点只需在VBO中存储一次,通过索引重复引用。
这种方式在复杂3D模型中能大幅减少内存开销和数据传输量。
2.1 基本定义
函数原型(OpenGL ES 2.0/3.x通用):
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void *indices);
作用:根据mode指定的图元类型,通过indices指向的索引缓冲区,读取对应的顶点数据,组装成图元。
2.2 关键参数解析
| 参数名 | 类型 | 核心作用 | 示例 |
|---|---|---|---|
mode | GLenum | 图元类型,与glDrawArrays一致 | GL_TRIANGLES |
count | GLsizei | 索引数组的元素总数(不是顶点数) | 6个索引对应2个三角形 |
type | GLenum | 索引数据的类型(必须是无符号整数) | GL_UNSIGNED_SHORT(常用)、GL_UNSIGNED_BYTE |
indices | const void * | 索引缓冲区的偏移量(绑定IBO后,此参数为偏移值,非指针) | 从索引缓冲区起始位置读取用0 |
2.3 工作流程
- 绑定VBO和IBO:分别绑定存储顶点数据的VBO和存储索引数据的IBO;
- 配置顶点属性:与
glDrawArrays一致,指定顶点数据格式; - 触发渲染:调用
glDrawElements,GPU先从IBO读取count个索引; - 索引查顶点:根据每个索引的值,到VBO中查找对应的顶点数据;
- 组装图元:按
mode规则组装图元,完成渲染。
2.4 代码示例:用glDrawElements画2个相邻三角形
需求:与上例相同,但通过索引复用顶点,仅需4个顶点+6个索引。
// 1. 定义顶点数据(4个独特顶点,无重复)
float[] vertices = {-0.5f, 0.5f, 0.0f, // 0: 左上-0.5f, -0.5f, 0.0f, // 1: 左下0.5f, -0.5f, 0.0f, // 2: 右下0.5f, 0.5f, 0.0f // 3: 右上
};// 2. 定义索引数据(6个索引,对应2个三角形:0,1,2 和 0,2,3)
short[] indices = {0, 1, 2, 0, 2, 3};// 3. 创建并绑定VBO(存储顶点数据)
int[] vbo = new int[1];
GLES20.glGenBuffers(1, vbo, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]);
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertices.length * 4, createFloatBuffer(vertices), GLES20.GL_STATIC_DRAW);// 4. 创建并绑定IBO(存储索引数据)
int[] ibo = new int[1];
GLES20.glGenBuffers(1, ibo, 0);
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, ibo[0]);
GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indices.length * 2, createShortBuffer(indices), GLES20.GL_STATIC_DRAW);// 5. 配置顶点属性(位置分量)
int aPositionLoc = GLES20.glGetAttribLocation(programId, "aPosition");
GLES20.glEnableVertexAttribArray(aPositionLoc);
GLES20.glVertexAttribPointer(aPositionLoc, 3, GLES20.GL_FLOAT, false, 3 * 4, 0
);// 6. 调用glDrawElements渲染(6个索引,类型GL_UNSIGNED_SHORT)
GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, 0 // 索引偏移0
);// 7. 清理状态(注意:IBO需最后解绑)
GLES20.glDisableVertexAttribArray(aPositionLoc);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
核心优势:顶点数据从6个减少到4个,内存占用降低约33%;若模型复杂(如立方体、人物),顶点复用率更高,内存节省效果更显著。
核心差异对比
| 对比维度 | glDrawArrays | glDrawElements |
|---|---|---|
| 顶点复用能力 | 无。顶点需按顺序连续使用,重复顶点必须重复存储 | 有。通过索引重复引用VBO中的顶点,无需重复存储 |
| 内存开销 | 高。复杂模型中重复顶点多,VBO体积大 | 低。VBO仅存储独特顶点,IBO体积小(索引多为2字节) |
| 数据传输量 | 大。每次渲染需传输更多顶点数据到GPU | 小。仅传输索引+独特顶点,带宽占用低 |
| 渲染效率 | 简单场景高效(无索引查找),复杂场景低效 | 复杂场景高效(硬件优化索引查找,抵消额外开销) |
| 适用场景 | 简单图形(点、线、单个三角形、矩形)、粒子系统 | 复杂3D模型(立方体、人物、场景)、需复用顶点的图形 |
| 灵活性 | 低。顶点使用顺序固定,无法自定义 | 高。通过索引可任意调整顶点顺序,支持复杂拓扑(如凹多边形) |
| 额外依赖 | 仅需VBO(顶点缓冲区) | 需VBO+IBO(索引缓冲区),多一步IBO管理 |
| 索引类型支持 | 不支持索引 | 支持GL_UNSIGNED_BYTE(最大256顶点)、GL_UNSIGNED_SHORT(最大65536顶点) |
实践选择策略
选择依据:场景优先
-
优先用glDrawArrays的场景:
- 渲染简单图形:如单个三角形、线段、点集(如粒子系统,每个粒子是独立点);
- 顶点无复用:如连续的独立三角形(无共用边/顶点);
- 追求代码简洁:无需管理IBO,适合快速验证功能(如测试着色器)。
-
优先用glDrawElements的场景:
- 复杂3D模型:如立方体(8个顶点→36个索引)、人物模型(数万顶点,大量复用);
- 需节省内存/带宽:移动设备内存有限,减少VBO体积可降低内存压力;
- 自定义顶点顺序:如绘制凹多边形(通过索引调整顶点顺序,避免图元异常)。
常见误区
-
“glDrawElements代码复杂,没必要用”:
虽然glDrawElements需多管理一个IBO,但现代开发中,建模工具(如Blender、Maya)导出模型时会自动生成索引,无需手动编写;且复杂模型的内存节省收益远大于代码复杂度。 -
“glDrawElements有索引查找,效率更低”:
现代GPU有专门的硬件单元优化索引查找(如索引缓存),额外开销极小;而减少顶点数据传输(内存带宽是移动设备的常见瓶颈)带来的效率提升,远超过索引查找的开销。 -
“索引类型随便选,用GL_UNSIGNED_BYTE就行”:
GL_UNSIGNED_BYTE仅支持最大256个顶点,超过此数量会导致索引溢出;复杂模型需用GL_UNSIGNED_SHORT(支持最大65536个顶点),OpenGL ES 3.0+还支持GL_UNSIGNED_INT(支持更多顶点)。
总结
glDrawArrays和glDrawElements的核心差异是是否通过索引复用顶点:前者简单直接,适合简单场景;后者高效灵活,是复杂3D渲染的标配。
二者没有绝对的“优劣”,而是针对不同场景的优化选择——理解顶点复用的价值,是掌握OpenGL ES渲染性能优化的关键一步。

