まえがき
株式会社スペースリー 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では一緒に働いてくださる方を大募集中です。


