読者です 読者をやめる 読者になる 読者になる

eaglesakuraの技術ブログ

twitterから派生した、技術的にちょっと込み入った話題とか。

SurfaceTextureでMediaPlayerやカメラ映像をOpenGLテクスチャとして使う場合の注意点

OpenGL Android

SurfaceTextureによるテクスチャへのマルチメディアレンダリング

Android 3.xからSurfaceTextureクラスが導入されて、OpenGLとカメラ・ビデオ等のマルチメディア連携が楽になりました。

具体的には、テクスチャとしてビデオやカメラの映像を取り込めるようになり、リアルタイムでシェーダーによる映像加工したり揺らしたりができるようになりました。

AndroidのコードをみてみるとVideoSurfaceView.javaというクラスでその実装を行なっているらしいですが、その他の情報が殆ど無いので実際に使ってみた結果のハマリどころメモです。

前提として、OpenGL ES 2.0を利用しています。

AndroidのSurfaceTextureは2Dテクスチャではない

正確には、GL_TEXTURE_2D定数を使ってはいけません。GL_TEXTURE_2Dは殆どの場合は「テクスチャを使う場合のおまじない」のような形でglTexXXX関数で使っていると思いますが、それを使うとドハマリします。

SurfaceTextureではGL_TEXTURE_2Dを使わず、GL_TEXTURE_EXTERNAL_OES定数を使います。もしテクスチャ系のUtilクラスを自作している場合、GL_TEXTURE_EXTERNAL_OES定数を使えるように加工しなければなりません。

例えば、私の場合は次のように変更しました。
glTexParameteriに与える引数が、定数直書きからメソッドの引数を利用するように変更になっています。

変更前テクスチャ管理クラス抜粋

    texture.alloc(device->getVRAM(), VRAM_Texture);

    {
        s32 index = state->getFreeTextureUnitIndex();
        this->bind(index);
        {
            // npotでは GL_CLAMP_TO_EDGE を指定する必要がある
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            context.wrapS = GL_CLAMP_TO_EDGE;
            context.wrapT = GL_CLAMP_TO_EDGE;

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            context.magFilter = GL_NEAREST;
            context.minFilter = GL_NEAREST;
        }
        this->unbind();
    }
変更後テクスチャ管理クラス抜粋

    texture.alloc(device->getVRAM(), VRAM_Texture);

    {
        s32 index = state->getFreeTextureUnitIndex();
        this->bind(index);
        {
            // npotでは GL_CLAMP_TO_EDGE を指定する必要がある
            glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            context.wrapS = GL_CLAMP_TO_EDGE;
            context.wrapT = GL_CLAMP_TO_EDGE;

            glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            context.magFilter = GL_NEAREST;
            context.minFilter = GL_NEAREST;
        }
        this->unbind();
    }

テクスチャ用のメモリ確保はSurfaceTextureが行う

通常のテクスチャの場合glTexImage2D(GL_TEXTURE_2D, XXX)関数を使ってVRAMへテクスチャのピクセルをコピーしますが、SurfaceTextureではそれを行なってはいけません。

例えば、間違ってglTexImage2D(GL_TEXTURE_EXTERNAL_OES, 0, GL_RGB, ビデオ幅, ビデオ高さ, 0, GL_RGB, GL_RGBA, 0)のように確保しようとすると例外が投げられるかエラーログが吐かれるはずです。

glGenTextures()で取得したテクスチャ名(ID)は、glTexParameterで設定後はメモリを確保せずにSurfaceTextureへ渡します。

SurfaceTextureはビデオをレンダリングするために十分なテクスチャ領域を自動的に確保します。

この時の制限として、ビデオの縦横サイズはGPUの性能限界(GL_MAX_TEXTURE_SIZE)を超えてはいけません。
ただし、現在のGPU性能だと2048x2048〜4096x4096程度の確保は行えるので、フルHDレベルであれば問題ありません。

確保されるタイミングはSurfaceTextureに渡したタイミングかも知れませんし、SurfaceTexture#updateTexImage()初回実行時かもしれませんが、開発者自身で確保してはいけません。

シェーダーではsampler2Dは使えない

GL_TEXTURE_2Dではないため、シェーダーではsampler2Dによるテクスチャアクセスはできません。
そのため、sampler2D変数の代わりにsamplerExternalOES変数を定義する必要があります。

使い方はsampler2D変数と変わりません。

内部的には、GL_OES_EGL_image_external機能を利用しますので、シェーダーのextension要求として次が必要です。定義しない場合、コンパイルに失敗するかアプリが強制終了する等の問題が発生します(GNで確認)

#extension GL_OES_EGL_image_external : require
フラグメントシェーダー全文

static const charactor *FRAGMENT_EXTERNAL_SHADER_SOURCE = 
        "#extension GL_OES_EGL_image_external : require\n"
        "        precision mediump float;"
        // UV setting"
        "        varying vec2 fTexCoord;"
        // texture
        "        uniform samplerExternalOES tex;"
        // color
        "        uniform mediump vec4    blendColor;"
        "        void main() {"
        "            vec4 color = texture2D(tex, fTexCoord) * blendColor;"
        "            color.a *= blendColor.a;"
        "            gl_FragColor = color;"
        "        }"
//

レンダリング領域の設定にはクセがある

ビデオの再生を開始すると、定期的にSurfaceTexture#OnFrameAvailableListenerを通じてコールバックが呼ばれます。
コールバック後、次の条件を満たすときにSurfaceTexture#updateTexImage()メソッドを呼び出すことでテクスチャにビデオやカメラの内容がコピーされます。

  • テクスチャと同じEGLContextがeglMakeCurrentに設定されている
    • バインドされていれば、おそらくスレッドは自由です
  • テクスチャがGL_TEXTURE_EXTERNAL_OESにバインドされている
    • 事前にtexture nameを渡しているため、コレは不要かもしれません・・・

SurfaceTextureは渡された空のテクスチャに対してレンダリングするために十分な領域を確保しますが、ビデオのサイズと完全に同一ではありません。
例えば、320x240のビデオをSurfaceTextureを通して描画させると、次のようなテクスチャが出来上がります。

f:id:eaglesakura:20130306094701j:plain

左右や下側に不要領域が作られているのが分かります。ジャストサイズでない理由はおそらく効率上の理由ですが、具体的にどの領域がビデオとして使われるかは問い合わせすることができません。

その代わり、SurfaceTexture#getTransformMatrix()メソッドでUV座標0.0〜1.0を利用する場合に設定するUV行列を取得することができます。引数にfloat[16]の配列を指定することで、4x4の行列として利用できます。

VertexShaderでUV座標を次のように処理すると、レンダリングに必要なUV領域として利用できます。
ただし、単純に行列を当てると、上下が反転してしまいます。

// SurfaceTexture#getTransformMatrix()で取得した行列
uniform mediump mat4    unif_texm;

-- 省略 --

fTexCoord = (unif_texm * vTexCoord).xy;

f:id:eaglesakura:20130306094742j:plain

その対策として、レンダリング用のポリゴンを作る時点で次のようにUV座標を上下反転させています。

これで見た目的に正常なレンダリング領域を設定出来ます。
頂点シェーダーを使ってもいいですが、どうせ毎度同じ処理をするならばUV単位で事前設定してしまったほうが負荷的にもプログラム的にも楽になります。

通常の四角形の場合

const static Quad::QuadVertex g_vertices = {
//
        /**
         // 位置情報
         left, top, //!< 左上
         right, top, //!< 右上
         left, bottom, //!< 左下
         right, bottom, //!< 右下

         //! UV情報
         0, 0, //!< 左上
         1, 0, //!< 右上
         0, 1, //!< 左下
         1, 1, //!< 右下
         */
        // 左上
        { LEFT, TOP, 0.0f, 0.0f, },
        // 右上
        { RIGHT, TOP, 1.0f, 0.0f },
        // 左下
        { LEFT, BOTTOM, 0.0f, 1.0f },
        // 右下
        { RIGHT, BOTTOM, 1.0f, 1.0f },
// end
        };
}

ビデオレンダリング用の四角形の場合

const static Quad::QuadVertex g_revert_vertices = {
// 左上
        { -0.5, 0.5, 0.0f, 1.0f, },
        // 右上
        { 0.5, 0.5, 1.0f, 1.0f },
        // 左下
        { -0.5, -0.5, 0.0f, 0.0f },
        // 右下
        { 0.5, -0.5, 1.0f, 0.0f }, };
//
}

f:id:eaglesakura:20130306094759j:plain

複数のSurfaceTextureを同時に利用する場合の注意点

一枚のSurfaceTextureで一つのVideoを再生するだけなら特に注意点はありません。
複数のビデオを同時に再生する場合や、効率化のためにテクスチャユニット等を細かく制御している場合はドハマリする場合があります。

GL_TEXTURE_EXTERNAL_OESは同時に一つのテクスチャユニットにしか割り当てることができません。
例えば、次のような制御を行うと意図しないテクスチャにビデオがレンダリングされる恐れがあります。

  1. glActiveTextureで0番ユニットに切り替える
  2. ビデオAをレンダリングさせる
  3. glActiveTextureで1番ユニットに切り替える
  4. ビデオBをレンダリングさせる
  5. 〜〜次のフレーム〜〜
  6. glActiveTextureで0番ユニットに切り替える
  7. ビデオAをレンダリングさせる
  8. 何故かビデオBがレンダリングされてる/(^o^)\

GL_TEXTURE_EXTERNAL_OES自体の仕様かAndroidが効率化のために設けた仕様かバグかは不明です。
コレを回避するために、私の場合はビデオ用のテクスチャを必ず同一のテクスチャユニットにバインドすることにしました。

必ず末尾のテクスチャユニットを利用する

    this->textureUnit = GPUCapacity::getMaxTextureUnits() - 1;

追記 3/7

単にActiveTextureの呼び出し忘れ(Utilクラスのバグ)だった疑惑(´・ω・`)

追記 3/8

単にActiveTextureの呼び出し忘れ(Utilクラスのバグ)だった。
glActiveTexture(index)されていて、かつglBindTexture(target, name)されているテクスチャに転送されるらしい。

最後に

SurfaceTextureを使えばカメラとかビデオとかと連携できて夢が広がりますね。
間違いとかあったら教えて下さいm(__)m

初めてのOpenGL ES

初めてのOpenGL ES