web-dev-qa-db-ja.com

AndroidのYUV-NV21カメラ画像をlibgdxの背景にOpenGLES 2.0でリアルタイムでレンダリングする方法は?

Androidとは異なり、私はGL/libgdxに比較的慣れていません。 AndroidカメラのYUV-NV21プレビュー画像をlibgdx内の画面背景にリアルタイムでレンダリングすることは、私が解決する必要があるタスクです。多面的です。主な懸念事項は次のとおりです。

  1. Androidカメラのプレビュー画像は、YUV-NV21スペース(およびUチャネルとVチャネルがインターリーブされずにグループ化されている同様のYV12スペース)にあることが保証されています。最新のほとんどのデバイスが暗黙のRGB変換を提供すると仮定すると、非常に間違っています。たとえば、最新のSamsung Note 10.1 2014バージョンはYUVフォーマットのみを提供します。 RGBでない限り、OpenGLの画面には何も描画できないため、色空間を何らかの方法で変換する必要があります。

  2. Libgdxドキュメントの例( libgdxとデバイスカメラ の統合)では、Androidサーフェスビューがすべての下にあり、GLES 1.1で画像を描画します。 2014年3月の初めに、OpenGLES 1.xサポートは廃止され、ほぼすべてのデバイスがGLES 2.0をサポートするようになったため、libgdxから削除されました。GLES2.0で同じサンプルを試すと、画像上に描画する3Dオブジェクトは半透明になります。背後の表面はGLとは関係がないため、これは実際には制御できません。BLENDING/ TRANSLUCENCYを無効にしても機能しません。したがって、このイメージのレンダリングは純粋にGLで行う必要があります。

  3. これはリアルタイムで行う必要があるため、色空間の変換は非常に高速でなければなりません。 Androidビットマップを使用したソフトウェア変換はおそらく遅すぎるでしょう。

  4. 副次的な機能として、カメラの画像は、Androidコードからアクセスできる必要があります。これは、画面に描画する以外のタスクを実行するために、たとえば、JNIを介してネイティブの画像プロセッサに送信するためです。

問題は、このタスクをどのように適切かつ可能な限り迅速に実行するかです。

20
Ayberk Özgür

簡単な答えは、カメラの画像チャネル(Y、UV)をテクスチャにロードし、カスタムフラグメントシェーダーを使用してこれらのテクスチャをメッシュに描画することです。これにより、色空間変換が行われます。このシェーダーはGPUで実行されるため、CPUよりもはるかに高速であり、Javaコードよりもはるかに高速です。このメッシュはGLの一部であるため、他の3D形状またはスプライト安全にその上または下に描画できます。

私はこの答えから始めて問題を解決しました https://stackoverflow.com/a/17615696/1525238 。私は次のリンクを使用して一般的な方法を理解しました: OpenGL ESでカメラビューを使用する方法 、それはバダのために書かれていますが、原理は同じです。変換式が少し変だったので、Wikipediaの記事 YUV Conversion to/from RGB の式に置き換えました。

ソリューションに至るまでの手順は次のとおりです。

YUV-NV21説明

Androidカメラのライブ画像はプレビュー画像です。デフォルトの色空間(および2つの保証された色空間の1つ)は、カメラのプレビュー用にYUV-NV21です。この形式の説明は非常にバラバラですが、ここで簡単に説明します:

画像データは(幅x高さ)x 3/2バイトで構成されます。最初の幅x高さバイトはYチャネルで、各ピクセルに1輝度バイトです。次の(幅/ 2)x(高さ/ 2)x 2 =幅x高さ/ 2バイトはUV平面です。 2 x 2 = 4元のピクセルの2つの連続するバイトはそれぞれ、V、U(NV21仕様に従ってこの順序で)クロマバイトです。言い換えると、UV平面はサイズが(width/2)x(height/2)ピクセルで、各次元で2の係数でダウンサンプリングされます。さらに、U、Vクロマバイトがインターリーブされます。

これは、YUV-NV12、NV21がちょうどU、Vバイトが反転していることを説明する非常に素晴らしい画像です。

YUV-NV12

この形式をRGBに変換する方法は?

質問で述べたように、この変換は、Androidコード内で実行すると、ライブになるまでに時間がかかります。幸い、GL内で実行できます。 GPUで実行されるシェーダー。これにより、非常に高速に実行できます。

一般的なアイデアは、画像のチャネルをテクスチャとしてシェーダーに渡し、RGB変換を行う方法でレンダリングすることです。そのためには、まず画像内のチャネルを、テクスチャに渡すことができるバッファにコピーする必要があります。

_byte[] image;
ByteBuffer yBuffer, uvBuffer;

...

yBuffer.put(image, 0, width*height);
yBuffer.position(0);

uvBuffer.put(image, width*height, width*height/2);
uvBuffer.position(0);
_

次に、これらのバッファーを実際のGLテクスチャに渡します。

_/*
 * Prepare the Y channel texture
 */

//Set texture slot 0 as active and bind our texture object to it
Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
yTexture.bind();

//Y texture is (width*height) in size and each pixel is one byte; 
//by setting GL_LUMINANCE, OpenGL puts this byte into R,G and B 
//components of the texture
Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE, 
    width, height, 0, GL20.GL_LUMINANCE, GL20.GL_UNSIGNED_BYTE, yBuffer);

//Use linear interpolation when magnifying/minifying the texture to 
//areas larger/smaller than the texture size
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_Edge);
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_Edge);

/*
 * Prepare the UV channel texture
 */

//Set texture slot 1 as active and bind our texture object to it
Gdx.gl.glActiveTexture(GL20.GL_TEXTURE1);
uvTexture.bind();

//UV texture is (width/2*height/2) in size (downsampled by 2 in 
//both dimensions, each pixel corresponds to 4 pixels of the Y channel) 
//and each pixel is two bytes. By setting GL_LUMINANCE_ALPHA, OpenGL 
//puts first byte (V) into R,G and B components and of the texture
//and the second byte (U) into the A component of the texture. That's 
//why we find U and V at A and R respectively in the fragment shader code.
//Note that we could have also found V at G or B as well. 
Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE_ALPHA, 
    width/2, height/2, 0, GL20.GL_LUMINANCE_ALPHA, GL20.GL_UNSIGNED_BYTE, 
    uvBuffer);

//Use linear interpolation when magnifying/minifying the texture to 
//areas larger/smaller than the texture size
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_Edge);
Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, 
    GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_Edge);
_

次に、前に準備したメッシュをレンダリングします(画面全体をカバーします)。シェーダーは、メッシュ上のバインドされたテクスチャのレンダリングを処理します。

_shader.begin();

//Set the uniform y_texture object to the texture at slot 0
shader.setUniformi("y_texture", 0);

//Set the uniform uv_texture object to the texture at slot 1
shader.setUniformi("uv_texture", 1);

mesh.render(shader, GL20.GL_TRIANGLES);
shader.end();
_

最後に、シェーダーがテクスチャをメッシュにレンダリングするタスクを引き継ぎます。実際の変換を実現するフラグメントシェーダーは次のようになります。

_String fragmentShader = 
    "#ifdef GL_ES\n" +
    "precision highp float;\n" +
    "#endif\n" +

    "varying vec2 v_texCoord;\n" +
    "uniform sampler2D y_texture;\n" +
    "uniform sampler2D uv_texture;\n" +

    "void main (void){\n" +
    "   float r, g, b, y, u, v;\n" +

    //We had put the Y values of each pixel to the R,G,B components by 
    //GL_LUMINANCE, that's why we're pulling it from the R component,
    //we could also use G or B
    "   y = texture2D(y_texture, v_texCoord).r;\n" + 

    //We had put the U and V values of each pixel to the A and R,G,B 
    //components of the texture respectively using GL_LUMINANCE_ALPHA. 
    //Since U,V bytes are interspread in the texture, this is probably 
    //the fastest way to use them in the shader
    "   u = texture2D(uv_texture, v_texCoord).a - 0.5;\n" +
    "   v = texture2D(uv_texture, v_texCoord).r - 0.5;\n" +

    //The numbers are just YUV to RGB conversion constants
    "   r = y + 1.13983*v;\n" +
    "   g = y - 0.39465*u - 0.58060*v;\n" +
    "   b = y + 2.03211*u;\n" +

    //We finally set the RGB color of our pixel
    "   gl_FragColor = vec4(r, g, b, 1.0);\n" +
    "}\n"; 
_

同じ座標変数_v_texCoord_を使用してYテクスチャとUVテクスチャにアクセスしていることに注意してください。これは_v_texCoord_が-1.1.の間にあるためです=実際のテクスチャピクセル座標ではなく、テクスチャの一方の端からもう一方の端にスケーリングします。これは、シェーダーの最も優れた機能の1つです。

完全なソースコード

Libgdxはクロスプラットフォームであるため、デバイスのカメラとレンダリングを処理するプラットフォームごとに異なる方法で拡張できるオブジェクトが必要です。たとえば、RGBイメージを提供するハードウェアを入手できる場合は、YUV-RGBシェーダー変換を完全にバイパスすることができます。このため、異なるプラットフォームごとに実装されるデバイスカメラコントローラーインターフェイスが必要です。

_public interface PlatformDependentCameraController {

    void init();

    void renderBackground();

    void destroy();
} 
_

Androidこのインターフェイスのバージョンは次のとおりです(ライブカメラ画像は1280x720ピクセルであると想定されています):

_public class AndroidDependentCameraController implements PlatformDependentCameraController, Camera.PreviewCallback {

    private static byte[] image; //The image buffer that will hold the camera image when preview callback arrives

    private Camera camera; //The camera object

    //The Y and UV buffers that will pass our image channel data to the textures
    private ByteBuffer yBuffer;
    private ByteBuffer uvBuffer;

    ShaderProgram shader; //Our shader
    Texture yTexture; //Our Y texture
    Texture uvTexture; //Our UV texture
    Mesh mesh; //Our mesh that we will draw the texture on

    public AndroidDependentCameraController(){

        //Our YUV image is 12 bits per pixel
        image = new byte[1280*720/8*12];
    }

    @Override
    public void init(){

        /*
         * Initialize the OpenGL/libgdx stuff
         */

        //Do not enforce power of two texture sizes
        Texture.setEnforcePotImages(false);

        //Allocate textures
        yTexture = new Texture(1280,720,Format.Intensity); //A 8-bit per pixel format
        uvTexture = new Texture(1280/2,720/2,Format.LuminanceAlpha); //A 16-bit per pixel format

        //Allocate buffers on the native memory space, not inside the JVM heap
        yBuffer = ByteBuffer.allocateDirect(1280*720);
        uvBuffer = ByteBuffer.allocateDirect(1280*720/2); //We have (width/2*height/2) pixels, each pixel is 2 bytes
        yBuffer.order(ByteOrder.nativeOrder());
        uvBuffer.order(ByteOrder.nativeOrder());

        //Our vertex shader code; nothing special
        String vertexShader = 
                "attribute vec4 a_position;                         \n" + 
                "attribute vec2 a_texCoord;                         \n" + 
                "varying vec2 v_texCoord;                           \n" + 

                "void main(){                                       \n" + 
                "   gl_Position = a_position;                       \n" + 
                "   v_texCoord = a_texCoord;                        \n" +
                "}                                                  \n";

        //Our fragment shader code; takes Y,U,V values for each pixel and calculates R,G,B colors,
        //Effectively making YUV to RGB conversion
        String fragmentShader = 
                "#ifdef GL_ES                                       \n" +
                "precision highp float;                             \n" +
                "#endif                                             \n" +

                "varying vec2 v_texCoord;                           \n" +
                "uniform sampler2D y_texture;                       \n" +
                "uniform sampler2D uv_texture;                      \n" +

                "void main (void){                                  \n" +
                "   float r, g, b, y, u, v;                         \n" +

                //We had put the Y values of each pixel to the R,G,B components by GL_LUMINANCE, 
                //that's why we're pulling it from the R component, we could also use G or B
                "   y = texture2D(y_texture, v_texCoord).r;         \n" + 

                //We had put the U and V values of each pixel to the A and R,G,B components of the
                //texture respectively using GL_LUMINANCE_ALPHA. Since U,V bytes are interspread 
                //in the texture, this is probably the fastest way to use them in the shader
                "   u = texture2D(uv_texture, v_texCoord).a - 0.5;  \n" +                                   
                "   v = texture2D(uv_texture, v_texCoord).r - 0.5;  \n" +


                //The numbers are just YUV to RGB conversion constants
                "   r = y + 1.13983*v;                              \n" +
                "   g = y - 0.39465*u - 0.58060*v;                  \n" +
                "   b = y + 2.03211*u;                              \n" +

                //We finally set the RGB color of our pixel
                "   gl_FragColor = vec4(r, g, b, 1.0);              \n" +
                "}                                                  \n"; 

        //Create and compile our shader
        shader = new ShaderProgram(vertexShader, fragmentShader);

        //Create our mesh that we will draw on, it has 4 vertices corresponding to the 4 corners of the screen
        mesh = new Mesh(true, 4, 6, 
                new VertexAttribute(Usage.Position, 2, "a_position"), 
                new VertexAttribute(Usage.TextureCoordinates, 2, "a_texCoord"));

        //The vertices include the screen coordinates (between -1.0 and 1.0) and texture coordinates (between 0.0 and 1.0)
        float[] vertices = {
                -1.0f,  1.0f,   // Position 0
                0.0f,   0.0f,   // TexCoord 0
                -1.0f,  -1.0f,  // Position 1
                0.0f,   1.0f,   // TexCoord 1
                1.0f,   -1.0f,  // Position 2
                1.0f,   1.0f,   // TexCoord 2
                1.0f,   1.0f,   // Position 3
                1.0f,   0.0f    // TexCoord 3
        };

        //The indices come in trios of vertex indices that describe the triangles of our mesh
        short[] indices = {0, 1, 2, 0, 2, 3};

        //Set vertices and indices to our mesh
        mesh.setVertices(vertices);
        mesh.setIndices(indices);

        /*
         * Initialize the Android camera
         */
        camera = Camera.open(0);

        //We set the buffer ourselves that will be used to hold the preview image
        camera.setPreviewCallbackWithBuffer(this); 

        //Set the camera parameters
        Camera.Parameters params = camera.getParameters();
        params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
        params.setPreviewSize(1280,720); 
        camera.setParameters(params);

        //Start the preview
        camera.startPreview();

        //Set the first buffer, the preview doesn't start unless we set the buffers
        camera.addCallbackBuffer(image);
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {

        //Send the buffer reference to the next preview so that a new buffer is not allocated and we use the same space
        camera.addCallbackBuffer(image);
    }

    @Override
    public void renderBackground() {

        /*
         * Because of Java's limitations, we can't reference the middle of an array and 
         * we must copy the channels in our byte array into buffers before setting them to textures
         */

        //Copy the Y channel of the image into its buffer, the first (width*height) bytes are the Y channel
        yBuffer.put(image, 0, 1280*720);
        yBuffer.position(0);

        //Copy the UV channels of the image into their buffer, the following (width*height/2) bytes are the UV channel; the U and V bytes are interspread
        uvBuffer.put(image, 1280*720, 1280*720/2);
        uvBuffer.position(0);

        /*
         * Prepare the Y channel texture
         */

        //Set texture slot 0 as active and bind our texture object to it
        Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
        yTexture.bind();

        //Y texture is (width*height) in size and each pixel is one byte; by setting GL_LUMINANCE, OpenGL puts this byte into R,G and B components of the texture
        Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE, 1280, 720, 0, GL20.GL_LUMINANCE, GL20.GL_UNSIGNED_BYTE, yBuffer);

        //Use linear interpolation when magnifying/minifying the texture to areas larger/smaller than the texture size
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_Edge);
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_Edge);


        /*
         * Prepare the UV channel texture
         */

        //Set texture slot 1 as active and bind our texture object to it
        Gdx.gl.glActiveTexture(GL20.GL_TEXTURE1);
        uvTexture.bind();

        //UV texture is (width/2*height/2) in size (downsampled by 2 in both dimensions, each pixel corresponds to 4 pixels of the Y channel) 
        //and each pixel is two bytes. By setting GL_LUMINANCE_ALPHA, OpenGL puts first byte (V) into R,G and B components and of the texture
        //and the second byte (U) into the A component of the texture. That's why we find U and V at A and R respectively in the fragment shader code.
        //Note that we could have also found V at G or B as well. 
        Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE_ALPHA, 1280/2, 720/2, 0, GL20.GL_LUMINANCE_ALPHA, GL20.GL_UNSIGNED_BYTE, uvBuffer);

        //Use linear interpolation when magnifying/minifying the texture to areas larger/smaller than the texture size
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_Edge);
        Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_Edge);

        /*
         * Draw the textures onto a mesh using our shader
         */

        shader.begin();

        //Set the uniform y_texture object to the texture at slot 0
        shader.setUniformi("y_texture", 0);

        //Set the uniform uv_texture object to the texture at slot 1
        shader.setUniformi("uv_texture", 1);

        //Render our mesh using the shader, which in turn will use our textures to render their content on the mesh
        mesh.render(shader, GL20.GL_TRIANGLES);
        shader.end();
    }

    @Override
    public void destroy() {
        camera.stopPreview();
        camera.setPreviewCallbackWithBuffer(null);
        camera.release();
    }
}
_

アプリケーションのメイン部分では、init()が最初に1回呼び出され、renderBackground()がすべてのレンダーサイクルで呼び出され、destroy()が最後に1回呼び出されるようにします。

_public class YourApplication implements ApplicationListener {

    private final PlatformDependentCameraController deviceCameraControl;

    public YourApplication(PlatformDependentCameraController cameraControl) {
        this.deviceCameraControl = cameraControl;
    }

    @Override
    public void create() {              
        deviceCameraControl.init();
    }

    @Override
    public void render() {      
        Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

        //Render the background that is the live camera image
        deviceCameraControl.renderBackground();

        /*
         * Render anything here (sprites/models etc.) that you want to go on top of the camera image
         */
    }

    @Override
    public void dispose() {
        deviceCameraControl.destroy();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}
_

他の唯一のAndroid固有の部分は、次の非常に短いメインAndroidコード、新しいAndroid特定のデバイスカメラハンドラーを作成し、それをメインに渡すだけですlibgdxオブジェクト:

_public class MainActivity extends AndroidApplication {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
        cfg.useGL20 = true; //This line is obsolete in the newest libgdx version
        cfg.a = 8;
        cfg.b = 8;
        cfg.g = 8;
        cfg.r = 8;

        PlatformDependentCameraController cameraControl = new AndroidDependentCameraController();
        initialize(new YourApplication(cameraControl), cfg);

        graphics.getView().setKeepScreenOn(true);
    }
}
_

それはどれくらい速いですか?

このルーチンを2つのデバイスでテストしました。測定値はフレーム間で一定ではありませんが、一般的なプロファイルを観察できます。

  1. Samsung Galaxy Note II LTE-(GT-N7105):ありARM Mali-400 MP4 GPU。

    • 1フレームのレンダリングには約5〜6ミリ秒かかり、数秒ごとに約15ミリ秒にジャンプすることもあります。
    • 実際のレンダリングライン(mesh.render(shader, GL20.GL_TRIANGLES);)は常に0〜1ミリ秒かかります
    • 両方のテクスチャの作成とバインドには、一貫して合計で1〜3ミリ秒かかります。
    • ByteBufferのコピーには通常、合計で1〜3ミリ秒かかりますが、おそらくJVMヒープ内で画像バッファーが移動されるため、約7ミリ秒にジャンプすることがあります。
  2. Samsung Galaxy Note 10.1 2014-(SM-P600):ARM Mali-T628 GPU。

    • 1フレームのレンダリングには約2〜4ミリ秒かかり、まれに約6〜10ミリ秒にジャンプします
    • 実際のレンダリングライン(mesh.render(shader, GL20.GL_TRIANGLES);)は常に0〜1ミリ秒かかります
    • 両方のテクスチャの作成とバインドには合計で1〜3ミリ秒かかりますが、数秒ごとに約6〜9ミリ秒にジャンプします。
    • ByteBufferのコピーは通常、合計で0〜2ミリ秒かかりますが、ごくまれに約6ミリ秒にジャンプします。

これらのプロファイルを他の方法で高速化できると思われる場合は、遠慮なく共有してください。この小さなチュートリアルが役に立てば幸いです。

70
Ayberk Özgür

最速で最も最適化された方法については、一般的なGL Extention

//Fragment Shader
#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES u_Texture;

ジャワより

surfaceTexture = new SurfaceTexture(textureIDs[0]);
try {
   someCamera.setPreviewTexture(surfaceTexture);
} catch (IOException t) {
   Log.e(TAG, "Cannot set preview texture target!");
}

someCamera.startPreview();

private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;

Java GLスレッド

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureIDs[0]);
GLES20.glUniform1i(uTextureHandle, 0);

色変換はすでに行われています。 Fragmentシェーダーで、思い通りのことができます。

プラットフォームに依存しているため、Libgdxソリューションはまったくありません。ラッパーでプラットフォームに依存するものを初期化し、それをLibgdxアクティビティに送信できます。

研究の時間を節約できることを願っています。

4
fky