uniformの基礎

uniformを使ってみる

 uniformの解説をします。h_doxasさんのwgld.orgを覗いてみたんですが、この記事uniformはどこで初登場かというと、なんといろいろ準備した後でさあポリゴン描画だってところで行列uniformを導入するために出現するんですね...しかもその行列も隠蔽されているので、実質的には説明ゼロで登場してます(ロケーションが必要くらいしか言ってない)。いきなり行列uniformは難易度が高いので、ここでは基礎ということで、float,vec2,vec3,vec4をやります。仕組みを理解する方がずっと重要なので。それでは参りましょう。

コード全文

作品9のリンク

uniformとはなにか

 頂点ごとに走るのがバーテックスシェーダーです。それらの処理は、頂点ごとに異なります。ピクセルごとに並列で走るのがフラグメントシェーダーです。その処理は、ピクセルごとに異なります。uniformは、それらで共通の値を使います。バーテックスシェーダーのuniformは、すべての頂点で共通の値が使われます。フラグメントシェーダーのuniformは、すべてのピクセルで同じ値が使われます。たとえば三角形の色を決めたりできます。そしてuniformは外部から変数を入力するので、同じプログラムで自在に挙動を変えることができます。もうマイナーチェンジを作る必要はありません。

uniformを宣言する

 シェーダー内でuniformの使用を宣言するには、頭にuniformを付けて普通に宣言します。vsの方は:

uniform vec2 uPos;
uniform float uZuzu;

で、fsの方は:

uniform vec3 uColor;
uniform float uAlpha;
uniform vec4 uMimi;

ですね。型と一緒に宣言します。ただシェーダーを読むと分かりますが、uZuzuとuMimiは使われていません。そこだけ気を付けてください。

uniform location

 uniformをプログラム内で使うには、位置を取得し、その位置に変数をCPUサイドで格納します。位置と言っても数値ではないです。オブジェクトです。uniform locationと言います。取得の仕方はまず、アクティブなユニフォームの個数をプログラムから取得します。

  // 「アクティブな」uniformの個数
  const numActiveUniforms = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS); // 3.

 ご覧の通り、3です。つまり3つです。しかしuniformは5つ宣言されています。実は、プログラムで使われないuniformは破棄される仕様となっています。そのようなuniformはlocationが与えられず、値を放り込むこともできません。
 なお、次のように変数として用意しただけでは、アクティブになりません:

void main(){
  vec2 p = positions[gl_VertexID];
  p += uPos;
  float z = uZuzu; // 出力に寄与しないのでアウト
  gl_Position = vec4(p, 0.0, 1.0);
}

 zはuZuzuの値で初期化されていますが、gl_Positionの計算に結びついていないので、プログラム内で使われているとみなされず、非アクティブとなります。次のように寄与させればアクティブになります:

void main(){
  vec2 p = positions[gl_VertexID];
  p += uPos;
  float z = uZuzu; // 出力に寄与しないのでアウト
  gl_Position = vec4(p, z, 1.0); // これならアクティブ
}

これを最適化と言います。使わないオブジェクトを弾くことで処理を高速にするわけです。速さが命なので。

 個数が分かったので、これに基づいてロケーションを取得します。なお順番に特に意味はありませんが、バーテックス、フラグメントの順のようです。アクティブなユニフォームを取得します。

    const uniform = gl.getActiveUniform(pg, i);
    console.log(uniform);

 アクティブユニフォームの範囲内でのみ、取得が可能です。中身を見てみましょう。

location, location, location
name: "uPos", "uColor", "uAlpha"
size: 1, 1, 1
type: 35664, 35665, 5126

 面倒なので順繰りに列挙しました。こんな感じです。sizeというのは配列じゃないからですが、今回配列は扱わないので全部1というわけです。名前は宣言通り、これも配列じゃないからで...詳しくは今後説明できるかと思います。typeはgl定数で、この場合、gl.FLOAT_VEC2, gl.FLOAT_VEC3, gl.FLOATです。

型関連のgl定数
gl.FLOAT5126
gl.FLOAT_VEC235664
gl.FLOAT_VEC335665
gl.FLOAT_VEC435666

 他にもあるんですがさしあたりこれだけ。たとえばgltfの解析などで使います。とりあえずuniformsオブジェクトには名前でlocationの一覧を登録しておきます。

    const location = gl.getUniformLocation(pg, uniform.name);

    // これはuniformに含まれていないので、付与して外で使えるようにする。
    uniform.location = location;

    uniforms[uniform.name] = uniform;

uniformを使ってみる

 今回の描画はTRIANGLESで三角形を描くだけです。デフォルトでは中央、右、上の三角形で右上に描画されます。位置と色をuniformで決めようというわけです。
 float, vec2, vec3, vec4のセットに使うのはそれぞれuniform1f, uniform2f, uniform3f, uniform4fです。これらの関数は、プログラムを引数に取りません。ロケーションと、代入する変数だけです。なぜでしょうか。実は、uniformはそのときに使われているプログラムにセットされるので、プログラムが走ってないと何も起こりません。走っている間に使うことでセットされます。一度セットされると保存され、他のプログラムを走らせても破棄されることはありません。なお、新しい値をセットするには再び関数を使います。
 余談ですが、p5なんかはいわゆる再代入を防ぐため、バリデーションで色々面倒なことをやっているんですが、まあそこまで負荷のかかる処理ではないので、気楽に自由に使えばいいと思います(少なくともreadPixelsに比べたら比較にならないほど軽いです)。
 関数の使い方ですが、列挙で指定します。配列ではないので注意が必要です。

  const prepareUniform = (gl, x, y, r, g, b, a) => {
    // 列挙で入れる
    gl.uniform2f(uniforms.uPos.location, x, y);
    gl.uniform3f(uniforms.uColor.location, r, g, b);
    gl.uniform1f(uniforms.uAlpha.location, a);
  }

 このように、ロケーションの後で変数を順繰りに記述します。vec3なら3つ、floatなら1つ、順番です。これでプログラムで使われます。なおアルファ値をblendでセットして使っています。右上に通常の赤、左上にやや暗い緑、左下にさらに暗い青、ですね。

  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

  prepareUniform(gl, 0, 0, 1, 0, 0, 1); // 赤い三角形(右上)
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  prepareUniform(gl, -1, 0, 0, 1, 0, 0.5); // やや暗い緑(左上)
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  prepareUniform(gl, -1, -1, 0, 0, 1, 0.25); // かなり暗い青(左下)
  gl.drawArrays(gl.TRIANGLES, 0, 3);

  gl.disable(gl.BLEND);
  gl.flush();

 参考までに、すべてのアルファ(uAlpha)が1の場合の画像を載せておきます。

all alpha 1

 以下、いくつかの補足をします。

locationの正体

 ロケーションは謎のオブジェクトです。console.logで出しても、位置などの情報は得られません。そもそもプログラム内の位置という概念なので数値で表現することはできないのでしょう。アクティブユニフォームの通し番号はロケーションではありません。あれはあくまでただのuniformの通し番号です。ただあれが無いとuniformのロケーションを取得できないので重要です。
 今後、配列を扱う際に少しだけ理解が進みますが、それでも謎のオブジェクトであることは確かです。

uniformを登録しない場合のデフォルト

 たとえばuniform3fだけ入れないようにしてみます。

  const prepareUniform = (gl, x, y, r, g, b, a) => {
    // 列挙で入れる
    gl.uniform2f(uniforms.uPos.location, x, y);
    //gl.uniform3f(uniforms.uColor.location, r, g, b);
    gl.uniform1f(uniforms.uAlpha.location, a);
  }

  gl.useProgram(pg);
  gl.clearColor(1, 1, 1, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
// 後は同じ

 背景は白で、uniform3fだけ使わず、あとは一緒です。こうすると、こうなります:

no color setting

 すなわち黒になります。デフォルトではすべて0が使われます。特にエラーは出ません。レファレンスにもありますが、リンク時にすべて0で初期化されるので、それが使われるわけです。アトリビュートはまた事情が違うので頭にとどめといてください。

意図せず発生するダミーユニフォームについて

 この節ではダミーユニフォームの例としてuZuzuとuMimiを用意しましたが、普通にコードを書いていればダミーなんか出てくるわけないと思うでしょう。しかしテストでユニフォームの使用箇所をコメントアウトしたりするとダミーは普通に発生します:

void main(){
  vec2 p = positions[gl_VertexID];
  //p += uPos;
  gl_Position = vec4(p, 0.0, 1.0);
}

 コメントはコンパイル時に排除されるため、これでuPosもダミー(非アクティブ)になります。そのため、もし仮に何も考えずにuniformの登録処理をメソッド化していた場合、ロケーションが取得できずエラーになります。なのでメソッド化する際には、ロケーションが取得できなければ処理をキャンセルするようにしておく必要があります。

vsとfsで同じユニフォームを使うことについて

 通常はvsとfsで同じ名前のユニフォームを宣言することはありません。面倒の原因になるからです。どうしても使う場合は違う名前にして同じものを登録した方がいいです。それでも使いたい場合のために挙動を説明すると、まず異なる型で宣言した場合、コンパイルエラーになります。

const vec2[3] positions = vec2[](
  vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0)
);
uniform vec2 uPos;
uniform float uMimi; // だめ

 uMimiはvsではfloat, fsではvec4で宣言されているため、リンクに失敗し、プログラムは作られません。この場合宣言だけでアウトになるのでアクティブかどうかは無関係です。では同じ型ならいいのかというと、ありです。uAlphaをvsで宣言し、位置情報に乗算します。

#version 300 es
const vec2[3] positions = vec2[](
  vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0)
);
uniform vec2 uPos;
uniform float uZuzu;
uniform float uAlpha; // こっちでもuAlphaを宣言
void main(){
  vec2 p = positions[gl_VertexID];
  p += uPos;
  p *= uAlpha; // 位置に乗算する
  gl_Position = vec4(p, 0.0, 1.0);
}
same uniform

 無事小さくなったので、uniformは機能しています。同じ型であれば、共通の値を問題なく使えます。

 今回はこんなところで。色々変数を変えて試してみてください。また次回。

今回登場した関数

getActiveUniform
getUniformLocation
uniform[1234][fi][v]

 ...
 ......
 ...あれ、まだ続きがあるぞ...?
 zuzuとmimiについてはこちら:
ZUZUとMIMIとルビーのポケモンたち
 それと、参考までに型定数の一覧を載せておきます。

型定数一覧

型関連のgl定数
gl.BYTE5120
gl.UNSIGNED_BYTE5121
gl.SHORT5122
gl.UNSIGNED_SHORT5123
gl.INT5124
gl.UNSIGNED_INT5125
gl.FLOAT5126
gl.FLOAT_VEC235664
gl.FLOAT_VEC335665
gl.FLOAT_VEC435666
gl.INT_VEC235667
gl.INT_VEC335668
gl.INT_VEC435669
gl.BOOL35670
gl.BOOL_VEC235671
gl.BOOL_VEC335672
gl.BOOL_VEC435673
gl.FLOAT_MAT235674
gl.FLOAT_MAT335675
gl.FLOAT_MAT435676
gl.SAMPLER_2D35678
gl.SAMPLER_2D_ARRAY36289
gl.SAMPLER_CUBE35680
gl.SAMPLER_3D35679

 なぜ35677(0x8B5D)が欠番なのかというと、GL_SAMPLER_1DがWebGLに導入されなかったからです。自分も詳しくないのでそれ以上は分かりません。