今回は描画ライブラリOpenGLの表現を高める話。
OpenGLの機能FBOを用いることでフィルタを掛けます。
フィルタ表現
はじめに
musicLineでは、コミュニティでユーザーが投稿した曲を再生できるようになっており、再生している曲のイメージを可視化できるソングビジュアライゼーションという機能があります。
ソングビジュアライゼーション
そのソングビジュア機能ですが、ランキング等で上位に入った曲はキラキラのエフェクトを追加し、よりリッチな表現にしています。
キラキラエフェクト
リッチな表現
ちなみに、この辺りの話はこちらの記事で紹介しています。
その中で、殿堂入りした曲はキラキラエフェクトを加えた後、さらにカラフルフィルタを掛けてよりリッチな表現を追求しています。
キラキラエフェクトとカラフルフィルタ
よりリッチな表現へ
フィルタはシーン(音符)の描画結果にさらに色を重ねるような表現です。
なので、フィルタは2回描画することで実現できます。
- シーンの描画
- 1の結果を使って描画
ちなみに、1回目の描画は画面に映し出されないのでオフスクリーンレンダリングと言います。1回目は画面ではなくテクスチャに描画し、そのシーンを描画したテクスチャにフィルタを掛けて画面に描画します。
なので、リアルタイムにテクスチャを作成しているとも言えます。
![https://ics.media/entry/17120/images/180202_webgl2_mrt_offscreen_rendering__960.png](https://ics.media/entry/17120/images/180202_webgl2_mrt_offscreen_rendering__960.png)
引用:サンプルで理解する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()
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種類のバッファの設定
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)
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0)
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
まず設定するFBOをバインドして、FBOに紐付けるレンダーバッファの設定をしています。
また、テクスチャも設定します。
3. テクスチャへ描画 (1回目描画)
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
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)
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)
GLES20.glVertexAttribPointer(texVtPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, vertexBuffer)
GLES20.glVertexAttribPointer(texUvPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, uvBuffer)
GLES20.glUniform1i(useTextureSlotLoc, 2)
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を書いています。
val useTexSlotUni = "tex_id"
val timeUni = "u_time"
val vtPosAttr = "v_pos"
val uvPosAttr = "v_uv"
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() {
private val textureSlot = GLES20.GL_TEXTURE2
private val textureId: Int
private val fboId: Int
private val renderId: Int
override val program = Program()
private val texVtPosLoc: Int
private val texUvPosLoc: Int
private val useTextureSlotLoc: Int
private val timeLoc: Int
init {
loadProgram()
textureId = IntArray(1).also {
GLES20.glGenTextures(1, it, 0)
}.first()
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)
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) {
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)
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0)
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
fun draw(drawFunction: () -> Unit) {
drawInFBO(drawFunction)
val propertyCount = 2
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)
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)
GLES20.glVertexAttribPointer(texVtPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, vertexBuffer)
GLES20.glVertexAttribPointer(texUvPosLoc, propertyCount, GLES20.GL_FLOAT, false, 0, uvBuffer)
GLES20.glUniform1i(useTextureSlotLoc, 2)
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)
}
class Program : Shader.Program() {
val useTexSlotUni = "tex_id"
val timeUni = "u_time"
val vtPosAttr = "v_pos"
val uvPosAttr = "v_uv"
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()
}
}
おわりに
今回はOpenGLの機能FBOを用いてフィルタを掛けてみました。
もっとリッチな表現を目指したい気持ちもありますが、作曲の機能拡張やiOS版の開発があるので深追いはやめておきます。。(・ω・;)