前言
OpenGL是一种包含了一系列可以操作图形图像函数的图形编程接口的规范标准,它规范了每个函数该如何执行以及它们的输出值,OpenGL ES是OpenGL的一个精简子集,主要用于手持和嵌入式设备,OpenGL ES很大程度上兼容了OpenGL,主要是删除了一些OpenGL的一些冗余接口和引入一些新功能提高处理手持设备和嵌入式设备的特定性能和功耗限制,因此在写此博客时主要是与OpenGL的对比过程中学习的领悟,学习OpenGL对于开发OpenGL ES Android程序应该能够让更好的知道是如何实现,而不仅仅停留在怎么实现上吧(看完《OpenGL ES应用开发实践指南 Android卷》的我貌似知道了OpenGL如何初步实现简单的Android程序,但是好像总是有很多地方知其然不知其所以然),其实写博客的目的也只是为了反思自己学了些什么内容,就当写日记了吧。
学习时所学习的资料是learnOpenGl网站(这个网站上的示例貌似都是使用的C实现的,我会尽量努力将它改成Java实现吧),结合学习过OpenGL ES的前辈给出的学习建议和在学习网站上的内容之前看过的《OpenGL ES应用开发实践指南 Android卷》
另外网站大部分知识基于OpenGL 3.3,正好对应的是OpenGL ES3.0
相关概念
- OpenGL支持扩展的特性,开发者可以使用这个特性提供一些更先进更有效的图形功能,且不必等待一个新的OpenGL规范出现,使用新的渲染特性时,只需检查一下设备是否支持此扩展
- OpenGL的状态机: OpenGL的状态通常被称为OpenGL上下文,状态机是一系列变量用于描述OpenGL此刻应当如何运行,譬如通过设置GL_LINES这个上下文变量来改变OpenGL状态,从而告诉OpenGL去绘制直线,当改变了状态,设置为GL_TRIANGLES,下一个绘制命令就会画出三角形;OpenGL也提供了一些状态设置函数和状态使用函数,前者用于改变上下文,后者用于根据当前状态执行一些操作
- 对象:OpenGL内核是C库,虽然支持多语言派生,由于C语言的一些语言结构不易翻译成其他高级语言,因此OpenGL开发时引入了一些抽象层,对象是其中之一,对象是指一些选项的集合,代表OpenGL状态的一个子集,在OpenGL中使用场景广泛,譬如在为物体表面附着纹理、将顶点数组注入顶点缓冲区等场景下,都会先创建一个对象,然后返回对象ID,再通过对ID的操作来操作对象
- 通过操作对象来进行的常见OpenGL工作流:创建一个对象,然后用一个id保存对它的引用(实际数据保存在后台);将对象绑定至上下文的目标位置;然后对这个对象进行需要的设置;最终将这个对象与id解绑,之前的设置保存在创建的对象中。这个工作流在之后会再见,到时候结合案例和代码将更加容易理解
- 图形渲染管线:一堆原始图像数据途径一个输送管道,期间经过各种变化处理最终出现在屏幕的过程,3D坐标经过OpenGL的图形渲染管线后转化为适应屏幕的有色2D像素,图形渲染管线主要分为两部分:先是将3D坐标转化为2D坐标,再见2D坐标转化为实际的有颜色的像素;OpenGL的渲染管线及OpenGL ES 3.0图形管线如下所示,与两张图片对比可看出,OpenGL主要是多了一个几何着色器的过程
- OpenGL图形渲染管线各个阶段会将前一阶段的输出作为输入,这些阶段是由高度专门化的特定函数执行,并且很容易并行执行;
- 渲染管线各阶段简析(以创建一个三角形为例):①首先以数组的形式传递用于表示一个三角形的3个3D坐标顶点作为输入,这个数组记为顶点数据;②顶点数据中每一个单独顶点作为输入传入顶点着色器,顶点着色器会将3D坐标转换成另一种3D坐标,同时对顶点属性进行一些基本的处理;③处理完的顶点作为输入装配成指定的图元形状(点、直线、三角形);
④图元形式的一系列顶点的集合传递给几何着色器,通过产生新顶点构造出新的或其他图元来生成其它形状;④’'几何着色器的输出会被传入光栅化阶段,图元被映射为最终屏幕上的相应像素,同时会对超出视图外的所有像素进行裁切,以提高执行效率;⑤光栅化生成片段将供片段着色器使用,计算出像素的最终颜色,实现OpenGL高级效果(如光照、纹理等);⑥最后将所有确定颜色值的对象传入最后一个阶段,进行α测试和混合,这个阶段检测片段对应的深度和模板值,用来确定当前像素是在其他物体前面还是后面,并对在后面的像素进行丢弃,这个阶段也会检测用于定义物体透明度的α值并对物体进行混合,所以即使在片段着色器中计算出了一个像素输出的颜色,在渲染多个三角形后,像素颜色也有可能完全不同 - 归一化设备坐标:以屏幕中心为原点(0,0),以向上为y轴正方向,以向右为x轴正方向,且归一化设备坐标中的任意一点的方向分量值均在-1.0f到1.0f之间;通过使用glViewport函数进行视口变换可以实现归一化设备坐标到屏幕空间坐标的转换
顶点与顶点缓冲对象
- 顶点:一个顶点是一个3D坐标的数据的集合,关于顶点的数据是使用顶点属性进行描述的,顶点属性可以包括:顶点位置、顶点颜色等;在开始绘制图形前,须先给OpenGL输入一些顶点数据,OpenGL是一个3D图库,它并不是简单的把指定的3D坐标转化为屏幕上的2D像素,仅当3D坐标点在归一化设备坐标范围中时才会处理它
- 通过Java代码初始化的顶点数组存储在JVM上,而OpenGL运行在本地环境中,是无法直接读取JVM上的顶点数据的,通常会采用改变内存的分配方式的方法,把数据从Java堆中复制到本地堆,Java中有一个特殊的集合可以分配本地内存块,并把Java数据复制到本地内存中
private final float[] vertexData = {
...}
FloatBuffer vertexArray = ByteBuffer.allocateDirect(vertexData.length * 4) //分配一块本地内存块,分配时需要知道分配多少字节,每个浮点数占4字节
.order(ByteOrder.nativeOrder()) //告诉字节缓冲区按照本地字节序列组织内容,这个顺序并不重要,重要的是全平台采用相同的顺序
.asFloatBuffer() //可以得到一个反映底层字节的FloatBuffer实例,直接使用浮点数
.put(vertexData); //把数组数据从JVM中复制到本地内存中,通常情况下,进程结束,内存释放
- 顶点缓冲对象:可以在GPU内存中存储大量的顶点,可以一次性发送一大批数据,而不是每个顶点送一次,提高性能,顶点缓冲对象是目前接触的第一个OpenGL对象,我们可以通过它来回顾一下OpenGL的工作流
//创建对象,同时将生成的对象id保存在buffers数组中
final int buffers[] = new int[1];
glGenBuffers(buffers.length, buffers, 0);
//判断对象创建是否成功,如果创建失败buffers数组中会存入0,否则,则会存入对象id
if(buffers[0] == 0) {
//TODO:throw exception or log
}
bufferId = buffers[