AndroidのOpenGL ESで、カメラからの入力の各画素を処理する

最近AndroidOpenGL ESを触っているので、少しだけ記事にしてみます。
多分タイトルは正確ではないんだろうなあ……。
OpenGL ESやGLSLに関しては全く詳しくないというか、自分のやりたいことはフラグメントシェーダを書くだけで実現できてしまったので、とりあえずその部分だけを記事にしています。

元にしたプロジェクト

今回のプロジェクトは、以下のプロジェクトをコピペして、OpenGL ESのバージョンを3.1に変え、フルスクリーン表示にしたものです。
qiita.com
カメラからの入力を受け取るための部分はこのプロジェクトのまんまです。

サンプルプロジェクト

全体は以下のGitHubリポジトリを参照して下さい。
ランタイムパーミッション関連を書いていないので、必要であればカメラパーミッションの設定をお願いします。
github.com

フラグメントシェーダ

OpenGL ESでは、GLSLで記述されたフラグメントシェーダが、各頂点の色に関する処理を行います。(今回は各頂点=各画素位のノリで書いていますが多分厳密には違う……)
今回はテストコードとして、画面の左半分であれば赤にノイズを掛け、青と緑を強める、という処理を実装してみました。
この内容はRenderer.java内に書いてあります。
今回は書いていませんが、行列演算も可能です。

#extension GL_OES_EGL_image_external : require
precision mediump float;
//texcoordVaryingには、テクスチャの0.0から1.0までの位置情報が格納されている
varying vec2 texcoordVarying;
uniform samplerExternalOES texture;

//https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
//0.0から1.0の一様乱数を返す関数、
//入力はその都度変わる何か、例えば色情報を用いる
float rand(vec2 co){
  return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);\n" +
}

//フラグメントシェーダのメイン関数
void main() {
  //色情報を取得, ここで取得されるのは0.0 〜 1.0の範囲の値を持つrgbaのベクトル
  vec4 v = texture2D(texture, texcoordVarying);
  //画面の左半分の場合処理する
  if(texcoordVarying.x < 0.5){
    //rに-0.5 〜 0.5の範囲の乱数を載せている
    //配列と同じようにアクセスできる
    v[0] += (rand(vec2(v[0] * v[1],v[0] * v[2])) - 0.5);
    //g, bを入力と1.0の平均で置き換える
    //このように、ベクトルはxyzw表記でアクセスしたり、ベクトルの一部を低次元のベクトルとして演算することが可能
    v.yz = vec2((v.y + 1.0)/2.0, (v.z + 1.0)/2.0);
  }
  gl_FragColor = v;
}
スクリーンショット

実行するとこんな感じになります。
f:id:wrongwrongwrongwrong163377:20170829185104p:plain

java上での取り扱い

上記のGLSLコードは、java内では、以下のようにStringで記述します。

    private static final String FRAGMENT_SHADER =
            "#extension GL_OES_EGL_image_external : require\n" +
                    "precision mediump float;\n" +
                    //texcoordVaryingには、テクスチャの0.0から1.0までの位置情報が格納されている
                    "varying vec2 texcoordVarying;\n" +
                    "uniform samplerExternalOES texture;\n" +

                    //https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
                    //0.0から1.0の一様乱数を返す関数、
                    //入力はその都度変わる何か、例えば色情報を用いる
                    "float rand(vec2 co){\n" +
                    "    return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);\n" +
                    "}\n"+

                    //フラグメントシェーダのメイン関数
                    "void main() {\n" +
                    //色情報を取得, ここで取得されるのは0.0 〜 1.0の範囲の値を持つrgbaのベクトル
                    "  vec4 v = texture2D(texture, texcoordVarying);\n" +
                    //画面の左半分の場合処理する
                    "  if(texcoordVarying.x < 0.5){\n" +
                    //rに-0.5 〜 0.5の範囲の乱数を載せている
                    //配列と同じようにアクセスできる
                    "    v[0] += (rand(vec2(v[0] * v[1],v[0] * v[2])) - 0.5);\n" +
                    //g, bを入力と1.0の平均で置き換える
                    //このように、ベクトルはxyzw表記でアクセスしたり、ベクトルの一部を低次元のベクトルとして演算することが可能
                    "    v.yz = vec2((v.y + 1.0)/2.0, (v.z + 1.0)/2.0);\n"+
                    "  }\n"+
                    "  gl_FragColor = v;\n" +
                    "}\n";

余談

カメラ入力に対して各画素への処理を行いたい場合、例えばOpenCVを使うようなことが考えられますが、実際に比較してみると、OpenGL ESのを用いたほうがはるかに高速でした。
これは、画像処理をGPUで行うため計算が高速であることと、カメラから取り込んだデータを処理するまで、処理した後でデータ転送や変換が少ないことが原因でしょう。
勉強し始めたばかりなので分からないことだらけで、特にAndroid上での取り扱いに関しては資料が少なく、苦戦中です。。。