Android で OpenGL ES 2.0 事始め FBO 編 〜スーパーサンプル〜
前回のエントリから半年近く空いてしまいましたが2013年初のエントリとして「Android で OpenGL ES 2.0 事始め」の続編として、今回は Android の OpenGL ES 2.0 ネタではなぜか極端に紹介が少ないフレームバッファオブジェクト (FBO) を使ってオフスクリーンレンダリングをしてみたいと思います。
FBO 自体の解説についてはググってねということにしますが、OpenGL 1.x/OpenGL ES 1.x 系のサイトも結構引っかかるかと思うので、ちょっと区別できるようにフォローしておくと、
- FBO は OpenGL ES 1.x では拡張サポート。
- OpenGL ES 2.0 では標準で FBO 使用可能。
- iOS で OpenGL ES 2.0 を使用する場合は、FBO の使用が必須。
となっています。Android で OpenGL ES 2.0 を使用する場合は、FBO の使用は必須ではないですが、デフォルトのフレームバッファへ直接描画を続けているとチラつきが発生してしまいますし、どの GPU ガイドにも FBO 使えと書いてあるので、実践的なアプリでは事実上必須となるはずです。
あとテクスチャサイズについてもフォローしておくと、
となっています。
さて、今回のソースは、
- FBO 管理クラス
- Shader 管理クラス
- FBO を使用する Renderer
- OpenGL ES 2.0 ユーティリティ
でポイントのみの差分ソースです。(※抜粋している為コンパイル通らないとかあれば前回、前々回など参照してみて下さい)
それではご覧下さい。
FBO 管理クラス
毎回書くのが面倒なので管理クラスにしてみました。後々この FBO のテクスチャへ描画して使うことになります。ちなみにテクスチャへの描画が終わったからと言ってフレームバッファとレンダーバッファを削除すると紐付いていたテクスチャを入力として使用した場合に正しく描画できない GPU があるので、このクラスではフレームバッファ、レンダーバッファ、テクスチャの全部を保持するか全部破棄するかにしています。また、FBO テクスチャのフォーマットは GL_RGBA しか許容しない GPU がありますが今のところ特に困らないので GL_RGBA 固定としています。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import android.annotation.TargetApi; import android.opengl.GLES20; import android.os.Build; /** * オフスクリーン描画用の OpenGL ES 2.0 のフレームバッファオブジェクト管理クラスを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.FROYO) public class GLES20FramebufferObject { /** * 幅を保持します。 */ private int mWidth; /** * 高さを保持します。 */ private int mHeight; /** * フレームバッファ識別子を保持します。 */ private int mFramebufferName; /** * レンダーバッファ識別子を保持します。 */ private int mRenderbufferName; /** * テクスチャ識別子を保持します。 */ private int mTexName; ////////////////////////////////////////////////////////////////////////// // セッター / ゲッター /** * 幅を返します。 * * @return 幅 */ public int getWidth() { return mWidth; } /** * 高さを返します。 * * @return 高さ */ public int getHeight() { return mHeight; } /** * テクスチャ識別子を返します。 * * @return テクスチャ識別子 */ public int getTexName() { return mTexName; } ////////////////////////////////////////////////////////////////////////// // パブリック メソッド /** * 指定された幅と高さでフレームバッファオブジェクト (FBO) を構成します。<p> * 既にフレームバッファオブジェクト (FBO) が構成されている場合は、 * 現在のフレームバッファオブジェクト (FBO) を削除して新しいフレームバッファオブジェクト (FBO) を構成します。 * * @param width 幅 * @param height 高さ * @throws RuntimeException フレームバッファの構成に失敗した場合。 */ public void setup(final int width, final int height) { // 現在のフレームバッファオブジェクトを削除します。 release(); try { mWidth = width; mHeight = height; final int[] args = new int[1]; // フレームバッファ識別子を生成します。 GLES20.glGenFramebuffers(args.length, args, 0); mFramebufferName = args[0]; // フレームバッファ識別子に対応したフレームバッファオブジェクトを生成します。 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferName); // レンダーバッファ識別子を生成します。 GLES20.glGenRenderbuffers(args.length, args, 0); mRenderbufferName = args[0]; // レンダーバッファ識別子に対応したレンダーバッファオブジェクトを生成します。 GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderbufferName); // レンダーバッファの幅と高さを指定します。 GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height); // フレームバッファのアタッチメントとしてレンダーバッファをアタッチします。 GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mRenderbufferName); // Offscreen position framebuffer texture target GLES20.glGenTextures(args.length, args, 0); mTexName = args[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexName); GLES20Utils.setupSampler(GLES20.GL_TEXTURE_2D, GLES20.GL_LINEAR, GLES20.GL_NEAREST); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); // フレームバッファのアタッチメントとして 2D テクスチャをアタッチします。 GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mTexName, 0); // フレームバッファが完全かどうかチェックします。 final int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { throw new RuntimeException("Failed to initialize framebuffer object " + status); } } catch (final RuntimeException e) { release(); throw e; } } /** * クリーンアップを行います。 */ public void release() { GLES20.glDeleteTextures(1, new int[]{ mTexName }, 0); GLES20.glDeleteRenderbuffers(1, new int[]{ mRenderbufferName }, 0); GLES20.glDeleteFramebuffers(1, new int[]{ mFramebufferName }, 0); mTexName = 0; mRenderbufferName = 0; mFramebufferName = 0; } /** * このフレームバッファオブジェクトをバインドして有効にします。 */ public void enable() { GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferName); } }
Shader 管理クラス
シェーダープログラムのところは最近突っ込み受けましたし、こちらも glsl 変わるたびに毎回書くのは面倒なのでクラスにしてみました。さりげなく Vertex Buffer Object (VBO) とかのベストプラクティスも使ってますが、所詮は 2D イメージ描画用なので気持ちの問題かもしれません。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.util.HashMap; import android.annotation.TargetApi; import android.opengl.GLES20; import android.os.Build; /** * OpenGL ES 2.0 向けのシェーダーオブジェクト管理クラスを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.GINGERBREAD) public class GLES20Shader { /** * デフォルトのポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコードです。 */ protected static final String DEFAULT_VERTEX_SHADER = "attribute vec4 aPosition;\n" + "attribute vec4 aTextureCoord;\n" + "varying highp vec2 vTextureCoord;\n" + "void main() {\n" + "gl_Position = aPosition;\n" + "vTextureCoord = aTextureCoord.xy;\n" + "}\n"; /** * デフォルトの色描画用のピクセル/フラグメントシェーダのソースコードです。 */ protected static final String DEFAULT_FRAGMENT_SHADER = "precision mediump float;\n" + // 演算精度を指定します。 "varying highp vec2 vTextureCoord;\n" + "uniform lowp sampler2D sTexture;\n" + "void main() {\n" + "gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + "}\n"; /** * 頂点データとテクスチャ座標 (UV マッピング) の構造体配列形式データです。 */ private static final float[] VERTICES_DATA = new float[] { // X, Y, Z, U, V -1f, 1f, 0f, 0f, 1f, // 左上 1f, 1f, 0f, 1f, 1f, // 右上 -1f, -1f, 0f, 0f, 0f, // 左下 1f, -1f, 0f, 1f, 0f // 右下 }; private static final int FLOAT_SIZE_BYTES = 4; protected static final int VERTICES_DATA_POS_SIZE = 3; protected static final int VERTICES_DATA_UV_SIZE = 2; protected static final int VERTICES_DATA_STRIDE_BYTES = (VERTICES_DATA_POS_SIZE + VERTICES_DATA_UV_SIZE) * FLOAT_SIZE_BYTES; protected static final int VERTICES_DATA_POS_OFFSET = 0 * FLOAT_SIZE_BYTES; protected static final int VERTICES_DATA_UV_OFFSET = VERTICES_DATA_POS_OFFSET + VERTICES_DATA_POS_SIZE * FLOAT_SIZE_BYTES; ////////////////////////////////////////////////////////////////////////// /** * 頂点シェーダーのソースコードを保持します。 */ private final String mVertexShaderSource; /** * フラグメントシェーダーのソースコードを保持します。 */ private final String mFragmentShaderSource; /** * プログラム識別子を保持します。 */ private int mProgram; /** * 頂点シェーダーの識別子を保持します。 */ private int mVertexShader; /** * フラグメントシェーダーの識別子を保持します。 */ private int mFragmentShader; /** * 頂点バッファオブジェクト名を保持します。 */ private int mVertexBufferName; /** * 変数名とハンドル識別子のマッピングを保持します。 */ private final HashMap<String, Integer> mHandleMap = new HashMap<String, Integer>(); ////////////////////////////////////////////////////////////////////////// // コンストラクタ /** * デフォルトコンストラクタです。 */ public GLES20Shader() { this(DEFAULT_VERTEX_SHADER, DEFAULT_FRAGMENT_SHADER); } /** * シェーダーのソースコードを指定してこのクラスのインスタンスを構築するコンストラクタです。 * * @param vertexShaderSource ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード * @param fragmentShaderSource 色描画用のピクセル/フラグメントシェーダのソースコード */ public GLES20Shader(final String vertexShaderSource, final String fragmentShaderSource) { mVertexShaderSource = vertexShaderSource; mFragmentShaderSource = fragmentShaderSource; } ////////////////////////////////////////////////////////////////////////// /** * 指定された GLSL ソースコードをコンパイルしてプログラムオブジェクトを構成します。 */ public void setup() { release(); mVertexShader = GLES20Utils.loadShader(GLES20.GL_VERTEX_SHADER, mVertexShaderSource); mFragmentShader = GLES20Utils.loadShader(GLES20.GL_FRAGMENT_SHADER, mFragmentShaderSource); mProgram = GLES20Utils.createProgram(mVertexShader, mFragmentShader); mVertexBufferName = GLES20Utils.createBuffer(VERTICES_DATA); } /** * フレームサイズを指定します。 * * @param width フレームの幅 * @param height フレームの高さ */ public void setFrameSize(final int width, final int height) { } /** * このシェーダーオブジェクトの構成を破棄します。 */ public void release() { GLES20.glDeleteProgram(mProgram); GLES20.glDeleteShader(mVertexShader); GLES20.glDeleteShader(mFragmentShader); GLES20.glDeleteBuffers(1, new int[]{ mVertexBufferName }, 0); mProgram = 0; mVertexShader = 0; mFragmentShader = 0; mVertexBufferName = 0; mHandleMap.clear(); } /** * 指定されたテクスチャ識別子を入力データとして描画します。 * * @param texName テクスチャ識別子 */ public void draw(final int texName) { useProgram(); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBufferName); GLES20.glEnableVertexAttribArray(getHandle("aPosition")); GLES20.glVertexAttribPointer(getHandle("aPosition"), VERTICES_DATA_POS_SIZE, GLES20.GL_FLOAT, false, VERTICES_DATA_STRIDE_BYTES, VERTICES_DATA_POS_OFFSET); GLES20.glEnableVertexAttribArray(getHandle("aTextureCoord")); GLES20.glVertexAttribPointer(getHandle("aTextureCoord"), VERTICES_DATA_UV_SIZE, GLES20.GL_FLOAT, false, VERTICES_DATA_STRIDE_BYTES, VERTICES_DATA_UV_OFFSET); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texName); GLES20.glUniform1i(getHandle("sTexture"), 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisableVertexAttribArray(getHandle("aPosition")); GLES20.glDisableVertexAttribArray(getHandle("aTextureCoord")); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } ////////////////////////////////////////////////////////////////////////// /** * プログラムを有効にします。 */ protected final void useProgram() { GLES20.glUseProgram(mProgram); } /** * 頂点バッファオブジェクトの識別子を返します。 * * @return 頂点バッファオブジェクトの識別子。または {@code 0} */ protected final int getVertexBufferName() { return mVertexBufferName; } /** * 指定された変数のハンドルを返します。 * * @param name 変数 * @return 変数のハンドル */ protected final int getHandle(final String name) { final Integer value = mHandleMap.get(name); if (value != null) { return value.intValue(); } int location = GLES20.glGetAttribLocation(mProgram, name); if (location == -1) { location = GLES20.glGetUniformLocation(mProgram, name); } if (location == -1) { throw new IllegalStateException("Could not get attrib or uniform location for " + name); } mHandleMap.put(name, Integer.valueOf(location)); return location; } }
FBO を使用する Renderer
今回一番肝となるクラスです。やってることは大したことなく FBO とシステムデフォルトのフレームバッファを切り替えつつ描画するのみです。今回のサンプルではこのクラスのサブクラスをつけませんので、目視で確認したい場合お手数ですがサブクラスを作り前回のサンプルなどを流用して何か描画してみて下さい。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.annotation.TargetApi; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.os.Build; /** * OpenGL ES 2.0 のフレームバッファオブジェクトによる * オフスクリーンレンダリングをサポートした {@link GLSurfaceView.Renderer} の基底クラスを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.FROYO) public abstract class GLES20FramebufferObjectRenderer implements GLSurfaceView.Renderer { /** * オフスクリーン描画用のフレームバッファオブジェクトを保持します。 */ private GLES20FramebufferObject mFramebufferObject; /** * オンスクリーン描画用の GLSL シェーダーオブジェクトを保持します。 */ private GLES20Shader mShader; ////////////////////////////////////////////////////////////////////////// // オーバーライドメソッド /** * 実装は、オフスクリーン描画用のフレームバッファオブジェクトとオンスクリーン描画用のシェーダーオブジェクトを初期化した後に、{@link #onSurfaceCreated(EGLConfig)} を呼び出します。 */ @Override public final void onSurfaceCreated(final GL10 gl, final EGLConfig config) { mFramebufferObject = new GLES20FramebufferObject(); mShader = new GLES20Shader(); mShader.setup(); onSurfaceCreated(config); } /** * 実装は、オフスクリーン描画用のフレームバッファオブジェクトを構成または再構成した後に、{@link #onSurfaceChanged(int, int)} を呼び出します。 */ @Override public final void onSurfaceChanged(final GL10 gl, final int width, final int height) { mFramebufferObject.setup(width, height); mShader.setFrameSize(width, height); onSurfaceChanged(width, height); } /** * 実装はオフスクリーン描画用のフレームバッファオブジェクトを有効にした後に、{@link #onDrawFrame(GLES20FramebufferObject)} を呼び出します。 * その後、フレームバッファオブジェクトの内容をウィンドウシステムが提供するデフォルトのフレームバッファへ描画します。 */ @Override public final void onDrawFrame(final GL10 gl) { //////////////////////////////////////////////////////////// // オフスクリーンレンダリング // FBO へ切り替えます。 mFramebufferObject.enable(); GLES20.glViewport(0, 0, mFramebufferObject.getWidth(), mFramebufferObject.getHeight()); // オフスクリーン描画を行います。 onDrawFrame(mFramebufferObject); //////////////////////////////////////////////////////////// // オンスクリーンレンダリング // ウィンドウシステムが提供するフレームバッファへ切り替えます。 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glViewport(0, 0, mFramebufferObject.getWidth(), mFramebufferObject.getHeight()); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); mShader.draw(mFramebufferObject.getTexName()); } ////////////////////////////////////////////////////////////////////////// /** * Called when the surface is created or recreated. * <p> * Called when the rendering thread * starts and whenever the EGL context is lost. The context will typically * be lost when the Android device awakes after going to sleep. * <p> * Since this method is called at the beginning of rendering, as well as * every time the EGL context is lost, this method is a convenient place to put * code to create resources that need to be created when the rendering * starts, and that need to be recreated when the EGL context is lost. * Textures are an example of a resource that you might want to create * here. * <p> * Note that when the EGL context is lost, all OpenGL resources associated * with that context will be automatically deleted. You do not need to call * the corresponding "glDelete" methods such as glDeleteTextures to * manually delete these lost resources. * <p> * * @param config the EGLConfig of the created surface. Can be used * to create matching pbuffers. */ public abstract void onSurfaceCreated(EGLConfig config); /** * Called when the surface changed size. * <p> * Called after the surface is created and whenever * the OpenGL ES surface size changes. * <p> * Typically you will set your viewport here. If your camera * is fixed then you could also set your projection matrix here: * <pre class="prettyprint"> * void onSurfaceChanged(int width, int height) { * // for a fixed camera, set the projection too * float ratio = (float) width / height; * Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7); * } * </pre> * @param width * @param height */ public abstract void onSurfaceChanged(int width, int height); /** * Called to draw the current frame.<p> * This method is responsible for drawing the current frame.<p> * The implementation of this method typically looks like this: * <pre> * void onDrawFrame(GLES20FramebufferObject fbo) { * GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); * //... other gl calls to render the scene ... * } * </pre> * @param fbo オフスクリーン描画用のフレームバッファオブジェクト */ public abstract void onDrawFrame(GLES20FramebufferObject fbo); }
OpenGL ES 2.0 ユーティリティ
前回からあまり変更ないですが、setupSampler や createBuffer メソッドなどを追加しています。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.opengl.GLES20; import android.opengl.GLException; import android.os.Build; import android.util.Log; import com.orangesignal.android.camera.preview.gl.BuildConfig; /** * OpenGL ES 2.0 に関するユーティリティを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.FROYO) public final class GLES20Utils { : 中略 : /** * サンプラーを構成します。 * * @param target * @param mag GL_TEXTURE_MAG_FILTER * @param min GL_TEXTURE_MIN_FILTER */ public static void setupSampler(final int target, final int mag, final int min) { // テクスチャを拡大/縮小する方法を設定します。 GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, mag); // 拡大するときピクセルの中心付近の線形で補完 GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, min); // 縮小するときピクセルの中心に最も近いテクスチャ要素で補完 // テクスチャの繰り返し方法を設定します。 GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); } private static final int FLOAT_SIZE_BYTES = 4; /** * 指定されたプリミティブ型配列のバッファーデータから {@link FloatBuffer} を作成して返します。 * * @param array バッファーデータ * @return 作成された {@link FloatBuffer} */ public static FloatBuffer toFloatBuffer(final float[] data) { final FloatBuffer buffer = ByteBuffer .allocateDirect(data.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); buffer.put(data).position(0); return buffer; } /** * 指定されたデータでバッファオブジェクトを新規に作成します。 * * @param data データ * @return バッファオブジェクト名 */ @TargetApi(Build.VERSION_CODES.FROYO) public static int createBuffer(final float[] data) { return createBuffer(toFloatBuffer(data)); } /** * 指定されたデータでバッファオブジェクトを新規に作成します。 * * @param data データ * @return バッファオブジェクト名 */ public static int createBuffer(final FloatBuffer data) { final int[] buffers = new int[1]; GLES20.glGenBuffers(buffers.length, buffers, 0); updateBufferData(buffers[0], data); return buffers[0]; } /** * 指定されたバッファオブジェクト名を指定されたデータで更新します。 * * @param bufferName バッファオブジェクト名 * @param data 更新するデータ */ public static void updateBufferData(final int bufferName, final float[] data) { updateBufferData(bufferName, toFloatBuffer(data)); } /** * 指定されたバッファオブジェクト名を指定されたデータで更新します。 * * @param bufferName バッファオブジェクト名 * @param data 更新するデータ */ public static void updateBufferData(final int bufferName, final FloatBuffer data) { GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferName); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, data.capacity() * FLOAT_SIZE_BYTES, data, GLES20.GL_STATIC_DRAW); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } }
以上が今回のサンプルです。
これで画面がチラつかない描画ができるようになりました。
蛇足
FBO を何枚か使い描画する場合は入力と出力の FBO (正確にはテクスチャ) を同じ物にすると正しく描画できない GPU があるので必ず別々の物を使う必要があります。