様々なアトリビュート

いろんな型があるよ

 この節ではいろんな型のアトリビュートを扱います。ここまでの記事ではfloat,vec2,vec3,vec4しか扱ってきませんでした。これらはすべて小数です(色であっても)。通常はそれしか使わないんですが、他にもあるよっていう話です。たとえばスキンメッシュアニメーションなどでは頂点ごとに関連する行列のインデックスを整数アトリビュートで指定したりします。ずっとお世話になってきたgl_VertexIDも実は整数アトリビュートです。また行列アトリビュート、まああんま使いませんが、そういうのもあるよというお話です。
 言ってしまうと、int, ivec2, ivec3, ivec4があり、uint, uvec2, uvec3, uvec4があり、行列としてはmat2, mat3, mat4があり、あと不一致(mat2x3とか)も全部あります。それ以外は知らない、というかおそらく無いはずです...まあ一通り触れましょう。

コード全文

作品26のリンク

整数アトリビュート

 検索:vertexAttribIPointer
 もしかして: vertexAttribPointer
 違います。私が検索したのはvertexAttrib「I」Pointerです。間違えないでください。AIのくせに...
 まあそれはさておき、整数アトリビュートです。宣言するには、32bit符号付き整数であればint, ivec2, ivec3, ivec4を使います。32bit非負整数であれば、uint, uvec2, uvec3, uvec4を使います。今回のコードではuintだけ使うことにしました。他のはまあ、気が向いたら使ってみてください(用途が分からないので)。
 今までは小数アトリビュートしか使ってきませんでした。まあこの後紹介する行列も小数アトリビュートの一種なんですが、それはとりあえずおいておきます。整数とスロットにセットされた用途指定から特定の値がフェッチにより生成され、それはバーテックスステージに行くんですが、その際にそのまま32bitFloatにキャスティングされるということです。今回はそこが違って、要するに32bitの符号付き整数や、32bitの非負整数に変換されるということです。なのでもちろんFLOATなどの「型」は使えません。整数タイプのみです。バッファの登録の仕方が独特ですが、先ほど述べたvertexAttrib「I」PointerをvertexAttribPointerの代わりに使うとこ以外は完全に同じです。なので気楽に学んでください。
 なお検索するときちんとこっちのPointerが一番上に来ます。安心してください。ヒットします。

vertexAttrib「I」Pointer

 今回はuniformで色配列を送って、アトリビュートの非負整数で色を取得する仕組みとしました。

#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in uint aColorIndex;
uniform vec3 uColor[4];
out vec3 vColor;
void main(){
  vec2 p = aPosition;
  vColor = uColor[aColorIndex];
  gl_Position = vec4(p, 0.0, 1.0);
}

 非負整数なので配列のindexなどに普通に使えます。なおなぜ4なのかというと、実はこの後で既定値を「3」にして、indexが3の時の値としてすべての色をデフォルト値にしようと画策しているんでそうなっています。とりあえず無視してください。登録方法を紹介します。

  const iBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, iBuf);
  gl.bufferData(gl.ARRAY_BUFFER, iData, gl.STATIC_DRAW);
  gl.vertexAttribIPointer(1, 1, gl.UNSIGNED_BYTE, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

 こんな風です。vertexAttribIPointerは、vertexAttribPointerと何が違うかというと、型指定のバリエーションとnormalizedの有無が違います。まず使えるtypeは8,16,32bitの符号付きないしは非負整数のみです(全部で6つ)。また小数にするわけではないためnormalizedは存在しません。そのままキャスティングされます。引数はvertexAttribPointerのnormalizedを省いただけの5つからなり順番も完全に一緒です。あっちのPointerを書き慣れた人ならすらすら書けるでしょう。特に説明は要らないですね。

 注意すべきはtypeの指定です。スロットはプログラムのことを知らないですから、使われ方がまずいと駄目出しをしてきます。floatの時はそんなことなかったんですが今回は別です。実は、typeは宣言型と同じ種類でなければならないという裏ルールがあって、宣言型が非負整数ならtypeも非負整数(3種類)でなければならず、宣言型が符号付き整数ならtypeも符号付き整数(3種類)でなければなりません。つまり、今回uintですから、次のやり方だとエラーになります:

  const iBuf = gl.createBuffer();
  const iOtherData = new Int8Array([0,1,2]);
  gl.bindBuffer(gl.ARRAY_BUFFER, iBuf);
  gl.bufferData(gl.ARRAY_BUFFER, iOtherData, gl.STATIC_DRAW);
  gl.vertexAttribIPointer(1, 1, gl.BYTE, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
GL_INVALID_OPERATION: glDrawArrays: Vertex shader input type does not match the type of the bound vertex attribute.

 なおこのケースの場合Int8Arrayで作っていますがそこは問題ではなく、gl.BYTEの方が問題です。ここをUNSIGNED_BYTEにすることでこのバグを回避できます。普通に描画されます。でもまあuintにデータを供給するならUint8Arrayの方が無難でしょうね。
 さらに注意として、当然ですが宣言型が小数の場合もエラーになります。それに関連して既定値のことを話したいので、もうちょっとお付き合いください。

integer attrの既定値

 アトリビュートには既定値(カレント)があることを前回話したんですが、それはFloat32Array([0,0,0,1])です。小数です。これを差し替えるにはvertexAttribを使うんですがそれは小数型の場合です。整数型の既定値を使うにはvertexAttribIという、これまた紛らわしい名前の関数を使います。
 検索:vertexAttribI
 次の検索結果を表示しています: vertex AttribUTE
 ぽんこつめ...ぽんこつめ...仕方ないので
 検索:vertexAttribI mdn
 これで上位にヒットします。困ったら半角スペースとmdnを入れればどんな関数名でも大体ヒットするのでおすすめです。調べると分かりますが全部で4つあり、符号付き整数の場合はvertexAttribI4i, vertexAttribI4ivを使います。非負整数の場合は、vertexAttribI4ui, vertexAttribI4uivを使います。違いは列挙か配列かだけで用途は完全に一緒です。あっちと違って引数は常に4つ分要るのが面倒なところです。今回は1つ目しか要らないのでそれ以外はデタラメにしました:

      // 非負整数値なのでvertexAttribIの4uiを使う
      // 4つずつでしかいじれない仕組み
      gl.vertexAttribI4ui(1, 3, 9, 9, 9);
      // これでもいい
      //gl.vertexAttribI4uiv(1, [3, 9, 9, 9]);

 オプションのdisableをチェックすると1番スロットは使えなくなり、既定値の3が使われ、3番にはコンフィグで入れた色が入っているので、その色で単色になる仕組みです。まあ地味ですが、挙動を確認したいだけなので、ご了承ください。
 ここでgetVertexAttribを使ってみます。

  console.log(gl.getVertexAttrib(1,gl.CURRENT_VERTEX_ATTRIB));
  console.log(gl.getVertexAttrib(1,gl.VERTEX_ATTRIB_ARRAY_INTEGER)); // true.

 vertexAttribIを使ってからこれを起動させると、普通にUint32Arrayが返ります。なお2つ目はvertexAttribIPointerで登録した場合にtrueになります。今まで扱ってきたスロットでは軒並みfalseになっていたものです。
 つまり、vertexAttribIで定めるとカレントは符号付き整数になったり非負整数になったりするわけです。これが、場合によっては問題になったりします。些細な内容なので、コードは載せず、お話にとどめます。

カレントとdisable

 既定値は常にFloat32Arrayがデフォルトです。すべてのスロットのカレントがそうなっています。ところで宣言型が整数の場合、データは整数で供給しないといけません。すなわちvertexAttirbIPointerです。なのでこれを使って供給します。しかし事情によりこのデータを使わず、使わないのでスロットをdisableにより閉じるとします。供給される場合は良かったんですが、供給されない場合でもdisableの場合は既定値が供給されてしまいます。その値はデフォルトのFloat32Arrayです。もうわかったと思いますが、宣言型が整数なので、エラーになります。描画不履行です。
 どういうことかというと、コメントアウトすると...

    if(config.disable){
      gl.disableVertexAttribArray(1);
      // 非負整数値なのでvertexAttribIの4uiを使う
      // 4つずつでしかいじれない仕組み
      //gl.vertexAttribI4ui(1, 3, 9, 9, 9); // ⇐コメントアウト
      // これでもいい
      //gl.vertexAttribI4uiv(1, [3, 9, 9, 9]);
    }
GL_INVALID_OPERATION: glDrawArrays: Vertex shader input type does not match the type of the bound vertex attribute.

 整数型で登録してるんだからデフォルトも整数になってもよさそうなものですが、融通が利かないんですね...。とはいえ、整数のアトリビュートを用意しておきながら、それを使用しないケースというのはおそらくあんまないので、まあ特に気にしなくても大丈夫でしょう。以上、補足でした。

 じゃあ次は行列ですね...KaTeX用意しなきゃ。 \[ \begin{pmatrix} 2 & 3 & 1 \\[5pt] 4 & 5 & 7 \\[5pt] 6 & 7 & 10 \end{pmatrix} \]  いいですね。じゃあ説明に入ります。

行列アトリビュート

 行列アトリビュートの設定の仕方っていったいいつ学ぶんでしょうね...自分の場合は、興味を持った時にそういうの無いかなと調べてたら、Qiitaの記事がたまたまヒットしたので覚えようと思ったんですが、その頃のアトリビュートの知識では理解できなかったので断念しました。wgldもやってないし。まあ使う機会もあんまなさそうなので仕方ないんですが。前回vertexAttribを紹介しましたがそのレファレンスでちょっとだけ触れています。ポイントはスロットの利用です。ざっくりいうと、サイズに応じて連番で複数使用します。それでは、きちんと説明しようと思います。

 あとさっきの記事ですが、いわゆる列ベースを採用していますね,..宗派が合わない...まあWebGLって数学屋じゃない人でも普通に扱ってるんで、仕方ないと言えば仕方ないですね。自分が混乱しなければ問題ありません。自分は数学と相性が合わないので嫌いですね。

コード全文

作品27のリンク

mat2x3のアトリビュート

 正方行列でもいいんですが、今回採用するのはmat2x3です。いわゆる、不一致行列です。これはどう解釈するかというと、uniformの時にちらっと言及したかもしれませんが、mat mxnは「m個のvec n」です。つまりmat2x3は2個のvec3をまとめたものです。vec3が横で縦に2本並んでいるところを想像してください。そういうわけなので、3次元のベクトルに作用して、2次元のベクトルを生成します。 \[ \begin{pmatrix} 1 & 3 & -2 \\[5pt] 4 & 1 & 6 \end{pmatrix} \begin{pmatrix} 2 \\[5pt] 5 \\[5pt] 3 \end{pmatrix} = \begin{pmatrix} 11 \\[5pt] 31 \end{pmatrix} \] つまりmat2x3の行列は、vec3に作用して、vec2を生成します。なのでシェーダーでもそのように書いています:

#version 300 es
layout (location = 0) in mat2x3 aMove;
layout (location = 2) in vec3 aColor; // locationが1だとエラーになる
out vec3 vColor;
void main(){
  vec2 p = vec2(0.5, 0.0);
  p = vec3(p, 1.0) * aMove;
  vColor = aColor;
  gl_Position = vec4(p, 0.0, 1.0);
}

 行列の項で説明したように、vec2のpから作るvec3にmat2x3のアトリビュートであるaMoveを堂々と右から掛けています。自分は行列に関して思考停止する必要は無いので、はっきりとした理解で堂々とコードを書きます。ここでやってる計算は、内容的にはaMoveに相当する行列を3次元の列ベクトルであるvec3(p, 1.0)に左から掛け算するのと同じ処理です。ですから結果はvec2で、それを再びpに代入しています。
 これも復習ですが、aMove[0]とaMove[1]はそれぞれvec3で、1行目と2行目になっています。とても分かりやすいですね。行列はベクトルをまとめたものであるということができます。これらと計算対象の列ベクトルで内積を取る、という見方もできます。
 ところでロケーションが0と2ですね...。別にひねくれてはいません。これについて触れましょうね。

行列attrのロケーション

 行列アトリビュートmat mxnのロケーションは、まあ方法は何でもいいんですが、ひとつ指定されたとします。locとします。このとき、同時にloc, loc+1, ..., loc+m-1が自動的に予約されます。つまりm個のロケーションを同時に使います。ぜいたく!これは何の数かというと、行列の行の数です。つまり行ごとに異なるスロットを使うわけです。スロットは16個しかないのに連番です。ぜいたくですね...
 当然ですが、ロケーションをかぶらせるとエラーを食らいます:

layout (location = 0) in mat2x3 aMove;
layout (location = 1) in vec3 aColor;
Attribute 'aColor' aliases attribute 'aMove' at location 1

 すなわち1はaMoveにより予約されているわけです。他の方法でロケーションをかぶらせた場合も一緒です。なおオート設定の場合も、きちんと避けるように連番で設定されます。かしこいですね!当然ですが、取得できるのは一番小さい番号なので、そこからの連番となります。

行列の解説

 今回使う行列ですが、こんな感じですね。色も用意しました。今回は青っぽくしました。

  // 回転させて、あとは0.5, -0.25だけ平行移動する。
  // 作るのは右を向いた正三角形です
  const angle = Math.PI*2/3;
  const mData = new Float32Array([
    Math.cos(0), -Math.sin(0), 0.5,
    Math.sin(0), Math.cos(0), -0.25,
    Math.cos(angle), -Math.sin(angle), 0.5,
    Math.sin(angle), Math.cos(angle), -0.25,
    Math.cos(angle*2), -Math.sin(angle*2), 0.5,
    Math.sin(angle*2), Math.cos(angle*2), -0.25,
  ]);
  const cData = new Uint8Array([
    0, 255, 255, 0, 128, 255, 0, 0, 255
  ]);

 行列ですが、3個分用意されています。これが3次元のベクトル(x,y,1.0)に作用するところを想像してください。目的は2次元のベクトル(x,y)の変換です。左側の2x2部分の行列は回転行列で、これにより回転します。そのあとでz成分の1.0が寄与して平行移動を実現します。つまりこれは、指定した角度だけ回転したうえで、そのあと平行移動させる処理です。それを3つの頂点の分だけ用意しようというわけです。それにより正三角形を作ります。数式で書くと、こう... \[ \begin{pmatrix} a & b & p \\[5pt] c & d & q \end{pmatrix} \begin{pmatrix} x \\[5pt] y \\[5pt] 1 \end{pmatrix} = \begin{pmatrix} a & b \\[5pt] c & d \end{pmatrix} \begin{pmatrix} x \\[5pt] y \end{pmatrix} + \begin{pmatrix} p \\[5pt] q \end{pmatrix} \]  うまくいけばこれにより3つの行列ができて、それらがvec3(0.5, 0.0, 1.0)に作用して、欲しい3本の2次元ベクトルが手に入ります。そして冒頭の画像になります。早速登録してみましょう。

  const mBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, mBuf);
  gl.bufferData(gl.ARRAY_BUFFER, mData, gl.STATIC_DRAW);
  // 行ごとに異なるスロットを予約しているので、
  // 然るべく用途を指定する
  // 行列が行ごとに分解できるのは行列の項でやったと思います
  // 不安な場合は 2-5:行列入門 を読み返しましょう
  gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);

 4バイトの小数が個々の行列ごとに6つあるのでstrideは24です。1行12バイトなので、オフセットを12にすることで、0番は1行目フェッチ、1番は2行目フェッチになります。つまり0番で1行目を管理して、1番で2行目を管理するわけですね。多分この先、インターリーブで似たようなことをすると思うので、慣れておいてください。

余談:mxn行列

 数学のmxn行列の定義は、これと全く同じです。すなわち、m個の成分数がnの行ベクトルです。縦にm行って横にnです。この定義は整合性があって、これによりmxn行列とnxl行列を順に右に向かって並べるとその積がmxl行列になります。分かりやすいですね。
 なんていうか...あれですね。行列が列ベクトルに作用しているのを見ると安心しますね。数学屋だからでしょうか。それを実感しています。

次回予告

 次はエレメント描画をアトリビュートでやってみましょう。基礎編はそれで終わりです。

今回登場した関数

vertexAttribIPointer
vertexAttribI[1234][u]i[v]