OpenGL ES:Android平臺EGL環境
前言
這篇文章簡單介紹一下在Android平臺下的EGL
環境的相關內容,由於OpenGL ES
並不負責視窗管理以及上下文管理,該職責由各個平臺自行完成;在Android平臺下OpenGL ES
的上下文環境是依賴EGL
的API進行搭建的。
對於EGL
這個框架,谷歌已經提供了GLSurfaceView/">SurfaceView
,是一個已經封裝EGL
相關處理的工具類,但是不夠靈活;對於更加核心的OpengGL ES
的用法(例如多執行緒共享紋理)則需要開發者自行搭建EGL
開發環境。
按照慣例先上一份原始碼ofollow,noindex">AndroidVideo 。
Java相關核心實現在EglBase14.java 和EglBase10.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
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; EGLConfigegl_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, 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
,也就是建立一個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的渲染
涉及到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!