OpenGL 学习教程
Android OpenGL ES 学习(一) – 基本概念
Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
Android OpenGL ES 学习(三) – 绘制平面图形
Android OpenGL ES 学习(四) – 正交投影
Android OpenGL ES 学习(五) – 渐变色
Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
Android OpenGL ES 学习(七) – 纹理
Android OpenGL ES 学习(八) –矩阵变换
Android OpenGL ES 学习(九) – 坐标系统和。实现3D效果
Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL
代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git
前面我们已经学习了 GL 的基本实现效果,我们一般的操作顺序就是
- onSurfaceCreated: 进行着色器的加载编译
- onSurfaceChanged:设置 gl 的大小,或者设置正交投影
- onDrawFrame:绘制
那你会不会好奇,为啥我们按照这样的顺序,弄完就可以正常显示了呢?你是否对OpenGL 绘制的 “完整流程” 感兴趣呢?你是否也遇到下面这些疑问呢?
- GL线程和普通线程有什么区别?
- texture所占用的空间是跟GL线程绑定的吗?
- 为什么通常一个GL线程的texture等数据,在另一个GL线程没法用?
- 为什么通常GL线程销毁后,为什么texture也跟着销毁了?
- 不同线程如何共享OpenGL数据
这一章,我们带着这些疑问,来探究 OpenGL 渲染的完整流程,今天要完成的效果,不使用GLSurfaceView,编写 EGL + SurfaceVIew 实现 3D 效果:
一. 渲染完整流程
为什么叫完整流程?前面也说道,Android 系统把复杂的过程都封装好了,我们只需要调用 GlSurfaceView 就可以很轻松的拿到OpenGL 的渲染环境,然后编写着色器代码和逻辑即可。
但实际上,OpenGL 的核心流程应该是这样的:
为了得到真相,我们通过查看GLSurfaceView 的源码去看。
二. GLSurfaceView 源码
从入口出发,我们都是调用 glSurface.setRenderer() 去设置的,进到源码后可以发现,它主要做了以下几件事情:
// EGLConfig 对象.用于指定OpenGL颜色、深度、模版等设置if (mEGLConfigChooser == null) {mEGLConfigChooser = new SimpleEGLConfigChooser(true);}// EGLContext 对象.用于提供EGLContext创建和销毁的处理if (mEGLContextFactory == null) {mEGLContextFactory = new DefaultContextFactory();}// EGLSurface窗口对象. 用于提供EGLSurface创建和销毁的处理if (mEGLWindowSurfaceFactory == null) {mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();}//开启 GLThread线程 mRenderer = renderer;mGLThread = new GLThread(mThisWeakRef);mGLThread.start();
额,看完这个注释,你可能会一头雾水,EGL 是啥,EGLSurface 又是啥,看来我们需要先了解 EGL .
2.1 EGL
EGL 是 OpenGL 与本地窗口系统 Native Window System)之间的通信接口,它的主要作用是:
- 与设备的原生窗口系统通信
- 查询绘图表面的可用类型和配置
- OpenGL 和其他图形渲染 API 渲染
不同平台上EGL配置是不一样的,而OpenGL的调用方式是一致的,也就是说OpenGL的跨平台特性依赖于EGL接口。
EGL 通过创建 eglSurface,和上下文 eglContext,就可以通过相关 api 访问本地窗口系统,实现绘制。如下图,体现了 EGL ,display 和 context 三者之间的关系。
- Display(EglDisplay):实际显示设备的抽象,你可以理解成显示模块
- Surface(EglSurface):用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer (颜色缓冲区),Stencil Buffer(模板缓冲区),Depth Buffer(深度缓冲区)
- Context(EglContext):存储 OpenGL ES 绘图的一些状态信息,比如存储 texture
在开发时,GLSurfaceView 已经帮我们对 Display,Surface,Context 进行封装管理,我们可以很方便的调用 GLSurfaceView.Render ,实现渲染绘制。
使用 EGL 渲染的一般步骤:
- 获取 EGLDisplay 对象,建立与本地窗口系统的连接:调用 eglGetDisplay 方法得到 EGLDisplay。
- 初始化 EGL 方法:打开连接之后,调用 eglInitialize 方法初始化。
- 获取 EGLConfig 对象,确定渲染表面的配置信息:调用 eglChooseConfig 方法得到 EGLConfig。
- 创建渲染表面 EGLSurface:通过 EGLDisplay 和 EGLConfig ,调用 eglCreateWindowSurface 或 eglCreatePbufferSurface 方法创建渲染表面,得到 EGLSurface,其中 eglCreateWindowSurface 用于创建屏幕上渲染区域,eglCreatePbufferSurface 用于创建屏幕外渲染区域。
- 创建渲染上下文 EGLContext :通过 EGLDisplay 和 EGLConfig ,调用 eglCreateContext 方法创建渲染上下文,得到 EGLContext。
- 绑定上下文:通过 eglMakeCurrent 方法将 EGLSurface、EGLContext、EGLDisplay 三者绑定,绑定成功之后 OpenGLES 环境就创建好了,接下来便可以进行渲染。
- 交换缓冲:OpenGLES 绘制结束后,使用 eglSwapBuffers 方法交换前后缓冲,将绘制内容显示到屏幕上,而屏幕外的渲染不需要调用此方法。
- 释放 EGL 环境:绘制结束后,不再需要使用 EGL 时,需要取消 eglMakeCurrent 的绑定,销毁 EGLDisplay、EGLSurface、EGLContext 三个对象。
原来如此,原来 GLSurfaceView 里面,已经帮我们把 EGL 初始化好了,回到 glSurface.setRenderer() 的方法:
// EGLConfig 对象.用于指定OpenGL颜色、深度、模版等设置if (mEGLConfigChooser == null) {mEGLConfigChooser = new SimpleEGLConfigChooser(true);}// EGLContext 对象.用于提供EGLContext创建和销毁的处理if (mEGLContextFactory == null) {mEGLContextFactory = new DefaultContextFactory();}// EGLSurface窗口对象. 用于提供EGLSurface创建和销毁的处理if (mEGLWindowSurfaceFactory == null) {mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();}//开启 GLThread线程 mRenderer = renderer;mGLThread = new GLThread(mThisWeakRef);mGLThread.start();
我们就知道,原来注释是这个意思,管理关键还得看一下 GL 线程做了什么。
2.2 GL 线程
我们去看看它和其他线程有什么区别:
实际上,它跟普通线程没有什么区别,也是继承Thread,关键就是 guradedRun() 方法,它实际上把 EGL 给初始化好,然后回调相关方法,让你去绘制,在一个 while 循环中,它做了哪些操作呢?
while(true){//初始化 egl,配置等信息mEglHelper.start()//回去 Surface,并配置到当前线程mElgHeper.createSurface()回调 onSurfaceCreated()回调 onSurfaceChanged()回调 onDrawFrame()//egl交换缓冲区,呈现画面mEglHelper.swap()
}
从这里看,guradedRun() 跟我们之前说的流程基本是一样的,还有非常熟悉的 onSurfaceCreated(), onSurfaceChanged() 和 onDrawFrame() 也是在 EGL 配置之后,再回调的。
这里我们也可以得出一个结论,就是 GL线程跟普通线程没什么区别,就是一个普通线程,只是按照 OpenGL 完整的绘图方式走了一遍,因此我们也可以自己自定义,也可以实现OpenGL 的绘制,从 glGenTextures 等绘图api 都是 native 方法也可以证明这一点。
2.3 为什么通常一个GL线程的texture等数据,在另一个GL线程没法用
回答这个问题之前,先看看纹理对象是怎么生成的,查看 GLES30.glGenTextures 方法:
// C function void glGenTextures ( GLsizei n, GLuint *textures )public static native void glGenTextures(int n,int[] textures,int offset
);
看来跟 EGL 没关系,那它是怎么知道是 GL 线程去调,还是普通线程去调呢?它又是怎么把 glGenTextures 和 glDeleteTextures 对应到正确的线程上的呢?看源码:
实际上,它会先通过 getGlThreadSpecific() 这个方法去拿到一个 context,这个 context 实际上就是 EGL Context ,它是比较特殊的。什么意思呢,就是不同线程去拿,得到的 EGL Context 可能都不一样,这取决于给这个 EGL Context 是什么,我的理解是相当于线程类自己的局部变量,每个线程存储的 context 都不一样。
那EGL Context 是什么时候被设置进去的呢?
还记得 前面说到的 eglMakeCurrent() 这个方法吗?
egl.eglMakeCurrent(display, eglSurface, eglSurface, eglContext)
实际上,第四个参数eglContext 最终会设置给 setGlThreadSpecific() 中的变量存储起来,给其 API 使用。
然后我们再看一下 eglMakeCurrent 做了什么:
可以得到以下总结:
- 获取当前线程 EGL context ,给底层使用
- 判断当前的 context 是否为 IS_CURRENT 状态,否则返回-1
- 如果 gl 是 IS_CURRENT 状态,但是不是当前线程,也return
- 如果 gl 不是 IS_CURRENT 状态,将 current 设置成非 IS_CURRENT 状态
- 将gl置为IS_CURRENT状态并将gl设置为当前线程的Thread Local的EGL Context
结论:
- 如果一个 EGL context 被一个线程使用 glMakeCurrent ,则它不能被另外一个线程 glMakeCurrent 了
- glMakeCurrent 之后, EGL Context 会跟当前的 EGL context 脱离关系
关于 texture 与 egl context 的关系,可以参考这篇文章 https://cloud.tencent.com/developer/article/1035505
因此,我们可以回答一下上面的问题:
- GL线程和普通线程有什么区别?: 没有区别,只是 GL线程 按照 OpenGL 的流程跑了一遍。
- texture所占用的空间是跟GL线程绑定的吗?:不是,跟 EGL context 绑定,跟 线程没关系。
- 为什么通常一个GL线程的texture等数据,在另一个GL线程没法用?:因为调用 OpenGL 接口,会先去获取 EGL context,而不同线程获取的状态是不一样的,而 texture 又放在 EGL context 中,因此无法在另一个 GL 线程使用。
- 为什么通常GL线程销毁后,为什么texture也跟着销毁了?:因为 GLSurfaceView 在销毁时,调用了 eglDestroyContext() 方法,销毁了 EGL context,从而 texture 也销毁了,跟 GL线程没关系
- 不同线程如何共享OpenGL数据:共享 EGL context,线程调用 eglCreateContext 时,传入另一个线程的 EGL Context。
三. 自定义 EGL 现成,使用SurfaceView+EGL 实现渲染
前面说到,GLSurfaceView 帮我们把 egl 的配置都弄好了,优点是使用简单,缺点是当我们想共享同个 EGL context,实现同个场景,不同 surface 的渲染时,GLSurfaceView 就使用了。
所以,我们自定义自己的 GLSurfaceView。
首先,创建一个类,让它也继承 SurfaceView,并创建一个线程,用来加载 EGL :
inner class EglSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {init {holder.addCallback(this)}loverride fun surfaceCreated(holder: SurfaceHolder) {}override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {}inner class EglThread(private val surface: Surface) : Thread() {override fun run() {....}}
}
接着,就需要创建 EGL 了,这里我们通过模仿 GLSurfaceView 的 EGLHelper 方法,得到以下类:
inner class EglHelper {private var egl: EGL10? = nullprivate var eglDisplay: EGLDisplay? = nullprivate var eglSurface: EGLSurface? = nullprivate var eglContext: EGLContext? = nullfun initEgl(surface: Surface) {//1、得到Egl实例:val egl = EGLContext.getEGL() as EGL10//2、得到默认的显示设备(就是窗口)val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)if (display === EGL10.EGL_NO_DISPLAY) {throw RuntimeException("eglGetDisplay failed")}//3、初始化默认显示设备val displayVersions = IntArray(2)if (!egl.eglInitialize(display, displayVersions)) {throw RuntimeException("eglInitialize failed")}//4、设置显示设备的属性val attr = intArrayOf(EGL14.EGL_RED_SIZE, 8,EGL10.EGL_GREEN_SIZE, 8,EGL10.EGL_BLUE_SIZE, 8,EGL10.EGL_ALPHA_SIZE, 8,EGL10.EGL_DEPTH_SIZE, 8,EGL10.EGL_STENCIL_SIZE, 8,EGL10.EGL_RENDERABLE_TYPE, 4,EGL10.EGL_NONE)//5、从系统中获取对应属性的配置val num_config = IntArray(1)if(!egl.eglChooseConfig(display, attr, null, 1, num_config)) {throw RuntimeException("eglChooseConfig failed")}val numConfigs = num_config[0]if (numConfigs <= 0) {throw RuntimeException("No configs match configSpec")}val configs = arrayOfNulls<EGLConfig>(numConfigs)if (!egl.eglChooseConfig(display, attr, configs, numConfigs, num_config)) {throw RuntimeException("eglChooseConfig#2 failed")}//6、创建EglContextval attrib_list = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,EGL10.EGL_NONE)val eglContext = egl.eglCreateContext(display,configs[0],EGL10.EGL_NO_CONTEXT, attrib_list)//7、创建渲染的Surfaceval eglSurface = egl.eglCreateWindowSurface(display, configs[0], surface, null)//8、绑定EglContext和Surface到显示设备中if (!egl.eglMakeCurrent(display, eglSurface, eglSurface, eglContext)) {throw RuntimeException("eglMakeCurrent fail")}this.egl = eglthis.eglDisplay = displaythis.eglSurface = eglSurfacethis.eglContext = eglContext}fun swapBuffers() {egl?.eglSwapBuffers(eglDisplay, eglSurface)}fun destroy() {egl?.apply {eglMakeCurrent(eglDisplay,EGL10.EGL_NO_SURFACE,EGL10.EGL_NO_SURFACE,EGL10.EGL_NO_CONTEXT)eglDestroySurface(eglDisplay, eglSurface)eglSurface = nulleglDestroyContext(eglDisplay, eglContext)eglContext = nulleglTerminate(eglDisplay)eglDisplay = nullegl = null}}}
步骤比较简单,就是:
- 拿到Egl实例 :EGLContext.getEGL()
- 得到默认的显示设备(就是窗口):egl.eglGetDisplay
- 初始化默认显示设备:egl.eglInitialize
- 设置显示设备的属性和配置到 egl 中:egl.eglChooseConfig
- 创建EglContext:egl.eglCreateContext
- 创建渲染的Surface:egl.eglCreateWindowSurface
- 绑定EglContext和Surface到显示设备中:egl.eglMakeCurrent
- 交换缓冲区,渲染:egl.eglSwapBuffers
然后我们把 EhlHeper 跟线程结合起来,完整的代码如下:
/**
* 自定义一个 SurfaceView,里面的线程模拟 EGL 环境
*/
inner class EglSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {init {holder.addCallback(this)}private var eglThread: EglThread? = nulloverride fun surfaceCreated(holder: SurfaceHolder) {eglThread = EglThread(holder.surface)eglThread?.start()}override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {eglThread?.changeSize(width, height)}override fun surfaceDestroyed(holder: SurfaceHolder) {eglThread?.release()}/*** gl线程,里面包含 EGL 创建流程*/inner class EglThread(private val surface: Surface) : Thread() {private var isExit = falseprivate var isFirst = trueprivate var isSizeChange = falseprivate var eglHelper: EglHelper? = nullprivate var width = 0private var height = 0override fun run() {super.run()eglHelper = EglHelper().apply {initEgl(surface)}while (true) {if (isExit) {release()break}if (isFirst) {isFirst = false//回调给主类this@L8_ShapeRender.onSurfaceCreated(null, null)}if (isSizeChange) {isSizeChange = falsethis@L8_ShapeRender.onSurfaceChanged(null, width, height)}eglHelper?.let {this@L8_ShapeRender.onDrawFrame(null)it.swapBuffers()}try {sleep(16)} catch (e: Exception) {}}}fun release() {isExit = trueeglHelper?.destroy()surface.release()}fun changeSize(width: Int, height: Int) {this.width = widththis.height = heightisSizeChange = true}}}
都比较好理解,可以看到,我们也通过 onSurfaceCreated,onSurfaceChanged 和 onDrawFrame 回调给外部,这样,着色器相关的,就不用改变啦。
看一下效果,可以正常运行:
参考:
https://cloud.tencent.com/developer/article/1035505
https://cloud.tencent.com/developer/article/1899820