OpenGL ES:Android平台EGL环境

作者: 码农叔叔 | 来源:发表于2018-11-27 15:23 被阅读21次

    前言

    这篇文章简单介绍一下在Android平台下的EGL环境的相关内容,由于OpenGL ES并不负责窗口管理以及上下文管理,该职责由各个平台自行完成;在Android平台下OpenGL ES的上下文环境是依赖EGL的API进行搭建的。

    对于EGL这个框架,谷歌已经提供了GLSurfaceView,是一个已经封装EGL相关处理的工具类,但是不够灵活;对于更加核心的OpengGL ES的用法(例如多线程共享纹理)则需要开发者自行搭建EGL开发环境。

    按照惯例先上一份源码 AndroidVideo
    Java相关核心实现在 EglBase14.javaEglBase10.java
    Native相关实现,可以参考 egl_base.cpp


    前置知识

    Java层实现
    在Java层,EGL封装了两套框架,分别是:

    • 位于javax.microedition.khronos.egl包下的EGL10
    • 位于android.opengl包下的EGL14

    其主要区别是:

    • EGL14是在Android 4.2(API 17)引入的,换言之API 17以下的版本不支持EGL14
    • EGL10不支持OpenGL ES 2.x,因此在EGL10中某些相关常量参数只能用手写硬编码代替,例如EGL14.EGL_CONTEXT_CLIENT_VERSION以及EGL14.EGL_OPENGL_ES2_BIT等等。

    PS:由于主体流程基本一致,所以本篇以EGL14的代码进行示例。

    Native层实现
    程序在Native层使用EGL环境时。
    需要引入EGL的so库:

    Android.mk:

    LOCAL_LDLIBS += -lEGL
    

    CMake:

    find_library( EGL-lib
            EGL )
    

    需要包含头文件:

    #include <EGL/egl.h>
    #include <EGL/eglext.h>
    

    EGL环境配置整体流程

    1. 获取默认的EGLDisplay
    2. EGLDisplay进行初始化。
    3. 输入预设置的参数获取EGL支持的EGLConfig
    4. 通过EGLDisplayEGLConfig创建一个EGLContext上下文环境。
    5. 创建一个EGLSurface来连接EGL和设备的屏幕。
    6. 在渲染线程绑定EGLSurfaceEGLContext
    7. 【进行OpenGL ES的API渲染步骤】(与EGL无关)
    8. 调用SwapBuffer进行双缓冲切换显示渲染画面。
    9. 释放EGL相关资源EGLSurfaceEGLContextEGLDisplay

    获取显示设备

    首先,EGL是需要知道绘制内容的目标在哪里,EGLDisplay是一个封装了物理屏幕的数据类型,也可以理解为绘制目标的一个抽象。

    通常通过eglGetDisplay()方法返回EGLDisplay来作为OpenGL ES的渲染目标,在该方法中,一般来说都会将常量EGL_DEFAULT_DISPLAY传进方法中,而各个手机厂商则会返回默认的显示设备。

    java代码:

    EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
    //需判断是否成功获取EGLDisplay
    if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
        throw new RuntimeException("Unable to get EGL14 display");
    }
    

    c代码:

    EGLDisplay egl_display;
    egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    //需判断是否成功获取EGLDisplay
    if (egl_display == EGL_NO_DISPLAY)
        return error;
    

    一般来说,我们需要验证eglGetDisplay()的返回值,如果是EGL_NO_DISPLAY的话,那么就是没有获取默认显示设备,需要返回给客户端上层处理异常。

    当我们获取到了EGLDisplay,我们需要调用eglInitialize()对其进行初始化,该方法会返回一个bool变量代表执行是否成功。

    java代码:

    int version[] = new int[2];
    if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
        throw new RuntimeException("Unable to initialize EGL14");
    }
    

    c代码:

    EGLint major, minor;
    if (!eglInitialize(egl_display, &major, &minor))
        return error;
    

    方法的后面参数代表MajorMinor的版本,比如EGL的版本号是1.0,那么Major将返回1,Minor返回0。
    如果不关心版本号,这两个参数可以传入NULL


    配置输出格式

    当我们获取到EGLDisplay后,其实已经可以将OpenGL ES的输出与设备的屏幕桥接起来了,但是还是需要指定一些配置项,例如色彩格式、像素格式、RGBA的表示以及SurfaceType等,实际上也就是指FrameBuffer的配置参数。

    一般来说不同平台的EGL标准是不同的,以下是Android平台一个比较通用的配置参数(Java的就不列举了):

    const EGLint config_attribs[] = {
            EGL_BUFFER_SIZE, 32,   //颜色缓冲区中所有组成颜色的位数
            EGL_ALPHA_SIZE, 8,     //颜色缓冲区中透明度位数
            EGL_BLUE_SIZE, 8,      //颜色缓冲区中蓝色位数
            EGL_GREEN_SIZE, 8,     //颜色缓冲区中绿色位数
            EGL_RED_SIZE, 8,       //颜色缓冲区中红色位数
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,  //渲染窗口支持的布局组成
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,  //EGL 窗口支持的类型
            EGL_NONE
    };
    

    PS:EGL的参数配置一般都以 id,value 依次存放,对于个别的属性可以只有 id 没有 value ,并以EGL_NONE标识结尾信息。
    最终可以通过调用eglChooseConfig()方法得到配置选项信息:

    java代码:

    EGLConfig[] configs = new EGLConfig[1];
    int[] numConfigs = new int[1];
    //检测返回值是否成功
    if (!EGL14.eglChooseConfig(eglDisplay, configAttributes, 0, configs, 0, configs.length,
            numConfigs, 0)) {
        throw new RuntimeException("eglChooseConfig failed");
    }
    //如果没有配置的Config
    if (numConfigs[0] < 0) {
        throw new RuntimeException("Unable to find any matching EGL config");
    }
    EGLConfig eglConfig = configs[0];
    //对应的Config不存在
    if (eglConfig == null) {
        throw new RuntimeException("eglChooseConfig returned null");
    }
    

    c代码:

    EGLint num_config;
    EGLConfig  egl_config;
    //检测返回值是否成功
    if (!eglChooseConfig(egl_display, config_attribs, &egl_config, 1, &num_config))
            return error;
    //如果没有配置的Config
    if (num_config < 0)
            return error;
    //对应的Config不存在
    if (_egl_config == NULL)
            return error;
    

    简单看一下函数原型:eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size,EGLint *num_config)可以知道:
    第2个参数attrib_list指的是配置参数列表,也就是上面的config_attribs[]
    第3个参数configs返回输出的EGLConfigs数据,可能有多个;
    第4个参数config_size则表示最多需要输出多少个EGLConfig
    第5个参数num_config则代表满足配置参数的EGLConfig的个数。

    附带一个EGLConfig属性表格:

    属性 描述 默认值
    EGL_BUFFER_SIZE 颜色缓冲区中所有组成颜色的位数 0
    EGL_RED_SIZE 颜色缓冲区中红色位数 0
    EGL_GREEN_SIZE 颜色缓冲区中绿色位数 0
    EGL_BLUE_SIZE 颜色缓冲区中蓝色位数 0
    EGL_LUMINANCE_SIZE 颜色缓冲区中亮度位数 0
    EGL_ALPHA_SIZE 颜色缓冲区中透明度位数 0
    EGL_ALPHA_MASK_SIZE 遮挡缓冲区透明度掩码位数 0
    EGL_BIND_TO_TEXTURE_RGB 绑定到 RGB 贴图使能为真 EGL_DONT_CARE
    EGL_BIND_TO_TEXTURE_RGBA 绑定到 RGBA 贴图使能为真 EGL_DONT_CARE
    EGL_COLOR_BUFFER_TYPE 颜色缓冲区类型 EGL_RGB_BUFFER, 或者EGL_LUMINANCE_BUFFER EGL_RGB_BUFFER
    EGL_CONFIG_CAVEAT 配置有关的警告信息 EGL_DONT_CARE
    EGL_CONFIG_ID 唯一的 EGLConfig 标示值 EGL_DONT_CARE
    EGL_CONFORMANT 使用EGLConfig 创建的上下文符合要求时为真
    EGL_DEPTH_SIZE 深度缓冲区位数 0
    EGL_LEVEL 帧缓冲区水平 0
    EGL_MAX_PBUFFER_WIDTH 使用EGLConfig 创建的PBuffer的最大宽度
    EGL_MAX_PBUFFER_HEIGHT 使用EGLConfig 创建的PBuffer最大高度
    EGL_MAX_PBUFFER_PIXELS 使用EGLConfig 创建的PBuffer最大尺寸
    EGL_MAX_SWAP_INTERVAL 最大缓冲区交换间隔 EGL_DONT_CARE
    EGL_MIN_SWAP_INTERVAL 最小缓冲区交换间隔 EGL_DONT_CARE
    EGL_NATIVE_RENDERABLE 如果操作系统渲染库能够使用EGLConfig 创建渲染渲染窗口 EGL_DONT_CARE
    EGL_NATIVE_VISUAL_ID 与操作系统通讯的可视ID句柄 EGL_DONT_CARE
    EGL_NATIVE_VISUAL_TYPE 与操作系统通讯的可视ID类型 EGL_DONT_CARE
    EGL_RENDERABLE_TYPE 渲染窗口支持的布局组成标示符的遮挡位EGL_OPENGL_ES_BIT, EGL_OPENGL_ES2_BIT, orEGL_OPENVG_BIT that EGL_OPENGL_ES_BIT
    EGL_SAMPLE_BUFFERS 可用的多重采样缓冲区位数 0
    EGL_SAMPLES 每像素多重采样数 0
    EGL_S TENCIL_SIZE 模板缓冲区位数 0
    EGL_SURFACE_TYPE EGL 窗口支持的类型EGL_WINDOW_BIT, EGL_PIXMAP_BIT,或EGL_PBUFFER_BIT EGL_WINDOW_BIT
    EGL_TRANSPARENT_TYPE 支持的透明度类型 EGL_NONE
    EGL_TRANSPARENT_RED_VALUE 透明度的红色解释 EGL_DONT_CARE
    EGL_TRANSPARENT_GRE EN_VALUE 透明度的绿色解释 EGL_DONT_CARE
    EGL_TRANSPARENT_BLUE_VALUE 透明度的兰色解释 EGL_DONT_CARE

    创建EGL上下文环境

    当拿到EGLDisplayEGLConfig后,就可以开始创建EGL的上下文环境EGLContext了。
    EGLContext的存在是因为OpenGL ES所创建的资源对于开发者来说可见的仅仅只是一个 ID 而已,而其实际内容依赖于这个上下文。
    一个EGLContext只能在一个线程中使用,如果将EGLContext所持有的OpengGL资源在多线程间共享,那么需要用到共享上下文(share context)。

    简单看下EGLContext的创建代码:

    java代码:

    //指定OpenGL ES2版本
    int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
    //创建EGLContext上下文
    EGLContext eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, null, contextAttributes, 0);
    //需要检测Context是否存在
    if (eglContext == EGL14.EGL_NO_CONTEXT) {
        throw new RuntimeException("Failed to create EGL context");
    }
    

    c代码:

    EGLContext egl_context;
    //指定OpenGL ES2版本
    const EGLint context_attribs[] = {
          EGL_CONTEXT_CLIENT_VERSION, 2,
          EGL_NONE
    };
    //创建EGLContext上下文
    egl_context = eglCreateContext(egl_display, egl_config, NULL, context_attribs);
    //需要检测Context是否存在
    if (egl_context == EGL_NO_CONTEXT)
        return error;
    

    函数eglCreateContext()的第三个参数可以传入一个EGLContext的变量,该变量的意义是指可以与正在创建的上下文环境共享OpenGL ES资源,包括纹理、FrameBuffer以及其他的Buffer等资源。
    如果传入NULL代表不需要与其他的OpenGL ES上下文共享任何资源。


    连接EGL和设备屏幕

    当我们需要将EGL跟设备的屏幕桥接起来时,我们需要用到EGLSurfaceEGL有一个“桥”的功能,从而使得OpenGL ES的输出可以渲染到设备屏幕上;
    EGLSurface其实就是一个FrameBuffer,通过EGL库提供的eglCreateWindowSurface()可以创建一个可实际显示的Surface;也可以通过EGL库提供的eglCreatePbufferSurface()方法创建一个OffScreenSurface

    java代码:

    //创建可显示的Surface
    EGLSurface eglSurface;
    int[] surfaceAttribs = {EGL14.EGL_NONE};
    eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface, surfaceAttribs, 0);
    if (eglSurface == EGL14.EGL_NO_SURFACE) {
        throw new RuntimeException("Failed to create window surface");
    }
    

    这里需要强调一点的是eglCreateWindowSurface()的第三个入参surface虽然可以传入Object;但是在EGL14中只支持SurfaceSurfaceTexture,在EGL10中只支持SurfaceHolderSurfaceTexture

    c代码:

    //创建可显示的Surface
    EGLSurface egl_surface;
    const EGLint attribs[] = { EGL_NONE };
    egl_surface = eglCreateWindowSurface(egl_display, egl_config, window, attribs);
    if (egl_surface == EGL_NO_SURFACE)
        return error;
    

    在Native层,eglCreateWindowSurface()的第三个入参window是需要传入一个ANativeWindow对象,也就是本地设备屏幕的表示。

    我们可以通过Surface(由SurfaceView或者TextureView获得或者构建出来的Surface对象)来构建ANativeWindow
    需要引入头文件:

    #include <android/native_window.h>
    #include <android/native_window_jni.h>
    

    获取ANativeWindow的代码如下:

    //surface也就是一个jobject,对应java层的Surface。
    ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
    

    如果我们要做离屏渲染的话,就需要用到离屏处理的Surface,也就是创建一个PBufferSurfacePBufferSurface的保存位置是在显存中的帧,具体代码可以参考。

    java代码:

    //创建离屏Surface
    EGLSurface eglSurface;
    int[] surfaceAttribs = {
            EGL14.EGL_WIDTH, width,
            EGL14.EGL_HEIGHT, height,
            EGL14.EGL_NONE
    };
    eglSurface = EGL14.eglCreatePbufferSurface(eglDisplay, eglConfig, surfaceAttribs, 0);
    if (eglSurface == EGL14.EGL_NO_SURFACE) {
        throw new RuntimeException("Failed to create pixel buffer surface");
    }
    

    c代码:

    //创建离屏Surface
    EGLSurface egl_surface;
    const EGLint attribs[] = {
        EGL_WIDTH, width,
        EGL_HEIGHT, height,
        EGL_NONE
    };
    egl_surface = eglCreatePbufferSurface(egl_display, egl_config, attribs);
    if (egl_surface == EGL_NO_SURFACE)
        return error;
    

    另外说一点,EGLSurface还支持参数的查询与设置,例如我们想知道新创建的Surface的宽高,那么可以用到下面的方法。

    java代码:

    //查询Surface的width
    int[] array = new int[1];
    EGL14.eglQuerySurface(eglDisplay, eglSurface, EGL14.EGL_WIDTH, array, 0);
    //设置Surface的width
    if (!EGL14.eglSurfaceAttrib(eglDisplay, eglSurface, EGL14.EGL_WIDTH, 600))
        throw new RuntimeException("eglSurfaceAttrib fail");
    

    c代码:

    //查询Surface的width
    EGLint value;
    eglQuerySurface(_egl_display, _egl_surface, EGL_WIDTH, &value);
    //设置Surface的width
    if (!eglSurfaceAttrib(_egl_display, _egl_surface, EGL_WIDTH, 600))
        return error;
    

    EGL变量与线程的绑定

    一般来说,开发者需要为OpenGL ES开辟一个新的线程,来执行渲染操作,并且需要为该线程绑定显示设备EGLSurface和上下文环境EGLContext

    每个线程都需要绑定一个上下文,才可以开始执行OpenGL ES指令,我们可以通过eglMakeCurrent来为该线程绑定SurfaceContext,值得注意一点的是一个EGLContext只能绑定到一个线程上面。

    java代码:

    if (!EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT)) {
        throw new RuntimeException("detachCurrent failed");
    }
    

    c代码:

    if (!eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context))
        return error;
    

    可以通过返回值来判断eglMakeCurrent()是否成功,这个也是必要的。


    OpenGL的渲染

    涉及到OpenGl ES的绘图API和纹理相关方面的知识,本篇这里不做介绍,请关注后续相关文章。


    双缓冲机制

    EGL在初始化的时候默认设置的是双缓冲模式,也就是两份FrameBuffer;
    即一份缓冲用于绘制图像,一份缓冲用于显示图像,每次显示时需要交换两份缓冲。
    我们需要在OpenGL ES绘制完毕后,调用

    eglSwapBuffers(egl_display, egl_surface);
    

    将前台的FrameBuffer和后台FrameBuffer进行交换。


    EGL的资源释放

    当然,EGL在我们不需要的时候也是需要进行释放的。
    我们需要将线程跟EGL环境解除绑定:

    eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    

    然后要销毁EGLSurface:

    eglDestroySurface(egl_display, egl_surface);
    

    接着清理掉上下文环境:

    eglDestroyContext(egl_display, egl_context);
    

    最终关闭掉显示设备:

    eglTerminate(egl_display);
    

    通过上面也就是完成了一个EGL的资源释放工作。


    最后说一些

    在Android平台上面,当程序切到后台的时候,需要释放EGL环境,在应用挪回前台时,重新初始化相关环境。
    不过谷歌提供了一个已经封装完善的GLSurfaceView提供OpenGL ES的绘制环境,普通需求下采用GLSurfaceView进行绘制也是一个不错的选择。
    有兴趣可以读读这篇文章:源码解析:Android源码GLSurfaceView源码解析
    最后放一个EGL的官方文档地址


    结语

    这篇文章简单介绍了一下Android平台下的EGL环境的相关内容,并提供了基于java层EGL14和native层的EGL相关API的使用示例。
    后续文章将介绍怎么在EGL环境下进行OpenGL ES相关 API的一些使用。

    本文同步发布于简书CSDN

    End!

    相关文章

      网友评论

        本文标题:OpenGL ES:Android平台EGL环境

        本文链接:https://www.haomeiwen.com/subject/jzalqqtx.html