はじめに
自分のスマホでは環境の都合上、大規模なuniformを扱うことができません。具体的には、mat4の長さ100の配列、とかです。実に64x100で6400バイトです。このサイズだとスマホではエラーが出てしまい、コンパイルエラーになります。富士通のノートパソコンでは動くんですが。
webGLはとかく環境に左右されがちです。以前直した(attributeの)参照外エラーもそうですが、機種によっては発生するエラーというのが、実際に、存在します。自分のスマホは古いのでこの手のエラーにはよく見舞われます(それでもバリバリやってる人に比べたらおそらく少ない方だとは思いますが)。そういうわけで、これを解消しようと思いました。
なんで行列の長さ100の配列なんか扱う羽目になったのかというと、スキンメッシュアニメーションをやりたかったからです。
twisted cube
const mats = [];
for(let k=0; k<bone.tfs.length; k++){
const gm = bone.mat(k, "bone"); // boneですよ!!
mats.push(...gm.array());
}
myShader.setUniform("uJointMatrices", mats);
ここのbone.tfs.lengthというのが101で、要するに長さ101のmat4の配列ですね。これをスマホで実行しようとすると次のエラーを出されます。
これはuniformで送ることのできるベクトルの個数が関係しているようです。ベクトル一つ分は16バイトですが、それの個数が次のコードで取得できます。
console.log(gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS));
それで、富士通のノートパソコンだと4096なんですがスマホでは256でしたね...で、mat4はひとつでベクトル4個分なので、これを4で割ると64です。なので64が上限だと思ったんですが調べたら60でエラーになりました。59ではセーフでした。よく分かりませんが、ともかくスマホの方が極端に上限が抑えられていることが分かりました。スマホで大量のuniformを扱うのは無理があるということです。
そこで、UBOというわけですね。
この記事ではwgldさんのありがたい記事を元に初歩的な扱い方だけを説明しようと思います。
なお、フラグメントの方ですが、こっちはノートパソコンでは1024でスマホは256でした。機種依存は大変ですね。
console.log(gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS));
サンプルは自前のプログラム生成機で作ったんですが、タイトルにある通りp5でやるという縛りを設けているのでp5で解説します。
p5のUBO導入に関するissue:
サンプルプログラムのリンク
一応3パターン用意しました。ほんとは構造体も気になるんですが、気が向いたらでいいと思うので省略しました。
UBOその1:基本編(といいつつ配列)
/*
UBO基本
wgld:https://wgld.org/d/webgl2/w009.html
webgl2sample:https://github.com/WebGLSamples/WebGL2Samples/blob/master/samples/buffer_uniform.html
*/
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
// 0にすると左上、1にすると右上
const sh = baseMaterialShader().modify({
vertexDeclarations:`
layout (std140) uniform myPosition{
vec3 pos[2];
} uPositions;
`,
'vec3 getLocalPosition': `(vec3 position) {
return position + uPositions.pos[0];
}`
});
shader(sh);
const pg = sh._glProgram;
const posLoc = gl.getUniformBlockIndex(pg, "myPosition");
gl.uniformBlockBinding(pg, posLoc, 0);
// 55をなくすとエラー
// GL_INVALID_OPERATION: It is undefined behaviour to use a uniform buffer that is too small.
// また、99と55は無視される。
const posData = new Float32Array([
-2,-2,0, 99, 1,4,0, 55
]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, buf);
gl.bufferData(gl.UNIFORM_BUFFER, posData, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, buf);
draw = () => {
background(255);
shader(sh);
lights();
fill(255);
noStroke();
sphere(40);
}
}
p5はいろいろ用意してくれるのでありがたいですね~。まずは基本です。基本と言いつつちょっとひねってもあります。vec3の長さ2の配列です。まずwgldの記事にもありますが、
layout (std140) uniform myPosition{
vec3 pos[2];
} uPositions;
layoutというおなじないが必要です。std140というのは規格で、とりあえずこれさえ書いておけば便利に使えるようです。まあ何か問題が起きたらその都度対処すればいいと思います。
このように、uniformと書いた後でuniform Blockの名前を指定します。これはブロック名であり、uniform変数とは異なるものです。肝心の変数はエイリアスのような形で最後の}のあとに記述します。uPositionsがそれですね。それで、構造体のように変数を取得します。
return position + uPositions.pos[0];
今回はsphereのattribute変数の位置変数に数値を加えて描画位置をずらすのが目的ですね。それが一番分かりやすいので。
手順を説明します。まずuniform Blockの位置を取得します。これはプログラムから取得するんですが、
createShader()の時点ではプログラムができていない
ので、まずshader(sh)でプログラムを作ります。これで_glProgramを取得するとプログラムが取得できます。なお、UBOはプログラムに変数を記録する通常のuniformとは全く仕組みが異なるので、uniformのようにプログラムを走らせる必要はありません。
shader(sh);
const pg = sh._glProgram;
const posLoc = gl.getUniformBlockIndex(pg, "myPosition");
gl.uniformBlockBinding(pg, posLoc, 0);
posLocは0です。なおこの数字は自由に決めることはできません(本来はできるらしいんですがwebgl2ではできないらしい?)、まあ決められなくても問題は無いんですが。これを、uniformBlockBindingという関数でbufferBaseというバッファ置き場の数字と紐付けます。バッファ置き場があるんですよね。その番号を指定します。attributeの仕組みが分かっている人には難しくないと思います。なお、bufferBaseはTFFにも出てきます。実は共同住宅です。あっちはデータを書き込むために、ここにバッファを置くんですが、UBOでは読み取るために置きます。なお、TFOのように配置状況を記録する仕組みは無いようです。まあなくても困らないですが...。
bufferBaseの置き場を指定してもそこにデータがなければエラーになってしまいます。データを置く流れは次の通り:
// 55をなくすとエラー
// GL_INVALID_OPERATION: It is undefined behaviour to use a uniform buffer that is too small.
// また、99と55は無視される。
const posData = new Float32Array([
-2,-2,0, 99, 1,4,0, 55
]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, buf);
gl.bufferData(gl.UNIFORM_BUFFER, posData, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, buf);
やってることはattributeの際にデータを置くのと似ていますが、あっちと違ってデータ採取の方法が単純なので内容は至ってシンプルです。読んで字のごとく、「バッファを作って、置くだけ」です。ただ注意があって、今vec3の配列なんですが、配列の場合、各変数は16バイト扱いです。そのためここではデータを32バイト分(小数8つ分)用意しています。そのため4つ目と8つ目は無視されます。また8つ目の55をカットするとデータ不足でエラーを食らいます。なおfloatが2つでも同じような感じになり小数6つ分がゴミデータになります。賢く使いましょう。
バッファの作り方に関してですが、attributeの時にARRAY_BUFFERを使ったと思うんですが今回はUNIFORM_BUFFERを指定します。あとDYNAMIC_DRAWにしていますが、UBOの場合uniformと違って楽に内容を書き換える仕組みが無いので基本的に動的更新となります。もちろん、更新する予定がないならSTATIC_DRAWでいいと思います。
つまりデータとしては0番のvec3に-2,-2,0が、1番のvec3に1,4,0が使われます。数字を変えて試してみてください。とりあえず、導入はこれで以上です。
UBOその2:複数のプログラムで使いまわす
UBOの便利な点です。uniformはプログラムに紐付けられるため、複数のプログラムで使いまわすことができません。もちろんuniformの方が仕組みは単純なので、あくまで重要なのは用途に応じた使い分けです。まあなんでもそうですが...
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
const sh0 = baseMaterialShader().modify({
vertexDeclarations:`
layout (std140) uniform myPos0{
vec3 pos;
} uPos0;
`,
'vec3 getLocalPosition':`(vec3 p){
return p + uPos0.pos;
}`
});
const sh1 = baseMaterialShader().modify({
vertexDeclarations:`
layout (std140) uniform myPos1{
vec3 pos;
} uPos1;
`,
'vec3 getLocalPosition':`(vec3 p){
return p + uPos1.pos;
}`
});
shader(sh0);
const pg0 = sh0._glProgram;
const uPos0Loc = gl.getUniformBlockIndex(pg0, "myPos0");
gl.uniformBlockBinding(pg0, uPos0Loc, 0);
// 今回は99をなくしてもエラーにならない。
// vec3とvec3[2]では挙動が違う
// 詳しくはおそらく
// https://tkaaad97.hatenablog.com/entry/2019/11/16/202505
// 配列の場合は16バイトに切り上げられるが
// vec2なら8バイトだしvec3なら12バイトのようです
// だから99をなくしても問題ないと。
const posData = new Float32Array([3,2,0,99]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, buf);
gl.bufferData(gl.UNIFORM_BUFFER, posData, gl.STATIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, buf);
// 同じuniformBufferをsh1で使うにはどうするかということ
shader(sh1);
const pg1 = sh1._glProgram;
const uPos1Loc = gl.getUniformBlockIndex(pg1, "myPos1");
// 0番にデータは入ってるので紐付ければいいだけ
gl.uniformBlockBinding(pg1, uPos1Loc, 0);
// これで使いまわしができる
draw = () => {
background(255);
//shader(sh0);
shader(sh1); // 同じ結果。
lights();
fill(255);
noStroke();
sphere(40);
}
}
今回のプログラムではvec3を1つだけ作っています。こうすると16バイトへの切り詰めは起きません。素直に12バイトです。なので、末尾の99を外してもエラーになりません。さらに削ればエラーになりますが...
pg0のためにもろもろ揃えた後で、pg1を作り、indexを取得し、bufferBaseの0番に紐付けています。プログラムのuniform BlockをbufferBaseの番号に紐付ける処理と、bufferBaseにデータを供給する処理は独立しているので、同じデータを別のプログラムが使えるわけですね。乱数テーブルとかで重宝しそうです。
ちなみに...
// 0番にデータは入ってるので紐付ければいいだけ
// gl.uniformBlockBinding(pg1, uPos1Loc, 0); // え?
コメントアウトしてもエラーは出ません。どうやら既定値が0のようで、もともと0番に紐付けられているようです。そのため0番にデータが供給されていればわざわざ指定せずともデータが使われます
が
自分としてはそんな読みにくいコードは書きたくないですね...どこのバッファを使うのかはきちんと明示すべきでしょう。省略しない方がいいと思います。
なお実行結果については省略します。位置をずらして球を置くだけですし、リンク先で確かめればいいと思います。
UBOその3:異なるデータ型を複数使う
最後に、複数の異なるデータ型を使う場合を扱います。
/*
UBO基本
wgld:https://wgld.org/d/webgl2/w009.html
webgl2sample:https://github.com/WebGLSamples/WebGL2Samples/blob/master/samples/buffer_uniform.html
*/
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
// vec2, vec2, vec3, floatで詰め詰めで並ぶわけ
const sh = baseMaterialShader().modify({
vertexDeclarations:`
layout (std140) uniform myPosition{
vec2 posA;
vec2 posB;
vec3 posC;
float invert;
vec3 posD;
vec3 posE;
} uPositions;
`,
'vec3 getLocalPosition': `(vec3 position) {
//return position + uPositions.posE;
return position + uPositions.posC * uPositions.invert;
//return position + vec3(uPositions.posB, 0.0);
}`,
fragmentDeclarations:`
layout (std140) uniform myCol{
vec3 col;
bool invert;
} uCol;
`,
'vec4 getFinalColor':`(vec4 c){
c.rgb *= (uCol.invert ? 1.0 - uCol.col : uCol.col);
return c;
}`
});
shader(sh);
const pg = sh._glProgram;
const posLoc = gl.getUniformBlockIndex(pg, "myPosition");
gl.uniformBlockBinding(pg, posLoc, 0);
// 最後のvec3, vec3についてはposDのあとパディングが追加されて16バイトとなり
// posEは一つ空けてその次の4からのフェッチとなる
// そして末尾の111はカットしても問題ない
// その前の0もカットするとさすがに怒られる
const posData = new Float32Array([
-2,-2, 3,1, -3,3,0,-1, -2,2,0,999, 4,-2,0,111
]);
const posBuf = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, posBuf);
gl.bufferData(gl.UNIFORM_BUFFER, posData, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, posBuf);
const colLoc = gl.getUniformBlockIndex(pg, "myCol");
gl.uniformBlockBinding(pg, colLoc, 1);
// boolも4バイトの小数のように扱われる
const colData = new Float32Array([
1, 0.5, 0, 1
]);
const colBuf = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, colBuf);
gl.bufferData(gl.UNIFORM_BUFFER, colData, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, colBuf);
draw = () => {
background(255);
shader(sh);
lights();
fill(255);
noStroke();
sphere(40);
}
}
このコードではまずvertexShaderの方で
vec2, vec2, vec3, float, vec3, vec3
を使っています。そこにデータを供給する場合、まず初めの4つは二つのvec2に対応します。そのあとの4つも、vec3とfloatに対応します。ここまでは自然ですが、そのあとの2つのvec3が問題です。ここは、間に4バイト分のブランクが入ります。つまりここの8つの変数については、初めの3つがまず使われ、一つ空けてそのあとの3つが使われます。そして末尾についてはカットしても問題ありません。111ですね。カット出来ます。
つまり、16バイトごとに切り分けらるようです。中途半端だとブランクができてしまうわけです。
フラグメントの方は、vec3とboolにしているんですが、floatと同じでこれも4バイト扱いです。データ的には1bitですが4バイト(32bit)...なんだかすごく、もったいない気がしますね...この記事では仕様を調べるのが目的なので、テストのために用意しました。
プログラム内でデータを扱う方法はたくさんあります。通常uniformでやるかUBOでやるか。内部で加工するか外部で加工するか。最適な選択肢は、書きたいコードにより異なるので、その都度考えましょう。
参考:
おわりに
以上、UBOの基本的な使い方でした。自分もこのやり方でしか今のところ使っていません。ほとんどの場合はプログラムにデータをダイレクトに紐付ける「uniform」で充分なんですが、最初に話したように、UBOでないと使いたいデータが多すぎてできないようなコードを書きたい場合に不便なので、そういう場合のための非常手段として用意しておくと良い事があるかもしれないですね。
ちなみにUBOで書き換えたところ、無事スマホでも動くようになりました!うれしい!!
twisted cube with UBO
ここまでお読みいただいてありがとうございました。