[모두의 메모] OpenGL ES의 Texture - boostcampwm-2021/android05-boomerang GitHub Wiki

이제 그림을 그릴 시간입니다. Android에서 OpenGL ES 렌더링 과정, 특히 SurfaceTexture를 사용하여 이미지를 텍스처로 사용하는 과정에 대해 정리하겠습니다.

SurfaceTexture

Texture 초기화

우선 SurfaceTexture의 생성자부터 하나씩 확인해보도록 하겠습니다.

SurfaceTexture(texName: Int)

SurfaceTexture를 생성하기 위해서는 외부에서 OpenGL 텍스처를 생성하여 주입시켜야 합니다. texName에서 glGenTextures로 생성된 텍스처의 이름이 들어가야 합니다. GLES20.glGenTextures는 아래와 같이 만들 수 있습니다.

GLES20.glGenTextures(n: Int, textures: IntArray, offset: Int)
  • n: 생성할 텍스처의 수
  • textures: 생성된 텍스처가 저장될 배열. 빈 배열로 생성해두되, n 이상의 크기를 가지고 있어야 할 것입니다.
  • offset: 기본값

제가 만든 앱을 예로 들자면, 이미지는 배경으로 깔아줄 것이기 때문이 텍스처는 하나만 있으면 되니

val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)

이렇게 쉽게 생성할 수 있습니다. 하지만 생성만 한다고 해서 사용할 수 있지는 않습니다. 다음으로 어떤 대상을 텍스처를 쓸 것인지에 대해 바인딩하는 작업이 필요합니다. glBindTexture를 사용하여 위에서 생성한 텍스처에 어떤 종류의 텍스처가 될 것인지를 지정해야 합니다. 여기서 주의할 점은 Android의 SurfaceTexture 클래스에서 쓰는 텍스처는 일반적인 텍스처의 종류와는 별개라는 것입니다.

위의 glBindTexture 링크에서는 텍스처의 종류인 target 으로 GL_TEXTURE_2D, GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP 중 하나를 선택하라고 하지만 AOSP SurfaceTextureSurfaceTexture 페이지에서는 외부 GLES 텍스처(GL_TEXTURE_EXTERNAL_OES)를 사용하라고 명시되어 있습니다.

GL_OES_EGL_image_externalGL_TEXTURE_2D와 몇 가지 차이점이 있습니다. 우선 대부분의 텍스처 관련 함수들(예를 들어 glTexImage() 등)을 사용할 수 없습니다. 그렇다면 왜 사용하는 것일까요? 바로 렌더링 속도에서 이득을 얻을 수 있기 때문입니다. 다른 텍스처들과는 다르게, 외부 GLES 텍스처는 EGLImage를 텍스처로 바꾸는 방법을 사용합니다. 이 말은, 이미지를 텍스처에 할당하는 과정이 생략될 수 있다는 것입니다. 다른 텍스처 타겟을 사용한다면 glTexImage2D(...)등의 함수를 이용하여 이미지를 텍스처에 할당하는 과정이 필요하게 됩니다.

val texture = textures[0]
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)

glGenTextures(...)로 가져온 텍스처를 glBindTexture(...)texture로 사용하면 됩니다.

다음으로는 텍스처에 몇 가지 속성을 지정할 수 있습니다. 저는 텍스처 필터링(Texture Filtering)과 텍스처 래핑(Texture Wrapping) 두 가지 속성을 지정하였습니다.

const val textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES

GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)

GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_BORDER)
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_BORDER)

glTexParameteri(...) 함수를 통해 텍스처와 파라미터, 파라미터 값을 지정할 수 있습니다. 뒤에 붙은 i는 Int를 나타내며, f로 바꿔 Float 값을 사용할 수도 있습니다.

우선 위의 두 줄, GL_TEXTURE_MAG_FILTERGL_TEXTURE_MIN_FILTER는 각각 확대와 축소를 위한 필터입니다.

GL_LINEAR는 주변의 2*2 텍셀 값의 평균으로 하는데, 가까운 텍셀의 값에 가중치를 두는 방식을 사용합니다. 확대를 할 때 GL_NEAREST를 사용한다면 가장 가까운 값만을 가져오게 되므로 경계가 뚜렷해져 흔히 말하는 깨져보이는 현상이 발생하게 됩니다. 따라서 확대 필터는 GL_LINEAR 방식을 사용하였습니다.

GL_NEAREST는 OpenGL의 필터링 기본값으로, 목표 위치에 가장 가까운 텍셀(텍스처에서의 화소. 렌더링이 완료된 후 프레임버퍼에서의 화소는 픽셀이라고 함)의 값을 가지게 됩니다. 축소 필터 사용시에는 더 작아지기 때문에 깨져보이는 것에 대한 걱정이 없습니다. 따라서 원래의 색상을 그대로 사용하는 이 방식을 적용하였습니다.

다음 두 줄은 텍스처 래핑 파라미터를 지정합니다. 텍스처를 입힐 도형과 이미지의 크기가 맞지 않을 때 어떻게 처리할 것인지에 대한 속성입니다. 2차원 평면에서의 사각형에 텍스처를 입힐 때에는 쓰일 일이 크게 없을 것으로 생각됩니다. 주로 원의 끝부분, 또는 양 끝이 이어지는 형태에서 끝 부분 처리에 쓰일 것으로 생각됩니다.

GL_CLAMP_TO_BORDER를 사용하면 비어있는 부분에 대해 텍스처를 이용하여 처리하는 대신 따로 색을 입혀 검은 배경으로 두는 식의 처리를 할 수 있지만 OpenGL ES 3.2부터 사용 가능하기 때문에 GL_CLAMP_TO_BORDER로 설정하였습니다. 다만 이 부분은 실제 구현 결과에는 영향을 미치지 않았고, 이 파라미터 자체를 따로 설정하지 않아도 문제없이 작동합니다.

이제 OpenGL ES 텍스처가 생성되었습니다. SurfaceTexture의 생성자에 이 텍스처를 넣어 인스턴스가 생성되면, 이제 이미지를 가져올 차례입니다.

updateTexImage

surfaceTexture.updateTexImage()

이미지를 가져오는 것은 updateTexImage() 함수를 사용하면 됩니다. 그러면 SurfaceTexture에 Surface로 연결된 이미지 스트림 생산자에서 전달한 가장 최신의 이미지를 가져오게 되고, SurfaceTexture 생성시에 사용한 텍스처에 담기게 됩니다.

getTransformMatrix

private val transformMatrix = FloatArray(16)
surfaceTexture.getTransformMatrix(transformMatrix)

updateTexImage()으로 새로운 이미지를 가져올 때, 변환 행렬 또한 새롭게 가져옵니다. SurfaceTexture에 이미지 버퍼를 보낼 때, 방향이 잘못된 경우가 발생할 수 있습니다. 이 때 방향을 수정해서 SurfaceTexture에 보내기 보다는, 그대로 보내면서 고쳐야 할 정보에 대해서만 알려주고 렌더링 과정에서 고치도록 하는 것이 더 효율적이기 때문에 이런 방식을 사용한다고 합니다.

getTransformMatrix(...) 함수를 사용해서 이미지의 변환 행렬을 4*4 행렬에 저장합니다. 각 좌표를 (x, y, z, w) 의 동차좌표 형식으로 표현하기 때문에 변환 행렬을 사용하여 쉽게 변환할 수 있습니다. x, y, z는 3차원을 표현하고 w 값은 0일 때 방향, 1일 때 위치를 의미합니다. 이미지를 표현할 때에는 2차원이기 때문에 (x, y, 0, 1)으로 사용할 수 있습니다.

이미지를 텍스처로 가져오는 과정은 여기까지입니다. 다음 포스트에서는 렌더링 과정에 대해 작성하도록 하겠습니다.