はじめに
uniformの説明をします。webglの描画におけるuniformという仕様についての初歩的な説明です。基本的な解説しかしません。詳しいことは調べましょう。mdnのレファレンスは優秀で、様々なことが書いてあります。
p5.jsを引き続き間借りさせていただいています。これ便利なので。キャンバスやレンダラーを用意するのめんどくさいんですよね...ただ、いわゆるvanilla webglに移行するのは難しくないです。キャンバスとレンダラーを用意するだけなので。ただ前回の深度値の項で取り上げたように、テストが非有効で有効化してもLESSになっているなど、若干の違いはあります。
現在のプログラムによるwebgl描画では、たとえば0,3のTRIANGLESのように、0,1,2という整数に対してgl_Positionを決め、3つの座標から三角形を作り...とかやっているわけですが、これらを「頂点(vertex)」と呼びます。処理単位のことで、抽象的な言い方とも言えます。処理単位のことを「頂点」と呼んでいるわけです。実際、三角形にせよ線分や点にせよ実際に頂点の位置を決めているのでふさわしい呼び名です。
uniformとは、頂点に依らず同じ値を使う仕組みのことです。たとえば2だったらすべての頂点で2が使われます。ゆえにこれはプログラムに付随する概念です。値をセットするにも、セットした値を調べるにも、プログラムが無いと何にもできません。シェーダという呼び名が霞んでしまいそうです。結局のところ、走っているのは「プログラム」です。ただシェーダという呼び名も普及しているので自分もしばしばシェーダと呼んでいます。まあそれはいいとして...
本題に入ります。今回はプログラムが4つくらいあるので豪華ですが、描画対象は一緒なので上書きしまくりです。適当にコメントアウトして中身を覗いてみてください。
なお後編:p5.jsで生のwebglのコードを書いてuniform変数で遊ぶ(後編)
コード全文
understand uniform
長いので折りたたんでおきます
コードはこちら
// cf:シェーダーのコンパイル:https://wgld.org/d/webgl/w011.html
// uniformで遊ぼう
// boolやintや行列などは次回に回す。とりあえず基本だけ。
// 恒常ループで走らせてsetupでuniformを入れるとかも次回。
// 上向きの三角形
const vsTriangle0 =
`#version 300 es
const float TAU = 6.28318;
uniform vec3 uRed;
uniform vec3 uGreen;
uniform vec3 uBlue;
uniform float uShift;
uniform vec4 uDummyVec4; // 無視される
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec4 dummyVec4 = uDummyVec4; // これでも無視される
vec2 p = vec2(-sin(TAU*fi/3.0), cos(TAU*fi/3.0));
p.x += uShift;
vColor = (i==0 ? uRed : (i==1 ? uBlue : uGreen));
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// 配列で書こう
const vsTriangle1 =
`#version 300 es
const float TAU = 6.28318;
uniform vec3 uColors[3];
uniform float uShift;
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-sin(TAU*fi/3.0), cos(TAU*fi/3.0));
p.x += uShift;
vColor = (i==0 ? uColors[0] : (i==1 ? uColors[1] : uColors[2]));
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// 構造体で書こう
const vsTriangle2 =
`#version 300 es
const float TAU = 6.28318;
// 構造体
struct colors{
vec3 red;
vec3 green;
vec3 blue;
}; // ここのセミコロンを忘れるミスを100回くらいしました(誇張)
uniform colors uColors;
uniform float uShift;
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-sin(TAU*fi/3.0), cos(TAU*fi/3.0));
p.y += uShift;
vColor = (i==0 ? uColors.red : (i==1 ? uColors.blue : uColors.green));
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// 構造体の配列を使おう。
const vsTriangle3 =
`#version 300 es
const float TAU = 6.28318;
// 構造体
struct triangleParam{
vec2 shift;
vec3 colors[3];
};
uniform triangleParam uParams[2];
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-0.5*sin(TAU*fi/3.0), 0.5*cos(TAU*fi/3.0));
if(i < 3) {
p += uParams[0].shift;
vColor = uParams[0].colors[i];
} else {
p += uParams[1].shift;
vColor = uParams[1].colors[i-3];
}
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// fsは一緒
const fsTriangle =
`#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
uniform vec2 uDummy; // 無視される
uniform float uFactor;
void main(){
vec2 dummy = uDummy; // これでも無視される
fragColor = vec4(vColor * uFactor, 1.0);
}
`;
function setup() {
createCanvas(400, 400, WEBGL);
// 1.レンダラーの取得
const gl = this._renderer.GL;
// 2.shaderProgramの用意
const pg0 = createShaderProgram(gl, {vs:vsTriangle0, fs:fsTriangle});
// pg0のuniformにはuDummyVec4とuDummyもあるが、有効でないのでコンパイル時に
// 破棄されている。locationを取得しようとするとnullが返され、値を代入できない。
console.log(gl.getUniformLocation(pg0, "uDummyVec4"));
console.log(gl.getUniformLocation(pg0, "uDummy"));
const pg1 = createShaderProgram(gl, {vs:vsTriangle1, fs:fsTriangle});
const pg2 = createShaderProgram(gl, {vs:vsTriangle2, fs:fsTriangle});
const pg3 = createShaderProgram(gl, {vs:vsTriangle3, fs:fsTriangle});
if(pg0 === null || pg1 === null || pg2 === null || pg3 === null){
return;
}
draw0(gl, pg0);
draw1(gl, pg1);
draw2(gl, pg2);
draw3(gl, pg3);
// ここでpg0にgetUniformを使うと
// さっき入れたuniformが普通に入っていることを確認できる
gl.useProgram(pg0);
// Float32Arrayの[0,0,1]
console.log(gl.getUniform(pg0, gl.getUniformLocation(pg0, 'uBlue')));
// だから再び走らせる場合、同じ値を使うのであれば再度入れる必要はない。
// 下記のコードを実行すると、draw0と同じ結果になる。
/*
gl.clearColor(0.5, 0.5, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
*/
}
// 通常の場合
function draw0(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.5, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)
// 3fや4fは3つ、4つの数を列挙で入れる。
gl.uniform3f(uniforms.uRed.location, 1, 0, 0);
gl.uniform3f(uniforms.uGreen.location, 0, 1, 0);
gl.uniform3f(uniforms.uBlue.location, 0, 0, 1);
gl.uniform1f(uniforms.uShift.location, 0.5);
gl.uniform1f(uniforms.uFactor.location, 0.5);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
// 配列uniformの場合
function draw1(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.0, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)
// 配列で指定する場合は3fvを使う。vが付くと配列。配列の場合、末尾に[0]が付く。
gl.uniform3fv(uniforms['uColors[0]'].location, [1,0,0,0,1,0,0,0,1]);
gl.uniform1f(uniforms.uShift.location, -0.5);
gl.uniform1f(uniforms.uFactor.location, 0.5);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
// 構造体uniformの場合
function draw2(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.0, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)
// 構造体で指定する場合、ドットでアクセスする。個々の変数にアクセスした後は
// 通常と同じ。もしそれが構造体ならさらに掘り下げるだけ。
gl.uniform3f(uniforms['uColors.red'].location, 1, 0, 0);
gl.uniform3f(uniforms['uColors.green'].location, 0, 1, 0);
gl.uniform3f(uniforms['uColors.blue'].location, 0, 0, 1);
// 今回は下に動かします。
gl.uniform1f(uniforms.uShift.location, -0.5);
gl.uniform1f(uniforms.uFactor.location, 0.5);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
// 構造体配列uniformの場合
function draw3(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.0, 0.0, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)...しつこい?
// 大事なことは何回でも言います。ステートマシンと言って、そういう仕組みなのです。
// []で各構造体にアクセスした後、ここまでと同じように配列や非配列メンバに
// アクセスして値を設定する
gl.uniform2f(uniforms['uParams[0].shift'].location, -0.5, 0.5);
gl.uniform2f(uniforms['uParams[1].shift'].location, 0.5, -0.5);
gl.uniform3fv(uniforms['uParams[0].colors[0]'].location, [1,0,0,0,1,0,0,0,1]);
gl.uniform3fv(uniforms['uParams[1].colors[0]'].location, [1,1,0,1,0,1,0,1,1]);
// 今回は明るくしましょ
gl.uniform1f(uniforms.uFactor.location, 2.0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.flush();
// getUniformを使うとプログラムのuniformの値を取得できる。
// 配列の場合は[]で位置を特定したうえで名前とし、locationを取得することで
// 調べることができる。1や2を足してもダメ。整数では無いので。
// 参考:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getUniform
// Float32Arrayで[-0.5, 0.5]
console.log(gl.getUniform(pg, uniforms['uParams[0].shift'].location));
// Float32Arrayで[0.5, -0.5]
console.log(gl.getUniform(pg, uniforms['uParams[1].shift'].location));
// Float32Arrayで[1,0,0]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[0].colors[0]')));
// Float32Arrayで[0,1,0]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[0].colors[1]')));
// Float32Arrayで[0,0,1]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[0].colors[2]')));
// Float32Arrayで[1,1,0]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[1].colors[0]')));
// Float32Arrayで[1,0,1]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[1].colors[1]')));
// Float32Arrayで[0,1,1]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[1].colors[2]')));
// 2
console.log(gl.getUniform(pg, uniforms.uFactor.location));
}
function createShaderProgram(gl, params = {}){
const {vs, fs} = params;
const vsShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vsShader, vs);
gl.compileShader(vsShader);
if(!gl.getShaderParameter(vsShader, gl.COMPILE_STATUS)){
console.log("vertex shaderの作成に失敗しました");
console.error(gl.getShaderInfoLog(vsShader));
return null;
}
const fsShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fsShader, fs);
gl.compileShader(fsShader);
if(!gl.getShaderParameter(fsShader, gl.COMPILE_STATUS)){
console.log("fragment shaderの作成に失敗しました");
console.error(gl.getShaderInfoLog(fsShader));
return null;
}
const program = gl.createProgram();
gl.attachShader(program, vsShader);
gl.attachShader(program, fsShader);
gl.linkProgram(program);
if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
console.log("programのlinkに失敗しました");
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
/*
定数値。たとえば35665と出たらそれはvec3である。
参考:https://gist.github.com/szimek/763999
gl.BOOL: 35670
gl.FLOAT: 5126
gl.FLOAT_MAT2: 35674
gl.FLOAT_MAT3: 35675
gl.FLOAT_MAT4: 35676
gl.FLOAT_VEC2: 35664
gl.FLOAT_VEC3: 35665
gl.FLOAT_VEC4: 35666
gl.INT: 5124
gl.INT_VEC2: 35667
gl.INT_VEC3: 35668
gl.INT_VEC4: 35669
gl.SAMPLER_2D: 35678
gl.SAMPLER_CUBE: 35680
*/
function getActiveUniforms(gl, pg){
console.log("-------------------------------------------------------------");
const uniforms = {};
// active uniformの個数を取得。uDummyが漏れているのが分かる。
const numActiveUniforms = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS);
console.log(`active uniformの個数は${numActiveUniforms}個です`);
for(let i=0; i<numActiveUniforms; i++){
// uniformの取得に使う変数はactiveUniformの個数に応じた整数。
// たとえば4つであれば0,1,2,3で取得できる。uDummyは漏れているので出てこない。
// というか取得する方法がない。
const uniform = gl.getActiveUniform(pg, i);
console.log(uniform);
// locationはgetUniformLocationで取得できるがこれは整数ではない。内部で使われる
// オブジェクトである。この値を調べる方法もない。consoleしてもなんか変な
// メッセージが表示されるだけ。しかしuniformの登録にこのオブジェクトは必須。
const location = gl.getUniformLocation(pg, uniform.name);
//console.log(location);
// これはuniformに含まれていないので、付与して外で使えるようにする。
uniform.location = location;
// 名前で登録する。名前の仕組み...
// 配列の場合は末尾に[0]が付く(例:uColors[0])
// 実はたとえば長さ3の場合[0],[1],[2]のすべてにlocationが与えられている
// (0で初期化されているが)。そんで並んでいる。並んでいるがオブジェクトである。
// で、たとえば[2]でvec2の場合、[0]のlocationで[1,2,3,4]とすると
// 0番が(1,2)となり1番が(3,4)となるが、[1]のlocationで[1,2,3,4]とすると
// 1番が(1,2)となり0番は未定義となる(0で初期化されている)。
uniforms[uniform.name] = uniform;
// 構造体の場合は、掘り下げていってメンバのところで止まる。配列の場合、
// 0番のメンバ、1番のメンバ、という風に分かれる。たとえば
// struct hoge{vec2 hoho; vec3 gege};であればhoge[2]だとして
// hoge[0].hoho, hoge[0].gege, hoge[1].hoho, hoge[1].hohoすべて個別に設定する
// まあ当然か。なお配列...{vec4 huhu[3]}とかなってる場合であっても、
// hoge[0].huhu[0]、hoge[1].huhu[0]にそれぞれ長さ12の配列を定義する仕組みに
// なります。もしstructが入れ子なら、ドットや[]でそこにアクセスしてからさらに
// 掘り下げてstructでないメンバに行き着くまで続ける。
}
return uniforms;
}
// なお、p5でstructのメンバが配列の場合にstructの配列uniformで配列に値を入れようと
// すると失敗する。これは仕様が「size>1ならば最初の[0]で切る」としているため。
// 実はこれは「名前が[0]で終わっているなら配列認定して[0]を切る」が正解。
// 気になる人はissueでも出せばいい。需要が無いので誰もやらんだろうが。
コンソール部分
nullactive uniformの個数は5個です
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35665, name: "uRed", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35665, name: "uGreen", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35665, name: "uBlue", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uShift", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uFactor", constructor: Object}
active uniformの個数は3個です
WebGLActiveInfo {location: WebGLUniformLocation, size: 3, type: 35665, name: "uColors[0]", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uShift", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uFactor", constructor: Object}
active uniformの個数は5個です
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35665, name: "uColors.red", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35665, name: "uColors.green", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35665, name: "uColors.blue", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uShift", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uFactor", constructor: Object}
active uniformの個数は5個です
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35664, name: "uParams[0].shift", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 3, type: 35665, name: "uParams[0].colors[0]", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 35664, name: "uParams[1].shift", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 3, type: 35665, name: "uParams[1].colors[0]", constructor: Object}
WebGLActiveInfo {location: WebGLUniformLocation, size: 1, type: 5126, name: "uFactor", constructor: Object}
Float32Array {0: -0.5, 1: 0.5}
Float32Array {0: 0.5, 1: -0.5}
Float32Array {0: 1, 1: 0, 2: 0}
Float32Array {0: 0, 1: 1, 2: 0}
Float32Array {0: 0, 1: 0, 2: 1}
Float32Array {0: 1, 1: 1, 2: 0}
Float32Array {0: 1, 1: 0, 2: 1}
Float32Array {0: 0, 1: 1, 2: 1}
2
Float32Array {0: 0, 1: 0, 2: 1}
draw1
draw2
draw3
コードの簡単な説明
プログラムを4つ作ります。それらを使って順繰りに描画します。描画のたびにキャンバスを単色でクリアしているのでどんどん上書きされます。なおクリアしているのは色と深度値の両方です。おわり!
uniform location (ユニフォームロケーション)
まずdraw0から説明します。
#version 300 es
const float TAU = 6.28318;
uniform vec3 uRed;
uniform vec3 uGreen;
uniform vec3 uBlue;
uniform float uShift;
uniform vec4 uDummyVec4; // 無視される
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec4 dummyVec4 = uDummyVec4; // これでも無視される
vec2 p = vec2(-sin(TAU*fi/3.0), cos(TAU*fi/3.0));
p.x += uShift;
vColor = (i==0 ? uRed : (i==1 ? uBlue : uGreen));
gl_Position = vec4(p, 0.0, 1.0);
}
#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
uniform vec2 uDummy; // 無視される
uniform float uFactor;
void main(){
vec2 dummy = uDummy; // これでも無視される
fragColor = vec4(vColor * uFactor, 1.0);
}
内容的には、今までconstで色を指定していたところをすべてuniformで書き換えています。外部入力で色を決めるわけです。vsとfsと合わせて7つも用意しています。ここに、頂点に依らない値が代入されるわけですが、uDummyVec4とuDummyという変数が描画に寄与していないのが分かるでしょうか。
それに注意したうえで、代入の仕方を説明します。まずプログラムと変数名からロケーションというオブジェクトをゲットします。代入したいプログラムを走らせます。走っている間に(ここ重要)、走っている間に、ロケーションを使って入れたい値を放り込むと、走っているプログラムに値がセットされます。おわり。
描画時にその値が使われます!
ロケーションの取得
取得の仕方を説明します。
function getActiveUniforms(gl, pg){
const uniforms = {};
// active uniformの個数を取得。uDummyが漏れているのが分かる。
const numActiveUniforms = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS);
console.log(`active uniformの個数は${numActiveUniforms}個です`);
for(let i=0; i<numActiveUniforms; i++){
// uniformの取得に使う変数はactiveUniformの個数に応じた整数。
// たとえば4つであれば0,1,2,3で取得できる。uDummyは漏れているので出てこない。
// というか取得する方法がない。
const uniform = gl.getActiveUniform(pg, i);
console.log(uniform);
// locationはgetUniformLocationで取得できるがこれは整数ではない。内部で使われる
// オブジェクトである。この値を調べる方法もない。consoleしてもなんか変な
// メッセージが表示されるだけ。しかしuniformの登録にこのオブジェクトは必須。
const location = gl.getUniformLocation(pg, uniform.name);
//console.log(location);
// これはuniformに含まれていないので、付与して外で使えるようにする。
uniform.location = location;
uniforms[uniform.name] = uniform;
}
return uniforms;
}
見やすさのため一部のコメントをカットしました。まずアクティブユニフォームという概念について説明します。シェーダのコンパイルの際、「このユニフォーム要らねーじゃん」ってコンパイラが判断したユニフォームは破棄されます。この場合uDummyVec4とuDummyです。明らかに寄与していません。これらのuniformは存在しないものとみなされ、ロケーションが取得できません。取得にはプログラムと変数名が必要で、getUniformLocationという関数を使いますが(まんまや)、これらに対してはnullが返ります。代入できないわけです。
console.log(gl.getUniformLocation(pg0, "uDummyVec4")); // null
console.log(gl.getUniformLocation(pg0, "uDummy")); // null
使用許可の下りたユニフォームのことをアクティブユニフォームと言います。その個数を取得するためにgetProgramParameter()を使います。ACTIVE_UNIFORMSを指定すると個数を教えてくれるので、その番号でgetActiveUniformを使うと、その番号のユニフォームがオブジェクトの形で取得できます。その中にも名前の情報が入っており、それを使うことでもロケーションを取得できます。ロケーションはプログラムと変数名からダイレクトに取得することもできますが、アクティブユニフォームの変数名を使うことの利点は、アクティブと判断されたものだけを使うことで安心できるのと、uniformオブジェクトにいろいろ情報が入っていて便利だからですね。ただlocationの情報は入ってないので、こっちで付与する必要があります。
取得するたびに、uniformsというオブジェクトに名前で登録していき、それを返しています。以降は、これを使ってロケーションにアクセスし、値を代入することになります。
ロケーションって何
このロケーションですが、console.logするとわかりますが、整数ではありません。オブジェクトです。得体のしれないオブジェクトです。変数の代入先としての役割を果たしています。それで充分です。この後これを使って代入しますが、その際にこれの代わりに0や1を使うことはできません。あくまでこのロケーションという物体を使わないと、値の代入はできない仕組みになっています。アクティブユニフォームの通し番号があるじゃないかと思うかもしれませんが、厳密には異なる概念なので混同しないようにしてください。
draw0(通常のuniform)
function draw0(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.5, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)
// 3fや4fは3つ、4つの数を列挙で入れる。
gl.uniform3f(uniforms.uRed.location, 1, 0, 0);
gl.uniform3f(uniforms.uGreen.location, 0, 1, 0);
gl.uniform3f(uniforms.uBlue.location, 0, 0, 1);
gl.uniform1f(uniforms.uShift.location, 0.5);
gl.uniform1f(uniforms.uFactor.location, 0.5);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
まずdraw0です。このプログラムに出てくるユニフォームはvec3とfloatだけです。uShiftは三角形を右にずらしています。uFactorは0.5をフラグメントシェーダで色に掛けて、若干暗くしています。背景は今回暗めの金色にしました。代入に使うのはuniform1fやuniform3fという関数です。これはfloat,vec2,vec3,vec4用で、列挙することで値が入ります。第一引数はロケーションオブジェクトです。さっき作ったuniformsを使ってアクセスしていますね。これで代入され、内部ではその値が使われます。
なお当然ですが、バーテックスシェーダとフラグメントシェーダで同じ名前のユニフォームを使うとフェイタルエラーで描画できません。アクティブな場合のみこのエラーが出ます。どうしても使いたいなら名前がかぶらないようにしたうえで同じ値を個別に代入してください。
uniformのsizeとtypeについて
uniformをconsole出力するとsizeとtypeが出てきます。sizeは配列でなければ1で、配列の場合はその長さが出ます。配列uniformはこの後扱います。typeはいわゆるgl定数で、たとえば35665とはgl.FLOAT_VEC3のことです。5126はgl.FLOATです。
draw1(配列uniform)
次に、draw1の説明をします。
#version 300 es
const float TAU = 6.28318;
uniform vec3 uColors[3];
uniform float uShift;
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-sin(TAU*fi/3.0), cos(TAU*fi/3.0));
p.x += uShift;
vColor = (i==0 ? uColors[0] : (i==1 ? uColors[1] : uColors[2]));
gl_Position = vec4(p, 0.0, 1.0);
}
今回は色として長さ3のvec3の配列を使っています。意味的には0番がvec3の何か、1番がvec3の何か、2番がvec3の何かです。この場合のアクティブユニフォームの名前の一覧はどうなるかというと、コンソールを見ればわかりますが、
「'uColors[0]', 'uShift', 'uFactor'」
となります。ロケーションの取得にもこれらが使われます。つまり
gl.getUniformLocation(pg1, 'uColors[0]');
などとするわけですね。
代入の様子もさっきとちょっとだけ違います。
function draw1(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.0, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)
// 配列で指定する場合は3fvを使う。vが付くと配列。配列の場合、末尾に[0]が付く。
gl.uniform3fv(uniforms['uColors[0]'].location, [1,0,0,0,1,0,0,0,1]);
gl.uniform1f(uniforms.uShift.location, -0.5);
gl.uniform1f(uniforms.uFactor.location, 0.5);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
gl.uniform3fvというのが出てきました。これは配列uniform用の関数です。これの場合、列挙形式は取らず、第二引数は配列となります。第一引数は同じでロケーションです。ただし[0]番の、です。配列には入れたい値をフラットにずらっと並べます。赤、緑、青の順に1,0,0,0,1,0,0,0,1を並べました。これで入りますね。
ところで、このコードではuColors[0]のロケーションを使いましたが、実はuColors[1]とuColors[2]にもロケーションが設定されています。実は、
gl.uniform3fv(uniforms['uColors[0]'].location, [1,0,0,0,1,0,0,0,1]);
の代わりに、
gl.uniform3fv(gl.getUniformLocation(pg, 'uColors[0]'), [1,0,0]);
gl.uniform3fv(gl.getUniformLocation(pg, 'uColors[1]'), [0,1,0]);
gl.uniform3fv(gl.getUniformLocation(pg, 'uColors[2]'), [0,0,1]);
と書いたり、
gl.uniform3fv(gl.getUniformLocation(pg, 'uColors[0]'), [1,0,0]);
gl.uniform3fv(gl.getUniformLocation(pg, 'uColors[1]'), [0,1,0,0,0,1]);
と書いたりできます。要するにポインタをずらすみたいなことをしているわけですね。だからといってこのロケーションを足し算かなんかでずらして、とかそういうことはできません。素直に0番のロケーションに全部放り込みましょうね。せっかく配列を使ってるので。整数では無いんですが、順繰りに並んでいるような感じではあるようです。
draw2(構造体uniform)
ユニフォームと書いたりuniformと書いたり面倒ですが、サクサク行きましょう。draw2では構造体を使っています。要するにデータをオブジェクト形式でまとめちゃうやつです。便利です。
#version 300 es
const float TAU = 6.28318;
// 構造体
struct colors{
vec3 red;
vec3 green;
vec3 blue;
}; // ここのセミコロンを忘れるミスを100回くらいしました(誇張)
uniform colors uColors;
uniform float uShift;
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-sin(TAU*fi/3.0), cos(TAU*fi/3.0));
p.y += uShift;
vColor = (i==0 ? uColors.red : (i==1 ? uColors.blue : uColors.green));
gl_Position = vec4(p, 0.0, 1.0);
}
structでcolorsという構造体を定義しています。red,green,blueでいずれもvec3です。今回はこれらを使って暗めのRGB三角形を書こうというわけですね。またuShiftでは$y$座標をいじっています。どのコードも三角形の位置と大きさを微妙にずらしています。
構造体の定義の後のセミコロンを忘れないでください。エラーを吐きます。何回これでエラーを食らったか数えきれないです。
構造体の場合、名前はどうなるかというと、非常に簡単で、ドットでアクセスするだけです。
function draw2(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.0, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)
// 構造体で指定する場合、ドットでアクセスする。個々の変数にアクセスした後は
// 通常と同じ。もしそれが構造体ならさらに掘り下げるだけ。
gl.uniform3f(uniforms['uColors.red'].location, 1, 0, 0);
gl.uniform3f(uniforms['uColors.green'].location, 0, 1, 0);
gl.uniform3f(uniforms['uColors.blue'].location, 0, 0, 1);
// 今回は下に動かします。
gl.uniform1f(uniforms.uShift.location, -0.5);
gl.uniform1f(uniforms.uFactor.location, 0.5);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
このようにuColors.red, uColors.green, uColors.blueでそれぞれのvec3にアクセスします。ロケーションもこの名前でアクセスします。つまり、個別にuniformが用意されるイメージです。構造体が入れ子になっている場合は、ドットでさらに降りて行って、構造体でないメンバのところまで行きます。それらすべてに対してuniform locationが割り当てられ、変数を代入することになります。
draw3(構造体配列のuniform)
最後に、構造体配列の場合を扱います。また、メンバには配列と非配列の両方を用意します。
#version 300 es
const float TAU = 6.28318;
// 構造体
struct triangleParam{
vec2 shift;
vec3 colors[3];
};
uniform triangleParam uParams[2];
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-0.5*sin(TAU*fi/3.0), 0.5*cos(TAU*fi/3.0));
if(i < 3) {
p += uParams[0].shift;
vColor = uParams[0].colors[i];
} else {
p += uParams[1].shift;
vColor = uParams[1].colors[i-3];
}
gl_Position = vec4(p, 0.0, 1.0);
}
今回は趣向を変えてドローコールの処理単位の個数を6つにしました。0,1,2と3,4,5で三角形を作ります。また構造体はtriangleParamといって頂点の位置をvec2で、色をvec3の配列で格納しています。構造体の本領発揮です。こうしてまとめられるわけですね。ちなみに自分は構造体配列のuniformをmebiusBoxさんの物理ベースレンダリングの記事で知ってそれで理解が深まって自分のライブラリに落とし込んだりしました。そうやっていろんなコードを見ると勉強になります。
i<3かどうかで処理を分けているところで位置をずらしたり色を確定させたりしています。VertexIDは整数なので、そのまま配列の引数に使えるのが嬉しい所です。なお三角形は半分のサイズにしてあります。
値の代入については以下:(とりあえず前半だけ、後半は後で説明)
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.0, 0.0, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// ここで入れる(そのとき走ってるプログラムに入る)...しつこい?
// 大事なことは何回でも言います。ステートマシンと言って、そういう仕組みなのです。
// []で各構造体にアクセスした後、ここまでと同じように配列や非配列メンバに
// アクセスして値を設定する
gl.uniform2f(uniforms['uParams[0].shift'].location, -0.5, 0.5);
gl.uniform2f(uniforms['uParams[1].shift'].location, 0.5, -0.5);
gl.uniform3fv(uniforms['uParams[0].colors[0]'].location, [1,0,0,0,1,0,0,0,1]);
gl.uniform3fv(uniforms['uParams[1].colors[0]'].location, [1,1,0,1,0,1,0,1,1]);
// 今回は明るくしましょ
gl.uniform1f(uniforms.uFactor.location, 2.0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.flush();
構造体の配列は、たとえば長さが2の場合、つまり今ですが、0番と1番それぞれユニフォームが用意されているように考えます。個別に構造体ユニフォームが用意されているとみなしてやるわけです。あとはさっきと同じです。配列メンバであれば、draw1と同じように3fvや1fvを使って配列で代入します。もしメンバに構造体配列があれば、やはり別々とみなして、さらに掘り下げて、構造体が出てこなくなるところまで行き、同じようにやるだけです。まあレアレアケースですが。
ユニフォームへの代入の仕方についての初歩的な説明は以上です。あと追記をいくつか述べます。
uniformの中身を知る(getUniform関数)
プログラムにユニフォームをセットするには、それを走らせたうえで3fvや1fを呼ぶ必要があります。それによりセットされたユニフォームの値を知ることができます。それがgetUniform関数です。これを使う場合、プログラムとロケーションだけで良く、プログラムを走らせる必要はないです。何が走っていようとも取得できます。
draw3では描画の後でセットしたユニフォームの内容を調べています。
// Float32Arrayで[-0.5, 0.5]
console.log(gl.getUniform(pg, uniforms['uParams[0].shift'].location));
// Float32Arrayで[0.5, -0.5]
console.log(gl.getUniform(pg, uniforms['uParams[1].shift'].location));
// Float32Arrayで[1,0,0]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[0].colors[0]')));
// Float32Arrayで[0,1,0]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[0].colors[1]')));
// Float32Arrayで[0,0,1]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[0].colors[2]')));
// Float32Arrayで[1,1,0]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[1].colors[0]')));
// Float32Arrayで[1,0,1]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[1].colors[1]')));
// Float32Arrayで[0,1,1]
console.log(gl.getUniform(pg, gl.getUniformLocation(pg, 'uParams[1].colors[2]')));
// 2
console.log(gl.getUniform(pg, uniforms.uFactor.location));
参考:gl.getUniform
getUniformにプログラムとロケーションをセットするとそのロケーションの変数を取得できます。たとえばvec3の場合は長さ3のFloat32Arrayが返ったりします。何が返るかは上記のリンクを見ていただけると分かるかと思います。
なお配列の場合は個別にアクセスする必要があって、たとえば2番の値を知るには2番の変数のロケーションが必要です。ちなみに1番や2番のロケーションを0番のロケーションから知ることは「できない!」ので、個別に取得しなければなりません。配列だからと言って、長さ9の配列で返ったりはしません。そういう理由から、このコードでは個別にロケーションを取得して調べています。ちゃんと設定された値が返るのを確認できます。最後だけuFactorの値は2にしました。明るい方がいいでしょ?
uniformの設定はクリアされない
他のプログラムを走らせたからと言って、設定したuniformの値がクリアされたりはしません。そのまま残っています。それはgetUniformでも確認できますし、以下のコメントアウトを外せば、設定せずともdraw0の結果が再現されます。
// ここでpg0にgetUniformを使うと
// さっき入れたuniformが普通に入っていることを確認できる
gl.useProgram(pg0);
// Float32Arrayの[0,0,1]
console.log(gl.getUniform(pg0, gl.getUniformLocation(pg0, 'uBlue')));
// だから再び走らせる場合、同じ値を使うのであれば再度入れる必要はない。
// 下記のコードを実行すると、draw0と同じ結果になる。
/*
gl.clearColor(0.5, 0.5, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
*/
ちなみにp5では重複代入を避けるために、面倒な照合処理をして、意地でも同じuniformの代入が発生するのを避けようと四苦八苦しています。そのため非常に読みにくいコードになっています。もし独自に設定できれば、重複なんて簡単に避けられるわけですが...p5はとにかくユーザーに苦労させたくないので、そこら辺は異常に気を使っているわけです。
このように、プログラムを走らせるのはユニフォームを登録するためでもあります。描画以外にも走らせる用途は存在するということです。たとえばスケッチの中でこのユニフォームはいじらないだろうというものを事前に代入を済ませておく、などといった使い方ができます。
余談(構造体配列uniformの配列メンバ)
先ほど構造体配列のuniformを利用しましたが、実は構造体配列のuniform自体はp5でも扱えます。ただメンバに配列がある場合、うまくいきません。なぜかというと、ここでトリミングしているからです。このトリミングはサイズが1より大きい場合に実行されます。ゆえに構造体配列で名前に「[0]」が入っている場合であっても、メンバが配列でなく、サイズが1の場合は、実行されません。しかしサイズが1より大きい配列の場合は実行されます。その場合名前が「uHoge[0].hoho[0]」のようになってしまうわけですが、これに対してトリミングした結果は「uHoge」です。「uHoge[0].hoho」ではなく。なぜならindexOfは最初に見つかった照合先を返すからです。
おそらくこのアルゴリズムは「おしりの[0]をカットする」とすれば改善できるでしょう...が、レアケースであるため取り組む価値は薄いかもしれません。少なくとも自分はやりません。役に立たないなら取り組む価値は薄いです。
おわりに
以上、webglのuniformについての基本的な説明でした。後編では行列のユニフォームとか扱うかもしれないです。整数やブール値も一応扱っておきたいので。気が向いたら取り組むと思います。
ここまでお読みいただいてありがとうございました。
追記:UBOについて
uniform buffer objectという概念があって、複数のプログラムで同じユニフォームを扱う場合に便利だそうです。ここで説明したように、uniformはプログラムに付随する概念のため、異なるプログラム間で共通のuniformを扱うなどといったことはできません。それを可能にする仕組みです。いずれ説明できるかもしれませんが、ざっとこの記事で内容を見てみたところ、アトリビュートと似たような感じなので、今説明しても多分分かんないですね...いつか説明できるかもです。その時までに理解を深めておきます(正直あんま用途が思いつかないですが...)。
追記2:ダミーuniformについて
このプログラムでは、コンパイル時に弾かれるuniformを敢えて用意しました。はっきり言ってコード内ではゴミのような存在であり、わざわざ用意するものではないです(以降のプログラムにはもちろん出てきません)。それでもこれを解説したのは、そういう仕様であることを説明するためと、こういう状況は実は普通に起こりうるからです。動作確認のために特定の行をコメントアウトすると、簡単にそのuniformが寄与しない状況は発生するので、その場合にロケーションを取得するようなコードを書いていると、undefinedのエラーが発生したりします。
#version 300 es
uniform vec2 uPos[4];
uniform float uScale;
uniform vec2 uTranslate;
void main(){
vec2 p = uPos[gl_VertexID];
// 以下の2行をコメントアウトするとuScaleとuTranslateがゴミになる
p *= uScale;
p += uTranslate;
gl_Position = vec4(p, 0.0, 1.0);
}
それでもuScaleやuTranslateにデフォルト値が設定されるだけで、プログラム自体は問題なく動きます。なので、正式にはuniformの設定箇所でエラー対応を適切に行えばこの問題は回避できます。この記事ではそういう面倒を省いていますが、実装する際は気を付けるようにしてください。
実装の一例:
function setUniformValue(gl, type, uniforms, name){
// 存在しない場合はスルー
if(uniforms[name] === undefined) return;
// 存在するならlocationを取得
const location = uniforms[name].location;
// nameのあとに引数を並べる。そのまま放り込む。
const args = [...arguments].slice(4);
switch(type){
case "1f": gl.uniform1f(location, ...args); break;
case "2f": gl.uniform2f(location, ...args); break;
case "3f": gl.uniform3f(location, ...args); break;
case "4f": gl.uniform4f(location, ...args); break;
case "1fv": gl.uniform1fv(location, ...args); break;
case "2fv": gl.uniform2fv(location, ...args); break;
case "3fv": gl.uniform3fv(location, ...args); break;
case "4fv": gl.uniform4fv(location, ...args); break;
case "1i": gl.uniform1i(location, ...args); break;
case "2i": gl.uniform2i(location, ...args); break;
case "3i": gl.uniform3i(location, ...args); break;
case "4i": gl.uniform4i(location, ...args); break;
case "1iv": gl.uniform1iv(location, ...args); break;
case "2iv": gl.uniform2iv(location, ...args); break;
case "3iv": gl.uniform3iv(location, ...args); break;
case "4iv": gl.uniform4iv(location, ...args); break;
}
if(type === "matrix2fv"||type==="matrix3fv"||type==="matrix4fv"){
const v = (args[0] instanceof Float32Array ? args[0] : new Float32Array(args[0]));
switch(type){
case "matrix2fv": gl.uniformMatrix2fv(location, false, v); break;
case "matrix3fv": gl.uniformMatrix3fv(location, false, v); break;
case "matrix4fv": gl.uniformMatrix4fv(location, false, v); break;
}
}
}
gl.uniform2fなどの関数は直接渡せないので力業に頼っています。4番以降の引数にセットする引数を並べてそのままぶち込むだけです。弾かれている場合はエラーを出さず、何もしないでreturnする仕組みなので安心です。
使用例:
setUniformValue(gl, "2fv", uniforms, "uPos[0]", [-1,-1,1,-1,-1,1,1,1]);
setUniformValue(gl, "1f", uniforms, "uScale", 0.1);
setUniformValue(gl, "2f", uniforms, "uTranslate", 0.5*sin(time*0.5*TAU), 0.5);
なお行列にも対応させましたが、今後説明するので、今はスルーしてください。後で読むと意味内容が分かります。