OpenGL

Open Graphics Library
图形领域的工业标准,是一套跨编程语言、跨平台的、专业的图形编程(软件)接口。它用于二维、三维图像,是一个功能强大,调用方便的底层图形库。
与硬件无关。可以在不同的平台如Windows、Linux、Mac、Android、IOS之间进行移植。因此,支持OpenGL的软件具有很好的移植性,可以获得非常广泛的应用。

OpenGL版本和Android版本对应关系:

  • OpenGL ES 1.0 和 1.1: Android 1.0和更高的版本支持这个API规范
  • OpenGL ES 2.0:Android 2.2(API 8)和更高的版本支持这个API规范
  • OpenGL ES 3.0:Android 4.3(API 18)和更高的版本支持这个API规范
  • OpenGL ES 3.1:Android 5.0(API 21)和更高的版本支持这个API规范

OpenGL ES

针对手机、PDA和游戏主机等嵌入式设备而设计的OpenGL API 子集。

Android开发中可以通过 Android 的 Java api 使用,也可以采用 c++ 和 JNI 的方式使用。

Android 中使用 GLSurfaceView 。
GLSurfaceView:

  • 继承至 SurfaceView,它内嵌的 surface 专门负责 OpenGL 渲染
  • 管理 Surface 与 EGL
  • 允许自定义渲染器(render)。
  • 让渲染器在独立的线程里运作,和UI线程分离。
  • 支持按需渲染(on-demand)和连续渲染(continuous)。

OpenGL是一个跨平台的操作GPU的API,但OpenGL需要本地视窗系统进行交互,这就需要一个中间控制层,EGL就是连接OpenGL ES和本地窗口系统的接口,引入EGL就是为了屏蔽不同平台上的区别。

这篇文章通过参考网上一个 OpenGL 实践的例子完成。主要以代码+注释为主,通过代码注释详尽描述 Android 中 OpenGL 的使用。

HockeyActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class HockeyActivity : AppCompatActivity() {  

companion object {
private const val TAG = "HockeyActivity"

}

private lateinit var glSurfaceView: GLSurfaceView
private var renderSet = false


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 创建一个 GLSurfaceView,用来替换默认的 ContentView glSurfaceView = GLSurfaceView(this)
if (enableEs2()) {
// 设置 EGLContext 客户端版本
glSurfaceView.setEGLContextClientVersion(2)
// 设置与此视图关联的渲染器。
glSurfaceView.setRenderer(HockeyRenderer(this))
renderSet = true
} else {
toast("该设备不支持OpenGL.ES 2.0")
return
}

// 替换
setContentView(glSurfaceView)
}

override fun onResume() {
super.onResume()

// 开启渲染线程
if (renderSet) {
glSurfaceView.onResume()
}
}

override fun onPause() {
super.onPause()

// 暂停渲染线程
// 这个和上边 Resume 中的都比较重要,如果不添加,就有可能导致 OpenGL 上下文出问题,导致程序奔溃
if (renderSet) {
glSurfaceView.onPause()
}
}

/**
* 检查系统是否支持 OpenGL 2.0 */ private fun enableEs2(): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val deviceConfigurationInfo = activityManager.deviceConfigurationInfo
val glVersion = deviceConfigurationInfo.reqGlEsVersion
return (glVersion >= 0x20000
|| (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")))
}
}

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**  
* OpenGL 工具类
*/
object ShaderHelper {
private const val TAG = "ShaderHelper"

/**
* 编译 顶点着色器
*/
fun compileVertexShader(shaderCode: String): Int {
return compileShader(GL_VERTEX_SHADER, shaderCode)
}

/**
* 编译 片元着色器
*/
fun compileFragmentShader(shaderCode: String): Int {
return compileShader(GL_FRAGMENT_SHADER, shaderCode)
}

/**
* 编译着色器
* @param type 着色器类型
* @param shaderCode 着色器源码
*/
private fun compileShader(type: Int, shaderCode: String): Int {
// 调用 glCreateShader() 创建了一个新的着色器对象,并把这个对象的ID存入变量 shaderObjectId // shaderObjectId 是个整型值,代表这个对象,有点像C++中的指针地址,后边使用这个对象都用这个整型值代替
// 如果 shaderObjectId = 0,表示没有这个对象,有点像 java 中的 null val shaderObjectId = glCreateShader(type)
if (shaderObjectId == 0) {
// GLSL 中的错误信息通过方法 glGetError() 获取
LogUtil.w(TAG, "Warning! Could not create new shader, glGetError:${glGetError()}")
return 0
}
// 调用glShaderSource(shaderObjectId, shaderCode) 上传源代码
// 调用告诉OpenGL读入字符串shaderCode定义的源代码,并把它与shaderObjectId所引用的着色器对象关联在一起
glShaderSource(shaderObjectId, shaderCode)
// 调用 glCompileShader(shaderObjectId) 编译这个着色器
glCompileShader(shaderObjectId)
// 调用 glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0) 检查编译是失败还是成功
val compileStatus = IntArray(1)
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0)
// 可以通过调用 glGetShaderInfoLog(shaderObjectId) 获得一个可读的状态信息
LogUtil.i(
TAG, """
Result of compiling source: $shaderCode
${glGetShaderInfoLog(shaderObjectId)}
""".trimIndent()
)
// 验证编译状态
if (compileStatus[0] == 0) {
glDeleteShader(shaderObjectId)
LogUtil.w(TAG, "Warning! Compilation of shader failed, glGetError:${glGetError()}")
return 0
}
// 返回着色器对象ID
return shaderObjectId
}

/**
* 链接着色器
* @param vertexShaderId 顶点着色器ID
* @param fragmentShaderId 片元着色器ID
*/ fun linkProgram(vertexShaderId: Int, fragmentShaderId: Int): Int {
// 调用 glCreateProgram() 新建程序对象,用 programObjectId 记录这个程序对象的 ID val programObjectId = glCreateProgram()
if (programObjectId == 0) {
LogUtil.w(TAG, " Warning! Could not create new program, glGetError:${glGetError()}")
return 0
}
// 使用 glAttachShader 附上 顶点着色器 和 片元着色器
glAttachShader(programObjectId, vertexShaderId)
glAttachShader(programObjectId, fragmentShaderId)
// 调用glLinkProgram把两个着色器链接起来
glLinkProgram(programObjectId)
// 调用 glGetProgramiv 检查程序的信息日志
val linkStatus = IntArray(1)
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0)
LogUtil.i(
TAG, "Result of linking program:${glGetProgramInfoLog(programObjectId)}"
)
// 验证链接状态
if (linkStatus[0] == 0) {
glDeleteProgram(programObjectId)
LogUtil.w(TAG, " Warning! Linking of program failed, glGetError:${glGetError()}")
return 0
}
// 返回程序对象ID
return programObjectId
}

/**
* 创建OpenGL程序,包含编译、链接着色器
* @param vertexShaderSource 顶点着色器源码
* @param fragmentShaderSource 片元着色器源码
*/
fun buildProgram(vertexShaderSource: String?, fragmentShaderSource: String?): Int {
val programObjectId: Int
val vertexShader = compileVertexShader(vertexShaderSource!!)
val fragmentShader = compileFragmentShader(fragmentShaderSource!!)
programObjectId = linkProgram(vertexShader, fragmentShader)
validateProgram(programObjectId)
return programObjectId
}

/**
* 验证 OpenGL 程序是否正确
*/
fun validateProgram(programObjectId: Int): Boolean {
// 调用 glValidateProgram 来验证这个程序
glValidateProgram(programObjectId)
val validateStatus = IntArray(1)
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0)
LogUtil.i(
TAG, """
Result of validating program:${validateStatus[0]}
Log:${glGetProgramInfoLog(programObjectId)}
""".trimIndent()
)
return validateStatus[0] != GL_FALSE
}
}

Renderer渲染器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class HockeyRenderer(context: Context) : GLSurfaceView.Renderer {  
companion object {
private const val TAG = "HockeyRenderer"

// 一个顶点有2个分量(x, y)
private const val POSITION_COMPONENT_COUNT = 2

private const val BYTES_PER_FLOAT = 4

// 着色器参数名称
private const val U_COLOR = "u_Color"
private const val A_POSITION = "a_Position"
}

private var vertexShaderSource: String
private var fragmentShaderSource: String

// 着色器参数值
private var uColorLocation = 0
private var aPositionLocal = 0

private var vertexData: FloatBuffer

// 桌子、中线、摇杆坐标定义
private val tableVerticesWithTriangles = floatArrayOf(
// 第一个三角形
-0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f,
// 第二个三角形
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,

// 中间分界线
0f, 0.5f,
0f, -0.5f,
// 两个摇杆的质点位置
-0.25f, 0f,
0.25f, 0f
)

/**
* 当Surface被创建的时候,GLSurfaceView会调用这个方法;
* 这发生在应用程序第一次运行的时候,并且当设备长时间休眠,系统回收资源后重新被唤醒,这个方法也可能会被调用。
* 这意味着,本方法可能会被调用多次。
*/
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 设置清空屏幕用的颜色;前三个参数分别对应红,绿和蓝,最后的参数对应透明度。
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)

val programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource)
GLES20.glUseProgram(programId)

//
uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR)
aPositionLocal = GLES20.glGetAttribLocation(programId, A_POSITION)

vertexData.position(0)
// 调用GLES20.glVertexAttribPointer告诉OpenGL,它可以在缓冲区vertexData中找到a_Position对应的数据
GLES20.glVertexAttribPointer(
aPositionLocal,
POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT,
false, 0,
vertexData
)
GLES20.glEnableVertexAttribArray(aPositionLocal)
}

/**
* 在Surface被创建以后,每次Surface尺寸变化时,在横竖屏切换的时候,这个方法都会被调用到。
*/
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
// 设置视口的尺寸,这个视口尺寸怎么理解呢,就是锁定你操作的渲染区域是哪部分,
// 整个屏幕那就是 (0.0)点开始,宽度为width,长度为height咯。
// 如果你只想渲染左半个屏幕,那就(0.0),宽度width/2,长度为height/2。
// 这样设置viewport大小后,你之后的GL画图操作,都只作用这部分区域,右半屏幕是不会有任何反应的。
GLES20.glViewport(0, 0, width, height)
}

/**
* 当绘制一帧时,这个方法会被调用。
* 在这个方法中,我们一定要绘制一些东西,即使只是清空屏幕;
* 因为在这方法返回之后,渲染缓冲区会被交换,并显示在屏幕上,
* 如果什么都没画,可能会看到糟糕的闪烁效果。
*
* @param gl OpenGL.ES 1.0的API遗留下来的,直接忽略即可
*/
override fun onDrawFrame(gl: GL10?) {
// 清空屏幕,这会擦除屏幕上的所有颜色,并用之前glClearColor调用定义的颜色填充整个屏幕
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

//========= 绘制桌子 =====================
// 调用glUniform4f更新着色器代码中的u_Color的值,u_Color为白色,
// 与attribute属性不同,uniform的分量没有默认值,因此,如果一个uniform在着色器中被定义vec4类型,我们需要提供所有四个分量的值
GLES20.glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f)
// 用 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6)
// 第一个参数告诉OpenGL我们要画三角形
// 第二个参数0,告诉OpenGL从顶点数组(tableVerticesWithTriangles )的开头处开始读顶点
// 第三个参数6,告诉OpenGL读入六个顶点。因为每个三角形有三个顶点,所以调用最终会画出2个三角形,也就是一个矩形桌子
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6)

//========= 绘制中线 =====================
// u_Color为红色
GLES20.glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f)
// 这次画的是线 GLES20.GL_LINES,从顶点数据的第6组开始,每组也是2个元素
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2)

//========= 绘制摇杆 =====================
// u_Color为蓝色
GLES20.glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f)
// 这次画的是点 GLES20.GL_POINTS,从顶点数据的第8组开始,每组1个元素
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1)
// u_Color为绿色
GLES20.glUniform4f(uColorLocation, 0.0f, 1.0f, 0.0f, 1.0f)
// 这次画的是点 GLES20.GL_POINTS,从顶点数据的第9组开始,每组1个元素
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1)
}


init {
vertexShaderSource = Utils.loadStringFromAssets(context, "hockey/basic.vs")
fragmentShaderSource = Utils.loadStringFromAssets(context, "hockey/basic.fs")

vertexData = ByteBuffer
.allocateDirect(tableVerticesWithTriangles.size * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
vertexData.put(tableVerticesWithTriangles)
}
}

顶点着色器 basic.vs

1
2
3
4
5
6
attribute vec4 a_Position;  

void main(){
gl_Position = a_Position;
gl_PointSize = 10.0;
}

片元着色器 basic.fs

1
2
3
4
5
6
7
precision mediump float;  

uniform vec4 u_Color;

void main(){
gl_FragColor = u_Color;
}

最终效果