大数跨境
0
0

实践解析 | 如何用 OpenGL 实现跨平台应用高效渲染

实践解析 | 如何用 OpenGL 实现跨平台应用高效渲染 拍乐云Pano
2021-05-17
0
导读: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, // 像素格式为RGBA    WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, // 支持硬件加速    WGL_COLOR_BITS_ARB, 32, // 颜色缓冲位深32    WGL_ALPHA_BITS_ARB, 8,  // alpha通道位深 8    WGL_DEPTH_BITS_ARB,24, // 深度缓冲位深 24    WGL_STENCIL_BITS_ARB, 8, // 模板缓冲位深 8    WGL_SAMPLE_BUFFERS_ARB, GL_TRUE,// 开启多重采样MSAA    WGL_SAMPLES_ARB, 4,    // 4倍多重采样MSAA    0}; // 以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, //多重采样buffer    NSOpenGLPFASamples, (NSOpenGLPixelFormatAttribute)4,  // 多重采样数    NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,  // OpenGL3.2    0};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 2    glContext=[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];}GLKView*glkView=[[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 的创建上下文环境主要有初始化、选择和设置合适的配置、创建表面、创建上下文四个步骤。

1. 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, // 基于窗口的surface    EGL_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年,其最终消亡肯定还有很长的路。


拍乐云成立于2019年,是国内第一家视频会议背景的实时互动通信云服务提供商,汇聚了一大批专注于音频、视频、白板、网络、AI等领域的资深技术专家。通过集成 Pano SDK,企业开发者即可在全球范围内快速实现互动小班、超级小班、双师大班、语音聊天室、视频社交、直播连麦、游戏语音、视频客服、远程医疗、办公协作等场景。

关注拍乐云 Pano 的公众号,我们将为大家分享更多的技术探索和实践经验,陪你一起成长充电。

【声明】内容源于网络
0
0
拍乐云Pano
杭州拍乐云科技有限公司旗下的拍乐云 Pano,是一家专业的实时音视频平台即服务(PaaS)提供商,专注于为用户提供高清晰度、稳定可靠、用户友好和低延迟的实时互动体验。
内容 93
粉丝 0
认证用户
拍乐云Pano 杭州拍乐云科技有限公司 杭州拍乐云科技有限公司旗下的拍乐云 Pano,是一家专业的实时音视频平台即服务(PaaS)提供商,专注于为用户提供高清晰度、稳定可靠、用户友好和低延迟的实时互动体验。
总阅读46
粉丝0
内容93