OpenGL ES for Android(一) 之画个三角形

android studio 下载 | 2019-03-03 17:45

这是OpenGL ES for Android系列文章的第一篇,在阅读本文之前,希望您对Android开发和Kotlin有一定的了解。如果对于以上内容还不太了解,建议您先了解一下以上内容,再继续阅读本文。

在Android中要进行OpenGL 环境的初始化就不得不提到GLSurfaceView。那么什么是GLSurfaceView呢?官方给它的定义是SurfaceView的一种实现,使用了专用的Surface完成对OpenGL渲染对象的显示。简单的理解为它就是一块画布用来呈现OpenGL渲染出来的图像。

光有画布还不成,还得需要一只画笔来绘制图像。这里再引入一个概念GLSurfaceView.Render,这是一个接口其中定义了三个方法:

onSurfaceCreated(GL10 gl, EGLConfig config)

在Surface完成创建或者重新创建时调用,因为Activity切换到后台换时,Surface环境会释放,所以可能存在多次调用的情况

onSurfaceChanged(GL10 gl, int width, int height)

在Surface创建之后合当surface大小发生变化时调用,例如横竖屏切换时。

onDrawFrame(GL10 gl)

真正绘制每一帧的地方,这个方法被调用后将会在屏幕上展现绘制的内容,所以无论有没有做什么都要绘制一些东西,否则可能导致屏幕闪烁。

GLSurfaceView提供了setRender(GLSurfaceView.Render)方法,可以将实现的Render传递进去在一个专门的线程(一般称之为GLThread)完成对OpenGL对象的渲染。

注意:因为在一个GLSurfaceView专门的线程中进行的Renderer,而Android UI是在主线程中,所以二者进行交互时要注意线程切换的问题。

接下来我们打开Android Studio创建一个工程,至于工程怎么命名那就随意了。然后打开刚刚创建好的工程创建一个新的TriangleActivity就像下面这样,

class TriangleActivity : AppCompatActivity() {

private lateinit var mGLSurfaceView: GLSurfaceView

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

mGLSurfaceView = GLSurfaceView(this)

// 设置 OpenGL ES 的环境版本为2.0

mGLSurfaceView.setEGLContextClientVersion(2)

setContentView(mGLSurfaceView)

接下来我们再创建一个class TriangleGLRender就像下面这样:

class TriangleGLRender(val context: Context) : GLSurfaceView.Renderer {

override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {

override fun onDrawFrame(gl: GL10?) {

这里看到每一个方法都还是空实现,不着急我们先回到TriangleActivity 的 onCreate()方法在 mGLSurfaceView.setEGLContextClientVersion(2)后面添加这么2段代码:

mGLSurfaceView.setRenderer(TriangleGLRender(this))

mGLSurfaceView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY

setRenderer上面已经解释过了,这里说一下renderMode,他只能在setRenerer之后调用,有两种模式可以设置分别是 RENDERMODE_CONTINUOUSLY、 RENDERMODE_WHEN_DIRTY。前面一种是会反复的重绘,后一种则之会在surface创建的时候和 GLSurfaceView调用了requestRender的时候进行绘制,至于用哪一种就看需求了。因为我们不需要反复的去绘制,所以我们这里选择了 RENDERMODE_WHEN_DIRTY。

GLSurfaceView基本上配置完成,接下来我们来处理一下Activity的生命周期,在TriangleActivity中添加如下代码:

override fun onResume() {

super.onResume()

mGLSurfaceView.onResume()

override fun onPause() {

super.onPause()

mGLSurfaceView.onPause()

这一步至关重要,如果不进行设置可能导致app发生问题乃至闪退。因为OpenGL的绘制都是在专门的GLThread中进行绘制的,如果当我们的Activity不可见时,就要暂停OpenGL的绘制了,当再次可见时就重新开始绘制。这一步的目的就是GLThread唤醒和暂停。

开始绘制之前我们来了解一下着色器语言GLSL(openGL Shading Language),格式和C语言很类似。当我们要用OpenGL操作GPU时就需要用到这种语言编写着色器。这里提到了着色器,着色器呢主要分为两种:顶点着色器(Vertex shader)和片源着色器(Fragment shader)。 可以这么理解顶点着色器就是用来定位要绘制的图像坐标,而片源着色器就是对每一个顶点着色器定位范围内的像素进行上色。

好了现在我们开始在res/raw文件夹下新建一个vertex_shader.glsl的文件,然后在其中写上这段代码

attribute vec4 a_Position;

void main(){

// 分配最终坐标 给当前顶点

gl_Position = a_Position;

这是一种固定的格式,像C语言一样有一个main()函数作为程序的唯一入口,vec4 表示一个四维向量还有vec2,ve3等几种类型,attribute 表示将如何给aPosition进行赋值。 定义完顶点着色器,接着还是在raw目录下新建一个fragmentshader.glsl,同样写上如下代码:

precision mediump float; // lowp mediump highp(只支持部分实现) 三种精度

uniform vec4 u_Color; // r,g,b,a

void main(){

gl_FragColor = u_Color;

和顶点着色器一样main()函数作为唯一入口,glFragColor 要传递给GPU绘制的颜色。precision 精度表示(lowp mediump highp(只支持部分实现) 三种精度),一般来说定义为mediump就可以了, uniform 同理表示如何将值赋值给uColor。 那么如何使用刚刚编写好的着色器呢?

在工程里新建一个util包,新建一个object,命名为 ShaderHelper在其中新写一个函数

fun compileShader(type: Int, shaderCode: String){

val shaderObjectId = GLES20.glCreateShader(type) // 根据shader类型 创建shaderid 也就是当前shader的引用

* shaderObjectId == 0 表示编译失败,类似于java 的 null

* shaderObjectId 表示 OpenGL 对象的一个引用,无论后面如何引用这个对象,传入这个id即可

if (shaderObjectId == 0) {

Log.e(TAG, " crate shader fail")

* 构建完 shaderObjectId ,然后上传写好的 shaderCode 到 OpenGL

* 建立起 shaderObjectId 和 shaderCode 之间的关联

GLES20.glShaderSource(shaderObjectId, shaderCode) // 关联 shaderObjectId 和 shaderCode

* 因为 shaderCode 已经和 shaderObjectId 建立了关联 所以这里传入 shaderObjectId 就可以完成编译

GLES20.glCompileShader(shaderObjectId) // 编译shader

val compileStatus = IntArray(1) // 接收编译状态的数组

* 接收编译状态 并将编译状态存入 compileStatus[0] 返回

GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0) // 获取编译状态

* GLES20.glGetShaderInfoLog(shaderObjectId) 可以获取到当前shader的详细信息

* 这里打印出编译时的信息,已告知我们可能出错的原因

Log.d(TAG, "Results of compiling source: \n $shaderCode \n ${GLES20.glGetShaderInfoLog(shaderObjectId)}")

* 同理 如果 状态是0 则编译失败 这里就删除申请的 shader 对象

if (compileStatus[0] == 0) {

GLES20.glDeleteShader(shaderObjectId) // 删除shader

Log.w(TAG, "Compilation of shader failed.")

return shaderObjectId

代码中的注释已经比较详细了,这里说一下 // 这里的shadercode就是之前我们写好的着色器语言了 GLES20.glShaderSource(shaderObjectId,shadercode)

现在我们回到 TriangleGLRender在 onSurfaceCreated中添加如下代码:

GLES20.glClearColor(0f, 0f, 0f, 0f)

// 从raw文件中读取shader 源码

val vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader)

val fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader)

* 编译shader 并获取已经编译好的shader对象

val vertexShader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource)

val fragmentShader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource)

通过 ShaderHelper.compileShader 就将我们需要的着色器语言编写好了,并获取到了OpenGL给其分配的id引用。但是顶点着色器和片源着色器总是成对出现的,意思就是说有一个顶点着色器就相应的要有一个片源着色器,这样才能操作GPU并给你想操作的位置绘制上你喜欢的颜色。刚编译完的两个shader目前还是处于各自为战的状态,所以接下来让我们来将他们绑定在一起。 继续回到 ShaderHelper给其添加如下代码:

* program 绑定 顶点着色器 和 片源着色器 因为二者总是紧密结合的

fun linkProgram(vertexShaderId: Int, fragmentShaderId: Int): Int {

* 创建 program 对象并将其引用返回给 programObjectId

val programObjectId = GLES20.glCreateProgram() // 创建 program 对象

if (programObjectId == 0) {

Log.w(TAG, "could not crate new program.")

* glAttachShader 给 program 对象 吸附上 着色器

GLES20.glAttachShader(programObjectId, vertexShaderId) // 绑定 program 和 shader

GLES20.glAttachShader(programObjectId, fragmentShaderId)

GLES20.glLinkProgram(programObjectId) // 链接 两着色器到一块儿

val linStatus = IntArray(1)

GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linStatus, 0)

Log.v(TAG, "Results of linking program:\n ${GLES20.glGetProgramInfoLog(programObjectId)}")

if (linStatus[0] == 0) {

GLES20.glDeleteProgram(programObjectId)

Log.w(TAG, "Linking of program failed.");

return programObjectId

这里我们定义了linkProgram函数可以将编译好的顶点着色器和片源着色器对象传递给它,然后由它完成将顶点着色和片源着色器绑定在一起的艰巨任务。 接着回到onSurfaceCreated方法在它后面添加

* 绑定片源和顶点着色器

program = ShaderHelper.linkProgram(vertexShader, fragmentShader)

这里就拿到了绑定好两着色之后的program对象了。要使用它我们还要继续在后面添加如下代码

* 使得opengl 能使用这个program来进行操作

GLES20.glUseProgram(program)

还记得我们之前在顶点着色器中定义的 a_Position和片源着色器中定义的 u_Color变量吧。现在我们来从代码中获取到这两变量,首先在 TriangleGLRender中定义两全局变量:

private var uColorLocation = 0 // u_Color

private var aPositionLocation = 0 // u_Position

接着在 onSurfaceCreated最后写上如下代码:

uColorLocation = GLES20.glGetUniformLocation(program, "u_Color")

aPositionLocation = GLES20.glGetAttribLocation(program, "a_Position")

这里获取的原则是不是很简单,着色器语言怎么定义的类型我们就使用那种函数就能获取到编译好的着色器里面对应的变量了。

获取完变量,那就要对变量进行赋值了,首先我们给 aPositionLocation赋值,因为只有确定了我们要绘制的位置,我们才好进行上色操作。 给 aPositionLocation赋值我们需要调用如下函数:

public static void glVertexAttribPointer(

boolean norma=lized,

int stride,

知道了每一个参数的意义,现在开始准备要传入的参数。在TriangleGLRender类首部定义一个float类型的数组以表示我们要传入的坐标:

private val vertex = floatArrayOf(

0.0f, 0.5f,

-0.5f, -0.5f,

0.5f, -0.5f

数组中的元素,连个一组组成一组x,y坐标。这里我们定义了三个点,分别表示要绘制的三角形的三个顶点坐标。注意这里和我们平时所理解的view坐标系不一样。在OpenGL中整个坐标系的范围是[-1.0,1.0]之间,下面这张图就很好的解释了OpenGL坐标系在屏幕中的对应关系。

屏幕的左上角是(-1.0,1.0),最右下角是(1.0,-1.0)。 很好理解是不是。 这还只是一个kotlin中的float数组还并不能直接传递给OpenGL使用,所以接下来我们还要构造一个 ByteBuffer对象,借助它的力量我们就能够把这么一个数组,给推入到OpenGL中了。

private val floatBuffer = ByteBuffer.allocateDirect(vertex.size * 4)

.order(ByteOrder.nativeOrder())

.asFloatBuffer()

allocateDirect 指要申请多大一块内存,这里是顶点数组大小*4因为每个float的大小是4byte,所以要乘以4 定义好了 floatBuffer接下来在类的 init块中写下如下代码:

floatBuffer.put(vertex)

这就将定义好的顶点坐标数组放入到了floatBuffer中了。 参数准备妥当,现在回到onSurfaceCreated方法在其最后面写上

floatBuffer.position(0) // 从buffer起始位置读数据

GLES20.glVertexAttribPointer(aPositionLocation, 2, GLES20.GL_FLOAT, false

, 0, floatBuffer)

顶点坐标已经就位,现在开始给顶点坐标指定范围内的像素涂上我们喜欢的颜色吧。 定位到 onDrawFrame方法,在其中写下如下代码

GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 清屏

GLES20.glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f) // 给 uColorLocation 赋值 r,g,b,a

GLES20.glEnableVertexAttribArray(aPositionLocation)

GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)

GLES20.glDisableVertexAttribArray(aPositionLocation)

代码很少,也很好理解。对于Attribute类型需要先调用glEnableVertexAttribArray才能够生效,并且在绘制完后还要glDisableVertexAttribArray掉。glDrawArrays就是最终的绘制三角形的代码了 GLES20.GL_TRIANGLES表示我们绘制的是三角形,第二个参数表示起始位置传0即可,第三个参数表示顶点个数,因为只定义了3个顶点所以传3就行。

好了现在就run起我们的代码吧,如果你在屏幕中看到了如下图像说明我们绘制成功了。

本篇主要讲述了如何在Android中初始化GL环境以及如何编译shader并完成对图像的绘制。

在GL环境初始化中主要涉及到了GLSurfaceView和GLSurfaceView.Render,分别讲述了onSurfaceChanged、onSurfaceChanged、onDrawFrame的作用。

在绘制环节,主要是大致讲述了什么是着色器语言,以及如何对着色器进行编译链接,以及如何获取到定义其中的变量并为其赋值,最后讲述了如何完成三角形的绘制。

OpenGL只能绘制点、线、三角形,那么如果我要绘制一个矩形改如何实现呢?