musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

描画表現の追求(OpenGL FBO)

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

フィルタ表現



はじめに

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

ソングビジュアライゼーション

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

キラキラエフェクト

リッチな表現

ちなみに、この辺りの話はこちらの記事で紹介しています。

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

キラキラエフェクトとカラフルフィルタ

よりリッチな表現へ



フィルタを掛けてレンダリング

フィルタはシーン(音符)の描画結果にさらに色を重ねるような表現です。 なので、フィルタは2回描画することで実現できます。

  1. シーンの描画
  2. 1の結果を使って描画


ちなみに、1回目の描画は画面に映し出されないのでオフスクリーンレンダリングと言います。1回目は画面ではなくテクスチャに描画し、そのシーンを描画したテクスチャにフィルタを掛けて画面に描画します。
なので、リアルタイムにテクスチャを作成しているとも言えます。

https://ics.media/entry/17120/images/180202_webgl2_mrt_offscreen_rendering__960.png
引用:サンプルで理解するWebGL 2.0 – Multiple Render Targetsによる動的なライティング表現 - ICS MEDIA



実装

AndroidOpenGLのFBOを使用して、画面にカラーフィルタを掛けてみます。

FBO実装の参考ページ
orangesignal.hatenadiary.org

処理の流れは

  1. 3種類(テクスチャ、FBO、レンダーバッファ)のバッファ領域確保
  2. 3種類のバッファの設定
  3. テクスチャへ描画 (1回目描画)
  4. フィルタを掛けて画面へ描画 (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版の開発があるので深追いはやめておきます。。(・ω・;)