眼下针对移动设备的应用开发依然如火如荼,而游戏开发几乎能占到半壁江山,而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支持。