はじめに
インターリーブです。すでにそういうコードを見ているかもしれませんが、きちんとやります。wgldの次の記事は参考になります。
インターリーブ配列 VBO
webglにおけるインターリーブについての次の記事も参考になると思います。
GPU本来の性能を引き出すWebGL頂点データ作成法
速度とかパフォーマンスとかそういう話が書いてあります。冒頭にあるように非インターリーブの方が現在では高速なようです。ここでやりたいことはインターリーブのサンプルを用意して雰囲気を感じてもらうことです。速度とか難しいことはよく分かんないのでここでは議論しません。緩くやるのが目的なので、ベンチマークとかそういう話はよそでお願いします。
引き続きp5.jsでやります。理由は便利だからです。
インターリーブをやる利点としては、そういうコードがあったときに読み解けるよう、というのもあっていいと思います。ネイティブwebGLは割とやりたい放題やってるので。幅を広げるのは良い事です。
基本は終わってるので、それが押さえられていれば難しくない内容です。サクサク行きましょう。れっつご!
コード全文
// やってみよう。アトリビュート。
// インスタンシング
// 楽。
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/drawArraysInstanced
// 三角形いっぱい描いておわり
// ちょっといじってインターリーブ
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
const vs =
`#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec2 aOffsetPosition;
layout (location = 2) in mat2 aOffsetRotation;
layout (location = 4) in vec4 aOffsetColor;
out vec4 vColor;
void main(){
vColor = aOffsetColor;
gl_Position = vec4((aPosition * aOffsetRotation)*0.07 + aOffsetPosition, 0.0, 1.0);
}
`;
const fs =
`#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main(){
fragColor = vColor;
}
`;
const pg = createShaderProgram(gl, {
vs, fs
});
// まず位置
const pBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pBuf);
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.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// インスタンシング用の位置と回転と色を1000個分
// 8-8-4(4-4-2-2-2-2-1-1-1-1)
const ab = new ArrayBuffer(20000);
const dw = new DataView(ab);
colorMode(HSB,1);
for(let i=0; i<1000; i++){
const x = random(-1,1);
const y = random(-1,1);
const t = random(TAU);
const c = floor(cos(t)*32767);
const s = floor(sin(t)*32767);
const col = color((y+1)*0.5, 0.3+(x+1)*0.35, 0.3+(x+1)*0.35);
const r = floor(red(col));
const g = floor(green(col));
const b = floor(blue(col));
const offset = i*20;
dw.setFloat32(offset, x, true);
dw.setFloat32(offset+4, y, true);
dw.setInt16(offset+8, c, true);
dw.setInt16(offset+10, -s, true);
dw.setInt16(offset+12, s, true);
dw.setInt16(offset+14, c, true);
dw.setUint8(offset+16, r);
dw.setUint8(offset+17, g);
dw.setUint8(offset+18, b);
dw.setUint8(offset+19, 0);
}
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, ab, gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 0);
gl.vertexAttribPointer(2, 2, gl.SHORT, true, 20, 8);
gl.vertexAttribPointer(3, 2, gl.SHORT, true, 20, 12);
gl.vertexAttribPointer(4, 4, gl.UNSIGNED_BYTE, true, 20, 16);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 準備完了
for(let i=0; i<5; i++){
gl.enableVertexAttribArray(i);
if(i>0){
gl.vertexAttribDivisor(i, 1); // 1~4に対して実行
}
}
// 描くか。
gl.useProgram(pg);
gl.clearColor(0.1,0.1,0.1,1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// インスタンシング
// 雑にBLEND
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 1000);
gl.flush();
}
function createShaderProgram(gl, params = {}){
/* 略 */
}
// レイアウトの指定。各attributeを配列のどれで使うか決める。
// 指定しない場合はデフォルト値が使われる。基本的には通しで0,1,2,...と付く。
function setAttributeLayout(gl, pg, layout = {}){
/* 略 */
}
function getActiveAttributes(gl, pg){
/* 略 */
}
function setUniformValue(gl, pg, type, name){
/* 略 */
}
実行結果:
ちなみにvertexAttribPointerのレファレンスでもインターリーブをやっています。こちらは3D描画の内容ですが、実質的には似たようなものです。
vertexAttribPointer
ArrayBufferとDataView
インデックスバッファあたりからたびたび言及していますが、エレメント描画やアトリビュートに出てくるwebGLBufferの実体はバイト列です。ここに4バイトやら2バイトやら1バイトの情報を詰め込んで、然るべきやり方でフェッチすることで、インデックスを決定したり、バッファからデータを取り出すわけです。
今回は、前回のインスタンシングのコードをいじって、いろんなパラメータで合計1000個描画します。いじるのは位置と回転と色です。次のように格納しようと思います。
位置はFLOATを2つ用意します。vec2です。回転は、SHORTの正規化で4つ用意します。2x2行列なので連番で2つのロケーションを占有します。色は通常のバイトの正規化で4つ用意します。これで8,8,4バイト、ひとつあたり20バイトとなります。シェーダーでの利用:
#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec2 aOffsetPosition;
layout (location = 2) in mat2 aOffsetRotation;
layout (location = 4) in vec4 aOffsetColor;
out vec4 vColor;
void main(){
vColor = aOffsetColor;
gl_Position = vec4((aPosition * aOffsetRotation)*0.07 + aOffsetPosition, 0.0, 1.0);
}
#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main(){
fragColor = vColor;
}
色をそのまま渡しているだけです。ポイントがあるとすれば三角形を小さくしていることと、位置に回転の行列を「右から」掛けていることくらいですね。まあ回転行列なので結果だけ見ればどっちでもいいんですが、正確な理解をおろそかにするといずれ足をすくわれます。きちんとした理解は大事です。
このような詰める行為を楽々実現するための仕組みがArrayBufferとDataViewです。IEEEの流儀に則ったバイト列変換なんてまだるっこしいことをしてられないからです。
DataView
前にも述べたと思いますが、webGLは徹底的にリトルエンディアンです。なので引数のtrueは必須で、無いと確実にバグります。まず20バイト掛ける1000個なので20000バイト分の領域を確保します。そこからDataViewを生成しておきます。
// インスタンシング用の位置と回転と色を1000個分
// 8-8-4(4-4-2-2-2-2-1-1-1-1)
const ab = new ArrayBuffer(20000);
const dw = new DataView(ab);
DataViewは、まさに上で図解したようなバイト単位での詰め込みを極めて容易に実現するための魔法の道具です。こんな風に:
colorMode(HSB,1);
for(let i=0; i<1000; i++){
const x = random(-1,1);
const y = random(-1,1);
const t = random(TAU);
const c = floor(cos(t)*32767);
const s = floor(sin(t)*32767);
const col = color((y+1)*0.5, 0.3+(x+1)*0.35, 0.3+(x+1)*0.35);
const r = floor(red(col));
const g = floor(green(col));
const b = floor(blue(col));
const offset = i*20;
dw.setFloat32(offset, x, true);
dw.setFloat32(offset+4, y, true);
dw.setInt16(offset+8, c, true);
dw.setInt16(offset+10, -s, true);
dw.setInt16(offset+12, s, true);
dw.setInt16(offset+14, c, true);
dw.setUint8(offset+16, r);
dw.setUint8(offset+17, g);
dw.setUint8(offset+18, b);
dw.setUint8(offset+19, 0);
}
レインボーを作るにはp5.jsのcolorModeが便利なのでそれでやっています。バイト単位でのオフセットと値、それにtrueを指定するだけなので極めて容易です。行列の成分は32767を掛けて整数にすることでSHORTとして扱います。アルファのところが0になっているのは間違いではないです。このあとのブレンド用です。
頂点アトリビュート配列への登録
DataViewを使って望みのArrayBufferが得られたので、登録作業をします。
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, ab, gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 0);
gl.vertexAttribPointer(2, 2, gl.SHORT, true, 20, 8);
gl.vertexAttribPointer(3, 2, gl.SHORT, true, 20, 12);
gl.vertexAttribPointer(4, 4, gl.UNSIGNED_BYTE, true, 20, 16);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
さっきのArrayBufferをbufferします。アトポンは行列で2と3を用いていることくらいしか説明すべきことがないですね...上の図と、これまで説明したアトリビュートの基本に則って考えれば難しい所はないと思います。いくつもバッファを用意しなくていいので楽ちんですね。インターリーブいいですね~。
描画
さっさと描いてしまおう。その前にdivisorを設定してこれらがインスタンシング用であることを通知しておきます。1,2,3,4がそういうアトリビュートです。よろしく!
// 準備完了
for(let i=0; i<5; i++){
gl.enableVertexAttribArray(i);
if(i>0){
gl.vertexAttribDivisor(i, 1); // 1~4に対して実行
}
}
有効化と通知が終わったので描画します。
// 描くか。
gl.useProgram(pg);
gl.clearColor(0.1,0.1,0.1,1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// インスタンシング
// 雑にBLEND
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 1000);
gl.flush();
前回ちょこっとだけ使ったBLENDを今回も使っているんですが、あんま気にしなくていいです。絵作りへたくそなので。Fでやっても良かったんですが、あんまあれこれ詰めたくないんですよね。焦点がぼやけるので。それでただの三角形です。でもインターリーブの雰囲気は伝わったかと思います。
すごくどうでもいいですが。DataViewをdvではなくdwとしているのはdvという文字の並びがいい印象を与えないからです。余談終わり。こういうの気にするんですよね。数学やってると。
おわりに
インスタンシングがp5.jsに導入される日は来るのか?実はもう導入されています。
Add support for webGL instancing #6276
EndShape
しかしイミディエイトでしか使えないので遅いし、シェーダーを書かないといけない、まあそれは当然ですが、InstanceIDしか使えません。ユーザーが...p5はユーザーがアトリビュートの知識を持っていることを仮定出来無いので、限界があるわけです。
しかし前回見たように、実際はドローコールをちょっといじってグローバルステートをちょっといじるだけのことです。アトリビュートの知識が仮定出来ないために封印されている技術ですが、実行すること自体は難しくないです。
これ以上の言及は避けます...と言いたいところですが、開発の現場が気になる人は上記のプルリクエストをよく読んでみるといいと思います。なおこのプルリクエストのコントリビュータのRandomGamingDevは実装の提案時になぜかサンプルを用意しなかったりすることが当たり前の問題児で、今回もそのことを指摘されていますね...それはおいといて、結局何が難しいかというと、開発サイドがどこまで自由にやるのか、どこから先をユーザーに委ねるかの線引きができないからです。ユーザーに何をさせたいのか、ユーザーは何がしたいのか、その辺があやふやな状態で議論を進めなければならないからです。加えて今回の場合、ユーザーはシェーダーやアトリビュートの知識が皆無であることを想定しているので、アトリビュートの本質に深くかかわる今回のようなフィーチャーは絶望的に向いてません。霧に包まれた的を目隠ししながら矢で射貫くような無謀なことをやっているわけです。それで苦労して実装したところで誰もその機能を使いません。開発はそういう感じで行なわれています。興味がある人は取り組んでみてください。楽しいですよ!
ここまでお読みいただいてありがとうございました。