前言
这篇文章简单介绍一下在 Android 平台下的 EGL 环境的相关内容, 由于 OpenGL ES 并不负责窗口管理以及上下文管理, 该职责由各个平台自行完成; 在 Android 平台下 OpenGL ES 的上下文环境是依赖 EGL 的 API 进行搭建的.
对于 EGL 这个框架, 谷歌已经提供了 GLSurfaceView, 是一个已经封装 EGL 相关处理的工具类, 但是不够灵活; 对于更加核心的 OpengGL ES 的用法 (例如多线程共享纹理) 则需要开发者自行搭建 EGL 开发环境.
按照惯例先上一份源码 https://github.com/net168/AndroidVideo .
Java 相关核心实现在 和 .
Native 相关实现, 可以参考 .
前置知识
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 环境配置整体流程
获取默认的 EGLDisplay.
对 EGLDisplay 进行初始化.
输入预设置的参数获取 EGL 支持的 EGLConfig.
通过 EGLDisplay 和 EGLConfig 创建一个 EGLContext 上下文环境.
创建一个 EGLSurface 来连接 EGL 和设备的屏幕.
在渲染线程绑定 EGLSurface 和 EGLContext.
[进行 OpenGL ES 的 API 渲染步骤] (与 EGL 无关)
调用 SwapBuffer 进行双缓冲切换显示渲染画面.
释放 EGL 相关资源 EGLSurface,EGLContext,EGLDisplay.
获取显示设备
首先, 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;
方法的后面参数代表 Major 和 Minor 的版本, 比如 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 上下文环境
当拿到 EGLDisplay 和 EGLConfig 后, 就可以开始创建 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 跟设备的屏幕桥接起来时, 我们需要用到 EGLSurface 让 EGL 有一个 "桥" 的功能, 从而使得 OpenGL ES 的输出可以渲染到设备屏幕上;
EGLSurface 其实就是一个 FrameBuffer, 通过 EGL 库提供的 eglCreateWindowSurface()可以创建一个可实际显示的 Surface; 也可以通过 EGL 库提供的 eglCreatePbufferSurface()方法创建一个 OffScreen 的 Surface.
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 中只支持 Surface 和 SurfaceTexture, 在 EGL10 中只支持 SurfaceHolder 和 SurfaceTexture.
c 代码:
- // 创建可显示的 Surface
- EGLSurface egl_surface;
- const EGLint attribs[] = { EGL_NONE };
- egl_surface = eglCreateWindowSurface(egl_display, egl_config, Windows, attribs);
- if (egl_surface == EGL_NO_SURFACE)
- return error;
在 Native 层, eglCreateWindowSurface()的第三个入参 Windows 是需要传入一个 ANativeWindow 对象, 也就是本地设备屏幕的表示.
我们可以通过 Surface(由 SurfaceView 或者 TextureView 获得或者构建出来的 Surface 对象)来构建 ANativeWindow.
需要引入头文件:
- #include <Android/native_window.h>
- #include <Android/native_window_jni.h>
获取 ANativeWindow 的代码如下:
- //surface 也就是一个 jobject, 对应 java 层的 Surface.
- ANativeWindow *Windows = ANativeWindow_fromSurface(env, surface);
如果我们要做离屏渲染的话, 就需要用到离屏处理的 Surface, 也就是创建一个 PBufferSurface,PBufferSurface 的保存位置是在显存中的帧, 具体代码可以参考.
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 来为该线程绑定 Surface 和 Context, 值得注意一点的是一个 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 的渲染
来源: http://www.jianshu.com/p/d5ff1ff4ee2a