uniformのメソッド化

uniform処理を簡略化

 ここでは前章でやったような共通処理のメソッド化をします。いちいちロケーションがどうのこうのってやるのははっきり言って面倒です。メソッドにしてやりやすくしましょう。
 もちろん、理解しているからこそのメソッド化です。理解が不十分なのに下手にメソッド化するとブラックボックス化してバグの温床になります。不安があったらそのたびに戻ってください。色々実験するのも助けになります。
 また、今まで恒常ループは扱ってこなかったんですが、今回初めてやります。アニメーションってやつです。

コード全文

作品16のリンク

common.jsの更新

 今回はcommon.jsが更新されているので、注目すべきはそっちですね。なおprogramにuniformsを付与する処理が追加されたことで、前回のコードにおいてはその処理が重複して実行されることになります。というかcommon.jsを用いているすべてのコードに影響が出ています...が、1章のコードはuniformを扱っていないし、前回の節も付与される内容は完全に一緒なので、特に問題はありません。むしろこの節で導入されるuniformXが非常に強力なので、これを使って前節までのコードを書きなおすのはいい練習になると思います。
 前置きはこのくらいにして、さっそく内容を見て行きましょう。

active uniformsの取得

 まずactive uniformの取得のための関数を追加しました。

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++){
    const uniform = gl.getActiveUniform(pg, i);
    const location = gl.getUniformLocation(pg, uniform.name);

    uniform.location = location;
    uniforms[uniform.name] = uniform;
  }
  return uniforms;
}

 内容的には今までやってきたことそのまんまです。activeなuniformを洗い出して、名前からロケーションを取得して登録します。それをまとめてuniformsとし、出力します...が、これを直接使うことはほぼありません。なぜならプログラムに紐付けてしまうからです。実はcreateShaderProgramをこれに伴って更新しています。こんな風に:

  if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
    console.log("programのlinkに失敗しました");
    const infoLog = gl.getProgramInfoLog(program);
    console.error(infoLog);
    return infoLog;
  }

  // uniform情報を作成時に登録してしまおう
  program.uniforms = getActiveUniforms(gl, program);

  return program;

 そういうわけなのでprogramを介してこの情報は使われます。ロケーションの記述はもう必要ありません。

uniformの登録処理

 それで、それを使ってどうやってuniformを登録するかというと、次のuniformXという関数を使うわけです。

function uniformX(gl, pg, type, name){
  const {uniforms} = pg;

  // 存在しない場合はスルー
  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]; // 通常配列でいい
    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;
    }
  }
}

 引数はコンテキスト、プログラム、使う関数の種類(type)、そしてuniform名です。ただし配列の場合は然るべく[0]を付けたりします。なお配列uniformでやったような途中のインデックスのロケーションに基づいた登録などはできません。あくまでactive uniformとして自然に取得されるもののみが対象です。まあ仕組みが分かってれば個別に対応できるので、何の問題もありませんね。
 それで、引数が4つしかないんですが、実はいわゆるvalueについては可変引数で対処しています。argsのところ...

  // nameのあとに引数を並べる。そのまま放り込む。
  const args = [...arguments].slice(4);

こうなっています。たとえば3fや4fなら列挙ですから単純に1つずつ並べます。配列の場合は配列をポンとおきます。それでOKです。行列のところですが、まあFloat32Arrayに限定してもいいんですが、不便なので通常配列でもOKとしています。ただ、内部的には32bitのfloatが使われるということだけは留意しておいてください(推奨されている理由はそれです...丸め込みと言います)。
 それと見ればわかりますがfalse限定です。trueにするオプションはありません。シンプルな方が使いやすいので。

コードの解説(使用例)

 どのように使われているのか見てみましょう。はじめに、プログラムを用意して固定uniformを設定するところまでです。

  const pg = createShaderProgram(gl, {vs, fs});

  gl.useProgram(pg);
  // 反時計回り
  uniformX(gl, pg, "2fv", "uPos[0]", [
    Math.cos(0), Math.sin(0),
    Math.cos(Math.PI*2/3), Math.sin(Math.PI*2/3),
    Math.cos(Math.PI*4/3), Math.sin(Math.PI*4/3)
  ]);
  uniformX(gl, pg, "3fv", "uCol[0]", [1, 0, 0, 0, 1, 0, 0, 0, 1]);

 三角形の頂点を反時計回りに右、左上、左下の順で用意します。色はそれぞれ赤、ライム、青。いつも通りですね。しっかりgl,pgを用意し、配列なのでそれぞれ2fv, 3fvですね。さらに名前にはきちんと[0]を付けています。最後に配列をvalueとして用意すれば完成です。いちいちlocationを用意する必要はありません。簡単!うれしいですね。
 次に恒常ループです。

  const loopFunction = () => {
    gl.clearColor(0,0,0,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const time = window.performance.now()/1000;
    // 8秒で一回転
    const angle = time*Math.PI*0.25;
    // 反時計回りに回れば合格
    const rotMat = [
      Math.cos(angle), -Math.sin(angle), Math.sin(angle), Math.cos(angle)
    ];
    uniformX(gl, pg, "matrix2fv", "uRot", rotMat);

    gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.flush();

    window.requestAnimationFrame(loopFunction);
  }

 黒でクリアした後で時間を取得してます。コードが解析されてからの経過ミリ秒です。1000で割って秒数とし、1増えた時に1/8回転するようにしたいので、回転角をMath.PI/4とします。回転行列をmatrix2fvで用意します。これは、この順でいいのはこれを列ベクトルに掛け算するところをイメージしてください。(1,0)に[0,-1,1,0]を作用させると(0,1)になりますよね?だからcos,-sin,sin,cosの順です。これで回転になります。
 最後に三角形をひとつ用意して終わりです。内部では次のような計算をしています:

#version 300 es
uniform vec2 uPos[3];
uniform mat2 uRot;
uniform vec3 uCol[3];
out vec3 vCol;
void main(){
  vec2 p = uPos[gl_VertexID];
  p *= uRot;
  vCol = uCol[gl_VertexID];
  gl_Position = vec4(p, 0.0, 1.0);
}

 特に問題なく位置変数であるpに行列uRotを掛けています。外と同じ計算なのは前回説明した通りです。簡明ですね。それで実際に反時計回りに回転しています。OKですね。説明は以上です。

おわりに

 uniformの解説が終わりました。ほんとは2x3とか4x3の行列のuniformとか、unsigned intのuniformとかを端折っているんですが、必要なことは説明し終えたと思っているので、応用的な内容については省きます。次章からはindex bufferをやります。ポリゴン描画には必須の技術ですが、どちらかというと目的はWebGLBufferの入門です。バイト列の取り扱いに慣れておかないとアトリビュートの理解で詰むからです。
 この章で触れなかったUBOもWebGLBufferの話題なので、取り扱うことができませんでした。次章以降ですね。
 全く触れないのもあれなので、不一致行列についてはこれだけ覚えてください:2x3行列は2個のvec3.行列のところでglsl内部で2x2の行列を2個のvec2で表したと思います。なんとなくわかったでしょうか。そういうことです。使う機会はおそらくほぼ無いですが、参考になれば幸いです。

テクスチャ?

 あ、テクスチャを忘れてましたね。
 いずれ取り扱うかもしれませんが、p5をやってる人ならわかるかと思いますが、glsl内部で画像データを取り扱うテクスチャと呼ばれる技術があります。あれもuniformです。というかuniformでしか扱えません。もっともあれも基本的には配列データですが...2次元化したり3次元化しているだけです。これについては特別な関数は用意されていません。実は、テクスチャuniformはuniform1iで登録する仕組みになっています。つまり単整数です。なぜ?実はテクスチャは、とある配列のような構造にひとつずつ、個別に格納する仕組みになっていて、部屋番号だけ指定するシステムなんです(テクスチャスロットといいます)。部屋番号があれば、シェーダーはそこからデータを取得できるというわけです。仕組みを考えた人は天才ですね。素晴らしい。
 補足は以上です。