Pocket

OpenGL ESのシェーダとは、グラフィックスを操作するためのC言語ベースのプログラムです。例えば、cyberagent製のGPUImageのGPUImageContrastFilterのフラグメントシェーダは、次のようなものです。

public static final String CONTRAST_FRAGMENT_SHADER = "" +
    "varying highp vec2 textureCoordinate;\n" +
    " \n" +
    " uniform sampler2D inputImageTexture;\n" +
    " uniform lowp float contrast;\n" +
    " \n" +
    " void main()\n" +
    " {\n" +
    "     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
    "     \n" +
    "     gl_FragColor = vec4(((textureColor.rgb - vec3(0.5)) * contrast + vec3(0.5)), textureColor.w);\n" +
    " }";

この中の5行目、uniform lowp float contrast; がuniform変数です。uniform変数は、値を動的に変更できます。

まず、GPUImageFilterのonInit()で、変数名を指定して、uniform変数のロケーション番号を取得します。見つからなければ、-1、見つかれば、0以上の数値が返ってきます。

public void onInit() {
    super.onInit();
    contrastLocation = GLES20.glGetUniformLocation(getProgram(), "contrast");
}

次に、UIでスライダーなどが操作されたタイミングで、uniform変数の値を更新します。

public void setContrast(final float contrast) {
    this.contrast = contrast;
    setFloat(contrastLocation, this.contrast);
}

uniform変数の型に合わせて、setInteger、setFloatVec3、setFloatArrayなどのメソッドも用意されています。

さて、GPUImageFilterのサブクラスを作り、独自のシェーダを作っていたときのことです。まず、何も加工しないシェーダを用意しました。いずれ、使う予定のuniform変数data1も宣言しました。

public static final String MY_FRAGMENT_SHADER = "" +
    "varying highp vec2 textureCoordinate;\n" +
    " \n" +
    " uniform sampler2D inputImageTexture;\n" +
    " uniform lowp float data1;\n" +
    " \n" +
    " void main()\n" +
    " {\n" +
    "     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +
    " }";

次に、シェーダのmain()の処理を書く前に、onInit()でuniform変数data1のロケーションを取得できているかをトレースで確認しました。0か1が返ってくるだろうと。

public void onInit() {
    super.onInit();
    data1Location = GLES20.glGetUniformLocation(getProgram(), "data1");
}

予想に反して、data1Locationに -1 が返ってきました!変数名の長さを短くしたり、キャメルケース、スネークケースを試したり、x や contrastにしても、-1です。

そこで、GPUImageContrastFilter が動くことを確認してから、contrastの下にdata1を宣言しました。

public static final String CONTRAST_FRAGMENT_SHADER = "" +
    "varying highp vec2 textureCoordinate;\n" +
    " \n" +
    " uniform sampler2D inputImageTexture;\n" +
    " uniform lowp float contrast;\n" +
    " uniform lowp float data1;\n" +
    " \n" +
    " void main()\n" +
    " {\n" +
    "     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
    "     \n" +
    "     gl_FragColor = vec4(((textureColor.rgb - vec3(0.5)) * contrast + vec3(0.5)), textureColor.w);\n" +
    " }";

onInit()で、contrastとdata1のuniformロケーション番号を取得しました。すると、

public void onInit() {
    super.onInit();
    contrastLocation = GLES20.glGetUniformLocation(getProgram(), "contrast");
    data1Location    = GLES20.glGetUniformLocation(getProgram(), "data1");
}

contrastLocationには、0が返ってきますが、data1Locationには、-1が返ってきました。

シェーダ内のuniform変数の宣言順序を入れ替えてみました。

    " uniform lowp float data1;\n" +
    " uniform lowp float contrast;\n" +

結果は同じでした。

ここにいたって、data1は宣言だけして、値を参照していないから?ということに気づきました。

試しに、main()内の、contrastをdata1; で置き換えてみました。

    " void main()\n" +
    " {\n" +
    "     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
    "     \n" +
    "     gl_FragColor = vec4(((textureColor.rgb - vec3(0.5)) * data1 + vec3(0.5)), textureColor.w);\n" +
    " }";

すると、contrastLocationに -1、data1Locationに 0が返ってきました。

シェーダがコンパイルされるとき、コンパイラーの最適化で、参照していない変数は削除されてしまうんですね。

この記事は実際より簡略化したので、すんなり原因がわかったように見えます。実際は、7個のuniform変数を宣言し、mainでも少し処理を書いていたので、原因がわかるまで、かなりさまよいました。