今回は描画ライブラリOpenGLの表現を高める話。
OpenGLの機能FBOを用いることでフィルタを掛けます。


はじめに
musicLineでは、コミュニティでユーザーが投稿した曲を再生できるようになっており、再生している曲のイメージを可視化できるソングビジュアライゼーションという機能があります。

そのソングビジュア機能ですが、ランキング等で上位に入った曲はキラキラのエフェクトを追加し、よりリッチな表現にしています。


ちなみに、この辺りの話はこちらの記事で紹介しています。
その中で、殿堂入りした曲はキラキラエフェクトを加えた後、さらにカラフルフィルタを掛けてよりリッチな表現を追求しています。


フィルタを掛けてレンダリング
フィルタはシーン(音符)の描画結果にさらに色を重ねるような表現です。 なので、フィルタは2回描画することで実現できます。
- シーンの描画
- 1の結果を使って描画
ちなみに、1回目の描画は画面に映し出されないのでオフスクリーンレンダリングと言います。1回目は画面ではなくテクスチャに描画し、そのシーンを描画したテクスチャにフィルタを掛けて画面に描画します。
なので、リアルタイムにテクスチャを作成しているとも言えます。

引用:サンプルで理解するWebGL 2.0 – Multiple Render Targetsによる動的なライティング表現 - ICS MEDIA
実装
AndroidでOpenGLのFBOを使用して、画面にカラーフィルタを掛けてみます。
FBO実装の参考ページ
orangesignal.hatenadiary.org
処理の流れは
- 3種類(テクスチャ、FBO、レンダーバッファ)のバッファ領域確保
- 3種類のバッファの設定
- テクスチャへ描画 (1回目描画)
- フィルタを掛けて画面へ描画 (2回目描画)
となります。
1. 3種類(テクスチャ、FBO、レンダーバッファ)のバッファ領域確保
// テクスチャ textureId = IntArray(1).also { GLES20.glGenTextures(1, it, 0) }.first() // FBO fboId = IntArray(1).also { GLES20.glGenFramebuffers(1, it, 0) }.first() // レンダーバッファ renderId = IntArray(1).also { GLES20.glGenRenderbuffers(1, it, 0) }.first()
まずOpen GLのバッファを使うことを宣言します。
ちなみに、使用後必要がなくなったバッファは解放しないとメモリリークになります。
GLES20.glDeleteTextures(1, arrayListOf(textureId).toIntArray(), 0) GLES20.glDeleteFramebuffers(1, arrayListOf(fboId).toIntArray(), 0) GLES20.glDeleteRenderbuffers(1, arrayListOf(renderId).toIntArray(), 0)
2. 3種類のバッファの設定
// FBOをバインド GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId) // レンダーバッファの幅と高さを指定します。 GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height) // フレームバッファのアタッチメントとしてレンダーバッファをアタッチします。 GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, renderId) // テクスチャの設定 GLES20.glActiveTexture(textureSlot) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, texFormat, width, height, 0, texFormat, GLES20.GL_UNSIGNED_BYTE, null) // FBOにテクスチャをアタッチ GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0) // FBO, Textureのバインドを解除 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
まず設定するFBOをバインドして、FBOに紐付けるレンダーバッファの設定をしています。
また、テクスチャも設定します。
3. テクスチャへ描画 (1回目描画)
// 使用するFBOの指定 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId) // 使用するテクスチャの指定 GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0) // シーンの描画 (オフスクリーンレンダリング) drawFunction() GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
使用するFBOとテクスチャを指定してから、シーンを描画することでテクスチャに描画します。
drawFunction()の箇所で通常行っている描画処理を行います。
4. フィルタを掛けて画面へ描画 (2回目描画)
// 情報を送信 val propertyCount = 2 // X, Y 座標 // テクスチャ頂点とUV座標 val texVtPoss = listOf( Point(-1f, 1f), // 左上 Point(1f, 1f), // 右上 Point(1f, -1f), // 右下 Point(-1f, -1f),// 左下 ) val vertexBuffer = toFloatBuffer(texVtPoss) val texUvPoss = listOf( Point(0f, 1f), // 左上 Point(1f, 1f), // 右上 Point(1f, 0f), // 右下 Point(0f, 0f) // 左下 ) val uvBuffer = toFloatBuffer(texUvPoss) // GLSL設定 GLES20.glUseProgram(shaderProgramId) // 機能有効 GLES20.glEnable(GLES20.GL_BLEND) GLES20.glEnable(GLES20.GL_TEXTURE) GLES20.glDisable(GLES20.GL_DEPTH_TEST) // 機能設定 GLES20.glActiveTexture(textureSlot) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA) // 変数有効 GLES20.glEnableVertexAttribArray(texVtPosLoc) GLES20.glEnableVertexAttribArray(texUvPosLoc) // 変数設定 // stride 格納されているデータの間隔を(Objectサイズ)で指定する. 0 を指定したときは, データは密に並んでいるとみなされてsize*typeで自動的に計算される GLES20.glVertexAttribPointer(texVtPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, vertexBuffer) GLES20.glVertexAttribPointer(texUvPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, uvBuffer) GLES20.glUniform1i(useTextureSlotLoc, 2) // GL_TEXTURE2 GLES20.glUniform1f(timeLoc, time * 0.001f) // 描画 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, texVtPoss.size) // 変数解除 GLES20.glDisableVertexAttribArray(texVtPosLoc) GLES20.glDisableVertexAttribArray(texUvPosLoc) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) // 機能解除 GLES20.glDisable(GLES20.GL_TEXTURE) GLES20.glDisable(GLES20.GL_BLEND)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) で1回目の描画で作成したテクスチャを指定しています。
リアルタイムに作成したテクスチャを使って新しくレンダリングします。
今回の例では画面全体がカラフルになるようなShaderを書いています。
// uniform val useTexSlotUni = "tex_id" val timeUni = "u_time" // attribute val vtPosAttr = "v_pos" val uvPosAttr = "v_uv" // varying private val uvPosVary = "f_uv" override val vertexCode = """ attribute vec2 $vtPosAttr; attribute vec2 $uvPosAttr; varying vec2 $uvPosVary; void main() { gl_Position = vec4($vtPosAttr, 0.0, 1.0); $uvPosVary = $uvPosAttr; } """.trimIndent() override val fragmentCode = """ precision mediump float; varying vec2 $uvPosVary; uniform sampler2D $useTexSlotUni; uniform float $timeUni; #define PI 3.14159265359 void main() { vec4 color = texture2D($useTexSlotUni, $uvPosVary); vec3 shift = vec3(100.0 * (pos - $timeUni), 1.0, 1.0); gl_FragColor = vec4(shift_col(color.rgb, shift), color.a); } """.trimIndent()
全コードを表示
class ColorfulShiftShader() { // region Property private val textureSlot = GLES20.GL_TEXTURE2 // テクスチャ使用用にGL_TEXTURE1は開けとく private val textureId: Int private val fboId: Int private val renderId: Int override val program = Program() // Location private val texVtPosLoc: Int // 頂点位置 private val texUvPosLoc: Int // UV位置 private val useTextureSlotLoc: Int // テクスチャスロットNo private val timeLoc: Int //時間 // endregion // region Initializer init { loadProgram() // Textureを作成 textureId = IntArray(1).also { GLES20.glGenTextures(1, it, 0) }.first() // FBO のバッファ領域確保 fboId = IntArray(1).also { GLES20.glGenFramebuffers(1, it, 0) }.first() // レンダーバッファ領域確保 renderId = IntArray(1).also { GLES20.glGenRenderbuffers(1, it, 0) }.first() setupFBO(textureSlot, textureId, fboId, renderId, GLES20.GL_RGB) // Location設定 texVtPosLoc = GLES20.glGetAttribLocation(shaderProgramId, program.vtPosAttr) texUvPosLoc = GLES20.glGetAttribLocation(shaderProgramId, program.uvPosAttr) useTextureSlotLoc = GLES20.glGetUniformLocation(shaderProgramId, program.useTexSlotUni) timeLoc = GLES20.glGetUniformLocation(shaderProgramId, program.timeUni) } private fun setupFBO(textureSlot: Int, textureId: Int, fboId: Int, renderId: Int, texFormat: Int = GLES20.GL_RGBA) { // FBOをバインド GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId) // レンダーバッファの幅と高さを指定します。 GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height) // フレームバッファのアタッチメントとしてレンダーバッファをアタッチします。 GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, renderId) // テクスチャの設定 GLES20.glActiveTexture(textureSlot) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, texFormat, width, height, 0, texFormat, GLES20.GL_UNSIGNED_BYTE, null) // FBOにテクスチャをアタッチ GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0) // FBO, Textureのバインドを解除 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) } // endregion // region Method fun draw(drawFunction: () -> Unit) { drawInFBO(drawFunction) // 情報を送信 val propertyCount = 2 // X, Y 座標 // テクスチャ頂点座標 val texVtPoss = listOf( Point(-1f, 1f), // 左上 Point(1f, 1f), // 右上 Point(1f, -1f), // 右下 Point(-1f, -1f),// 左下 ) val vertexBuffer = toFloatBuffer(texVtPoss) // テクスチャUV座標 val texUvPoss = listOf(// テクスチャのUV座標 Point(0f, 1f), // 左上 Point(1f, 1f), // 右上 Point(1f, 0f), // 右下 Point(0f, 0f) // 左下 ) val uvBuffer = toFloatBuffer(texUvPoss) // GLSL設定 GLES20.glUseProgram(shaderProgramId) // 機能有効 GLES20.glEnable(GLES20.GL_BLEND) GLES20.glEnable(GLES20.GL_TEXTURE) GLES20.glDisable(GLES20.GL_DEPTH_TEST) // 機能設定 GLES20.glActiveTexture(textureSlot) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA) // 変数有効 GLES20.glEnableVertexAttribArray(texVtPosLoc) GLES20.glEnableVertexAttribArray(texUvPosLoc) // 変数設定 // Ref:stride 格納されているデータの間隔を(Objectサイズ)で指定する. 0 を指定したときは, データは密に並んでいるとみなされてsize*typeで自動的に計算される GLES20.glVertexAttribPointer(texVtPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, vertexBuffer) GLES20.glVertexAttribPointer(texUvPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, uvBuffer) GLES20.glUniform1i(useTextureSlotLoc, 2) // GL_TEXTURE2 GLES20.glUniform1f(timeLoc, time * 0.001f) // 描画 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, texVtPoss.size) // 変数解除 GLES20.glDisableVertexAttribArray(texVtPosLoc) GLES20.glDisableVertexAttribArray(texUvPosLoc) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) // 機能解除 GLES20.glDisable(GLES20.GL_TEXTURE) GLES20.glDisable(GLES20.GL_BLEND) } // オフスクリーンレンダリング(テクスチャへ描画) private fun drawInFBO(drawFunction: () -> Unit) { // 機能設定 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId) GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0) drawFunction() GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) } // endregion // region InnerClass class Program : Shader.Program() { // uniform val useTexSlotUni = "tex_id" val timeUni = "u_time" // attribute val vtPosAttr = "v_pos" val uvPosAttr = "v_uv" // varying private val uvPosVary = "f_uv" override val vertexCode = """ attribute vec2 $vtPosAttr; attribute vec2 $uvPosAttr; varying vec2 $uvPosVary; void main() { gl_Position = vec4($vtPosAttr, 0.0, 1.0); $uvPosVary = $uvPosAttr; } """.trimIndent() override val fragmentCode = """ precision mediump float; varying vec2 $uvPosVary; uniform sampler2D $useTexSlotUni; uniform float $timeUni; #define PI 3.14159265359 void main() { vec4 color = texture2D($useTexSlotUni, $uvPosVary); vec3 shift = vec3(100.0 * (pos - $timeUni), 1.0, 1.0); gl_FragColor = vec4(shift_col(color.rgb, shift), color.a); } """.trimIndent() } // endregion }
おわりに
今回はOpenGLの機能FBOを用いてフィルタを掛けてみました。
もっとリッチな表現を目指したい気持ちもありますが、作曲の機能拡張やiOS版の開発があるので深追いはやめておきます。。(・ω・;)