이제 그림을 그릴 시간입니다. Android에서 OpenGL ES 렌더링 과정, 특히 SurfaceTexture를 사용하여 이미지를 텍스처로 사용하는 과정에 대해 정리하겠습니다.
SurfaceTexture
Texture 초기화
지난 글에서 onFrameAvailableListener
를 통해 새로운 이미지 프레임을 받은 것을 알 수 있다고 하였습니다. 우선 SurfaceTexture의 생성자부터 하나씩 확인해보도록 하겠습니다.
1
SurfaceTexture(texName: Int)
SurfaceTexture를 생성하기 위해서는 외부에서 OpenGL 텍스처를 생성하여 주입시켜야 합니다. texName에서 glGenTextures로 생성된 텍스처의 이름이 들어가야 합니다. GLES20.glGenTextures는 아래와 같이 만들 수 있습니다.
1
GLES20.glGenTextures(n: Int, textures: IntArray, offset: Int)
- n: 생성할 텍스처의 수
- textures: 생성된 텍스처가 저장될 배열. 빈 배열로 생성해두되, n 이상의 크기를 가지고 있어야 할 것입니다.
- offset: 기본값
제가 만든 앱을 예로 들자면, 이미지는 배경으로 깔아줄 것이기 때문이 텍스처는 하나만 있으면 되니
1
2
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 SurfaceTexture와 SurfaceTexture 페이지에서는 외부 GLES 텍스처(GL_TEXTURE_EXTERNAL_OES
)를 사용하라고 명시되어 있습니다.
GL_OES_EGL_image_external은 GL_TEXTURE_2D
와 몇 가지 차이점이 있습니다. 우선 대부분의 텍스처 관련 함수들(예를 들어 glTexImage()
등)을 사용할 수 없습니다. 그렇다면 왜 사용하는 것일까요? 바로 렌더링 속도에서 이득을 얻을 수 있기 때문입니다. 다른 텍스처들과는 다르게, 외부 GLES 텍스처는 EGLImage
를 텍스처로 바꾸는 방법을 사용합니다. 이 말은, 이미지를 텍스처에 할당하는 과정이 생략될 수 있다는 것입니다. 다른 텍스처 타겟을 사용한다면 glTexImage2D(...)
등의 함수를 이용하여 이미지를 텍스처에 할당하는 과정이 필요하게 됩니다.
1
2
val texture = textures[0]
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)
glGenTextures(...)
로 가져온 텍스처를 glBindTexture(...)
의 texture
로 사용하면 됩니다.
다음으로는 텍스처에 몇 가지 속성을 지정할 수 있습니다. 저는 텍스처 필터링(Texture Filtering)과 텍스처 래핑(Texture Wrapping) 두 가지 속성을 지정하였습니다.
1
2
3
4
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)
glTexParameteri(…) 함수를 통해 텍스처와 파라미터, 파라미터 값을 지정할 수 있습니다. 뒤에 붙은 i
는 Int를 나타내며, f
로 바꿔 Float 값을 사용할 수도 있습니다.
우선 위의 두 줄, GL_TEXTURE_MAG_FILTER
와 GL_TEXTURE_MIN_FILTER
는 각각 확대와 축소를 위한 필터입니다.
GL_LINEAR
는 주변의 2*2 텍셀 값의 평균으로 하는데, 가까운 텍셀의 값에 가중치를 두는 방식을 사용합니다. 확대를 할 때 GL_NEAREST
를 사용한다면 가장 가까운 값만을 가져오게 되므로 경계가 뚜렷해져 흔히 말하는 깨져보이는 현상이 발생하게 됩니다. 따라서 확대 필터는 GL_LINEAR
방식을 사용하였습니다.
GL_NEAREST
는 OpenGL의 필터링 기본값으로, 목표 위치에 가장 가까운 텍셀(텍스처에서의 화소. 렌더링이 완료된 후 프레임버퍼에서의 화소는 픽셀이라고 함)의 값을 가지게 됩니다. 축소 필터 사용시에는 더 작아지기 때문에 깨져보이는 것에 대한 걱정이 없습니다. 따라서 원래의 색상을 그대로 사용하는 이 방식을 적용하였습니다.
다음 두 줄은 텍스처 래핑 파라미터를 지정합니다. 텍스처를 입힐 도형과 이미지의 크기가 맞지 않을 때 어떻게 처리할 것인지에 대한 속성입니다. 2차원 평면에서의 사각형에 텍스처를 입힐 때에는 쓰일 일이 크게 없을 것으로 생각됩니다. 주로 원의 끝부분, 또는 양 끝이 이어지는 형태에서 끝 부분 처리에 쓰일 것으로 생각됩니다.
이제 OpenGL ES 텍스처가 생성되었습니다. SurfaceTexture의 생성자에 이 텍스처를 넣어 인스턴스가 생성되면, 이제 이미지를 가져올 차례입니다.
updateTexImage
1
surfaceTexture.updateTexImage()
이미지를 가져오는 것은 updateTexImage()
함수를 사용하면 됩니다. 그러면 SurfaceTexture에 Surface로 연결된 이미지 스트림 생산자에서 전달한 가장 최신의 이미지를 가져오게 되고, SurfaceTexture 생성시에 사용한 텍스처에 담기게 됩니다.
getTransformMatrix
1
2
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)으로 사용할 수 있습니다.
이미지를 텍스처로 가져오는 과정은 여기까지입니다. 다음 포스트에서는 렌더링 과정에 대해 작성하도록 하겠습니다.