眼下針對移動裝置的應用開發依然如火如荼,而遊戲開發幾乎能占到半壁江山,而OpenGL ES也趁着這個東風大紅大紫。To be honest,我并不是什麼OpenGL的鐵粉,OpenGL那種狀态機(state machine)式的API風格實在不合我的口味,但大丈夫能曲能伸,該學了還得學,該用了還能用。
本文将讨論與OpenGL ES API相關的另一組API -- EGL API的一些基本操作。
首先EGL是一個介于OpenGL ES API與作業系統API之間的橋梁API, 其具體一些平台相關性,比如顯示類型、視窗類型等。
稍微過一下相關的API函數,我比較懶,估計下文會比較簡練,先貼個官方文檔連結,以免有人看不明白http://www.khronos.org/registry/egl/sdk/docs/man/xhtml/(也避免我自已哪天忘了
)。
相關API函數
EGLDisplay eglGetDisplay(EGLNativeDisplayType native_display);
EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
EGLBoolean eglTerminate(EGLDisplay dpy);
eglGetDisplay用于擷取一個顯示連接配接,其中display_id即要連接配接的display的辨別,在Windows平台上,這是個HDC(對此我個人有些不解)。一般情況下,使用預設顯示EGL_DEFAULT_DISPLAY就可以。
eglInitialize用于初始化顯示連接配接,major/minor是一對輸出參數,用于傳回EGL的版本号。如果不關心,可以傳NULL。
eglTerminate用于結束顯示連接配接,如果與之關聯的context正在使用,則連接配接會延遲到context停止使用後再釋放。
雖然标準上沒有指定,但一般eglInitialize/eglTerminate是通過引用計數來工作的,即對同一個顯示連接配接,調用多少次eglInitialize必須調用相同次數的eglTerminate才能真正釋放顯示連接配接。以避免應用程式的不同子產品各自調用這兩個函數引發沖突。
EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
EGLBoolean eglDestroyContext(EGLDisplay dpy, EGLContext ctx);
建立上下文時需要指定config、shared context、attributes。
config指定要建立的context必須支援的frame buffer配置,該值一般使用eglChooseConfig來擷取。
share_context表示與要建立的context共享資源的context,但據說很多實作并不能很好的支援這一功能;如不需要共享上下文使用EGL_NO_CONTEXT。
attrib_list是指定此上下文的特性的輸入參數,目前僅支援EGL_CONTEXT_CLIENT_VERSION。該參數的格式為一個EGLint的數組,數組元素由特性辨別、特性值交替組成,最後以常量EGL_NONE結尾。标準中規定該參數可以為NULL,但Mali OpenGLES模拟器實作不支援NULL。
EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
attrib_list表示要擷取的配置的特性清單,格式與eglCreateContext的attrib_list參數相同。幾個重要的特性辨別有:
EGL_SURFACE_TYPE:支援的surface類型,如EGL_WINDOW_BIT(window surface)、EGL_PBUFFER_BIT(pixel buffer surface)。
EGL_RED_SIZE/EGL_GREEN_SIZE/EGL_BLUE_SIZE/EGL_ALPHA_SIZE/EGL_DEPTH_SIZE:分别表示紅、綠、藍、透明、深度值的位數,前4個一般為8,後一個一般為16。
EGL_COLOR_BUFFER_TYPE:顔色緩沖的類型,如EGL_RGB_BUFFER
EGL_RENDERABLE_TYPE:指定要擷取的配置必須支援的client API,指定EGL_OPENGL_ES2_BIT表示OpenGLES v2.0。
EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
該函數從一個native window建立surface,一般用來在視窗上繪制圖形。
config參數同建立context時的config參數。
win參數表示一個視窗對象,在Windows平台下為HWND句柄。
attrib_list指定要建立的surface的特性,可以為NULL。
EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
EGLContext eglGetCurrentContext(void);
EGLSurface eglGetCurrentSurface(EGLint readdraw);
EGLDisplay eglGetCurrentDisplay(void);
eglMakeCurrent設定目前上下文及讀、寫表面,之後的所有OpenGL ES操作都基于在此指定的上下文和讀、寫surfaces,直到下一次eglMakeCurrent調用。
其它三個函數擷取目前的current對像。
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
交換緩沖區,将繪制結果推到native window上。如果正在繪制的表面為pixel buffer surface,該函數調用不産生任何效果。
以上就是EGL主要的幾個函數,那麼一個最簡單的OpenGL ES應用的大緻工作過程為:
擷取顯示連接配接、初始化顯示連接配接、建立上下文、建立視窗表面、進行初始化操作(設定OpenGL ES選項、加載貼圖、建立Shader、建構繪圖模型等)、進入消息循環(處理消息、進行繪制)、退出消息循環、進行資源清理、銷毀視窗表面、銷毀上下文、關閉顯示連接配接;
典型的消息循環過程一般是有視窗消息時處理消息,空閑時進行繪制。
基于“代碼即文檔"的思想原則,下邊是一些代碼片斷及要點說明
class egl_context {
private:
EGLDisplay _display;
EGLContext _context;
EGLConfig _config;
public:
egl_context() {
_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(_display, nullptr, nullptr);
_config = choose_config(_display);
EGLint attrs[] = { EGL_NONE };
_context = eglCreateContext(_display, _config, EGL_NO_CONTEXT, attrs);
}
explicit egl_context(const egl_context& other) = delete;
~egl_context() {
if (_context != EGL_NO_CONTEXT)
eglDestroyContext(_display, _context);
if (_display)
eglTerminate(_display);
}
EGLContext get() const {
return _context;
}
EGLConfig config() const {
return _config;
}
EGLConfig display() const {
return _display;
}
private:
static EGLConfig choose_config(EGLDisplay display) {
static EGLint attrs[] = { //
//
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT, //
EGL_RED_SIZE, 8, //
EGL_GREEN_SIZE, 8, //
EGL_BLUE_SIZE, 8, //
EGL_ALPHA_SIZE, 8, //
EGL_DEPTH_SIZE, 16, //
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, //
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //
EGL_NONE };
EGLConfig config;
EGLint count;
if (eglChooseConfig(display, attrs, &config, 1, &count) && count)
return config;
return nullptr;
}
};
該類管理EGLDisplay和EGLContext,使用RAII idiom,在構造函數中擷取顯示連接配接,選擇配置、建立context;在析構函數中釋放資源。該類不能複制;比較懶也沒有實作move構造函數。
class egl_window: public window {
public:
egl_window() {
}
virtual ~egl_window() noexcept {
}
egl_window(egl_window&& other) :
window(std::move(other)) {
}
size client_size() const {
RECT rc;
GetClientRect(handle(), &rc);
return size( { (int) (rc.right - rc.left), (int) (rc.bottom - rc.top) });
}
protected:
virtual LRESULT procedure(UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(handle(), msg, wParam, lParam);
}
private:
friend class egl_window_surface;
};
該類封裝了一個視窗。基類定義見筆者另一篇文章http://blog.csdn.net/badalloc/article/details/16860349
class egl_window_surface {
private:
const egl_context& _context;
const egl_window& _window;
EGLSurface _surface;
public:
egl_window_surface(const egl_context& context, const egl_window& window) :
_context(context), _window(window) {
_surface = eglCreateWindowSurface(_context.display(), _context.config(), _window.handle(), 0);
}
explicit egl_window_surface(const egl_window_surface& other) = delete;
~egl_window_surface() {
if (_surface != EGL_NO_SURFACE)
eglDestroySurface(_context.display(), _surface);
}
EGLSurface get() const {
return _surface;
}
};
該類封裝了一個EGLSurface,構造函數中建立surface,析構時銷毀。
class egl_drawing_scope {
private:
EGLDisplay _prev_display;
EGLContext _prev_context;
EGLContext _prev_surface_draw;
EGLContext _prev_surface_read;
EGLDisplay _display;
EGLSurface _surface;
public:
egl_drawing_scope(const egl_context & context,
const egl_window_surface& surface_draw,
const egl_window_surface& surface_read) :
_prev_display(eglGetCurrentDisplay()), //
_prev_context(eglGetCurrentContext()), //
_prev_surface_draw(eglGetCurrentSurface(EGL_DRAW)), //
_prev_surface_read(eglGetCurrentSurface(EGL_READ)) {
_display = context.display();
_surface = surface_draw.get();
eglMakeCurrent(context.display(), surface_draw.get(),
surface_read.get(), context.get());
}
explicit egl_drawing_scope(const egl_drawing_scope& other) = delete;
~egl_drawing_scope() {
eglSwapBuffers(_display, _surface);
eglMakeCurrent(_prev_display, _prev_surface_draw, _prev_surface_read,
_prev_context);
}
};
一個輔助類,用于封裝對make current的調用,并在析構時swap buffers并将current objects設定回原值。
class demo {
std::unique_ptr<egl_context> _context;
std::unique_ptr<egl_window> _window;
std::unique_ptr<egl_window_surface> _surface;
public:
demo() {
_context.reset(new egl_context());
_window.reset(new egl_window());
_surface.reset(new egl_window_surface(*_context, *_window));
egl_drawing_scope(*_context, *_surface, *_surface);
glClearColor(1, 1, 1, 1);
}
void frame() {
size client_size = _window->client_size();
egl_drawing_scope drawing_scope(*_context, *_surface, *_surface);
glViewport(0, 0, client_size.width, client_size.height);
draw();
}
void draw() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
static void main() {
demo d;
MSG msg;
while (true) {
if (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessageW(&msg);
} else {
d.frame();
}
}
}
};
這是入口類,其main方法會被main或WinMain調用。
該類構造時,建立上邊的egl_contex、egl_window、egl_surface類執行個體,之後設定建立的這些對象為current objects,并通過glClearColor這個GLES函數設定清除色彩緩沖時用的背景色。
該類還包括一個 用于繪制每一幀的frame函數,其實作包括設定目前上下文、表面,設定視口,然後調用draw函數來執行實際的繪制操作。
負責實際繪制操作的draw函數目前隻有一行清除緩沖區的操作,對色彩緩沖區、深度緩沖區進行清除。
該類的main函數是實際的入口函數,其中建立了一個demo類執行個體,并啟動了一個消息循環,在有消息時處理消息,沒有消息時進行繪制。
以上代碼大部分原則上可以拿Android的NDK項目中使用;而iOS不直接提供EGL API,取而代之的是Apple的EAGL,個人感覺其倒是比标準EGL簡化了一些。
示例代碼編譯環境:
ARM的Mali OpenGL ES模拟器的Windows x64版本(http://malideveloper.arm.com/cn/develop-for-mali/tools/opengl-es-3-0-emulator/);
mingw64_gcc-4.8.2_x86_64編譯器開啟C++11支援。