从零开始的跨平台渲染引擎——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图形相关应用的开发,应该会了解过GLSurfaceViewGLSurfaceView已经为我们初始化好了OpenGL环境,我们只需要使用GL10里的相关API实现OpenGL的调用。

但是,这些API都是Android的Java层接口。如果考虑到跨平台特性,就需要使用OpenGL的C++接口了。而NDK提供了EGL工具。在上篇文章中也提到了,Android平台的容器需要使用TextureViewSurfaceView,这两个控件中又可以产生或持有android.View.Surface。而这个Surface则可以通过JNI传递到C++层,用于创建本地窗口。

下面对EGL环境搭建和销毁进行详细介绍。

初始化

  1. 获取并初始化环境
EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);

EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);

这两个API用于获取系统屏幕资源,并与其进行连接。设备有可能会有多个屏幕,eglGetDisplay方法的参数就是用于选择屏幕。通常情况下,在手机上我们使用EGL_DEFAULT_DISPLAY来获取默认主屏幕。

  1. 配置选择
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);
  1. 创建EGLContext
EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);

eglCreateContext方法用于创建一个新的EGL环境,其中的参数displayconfig就是前两步所得到的结果;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);
  1. 通过Java的android.View.Surface创建ANativeWindow
ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface);

这个方法是对接了Android系统的本地窗口。上文我们讲到,在Android平台上,Java层中我们会使用JNI从TextureViewSurfaceView中传递一个android.view.Surface到C++层中来。这个方法中的surface参数,就是这个android.View.Surface

关于JNI的使用方法,这里就不展开讲了。

  1. 创建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。

  1. MakeCurrent
EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);

这一步很重要,这个方法用于切换当前线程的EGLContextEGLSurface。只有执行了这一步,之前创建的EGLContextEGLSurface才算是真正的在当前线程可用。

要注意的是:

  • 不能在两个线程里绑定同一个EGLContext
  • 不能在两个线程里绑定同一个EGLSurface到不同的EGLContext
  1. 展示画面
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);

这一步发生在所有的OpenGL渲染指令结束后,将绘制结果展示到本地窗口上去。可以理解为,我们所进行的OpenGL绘制,都是发生在隐藏在屏幕后面的一块内存中,当我们需要将绘制结果展示到屏幕上去,我们就要将屏幕上对应那块内存与需要展示的内存互换位置(这里实际上是互换的内存指针,这样效率更高),就像是更换相框里的相片一样。

销毁

  1. 移除Current的EGLContextEGLSurface
eglMakeCurrent(display_, EGL_NO_SURFACE, EGK_NO_SURFACE, EGL_NO_CONTEXT);

在销毁EGLContextEGLSurface之前,需要将他们从当前线程中解除绑定,防止资源泄漏。

  1. 销毁EGLSurfaceEGLContext
EGLBoolean eglDestroySurface(EGLDisplay dpy, EGLSurface surface);

EGLBoolean eglDestroyContext(EGLDisplay dpy, EGLContext ctx);
  1. 终止EGL环境
EGLBoolean eglTerminate(EGLDisplay dpy);

最后,如果需要退出当前整个EGL环境,则还需要解除EGLDisplay的连接。

iOS EAGL 环境

苹果公司为iOS操作系统提供了自己实现的一套类似EGL的EAGL框架。下面简单说一下EAGL环境的搭建与销毁。

初始化

  1. 创建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];
  1. EAGLContext设置为current
+ (BOOL)setCurrentContext:(nullable EAGLContext*) context;

绑定EAGLContext到当前线程中。

  1. 创建绑定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,可以形象的理解为在画架上贴上画布。

  1. 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参数是UIViewlayer属性。这里需要说明的是,在平台容器层中,我们需要从UIView继承一个自定义视图,并将其layerClass静态方法重写为如下方法:

+ (Class)layerClass {
  return [CAEAGLLayer class];
}
  1. 将Renderbuffer展示到屏幕上
- (BOOL)presentRenderbuffer:(NSUInteger)target;

其中的target必须为GL_RENDERBUFFER

销毁

  1. 销毁framebuffer和renderbuffer
void glDeleteFramebuffers(GLsizei n, const GLuint* framebuffers);
void glDeleteRenderbuffers (GLsizei n, const GLuint* renderbuffers);
  1. 将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的可用时间与SurfaceViewTextureView创建时间会有一定的时间差,要注意在这个时间差内,不能进行OpenGL ES的绘制操作。

为了解决这个问题,则需要对引擎进行类似音乐播放的暂停、继续操作。

注意避免内存泄漏

因为OpenGL是一个大型的状态机,每一步的操作都需要若干步的操作。所以在环境销毁掉时候,若是不慎漏掉一步操作,就很容易引起内存泄漏问题。

为了解决这个问题,就需要对这些必要步骤进行包装,比如简单的举个栗子:可以在类的构造函数中包装一些创建和初始化的操作,而在析构函数中则包装一些解除绑定和销毁目标等操作。当然,这里只是一个简单的栗子,还有其他很多方法来进行包装,根据实际情况来使用,可以有效避免内存泄漏的问题。

总结

本文主要介绍了OpenGL的基础环境搭建,以及部分工程化时需要考虑到的问题。

在文章的最后,放出一个简单的测试效果,用于测试环境搭建是否成功:测试效果

以上是关于从零开始的跨平台渲染引擎——OpenGL基础环境搭建的主要内容,如果未能解决你的问题,请参考以下文章

从零开始的跨平台渲染引擎——OpenGL基础环境搭建

从零开始的跨平台渲染引擎(零)——基础架构分析与设计

从零开始的跨平台渲染引擎(零)——基础架构分析与设计

从零开始的跨平台渲染引擎(零)——基础架构分析与设计

从零开始写一个opengl渲染器——基础设施搭建篇

从零开始的跨平台渲染引擎(番外一)——光和相机