经过一段时间 OpenGL 的学习,我们已经掌握了如何使用 glwf 在桌面端绘制简单图形。现在让我们把目光投向移动端,看看如何在 Android 上使用 OpenGL 绘制图形。本文假设你对 Android 基础有所了解,并使用 Kotlin 编写示例 demo,项目的代码你可以在 Android_OpenGLES3_Demo 中找到。
本文参考了以下资料
OpenGL ES(OpenGl for Embedded System)是 Khronos 公司为移动端提供的 OpenGL 子集,用于在嵌入式设备和移动端设备中渲染 2D 和 3D 图形,它是为了适应低功耗设备需求而设计的。OpenGL ES 去掉了一些冗余的 API,保留了最核心有用的部分。
Android 支持多版 OpenGL ES API,从 Android 1.0 开始支持 OpenGL ES 1.0 和 1.1,从 Android 2.2 开始支持 OpenGL ES 2.0,从 Android 4.3 开始支持 OpenGL ES 3.0,从 Android 5.0 开始支持 OpenGL ES 3.11。Android 对应 OpenGL ES 的版本支持如下表所示
Android 版本 | OpenGL ES 版本 |
---|---|
Android 1.0 | OpenGL ES 1.0/1.1 |
Android 2.2 | OpenGL ES 2.0 |
Android 4.3 | OpenGL ES 3.0 |
Android 5.0 | OpenGL ES 3.1 |
由于历史原因,和一些兼容性的问题,很多项目中都使用了 OpenGL ES 2.0 版本而不是 3.0。对比两个版本,它们有着一下区别
性能方面,OpenGL ES 3.0 相比于 OpenGL ES 2.0 增加了一些新功能和优化,例如更大的缓冲区、更多的格式、更多的统一变量、实例渲染、像素缓冲对象和遮挡查询等。这些功能可以提高图形渲染的效率和质量,但也可能增加开发的复杂度和资源消耗。
兼容性方面,OpenGL ES 3.0 是向后兼容 OpenGL ES 2.0 的,也就是说使用 2.0 编写的应用程序是可以在 3.0 中继续使用的。但是,并不是所有的设备都支持 OpenGL ES 3.0 ,所以如果要考虑跨平台的兼容性,可能还需要使用 OpenGL ES 2.0 或者做一些适配工作。
易用性方面,OpenGL ES 3.0 和 OpenGL ES 2.0 都是可编程图形管线,开发者可以自己编写图形管线中的顶点着色器和片段着色器两个阶段的代码。这样可以提供更多的灵活性和创造力,但也需要更多的编程技巧和知识。相比于 OpenGL ES 1.x 系列的固定功能管线,OpenGL ES 2.0/3.0 不支持 OpenGL ES 1.x 支持的固定功能定点单元。
那么为什么选择 OpenGL ES 3.0 呢?很直白的一个理由:ES 3.0 中使用的 OpenGL Shader Language(GLSL)版本是 300,而 LearnOpenGL 教程中使用的是 GLSL 330,两者几乎没有差异,只需最小的学习成本就能够掌握。但如果使用 ES 2.0,那么你还得花一些时间来学习和适应不同版本 GLSL。
在 Android 中,GLSurfaceView 是一个基础自 SurfaceView 的类,它是专门用于显示 OpenGL 渲染的内容。 而 GLSurfaceView.Render 则负责具体地渲染画面。简单来说,GLSurfaceView 类似于 glwf 中窗口的概念,它负责画面的显示、OpenGL Context 的管理、渲染线程的控制等等,而 GLSurfaceView.Render 则是 OpenGL API 调用的逻辑的抽象,你需要创建自己的 Render 来绘制图形。
还是上代码进行说明
class MyOpenGLSurfaceView(context: Context?, attrs :AttributeSet?) : GLSurfaceView(context, attrs) {
constructor(context: Context?) : this(context, null)
var render = MyOpenGLSurfaceRender()
init {
setEGLContextClientVersion(3)
setRenderer(render)
}
}
MyOpenGLSurfaceView
继承自 GLSurfaceView
,注意要实现 GLSurfaceView(Context context, AttributeSet attrs)
构造函数,我们才能在 layout.xml 布局文件中直接使用 MyOpenGLSurfaceView
,否则会出现 Binary XML file line # : Error inflating class
错误init
中,setEGLContextClientVersion(3)
表示我们使用 OpenGL ES 3.0 版本init
中,setRenderer
设置 Renderclass MyOpenGLSurfaceRender : GLSurfaceView.Renderer {
// ...
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
//设置背景颜色
GLES30.glClearColor(0.0f, 0.5f, 0.5f, 0.5f)
//...
}
override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
GLES30.glViewport(0, 0, width, height)
}
override fun onDrawFrame(p0: GL10?) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
// ...
}
}
MyOpenGLSurfaceRender
继承自 GLSurfaceView.Renderer
,需要实现 onSurfaceCreated
、onSurfaceChanged
和 onDrawFrame
onSurfaceCreated
、onSurfaceChanged
和 onDrawFrame
一定会在同一个渲染线程中执行,你可以通过 Log 来确定这一点,例如 Log.d("xxx", "onSurfaceCreated thread " + Thread.currentThread())
onSurfaceCreated
当 surface 被创建或者重新创建时调用,在这个函数中放置一些创建资源的逻辑,例如设置 vbo 和 vao,编译 shader 等等。onSurfaceChanged
当 surface 大小发生变化时调用,我们可以在这里设置 viewportonDrawFrame
负责渲染当前帧。类比的话,可以将之前 LearnOpenGL 代码中 while
循环中的代码放置在这里在 Android 中调用 OpenGL ES API 的方式与 OpenGL API 几乎无异,仿照 LearnOpenGL 中的代码,给出在 Kotlin 代码,只是有一些细节的地方需要注意,接下来讨论代码。
首先是工具类 Shader
class Shader(private val vertexShaderSource: String, private val fragmentShaderSource: String) {
private val TAG = "Shader"
var id : Int = 0
fun prepareShaders(): Int{
// compile vertex shader
val vertexShader = createAndCompileShader(GLES30.GL_VERTEX_SHADER, vertexShaderSource)
if(vertexShader == -1){
return -1
}
// compile fragment shader
val fragmentShader = createAndCompileShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderSource)
if(fragmentShader == -1){
return -1
}
id = createShaderProgram(vertexShader, fragmentShader)
if(id == -1){
return -1
}
return 0
}
private fun createAndCompileShader(type: Int, source: String): Int{
val success: IntArray = intArrayOf(0)
val shader = GLES30.glCreateShader(type)
GLES30.glShaderSource(shader, source)
GLES30.glCompileShader(shader)
GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, success, 0)
if (success[0] == 0) {
val log = GLES30.glGetShaderInfoLog(shader)
Log.e(TAG, "compile vertex source failed.$log")
GLES30.glDeleteShader(shader)
return -1
}
return shader
}
private fun createShaderProgram(vertexShader: Int, fragmentShader: Int): Int{
val success: IntArray = intArrayOf(0)
val program = GLES30.glCreateProgram()
GLES30.glAttachShader(program, vertexShader)
GLES30.glAttachShader(program, fragmentShader)
GLES30.glLinkProgram(program)
GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, success, 0)
if (success[0] == 0) {
val log = GLES30.glGetShaderInfoLog(program)
Log.e(TAG, "link shader program failed. $log")
GLES30.glDeleteProgram(program)
return -1
}
GLES30.glDeleteShader(vertexShader)
GLES30.glDeleteShader(fragmentShader)
return program
}
}
prepareShaders
来编译和链接 shader,如果失败将返回 -1;如果调用成功,使用 id
来引用 当前的 shader programTriangle 类:
class Triangle {
private val vertices = floatArrayOf(
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
)
val vaos: IntArray = intArrayOf(0)
val vbos: IntArray = intArrayOf(0)
fun prepareData(){
// prepare vbo data
val vertexBuffer = ByteBuffer.allocateDirect(vertices.size * Float.SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer()
vertexBuffer.put(vertices)
vertexBuffer.position(0)
// generate vao and vbo
GLES30.glGenVertexArrays(1, vaos, 0)
GLES30.glGenBuffers(1, vbos, 0)
GLES30.glBindVertexArray(vaos[0])
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbos[0])
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
Float.SIZE_BYTES * vertices.size,
vertexBuffer,
GLES30.GL_STATIC_DRAW
)
GLES30.glVertexAttribPointer(
0, 3, GLES30.GL_FLOAT, false,
3 * Float.SIZE_BYTES, 0
)
GLES30.glEnableVertexAttribArray(0)
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)
GLES30.glBindVertexArray(0)
}
}
vertexBuffer
,由于 Kotlin 数据使用大端(BigEnd)存放,而 OpenGL 的数据为小端,因此在 Android 下使用 OpenGL 必须做大小端的转换。class MyOpenGLSurfaceRender : GLSurfaceView.Renderer {
private val tri = Triangle()
private val shader: Shader = Shader(
VertexShaderSource.vertexShaderSource,
FragmentShaderSource.fragmentShaderSource
)
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
GLES30.glClearColor(0.0f, 0.5f, 0.5f, 0.5f)
tri.prepareData()
shader.prepareShaders()
}
override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
GLES30.glViewport(0, 0, width, height)
}
override fun onDrawFrame(p0: GL10?) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
GLES30.glUseProgram(shader.id)
GLES30.glBindVertexArray(tri.vaos[0])
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 3)
}
}
onSurfaceCreated
函数中,创建三角形需要的数据和 shaderonDrawFrame
函数中,glUseProgram
使用编译好的 shader id;glBindVertexArray
使用设置好的 vao 进行渲染GLES30.
的函数调用都要在渲染线程中操作,也就是说这些函数必须在 onSurfaceCreated
、onSurfaceChanged
和 onDrawFrame
中调用,否则渲染失败。最后,你可以在布局文件中,直接引用 MyOpenGLSurfaceView
作为控件,就像 Button 一样方便,例如
<com.xinging.opengltest.MyOpenGLSurfaceView
android:layout_width="match_parent"
android:layout_height="500dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
本文介绍了如何在 Android 下使用 OpenGL ES 3.0 绘制三角形。通过自定义 GLSurfaceView 和 GLSurfaceView.Render ,在 Android 下调用 OpenGL API 来实现绘图;本文所有代码在 Android_OpenGLES3_Demo