はじめに
VAOです。アトリビュートカテゴリーの目玉です。一番目立つ中盤に持ってきました(応用編の中盤という意味)。これは要するに記録装置です。プログラムでアトリビュートをどのように使うのか、その設定を保存して、必要な時に呼び出して、切り替えたりできるものです。その仕組みを詳しく説明するのがこの記事の趣旨です。ふわっとではなく、詳しく、です。可能な限り、正確に。将来捉え方の更新を迫られたときに慌てず落ち着いて対処できるように、土台を固めるのが目的です。
p5.jsでやります。
どうでもいい雑談ですが、vertexAttribPointerの引数、多すぎて覚えるの大変なんですよね...全部覚えたのは最近です。基礎固め始めてから数週間後です。
gl.vertexAttribPointer(index, size, type, normalize, stride, offset);
なぜなら自分のライブラリではtype以降の4つがすべて固定だからです。p5もそうしています。p5を参考にして作ったのでそういう感じになってるわけです、まあ汎用性を考えるなら余計なことはしなくていいんですが、最近は色ならバイトで入れたいなとか思ったりしてます。
ちなみにtexImage2Dは最大で9個くらい引数がある化け物みたいな関数で、加えて引数のバリエーションが豊富なので覚えきれないです。一番オーソドックスな使い方をするにしても:
gl.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
// ※levelはだいたい0、borderはmust be 0(0以外認めんってレファレンスが言ってる)
なので覚えきれないです。
ただこういうのは使うたびに調べるのもありだと思います。使い方とイメージさえあれば関数定義は調べればいい、暗記することは少ない方がいいです。こんなの暗記するより仕様で引っかかるポイントをなくす方が大事だと思います。
話がそれました。まず最初に、グローバルステートの復習からです。その前に...
vertexAttributeArrayのlayoutのデフォルトについて
グローバルステート。
globalState = {
currentProgram: null,
bindingBuffer:{
arrayBuffer: null,
elementArrayBuffer: null,
uniformBuffer: null, ...
},
vertexAttributeArray:[
{enable: false, arrayBuffer: null, layout: {??}, divisor: 0,
current: DEFAULT_VERTEX_ATTRIB}, {}, {}, ...
], ...
}
// ※DEFAULT_VERTEX_ATTRIBはFloat32Arrayの0,0,0,1
これは自分なりにwebglの仕組みを理解するためのインタフェースです。こういうのが無いとあやふやな状態であれこれやる羽目になるので、それが嫌で何かしらイメージを作っているわけです。頭のいい人なら要らないかもですが...ステートマシンはめんどくさいのです。
今回注目するのはlayoutです。自分は今まで、バッファが付いてないならlayoutは未定義だろうと思っていました。なぜならvertexAttribPointerはバッファの割り当てとレイアウトの決定を同時に実行するからです。しかしこのlayoutには初期値がありました(最近実験したらそうなってました)。それを確かめるには、次のようにします。
// VAOは何を記録するのか?
/*
デフォルトとその求め方
ちなみに
bindingBufferのarrayBuffer枠がnullの状態でレイアウトを変更しようとすると
失敗します。すべてのレイアウト変更が反映されません。注意。
これはエラーすら出ないのでびっくりします。なんとレファレンスにも書いてない。
*/
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
// なんか紐付けられていればレイアウトを変更できる。
//const buf = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buf);
// bindされたarrayBufferが無いと失敗する
//gl.vertexAttribPointer(0, 1, gl.UNSIGNED_BYTE, true, 3, 2);
// arrayBuffer枠を埋めた後でも変更はキャンセルされる。
// あくまでbindingBufferのarrayBuffer枠が埋まってないと変更できない
//gl.bindBuffer(gl.ARRAY_BUFFER, null);
//gl.vertexAttribPointer(0, 3, gl.SHORT, true, 8, 12);
// true
console.log(gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING) === null);
// すべてfalse
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED));
}
// すべてtrue
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING) === null);
}
// すべて4
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE));
}
// すべて5126(gl.FLOAT)
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE));
}
// すべてfalse
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED));
}
// すべて0
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE));
console.log(gl.getVertexAttribOffset(i, gl.VERTEX_ATTRIB_ARRAY_POINTER));
}
// すべて0
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_DIVISOR));
}
// 後で説明するが、VAOに参照が移るのは以上のパラメータとなります。
// current vertex attrib
for(let i=0; i<16; i++){
// すべてFloat32の[0,0,0,1]
console.log(gl.getVertexAttrib(i, gl.CURRENT_VERTEX_ATTRIB));
}
}
上から順にいろいろ調べています。なお、bindingBufferのarrayBuffer枠はnullになっています。もちろんvertexAttributeArrayのいずれのスロットにもarrayBufferは紐付けられていません。その状態で調べています。
まず...
- gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING)
bindingBufferのelementArrayBuffer枠を調べています。もちろんnullです。なぜこれを調べているのかは後で説明するんで、とりあえずスルーしてください。
では本題で、vertexAttributeArrayの状態を調べていきます。これですべてのはずです。少なくとも描画に必要なものは網羅しているので、他にあったとしても特に問題はないでしょう。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED)
i番スロットの有効状態を調べています。もちろんすべてfalseです。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING)
i番スロットに紐付けられたarrayBuffer枠がnullなのかを調べています。もちろん、すべてtrue(つまりnull)です。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE)
i番スロットのレイアウトのsizeプロパティを調べています。これはすべて4になっています。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE)
i番スロットのレイアウトのtypeプロパティを調べています。これはすべてgl.FLOATとなっています。さっきのと合わせると32bitFLOATのvec4というわけですね。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED)
i番スロットのレイアウトのnormalizeプロパティを調べています。すべてfalseです。FLOATデフォルトなのである意味当然ではあります。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE)
- gl.getVertexAttribOffset(i, gl.VERTEX_ATTRIB_ARRAY_POINTER)
上から順にi番スロットのレイアウトのstrideプロパティとoffsetプロパティを調べています。いずれもすべてデフォルトの値は0です。ところで、なぜかoffsetだけ別の関数で取得するようになっています。なぜなのでしょうか。
知らんがな...
これ見つからなくてちょっと焦ったんですけど、見つかってよかったです💦...おそらくプロパティごとに別の関数を用意しようと思っていたところ、方針が変わったんじゃないでしょうか(憶測)。
- gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_DIVISOR)
i番スロットのdivisorの値を調べています。もちろんすべて0です。
最後に、カレントを調べています。シャッターが閉まってる時のデフォルト値ですね。
- gl.getVertexAttrib(i, gl.CURRENT_VERTEX_ATTRIB)
i番スロットが閉じてる時のデフォルトの値は、すでに説明したとおりすべてFloat32の0,0,0,1となっています。これだけ別枠で調べた理由は後でわかるかと思います。
そういうわけで、レイアウトはちゃんと設定されていたんですね...
globalState = {
currentProgram: null,
bindingBuffer:{
arrayBuffer: null, // ここがnullのときにvertexAttribPointerを呼び出すと無効になる
elementArrayBuffer: null,
uniformBuffer: null, ...
},
vertexAttributeArray:[
{enable: false, arrayBuffer: null,
layout: {size:4, type:gl.FLOAT, normalize:false, stride:0, offset:0},
divisor: 0,
current: DEFAULT_VERTEX_ATTRIB}, {}, {}, ...
], ...
}
// ※DEFAULT_VERTEX_ATTRIBはFloat32Arrayの0,0,0,1
なお、コードにも書きましたが、bindingBufferのarrayBuffer枠がnullの状態でレイアウトだけ変更しようと思ってvertexAttribPointerを呼び出すと、バッファの紐付けが実行されないのは当然ですが、なんとレイアウトの変更も不可能になります。つまり変えられないんですね。独立してデフォルト値が設定されているにもかかわらず、です。気になる人はコメントアウトを外して実験してみてください。呼び出しがキャンセルされます。
さらに、バッファをvertexAttributeに紐付けた後でレイアウトだけ変更するにも、グローバルステートのbindingBuffer内のarrayBuffer枠が埋まってないとvertexAttribPointerによるレイアウト変更は失敗します。自身のarrayBuffer枠が埋まっているにもかかわらず、です。あくまで、bindingBufferのarrayBuffer枠からバッファをコピーできないとだめなのです。おそらくそれに失敗したタイミングで処理をキャンセル(return)してしまっているのでしょう。それもコメントアウトで確かめられるようにしてあります。
ちなみに、enable/disableやdivisorに関しては、バッファが無関係なので、いつでもいくらでも変更できます。カレントも然り、です。
レイアウトの説明が終わったので、いよいよ本題に入ろうと思います。
VAOの仕組み
VAOとは、一言で言うと、
「グローバルステートの一部の状態を取得、および更新する機能を、別の内部オブジェクトに一時的に委譲する仕組みの一種」
です。webGLにはそういうオブジェクトがいくつかあり、おそらくFBOとTFOもそうなんですが、とりあえず後回しで...おそらくTFOはそのうち取り上げられるかと思います。FBOは難しいのでやらないかもです。緩くやりたいので。
ここでいう別の内部オブジェクトというのがVAOです。要するに、外部記憶媒体のようなものです。それはグローバルステートの一部のプロパティと同じものを有しており、これが接続されている間、それらの情報に関しては、そこから情報を得ます。また、それらの情報が書き換えられる場合、媒体の方が書き換えられ、グローバルステートはそのまま、一切変更されません。接続を切り、なにも接続されていない状態に戻すと、再び情報の取得、及び更新は、グローバルステートに対するものになります。また、別の媒体(別のVAO)が代わりに接続された場合は、そっちに権限が委譲するだけです。
では具体的にどのプロパティかというと、自分の調べた限りでは次のものです。
すなわち、「bindingBufferのelementArrayBuffer枠、及びvertexAttributeArrayの各々のvertexAttributeのプロパティの中のcurrent以外すべて」です。これらと同じものをVAOは有しており、VAOが接続されている場合には、これらのプロパティはVAOから取得します。また、これらのプロパティの変更に関わる全ての関数は、VAOに対するものとなります。VAOが接続されている間に実行されたすべての変更に対し、グローバルステートはその変更を受け付けません。VAOを外したら元に戻るわけです。具体的には次の関数です。
- bindBuffer(ただしtargetがgl.ELEMENT_ARRAY_BUFFERの場合のみ)
- enable/disable VertexAttribArray
- vertexAttribPointer/vertexAttribIPointer
- vertexAttribDivisor
つまりvertexAttirb4fvとかは入ってないわけです。また、bindingBufferのarrayBuffer枠も対象外です。
これを実験するため、次のようなコードを実行してみます。
// VAOは何を記録するのか?
/*
VAOを紐付けることで一旦リセットされるのを見る
それにより取得と更新の主導権がVAOに移るのを確認する
*/
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
// 好き勝手に変更する
executeChange(gl);
// VAOを作る
const vao = gl.createVertexArray();
// VAOを紐付ける
//gl.bindVertexArray(vao);
// true
console.log(gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING) === null);
// falseになる。つまり、bindingBufferのarrayBuffer枠の状態は保存されない
console.log(gl.getParameter(gl.ARRAY_BUFFER_BINDING) === null);
// すべてfalse
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED));
}
// すべてtrue. ということはVAAサイドの枠については保存されるということ。
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING) === null);
}
// すべて4
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE));
}
// すべて5126(gl.FLOAT)
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE));
}
// すべてfalse
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED));
}
// すべて0
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE));
console.log(gl.getVertexAttribOffset(i, gl.VERTEX_ATTRIB_ARRAY_POINTER));
}
// すべて0
for(let i=0; i<16; i++){
console.log(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_DIVISOR));
}
// 後で説明するが、VAOに参照が移るのは以上のパラメータとなります。
// current vertex attrib
for(let i=0; i<16; i++){
// すべてFloat32の[0,0,0,1]
console.log(gl.getVertexAttrib(i, gl.CURRENT_VERTEX_ATTRIB));
}
}
// 好き勝手に変更しましょ
// arrayBuffer枠とelementArrayBuffer枠を両方埋めてしまいます
// どっちも解除しないで抜けます
function executeChange(gl){
const eBuf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, eBuf);
const aBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, aBuf);
gl.enableVertexAttribArray(0);
gl.vertexAttribDivisor(0, 12);
gl.vertexAttrib4fv(0, [1,2,3,4]);
gl.vertexAttribPointer(0, 1, gl.UNSIGNED_BYTE, true, 4, 9);
}
レンダラーを用意して、グローバルステートの変更を実行します。bindingBufferのarrayBufferとelementArrayBufferを埋めて、enableVertexAttribArrayで0番を有効化、vertexAttribPointerでarrayBuffer枠を埋めてレイアウトをぐちゃぐちゃにし、divisorを変更し、ついでに0番のカレントもいじっておきます。そのあとで、VAOを作ります。
const vao = gl.createVertexArray();
これはとりあえずおいといて、とにかく実行します。
ちなみに、コードを見ると分かりますが、さっきのコードに加えてbindingBufferのarrayBuffer枠も確かめられるようにコードを追加しました。
- gl.getParameter(gl.ARRAY_BUFFER_BINDING)
それも含めて、いじったところはすべて変更が反映されています。最初の2つのfalseはarrayBuffer枠とelementArrayBuffer枠は両方埋まっているという意味です。
では、コメントアウトを外してVAOをbindしてみます。そのうえで実行します。
紐付けた場合、内容は最初にレイアウトのデフォルトなどを調べた場合と「ほぼ」同じですが、2ヶ所だけ違います。ひとつはbindingBufferのarrayBuffer枠です。falseです。紐付けられたままです。bindingBufferのarrayBuffer枠は、グローバルステートを参照しています。もうひとつはカレントです。変更されたままです。これも、グローバルステートを参照しています。それ以外はすべてリセットされています。つまりVAOの方を参照しています。そういう仕組みになっています。
次に、権限が委譲されていることを見るために、コードの上の方をちょっと変更します。VAOが紐付けられた状態で変更を実行し、その後VAOを外してみます。結果は:
このようになります。つまり更新はVAOの方に適用され、グローバルステートの更新はなされていないことが分かりました。ちゃんと記録されていることを見るため、再度紐付けを実行してみます。
適用した変更が再び反映されています。このことからわかるように、VAOが紐ついてる間、取得と更新はVAOに対するものとなるわけです。
VAOの利点
VAOが有するプロパティは、いずれも描画に関するものであり、必要なものは大体揃っています。vertexAttributeについてはカレント以外すべてそろっているので、enableの場合のフェッチの結果は完全に復元できます。disableの場合はカレント依存なのであれですが、そもそもenable/disableは保存されるのであまり問題にならないと思います。例の機種依存バグも、あらかじめdisableで保存しておけば防ぐことができます。またbindingBufferのelementArrayBuffer枠も保存されるので、インデックスバッファによる描画でも安心です。これをバインドするだけで、ドローコールの結果を容易に復元できます。
なお、bindingBufferのarrayBuffer枠は描画に使わないので問題ないです。そもそもここは基本常にnullです。それはとても重要なことです。上でも述べたように、vertexAttribPointerが機能するのはここがnullでない場合だけだからです。ここをnullに保つことで、予期せぬ変更を防ぐことができます。保存されないのでどこでnullにしてもいいんですが、用が済んだら極力早めにnullをバインドしておきましょう。
バッファとプログラムの組み合わせに応じていくつかVAOを作っておき、それをとっかえひっかえするだけで、面倒なアトポン、divisor, enable/disable、加えてインデックスバッファの切り替えがすべて不要になるのは素晴らしい利点だと思います。コードが整理されますし、余計な関数呼び出しも減らすことができます。
そういうわけで簡単なデモを作ろうと思います。今から作ります。実はこれを書いてる時点で出来上がっていません。サクッとなんか作ろうと思います。
かれこれ2時間くらいかかってVAOのサンプルができた
ふぅ、こんなもんでいいでしょ...
use VAO
// indexBufferで遊ぼう。
// Fを描く
// インスタンシング
let loopFunction;
function setup() {
createCanvas(400, 400, WEBGL);
const vs_drawF =
`#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec2 aOffsetPosition;
layout (location = 2) in float aOffsetAngle;
uniform float uTime;
void main(){
vec2 p = aPosition;
p = (p - vec2(1.5, 2.5)) / 48.0;
float t = aOffsetAngle + uTime;
mat2 rotMat = mat2(cos(t), -sin(t), sin(t), cos(t));
gl_Position = vec4((p*rotMat) + aOffsetPosition, 0.0, 1.0);
}
`;
const fs_drawF =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(0.3, 0.5, 0.9, 0.0);
}
`;
const vs_drawTriangle =
`#version 300 es
layout (location = 3) in vec2 aPosition;
layout (location = 2) in vec3 aColor;
uniform float uTime;
out vec3 vColor;
void main(){
vec2 p = aPosition;
float t = uTime;
p *= mat2(cos(t), -sin(t), sin(t), cos(t));
vColor = aColor;
gl_Position = vec4(p, 0.0, 1.0);
}
`;
const fs_drawTriangle =
`#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
fragColor = vec4(vColor, 0.5);
}
`;
const gl = this._renderer.GL;
const pg0 = createShaderProgram(gl, {
vs:vs_drawF, fs:fs_drawF
});
const vao0 = gl.createVertexArray();
gl.bindVertexArray(vao0); // 記録開始
// アトリビュートで頂点の位置を決める
const positions = new Float32Array(48);
for(let y=0; y<6; y++){
for(let x=0; x<4; x++){
positions[2*(x+y*4)] = x;
positions[2*(x+y*4)+1] = y;
}
}
const pBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pBuf);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 今回は位置と回転をいじる
const pOffsets = new Float32Array(2000);
const rOffsets = new Float32Array(1000);
for(let i=0; i<1000; i++){
pOffsets[2*i] = random(-1,1);
pOffsets[2*i+1] = random(-1,1);
const angle = random(TAU);
rOffsets[i] = angle;
}
const pOffsetBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pOffsetBuf);
gl.bufferData(gl.ARRAY_BUFFER, pOffsets, gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const rOffsetBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, rOffsetBuf);
gl.bufferData(gl.ARRAY_BUFFER, rOffsets, gl.STATIC_DRAW);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// インデックスの配列。これとTRIANGLESを組み合わせる。
const indexArray = [
// F. 54 indices
0,1,4, 4,1,5, 4,5,8, 8,5,9, 8,9,12, 12,9,13,
9,10,13, 13,10,14, 10,11,14, 14,11,15, 12,13,16, 16,13,17,
16,17,20, 20,17,21, 17,18,21, 21,18,22, 18,19,22, 22,19,23,
];
// この時点ではなんにでも使える
const iBuf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indexArray), gl.STATIC_DRAW);
//gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
for(let i=0; i<3; i++){
gl.enableVertexAttribArray(i);
if(i>0){
gl.vertexAttribDivisor(i, 1);
}
}
gl.bindVertexArray(null); // 記録終了
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); // 記録終了後にbindを解除する
// ------------- //
const pg1 = createShaderProgram(gl, {
vs:vs_drawTriangle, fs:fs_drawTriangle
});
const vao1 = gl.createVertexArray();
gl.bindVertexArray(vao1); // 記録開始
const buf3 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf3);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(
[-sin(0),cos(0),-sin(TAU/3),cos(TAU/3),-sin(TAU*2/3),cos(TAU*2/3)]
),gl.STATIC_DRAW);
gl.enableVertexAttribArray(3);
gl.vertexAttribPointer(3,2,gl.FLOAT,false,0,0);
gl.bindBuffer(gl.ARRAY_BUFFER,null);
const buf4 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf4);
gl.bufferData(gl.ARRAY_BUFFER,new Uint8Array(
[0,0,0,0,128,0,128,255,128]
),gl.STATIC_DRAW);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2,3,gl.UNSIGNED_BYTE,true,0,0);
gl.bindBuffer(gl.ARRAY_BUFFER,null);
gl.bindVertexArray(null); // 記録終了
// 先にBLEND
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
loopFunction = (time) => {
// クリア
gl.clearColor(0.2, 0.2, 0.2, 1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.useProgram(pg0);
gl.bindVertexArray(vao0); // プログラムと順番はどっちでもいい
setUniformValue(gl, pg0, "1f", "uTime", time);
gl.drawElementsInstanced(gl.TRIANGLES, 54, gl.UNSIGNED_BYTE, 0, 1000);
// インデックスバッファのbind/unbindはもはや不要
gl.useProgram(pg1);
gl.bindVertexArray(vao1); // そのまま切り替えればOK
setUniformValue(gl, pg1, "1f", "uTime", time);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindVertexArray(null); // 念のため解除しておく
gl.flush();
}
}
function draw(){
loopFunction(millis()/1000);
}
function createShaderProgram(gl, params = {}){
/* 略 */
}
function getActiveUniforms(gl, pg){
/* 略 */
}
function getActiveAttributes(gl, pg){
/* 略 */
}
function setUniformValue(gl, pg, type, name){
/* 略 */
}
実行結果:
解説
こないだ作ったインスタンシングで「F」を大量に作るコードを改造して、オフセットだけインスタンス変数とし、時間ユニフォームでそれぞれ回転させるようにしました。で、それとは別におっきな三角形を回転させるコードを書いています。後半のpg1がそれです。ひねくれて3と2をこの順でロケーションにしているのはテストのためです。それぞれ、登録作業をVAOに記録しているんですが、pg0(大量の「F」の方)について、elementArrayBufferというかまあインデックスバッファの解除をVAOの解除後に実行しています。これの順序が逆になるとやばいので注意ですね。あとは、特に問題ないと思います。
恒常ループ内でインデックスバッファのバインドと解除を一切やってないのが分かるかと思います。また、共有されている2番スロットの使い道は本来きちんと切り替えないといけないのに、一切やってません。すべてVAOに委ねています。それでうまく回っています。文字通り、回っています。複数の描画機構を共存させるうえで、VAOは無くてはならない存在です。最後に念のためVAO枠をnullにしていますが、このコードの場合は不要です。ただ何らかの理由でvertexAttributeArrayをクリーンにしておきたい場合は必要かもしれないです。
おわりに
VAOは便利なんですが、じゃあp5はなんでVAOなんか使わなくてもありとあらゆる描画機構を共存させることができているかというと、まあ想像つくと思いますが、上でやったようなアトポンやenableをいちいち全部、必要なだけ実行しているから、というのが正解です。結局のところ、メソッド化されてしまえば手間はかからないも同然ですから、VAOの利点は消し飛んでしまうわけです。同じ理由で自分の自作ライブラリでもVAOは採用していません...が...
VAOの本当の利点はグローバル状態を必要なだけクリーンに保つことができることです。デフォルトの状態というのを常に保つことで状況を整理しやすくできるのがVAOの最大の利点です。ステートマシン方式はとかくいろんな状態が把握しづらくなってしまうので、頂点アトリビュート配列だけでも把握しやすくなっているとバグを防ぎやすいなど、いろいろ利点があります。不必要なスロットがenableになっていることで生じるバグも防げますし。
しかしこのシステムを考えた人は天才だと思います...bindして、解除する。同じやり方でセーブとロードを両方実現できる、頭のいいやり方です。参考にしたらいいコードを書けそうな気がします(思いつかないのであれですが)。
ここまでお読みいただいてありがとうございました。多分次の動的更新でアトリビュートは最後かと思います。バッファの中身を外部からいじるお話です。
自由研究:p5.jsにおけるVAAの状況を調べてみよう
先ほどp5.jsは描画のたびに必要なだけアトポンやenableを繰り出していると書きましたが、ほんとにそんな感じです。それを確かめるコードを書いてみましょう。
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
const gr = createGraphics(256, 256);
gr.background(64, 128, 255);
texture(gr);
//fill(255);
stroke(128, 255, 64);
//noStroke();
lights();
sphere(128);
console.log(getVAAState(gl, 0));
console.log(getVAAState(gl, 1));
console.log(getVAAState(gl, 2));
console.log(getVAAState(gl, 3));
console.log(getVAAState(gl, 4));
console.log(getVAAState(gl, 5));
}
function getVAAState(gl, i){
const enabled = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED);
const ab = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING);
const size = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE);
const type = getType(gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE));
const normalize = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED);
const stride = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE);
const offset = gl.getVertexAttribOffset(i, gl.VERTEX_ATTRIB_ARRAY_POINTER);
const divisor = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_DIVISOR);
const current = getCurrent(gl.getVertexAttrib(i, gl.CURRENT_VERTEX_ATTRIB));
return `${i}番は有効化されていま${(enabled ? "す":"せん")}。紐付けられたバッファは${(ab===null ? "ありません":"あります")}。sizeは${size}で、型は${type}です。正規化はしま${(normalize ? "す":"せん")}。strideは${stride}でoffsetは${offset}です。divisorは${divisor}です。disableの場合の既定値の型は${current.type}で、値は${current.values}から取られます。`;
}
// https://gist.github.com/szimek/763999
// FLOAT 5126
// HALF_FLOAT 5131
// UNSIGNED_BYTE 5121
// UNSIGNED_SHORT 5123
// BYTE 5120
// SHORT 5122
function getType(type){
switch(type){
case 5126: return "FLOAT";
case 5131: return "HALF_FLOAT";
case 5121: return "UNSIGNED_BYTE";
case 5123: return "UNSIGNED_SHORT";
case 5120: return "BYTE";
case 5122: return "SHORT";
}
return type;
}
function getCurrent(current){
// currentの型は3通り。
const values = new Array(4);
for(let i=0; i<4; i++)values[i] = current[i];
if(current instanceof Float32Array){
return {type:"FLOAT", values};
}
if(current instanceof Int32Array){
return {type:"INT", values};
}
if(current instanceof Uint32Array){
return {type:"UNSIGNED_INT", values};
}
return {type:"undefined", values};
}
こんな感じです。0~4がすべて開いています。すべてFLOATで、正規化無し、またstrideとoffsetは0です。別々のバッファから素直に取られています。いろんな描画の仕方を共存させたい場合、これが一番手っ取り早いです。UVとか、あるかないかわかんないものがあるとインターリーブもできないので。ちなみにアトリビュートの詳細に踏み込むのは面倒なのでやめておきます。なお、noStroke()で線が描画されないようにすると、
こんな風になります。実は線の描画が後なので、内容が上書きされているわけです。sizeが2のものがありますが、おそらくこれがUVですね。こんな感じで可視化するのも楽しいものです。
p5がVAOを採用できない理由は前回のあとがきで述べた内容がほぼそのまま当てはまるんですが、要するに柔軟性を獲得できないからですね。p5はお手軽楽ちん3D描画が売りなので、それと相性が悪いわけです。それだけのことです。
なお、setupの内容を次のように書き換えると...
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const gr = createGraphics(256, 256);
gr.background(64, 128, 255);
texture(gr);
//fill(255);
stroke(128, 255, 64);
noStroke();
lights();
sphere(128);
gl.bindVertexArray(null);
console.log(getVAAState(gl, 0));
console.log(getVAAState(gl, 1));
console.log(getVAAState(gl, 2));
console.log(getVAAState(gl, 3));
console.log(getVAAState(gl, 4));
console.log(getVAAState(gl, 5));
}
すべての状態がクリアされます。
そうなる理由はもはや説明するまでもないですね。これが何の役に立つのか分かりませんが、すっきりしていいなと思いました(小並感)。
p5はあらゆる処理を隠蔽しています。それが便利に感じられることもありますが、自分としては自分で管理できる部分の多い方が楽しいと感じてしまうので、相容れないのは仕方ないと改めて思いました。