请添加图片描述

glDrawArrays与glDrawElements

在OpenGL ES渲染管线中,glDrawArraysglDrawElements是触发图元渲染的两大核心函数。

二者的核心差异在于顶点数据的使用方式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_POINTSGL_LINESGL_TRIANGLES 渲染三角形用GL_TRIANGLES
first GLint 顶点缓冲区中起始顶点的索引(从0开始) 从第0个顶点开始取数据用0
count GLsizei 需读取的顶点总数 渲染2个三角形(6个顶点)用6

1.3 工作流程

  1. 绑定顶点缓冲区:通过glBindBuffer(GL_ARRAY_BUFFER, vboId)绑定存储顶点数据的VBO;
  2. 配置顶点属性:通过glVertexAttribPointer指定顶点数据的格式(如每个顶点3个位置分量);
  3. 触发渲染:调用glDrawArrays,GPU按顺序从VBO的first位置读取count个顶点;
  4. 组装图元:根据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 工作流程

  1. 绑定VBO和IBO:分别绑定存储顶点数据的VBO和存储索引数据的IBO;
  2. 配置顶点属性:与glDrawArrays一致,指定顶点数据格式;
  3. 触发渲染:调用glDrawElements,GPU先从IBO读取count个索引;
  4. 索引查顶点:根据每个索引的值,到VBO中查找对应的顶点数据;
  5. 组装图元:按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顶点)

实践选择策略

选择依据:场景优先

  1. 优先用glDrawArrays的场景

    • 渲染简单图形:如单个三角形、线段、点集(如粒子系统,每个粒子是独立点);
    • 顶点无复用:如连续的独立三角形(无共用边/顶点);
    • 追求代码简洁:无需管理IBO,适合快速验证功能(如测试着色器)。
  2. 优先用glDrawElements的场景

    • 复杂3D模型:如立方体(8个顶点→36个索引)、人物模型(数万顶点,大量复用);
    • 需节省内存/带宽:移动设备内存有限,减少VBO体积可降低内存压力;
    • 自定义顶点顺序:如绘制凹多边形(通过索引调整顶点顺序,避免图元异常)。

常见误区

  1. “glDrawElements代码复杂,没必要用”
    虽然glDrawElements需多管理一个IBO,但现代开发中,建模工具(如Blender、Maya)导出模型时会自动生成索引,无需手动编写;且复杂模型的内存节省收益远大于代码复杂度。

  2. “glDrawElements有索引查找,效率更低”
    现代GPU有专门的硬件单元优化索引查找(如索引缓存),额外开销极小;而减少顶点数据传输(内存带宽是移动设备的常见瓶颈)带来的效率提升,远超过索引查找的开销。

  3. “索引类型随便选,用GL_UNSIGNED_BYTE就行”
    GL_UNSIGNED_BYTE仅支持最大256个顶点,超过此数量会导致索引溢出;复杂模型需用GL_UNSIGNED_SHORT(支持最大65536个顶点),OpenGL ES 3.0+还支持GL_UNSIGNED_INT(支持更多顶点)。

总结

glDrawArraysglDrawElements的核心差异是是否通过索引复用顶点:前者简单直接,适合简单场景;后者高效灵活,是复杂3D渲染的标配。

二者没有绝对的“优劣”,而是针对不同场景的优化选择——理解顶点复用的价值,是掌握OpenGL ES渲染性能优化的关键一步。
请添加图片描述

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐