实践解析 | 如何用 OpenGL 实现跨平台应用高效渲染
OpenGL(Open Graphics Library 开放式图形库)是一个定义了跨编程语言、跨平台的编程接口规格的专业图形程序接口。它可用于三维、二维图形图像的渲染,是一个功能强大,调用方便的底层图形库。在一个 RTC 应用中,因视频渲染或算法处理的需要,OpenGL 是一种高效的渲染或处理实现方式。OpenGL 的高效实现在 Windows、Linux 和 macOS 都有相应支持。此外,移动平台 iOS 和 Android 都能支持 OpenGL ES(OpenGL for Embedded Systems)。利用 OpenGL 进行跨平台的开发是非常方便的。本文就来分享一下如何利用 OpenGL 实现跨平台应用高效渲染。
因为各种硬件和软件的实现不同,OpenGL 在各平台支持的能力和规范也有差别。
移动平台支持的 OpenGL ES 是 OpenGL 三维图形 API 的一个子集,它是针对手机等嵌入式设备而设计。因此,需要兼容移动平台的跨平台开发时,尽量选择 OpenGL ES 支持的 API 。
OpenGL 和 OpenGL ES 的早期版本是针对固定管线的,从 2.0 开始支持着色语言(shading language)和可编程管线。可编程管线的 API 使用更灵活,支持的特性更丰富。目前多数硬件都已经支持可编程管线,而且官方已经建议废弃固定管线 API 使用,在高版本中也移除了固定管线 API 。因此建议使用着色语言和可编程管线 API ,并至少支持 2.0 版本。如果要求支持的设备或系统都比较新,可以直接使用 3.0 或更高版本。
在应用程序调用任何 OpenGL 函数之前,都需要首先创建一个 OpenGL 上下文(Context)。这个 OpenGL 上下文可以理解为一个非常庞大的状态机,保存了 OpenGL 中的各种状态,这是 OpenGL 执行各种指令的基础。某种程度上,OpenGL 函数都是对其上下文这个状态机中对象或状态的操作。由于 OpenGL 上下文是一个巨大的状态机,切换上下文的开销往往比较大。不同的绘制模块又需要不同的状态和对象管理。因此在应用程序中可以创建多个不同的上下文,在不同线程中使用不同的上下文,上下文之间可以共享缓冲区,纹理等资源。应当尽量避免反复切换上下文和修改大量状态,提高处理效率。
虽然 OpenGL 是跨平台的,但各平台的实现和 OpenGL 的环境搭建会有所不同。开发过程中各平台的 OpenGL 上下文(Context)的建立和切换都有所不同。开发者既可以使用如 GLFW、GLUT 等开源框架帮助完成 OpenGL 上下文环境的建立和管理,也可以使用各平台的原生 API 来完成。本文主要介绍使用各平台原生 API 的方法。下面分平台介绍各平台的 OpenGL 环境建立与上下文创建。
一、Windows
Windows 平台由于微软的 Direct3D 存在,微软对 OpenGL 的支持并不积极。在大多数微软操作系统中所支持 OpenGL 版本还是 1.0 和 1.1 ,仅支持固定管线 API ,对于现代使用 OpenGL 开发的程序并不友好。不过通过 OpenGL 的 ARB 扩展机制可以让我们访问到 OpenGL 的高级特性接口。Windows OpenGL 实现提供了名为 wglGetProcAddress 的函数,允许我们对指向一个由驱动程序提供的 OpenGL 函数的指针进行检索。不过还有一种更为快捷的方法。通过 GLEW(GL Extension Wrangler)库完成这一繁琐的检索过程。只需要引入头文件 glew.h 和 glew 库并在应用程序的开始调用 glewInit(),之后 OpenGL 1.1 以上的扩展和核心特性的所有函数指针都将自动被设置完成。
glewInit 的调用需要先创建一个 OpenGL 上下文环境,在初始化完成后,再删除这个环境。之后重新创建一个支持 WGL_ARB 扩展的 OpenGL 上下文环境。示例代码如下:
/* 注册窗口类 /WNDCLASSEX wcex;ZeroMemory(&wcex, sizeof(WNDCLASSEX));wcex.cbSize = sizeof(WNDCLASSEX);wcex.hInstance = GetModuleHandle(NULL);wcex.lpfnWndProc = GLEW_WindowProc;wcex.lpszClassName = kszGlewInitClassName;
/创建窗口以初始化glew /HWND hwnd = CreateWindow(kszGlewInitClassName,L"", (WS_POPUP | WS_DISABLED), CW_USEDEFAULT, CW_USEDEFAULT, 10, 10,NULL, NULL, GetModuleHandle(NULL), NULL);/ 设置像素格式 /HDC hdc = GetDC(hwnd);PIXELFORMATDESCRIPTOR pfd;memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);pfd.nVersion = 1;pfd.dwFlags=PFD_DOUBLEBUFFER | PFD_SUPPORT_OPENGL |PFD_DRAW_TO_WINDOW;pfd.iPixelType = PFD_TYPE_RGBA;pfd.cColorBits = 24;pfd.cDepthBits = 32;pfd.cStencilBits = 8;pfd.iLayerType = PFD_MAIN_PLANE;int32_t nPixelFormat = ChoosePixelFormat(hdc,&pfd);BOOL bRet = SetPixelFormat(hdc, nPixelFormat,&pfd);/ 创建OpenGL上下文环境并设置为当前上下文*/HGLRC hglrc = wglCreateContext(hdc);wglMakeCurrent(hdc, hglrc);/* 初始化glew /if (glewInit()) {printf("glewInitfailed\n");// handleinit fail……}/ 释放当前OpenGL上下文环境*/wglMakeCurrent(NULL, NULL);wglDeleteContext(hglrc);ReleaseDC(hwnd, hdc);DestroyWindow(hwnd);UnregisterClass(kszGlewInitClassName, NULL);/* 开始选择真正的格式并创建相应的OpenGL上下文*//* 再次创建窗口 /……/ 设置关心的重要属性 /int32_t pixAttribs[] = {WGL_DRAW_TO_WINDOW_ARB, GL_TRUE, // 绘制在窗口的像素格式WGL_SUPPORT_OPENGL_ARB, GL_TRUE, //支持OpenGL渲染WGL_DOUBLE_BUFFER_ARB, GL_TRUE, // 支持双缓冲WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, // 像素格式为RGBAWGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, // 支持硬件加速WGL_COLOR_BITS_ARB, 32, // 颜色缓冲位深32WGL_ALPHA_BITS_ARB, 8, // alpha通道位深 8WGL_DEPTH_BITS_ARB,24, // 深度缓冲位深 24WGL_STENCIL_BITS_ARB, 8, // 模板缓冲位深 8WGL_SAMPLE_BUFFERS_ARB, GL_TRUE,// 开启多重采样MSAAWGL_SAMPLES_ARB, 4, // 4倍多重采样MSAA0}; // 以NULL结束/ 要求寻找与我们属性相匹配的最佳格式,并取回一种*/uint32_t pixCount = 0;int32_t nPixelFormat = 0;wglChoosePixelFormatARB(hdc,&pixAttribs[0],NULL,1, &nPixelFormat, &pixCount);if (nPixelFormat != -1) {/* 设置选中的最佳格式 /SetPixelFormat(hdc, nPixelFormat, &pfd);/ 创建相应的OpenGL上下文环境*/int32_tattribs[] = {WGL_CONTEXT_MAJOR_VERSION_ARB, 3,WGL_CONTEXT_MINOR_VERSION_ARB, 3,WGL_CONTEXT_FLAGS_ARB, 0,0};HGLRCwglrc = wglCreateContextAttribsARB(hdc, 0, attribs);if(wglrc) {wglMakeCurrent(hdc, wglrc);hglrc_ = wglrc;} else {printf("wglCreateContextAttribsARBfailed\n");//handle failed……}} else {printf("ChoosePixelFormatfailed\n");// handlefailed……}/* 查询并打印OpenGL版本 */const GLubyte *GLVersionString = glGetString(GL_VERSION);int32_t OpenGLVersion[2];glGetIntegerv(GL_MAJOR_VERSION, &OpenGLVersion[0]);glGetIntegerv(GL_MINOR_VERSION,&OpenGLVersion[1]);printf("OpenGLversion %d.%d\n", OpenGLVersion[0], OpenGLVersion[1]);
二、macOS
macOS 提供了 glut,NSOpenGL,CGL 等接口来创建和管理 OpenGL 环境。本文以 NSOpenGL 为例来介绍。
NSOpenGLView 是一个轻量级的 NSView 子类封装,方便地提供了 OpenGL 绘制环境的创建与管理。在其内部维护了 NSOpenGLPixelFormat 和 NSOpenGLContext 对象,用户可以方便的对其进行访问和管理。
NSOpenGLView 的创建很简单,首先通过设定 NSOpenGLPixel FormatAttribute 属性值创建 NSOpenGLPixelFormat 对象,再用 NS OpenGLPixelFormat 创建 NSOpenGLView。
static NSOpenGLPixelFormatAttribute kDefaultAttributes[]= {NSOpenGLPFADoubleBuffer, //双缓冲 NSOpenGLPFADepthSize, 24, //深度缓冲位深NSOpenGLPFAStencilSize, 8, //模板缓冲位深NSOpenGLPFAMultisample, //多重采样NSOpenGLPFASampleBuffers, (NSOpenGLPixelFormatAttribute)1, //多重采样bufferNSOpenGLPFASamples, (NSOpenGLPixelFormatAttribute)4, // 多重采样数NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, // OpenGL3.20};NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormatalloc] initWithAttributes: kDefaultAttributes];NSOpenGLView*renderView=[[NSOpenGLView alloc]initWithFrame:frameRect pixelFormat:pixelFormat];开发者可以继承 NSOpenGLView 实现子类,并通过实现 -(void)prepareOpenGL ;和 - (void)clearGLContext ;自定义 OpenGL Context 初始化和删除时的一些行为。具体的绘制操作可以在 -(void)drawRect: (NSRect) bounds; 中实现。需要注意的是,为保证Retina 屏的显示效果,可以设置 NSOpenGLView 的属性 wantsBestResolutionOpenGLSurface 为 YES。
(void)prepareOpenGL {[super prepareOpenGL];[self ensureGLContext];// setupOpenGL resources……}
(void)clearGLContext {[self ensureGLContext];// cleanup OpenGL resources……[super clearGLContext];}-(void) drawRect: (NSRect) bounds {[self ensureGLContext];// draw OpenGL……[super drawRect:rect];}
(void)ensureGLContext {NSOpenGLContext*context = [self openGLContext];if([NSOpenGLContext currentContext] != context) {[context makeCurrentContext];}}
三、iOS
iOS 从 2013 年 9 月上线的 iOS 7 及同期发布的新设备开始支持 OpenGL ES 3.0,Apple 也是从这个时间点开始发布了 64 位设备。因此目前市面上除了少量早期的 iOS 设备外,绝大多数 iOS 设备都已支持 OpenGL ES 3.0。
和 macOS 类似,iOS 也提供了封装好的 UIView——GLKView,开发者可以方便地利用此 View 实现 OpenGL ES 的绘制。此外也提供了 OpenGL ES 的 framebuffer 对象可以实现离屏渲染,或基于 CAEAGLLayer 实现 CALayer 层面的绘制。本文还是以 GLKView 为例进行说明。
EAGLContext glContext = [[EAGLContext alloc]initWithAPI: kEAGLRenderingAPIOpenGLES3];if (!glContext) {// OpenGL ES 3 创建失败,创建OpenGL ES 2glContext=[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];}GLKViewglkView=[[GLKView alloc] initWithFrame:frameRect context:glContext];// 配置renderbuffersglkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;glkView.drawableDepthFormat = GLKViewDrawableDepthFormat24;glkView.drawableStencilFormat = GLKViewDrawableStencilFormat8;glkView.drawableMultisample = GLKViewDrawableMultisample4X;glkView.delegate = self;
具体的渲染操作通过GLKViewDelegate的方法-(void)glkView:(GLKView*)view drawInRect:(CGRect)rect;实现
(void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect {if ([EAGLContext currentContext] != glContext_) {[EAGLContext setCurrentContext:glContext_];}// draw OpenGL……}
四、Android
Android 也是从 2013 年发面的 Android 4.3 开始支持 OpenGL ES 3 的,但相比封闭的 iOS 生态,真正支持 OpenGL ES 3 的设备并不容易判断。可以通过创建 OpenGL ES 3 的上下文是否成功来判断。
Android 也有和 iOS GLKView 类似的封装好的 OpenGL View——GLSurfaceView,开发 者可以直接通过 GLSurfaceView 的方法来创建和管理 OpenGL ES 上下文。也可以基于 SurfaceView 和 EGL 的接口创建自己的 OpenGL 上下文环境和渲染 View。这里介绍一种基于 EGL native 接口的方法(使用 EGL 的 Java 接口,调用流程也是一样的)。
EGL 是图形渲染 API(如 OpenGL ES)与本地平台窗口系统之间的一层接口,它保证了 OpenGL ES 的平台独立性。提供了创建渲染表面(rendering surface)、图形上下文(graphics context),同步应用程序和平台渲染 API,显示设备访问,渲染配置等管理功能。基于 EGL 的创建上下文环境主要有初始化、选择和设置合适的配置、创建表面、创建上下文四个步骤。
EGL 初始化
EGLBoolean success = EGL_FALSE;EGLint err = 0;EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);if (EGL_NO_DISPLAY == display_) {printf("eglGetDisplay error %d\n",eglGetError());return;}EGLint major=0, minor=0;if (EGL_FALSE == eglInitialize(display, &major, &minor);) {printf("eglInitialize error %d\n", eglGetError());return;}
2.选择和设置合适的配置
const EGLint configAttribs[] = {EGL_RED_SIZE,8,EGL_GREEN_SIZE,8,EGL_BLUE_SIZE,8,EGL_ALPHA_SIZE, 8,EGL_STENCIL_SIZE, 8,EGL_SAMPLE_BUFFERS, 1,EGL_SAMPLES,4,EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,EGL_SURFACE_TYPE, EGL_WINDOW_BIT, // 基于窗口的 surfaceEGL_NONE};EGLConfig config;EGLint numConfigs;if (EGL_FALSE == eglChooseConfig(display_,configAttribs, &config, 1, &numConfigs)) {// handle andselect other configs……}
3.创建表面
EGLint format;if (!eglGetConfigAttrib(display_, config,EGL_NATIVE_VISUAL_ID, &format)) {printf("eglGetConfigAttrib error %d\n", eglGetError());return;}ANativeWindow *window; // ANativeWindow 可以为 SurfaceView 中获取的 Surface 对象 ANativeWindow_setBuffersGeometry(window, 0, 0, format);EGLSurface surface = eglCreateWindowSurface(display, config, window, NULL);if (!surface_) {printf("eglCreateWindowSurface error %d\n", eglGetError());return;}
4.创建上下文
EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3,EGL_NONE};EGLContext context = eglCreateContext(display, config,EGL_NO_CONTEXT, contextAttribs);if (context == EGL_NO_CONTEXT) {printf("eglCreateContext create OpenGL ES 3 contextfailed\n");EGLintcontextAttribs2[] = {EGL_CONTEXT_CLIENT_VERSION, 2,EGL_NONE};context = eglCreateContext(display_,config, NULL, contextAttribs2);if (context_== EGL_NO_CONTEXT) {printf("eglCreateContextcreate OpenGL ES 2 context failed\n");return;}}eglMakeCurrent(display, surface, surface, context)) {
至此,本文介绍了几大主流平台上 OpenGL 原生开发的上下文环境创建与管理。除了 Windows 对 OpenGL 的支持相对较弱,需要依赖第三方库才能便捷的使用。其它平台都可以相对较快的建立 OpenGL 上下文环境,甚至有封装好的 View 帮助开发者快速接入。不过 OpenGL 制定的较早,已经不太适应现代 GPU 图形技术的发展了,遇到了一些问题:如现代 GPU 渲染管线发生了变化,不支持多线程操作,不支持异步处理等。
未来新一代的图形 API Vulkan 可能会取代 OpenGL。Vulkan 会大幅降低绘制命令开销,发送多线程性能,渲染性能更快。谷歌也已经明确 Android 会支持 Vulkan。微软的 DirectX12 背后理念与 Vulkan 也是一致的。苹果公司则在 2014 年推出了自行设计的 Metal API,目标也是替代 OpenGL,以适应现代 GPU 技术,其指令开销和渲染性能等也得到大幅提升。2018 年苹果已经宣布 OpenGL 和 OpenGL ES 相关 API 从 macOS 10.14 和 iOS 12 中废弃,今后不再维护。开发者今后迁移到新的图形 API 也是大势所趋,不过 OpenGL 作为主流图形 API 存在了超过 20 年,其最终消亡肯定还有很长的路。
评论