从零开始的跨平台渲染引擎——OpenGL基础环境搭建
Posted 董小虫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始的跨平台渲染引擎——OpenGL基础环境搭建相关的知识,希望对你有一定的参考价值。
前言
在前文中,我主要分析了渲染引擎的整体架构。本文的主要内容是对OpenGL基础环境的搭建,主要涉及架构图中的Container、Graphic Wrapper和Platform层。
这里也放上代码仓的链接:https://github.com/dongzhong/ReZero。代码也是从零开始搭建,会逐步的完善功能。
一、OpenGL环境介绍
OpenGL是一些操作GPU的API集合。但是OpenGL并没有提供操作平台本地窗口的API。所以需要另外一套可以起到本地窗口和OpenGL之间桥梁作用的工具。OpenGL的维护组织Kronos也提供了一套本地窗口接口EGL,而苹果公司也为ios系统提供了一套EAGL框架。
下面就对这两个工具、框架进行相关API介绍,以及如何在工程中应用,可能面临的问题。
二、OpenGL环境搭建基本流程
android EGL 环境
如果开发过Android图形相关应用的开发,应该会了解过GLSurfaceView
。GLSurfaceView
已经为我们初始化好了OpenGL环境,我们只需要使用GL10
里的相关API实现OpenGL的调用。
但是,这些API都是Android的Java层接口。如果考虑到跨平台特性,就需要使用OpenGL的C++接口了。而NDK提供了EGL工具。在上篇文章中也提到了,Android平台的容器需要使用TextureView
或SurfaceView
,这两个控件中又可以产生或持有android.View.Surface
。而这个Surface
则可以通过JNI传递到C++层,用于创建本地窗口。
下面对EGL环境搭建和销毁进行详细介绍。
初始化
- 获取并初始化环境
EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);
EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
这两个API用于获取系统屏幕资源,并与其进行连接。设备有可能会有多个屏幕,eglGetDisplay
方法的参数就是用于选择屏幕。通常情况下,在手机上我们使用EGL_DEFAULT_DISPLAY
来获取默认主屏幕。
- 配置选择
EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
这个方法返回一些列满足给定属性的配置。举个栗子:
// Example
const EGLint config_attributes[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_STENCIL_SIZE, 0,
EGL_NONE,
};
EGLConfig config = nullptr;
EGLint count = 0;
eglChooseConfig(display_, config_attributes, &config, 1, &count);
- 创建
EGLContext
EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
eglCreateContext
方法用于创建一个新的EGL环境,其中的参数display
和config
就是前两步所得到的结果;share_context
则是用于共享GL资源的EGLContext
(这里涉及到OpenGL的多线程使用,后续再详细讲解)。使用栗子:
// Example
const EGLint attributes[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE,
};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, attributes);
- 通过Java的
android.View.Surface
创建ANativeWindow
ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface);
这个方法是对接了Android系统的本地窗口。上文我们讲到,在Android平台上,Java层中我们会使用JNI从TextureView
或SurfaceView
中传递一个android.view.Surface
到C++层中来。这个方法中的surface
参数,就是这个android.View.Surface
。
关于JNI的使用方法,这里就不展开讲了。
- 创建
EGLSurface
// Onscreen surface
EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
// Offscreen surface
EGLSurface eglCreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);
EGLSurface
可以分为上屏Surface和离屏Surface。其中上屏Surface需要从上一步得到的ANativeWindow
中创建。一般我们需要显示OpenGL绘制的内容,需要创建上屏Surface。
- MakeCurrent
EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
这一步很重要,这个方法用于切换当前线程的EGLContext
和EGLSurface
。只有执行了这一步,之前创建的EGLContext
和EGLSurface
才算是真正的在当前线程可用。
要注意的是:
- 不能在两个线程里绑定同一个
EGLContext
; - 不能在两个线程里绑定同一个
EGLSurface
到不同的EGLContext
上
- 展示画面
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
这一步发生在所有的OpenGL渲染指令结束后,将绘制结果展示到本地窗口上去。可以理解为,我们所进行的OpenGL绘制,都是发生在隐藏在屏幕后面的一块内存中,当我们需要将绘制结果展示到屏幕上去,我们就要将屏幕上对应那块内存与需要展示的内存互换位置(这里实际上是互换的内存指针,这样效率更高),就像是更换相框里的相片一样。
销毁
- 移除Current的
EGLContext
和EGLSurface
eglMakeCurrent(display_, EGL_NO_SURFACE, EGK_NO_SURFACE, EGL_NO_CONTEXT);
在销毁EGLContext
和EGLSurface
之前,需要将他们从当前线程中解除绑定,防止资源泄漏。
- 销毁
EGLSurface
和EGLContext
EGLBoolean eglDestroySurface(EGLDisplay dpy, EGLSurface surface);
EGLBoolean eglDestroyContext(EGLDisplay dpy, EGLContext ctx);
- 终止EGL环境
EGLBoolean eglTerminate(EGLDisplay dpy);
最后,如果需要退出当前整个EGL环境,则还需要解除EGLDisplay
的连接。
iOS EAGL 环境
苹果公司为iOS操作系统提供了自己实现的一套类似EGL的EAGL框架。下面简单说一下EAGL环境的搭建与销毁。
初始化
- 创建
EAGLContext
- (nullable instancetype)initWithAPI:(EAGLRenderingAPI) api sharegroup:(EAGLSharegroup*) sharegroup;
EAGLContext
可以类比的看作EGL中的EGLContext
。这里的参数api
指的是创建的OpenGL ES版本;而参数sharegoup
则是制定共享状态的EAGLContext
。举个使用栗子:
// Example
EAGLContext* context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3
sharegroup:nil];
- 将
EAGLContext
设置为current
+ (BOOL)setCurrentContext:(nullable EAGLContext*) context;
绑定EAGLContext
到当前线程中。
- 创建绑定Framebuffer和Renderbuffer
void glGenFramebuffers(GLsizei n, GLuint* framebuffers);
void glBindFramebuffer(GLenum target, GLuint framebuffer);
void glGenRenderbuffers(GLsizei n, GLuint* renderbuffers);
void glBindRenderbuffer(GLenum target, GLuint renderbuffer);
void glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
此处使用了OpenGL ES本身的API,可以形象的理解为在画架上贴上画布。
- 将
EAGLDrawable
作为存储空间,附着到OpenGL ES的Renderbuffer上,或者分配离屏空间
- (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(nullable id<EAGLDrawable>)drawable;
void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height)
这两个方法是为Renderbuffer分配内存。
renderbufferStorage:fromDrawable:
方法中的drawable
参数是UIView
的layer
属性。这里需要说明的是,在平台容器层中,我们需要从UIView
继承一个自定义视图,并将其layerClass
静态方法重写为如下方法:
+ (Class)layerClass {
return [CAEAGLLayer class];
}
- 将Renderbuffer展示到屏幕上
- (BOOL)presentRenderbuffer:(NSUInteger)target;
其中的target
必须为GL_RENDERBUFFER
。
销毁
- 销毁framebuffer和renderbuffer
void glDeleteFramebuffers(GLsizei n, const GLuint* framebuffers);
void glDeleteRenderbuffers (GLsizei n, const GLuint* renderbuffers);
- 将current context置空,并销毁context
[EAGLContext setCurrent:nil];
[context release];
三、工程化实现
OpenGL的基础环境搭建的流程其实很简单。但是如果想要在一个工程项目中完整实现,那么需要考虑的就不仅仅是这些了。下面简单的说几个在工程化使用EGL/EAGL需要注意的点。
多线程
OpenGL对多线程的支持比较弱。但是在做渲染引擎时,又对多线程渲染有着强烈的需求,比如我们肯定不希望在主线程上做消耗时间的渲染操作;又或者在上传一个不是紧急使用的纹理数据时,我们不希望占用大量的渲染时间。在这些情况下,我们就需要在多个线程中对GLES环境进行操作。
为了解决这个问题,我们首先要有一个比较好的线程工具。在ReZero中,我借鉴了Flutter的线程工具MessageLoop和TaskRunner。并且在很多时间需要阻塞线程,来等待特定操作结束。
还需要注意EGL/EAGL在多线程使用时的注意事项。Context不能同时绑定到两个不同的线程上;不同的Context需要通过share创建出来,才能共享同样的GL状态。
在使用EAGL时,renderbufferStorage:fromDrawable:
分配内存方法的调用,一定要在主线程中执行。不然会出现内存不安全的问题。
Surface的创建时机
在Android平台上,android.View.Surface
的可用时间与SurfaceView
或TextureView
创建时间会有一定的时间差,要注意在这个时间差内,不能进行OpenGL ES的绘制操作。
为了解决这个问题,则需要对引擎进行类似音乐播放的暂停、继续操作。
注意避免内存泄漏
因为OpenGL是一个大型的状态机,每一步的操作都需要若干步的操作。所以在环境销毁掉时候,若是不慎漏掉一步操作,就很容易引起内存泄漏问题。
为了解决这个问题,就需要对这些必要步骤进行包装,比如简单的举个栗子:可以在类的构造函数中包装一些创建和初始化的操作,而在析构函数中则包装一些解除绑定和销毁目标等操作。当然,这里只是一个简单的栗子,还有其他很多方法来进行包装,根据实际情况来使用,可以有效避免内存泄漏的问题。
总结
本文主要介绍了OpenGL的基础环境搭建,以及部分工程化时需要考虑到的问题。
在文章的最后,放出一个简单的测试效果,用于测试环境搭建是否成功:测试效果
以上是关于从零开始的跨平台渲染引擎——OpenGL基础环境搭建的主要内容,如果未能解决你的问题,请参考以下文章