这个算是安卓音视频学习中必学的一块内容,并没有偏离安卓音视频学习的主线。我也是现学现卖,这个系列的文章讲解的就是如何使用安卓端的OPENGLES,本系列文章参考自《OpenGL-ES 3.0 Programming Guide》,讲解的比较基础,望高手海涵。

《OpenGL-ES 3.0 Programming Guide》全书以C语言api实现,所以我们使用的是native层环境开发,并且使用的OPENGLES3.0版本,所以需要简单的CMake基础,C/C++基础。

EGL是一系列跨平台的接口,主要提供了以下三个功能:

  1. 可以和设备的本地窗口进行交互
  2. 查询surface可用的类型和配置参数
  3. 管理渲染源

EGL为OPENGLES创建了上下文环境,可以方便的进行各种操作,本质上仍然是一系列的接口。

检查错误

EGL中大多数函数执行成功后返回EGL_TRUE,失败返回EGL_FALSE。除了这些,EGL环境还记录了错误码来指示错误原因,开发者必须查询这些错误码:

1
EGLint eglGetError()

这个函数返回的是最近一次发生错误时的错误码,假如一直没有错误,则会返回EGL_SUCCESS。这个地方算是EGL设计的时候比较谨慎的一个设计,它没有为每个函数设置一个返回值,假如那样的话每个函数执行必须检查错误,包括开发者自认为没有错误的代码。采用这种查询错误码设计后,开发者可以不用检查一定不会错误的函数返回值,只在一些特定的代码执行后来查询错误码即可,减少工作量。 这里我在EGLManager中汇总了一些错误码的信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void logError() {
    EGLint error = eglGetError();
    switch (error) {
        case EGL_SUCCESS:
            SAKA_LOG_INFO("SUCCESS");
            break;
        case EGL_BAD_DISPLAY:
            SAKA_LOG_ERROR("the EGLDisplay is not invalidate\n");
            break;
        case EGL_NOT_INITIALIZED:
            SAKA_LOG_ERROR("the EGL can not initialize\n");
            break;
        case EGL_BAD_ATTRIBUTE:
            SAKA_LOG_ERROR("the attribList contains an undefined EGL attribute or an"
                                   "attribute value is unrecognized or out of range");
            break;
        case EGL_BAD_MATCH:
            SAKA_LOG_ERROR("check window and EGLCOnfig attributes to determine compatibility,"
                                   "or erify the EGLConfig supports rendering to a window");
            break;
        case EGL_BAD_CONFIG:
            SAKA_LOG_ERROR("verify that provided EGLConfig is valid");
            break;
        case EGL_BAD_NATIVE_WINDOW:
            SAKA_LOG_ERROR("verify that provided EGLNativeWindow is valid");
            break;
        case EGL_BAD_ALLOC:
            SAKA_LOG_ERROR("not enough resources available;handle and recover");
            break;
        default:
            SAKA_LOG_ERROR("unknown error");
            break;
    }
}

在后边的代码编写中将会用到这些错误码。

与本地窗口交互

EGL为本地窗口和OpenGL环境创建了一个胶水层,首先EGL需要打开和本地窗口的驾六通道,然后决定使用哪个surface来绘制图形。不同的操作系统提供的窗口操作语法不同,E所有GL提供了一个包装类–EGLDisplay–来包装系统的窗口接口,方便开发者调用。建立本地窗口连接总共需要两步:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
EGLint majorVersion;
EGLint minorVersion;

display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
    SAKA_LOG_ERROR("can not get egl display");
    return EGL_FALSE;
}
if (!eglInitialize(display, &majorVersion, &minorVersion)) {
    logError();
    return EGL_FALSE;
}

eglGetDisplay

1
EGLDisplay eglGetDisplay(EGLNativeDisplayType displayEd)

该函数打开了一个和EGL display的连接,其中的参数EGLNativeDisplayType用来指定连接类型,使用EGL_DEFAULT_DISPLAY来创建一个系统的默认连接,通常情况下这个一定会成功。

假如连接不成功,该函数将返回EGL_NO_DISPLAY,表明OPEGL不可用。

eglInitialize

1
EGLBoolean eglInitialize(EGLDisplay display,EGLint *majorVersion,EGLint *minorVersion)

EGLdisplay打开成功后,需要初始化EGL,上面的函数就是为刚才打开的display初始化EGL内部的数据,传入的参数将会写入EGL的主要版本号和次要版本号。返回值是一个boolean值,成功是EGLTRUE,错误时EGL_FALSE。假如失败可以查询错误码(logerror()):

1
2
EGL_BAD_DIAPLAY --display不可用
EGL_NOT_INItiALIZED --EGL不能初始化

配置EGL的surface

初始化EGL后,接下来就是选取开发者需要的类型和配置,EGL提供了两种方式:

  1. 查询所有的surface配置,找出合适的一个
  2. 提供一系列的要求参数给EGL来找出最合适的一个

通常情况下推荐使用第二种。以上两种方式都会范湖第一个EGLconfig,这个结构体包含了一个surface和他的所有特征值。开发者可以通过eglGetConfigAttrib函数来查询这些特征值对应的值。

查询所有支持的配置
1
EGLBooean eglGetConfigs(EGLDisplay display,EGLConfig *configs,EGLint maxReturnConfigs,EGLint *numConfigs)

这里有两种使用eglGetConfigs的方法:

第一种,直接传入configs参数为NULL,这样函数返回值为EGL_TRUE,并且设置numConfigs为EGL所有可用的EGLConfigs的数量,这样就可以根据numConfigs来初始化configs的大小,确保能分配到足够的内存来装载所有的EGLConfig。

第二种,直接创建一个eglConfig数组,将它作为参数传递给函数,设置maxReturnConfigs为你想要的配置数量,最后一个参数将会被函数设置为返回的配置数量。

查询配置的参数

选择好EGLCinfig后,可以查询配置中的任何属性的值,可能我们并不关心这些值,但我们需要知道查询这些值的方法:

1
EGLBoolean eglGetConfigAttrib(EGLDisplay display,EGLConfig config,EGLint attribute,EGLint *value)

display是刚才创建的EGLDisplay,config是上边选出的EGLConfig,attribute是我们要查询的属性,函数会把value设置为属性对应的值。返回值EGL_TRUE表示查询成功,EGL_FALSE表示查询错误,这是可以调用lgoerr()方法来查询错误信息,一般错误码是EGL_BAD_ATTRIBUTE,表示属性没有。

让EGL选取配置文件

上面介绍的方法是比较繁琐的,假如开发者已经确定一些属性,那就可以用这些属性来让EGL选取出最合适的配置,开发者指定的属性越多,最后返回的EGLConfig越准确。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
EGLBoolean eglChooseConfig( EGLDisplay display,
                            const EGLint *attribList,
                            EGLConfig *configs,
                            EGLint maxReturnConfigs,
                            EGLint *numConfigs)

display是刚才创建的display

map的形式,EGL_NONE结尾


```c
 EGLint attribList[] = {
            EGL_BUFFER_SIZE,32,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_ALPHA_SIZE,8,
            EGL_DEPTH_SIZE, 16,
            EGL_RENDERABLE_TYPE,4,
            EGL_NONE
    };

第三个参数是开发者提供的限制返回EGLConfig的数量,最后一个参数是函数设置的实际返回的数量。

函数执行成功则返回EGL_TRUE,执行失败则返回EGL_FALSE,这时候可以lgoerr()来查询错误码,一般是EGL_BAD_ATTRIBUTE。

假如返回的结果个数很多的话,是有一个排序的,不过按照上面的写的属性来选择EGLConig,最后选取第一个来使用没问题。

创建屏幕渲染

关键函数是:

1
2
3
4
EGLSurface gleCreateWindowSurface(  EGLDisplay display,
                                    EGLConfig config,
                                    EGLNativeWindowType window,
                                    const EGLint *attribList)

display和config都是前边介绍过的需要传入的参数。

window在安卓中是需要传入的一个参数,类型是ANativeWindow类型,这个window可以是java层的任何surface,包GLSurfaceView,TextureView或者SurfaceView。以TextureView的创建介绍一下:

给一个TextureView设置TextureView.SurfaceTextureListener后,在第一个回调方法中设置给jni一个surface作为参数:

1
2
3
4
5
//java层
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    window = new Surface(surface);
    OpenGLUtils.initEGL(window, width, height);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//native层

extern "C"
JNIEXPORT void JNICALL
Java_com_saka_ndk_OpenGLUtils_initEGL(JNIEnv *env, jclass type, jobject surface, jint w, jint h) {

    EglManager eglManager(ANativeWindow_fromSurface(env, surface));
    if (!eglManager.initEGL()) {
        SAKA_LOG_ERROR("error occured");
    }
    GLuint programObject;
    init(&programObject);
    draw(w, h, programObject);
    eglManager.swapBuffer();
}

EGLManager的构造函数:

1
2
3
4
EglManager::EglManager(ANativeWindow *sur) : nativeWindow(sur) {


}

attriblist假如设置为NULL,则会选择默认参数。

创建渲染上下文环境

到了这一步就比较简单了:

1
2
3
4
EGLContext eglCreateContext(    EGLDisplay display,
                                EGLConfig config,
                                EGLContext shareContext,
                                const EGLint *attribList)

前两个参数是我们前边讲解过如何获取的,第三个可以设置EGL_NO_CONTEXT,这样就不和其他EGL共享这个环境,最后一个参数是我们需要管理的OPENGL的版本:

1
2
3
const EGLint attrList[] = {
            EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE
    };

函数调用后我们需要检查错误码,来获取函数是否执行成功了。

绑定当前上下文环境
1
2
3
4
EGLBoolean eglMakeCurrent(  EGLDisplay display,
                            EGLSurface draw,
                            EGLSurface read,
                            EGLContext context)

EGL环境是一个双缓冲环境,包含一个前台区域和后台区域,每次绘制的时候先将后台区域绘制完成,再讲后台数据交换到前台,这就是双缓冲,所有第二个和第三个参数都是EGLSurface。为了在创建多个EGLContext的情况下都能是OPENGL正常工作,必须调用elgMakeCurrent方法来绑定这个环境,即使只有单个EGLContex,也必须调用这个方法。

走到这一步EGLContext环境就创建好了,接下来我们就可以直接使用OPENGL了。