本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
通过阅读本文,你将获得以下收获:
1.Opengl EBO(Element Buffer Object)的作用和使用
2.如何使用VAO(Vertex Array Objects)优化程序
上篇回顾
上一篇博文一看就懂的OpenGL ES教程——缓冲对象优化程序(一)中介绍了OpenGL对象以及其中的Buffer Opbject相关概念,并详细介绍了VBO的作用和使用,但是常见的缓冲对象当然不止VBO,那么本文就继续开心地学习另外两种常见的缓冲对象EBO和VAO。
EBO(Element Buffer Object)
EBO全名Element Buffer Object是也,顾名思义,元素的缓冲对象,缓冲什么元素呢?
先回忆下上篇文章对于VBO优化程序的描述:
一帧需要渲染假如1万个三角形,帧率为30,那么即一秒中要传输90万个浮点数。那么,按照上面缓冲对象的解释,
是不是可以将顶点数据缓存在gpu中呢?假如渲染的图形都是不动的话(实际上即使需要变动,顶点数据也可以是固定的),那么其实只要传输一次顶点数据给gpu即可,即第一帧传递顶点数据即可,假如绘制每帧1万个三角形的话,那么只需要在第一帧传递3万个顶点坐标即可。
最后提到
只需要在第一帧传递3万个顶点坐标即可
3万个顶点坐标那就是至少9万个浮点数了,一看也是一个大数目,高级程序员本能反应肯定是:能否再优化一下?
假如我们要渲染的图像是类似以下的一个图:
咋一看三角形挺多的,那么顶点也非常多,但是敏锐的你一定能注意到一个特点:各个三角形的顶点也被其他很多三角形作为顶点使用了。
此时作为高级程序员的你将上面这句话和优化二字结合在一起,肯定能得出一个词:复用。
在程序开发领域,复用是一种很有效很常见的思维,能封装起来的代码肯定要封装起来,以后一行代码调用多香。那么EBO也是如此,它复用的元素就是顶点。
那么作为缓冲对象的它,缓冲的是什么?
此时请容许我先讲一个故事(方法)…
通过顶点索引绘制图元
之前我们如渲染指令使用的都是glDrawArrays方法绘制图元的,但是其实OpenGL服务还是比较到位的,已经提供了另一个绘制指令方法:
glDrawElements
它的作用就是根据顶点索引去绘制图元,而不像glDrawArrays方法直接绘制顶点。何为顶点索引呢?
比如当前的顶点属性数组是这样的,那么每个顶点都有一个自己的索引:
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
};
可以看出所谓索引就是当前顶点在顶点属性数组中按顺序的位置编号。
对于glDrawElements来说,它不是直接拿这一串顶点数组去绘制,而是要一个叫做索引数组的东西,假如此时要用上面4个顶点绘制2个三角形,那么索引数组如下:
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
官网 对glDrawElements方法定义是:
void glDrawElements | GLenum mode, |
|---|---|
GLsizei count, | |
GLenum type, | |
const void * indices; |
第一个参数mode:
和glDrawArrays方法一样,依然表示要绘制的具体图元类型(关于图元绘制相关,可以看下一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四))
第二个参数count:
表示要绘制多少个顶点,比如绘制一个三角形那就是3个顶点。
第三个参数type:
表示的是顶点索引的类型,必须是GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT其中一个种。
第四个参数indices:
就是索引数组的指针。
绘制代码什么样的呢?
顶点着色器:
#version 300 es
layout (location = 0)
in vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
void main() {
//直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
gl_Position = aPosition;
}";
片段着色器:
#version 300 es
precision mediump float;
out vec4 FragColor;
void main() {
FragColor = vec4(1.0 ,0.0 ,0.0 ,1.0);
};
都很简单~~
绘制代码:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//从indices中按顺序取出索引对应6个顶点依次进行绘制,图元类型为GL_TRIANGLES
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, indices);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
绘制出来的图形如图所示:
假设顶点(0.5f, 0.5f, 0.0f),(0.5f, -0.5f, 0.0f), (-0.5f, -0.5f, 0.0f), (-0.5f, 0.5f, 0.0f,)分别为V0,V1,V2,V3,则glDrawElements方法从indices中按顺序取出索引对应6个顶点依次进行绘制,取出来的顶点按顺序分别为:V0,V1,V3,V1,V2,V3。因为图元类型为GL_TRIANGLES,所以会使用索引为V0,V1,V3顶点绘制第一个三角形,V1,V2,V3顶点绘制第二个三角形。
glDrawElements方法的核心解决的是什么问题呢?聪明的你一定看出来了:通过缓存顶点索引,去实现顶点的复用.如果没有通过索引绘制图元,顶点数组应该是这样的:
float vertices[] = {
//第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
//第二个三角形
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f, // 左上角
0.5f, -0.5f, 0.0f, // 右下角
};
可以看出在我们这个例子中,是节省了2个顶点。如果是原来一帧3万个顶点的渲染的话,那可以节省的顶点数是非常可观的。
再回到EBO,它是一个缓冲对象,缓冲在哪呢?其实在上一篇文章一看就懂的OpenGL ES教程——缓冲对象优化程序(一) 已经说过OpenGL缓冲对象是缓冲在gpu中的,那EBO也是如此。所以同样作为Buffer Object的EBO和VBO非常相似,同样也是在gpu内部开辟一段内存区域,二者区别是VBO直接缓存顶点数据,而EBO缓存的是顶点的索引。,通过缓冲,解决了同样是cpu到gpu的数据传输成本问题。
使用EBO的代码:
unsigned int EBO;
//创建EBO缓冲对象
glGenBuffers(1, &EBO);
//绑定EBO缓冲对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//给EBO缓冲对象传入索引数据
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//解析顶点属性数据。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//通过顶点索引绘制图元,注意这里已经绑定了EBO,所以最后一个参数传入的内存是数据再EBO中内存的起始地址偏移量
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)0);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
//解绑EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDeleteBuffers(1, &EBO);
使用流程和VBO极为相似,主要区别点在于VBO的target类型是GL_ARRAY_BUFFER,而EBO是GL_ELEMENT_ARRAY_BUFFER。其他的这里就不赘述,不熟悉的话可以再看下一看就懂的OpenGL ES教程——缓冲对象优化程序(一)中VBO一小节的叙述。
此时,有些聪明的小伙伴可能会觉得不对劲……
刚才说通过索引绘制图元可以节省顶点个数,但是又增加了索引数组,这样下来好像也没怎么节省流量呀(传输缓存的数据)。
是的,比如上面绘制矩形的例子,用glDrawArrays的方式是6个顶点,一共18个浮点数,也就是64字节。
使用glDrawElements的方式是12个float类型顶点加上6个unsigned int索引,一样也是64字节,完全没有优化好么?!
列位看官且别急,咱再看一个例子~
我们再加上一个颜色属性看看:
顶点属性数组:
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
1.0, 0.0, 0.0,//右上角颜色
0.5f, -0.5f, 0.0f, // 右下角
0.0, 0.0, 1.0,//右下角颜色
-0.5f, -0.5f, 0.0f, // 左下角
0.0, 1.0, 0.0,//左下角颜色
-0.5f, 0.5f, 0.0f, // 左上角
0.5, 0.5, 0.5,//左上角颜色
};
索引数组依旧不变:
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
其中解析顶点属性数据调整为:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, vertices);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, vertices + 3);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
顶点着色器:
#version 300 es
layout (location = 0)
in vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
//输入的顶点的颜色 //如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0
layout (location = 1)
in vec4 aColor;
//输出的颜色
out vec4 vTextColor;
void main() {
//直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
gl_Position = aPosition;
//输出颜色属性
vTextColor = aColor;
}";
片段着色器:
#version 300 es
precision mediump float;
out vec4 FragColor;
void main() {
//这里改为接受传入的颜色属性
FragColor = vTextColor;
};
运行看看:
对此我只能说完美~
注意到索引数组依然不变,所以上面的说法有点不严谨,严谨的表达应该是:索引数组是一个顶点的所有属性的引用,即索引为0的时候,对应的数据不止有顶点坐标,是:
0.5f, 0.5f, 0.0f, // 右上角
1.0, 0.0, 0.0,//右上角颜色
那么如果使用原来的glDrawArrays的方式,顶点数组应该是:
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
1.0, 0.0, 0.0,//右上角颜色
0.5f, -0.5f, 0.0f, // 右下角
0.0, 0.0, 1.0,//右下角颜色
-0.5f, 0.5f, 0.0f, // 左上角
0.5, 0.5, 0.5,//左上角颜色
0.5f, -0.5f, 0.0f, // 右下角
0.0, 0.0, 1.0,//右下角颜色
-0.5f, -0.5f, 0.0f, // 左下角
0.0, 1.0, 0.0,//左下角颜色
-0.5f, 0.5f, 0.0f, // 左上角
0.5, 0.5, 0.5,//左上角颜色
};
共有36个float类型数共144个字节。
而如果用glDrawElements的方式是24个float类型顶点加上6个unsigned int索引,共120个字节,这样可以看出优化点了吧。如果顶点属性数组还包含法向量和矩阵,那么节省的只会越来越多。
VAO(Vertex Array Objects)
接下来本系列第三个缓冲对象也粉墨登场,那就是VAO(Vertex Array Objects)。
VAO是独立于Buffer Object的一种OpenGL对象,不同于Buffer Object缓存数据,它缓存的是状态,具体来说是顶点属性的状态。
我们先来看看不使用VAO的一个场景:
假如我们要画2个三角形,需要创建2个VBO来缓存顶点属性数据:
配置代码:
//第一个三角形顶点属性数组
static float triangleVerWithColor[] = {
0.0f, 0.8f, 0.0f,//顶点
1.0, 0.0, 0.0,//颜色
0.8f, 0.8f, 0.0f,//顶点
0.0, 1.0, 0.0,//颜色
0.0f, 0.0f, 0.0f,//顶点
0.0, 0.0, 1.0,//颜色
};
//第二个三角形顶点属性数组
static float triangleVerWithColor1[] = {
0.0f, -0.8f, 0.0f,//顶点
1.0, 0.0, 0.0,//颜色
-0.8f, -0.8f, 0.0f,//顶点
0.0, 1.0, 0.0,//颜色
0.0f, 0.0f, 0.0f,//顶点
0.0, 0.0, 1.0,//颜色
};
//创建2个VBO
unsigned int VBOs[2];
glGenBuffers(2, VBOs);
//绑定第一个VBO
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerWithColor), triangleVerWithColor, GL_STATIC_DRAW);
//解析第一个VBO的顶点属性数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, (void*)(3*4));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
//解绑第一个VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
//绑定第二个VBO
glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerWithColor1), triangleVerWithColor1, GL_STATIC_DRAW);
//解析第二个VBO的顶点属性数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, (void*)(3*4));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
现在只绘制第一个VBO,绘制代码:
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//绑定第一个VBO
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
运行如下:
咦,第一个VBO缓存的顶点坐标是:
static float triangleVerWithColor[] = {
0.0f, 0.8f, 0.0f,//顶点
1.0, 0.0, 0.0,//颜色
0.8f, 0.8f, 0.0f,//顶点
0.0, 1.0, 0.0,//颜色
0.0f, 0.0f, 0.0f,//顶点
0.0, 0.0, 1.0,//颜色
};
不是都是正的么,应该都在右上角才是啊。
而这里显示的三角形位置,不就是第二个三角形的顶点坐标么:
static float triangleVerWithColor1[] = {
0.0f, -0.8f, 0.0f,//顶点
1.0, 0.0, 0.0,//颜色
-0.8f, -0.8f, 0.0f,//顶点
0.0, 1.0, 0.0,//颜色
0.0f, 0.0f, 0.0f,//顶点
0.0, 0.0, 1.0,//颜色
};
所以,这里有个“残酷”的事实:
后面通过glVertexAttribPointer方法配置数据的VBO会覆盖前面VBO的配置数据。
怎么解决呢?我们必须有个可以缓存配置数据状态的东西,于是,VAO便应运而生。
VAO是专门用来记录Buffer Object中缓存顶点属性的缓冲对象的状态配置信息的。
VAO的使用和VBO整体也是类似的,只是一些细节不一样:
配置阶段:
graph TD
创建缓冲区 --> 绑定缓冲区到上下文 --> 将VBO或者EBO等缓冲的配置操作缓存起来 --> 解绑缓冲区 --> 删除缓冲区
绘制阶段:
graph TD
绑定缓冲区到上下文 --> 将VBO或者EBO等缓冲的配置数据进行绘制 --> 解绑缓冲区
反映到代码里就是:
配置阶段:
graph TD
glGenVertexArrays --> glBindVertexArray绑定VAO --> BufferObject相关配置操作 -->
glBindVertexArray解绑操作
绘制阶段:
graph TD
glBindVertexArray绑定VAO --> 使用BufferObject相关配置绘制 -->
glBindVertexArray解绑操作
添加VAO具体配置代码:
unsigned int VBOs[2];
unsigned int VAOs[2];
//创建2个VAO
glGenVertexArrays(2, VAOs); // we can also generate multiple VAOs or buffers at the same time
glGenBuffers(2, VBOs);
//绑定VAO[0],从此在解绑VAO之前的所有对VBOs[0]的操作都会记录在VAO内部
glBindVertexArray(VAOs[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerWithColor), triangleVerWithColor, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, (void*)(3*4));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//解绑VAO
glBindVertexArray(0);
//绑定VAO[1],从此在解绑VAO之前的所有对VBOs[1]的操作都会记录在VAO内部
glBindVertexArray(VAOs[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerWithColor1), triangleVerWithColor1, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, (void*)(3*4));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//解绑VAO
glBindVertexArray(0);
绘制代码:
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//绘制的时候再次绑定VAO[0],表示后面的绘制数据取自VAO[0]缓存的VBO[0]的川村数据
glBindVertexArray(VAOs[0]);
//原来绑定解绑VBO的代码可以去掉了,因为VBO的状态已经缓存在VAO了
// glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glDrawArrays(GL_TRIANGLES, 0, 3);
// glBindBuffer(GL_ARRAY_BUFFER, 0);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
//解绑VAO[0]
glBindVertexArray(0);
代码咋一看一堆bindXX好像很复杂,但实际上只要牢牢抓住一个要点即可:在一个缓冲的bind和unbind之间的代码都是对这个缓冲的配置或者绘制。
运行看下:
完美至极~
回顾下我们具体做了什么:
首先我们要画2个三角形,于是用了2个VBO去缓存顶点属性数据,先后配置了2个VBO后,发现后面一个VBO的配置数据被前面一个VBO覆盖了。正当你很焦虑的时候,VAO挺身而出,你分别使用2个VAO记录了2个VBO的配置数据,这样在绘制的时候,只要绑定其中一个VAO,就可以直接绘制其记录的那个VBO的顶点属性数据了。
三种缓冲对象综合使用
着色器不需要改变,配置代码为:
unsigned int VBO;
unsigned int VAO;
glGenVertexArrays(1, &VAO); // we can also generate multiple VAO or buffers at the same time
glGenBuffers(1, &VBO);
//依次绑定VAO,VBO,EBO,顺序不能错
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, vertices);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, vertices + 3);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
//解绑顺序和绑定要相反
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
绘制代码:
glBindVertexArray(VAO);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)0);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
glBindVertexArray(0);
另外要提到VAO另外一个好处,就是在绘制上因为状态已经被缓存了,就不需要重新具体去绑定VBO,EBO等缓冲,从性能角度上有一定的优化,另外也能节省代码,让代码更加简洁。
如果没有使用VAO,则绘制代码如下:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)0);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
可以看出这里还需要再次具体去绑定一个个缓冲对象,而使用VAO则一次性绑定VAO即可。
最后祭出一张著名的图来说明这三者的关系:
(图来源于: 你好,三角形)
总结
本文主要介绍了EBO和VAO的作用和使用,通过本文可以理解如何用EBO和VAO去优化程序。下一篇文章,将进入一个新的小领域,也是OpenGL非常重要和有趣的东西——纹理的绘制。
代码地址
opengl-es-study-demo (不断更新中)
参考
你好,三角形
www.khronos.org/opengl/wiki…
OpenGL Object
应该怎么理解 OpenGL 的 VAO 与 VBO
熟悉 OpenGL VAO、VBO、FBO、PBO 等对象,看这一篇就够了
原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~
系列文章目录
体系化学习系列博文,请看音视频系统学习的浪漫马车之总目录
实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目
轻松入门OpenGL系列
一看就懂的OpenGL ES教程——图形渲染管线的那些事
一看就懂的OpenGL ES教程——再谈OpenGL工作机制
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)
一看就懂的OpenGL ES教程——缓冲对象优化程序(一)
一看就懂的OpenGL ES教程——缓冲对象优化程序(二)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)