uniformの基礎
uniformを使ってみる

uniformの解説をします。h_doxasさんのwgld.orgを覗いてみたんですが、この記事uniformはどこで初登場かというと、なんといろいろ準備した後でさあポリゴン描画だってところで行列uniformを導入するために出現するんですね...しかもその行列も隠蔽されているので、実質的には説明ゼロで登場してます(ロケーションが必要くらいしか言ってない)。いきなり行列uniformは難易度が高いので、ここでは基礎ということで、float,vec2,vec3,vec4をやります。仕組みを理解する方がずっと重要なので。それでは参りましょう。
コード全文
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.FLOAT | 5126 |
gl.FLOAT_VEC2 | 35664 |
gl.FLOAT_VEC3 | 35665 |
gl.FLOAT_VEC4 | 35666 |
他にもあるんですがさしあたりこれだけ。たとえば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の場合の画像を載せておきます。

以下、いくつかの補足をします。
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だけ使わず、あとは一緒です。こうすると、こうなります:

すなわち黒になります。デフォルトではすべて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);
}

無事小さくなったので、uniformは機能しています。同じ型であれば、共通の値を問題なく使えます。
今回はこんなところで。色々変数を変えて試してみてください。また次回。
今回登場した関数
getActiveUniform
- リンク:getActiveUniform
- 概要:アクティブなuniformを通し番号から取得する。総数はgl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)で取得できる。
- 構文:
gl.getActiveUniform(program, index)
- 引数:
- program: 対象のWebGLProgram
- index: 該当する番号
- 返り値:WebGLActiveInfo. name, size, typeの情報が含まれる。
- 補足:typeは次の値である可能性があります。
- WebGL1まで
- gl.FLOAT
- gl.FLOAT_VEC2
- gl.FLOAT_VEC3
- gl.FLOAT_VEC4
- gl.INT
- gl.INT_VEC2
- gl.INT_VEC3
- gl.INT_VEC4
- gl.BOOL
- gl.BOOL_VEC2
- gl.BOOL_VEC3
- gl.BOOL_VEC4
- gl.FLOAT_MAT2
- gl.FLOAT_MAT3
- gl.FLOAT_MAT4
- gl.SAMPLER_2D
- gl.SAMPLER_CUBE
- WebGL2で追加
- gl.UNSIGNED_INT
- gl.UNSIGNED_INT_VEC2
- gl.UNSIGNED_INT_VEC3
- gl.UNSIGNED_INT_VEC4
- gl.FLOAT_MAT2x3
- gl.FLOAT_MAT2x4
- gl.FLOAT_MAT3x2
- gl.FLOAT_MAT3x4
- gl.FLOAT_MAT4x2
- gl.FLOAT_MAT4x3
- gl.SAMPLER_3D
- gl.SAMPLER_2D_SHADOW
- gl.SAMPLER_2D_ARRAY
- gl.SAMPLER_2D_ARRAY_SHADOW
- gl.SAMPLER_CUBE_SHADOW
- gl.INT_SAMPLER_2D
- gl.INT_SAMPLER_3D
- gl.INT_SAMPLER_CUBE
- gl.INT_SAMPLER_2D_ARRAY
- gl.UNSIGNED_INT_SAMPLER_2D
- gl.UNSIGNED_INT_SAMPLER_3D
- gl.UNSIGNED_INT_SAMPLER_CUBE
- gl.UNSIGNED_INT_SAMPLER_2D_ARRAY
- WebGL1まで
- 補足:サイトを見るとnameの規則が書いてあります。uniform buffer以外は今後説明する内容の通りです。uniform bufferについては自分も理解が浅いのでここでは触れられませんが、あれはそもそもuniform関数で値を設定しないので、必要ないかもしれません。
getUniformLocation
- リンク:getUniformLocation
- 概要:対象となるuniformのprogram内でのlocationを取得する。uniformの登録関数でこれを使う。
- 構文:
const location = gl.getUniformLocation(program, name);
- 引数:
- program: 対象のWebGLProgram
- name: 該当するuniformの名前
- 返り値:WebGLUniformLocationオブジェクト。整数ではない。
- 補足:nameはuniformの命名規則に従います。今後少しずつ説明していきます。とりあえず配列の場合は[0]を付与することだけ覚えておいてください(すぐ忘れるので)。
uniform[1234][fi][v]
- リンク:uniform
- 概要:シェーダー内のuniform変数に値をセットする
- 構文:
gl.uniform1f(location, v0); gl.uniform1fv(location, valueArray); gl.uniform1i(location, i0); gl.uniform1iv(location, iValueArray); gl.uniform2f(location, v0, v1); gl.uniform2fv(location, valueArray); gl.uniform2i(location, i0, i1); gl.uniform2iv(location, iValueArray); gl.uniform3f(location, v0, v1, v2); gl.uniform3fv(location, valueArray); gl.uniform3i(location, i0, i1, i2); gl.uniform3iv(location, iValueArray); gl.uniform4f(location, v0, v1, v2, v3); gl.uniform4fv(location, valueArray); gl.uniform4i(location, i0, i1, i2, i3); gl.uniform4iv(location, iValueArray);
- 引数:
- location: WebGLUniformLocationオブジェクト
- v0,v1,v2,v3: Number変数
- valueArray: Float32Arrayもしくは通常の配列(Array)
- i0,i1,i2,i3: Number変数の整数値
- iValueArray: Int32Array
- 返り値:なし
...
......
...あれ、まだ続きがあるぞ...?
zuzuとmimiについてはこちら:
ZUZUとMIMIとルビーのポケモンたち
それと、参考までに型定数の一覧を載せておきます。
型定数一覧
gl.BYTE | 5120 |
gl.UNSIGNED_BYTE | 5121 |
gl.SHORT | 5122 |
gl.UNSIGNED_SHORT | 5123 |
gl.INT | 5124 |
gl.UNSIGNED_INT | 5125 |
gl.FLOAT | 5126 |
gl.FLOAT_VEC2 | 35664 |
gl.FLOAT_VEC3 | 35665 |
gl.FLOAT_VEC4 | 35666 |
gl.INT_VEC2 | 35667 |
gl.INT_VEC3 | 35668 |
gl.INT_VEC4 | 35669 |
gl.BOOL | 35670 |
gl.BOOL_VEC2 | 35671 |
gl.BOOL_VEC3 | 35672 |
gl.BOOL_VEC4 | 35673 |
gl.FLOAT_MAT2 | 35674 |
gl.FLOAT_MAT3 | 35675 |
gl.FLOAT_MAT4 | 35676 |
gl.SAMPLER_2D | 35678 |
gl.SAMPLER_2D_ARRAY | 36289 |
gl.SAMPLER_CUBE | 35680 |
gl.SAMPLER_3D | 35679 |
なぜ35677(0x8B5D)が欠番なのかというと、GL_SAMPLER_1DがWebGLに導入されなかったからです。自分も詳しくないのでそれ以上は分かりません。