はじめに
uniformを扱う記事の後編です。大した情報は提供できませんが、おつきあいください。
p5.jsで生のwebglのコードを書いてuniform変数で遊ぶ(前編)
では、webglでuniformを扱う際の基本について説明しました。シェーダーに定義されたuniformで描画に寄与しないものは弾かれること、ロケーションを取得してそれにより値の代入が可能になること、代入はプログラムが走ってないと実行できないこと、代入したらその値はプログラムを切り替えても保たれること、配列や構造体の場合にロケーションを取得するための名前の設定の仕方などがポイントでした。何が言いたいかというと、その辺はもう解説しないので、気になったら前の記事を読んでください。こっちでは一切解説しません。もちろん、ダミーのuniformなどというゴミも出てきません。当然ですが。
今回は前回説明できなかったブール値、整数値、行列のuniformについての説明です。さらにテクスチャーもあり、ただテクスチャユニフォームはそもそもテクスチャを説明してないので無理です。それはしません。行列で終わりです。というか行列に関してはglsl内での行列を用いたあれこれの書き方についての説明がメインになるかもしれないです。慣れてないと割と混乱するので、この機会に押さえられればと思います。
コード全文
コードはこちら
// cf:シェーダーのコンパイル:https://wgld.org/d/webgl/w011.html
// uniformで遊ぼう
// boolやintや行列。恒常ループも扱うかも。
// 行列...
// 三角形の位置をいじろう。
// boolを使おう。
const vsTriangle_bool =
`#version 300 es
const float TAU = 6.28318;
uniform vec3 uRed;
uniform vec3 uBlue;
uniform bool uInvert;
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));
vColor = (i==0 ? uRed : uBlue);
// uInvertがtrueの場合は色反転させる
if(uInvert){ vColor = 1.0 - vColor; }
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// int配列。無理やり。整数で位置を決めちゃおう。floatにして10で割る。
const vsTriangle_int =
`#version 300 es
const float TAU = 6.28318;
uniform ivec2 uIpos[3];
uniform vec3 uColors[3];
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(float(uIpos[i].x) * 0.1, float(uIpos[i].y) * 0.1);
vColor = uColors[i];
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// 行列の挙動を理解する。答えは(0.17, -0.3)
const vsTriangle_mat =
`#version 300 es
const float TAU = 6.28318;
uniform mat2 uMat;
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-0.1*sin(TAU*fi/3.0), 0.1*cos(TAU*fi/3.0));
// 目的はこのベクトルに行列uMatを「掛けて」それをpに足すことです。
vec2 v = vec2(0.03, 0.07);
v *= uMat; p += v; // これが順当。
//p += (v * uMat); // これでもいい。
//p += (uMat * v); // 違う結果になる。(v * (uMatの転置))になる。(0.31,-0.36)
//p += (v * transpose(uMat)); // ほらね。一緒でしょ。比べてみて。やらないでね...
//あとinvertで逆行列。とにかく、右から掛けるのが基本。
// 0番が0,1成分のvec2で1番が2,3成分のvec2なのでこういう書き方もできる
//v = vec2(dot(v, uMat[0]), dot(v, uMat[1])); p += v;
vColor = vec3(1.0);
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// 行列の掛け算。答えは(0.51, -0.37)
// 2x2でやっているが、3x3でも4x4でも一緒です。
const vsTriangle_matMult =
`#version 300 es
const float TAU = 6.28318;
uniform mat2 uMat[3];
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-0.1*sin(TAU*fi/3.0), 0.1*cos(TAU*fi/3.0));
// 目的はこのvに行列uMat[0]を掛け、次いでuMat[1]を掛けて、それをpに足すこと。
vec2 v = vec2(0.03, 0.07);
v *= uMat[0]; v *= uMat[1]; p += v; // これが順当。
//p += (v * uMat[0]) * uMat[1]; // これでもOK
//p += v * (uMat[0] * uMat[1]); // これでもOK
//p += v * uMat[0] * uMat[1]; // 要するに左から2つずつ処理するのよね。でも非推奨。
// 行列への掛け算の仕様を調べよう。
//mat2 m = uMat[0]; m *= uMat[1]; v *= m; p += v; // つまり m0 *= m1;の結果はm0*m1.
// ある意味当然かもね...そして(m0*m1)を掛けるのとm0,m1の順に掛けるのは同じ。
//p += (v * uMat[2]); // これもOK. つまり掛け算の順序は逆。
//外のm1*m0がこっちのm0*m1というのは一見不合理に見えるが、
//外では v*=m のような書き方をしないので、そもそも常識の比較は不可能である。
//p += v * (uMat[1] * uMat[0]); // もちろん違う結果になる (0.01, 0.11)
//p += (uMat[0] * v) * uMat[1]; // 当然だが、期待した結果にはならない。
// 実はこれでもいい。mが(1,2,3,4)とすると、[0]でvec2(1,2),[1]でvec2(3,4)が得られる。
// 内積計算で成分を出している。結局のところ外で列ベクトルとして計算しているのと
// 同じ結果を得られるんだが、行列同士の掛け算などを考えると、転置されていると
// 考えた方が精神衛生的には良いと思う。つまり、vは、行ベクトル。
//v = vec2(dot(v, uMat[0][0]), dot(v, uMat[0][1]));
//v = vec2(dot(v, uMat[1][0]), dot(v, uMat[1][1]));
//p += v;
vColor = vec3(1.0);
gl_Position = vec4(p, 0.0, 1.0);
}
`;
// fsはシンプルに。基本的な説明は終わったので。
const fsTriangle =
`#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
fragColor = vec4(vColor, 1.0);
}
`;
// 答え合わせのために、線引き用shaderを用意する。
// 二本の線を引くだけの簡素なもの。uPos[0]-----uPos[1], uPos[2]-----uPos[3]
const vsLine =
`#version 300 es
uniform vec2 uPos[4];
void main(){
gl_Position = vec4(uPos[gl_VertexID], 0.0, 1.0);
}
`;
// 白線
const fsLine =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
`
function setup() {
createCanvas(400, 400, WEBGL);
// 1.レンダラーの取得
const gl = this._renderer.GL;
// 2.shaderProgramの用意
const pg_bool = createShaderProgram(gl, {vs:vsTriangle_bool, fs:fsTriangle});
const pg_int = createShaderProgram(gl, {vs:vsTriangle_int, fs:fsTriangle});
const pg_mat = createShaderProgram(gl, {vs:vsTriangle_mat, fs:fsTriangle});
const pg_matMult = createShaderProgram(gl, {vs:vsTriangle_matMult, fs:fsTriangle});
const pg_line = createShaderProgram(gl, {vs:vsLine, fs:fsLine});
if(pg_bool === null || pg_int === null || pg_mat === null || pg_matMult === null || pg_line === null){
return;
}
const lineUniforms = getActiveUniforms(gl, pg_line);
const linePosLoc = lineUniforms['uPos[0]'].location;
const drawLine = (posArray) => {
gl.useProgram(pg_line);
gl.uniform2fv(linePosLoc, posArray);
gl.drawArrays(gl.LINES, 0, 4);
gl.flush();
}
draw_bool(gl, pg_bool);
draw_int(gl, pg_int);
draw_mat(gl, pg_mat);
drawLine([0.17, -1, 0.17, 1, -1, -0.3, 1, -0.3]);
draw_matMult(gl, pg_matMult);
drawLine([0.51, -1, 0.51, 1, -1, -0.37, 1, -0.37]);
}
// bool
function draw_bool(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// 今回は二色。
gl.uniform3f(uniforms.uRed.location, 1, 0, 0);
gl.uniform3f(uniforms.uBlue.location, 0, 0, 1);
// bool値は整数の0,1を入れて表現するので、trueなら1,falseなら0を指定する
// のをp5はお行儀よくやっているが、実はtrue/falseでも問題なかったりする。
// ただし単整数で扱う都合上、指定には1iを使う。
// 配列の場合も1ivにして[true, false]などとすればOK
gl.uniform1i(uniforms.uInvert.location, true);
// なお35670とはgl.BOOLのことである。
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
// intの配列
function draw_int(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// 座標値(-5,5), (-5,-5), (5,5)に対して三角形を作る。
gl.uniform2iv(uniforms['uIpos[0]'].location, [-5,5, -5,-5, 5,5]);
gl.uniform3fv(uniforms['uColors[0]'].location, [1,0,0,0,1,0,0,0,1]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
// matの場合。今回はmat2. 本質的なところだけさらうつもり。3でも4でも一緒なので、
// 2で理解するのが手っ取り早い。
function draw_mat(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
const m = new Float32Array(4);
m[0] = 1;
m[1] = 2;
m[2] = 4;
m[3] = -6;
// 内部ではベクトル(0.03, 0.07)に行列[1,2,4,-6]が掛けられている
// どう掛けられるかというと
/*
(1 2) (0.03) = (0.17)
(4 -6) (0.07) = (-0.3)
*/
// こうですね。普通に横に並べて列ベクトルの計算をします。
// (0.17, -0.3)が正解。
// falseのところはレファレンス
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/uniformMatrix
// にもあるようにfalseがマストです。絶対に転置してはいけません。
gl.uniformMatrix2fv(uniforms.uMat.location, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
// matの掛け算。
// m0とm1とm1*m0を送る。m1*m0の計算はこちら:
/*
(3 0) (1 2) = ( 3 6)
(1 -1) (4 6) (-3 -4)
*/
// (m1*m0)v = m1*(m0*v)というイメージ。つまり先にm0,次いでm1が掛けられる。
// 連立方程式のイメージとかからしても、列ベクトルの方がイメージしやすいと思う。
// ぶっちゃけて言うとまあ、縦に並べたくないです。省スペースとかそういう話でもある。
function draw_matMult(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
const m = new Float32Array(12);
// m0:[1,2,4,6], m1:[3,0,1,-1] m1*m0:[3,6,-3,-4]
const sourceArray = [1,2,4,6, 3,0,1,-1, 3,6,-3,-4];
for(let i=0; i<12; i++){ m[i] = sourceArray[i]; }
/*
まず
(1 2) (0.03) = (0.17)
(4 6) (0.07) (0.54)
次に
(3 0) (0.17) = (0.51)
(1 -1) (0.54) (-0.37)
*/
// こうですね。普通に横に並べて列ベクトルの計算をします。
// (0.51, -0.37)が正解。なのでまとめてやる場合はm1*m0を掛けることになる。
gl.uniformMatrix2fv(uniforms['uMat[0]'].location, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
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の個数を取得。
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で取得できる。
const uniform = gl.getActiveUniform(pg, i);
console.log(uniform);
// locationはgetUniformLocationで取得できるがこれは整数ではない。内部で使われる
// オブジェクトである。uniformの登録にこのオブジェクトは必須。
const location = gl.getUniformLocation(pg, uniform.name);
// これはuniformに含まれていないので、付与して外で使えるようにする。
uniform.location = location;
uniforms[uniform.name] = uniform;
}
return uniforms;
}
draw_int
draw_mat
draw_matMult
boolean uniform
ブール値、というか真偽値のユニフォームです。要するにtrueまたはfalseですね。二値です。宣言するにはboolを使います。
#version 300 es
const float TAU = 6.28318;
uniform vec3 uRed;
uniform vec3 uBlue;
uniform bool uInvert;
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));
vColor = (i==0 ? uRed : uBlue);
// uInvertがtrueの場合は色反転させる
if(uInvert){ vColor = 1.0 - vColor; }
gl_Position = vec4(p, 0.0, 1.0);
}
なお、triangle系のfsは全部同じで、今回はシンプルです。vColorとして与えられる色の補間値で塗るだけです。
#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
fragColor = vec4(vColor, 1.0);
}
分かるように、uInvertという名前の値を定義しています。これがtrueの場合、色を反転させています。今回色は赤と青の二値で、これを反転させているわけです。しかしbool用の特殊な関数があるわけではないです。使うのは1iです。要するに単整数です。
// bool
function draw_bool(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// 今回は二色。
gl.uniform3f(uniforms.uRed.location, 1, 0, 0);
gl.uniform3f(uniforms.uBlue.location, 0, 0, 1);
// bool値は整数の0,1を入れて表現するので、trueなら1,falseなら0を指定する
// のをp5はお行儀よくやっているが、実はtrue/falseでも問題なかったりする。
// ただし単整数で扱う都合上、指定には1iを使う。
// 配列の場合も1ivにして[true, false]などとすればOK
gl.uniform1i(uniforms.uInvert.location, true);
// なお35670とはgl.BOOLのことである。
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
1ならtrue,0ならfalseを指定します...が、1iですから、本来は0や1を指定するのが行儀のよいやり方です。というかp5はそうしています。今回はおしゃれを優先してtrue/falseで書きました。良い子は真似しないでください。
配列の場合はもちろん1ivを使います。[1,0,0,1,0]のように指定します。これももちろん[true,false,...]という書き方ができます。好きなように書いたらいいと思います。
int uniform
サクサク行きましょう。もうやってしまった感がありますが、整数値です。ivec2やivec3とすると整数成分のベクトルになったりします。
#version 300 es
const float TAU = 6.28318;
uniform ivec2 uIpos[3];
uniform vec3 uColors[3];
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(float(uIpos[i].x) * 0.1, float(uIpos[i].y) * 0.1);
vColor = uColors[i];
gl_Position = vec4(p, 0.0, 1.0);
}
こっちのサンプルでは色は配列でいつものRGBですね。uIpos[3]というのが、ivec2が3つからなる配列です。指定の仕方は前回の f が i になっただけで全部一緒です。
// intの配列
function draw_int(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
// 座標値(-5,5), (-5,-5), (5,5)に対して三角形を作る。
gl.uniform2iv(uniforms['uIpos[0]'].location, [-5,5, -5,-5, 5,5]);
gl.uniform3fv(uniforms['uColors[0]'].location, [1,0,0,0,1,0,0,0,1]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
確かめについてはfloat化して0.1を掛けて位置としています。はっきり言って整数で送る意味がないですが、動作確認なのでこんなもんでいいでしょう。というか整数のuniformの使い道というのがあんま思いつかないのです。テクスチャのフェッチとか?
matrix uniform, 及びglslにおける行列の取り扱いについて
最後に、行列uniformについての簡単な説明をしてuniformの入門の締めくくりとします。ここでのメインはどっちかというとglslにおける行列の取り扱いの基本を学ぶことです。まず一つ目として、ベクトルに行列を掛けて値を出し、三角形の位置を決めるプログラムを走らせます。次いで、複数の行列を順にベクトルに掛けて値を出し、三角形の位置を(以下略)。
その前にちょっとした準備をします。
答え合わせ用の単に線を引くだけのプログラム
これからやることはuniformで行列を送り、それを固定したベクトルに掛けて、値を出し、三角形の位置を決めることです。その位置が可視化されていないとやりづらいので、軸に平行な二本の線分を引くために、二本の線分を引くだけの単純なプログラムを作っておきます。すなわち、
[p0x,~p0y,~p1x,~p1y,~p2x,~p2y,~p3x,~p3y]
を与えると$(p0x,~p0y)$と$(p1x,~p1y),~$及び$(p2x,~p2y)$と$(p3x,~p3y),~$の間にそれぞれ線が引かれるわけですね。
#version 300 es
uniform vec2 uPos[4];
void main(){
gl_Position = vec4(uPos[gl_VertexID], 0.0, 1.0);
}
#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
なお、ここ最近上げている記事でアトリビュートを使わずドローコールで指定されるインデックスだけで描画していますが、これは初歩的なことをやっているというよりは、アトリビュートが難しいのでそういうやり方からやってるだけです。習熟した後でも、アトリビュートを使わない簡単なプログラムを書くという選択肢は簡便法として残り続けると思いますし、そう考えるべきです。選択肢は多いに越したことはありません。
内容的には、vec2が4つ与えられるので、それに基づいて0,1,2,3番の正規化デバイス座標内の位置を決めている「だけ」です。あとはLINESで0,4とすれば0---1,2---3に線が引かれます。線の色は真っ白です。以上です。慣れた人は線の色を変えたり、グラデーションを付けてみると楽しいと思います。
const pg_line = createShaderProgram(gl, {vs:vsLine, fs:fsLine});
/* ~~~~~~~~~ */
const lineUniforms = getActiveUniforms(gl, pg_line);
const linePosLoc = lineUniforms['uPos[0]'].location;
const drawLine = (posArray) => {
gl.useProgram(pg_line);
gl.uniform2fv(linePosLoc, posArray);
gl.drawArrays(gl.LINES, 0, 4);
gl.flush();
}
実行するにはこう:
drawLine([0.17, -1, 0.17, 1, -1, -0.3, 1, -0.3]);
これで$x=0.17,~y=-0.3$の点を通り、$x$軸及び$y$軸に平行な線分が引かれます。
行列uniform(1):単純に行列を掛けるだけの場合
まず、行列を掛けるだけのプログラムで肩慣らしです。javascriptの方の処理はこちらです。この後紹介するバーテックスシェーダではuniformとしてmat2のuMatを用いています。
function draw_mat(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
const m = new Float32Array(4);
m[0] = 1;
m[1] = 2;
m[2] = 4;
m[3] = -6;
// 内部ではベクトル(0.03, 0.07)に行列[1,2,4,-6]が掛けられている
// どう掛けられるかというと
/*
(1 2) (0.03) = (0.17)
(4 -6) (0.07) = (-0.3)
*/
// こうですね。普通に横に並べて列ベクトルの計算をします。
// (0.17, -0.3)が正解。
// falseのところはレファレンス
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/uniformMatrix
// にもあるようにfalseがマストです。絶対に転置してはいけません。
gl.uniformMatrix2fv(uniforms.uMat.location, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
見ての通り、使う行列は(1,2,4,-6)です。こう並べた場合、どう並べるかとかそういう話になるわけですが、自分としてはjsの方で成分が縦ベースで並んでると気持ち悪いので、1,2,4,-6という場合は頭の中では
\begin{pmatrix}1 & 2 \\ 4 & -6\end{pmatrix}
をイメージしています。ていうか「\begin{pmatrix}1 & 2 \ 4 & -6\end{pmatrix}」と記述するわけです。その方が自然でしょう。これをベクトル$(0.03,0.07)$に掛けて点の位置を出します。
\begin{pmatrix}1 & 2 \\ 4 & -6\end{pmatrix} \begin{pmatrix}0.03 \\ 0.07\end{pmatrix} = \begin{pmatrix} 0.03+0.14 \\ 0.12-0.42 \end{pmatrix} = \begin{pmatrix} 0.17 \\ -0.3 \end{pmatrix}
行列の計算方法については既知とします。つまり横の行とベクトルで内積を取るのを成分数だけやるわけですね。こうして$(0.17,-0.3)$が得られます。三角形をこの位置に持ってきます。なお描画部分は
draw_mat(gl, pg_mat);
drawLine([0.17, -1, 0.17, 1, -1, -0.3, 1, -0.3]);
となっており、答えの点を通る座標軸に平行な二本の線が引かれるようになっています。
ここで新しい関数が出てきていますが、uniformMatrix2fvといいます。これは2fv,3fv,4fvがあります。それ以外もwebgl2だとあるんですが、使わないので解説しません。この記事の内容は正方行列ならサイズに依らず通用するもので、3x3でも、4x4でも、同じ仕様になっています。
2fvは、文字通り配列が対象です。しかしuMatは配列でないのでアクセスする際の名前に[0]は不要です。ただし行列の配列であればもちろん必要です。もう一つの注意点は、引数がFloat32Arrayでないといけないことです。他にも選択肢があるんですが、とりあえず代入値はFloat32Arrayでないと駄目ってことだけ理解すればいいです(そもそも詳しくない)。ちなみにp5では行列クラスの成分配列はこれを使っています。要するに、直接uniformとして代入できるように、です。ここに順番で成分を入れていきます。
しかし代入箇所が3番目の引数で、なぜか2番目がfalseになっています。レファレンスにもある通り、ここは必ずfalseでなければなりません。false必須です。多分昔の偉い人がそういう仕様変更をして、そのあと駄目だし食らって、後方互換性のために残されたものと思われます(想像)。とにかくfalseにしてください。
バーテックスシェーダはこちらです。さっきのベクトルを用意して、uMatを掛け、結果を p に足しています。三角形はちょっと小さくしました。
#version 300 es
const float TAU = 6.28318;
uniform mat2 uMat;
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-0.1*sin(TAU*fi/3.0), 0.1*cos(TAU*fi/3.0));
// 目的はこのベクトルに行列uMatを「掛けて」それをpに足すことです。
vec2 v = vec2(0.03, 0.07);
v *= uMat; p += v; // これが順当。
//p += (v * uMat); // これでもいい。
//p += (uMat * v); // 違う結果になる。(v * (uMatの転置))になる。(0.31,-0.36)
//p += (v * transpose(uMat)); // ほらね。一緒でしょ。比べてみて。やらないでね...
//あとinvertで逆行列。とにかく、右から掛けるのが基本。
// 0番が0,1成分のvec2で1番が2,3成分のvec2なのでこういう書き方もできる
//v = vec2(dot(v, uMat[0]), dot(v, uMat[1])); p += v;
vColor = vec3(1.0);
gl_Position = vec4(p, 0.0, 1.0);
}
そうして得られるのが冒頭3つ目の画像です。ちゃんと計算結果の三角形を線分が貫いています。基本の処理:
v *= uMat; p += v; // これが順当。
がきちんと実行されたおかげですね。この出し方が基本です(半分以上は自分が理解するために書いてます。半分以上?9割かもしれない)。
順番だけ見ると、 v にuMatを右から掛けています。さっきの、行列をベクトルに掛ける処理と逆です。あっちは列ベクトルでした。glslではこう書く方が自然ですね。だって
uMat =* v; // ?????
と書いて v が行列を掛けられた値になるところを想像できないので(もちろんこんなコードは通りません)。しかしあの順序だと v は行ベクトルのようです。実は、 v を行ベクトルとみなせば、同じ計算結果になります。
\begin{pmatrix} 0.03 & 0.07 \end{pmatrix} \begin{pmatrix} 1 & 4 \\ 2 & -6 \end{pmatrix} = \begin{pmatrix} 0.03+0.14 & 0.12-0.42 \end{pmatrix} = \begin{pmatrix} 0.17 & -0.3 \end{pmatrix}
成分は横に並べているので、やはりこの方が自然ですね。変わっているのは成分を列ベースで並べていることだけです。そこだけ。要するに、転置しています。もっとも、全体を転置しているだけで、やってる計算は外と全く同じなので、特に不自然なところはないですね。行列の書き方がちょっと違うだけです。
実際に転置していると考えると自然なことを見て行きます。
p += (v * uMat);
v にuMatを掛けたものを p に足します。何が言いたいかというと、 v にuMatを右から掛けたものは
v *= uMat;
の結果としての v と一致するということです。なお、
p += (uMat * v); // 違う結果になる。(v * (uMatの転置))になる。(0.31,-0.36)
//p += (v * transpose(uMat)); // ほらね。一緒でしょ。比べてみて。やらないでね...
実行すると分かりますが、uMatに右から v を掛けると違う結果になります。この結果は試せばわかることですが v にuMatの転置を右から掛けるのと同じ結果になります。転置というか、外での書き方と同じ書き方ですね。はっきり言って非推奨なので、glslでは常に右から行列を掛けるようにしましょう。
また、このような書き方もありです。
v = vec2(dot(v, uMat[0]), dot(v, uMat[1])); p += v;
uMat[0]とは(1,2)のことです。uMat[1]とは(4,-6)のことです。それらで内積を取って成分を決めています。要するにさっきの計算です。転置してできる行列から列ベクトルを抜き出す処理ととらえてもいいと思います。3x3や4x4でも同じです。
単独についてはこのくらいにして、本題に入ります。複数の行列の掛け算を実行する場合です。
行列uniform(2):複数の行列の掛け算を扱う場合。
// matの掛け算。
// m0とm1とm1*m0を送る。m1*m0の計算はこちら:
/*
(3 0) (1 2) = ( 3 6)
(1 -1) (4 6) (-3 -4)
*/
// (m1*m0)v = m1*(m0*v)というイメージ。つまり先にm0,次いでm1が掛けられる。
// 連立方程式のイメージとかからしても、列ベクトルの方がイメージしやすいと思う。
// ぶっちゃけて言うとまあ、縦に並べたくないです。省スペースとかそういう話でもある。
function draw_matMult(gl, pg){
// 3.uniform dataの取得
const uniforms = getActiveUniforms(gl, pg);
// 4.ドローコール(今回はgl_VertexIDを使用)
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg);
const m = new Float32Array(12);
// m0:[1,2,4,6], m1:[3,0,1,-1] m1*m0:[3,6,-3,-4]
const sourceArray = [1,2,4,6, 3,0,1,-1, 3,6,-3,-4];
for(let i=0; i<12; i++){ m[i] = sourceArray[i]; }
/*
まず
(1 2) (0.03) = (0.17)
(4 6) (0.07) (0.54)
次に
(3 0) (0.17) = (0.51)
(1 -1) (0.54) (-0.37)
*/
// こうですね。普通に横に並べて列ベクトルの計算をします。
// (0.51, -0.37)が正解。なのでまとめてやる場合はm1*m0を掛けることになる。
gl.uniformMatrix2fv(uniforms['uMat[0]'].location, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
glslサイドはこれ:
#version 300 es
const float TAU = 6.28318;
uniform mat2 uMat[3];
out vec3 vColor;
void main(){
int i = gl_VertexID;
float fi = float(i);
vec2 p = vec2(-0.1*sin(TAU*fi/3.0), 0.1*cos(TAU*fi/3.0));
// 目的はこのvに行列uMat[0]を掛け、次いでuMat[1]を掛けて、それをpに足すこと。
vec2 v = vec2(0.03, 0.07);
v *= uMat[0]; v *= uMat[1]; p += v; // これが順当。
//p += (v * uMat[0]) * uMat[1]; // これでもOK
//p += v * (uMat[0] * uMat[1]); // これでもOK
//p += v * uMat[0] * uMat[1]; // 要するに左から2つずつ処理するのよね。でも非推奨。
// 行列への掛け算の仕様を調べよう。
//mat2 m = uMat[0]; m *= uMat[1]; v *= m; p += v; // つまり m0 *= m1;の結果はm0*m1.
// ある意味当然かもね...そして(m0*m1)を掛けるのとm0,m1の順に掛けるのは同じ。
//p += (v * uMat[2]); // これもOK. つまり掛け算の順序は逆。
//外のm1*m0がこっちのm0*m1というのは一見不合理に見えるが、
//外では v*=m のような書き方をしないので、そもそも常識の比較は不可能である。
//p += v * (uMat[1] * uMat[0]); // もちろん違う結果になる (0.01, 0.11)
//p += (uMat[0] * v) * uMat[1]; // 当然だが、期待した結果にはならない。
// 実はこれでもいい。mが(1,2,3,4)とすると、[0]でvec2(1,2),[1]でvec2(3,4)が得られる。
// 内積計算で成分を出している。結局のところ外で列ベクトルとして計算しているのと
// 同じ結果を得られるんだが、行列同士の掛け算などを考えると、転置されていると
// 考えた方が精神衛生的には良いと思う。つまり、vは、行ベクトル。
//v = vec2(dot(v, uMat[0][0]), dot(v, uMat[0][1]));
//v = vec2(dot(v, uMat[1][0]), dot(v, uMat[1][1]));
//p += v;
vColor = vec3(1.0);
gl_Position = vec4(p, 0.0, 1.0);
}
今回は二つの行列を用意しています。(1,2,4,6)と(3,0,1,-1)です。処理としては、次の行列:
\begin{pmatrix} 1 & 2 \\ 4 & 6 \end{pmatrix} ,~~~~\begin{pmatrix} 3 & 0 \\ 1 & -1 \end{pmatrix}
を左の物から順に$(0.03,0.07)$に掛けて、結果を三角形の位置とするだけです。それぞれm0,m1とおくと、最終的なベクトルはm1 * (m0 * v)ですから、(m1 * m0) * vです。そこでm1 * m0も計算しておきます。
\begin{pmatrix} 3 & 0 \\ 1 & -1 \end{pmatrix}\begin{pmatrix} 1 & 2 \\ 4 & 6 \end{pmatrix} = \begin{pmatrix} 3 & 6 \\ -3 & -4 \end{pmatrix}
以上、3つの行列を送っています。行列3つからなる配列ですね。この場合も2fvを使います。長さ12のFloat32Arrayでよろしくやるだけです。
コードでもやっていますが、計算結果は$(0.51, -0.37)$です。なので、確かめるためのコードもこうなります。線を引きます。冒頭の4つ目の画像はこれで得られるものです。
draw_matMult(gl, pg_matMult);
drawLine([0.51, -1, 0.51, 1, -1, -0.37, 1, -0.37]);
一番オーソドックスなやり方は、 v に順繰りでuMat[0],uMat[1]を掛けることです。
v *= uMat[0]; v *= uMat[1]; p += v; // これが順当。
これと等価な書き方を3つ紹介します。
p += (v * uMat[0]) * uMat[1]; // これでもOK
//p += v * (uMat[0] * uMat[1]); // これでもOK
//p += v * uMat[0] * uMat[1]; // 要するに左から2つずつ処理するのよね。でも非推奨。
まず v にuMat[0]を掛け、ついでuMat[1]を掛けたものを足す方法です。次に、uMat[0]とuMat[1]をこの順序で掛け算し、それを v に右から掛けてそれを足すやり方です。同じ結果になります。最後に括弧を使わないやり方ですが、可読性が落ちるのでやめましょうね。
ここで行列の掛け算が出てきたと思いますが、ここにあるように、uMat[0]とuMat[1]を順に掛けると目的の行列になるようです。これがほぼ決定的で、要するに転置しているわけですね。
// 行列への掛け算の仕様を調べよう。
mat2 m = uMat[0]; m *= uMat[1]; v *= m; p += v; // つまり m0 *= m1;の結果はm0*m1.
// ある意味当然かもね...そして(m0*m1)を掛けるのとm0,m1の順に掛けるのは同じ。
こうも書けます。すなわちuMat[0]に右からuMat[1]を掛ける処理が、uMat[0] * uMat[1]と同じです。
p += (v * uMat[2]); // これもOK. つまり掛け算の順序は逆。
これも同じです。要するに、行列はglslに送られると転置されて、ベクトルは行とみなしてそこに右から掛ける形で考えないといけないわけです。
p += v * (uMat[1] * uMat[0]); // もちろん違う結果になる (0.01, 0.11)
//p += (uMat[0] * v) * uMat[1]; // 当然だが、期待した結果にはならない。
このようなコードを書いた場合、明らかに違う結果になることは、ここまで読んだ人ならわかるかと思います。
もちろん、列ベクトルとの内積でも出すことができます。
v = vec2(dot(v, uMat[0][0]), dot(v, uMat[0][1]));
v = vec2(dot(v, uMat[1][0]), dot(v, uMat[1][1]));
p += v;
しかし行列を掛けた方が分かりやすいでしょう。
そういうわけで転置されます。しかし一定の合理性があることは分かったと思います。コードが見やすいです。もし仮にあそこをtrueにした場合、ここまで見てきた自然な計算結果はすべて影響を受けることになります。自分としては、素直に転置されるものとして考えたいところです。
また、確かに転置されていると考えることもできるんですが、順繰りでベクトルに掛けていくということに関しては、外とやってることは何にも変わりません。掛け算のつじつま合わせのためにそういう解釈になるだけの話で、取り立てて意識することは少ないと思います。
おわりに
以上、uniformの説明でした。テクスチャについてはいつか記事を書けるかもしれないし、書かないかもしれないです。一つだけ述べておくと、テクスチャのuniform設定の際に使うのは整数です。ここでブール値の時用いた「1i」です。実はテクスチャをシェーダープログラムで使うにはそれ専用の格納倉庫(テクスチャスロット)にテクスチャを格納する必要があり、それが配列で、通し番号があるんですが、「1i」で指定するのはその番号です。
それはおいといて、行列に関していい感じに復習できて良かったです。自分的には凄く助かりました。
ここまでお読みいただいてありがとうございました。
補足:行か、列か。
この記事では行列の成分を外で行ベースのルールで並べて、glsl内部では転置しました。つまり[0,1,2,3,4,5,6,7,8]という3x3の行列mat3というのは
\begin{pmatrix}0&1&2\\3&4&5\\6&7&8\end{pmatrix}
です。そしてベクトルは列とみなしました。すなわちベクトルが(10,7,1)の場合、行列をベクトルに掛けた結果は、
\begin{pmatrix}0&1&2\\3&4&5\\6&7&8\end{pmatrix}\begin{pmatrix}10\\7\\1\end{pmatrix}
で計算されます。しかしglsl内部でこれに相当するベクトルは
\begin{pmatrix}10&7&1\end{pmatrix}\begin{pmatrix}0&3&6\\1&4&7\\2&5&8\end{pmatrix}
です。なので、もし外で成分を列ベースで並べることにして、ベクトルもすべて(例外なくすべて)行ベクトルであるとみなせば、転置していると考える必要はなくなります。そういう流儀でも問題ないです。この場合、ベクトルの関数として行列の右乗算をv.mult(M)のように定義できるなど、メリットがありそうです。
なお、どっちも行ベクトルと考える代わりにどっちも列ベクトルと考える流儀がありかと言えば、無いです。glslに合わせるなら、行列をベクトルに右から掛けることになるからです。上にも書きましたが、
M =* v; // ?????
行列をベクトルに左から掛ける記法は存在せず、右から掛けるなら行ベクトルで想定するのがマストです。
では、どっちも行ベクトルとみなすとして、どっちも行ベースで成分が並ぶように考えて扱える可能性があるかといえば、これも無いですね...上の式:
\begin{pmatrix}10&7&1\end{pmatrix}\begin{pmatrix}0&3&6\\1&4&7\\2&5&8\end{pmatrix}
におけるこの行列を意味するglslの成分記法はあくまでも(0,1,2,3,4,5,6,7,8)です。つまり列ベースです。どっちも行ベクトルとみなすなら、列ベースで考えないと駄目です。
何が言いたいかというと、次のサイトです。
mdnの行列計算情報サイト
ここに出てくるrowとは行のこと、columnとは列のことです。横と、縦。成分は、
c0r0, c1r0, c2r0, c3r0, c0r1, c1r1, ...
と並んでいます。つまり横並びです。にも拘わらず、ベクトルは行で扱うと明言されています。掛け算の結果を見ると分かりますが、第一成分に寄与しているのは0,4,8,12成分です。つまり行で並べて行ベクトルに作用させています。しかしここまで読んだ人ならわかるように、glsl内部でこの行列を意味する行列は0,4,8,12,1,5,...と成分を並べたものです。混乱します。webglに関連して取り上げるなら、いいやり方ではないです。また行列の掛け算のメソッドで引数の並び順と掛け算の並び順(上の方にコメントアウトでB * Aと書いてある)が逆です。これまた混乱します。あまり参考にしたくないところです。
いずれにせよ、自分の中できちんと流儀が定まっており、それに従って矛盾なく計算できれば何でもいいです。自分が混乱しないことこそ一番重要です。行列は、計算自体は小学生レベルの単純計算で、難しくも何ともありません。難しいのは、自分が混乱しない枠組み作りです。自分は行ベースで並んでる方が分かりやすいので、そうしているだけです。自分で考えましょう。
補足1の補足
自作ライブラリを確認したら、射影行列が列ベースなのでおかしいと思ったら、glslで堂々とベクトルに左から行列を掛けていて唖然としました。というかまあ人により流儀が異なるし、自分が混乱しなければいいんですが、数学屋の自分としては列ベクトルに行列を掛けたいのでこれはちょっと満足できないですね...原因は明らかで、まあこれを見ればわかりますがp5を真似した弊害です。行列はほんとに基本をおろそかにしてきたのでこういうことが起きてるわけです。なぜ今基礎を復習してるかというとこういうのが満足できなくなったからというのが一番の理由です。こんなぐらぐらした基礎でこれ以上コードを書くわけにはいかないからです。
さっきちらっと見たんですが、wgldの行列ライブラリなんですが、...これは、自分は嫌いですね...なぜmat1とmat2のこの順での掛け算で列ベースフェッチをしているのか...まあ人に依るんですが、これは少なくとも数学屋には受け入れられないと思います。不自然の極みなので。ただ、webGLは数学育ちの人しか使ってないわけではないわけです。システムさえできてしまえば、動くなら詳細はどうでもいい、そういう立場に立てば疑問や迷いも生じないものですから、こういうことは往々にしてあるのでしょう。自分は無理ですが、各々が個人的に好きなものを選んだり、自作すればいいと思います。
自分だってついさっき、というか今朝、行列をナチュラルに列ベースで扱っていることに気づいたくらいなので。振り返るって大事ですね。
補足2:恒常ループにおけるuniform
結局、恒常ループのコードを紹介する余裕がなくなってしまいました。行列大変だからね、仕方ないね。コードだけ載せておくので、適当に読み解いてください。ざっくりいうと位置決めとスケールだけ常に一緒なので1回だけセット、平行移動は毎フレーム実行するので毎フレームセットしています。millis()/1000で秒数を取得するのはp5.jsプログラミングの常套手段なので、特に解説する必要はないですね。
let loopFunction;
function setup() {
createCanvas(400, 400, WEBGL);
const vsLoop =
`#version 300 es
uniform vec2 uPos[4];
uniform float uScale;
uniform vec2 uTranslate;
uniform mat2 uMat;
void main(){
vec2 p = uPos[gl_VertexID];
p *= uScale;
p += uTranslate;
p *= uMat;
gl_Position = vec4(p, 0.0, 1.0);
}
`;
const fsLoop =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
`;
const gl = this._renderer.GL;
const pg = createShaderProgram(gl, {vs:vsLoop, fs:fsLoop});
const uniforms = getActiveUniforms(gl, pg);
// 今回走らせるのはこれだけなので先に起動させてしまう
gl.useProgram(pg);
// 頂点位置とスケールはいじらないので最初に1回だけuniformセット
setUniformValue(gl, "2fv", uniforms, "uPos[0]", [-1,-1,1,-1,-1,1,1,1]);
setUniformValue(gl, "1f", uniforms, "uScale", 0.1);
loopFunction = (time) => {
// 平行移動は毎フレーム実行するので恒常ループで実行する
setUniformValue(gl, "2f", uniforms, "uTranslate", 0.5*sin(time*0.5*TAU), 0.5);
// 行列も使おうか。反時計回りの回転。
setUniformValue(gl, "matrix2fv", uniforms, "uMat", [cos(time), -sin(time), sin(time), cos(time)]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.flush();
}
}
function draw() {
const gl = this._renderer.GL;
gl.clearColor(0.3, 0.3, 0.4, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
// 引数は秒数。millis()/1000で秒数を取得するのはp5の常套手段。
loopFunction(millis()/1000);
}
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;
}
function getActiveUniforms(gl, pg){
const uniforms = {};
// active uniformの個数を取得。
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で取得できる。
const uniform = gl.getActiveUniform(pg, i);
console.log(uniform);
// locationはgetUniformLocationで取得できるがこれは整数ではない。内部で使われる
// オブジェクトである。uniformの登録にこのオブジェクトは必須。
const location = gl.getUniformLocation(pg, uniform.name);
// これはuniformに含まれていないので、付与して外で使えるようにする。
uniform.location = location;
uniforms[uniform.name] = uniform;
}
return uniforms;
}
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;
}
}
}
ダミーuniformが発生してもいいようにちょっと書き換えました。