1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

p5.jsで生のwebglを使いながらアトリビュートで遊ぶ(2)(補足編)

Last updated at Posted at 2025-01-23

はじめに

 アトリビュートはやたらと話題が豊富なので、多分あと3つか4つくらい記事を書くと思います。だってdivisorがなんなのか気になるでしょ?あとまあいろいろとね...いろいろ、ね。

 (相変わらず自分の理解のために書いています。昨日の自分は他人なので、未来の自分がこれ以上webGLのバグで苦しまないように書いています。自作ライブラリを運用する上で変なバグが目立ってきたので基礎を見直そうと思って書いています。わからないことが、まだまだたくさんあります。)

 引き続きp5.jsです。あのねぇ、恒常ループとかキャンバスの生成とかめんどくさい...まあ作るだけなら簡単ではあるんだけどそれだけ作っても仕方ないし、それしか作んないならp5.jsの枠組みを間借りした方が手っ取り早いんですよね。Threeとかじゃこうはいかないので。すごく便利だと思います。あとはレンダラーのセッティングが自分好みという理由もあります。

 今回は前回の基礎編の補足です。あれだけだとイメージがつかみづらいと思うので、はみだしたことをいろいろやって基礎を固めておこうと思います。そりゃ基本的にはFloat32で作ってgl.FLOATを指定すればいいんですが、その程度の理解じゃ面白くなくなってしまったというのが本音です。付き合いたい人だけ、お付き合いください。

 一応簡単に復習します。グローバルステート。

  globalState = {
    currentProgram: null,
    bindingBuffer: {
      arrayBuffer: null,
      elementArrayBuffer: null,
      ...
    },
    vertexAttributeArray: [
      {enable: false, arrayBuffer: null, layout: undefined, divisor: 0, ...},
      {}, {}, ...
    ],
    ...
  }

divisorのあとに...としましたが、無いかもしれません。まあ知らないです。あったら困るので付けました。
 ドローコールはdrawArraysやdrawElementsでインデックス配列を計算し、それとプリミティブの構築ルーチンに従って点や、線や、三角形を作ります(そういえば点をやってませんがいずれ、いずれやります...あれこれいっぺんにはできないので)。その際頂点の位置を正規化デバイス座標上で決定するんですが、その決定に使うデータをインデックスから取得する仕組みがアトリビュートです。ロケーションが決まっており、上記のvertexAttributeArrayの何番から取得するかをそれで指定します。そして計算されたインデックスとレイアウトからデータのフェッチを実行します。2つとか3つの数ができるわけです。それはバーテックスシェーダ内ではFloat32となって使われます。いままではgl_VertexIDという、まあインデックスそのまんましか使えなかったんですが、この仕組みによりあらかじめ用意したデータをバイト列の形でGPUサイドに格納し、それを使えるわけですね。以上です。これでわかりにくいようなら前回の記事をご覧ください...
 今回はsetup()以外全部一緒なので、先にコードを挙げておきます。その内容についても前回説明したので、繰り返すことはしません。

コード全文...ではなく一部省略

function setup(){
  createCanvas(400, 400, WEBGL);
  const gl = this._renderer.GL;

  /* いろいろ、いろいろ */
}

function createShaderProgram(gl, params = {}){

  const {vs, fs, layout = {}} = 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);

  // レイアウト指定はアタッチしてからリンクするまでにやらないと機能しない。
  // なおこの機能はwebgl1でも使うことができる。webgl2で実装されたというのは誤解。
  setAttributeLayout(gl, program, layout);
  
  gl.linkProgram(program);

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

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

  return program;
}

// レイアウトの指定。各attributeを配列のどれで使うか決める。
// 指定しない場合はデフォルト値が使われる。基本的には通しで0,1,2,...と付く。
function setAttributeLayout(gl, pg, layout = {}){
  const names = Object.keys(layout);
  if(names.length === 0) return;

  for(const name of names){
    const index = layout[name];
    gl.bindAttribLocation(pg, index, name);
  }
}

function getActiveUniforms(gl, pg){
  /* 省略 */
}

function getActiveAttributes(gl, pg){
  const attributes = {};

  // active attributeの個数を取得。
  const numActiveAttributes = gl.getProgramParameter(pg, gl.ACTIVE_ATTRIBUTES);
  console.log(`active attributeの個数は${numActiveAttributes}個です`);

  for(let i=0; i<numActiveAttributes; i++){
    // 取得は難しくない。uniformと似てる。
    const attribute = gl.getActiveAttrib(pg, i);
    console.log(attribute);
    // 似てるのはここまで。
  
    // このlocationは整数である。そこがuniformとの違い。
    // なぜならこの整数はprogramには関係ないので。
    // vertexAttributeArrayという隠蔽されたグローバルステート内の配列の中の、
    // programがこのattributeを使いたい部屋の通し番号である。
    // (その部屋にはデータと扱い方が書かれた紙が置いてあり、フェッチして使う。)
    // それゆえ、他のprogramが同じ番号を指定した場合、部屋の取り合いが起きる。
    // もっとも一度に機能するProgramは1つだけだから、譲り合えばいいだけの話。
    const location = gl.getAttribLocation(pg, attribute.name);

    // 例によってlocationは含まれていないのでここで登録。
    attribute.location = location;
    // uniformの場合はデータがprogramに属するが、attributeの場合データは外部の
    // バッファに置いてあり、それを参照する形なので、そこが決定的に異なる。
    // locationはそのデータ置き場のindexである。

    attributes[attribute.name] = attribute;
  }

  return attributes;
}

function setUniformValue(gl, pg, type, name){
  /* 省略 */
}

実行結果:いろいろ。

vertexAttribPointerのnormalize引数について

 まずvertexAttribPointerについて省略したところからですね。

  gl.vertexAttribPointer(index, size, type, normalize, stride, offset);

やったぁ引数暗記した。まあこれだけ何回も呼び出してれば覚えるんよね。これはどういう関数かというと、グローバルステートのbindingBufferのarrayBuffer枠を、同じくグローバルステート内のvertexAttributeArrayのindex番に紐付ける、さらに整数が与えられた場合にそこからtypeの型の値を計算するための仕様書を、ついでに貼り付ける処理です。その値はFloat32となって、バーテックスシェーダで使われます。

どうでもいい説明用13.png

 前回言い忘れたんですが、strideもtypeのバイト長の整数倍でないとエラーになります。注意してください。たとえばFLOATで4バイトなら4の倍数でないと駄目です。またstride,offset共に負の数はアウトです。

 本題ですが、normalizeは、そうやってできた数をシェーダーへ送る際に加工するかどうかを決めるための引数です。
 vertexAttribute normalize

function setup() {
  createCanvas(400, 400, WEBGL);
  const gl = this._renderer.GL;

  const vs =
  `#version 300 es
  in vec2 aPosition; // 位置!です!
  in vec3 aColor;
  out vec3 vColor;
  void main(){
    vColor = aColor;
    vec2 p = aPosition;
    // サイズを半分にする
    gl_Position = vec4(p*0.5, 0.0, 1.0);
  }
  `;

  const fs =
  `#version 300 es
  precision highp float;
  in vec3 vColor;
  out vec4 fragColor;
  void main(){
    fragColor = vec4(vColor, 1.0);
  }
  `;

  // レイアウト指定。aPositionは0を使います。
  // 1が使いたいなら1でもいいですよ。
  const pg = createShaderProgram(gl, {
    vs:vs, fs:fs, layout:{aPosition:0, aColor:1}
  });

  const location_position = pg.attributes.aPosition.location;
  const location_color = pg.attributes.aColor.location;

  // バッファを用意

  // 位置のバッファ
  const buf_position = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf_position);
  const positionData = new Int8Array([-128,-128,127,-128,127,127]);
  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_position, 2, gl.BYTE, true, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  // 色のバッファ
  const buf_color = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf_color);
  const colorData = new Uint8Array([255, 192, 32, 32, 255, 192, 192, 32, 255]);
  gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_color, 3, gl.UNSIGNED_BYTE, true, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  
  // 該当するattributeの有効化
  gl.enableVertexAttribArray(location_position);
  gl.enableVertexAttribArray(location_color);

  // じゃあ描画しようか。
  gl.useProgram(pg);

  gl.clearColor(0.2, 0.2, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

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

  gl.flush();
  // お疲れ様でした。はや。
}

実行結果:

test0.png

 今回は2つのアトリビュートを決めています。位置と色でそれぞれ0,1番を「こっちで明示して」使います。vertexShaderでは位置の値を0.5倍しているのと、あと色のアトリビュート(vec3)をvaryingでフラグメントシェーダに渡していますね。それが補間されて、こんな感じになるので、そういう風に決めていると思われます。しかし、それらのデータは割と大きな値で指定されています。

  const positionData = new Int8Array([-128,-128,127,-128,127,127]);
  /* ~~~~~ */
  const colorData = new Uint8Array([255, 192, 32, 32, 255, 192, 192, 32, 255]);

にも関わらず、使われている値は割と小さいようです。
 これを実現するのがnormalizeです。まず位置データですが、これはBYTE(符号付き8bit整数)で定義しています。その数の範囲は-128~127です。実はnormalizeがtrueの場合、この値は-1~1に正規化されます。なお、-128と-127は共に-1.0になることが実験でわかるので、127で割ったのち、-1~1にクランプしているようです。同じように、色はUNSIGNED_BYTE(符号なし8bit整数)で定義されていますが、数の範囲は0~255です。これも、normalizeがtrueだと0~1になります。やはり、255で割っているようです。具体的にはこうです。

  • gl.BYTE: -128~-127 → -1~1
  • gl.SHORT: -32768~32767 → -1~1
  • gl.UNSIGNED_BYTE: 0~255 → 0~1
  • gl.UNSIGNED_SHORT: 0~65535 → 0~1
  • gl.FLOAT, gl.HALF_FLOAT: 効果なし

 効果があるのは整数の場合だけです。説明は以上です。たとえば色情報は0~255で指定することもちょいちょいあるので、便利な人は便利かもしれないです。
 当然ですが、色の方でnormalize:falseとすると値がクランプされて真っ白になります。気を付けて...

ewgeg4e444.png

 (補足:normalizeなんて何に使うのかと思うでしょう。もちろんp5もこんなことやってません。色でも、UVでも、何でもかんでもFLOATです。入れるのも、読みだすのも、常にFLOATなので、何の問題も起きないです...が、このねこいりねこさんのコードではそれが使われています。
 ねこいりねこさんのシェーダーデモ
一部抜粋:

  if ('a_Position' in program) {
  if(isRectBuffer) console.log("usePosition");
    gl.enableVertexAttribArray(program.a_Position);
    gl.vertexAttribPointer(program.a_Position, 3, gl.SHORT, true, 2 * 8, 2 * 0);
  }
  if ('a_Curvature' in program) {
    gl.enableVertexAttribArray(program.a_Curvature);
    gl.vertexAttribPointer(program.a_Curvature, 1, gl.SHORT, true, 2 * 8, 2 * 3);
  }

というかこのコードはほぼすべての情報(頂点とか法線とか)をSHORT(符号付き16bit整数)で格納して、normalizeして使っています。ライブラリのくびきに縛られないコードではこういうのが割と、あります。そういうのを読み解けるようになるには、この手の知識があると便利です。バイト単位で情報を管理しているのも見て取れるでしょうか。そういうことをやっています。
 ちなみにp5はこの手の処理をenableAttrib()という関数でやっています。これには引数が6つありますが、ほとんどデフォルト値しか使われておらず、gl.FLOATとnormalize=falseとstride,offsetが0で固定されています。それが一番わかりやすいですからね。もっと言うと自分のライブラリもそうなっています。こっちはもう使わねぇだろってことで、デフォルトどころか完全固定です。そういうわけで基礎から復習しているわけです。何が言いたいかというと、柔軟性が、欲しい。
 これなんかもインターリーブ使っていて興味深いです。
 buffer_uniform.html
このサイトのサンプルはwebgl2を深く理解するうえで非常に参考になるので、おすすめです。wgldがいい加減に済ませているところを深く突っ込みたい人におすすめです。)

同じデータを複数の頂点アトリビュートに供給できることについて

 文字通りの意味です。複数の頂点アトリビュートは、バッファを共有できます。たとえば
 WebGLRenderingContext.vertexAttribPointer()
このサンプルでもやっているように、頂点データ、法線データ、UVデータを頂点ごとにまとめて作ることができたりします。フェッチの仕組みを考えれば明らかですね。

どうでもいい説明用11.png

いわゆるインターリーブですが、そのうち別で取り上げるのでこれはおいといて、とりあえず今回は頂点データを流用して色を付けたいと思います。ただそのまま加工するのでは面白くないので、せっかくstrideやoffsetについての知識があるので、バイト単位で採取して色を付けたいと思います。

 use same buffer

// やってみよう。アトリビュート。
// 同じバッファを使う。0,128,0,179,93,191,215,179,93
// 参考:単精度浮動小数点数
// https://ja.wikipedia.org/wiki/%E5%8D%98%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0

function setup() {
  createCanvas(400, 400, WEBGL);
  const gl = this._renderer.GL;

  const vs =
  `#version 300 es
  in vec2 aPosition; // 位置!です!
  in vec3 aColor;
  out vec3 vColor;
  // テスト用
  const vec3[3] colors = vec3[](
    vec3(0.0, 128.0, 0.0), vec3(179.0, 93.0, 191.0), vec3(215.0, 179.0, 93.0)
  );
  void main(){
    vec3 color = colors[gl_VertexID];
    color /= 255.0;
    vColor = aColor;
    //vColor = color;
    vec2 p = aPosition;
    gl_Position = vec4(p, 0.0, 1.0);
  }
  `;

  const fs =
  `#version 300 es
  precision highp float;
  in vec3 vColor;
  out vec4 fragColor;
  void main(){
    fragColor = vec4(vColor, 1.0);
  }
  `;

  // レイアウト指定。aPositionは0を使います。
  // 1が使いたいなら1でもいいですよ。
  const pg = createShaderProgram(gl, {
    vs:vs, fs:fs, layout:{aPosition:0, aColor:1}
  });

  const location_position = pg.attributes.aPosition.location;
  const location_color = pg.attributes.aColor.location;

  // バッファを用意

  // 位置のバッファ
  const buf_position = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf_position);
  const positionData = new Float32Array([
    -sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_position, 2, gl.FLOAT, false, 0, 0);

  // 色もこれでやる
  gl.vertexAttribPointer(location_color, 3, gl.UNSIGNED_BYTE, true, 7, 2);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  // DataViewを使う。
  // 正しければ2,3,4, 9,10,11, 16,17,18 番目が使われてるはず
  const siteArray = [2,3,4,9,10,11,16,17,18];
  const ab = new ArrayBuffer(24);
  const dv = new DataView(ab);
  for(let i=0; i<6; i++){
    dv.setFloat32(i*4, positionData[i], true);
  }
  console.log("バイトデータ");
  for(let i=0; i<24; i++){console.log(dv.getUint8(i).toString(16));}
  console.log("色データ");
  for(let i=0; i<9; i++){
    console.log(dv.getUint8(siteArray[i]));
  }
  // 0,128,0,179,93,191,215,179,93
  
  // 該当するattributeの有効化
  gl.enableVertexAttribArray(location_position);
  gl.enableVertexAttribArray(location_color);

  // じゃあ描画しようか。
  gl.useProgram(pg);

  gl.clearColor(0.2, 0.2, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

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

  gl.flush();
  // お疲れ様でした。はや。
}

実行結果:

use same buffer.png

今回位置データはいつもの三角形です。その直後に、色についても割り当てを実行しています。

  const positionData = new Float32Array([
    -sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_position, 2, gl.FLOAT, false, 0, 0);

  // 色もこれでやる
  gl.vertexAttribPointer(location_color, 3, gl.UNSIGNED_BYTE, true, 7, 2);

符号なしバイトで正規化、strideは7でoffsetは2です。つまり7ずつとびとびでフェッチ、加えて2ずつずらしています。位置データは24バイトなのでちゃんと収まっていますね。シェーダを見れば分かりますが色はvec3です。ゆえにsizeは3です。これでどういう色が取得できるかというと、まあこんな感じ:

どうでもいい説明用12.png

位置データはリトルエンディアンで4バイトずつ格納されています。ちなみに単精度浮動小数点数の流儀に従えばこれらの変換を実行できるんですが、
 参考:単精度浮動小数点数
はっきりいってそんなまだるっこしいことはやってられないので、素直にDataViewを使って確かめています。

  // DataViewを使う。
  // 正しければ2,3,4, 9,10,11, 16,17,18 番目が使われてるはず
  const siteArray = [2,3,4,9,10,11,16,17,18];
  const ab = new ArrayBuffer(24);
  const dv = new DataView(ab);
  for(let i=0; i<6; i++){
    dv.setFloat32(i*4, positionData[i], true);
  }
  console.log("バイトデータ");
  for(let i=0; i<24; i++){console.log(dv.getUint8(i).toString(16));}
  console.log("色データ");
  for(let i=0; i<9; i++){
    console.log(dv.getUint8(siteArray[i]));
  }
  // 0,128,0,179,93,191,215,179,93

 まず24バイト分のArrayBufferを作り、DataViewを生成します。これでArrayBufferへの読み込みと書き込みが可能になります。次にpositionDataの各成分を4バイトのオフセットで放り込みます...このときtrueを指定しないとリトルエンディアンになんないので注意してください。あとはgetUint8で1バイトずつ読み出すだけです。楽ちん!素晴らしい...
 色に該当する部分を抜き出すと、
 0,128,0,179,93,191,215,179,93
となります。つまり色としてはRGBで[0,128,0],[179,93,191],[215,179,93]が使われているんですが、ホンマかと思うでしょう。なので確かめています。

vs
#version 300 es
in vec2 aPosition; // 位置!です!
in vec3 aColor;
out vec3 vColor;
// テスト用
const vec3[3] colors = vec3[](
  vec3(0.0, 128.0, 0.0), vec3(179.0, 93.0, 191.0), vec3(215.0, 179.0, 93.0)
);
void main(){
  vec3 color = colors[gl_VertexID];
  color /= 255.0;
  vColor = aColor;
  //vColor = color;
  vec2 p = aPosition;
  gl_Position = vec4(p, 0.0, 1.0);
}

ダイレクトに全く同じ色を用意しました。いちいちバッファを作るのは手間なので。コメントアウトを外してみてください。一緒でしょ。そういうことです。バイト単位でデータを取得している様子が、少しでも伝わったなら幸いです。

データのフェッチにおいてインデックスがはみ出した場合の挙動について

 さっきの例では無事フェッチの範囲が24バイト以内に収まりましたが、はみだしたらやばいですね。じつははみ出した場合の挙動は処理系に依存します。とりあえずコード...
 Enabled Vertex Attributes and Range Checking


/* 省略 */

  const vs =
  `#version 300 es
  in vec2 aPosition; // 位置!です!
  in vec3 aColor;
  out vec3 vColor;
  void main(){
    vColor = aColor;
    vec2 p = aPosition;
    gl_Position = vec4(p, 0.0, 1.0);
  }
  `;

/* 省略 */

  // 位置のバッファ
  const buf_position = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf_position);
  const positionData = new Float32Array([
    -sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_position, 2, gl.FLOAT, false, 0, 0);

  // 色もこれでやる
  // 7,8,9, 16,17,18, 25,26,27
  gl.vertexAttribPointer(location_color, 3, gl.UNSIGNED_BYTE, true, 9, 7);

実行結果(?)

fumme.png

strideが9でoffsetは7です。なお言い忘れましたがstrideはtypeのバイト長の倍数しか許されていません。今strideで好き勝手出来るのはtypeのバイト長が1だからです。9と7ですから、24バイトだと普通にはみ出します。

de3r3f3rf3.png

はみ出した場合の挙動についてはここに記述があります。
 6.6 Enabled Vertex Attributes and Range Checking
 enableVertexAttribPointerでvertexAttributeArrayのアトリビュートが有効化されていれば、プログラムはそこからデータを採取しようとします。もちろんそのロケーションのデータを、です。で、フェッチのたびにはみ出しのチェックをするんですが、その際にはみ出しが起きると、まあ書いてあるようにざっと3通りですかね...

  • 描画不履行(draw no geometry)
  • バッファのどっかから好きにデータを持ってくる
  • 素直に0かなんかで埋める

3番目の頂点が真っ黒なのを見て取れると思いますが、これはデータのフェッチに失敗したので黒を返しているわけです。これは自分の富士通のノートパソコンで見た場合の結果です。しかし自分のスマホは違う結果を返します。

efrf3f33g3.png

 見ての通り、描画不履行です。このように、処理系によって挙動が異なるわけです。最もこういうことは起きないのが普通だと思いますが、実はある状況では起きなくもないんです。というかp5はずっとこれを許してきた経緯があります。
 うまく説明できるかわかりませんが、やってみましょう。

p5のジオメトリーに関する機種依存バグ

 詳しくは:On my android phone sometimes objects in sketches drawn with webgl in p5.js disappear #5968
 p5の描画機構は、今説明に出てきているバッファの一連のセットを取り扱う仕組みになっており、描画時に使用するvertexAttributeArrayのスロットを必要なだけ有効化する仕組みになっています。今それを使ってplaneを描画したとします。頂点は4つしかない、小さいジオメトリーです。それには頂点と法線とUVが入っています。それぞれ0,1,2番のスロットを使用するとします。

fewf3tr3.png

その際に使用するArrayBufferもすべて上書きされるんですね。グローバル状態の上書きです。で、そのあと傘を描画しようと思ったんですね。p5にはp5.Geometryという独自にジオメトリを作る仕組みがあるんですが、その際UVを利用するかどうかは自由です。今回は別にUV彩色をしたくなかったので、用意しませんでした。その結果、描画の際にUVに関してはグローバルデータ、具体的には2番スロットの内容の上書きはされませんでした。データが無いのだから、当然ですね。

 しかし2番スロットは有効になっているし、使うプログラムにおいてUVのアトリビュートは死んでないので、当然そこからデータをフェッチしようとします。頂点の個数に応じて必要なだけ。しかしplaneがせいぜい4つしかないのに対して傘のジオメトリの頂点の個数は3000以上です。当然、はみ出します。とはいえUV彩色しているわけでは無いので、描画結果には全く影響はありません。そう、描画不履行でもされない限り...

 もうわかったと思いますが、自分のスマホは描画不履行を選んだので、消えてしまったんですね。

 これを回避するには簡単で、傘の描画時にシャッターを閉めればOKです。つまり

  gl.disableVertexAttribArray(2);

 たった一行、これを実行するだけです。で、planeの描画時に開ければいいだけ。

  gl.enableVertexAttribArray(2);

 シャッターが閉まっていれば、機種に依らずデフォルトの値が使われるので、機種依存のバグを回避できます。Threeなんかはこれをきちんとやっていたんですが、p5はやっていませんでした。なので、やるように要請しました。それが上に挙げたプルリクエストの内容です。説明は以上です。思い出話に付き合ってくださってありがとうございました。おわり。

 (これも裏話があって、たとえばUVを使わないにしても、0埋めなどしてダミーでいいから頂点の個数だけ大量に、使いもしないデータを用意すればいいわけです。はっきり言います。嫌です。そんなめんどくさいことをユーザーに要求するより関数使ってシャッター閉めた方が手っ取り早いじゃないですか。それにThreeはそうしているわけです。0埋めを提案されたんですが、disableはThreeも使ってると話したら認めてもらえました。やっぱり要求を通すなら虎の威を借るのが一番手っ取り早いですね。)

enableプロパティがfalseの場合に使われる値について

 というわけで、disableVertexAttribArrayでシャッターを閉めると既定の値が使われます。ちょうどいいのでそれを実験してみましょう。というか

  // 該当するattributeの有効化
  gl.enableVertexAttribArray(location_position);
  //gl.enableVertexAttribArray(location_color);

そもそもenableしなければいい。結果は:

ewd2r3.png

真っ黒です。スマホでも真っ黒になります。はみ出しているかどうかは無関係です。そもそもフェッチが実行されないからです。既定の値のデフォルトは0,0,0,1で、これらが順繰りに入ります。たとえばvec4で宣言して第四成分だけ使うと真っ白になったりします。
 この場合の値、すなわち既定値を変更する方法があります。vertexAttribといいます。
 WebGLRenderingContext: vertexAttrib
これにより水色を指定してみます。

  // 該当するattributeの有効化
  gl.enableVertexAttribArray(location_position);
  //gl.enableVertexAttribArray(location_color);
  gl.vertexAttrib4f(location_color,0,0.5,1,1);

結果は:

fef33fvgf4g4.png

こんな風に色が付きます。もちろんすべて既定値になるので単色です。デフォルトの設定を変更したいときに重宝します。配列で指定することもできます。

  gl.vertexAttrib4fv(location_color,new Float32Array([0,0.5,1,1]));

レファレンスにはFloat32使えって書いてあるんですが、なぜか次のコードも動くんですよね...

  gl.vertexAttrib4fv(location_color, [0, 0.5, 1, 1]);

we2wf2f22f2.png

まあいいや。好きな方を使えばいいと思います。補足はこれで以上です、お疲れ様でした。

おわりに

 アトリビュートでデータを用意するのはwebGLの描画の基本です。3Dはすべてこれを使ってデータを用意してやっています。整数フェッチでは限界があるので...そういうわけで、きちんと学んだ方がいいです。
 ここまでお読みいただいてありがとうございました。

 ああ、もうちょっと補足したいですね...忘れてた。整数アトリビュートと、size不一致について触れる必要があります。読みたい人だけ付き合ってください。

補足追記1:sizeとシェーダーで宣言した変数のサイズが不一致の場合について

 たとえばvec3で宣言したのにsizeが4の場合。sizeの方が大きいのであれば、この場合はsizeの個数の小数が、小さい方から数えて3つ順繰りに入るだけです。sizeの方がでかい場合の挙動はそんな感じです。
 では逆にsizeの方が小さい場合は?これは実験してみたんですが、どうやら0,0,0,1がそのまま該当する箇所を埋めるように入るようです。たとえばvec4で宣言してsizeが2の場合、フェッチして2,3が得られたとすると残りは既定の値から取って2,3,0,1となるようです。で、この値はスロットの既定値から取られると:
 WebGLRenderingContext.vertexAttribPointer()
ここに書いてあり、vertexAttrib(上で紹介したやつ)でいじれると書いてあるんですが、これ実験したんですがいくらやっても0,0,0,1のままでしたね...disableの場合の既定値はいじられるんですが、はみ出した場合の既定値はなぜかいじれませんでした。まあ特に問題ないですし、違う結果が得られた人は報告よろしくお願いします。

 はい、次。さっさと基礎編を終わらせたい...

補足追記2:整数アトリビュート

 たとえばgl_VertexIDが整数アトリビュートの一例です。そういうのをユーザーが使えるようにしようという仕組みです。調べたんですがちょっと厄介ですねこれ...
 integer attribute

function setup() {
  createCanvas(400, 400, WEBGL);
  const gl = this._renderer.GL;

  const vs =
  `#version 300 es
  uniform vec2 uPositions[3];
  uniform vec3 uColors[3];
  in uvec2 aIndex; // 整数
  out vec3 vColor;
  void main(){
    vColor = uColors[aIndex.y];
    gl_Position = vec4(uPositions[aIndex.x], 0.0, 1.0);
  }
  `;

  const fs =
  `#version 300 es
  precision highp float;
  in vec3 vColor;
  out vec4 fragColor;
  void main(){
    fragColor = vec4(vColor, 1.0);
  }
  `;

  // レイアウト指定。aPositionは0を使います。
  // 1が使いたいなら1でもいいですよ。
  const pg = createShaderProgram(gl, {
    vs:vs, fs:fs, layout:{aIndex:0}
  });

  const location_index = pg.attributes.aIndex.location;

  // バッファを用意
  const buf_index = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf_index);
  const data_index = new Uint8Array([0,1, 1,2, 2,0]);
  gl.bufferData(gl.ARRAY_BUFFER, data_index, gl.STATIC_DRAW);
  gl.vertexAttribIPointer(location_index, 2, gl.UNSIGNED_BYTE, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  // スロットの有効化
  gl.enableVertexAttribArray(location_index);

  // じゃあ描画しようか。
  gl.useProgram(pg);
  setUniformValue(gl, pg, "2fv", "uPositions[0]", [
    -sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)
  ]);
  setUniformValue(gl, pg, "3fv", "uColors[0]", [
    1,0,0, 0,1,0, 0,0,1
  ]);

  gl.clearColor(0.6, 0.4, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

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

  gl.flush();
  // お疲れ様でした。はや。
}

実行結果:

fjbh3fbj3hfjg.png

 いつもの0番、1番、2番に緑、青、赤が割り当てられています。成功です。in uvec2というのが見えると思いますがこれが整数アトリビュートです。符号なしの場合は
 uint, uvec2, uvec3, uvec4
で宣言し、符号ありの場合は
 int, ivec2, ivec3, ivec4
で宣言します。データのバインドとレイアウト指定にはvertexAttribIPointerという紛らわしい名前の関数を使います。

  // バッファを用意
  const buf_index = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf_index);
  const data_index = new Uint8Array([0,1, 1,2, 2,0]);
  gl.bufferData(gl.ARRAY_BUFFER, data_index, gl.STATIC_DRAW);
  gl.vertexAttribIPointer(location_index, 2, gl.UNSIGNED_BYTE, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

Uint8Arrayで用意して、1バイトずつsizeが2でフェッチするように指定します。vertexAttribPointerとよく似ていますが、いくつか違いがあります。

  vertexAttribIPointer(index, size, type, stride, offset);

indexはおなじです。sizeも同じです(1,2,3,4)。typeは、
 gl.BYTE, gl.SHORT, gl.INT, gl.UNSIGNED_BYTE, gl.UNSIGNED_SHORT, gl.UNSIGNED_INT
の6つから選びます。strideとtypeの役割は同じで、共にtypeのバイト長の整数倍とします。整数値を出力するため、normalizeの設定は無意味です。これを使うことでuvec2にデータが供給されます。

 ところで、整数アトリビュートが絡む場合、既定の値についてちょっと厄介なことが起きるので、それについて補足します。

 まず、グローバルステートのvertexAttributeの枠をちょっと変更します。currentでそのスロットの既定値を表現するとします。シャッターに書かれている「閉店時はこちらをお使いください」ですね。

  globalState = {
    vertexAttributeArray: [
      {enable: false, arrayBuffer: null, layout: undefined, divisor:0,
       current: DEFAULT_VERTEX_ATTRIBUTE, ...},
      {},{},...
    ]
  }

(ただしDEFAULT_VERTEX_ATTRIBUTEはFloat32Arrayで成分は0,0,0,1)
書いた通りです。currentというのは次のコードで取得できるんですが、

  console.log(gl.getVertexAttrib(index, gl.CURRENT_VERTEX_ATTRIB);

既定ではFloat32Arrayの0,0,0,1になっています。今、disableなどで整数アトリビュートがデータを取得したいスロットが閉まっているとすると、既定値が提供されるんですが、デフォルトではそれはFloat32の形のため、もしバーテックスシェーダでアトリビュートがuintやintで宣言されている場合、提供できないんですね。この場合どうなるかというと、機種に依らず描画不履行のエラーを食らいます。なので、既定値を変更する必要があります。

 vertexAttribI(だから紛らわしい名前を付けるなや)

  vertexAttribI4i, vertexAttribI4iv, vertexAttribI4ui, vertecAttribI4uiv

 4iと4uiは列挙、4ivと4uivは配列で指定します。4i,4ivの場合currentはInt32Arrayになり、4ui,4uivの場合currentはUint32Arrayになります。つまりuvec2でアトリビュートが宣言されているときは4uiや4uivを使わないといけないんですね。そうすれば、disableで非有効化されていても安心です。

 disableはそのスロットを使ってほしくない場合にも使用するんですが、前の設定が残ってたりすると厄介ですね...まあそこまで気にする必要はないかもしれないんですが、人生何が起こるかわからないものなので、変なバグが生じた時の可能性の一つとして、頭の片隅に留めておくといいと思います。

 このように、描画の際にenableだのcurrentだのいちいち気を遣うのは大変ですね...VAOはどこまで有効なんだろう。それも色々確かめる必要がありそうです。VAOというのは、なんかよくわかんないんですがvertexAttributeArrayの状態を保存していつでも呼び出せるようにする仕組みだそうです。レファレンスやwgldを読んでも詳細な仕組みがいまいちよく分かんなかったので、また今度調べてみようと思います。

 なお、レファレンスにもありますが、整数アトリビュートはスキンメッシュアニメーションに応用できます。

  /* 略 */
  in uvec4 boneIndices;
  void main() {
    vec4 skinnedPosition =
    bones[boneIndices.s] * vec4(position, 1.0) * boneWeights.s +
    bones[boneIndices.t] * vec4(position, 1.0) * boneWeights.t +
    bones[boneIndices.p] * vec4(position, 1.0) * boneWeights.p +
    bones[boneIndices.q] * vec4(position, 1.0) * boneWeights.q;
    gl_Position = mvMatrix * skinnedPosition;
  }

ちなみにp5で最近そういうコードを書きましたね
 skin action p5
ただまあp5は頂点アトリビュートの設定を固定しているのでいじれないですね。さすがに無理。まあでも個別に...だったら可能か、いや無理ですね。p5が勝手に割り当ててしまうので。描画時に、です。だから上書きできない。自前でやるしかなさそうです。
 自分でいじれた方がいろいろできて楽しいかもしれないですね。

 以上です。あと行列アトリビュートについて追記しようと思います。それで基礎編は終わりです。

補足追記3:行列アトリビュート

 配列と構造体はアトリビュートにできません。先に言っておきます。行列は可能です。
 2x2, 2x3, 2x4, 3x2, 3x3, 3x4, 4x2, 4x3, 4x4
すべて可能です。今回は2x3で作ります。これはvec3が2つ、です。一般にmxnは「m個のvec(n)」と考えるといいです。
 matrix attribute

// やってみよう。アトリビュート。
// 構造体アトリビュートはだめ
// 配列もダメ。行列はOK.
// たとえばvec4が3つの行列は3x4で指定する。「3つのvec4」と覚えるといい。

// 行列アトリビュートのヒント:
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttrib

function setup() {
  createCanvas(400, 400, WEBGL);

  const vs =
  `#version 300 es
  in mat2x3 aData; // vec3が2つの行列。[0]でvec3, [1]でvec3が返る。
  in float aBrightness;
  out vec4 vColor;
  void main(){
    vColor = vec4(aData[0], aBrightness);
    gl_Position = vec4(aData[1].xy, 0.0, 1.0);
  }
  `;

  const fs =
  `#version 300 es
  precision highp float;
  in vec4 vColor;
  out vec4 fragColor;
  void main(){
    fragColor = vec4(vColor.xyz * vColor.a, 1.0);
  }
  `;

  const gl = this._renderer.GL;

  // レイアウト指定。今回はオートで。
  const pg = createShaderProgram(gl, {
    vs:vs, fs:fs
  });
  // aDataが0,1を占有、aBrightnessが2を占有する。
  // 行列のロケーションは「それ」から続いて連番となる。
  // たとえばaBrightnessを1にした場合、aDataはそれを避けるために2,3を占有する。
  // 当然だが、aDataを0におきaBrightnessを1におくとコンパイルに失敗する。

  const location_data = pg.attributes.aData.location;
  const location_brightness = pg.attributes.aBrightness.location;

  const dBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, dBuf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    1,0.2,0.2, -1,-1,0, 0.2,1,0.2, 1,-1,0, 0.2,0.2,1, 1,1,0
  ]), gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_data, 3, gl.FLOAT, false, 24, 0);
  gl.vertexAttribPointer(location_data+1, 3, gl.FLOAT, false, 24, 12);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  const bBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, bBuf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    2, 1, 0
  ]), gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_brightness, 1, gl.FLOAT, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  // vaaの有効化
  gl.enableVertexAttribArray(location_data);
  gl.enableVertexAttribArray(location_data+1);
  gl.enableVertexAttribArray(location_brightness);

  // じゃあ描画しようか。
  gl.useProgram(pg);

  gl.clearColor(0.2, 0.2, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

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

  gl.flush();
  // お疲れ様でした。はや。
}

 コードにも書きましたが、mdnにも一応ヒントはあります:
 WebGLRenderingContext: vertexAttrib
3x3行列のロケーションを連番で扱っているのが分かるでしょうか。そうです。変数のロケーションからベクトルの個数だけ連番で確保されます。たとえばlayoutで1番を指定すると3x4の場合3つのvec4に対して1,2,3が占有されます。当然ですが、その際他のアトリビュートを3番に置いたりすると衝突してリンクに失敗します。
 vertexAttribPointerもそれぞれのロケーションに対して実行されます。今回はvec3が2つなので、0番と1番のそれぞれに対して実行しています。

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    1,0.2,0.2, -1,-1,0, 0.2,1,0.2, 1,-1,0, 0.2,0.2,1, 1,1,0
  ]), gl.STATIC_DRAW);
  gl.vertexAttribPointer(location_data, 3, gl.FLOAT, false, 24, 0);
  gl.vertexAttribPointer(location_data+1, 3, gl.FLOAT, false, 24, 12);

面倒なので色と位置を雑にFLOATで用意しました。インターリーブっぽいですが難しいことはやってないです。色と位置を交互で順繰りに並べただけです。まあ2つもバッファ用意するのめんどくさいですからね。それとは別にaBrightnessという単floatを用意してますがこれはロケーションのテスト用です。0と2が割り当てられています。行列は0と1を使っているわけです。色々数字を変えて遊んでいただければと思います。
 これで以上です。アトリビュートは本当に話題が豊富なのでいくら書いても書ききれないです。とりあえず現時点で予定しているのは

  • インスタンシング(divisor設定してドローコール変えるだけ)
  • インターリーブ(バイト単位での取り扱いでちょこっと遊ぶだけ)
  • VAO(vertexAttributeArrayの記録装置。参照先を変えるだけ)
  • 動的更新(bufferDataと違い場所の確保はせずバイト単位で中身だけ更新)

ですね。全部やるのは大変なのでだいぶつまみ食いになると思いますが、基本ができてれば難しくない内容ばかりなのでサクサク行ければと思います。
 作品作るのはもう疲れたので、仕様を調べて遊んでいます。仕様を調べるの、めっちゃ楽しい!!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?