Home [Android] 동영상에 그림 그리기 (3) - OpenGL ES, EGL
Post
Cancel

[Android] 동영상에 그림 그리기 (3) - OpenGL ES, EGL

OpenGL ES

그림을 그리기 위해서는, 어떻게 그림이 그려지는지 알 필요가 있습니다. 저도 이 과정에 대해 감히 모든 것을 알고 정리한다고 말씀드릴 수는 없을 것입니다. 제가 이 앱을 만들기 위해서 공부했던 범위에서 정리하도록 하겠습니다.

렌더링 파이프라인

OpenGL ES 3.0의 전체 렌더링 파이프라인은 크로노스 그룹 웹페이지에서 링크를 통해 찾아볼 수 있습니다. 저는 OpenGL ES를 사용하면서 아래 그림과 같이 최소한으로 간단하게 정리하여 생각하였습니다.

pipeline

이 중에서 사각형 상자 내의 Vertex ProcessingFragment Processing 과정에서 Shading 이라는 부분을 직접 프로그래밍이 가능합니다. Vertex Shader에서는 점의 위치를 결정하고, Fragment Shader에서는 색상을 결정합니다. 다만 Kotlin이 아닌 GLSL 이라는 별도의 언어를 사용해야 합니다.

물론 프로그래밍이 가능하지 않다는 것이 어떠한 조작도 불가능하다는 의미는 아닙니다. 제공되는 함수를 통해 기능을 조작 가능하지만, GLSL 등의 별도의 프로그래밍 언어를 사용하지 않는다는 것이고, 조작의 범위또한 제한적입니다.

버전

OpenGL ES는 여러 버전이 존재합니다. Android에서 특정 버전을 지원하는지 알기 위해서는 두 가지를 확인해야 합니다.

첫 번째로 API 레벨입니다. OpenGL ES 2.0은 API 8 이상에서 지원되고, OpenGL ES 3.0은 API 18 이상에서 지원됩니다. 이렇게만 보면 사실상 거의 모든 기기에서 OpenGL ES 3.0을 지원할 것으로 예상되고 실제로도 그렇기는 하지만 한 가지 더 확인해야 할 사항이 있습니다.

바로 GPU의 지원 여부입니다. GPU마다 지원하는 OpenGL ES의 버전이 다르기 때문에 기기에 따라 지원 여부가 달라질 수 있습니다. 물론 OpenGL ES 3.0이 2012년 8월에 공개된 것을 감안하면 거의 모든 기기가 지원하기는 하지만 Android 개발자 문서의 통계에 따르면 2020년 8월에도 OpenGL ES 2.0은 12%의 점유율을 가지고 있습니다.

다행히 하위호환이 되어 OpenGL ES 3.X를 지원하는 기기에서 OpenGL ES 2.0을 사용할 수 있습니다(OpenGL ES 1.X까지 지원하지는 않습니다. OpenGL 1.X와 2.0은 렌더링 파이프라인에 큰 차이가 있습니다. 1.X까지는 고정 파이프라인을 사용하다가 2.0부터 이를 제거하고 Shader 파이프라인, 즉 프래그래밍이 가능한 파이프라인으로 변경되었습니다).

그렇다면 OpenGL ES 2.0을 지원하는 기기에서 실행이 가능하도록 하고, OpenGL ES 3.X를 지원한다면 추가적인 기능을 사용하도록 구현하면 될 것입니다. 우선 OpenGL ES 2.0을 지원하는 기기에서만 설치될 수 있도록 해야합니다.

1
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

AndroidManifest.xml에 위 코드를 포함하면 됩니다. 그렇다면 OpenGL ES 3.X의 지원여부는 어떻게 알 수 있을까요? 이 부분은 아래 EGL 초기화 과정에 포함이 되어있습니다.

EGL

SurfaceTexture는 이미지 스트림 생성자에서 받은 이미지를 Texture로 사용하여 새로운 이미지를 그려내야 합니다. 그렇다면 어디에 그려야 할까요? 이 앱에서는 화면을 보여주기 위한 SurfaceView와, 동영상을 저장하기 위한 MediaCodec 두 가지가 필요합니다. OpenGL ES로 렌더링한 결과물은 어디에 그려지는지와, OpenGL ES로 렌더링을 하기 전 초기화 과정에 대해 알기 위해서는 EGL에 대한 이해가 필요합니다.

EGL은 크로노스 그룹의 API들과 여러 플랫폼의 호환을 위해 만들어졌습니다. OpenGL ES 뿐만이 아니라 OpenGL, OpenVG를 사용할 때도 쓰이며, 이러한 라이브러리들이 Android 외에도 다양한 OS와 프로그램에 쓰이기 때문에 이 둘을 이어주는 인터페이스가 필요했기 때문입니다.

EGL의 초기화 단계를 차례대로 살펴보겠습니다. 첫 번째 단계는 EGLDisplay 설정입니다.

EGLDisplay

1
2
3
4
private fun getEglDisplay() {
    eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
    if (eglDisplay === EGL14.EGL_NO_DISPLAY) throw Exception("EGLDisplay 가져오기 실패")
}

EGL14는 EGL 1.4를 의미합니다. EGL 1.5가 Android 10(API 29)부터 지원되지만 미만의 버전에서 호환성이 우려되어 API 17에 추가된 EGL 1.4를 사용하였습니다.

eglGetDisplay(displayId: Int) 함수를 통해 연결할 디스플레이를 지정하고, 하드웨어 사양을 가져옵니다. EGL14.EGL_DEFAULT_DISPLAY로 지정하면 OS에서 지정한 기본값을 넣어주게 됩니다.

eglInitialize

1
2
3
4
private fun eglInit() {
    val version = IntArray(2)
    if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 0)) throw Exception("EGL 초기화 실패")
}

다음으로는 eglInitialize(dpy: EGLDisplay, major: IntArray, majorOffset: Int, minor: IntArray, minorOffset: Int) 함수를 사용하여 EGLDisplay 연결을 초기화합니다. 파라미터가 많아서 어렵게 느껴질 수도 있지만 여기서 중요한 것은 eglDisplay 하나입니다. 위에서 선언한 eglDisplay 값을 그대로 넣어주면 됩니다.

version은 함수 실행 결과 후 version에 EGL 버전 값이 들어가게 됩니다. 하지만 이후에도 EGL 1.4를 사용할 것이기 때문에 이후에 version을 사용하지는 않을 것입니다. 실행 결과는 Boolean 값으로 반환되며 false 반환시 초기화 실패입니다.

eglBind

1
2
3
private fun eglBind() {
    if (!EGL14.eglBindAPI(EGL14.EGL_OPENGL_ES_API)) throw Exception("EGL 렌더링 API 설정 실패")
}

이후 eglBindAPI(api: Int) 함수에서 OpenGL ES를 사용할 것을 선언합니다.

EGLConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private fun getEglConfig(version: Int) {
    var renderableType = EGL14.EGL_OPENGL_ES2_BIT
    if (version >= 3) {
        renderableType = renderableType or EGLExt.EGL_OPENGL_ES3_BIT_KHR
    }

    val attribList = intArrayOf(
        EGL14.EGL_RED_SIZE, 8,
        EGL14.EGL_GREEN_SIZE, 8,
        EGL14.EGL_BLUE_SIZE, 8,
        EGL14.EGL_ALPHA_SIZE, 8,
        EGL14.EGL_RENDERABLE_TYPE, renderableType,
        EGL_RECORDABLE_ANDROID, 1,
        EGL14.EGL_NONE
    )

    val configs = arrayOfNulls<EGLConfig>(1)
    val numConfigs = IntArray(1)
    if (!EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, 1, numConfigs, 0)) {
        Log.d(TAG, "$version EGLConfig 실패")
    }
    eglConfig = configs[0]
}

이제 EGLConfig를 가져와야 합니다. 다른 함수들과는 다른 특이한 작동방식을 쓰는데, 위의 코드에서 attribList에 사용하기 원하는 속성과 값을 Array에 순서대로 넣어줍니다. 렌더링 방식은 일반적인 RGBA_8888을 사용할 것입니다.

EGL14.EGL_RENDERABLE_TYPE은 비스마스킹 방식으로 값을 지정해주어야 합니다. 각 값들이 1, 2, 4, 8 등으로 다른 비트 값을 가지고 있기 때문에 or 연산을 하면 됩니다. 위의 코드에서 EGL14.EGL_OPENGL_ES2_BIT은 4이고, EGLExt.EGL_OPENGL_ES3_BIT_KHR은 64입니다. OpenGL ES는 하위호환이 되기 때문에 OpenGL ES 2.0까지만 지원하는 기기에서는 or 연산으로 더하는 과정을 빼고, OpenGL ES 3.0을 지원한다면 두 값을 or 연산으로 더한 값을 사용하면 됩니다.

위의 getEglConfig(version: Int) 함수에서 getEglConfig(3)을 먼저 시도하여 성공하면 두 버전 모두를 사용하는 EGLConfig를 반환받고, 실패하면 getEglConfig(2)로 OpenGL ES 2.0만을 사용하는 EGLConfig를 반환받도록 코드를 작성하였습니다.

EGL_RECORDABLE_ANDROID는 별도의 상수를 지정하였습니다. 12610(0x3142) 값을 가지고 있습니다. 이 상수는 EGLExt.EGL_RECORDABLE_ANDROID로 이미 지정이 되어있긴 하지만, API 26에 추가되었기 때문에 Min API 21이었던 제 앱에서는 별도로 값을 입력하였습니다.

attribList 배열의 마지막 EGL14.EGL_NONE 값은 배열의 종료를 나타내는 값입니다.

함수 실행 성공시에 configs에 속성에 맞는 EGLConfig가 반환됩니다.

이외에도 속성이 많기 때문에 크로노스 그룹의 eglChooseConfig 문서와 함께 사용할 수 있는 값들에 대해 Android의 EGL14 페이지를 함께 참고해 사용 가능한 속성들을 찾으시는 것을 추천드립니다. 만약 속성들 중 입력하지 않은 값이 있다면 eglChooseConfig 링크에 적혀있는 기본값이 적용됩니다.

EGLContext

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
private fun getEglContext() {
    getEglConfig(3)
    if (eglConfig != null) {
        val gl3Setup = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE)
        val context = EGL14.eglCreateContext(
            eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT,
            gl3Setup, 0
        )
        if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
            Log.d(TAG, "GLES3 Config")
            eglContext = context
            glVersion = 3
        }
    } else {
        getEglConfig(2)
        val gl2Setup = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
        val context = EGL14.eglCreateContext(
            eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT,
            gl2Setup, 0
        )
        Log.d(TAG, "GLES2 Config")
        eglContext = context
        glVersion = 2
    }
}

마지막으로 EGLContext를 생성하여야 합니다. EGL14.eglCreateContext(dpy: EGLDisplay, config: EGLConfig, shareContext: EGLContext, attribList: IntArray, offset: Int) 함수로 생성하면 되는데, 앞에서 생성한 EGLDisplay와 EGLConfig가 들어가고, shareContext는 데이터를 공유할 또다른 EGLContext를 적는 파라미터입니다. 이 코드에서는 해당하지 않았기 때문에 EGL14.EGL_NO_CONTEXT 값을 지정하였습니다.

그 다음으로 들어가는 gl3Setup, gl2Setup은 eglBindAPI(api: Int)로 앞에서 선언했던 OpenGL ES의 버전을 선언하는 배열입니다. EGLConfig에서 attribList를 선언한 방법과 같이 속성과 값을 순서대로 넣고, EGL14.EGL_NONE 으로 배열의 끝을 알리는 방식입니다. EGL14.EGL_CONTEXT_CLIENT_VERSION 속성의 값을 3으로 설정하여 OpenGL ES 3.0이 가능한지 확인하고, 성공시 그대로 EGLContext를 가져와 사용합니다. 실패시 OpenGL ES 2.0을 같은 방식으로 시도합니다.

실제로 사용시에는 OpenGL ES 2.0의 코드 위주로 사용하고, OpenGL ES 3.0을 사용하였을 때 성능 향상이 있을 경우에만 버전 확인 후 사용하고, 그 경우에도 OpenGL ES 2.0의 코드만으로도 작성할 수 있는 방법을 함께 구현하여 버전에 따라 사용하도록 하였습니다.

이렇게 해서 EGL 초기화 과정을 마쳤고, OpenGL ES의 사용 가능한 버전을 런타임으로 확인하여 설정할 수 있었습니다. 다음 포스팅에서는 OpenGL ES를 사용하여 렌더링 과정에서 어떻게 동영상의 각 프레임 이미지를 표시하기 위해 텍스처를 초기화하고 이미지를 텍스처로 사용하는 법에 대해 작성하도록 하겠습니다.

This post is licensed under CC BY 4.0 by the author.

[Android] 동영상에 그림 그리기 (2) - Surface

[Android] 동영상에 그림 그리기 (4) - OpenGL ES의 Texture