まえがき
株式会社スペースリー Androidエンジニアのふかまちです。 普段は弊社サービスのツールであるAndroidアプリ「Spacely Photo Task」を開発しています。 現在、後述するパノラマ画像の360°Viewerをキャプチャーした動画を生成する開発に取り組んでおり、 本記事では開発で得た知見を元に、GLSurfaceViewの活用方法を一部ご紹介します。
Android x OpenGLならGLSurfaceView
GLSurfaceViewとは、OpenGL ESをサポートしている、Androidの3D描画を行うためのViewです。描画にはGPUを活用していることでメインスレッドの負荷を軽減でき、高速なフレームレートでの描画が可能だという特徴があります。 弊社では、パノラマ画像の360°Viewerで使用しています。
(参考)GLSurfaceViewを使ったパノラマ画像の360°Viewer
CustomRendererでのbitmap生成
今回360°Viewerを動画化するにあたって、以下のステップを設定しました。
- viewerの表示領域をbitmapとしてフレーム単位で取得
- 取得したbitmapのlistを繋ぎ合わせて、videoへ変換
本記事はステップ1のbitmapsをどう作ったかという部分になります。
GLSurfaceViewクラスはRendererを持っています。GLSurfaceViewは通常、このInterfaceを継承したclassを作成し、setRendererを呼び出してRendererをGLSurfaceViewに渡します。 Rendererにより、以下の制御が可能です。
- onDrawFrame(gl: GL10):現在のフレーム描画。フレーム毎に呼ばれる。
- onSurfaceChanged(gl: GL10, width: Int, height: Int):GLSurfaceViewの表示サイズが更新されたら呼ばれる。
- onSurfaceCreated(gl: GL10, config: EGLConfig):GLSurfaceView作成時に呼ばれる。
GL10を活用できそうなので、以下のように書きました。
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) { super.onSurfaceCreated(gl, config) this.gl = gl } override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) { super.onSurfaceChanged(gl, width, height) this.width = width this.height = height val buffer = ByteBuffer.allocateDirect(width * height * 4) buffer.order(ByteOrder.nativeOrder()) this.buffer = buffer.asIntBuffer() } override fun onDrawFrame(gl: GL10) { super.onDrawFrame(gl) createBitmap() } private fun createBitmap() { gl?.let { val intArray = IntArray(width * height) buffer.position(0) it.glReadPixels(0, 0, width, height, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, buffer) buffer.get(intArray) val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) buffer.position(0) bitmap.copyPixelsFromBuffer(buffer) buffer.clear() listener.setBitmap(bitmap) } }
GL10を保持させて、シンプルにbitmapを生成させることができました。 結果は以下のとおりです。
生じた課題と対処法
生成物から、以下3つの課題を考えました。
画像が上下反転している
GLSurfaceViewからbitmapを取得すると、下から上に向かってpixelを取得するので、そのままだと上下反転になるようです。Matrixを使ってbitmapを変換させる必要がありました。
val matrix = Matrix() matrix.setScale(1f, -1f) //Y軸を反転させる val flippedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) //持っているbitmapを変換する
縦横比がうまく反映されていない
扱うGL10に対して適切に高さと横幅を指定しないと、正方形になってしまうようです。glViewPortをsetする必要がありました。
gl.glViewport(0, 0, width, height)
フレームレートを制限できていない
今のままだと過剰にbitmapを生成してメモリを圧迫しかねないので、30fps~45fps程度に制限する必要がありました。
val currentTime = System.currentTimeMillis() val elapsedTime = currentTime - lastCalledPreviewTime if (elapsedTime >= 1000 / fps) { // 1000msあたり何回呼ぶかを制限 lastCalledPreviewTime = currentTime //do something }
修正
上記解消案を反映した結果が、以下のとおりです。
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) { super.onSurfaceCreated(gl, config) this.gl = gl } override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) { super.onSurfaceChanged(gl, width, height) gl.glViewport(0, 0, width, height) //表示領域 this.width = width this.height = height val buffer = ByteBuffer.allocateDirect(width * height * 4) buffer.order(ByteOrder.nativeOrder()) this.buffer = buffer.asIntBuffer() } override fun onDrawFrame(gl: GL10) { super.onDrawFrame(gl) val currentTime = System.currentTimeMillis() val elapsedTime = currentTime - lastCalledPreviewTime if (elapsedTime >= 1000 / fps) { // フレームレート制限 lastCalledPreviewTime = currentTime createBitmap() } } private fun createBitmap() { gl?.let { val intArray = IntArray(width * height) buffer.position(0) it.glReadPixels(0, 0, width, height, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, buffer) buffer.get(intArray) val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) buffer.position(0) bitmap.copyPixelsFromBuffer(buffer) buffer.clear() val matrix = Matrix() matrix.setScale(1f, -1f) //上下反転 val flippedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) listener.setBitmap(flippedBitmap) } }
期待した結果を得ることができました。
最後に
spacelyでは一緒に働いてくださる方を大募集中です。