はじめに
アトリビュートを説明します。ようやく。
引き続きp5.jsのテンプレを間借りしています。便利ですからね。便利なものは使わなきゃ。
引き続き、自分の理解のために書いています。それでも良ければお読みください。
ここまでのいろいろが分かっていれば難しくないです。ポイントはuniformとの違いです。似ているようで全然違います。uniformで述べましたが、あれはプログラムに付随する概念です。なぜなら整数列からプリミティブをドローコールによって作り、描画をやるわけですが、値がその整数に依らないからです。ロケーションが出てきましたが、あれもプログラム内のデータの置き場所を指定するものでした。アトリビュートは、その整数とデータの塊から、整数ごとに、データを取り出して、それを使います。どこかにそのデータがあるんですが、ロケーションはそのデータがある場所の番号です。外部にデータがあるという決定的な違いがあります。それでたとえば0,1,2に対し、ある配列の0,1,2番を使います...
と言いたいところですが、厳密には違います。
インデックスバッファのところで説明したと思いますが、データは基本的にバイト列として格納されています。drawElementsの仕様について説明した時、gl.UNSIGNED_SHORTを指定することで2バイトずつ取って整数にして扱う、といったようなことを説明したと思いますが、そういう感じでフェッチします。たとえば最もよく使われるFloat32,つまり32bit浮動小数点数、まあ4バイトですが、これを扱う場合であっても、4バイトずつ取得し、それをリトルエンディアンでFloat32と解釈して使うわけです。そういった利用法とバイト列の一組。これがvertexAttribute, 頂点アトリビュート、です。厳密にはもっといろいろプロパティがありますが、とりあえずそんな感じです。
グローバルステートを更新します。
globalState = {
currentProgram: null,
bindingBuffer: {
arrayBuffer: null,
elementArrayBuffer: null,
uniformBuffer: null,
...// 色々
}.
vertexAttributeArray: [
{enable: false, arrayBuffer: null, layout: undefined, divisor: 0, ...},
{enable: false, arrayBuffer: null, layout: undefined, divisor: 0, ...},
// いっぱい
],
...// 他にも色々
}
vertexAttributeArrayというのを追加しました。これは見ての通り配列です。attributeのロケーションというのはこの配列の番号のことです。それぞれの成分がvertexAttribute,頂点アトリビュートです。
アトリビュートとは、ロケーションに基づいてこの中のどれかを指定し、そこにあるデータ、というかarrayBuffer(バイト列)ですが、ここから整数に基づいて(整数というのは要するに
drawArrays(gl.TRIANGLES,0,3)
だったら0,1,2ですがそれ)layoutに従ってデータを取り出し、数として利用します。layoutというのは要するにインデックスバッファのところでgl.UNSIGNED_SHORTを指定して「2バイトずつ取ってuint16の整数として使ってね」って命令したと思いますが、ああいうことが書いてあります。それが無いと、ただのバイト列ではデータの出しようがないです。
もちろん、インデックスバッファの場合は然るべきバッファからフェッチして得られた整数に基づいて、全く同じ処理をします。また前回の記事で、用意するときとドローコール時で違う型を指定しましたが、基本的にあんなことはしないです(単純にややこしいので)。ただ実際にはバイト列を扱っている、というイメージをつかむために敢えてああいうことをしました(そういうことにしよう。そうしよう)。
またenableとありますが、これがtrueでないとそもそもデータが出せません。倉庫のシャッターのようなものが個別に用意されてるわけです。出せない場合は仕方ないので、既定の値が使われます。倉庫のシャッターに「この値を使ってください」と張り紙が貼ってあるようなものです。それもいずれ説明できればと思います。divisorが何か?今は無視してください。既定値は0です。そのうちやります...多分。
また、vertexAttributeArrayの長さは次のコードで取得できます。
console.log(`vertexAttributeArrayの長さは${gl.getParameter(gl.MAX_VERTEX_ATTRIBS)}です`);
基本的に16ですが、グラフィックカードに依存するようです。そのため複数のプログラムが同じ番号を指定するのは日常茶飯事で、同じ頂点アトリビュートの部屋の取り合いになりがちです。ただ一度に走ることのできるプログラムは一つだけなので、譲り合えばいいだけのことです。
この記事の内容をきちんと理解するために、次のサイトは非常に、非常に参考になります。
WebGLRenderingContext.vertexAttribPointer()
コード全文
// やってみよう。アトリビュート。
function setup() {
createCanvas(400, 400, WEBGL);
const vs =
`#version 300 es
in vec2 aPosition; // 位置!です!
in vec4 aColor; // 無視されます!
void main(){
vec4 color = aColor; // これでも無視されます!!
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;
const fs =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
`;
const gl = this._renderer.GL;
console.log(`vertexAttributeArrayの長さは${gl.getParameter(gl.MAX_VERTEX_ATTRIBS)}です`);
// レイアウト指定。aPositionは0を使います。
// 1が使いたいなら1でもいいですよ。
const pg = createShaderProgram(gl, {
vs:vs, fs:fs, layout:{aPosition:0}
});
const location_position = pg.attributes.aPosition.location;
// バッファを用意
const buf = gl.createBuffer();
// ARRAY_BUFFERとして用いる。
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
// データを用意する
const positionData = new Float32Array([
-sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)
]);
// データを登録。いじらないのでSTATIC_DRAW.
gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
// aPositionを使う番号を指定。[location_position]ですね。
// vec2なので2つずつ、またFLOATで利用してもらう。
// Float32で用意したので。
// 正規化はしない。されちゃ困る。
// 値はくっついてるので0, 0でOK.
gl.vertexAttribPointer(location_position, 2, gl.FLOAT, false, 0, 0);
// 解除。
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// aPositionは[location_position]番で使うのでそこを開けておく。
gl.enableVertexAttribArray(location_position);
// じゃあ描画しようか。
gl.useProgram(pg);
gl.clearColor(0.2, 0.3, 0.5, 1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
// お疲れ様でした。はや。
}
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){
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 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){
/* 省略 */
}
uniformは今回使わないので、一部省略しました。activeUniformの取得のところは、比較用に残してあります。
実行結果:
active attribute(有効なアトリビュート)
uniformと違うとはいえ、似てる部分も多いです。まず有効なアトリビュートについてです。
#version 300 es
in vec2 aPosition; // 位置!です!
in vec4 aColor; // 無視されます!
void main(){
vec4 color = aColor; // これでも無視されます!!
gl_Position = vec4(aPosition, 0.0, 1.0);
}
#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
aPositionというのが今回使うアトリビュートです。なお、値は常に小数です。基本的にFloat32です。さきほどデータのフェッチについて触れましたが、フェッチの結果がFloat32だろうと、バイト単位の整数だろうと、バーテックスシェーダに送られた際にはFloat32の小数として扱われます。実は整数も使えるんですがまあそのうち...そのうちね。そもそも今まで使ってきたgl_VertexIDが整数なわけですし。今回それは登場していませんね。レベルが上がったような、難しさを予感させます。まあ難しくないんですが。
vec2です。つまりFloat32が2つです。それもレイアウトで指定します。
aColorも指定してあります。しかし見ればわかるように、描画に寄与していません。そういうわけで、無視されます。これもuniformに似ていますね...
プログラムを書き換えてuniformの情報を付与できるようにしたんですが、今回アトリビュートの情報も追加できるようにしました。まずはそこから説明します。
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;
}
numActiveAttributesという形で個数を取得します。1個です。aPositionのことです。aColorは無視されます。次にgetActiveAttribでアクティブなアトリビュートを一つずつチェックしていきます。この通し番号に特に意味はなく、要するに全部チェックできればいいだけの話ですね。
そしてロケーションを取得します。このロケーションの意味合いが、uniformとの決定的な違いです。
const location = gl.getAttribLocation(pg, attribute.name);
attribute location (アトリビュートロケーション)
このロケーションは、最初に説明した通り、データの置き場所の通し番号です。置き場所は外部にあり、プログラムから独立しています。グローバルステートです。指定された番号の倉庫から、そこにおいてあるデータと、利用法に従って、データを取得します。その際にドローコールの実行の際に供される整数列の整数を使います。
このロケーションは見ての通り、プログラムから取得します。つまりプログラムがリンクされて完成した瞬間に決定されます。以降、決して変更できません。つまりリンク前であれば変更が可能です。普通は有効なアトリビュートに順繰りで0から順番に通し番号が振られるんですが、実は自前で決めるやり方があります。二つ、あります。
レイアウト指定方法1:bindAttribLocationを使う
一つはこのコードでやっているように、アタッチしてからリンクするまでの間に関数を使って指定する方法です。
// レイアウト指定。aPositionは0を使います。
// 1が使いたいなら1でもいいですよ。
const pg = createShaderProgram(gl, {
vs:vs, fs:fs, layout:{aPosition:0}
});
まずcreateShaderProgramのparamsにlayoutという枠を設けました。こういうことが自由にできるのがオブジェクト形式の偉い所です。引数をじゃらじゃら増やすのはかっこ悪いので(状況によりますが)。で、これをどう使うかというと、
const {vs, fs, layout = {}} = params;
/* 略 */
const program = gl.createProgram();
gl.attachShader(program, vsShader);
gl.attachShader(program, fsShader);
// レイアウト指定はアタッチしてからリンクするまでにやらないと機能しない。
// なおこの機能はwebgl1でも使うことができる。webgl2で実装されたというのは誤解。
setAttributeLayout(gl, program, layout);
gl.linkProgram(program);
こんな感じでattachしてからlinkするまでの間にAttributeのLayoutを指定します。こんな風に:
// レイアウトの指定。各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);
}
}
引数のオブジェクトであるlayoutに記載された通り、例えばこの場合aPosition:0ですから、aPositionには0が割り当てられます。第一引数はもちろんプログラムです。プログラムがそのアトリビュートを何番の倉庫で使うか、こっちで決められるわけですね。
試しに1や2にしてみてください。普通に動きます。
const pg = createShaderProgram(gl, {
vs:vs, fs:fs, layout:{aPosition:2}
});
なおこのbindAttribLocationという関数はwebgl1の頃から存在するものです。何が言いたいかというと、webgl1でもレイアウトを自由に決めることは可能ということです。
レイアウト指定方法2:layout修飾子を使う
もうひとつは、シェーダーで決める方法です。たとえば次のようにします。
#version 300 es
layout (location = 1) in vec2 aPosition; // 位置!です!
layout (location = 2) in vec4 aColor; // 無視されます!
void main(){
vec4 color = aColor; // これでも無視されます!!
gl_Position = vec4(aPosition, 0.0, 1.0);
}
こちらはwebgl2で可能になりました。
なお、レイアウトという語を「プログラムにおけるアトリビュートロケーションの設定」と「頂点アトリビュートにおけるデータの扱い方」の両方で用いていますが、公式も、いずれもレイアウトと呼んでいるので仕方ないですね。というかシェーダーサイドのレイアウトにも「そういう」役割があるのである意味当然ではあります(この辺に関しては理解が及んでいないので詳しい言及は避けます)。
layout修飾子ではいろんなものを定義できるんですが、とりあえず今回はロケーションだけ説明します(自分も今のところこれしか知らないので)。要するに、あんな感じで「ロケーションはここ」ってやるとそうなります。つまり、aPositionのロケーションは1になります。2とすれば、2になります。
補足1: attribute locationは重複できない
アトリビュートのロケーションは自由に設定できますが、同じ値を設定するとエラーを食らいます。どっちも0番とかはだめです。同じところからデータをフェッチするだけなので一見できそうですが、プログラムのリンクに失敗します。不可です。
ただし、異なる頂点アトリビュート、っていうかデータの倉庫に、同じデータを供給して同じ使い方を要求することは可能です。実質的には、同じところからデータをフェッチするのと同じことです。そこら辺については後で解説します。
補足2: bindAttribLocationはロケーションが未定義の場合しか使えない
次に、bindAttribLocationですが、これはlayout修飾子でロケーションが設定されたアトリビュートに対しては使えません(エラーは出ませんが、効果が発揮されません)。未定義の場合にのみ実行されます。また、それによりロケーションの重複が起きるとリンクに失敗します。
ちなみにlayout修飾子でロケーションを設定する際に一部のアトリビュートにしかそれを設定しなかった場合、残りのアトリビュートのロケーションは未定義となりますが、それがbindAttribLocationでも設定されなかった場合は、衝突しないように0から順番に通し番号が振られるようになっています。
当然ですが、アクティブでないアトリビュートは最後まで無視されます。そもそもロケーションの割り当てが実行されません。
ロケーションの説明が終わったので、いよいよ頂点アトリビュートにデータとレイアウトを設定する方法を説明します。
// これのarrayBufferとlayoutのところ。
vertexAttributeArray = [
{enable: false, arrayBuffer: null, layout: undefined, divisor: 0, ...}, {...}, ... ]
vertexAttribPointer(データの供給とレイアウトの指定)
頂点アトリビュートにデータを入れるにはvertexAttribPointerという関数を使います。まず、データの生成からです。
// バッファを用意
const buf = gl.createBuffer();
// ARRAY_BUFFERとして用いる。
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
// データを用意する
const positionData = new Float32Array([
-sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)
]);
// データを登録。いじらないのでSTATIC_DRAW.
gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
インデックスバッファのときとの違いは、bindBufferでgl.ARRAY_BUFFERを指定するところです。jsにArrayBufferというのがあると思いますが、これはバイト列です。バイナリデータの列です。0~255の範囲の値がずらっと並んでいる感じの何かです。配列と名前が付いていますが配列ではない何かです。インデックスバッファの正式名称はelementArrayBufferですが、要するにあれもArrayBufferの一種です。
インデックスバッファの時のようにまずwebGLBufferを作り、グローバルステートのarrayBuffer枠にバインドします。
globalState = {
...,
bindingBuffer: {
arrayBuffer:buf,
...
},
...
}
データを作ります。Float32Arrayで作ります。6つの値は今まで三角形の描画に用いていた頂点の配列と全く同じものです。これを同じようにbufferDataで登録します。この時参照されているのはグローバルステートのarrayBuffer枠です。変更する予定は無いのでSTATIC_DRAWです。
次に、これをvertexAttributeArrayの部屋に登録して、プログラムがそれを使えるようにします。
// aPositionを使う番号を指定。[location_position]ですね。
// vec2なので2つずつ、またFLOATで利用してもらう。
// Float32で用意したので。
// 正規化はしない。されちゃ困る。
// 値はくっついてるので0, 0でOK.
gl.vertexAttribPointer(location_position, 2, gl.FLOAT, false, 0, 0);
// 解除。
gl.bindBuffer(gl.ARRAY_BUFFER, null);
vertexAttribPointerの仕様:
vertexAttribPointer(index, size, type, normalize, stride, offset);
模式図:
初めに述べておきます。normalizeはfalseを指定します。これは補足で述べるので、後回しにします。
まずindexですが、これはグローバルステートのarrayBufferに登録されたwebGLBufferをvertexAttributeArrayの何番目の部屋に入れるかのインデックスです。2なら2番目の部屋に入ります。順番的にはstrideとoffsetが先なので、そっちから説明します(sizeとtypeも出てきます)。
まずarrayBufferの実体は、bufferDataで登録するときに用いたバイト列です。今回はFloat32Arrayを使いましたが、これも内部では4バイトずつリトルエンディアンでバイト列になって格納されています。また、今現在ドローコールが実行されていると考えてください。drawArraysにせよ、drawElementsにせよ、なんらかの整数列が指定されています。その整数が与えられた場合にプログラムがバイト列のどこをどう言う風に使えばいいのか、それを指定するのがレイアウトです(画像で言うところの左上の$n$です)。
開始位置は$n$×strideです...が、strideの指定について説明します。まずstrideが正の数ならそのまま使います。0の場合、strideは(typeに使うバイト数)×(size)となります。typeの候補は、
- gl.BYTE(-128~127、1バイト)
- gl.SHORT(-32768~32767、2バイト)
- gl.UNSIGNED_BYTE(0~255、1バイト)
- gl.UNSIGNED_SHORT(0~65535、2バイト)
- gl.FLOAT(32bitIEEE浮動小数点数、4バイト)
- gl.HALF_FLOAT(webgl2限定、16bitIEEE浮動小数点数、2バイト)
です。このバイト数にsizeを掛けたものになります。offsetですが、これはバイト数で指定します...が、typeのバイト長の倍数でないとエラーになります。たとえばgl.FLOATであれば4の倍数でないといけません。offsetは自由に指定できるんですが、これはデータフェッチの開始位置の文字通りの「offset」です。つまりデータのフェッチはstride × n + offsetから開始するということです。
if(stride===0){
stride = byteLengthOfType(type)*size;
}
if(offset % byteLengthOfType(type) !== 0){ console.error(); return null; }
startingIndex = stride * n + offset;
はみ出しても気にしません。strideが8でoffsetが12の場合、本当に8n+12から開始されます。
(あとで知ったんですが、strideもtypeのバイト長の整数倍でないとエラーになります...日本語レファレンスの方はスルーしていますが、
5.14.10 Uniforms and attributes
こっちにはちゃんと書いてあります。以降の記事で補填していますが、一応こっちにも書いておきます。)
開始点が決まったら、そこからtypeのバイト長の分だけバイト列から採取、型に従って数に変換、それをsizeの分だけ取得します。これで数の組ができました。ようやく完成です。
今の場合、FLOATが6つ用意されているので、バイト長は24です。そこからの数の採取は、要するにこういうことです。
まずFloat32Arrayで作ったので4バイトの小数がバイト形式になり、このように並んでいます。strideは0指定なのでtypeがFLOAT、sizeが2であることより8です。nは1なのでスタートは8で、offsetは0なので8からです。typeは4バイトなので4バイトずつ、sizeは2なので2つブロックを採取、それらをFLOATの仕様で小数に復元。できました。プログラムサイドのvec2に順繰りでセットします。これでようやく、整数「1」がvec2の然るべき値になりました。めでたし。
同じことを0と2に対してもやれば、三角形の頂点が復元されます。処理の最後に、グローバルステートのarrayBuffer枠をnullにしておきます。データのバッファリングと同様、登録が終わるたびにあそこは解除しておくのがマナーです。もちろんbindingBufferのarrayBuffer枠をnullにするだけで、頂点アトリビュートにセットしたbufferはそのまま残っています。こんな感じで設定されました。
globalState = {
...,
bindingBuffer:{arrayBuffer: null, ...},
vertexAttributeArray:[
{enable: false, arrayBuffer: buf,
layout:{size:2, type:gl.FLOAT, stride:8, offset:0}, divisor: 0, ...},
...
],
...
}
この0番にアクセスすることで、データを取得できるわけですね。
enableVertexAttribArray(頂点アトリビュートの有効化)
しかし、このデータを使うにはenableのプロパティをtrueにする必要があります。これを実行するのがenableVertexAttribArrayです。
// aPositionは[location_position]番で使うのでそこを開けておく。
gl.enableVertexAttribArray(location_position);
指定するのは整数です。当然ですね。vertexAttributeArrayのindexです。指定した番号のアトリビュートのenableプロパティがtrueになります。trueであれば、先ほど述べた仕組みでデータがフェッチされます。falseの場合、既定値が使用されます。それはまたいつか述べますが、0,0,0,1が必要なだけ使われる、とだけ言っておきます。たとえば今の場合、すべて0になります。
同様にfalseにする関数も存在し、disableVertexAttribArrayといいます。
gl.disableVertexAttribArray(index);
役割は該当するindexのアトリビュートのenableプロパティをfalseにすることです。
ドローコール
準備ができたので描画します。
// じゃあ描画しようか。
gl.useProgram(pg);
gl.clearColor(0.2, 0.3, 0.5, 1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
// お疲れ様でした。はや。
vertexAttributeArrayの0番はすでに有効になっているので、プログラムが走り、ドローコールで0,1,2が指定された場合、プログラムは整数から上に述べたやり方でデータを採取し(aPositionのロケーションは0番なので0番が使われる)、vec2のaPositionを構成します。それがシェーダー内で使われ、頂点の位置が決まります。以下略。三角形ができました。
もちろん手動でaPositionのロケーションを1にすれば、1番にデータを供給し、1番を有効にすることで、1番が使われたりします。結果は一緒です。色々試してみてください。
いくつかの補足
以下、いくつか補足します。
- 引数のnormalizeについて
- 同じデータを複数の頂点アトリビュートに供給できる
- データのフェッチにおいてインデックスがはみ出した場合の挙動
- enableプロパティがfalseの場合に使われる値について
しかし長くなったので、次回に回します。
おわりに
今回はアトリビュートについて基礎的なことを説明しました。自分もずっとアトリビュートは配列がそのまま使われていると思ってたんですが、バイト列が実体だったことを知ったのは最近です。思えばARRAY_BUFFER, ArrayBufferですから、当然ですね...ただまあp5もデータは常にFloat32で用意しており、あそこも常にgl.FLOATですから、その方が色々と楽ですし、不便が無いのは事実ですから仕方のない事ではあります。
どちらかというとvertexAttributeArrayの概念の存在を意識することの方がずっと重要です。p5のハッキングをしていた頃、
(だいたいこのへん)
Disable unused vertexAttribute to avoid environment-dependent vanishing bugs #5970
この記事で紹介したenable/disableをシェーダー(まあ要するにプログラムのこと...プログラムって言うべきなんだけどねぇ)が、管理してたのを見つけたんですよね。でもプログラムは管理できないんですよね。だって独立してるんだから。もっと言うと管理するべきではないんです...そういういろんな経験をしてちょっとずつ理解が深まってきたという経緯があります。ただまあまだ理解が不十分なところもあると思います。これからもいろいろ知っていかないといけないですね。
なおこのプルリクはざっくりいうと「今の仕様だと、私のスマホが不具合起こしかねないから、disableVertexAttribArrayを導入してくれませんか」とかそんな感じのものです。次回の補足編で詳しい内容を説明できるかもしれないです。
ここまでお読みいただいてありがとうございました。