一.前言
在这里,我会通过一个空气曲棍球游戏来一步步介绍OpenGL ES 3.0的相关内容。空气曲棍球游戏的规则是:我们首先需要一个有两个球门的长方形桌子,一个冰球和两个用来击打冰球的木槌;在每个回合开始前,冰球都会放在桌子的中间,每个玩家要尽力把冰球击进对方的球门,同时要防御对方的进攻,每进一球得一分,获得7分后就意味着该玩家获得了游戏的胜利。
二.定义空气曲棍球的桌子结构
在桌子绘制在屏幕之前,我们需要告诉OpenGL要画什么。开发过程的第一步,我们需要以OpenGL可以理解的形式定义一个桌子,在OpenGL中,所有东西的结构都是从一个顶点开始。接下来,我们给出顶点的定义:简单的说,一个顶点就是代表几何对象拐角的点,这个点有许多的属性,最重要的属性就是位置。为了简单起见,我们用一个长方形代表桌子结构,那么我们只需要定义4个顶点即可。
三.OpenGL中的点,直线和三角形
OpenGL只支持绘制点,直线和三角形。三角形是最基本的几何图形,因为它的结构非常稳定,拿掉一个点之后就成了直线了,再拿掉一个点之后就只剩一个点了。点和直线可以用于某些效果,只有三角形才能用来构建拥有复杂对象和纹理的场景。在OpenGL中,我们把一系列的点放到一个数组里去构建三角形,然后告诉OpenGL如何去连接这些点。我们想要构建的所有物体都需要用点,直线和三角形定义,现在我们想要绘制一个长方形,但OpenGL不能直接绘制长方形,所以我们可以绘制两个三角形来拼凑一个长方形。接下来,我们需要给桌子添加一个中间线,并绘制两个点来表示木槌,这是很容易做到的。
四.使数据可以被OpenGL存取
我们已经完成了顶点的定义了,但是在OpenGL存取他们之前,我们还需要完成另外一步。这里存在的主要问题是我们所编写的代码的运行环境和OpenGL的运行环境使用了不同的语言,我们编写的java/kotlin代码运行在Dalvik虚拟机上,运行在虚拟机上的代码不能直接访问本地环境,除非通过特定的api。而且Dalvik虚拟机还使用了垃圾回收机制,当虚拟机检测到一个变量,对象或其他内存片段不再使用的时候,就会把这些内存释放掉以备重用。但OpenGL是运行在本地环境中的,本地环境并不是这样工作的,它不期望内存块会被移来移去或者自动释放,也就是说本地环境是没有垃圾回收机制的。那么,我们所编写的代码运行在虚拟机上,它怎么和OpenGL通信呢?有两种技术,一种是JNI技术,当调用android.opengl.GLES30包里面的方法时,实际上就是通过JNI技术在后台调用本地系统库的方法。第二种技术是改变内存的分配方式,java有一个特殊的类集合,可以分配本地内存块,并且把java的数据复制到本地内存,本地内存可以被本地环境存取,而不受垃圾回收器的管控。传输数据的方式如下图所示:
下面给出定义长方形顶点和分配本地内存的代码:
private var vertexData:FloatBuffer
init{
val tableVertices=floatArrayOf(
//Triangle one
0f,0f,
9f,14f,
0f,14f,
//Triangle two
0f,0f,
9f,0f,
9f,14f,
//Mid Line
0f,7f,
9f,7f,
//Mallets
4.5f,2f,
4.5f,12f
)
//分配本地内存块
vertexData=ByteBuffer.allocateDirect(tableVertices.size* BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())//按照本地字节序组织内容
.asFloatBuffer()
vertexData.put(tableVertices)
}
companion object{
private val POSITION_COMPONENT_COUNT=2//记录一个顶点有两个分量
private val BYTES_PER_FLOAT=4//每个浮点数4个字节
}
五.引入OpenGL管道
现在,我们已经定义了空气曲棍球桌子的结构,并把这些数据复制到了OpenGL可以存取的本地内存,在把曲棍球桌子画到屏幕上之前,他需要在OpenGL管道中传递,这就需要使用着色器了。这些着色器会告诉图形处理单元如何绘制这些数据,有两种类型的着色器,在绘制任何内容到屏幕上之前,都需要定义他们。
- 顶点着色器:生成每个顶点的最终位置,针对每个顶点,它都会执行一次,一旦最终位置确定,OpenGL会将这些顶点组装成点,直线和三角形
- 片段着色器:为组成点,直线,三角形的每个片段生成最终的颜色,针对每个片段,它都会执行一次,一个片段是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素
一旦最终的颜色生成了,OpenGL就会把他们写在一个称为帧缓冲区的内存块,然后Android会把这个帧缓冲区显示在屏幕上。整个流程如下图所示:
光栅化图元是指将每个点,直线和三角形分解成大量的小片段,他们可以映射到移动设备显示屏的像素上,从而生成一副图像。
接下来,我们需要创建顶点着色器和片段着色器,这需要用到GLSL语言,他是OpenGL的着色语言,和c语言类似。我们需要在res文件夹下新建一个raw资源文件夹,然后在下面新建一个simple_vertex_shader.glsl文件,内容如下:
#version 300 es
layout(location=0) in vec4 a_Position;
void main() {
gl_Position=a_Position;
}
开头先申明opengl es的版本为3.0,in关键字用于声明输入变量,通常在顶点着色器中接收顶点数据,或者在片段着色器中接收插值后的数据,out关键字用于声明输出变量,一般是指从顶点着色器传递给片段着色器的数据,没有out变量则会直接输出,layout关键字用于指定输入和输出变量的位置,gl_Position是OpenGL中一个内建的变量,用于指定顶点的位置。vec4是一个包含4个分量的向量,在这里分别指x,y,z,w,其中x,y,z表示一个三维位置,w是一个特殊的坐标,后续会进行说明。如果这些值没有指定,默认情况下,前三个会赋值为0,w会赋值为1。
然后,我们再定义一个片段着色器,命名为simple_fragment_shader.glsl,这个着色器会为每个片段生成最终的颜色,片段着色器的内容如下:
#version 300 es
uniform vec4 u_Color;
out vec4 fragColor;
void main() {
fragColor=u_Color;
}
uniform声明的变量的指一般由cpu端的应用程序设置,而不能在着色器内部直接修改。fragColor是一个向量,在这里包括四个分量,分别指红绿蓝和透明度。