はじめに
インスタンシングで、イッツダンシング!いぇいいぇい。
冗談はさておき、p5の枠組みでインスタンシングをやります。次の記事でVAOをやってたので参考にしようと思いました。
それで、例によって前回の続きです。
まあシェーダーは本家の物を使うべきなんですが、どうせ見栄えのする作品を作るのが目的ではない、ただのお遊びなので、これで問題ないです。それではさっそくいきます。
コード全文
p5.jsのバージョンは2.0.5です。
// 3D.
// インスタンシング。
const vs =
`#version 300 es
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec3 aOffsetPosition;
uniform mat3 uModel;
uniform float uScale;
uniform vec3 uEye;
uniform vec3 uViewX;
uniform vec3 uViewY;
uniform vec3 uViewZ;
uniform vec4 uProj; // fov,aspect,near,far
out vec3 vNormal;
void main(){
vNormal = aNormal;
vec3 p = aPosition * uModel;
p *= uScale;
p += aOffsetPosition;
p -= uEye;
vec3 q = vec3(dot(p, uViewX), dot(p, uViewY), dot(p, uViewZ));
float fov = uProj.x;
float aspect = uProj.y;
float near = uProj.z;
float far = uProj.w;
float factor = 1.0/tan(fov/2.0);
gl_Position = vec4(
q.x * aspect * factor,
q.y * factor,
((near+far)/(near-far)) * q.z + (2.0*near*far)/(near-far),
-q.z
);
}
`;
const fs =
`#version 300 es
precision highp float;
in vec3 vNormal;
out vec4 fragColor;
void main(){
fragColor = vec4(0.5+0.5*vNormal, 1.0);
}
`;
function setup(){
createCanvas(400,400,WEBGL);
const geom = new p5.Geometry();
const cv = (x,y,z) => createVector(x,y,z);
//geom.vertices.push(createVector(-50,-50,0), createVector(50,-50,0), createVector(-50,50,0), createVector(50,50,0));
//geom.vertexNormals.push(createVector(0,0,1),createVector(0,0,1),createVector(0,0,1),createVector(0,0,1));
//geom.faces.push([0,1,2],[2,1,3]);
geom.vertices.push(
cv(-1,-1,1), cv(1,-1,1), cv(-1,1,1), cv(1,1,1),
cv(-1,-1,-1), cv(1,-1,-1), cv(-1,1,-1), cv(1,1,-1)
);
geom.vertexNormals = geom.vertices.map((v) => v.copy().normalize());
geom.faces.push(
[0,1,2], [2,1,3], [0,4,5], [0,5,1], [1,5,7], [1,7,3],
[2,6,4], [2,4,0], [6,2,3], [6,3,7], [4,6,7], [4,7,5]
);
const eye = createVector(160,200,240);
const center = createVector(0,0,0);
const near = 20*sqrt(3);
const far = 2000*sqrt(3);
const aspect = width/height;
const fov = PI/3;
const v0 = center.copy().sub(eye).normalize(); // 視線方向単位ベクトル
const up = createVector(0,1,0); // 暫定上方向ベクトル
const viewX = v0.cross(up).normalize();
const viewZ = v0.copy().mult(-1).normalize();
const viewY = viewZ.cross(viewX).normalize();
const sh = createShader(vs, fs);
shader(sh);
const gl = drawingContext;
gl.enable(gl.CULL_FACE);
const data = new Float32Array(729*3);
let offset = 0;
for(let z=-4; z<=4; z++){
for(let y=-4; y<=4; y++){
for(let x=-4; x<=4; x++){
data[offset++] = x * 30;
data[offset++] = y * 30;
data[offset++] = z * 30;
}
}
}
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
model(geom);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.enableVertexAttribArray(2);
gl.vertexAttribDivisor(2, 1);
gl.bindVertexArray(null);
const loopFunction = () => {
const t = frameCount*TAU/120;
background(0);
const axis = createVector(1,1,1).normalize();
sh.setUniform("uModel", [
cos(t) + (1-cos(t))*axis.x*axis.x, (1-cos(t))*axis.x*axis.y - sin(t)*axis.z, (1-cos(t))*axis.z*axis.x +sin(t)*axis.y,
(1-cos(t))*axis.x*axis.y + sin(t)*axis.z, cos(t) + (1-cos(t))*axis.y*axis.y, (1-cos(t))*axis.y*axis.z - sin(t)*axis.x,
(1-cos(t))*axis.z*axis.x - sin(t)*axis.y, (1-cos(t))*axis.y*axis.z + sin(t)*axis.x, cos(t) + (1-cos(t))*axis.z*axis.z
]);
sh.setUniform("uScale", 5);
sh.setUniform("uEye", [eye.x, eye.y, eye.z]);
sh.setUniform("uViewX", [viewX.x, viewX.y, viewX.z]);
sh.setUniform("uViewY", [viewY.x, viewY.y, viewY.z]);
sh.setUniform("uViewZ", [viewZ.x, viewZ.y, viewZ.z]);
sh.setUniform("uProj", [fov, aspect, near, far]);
gl.bindVertexArray(vao);
//model(geom);
//gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
gl.drawElementsInstanced(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0, 729);
gl.bindVertexArray(null);
}
draw = loopFunction;
}
見てすぐわかると思いますが、インデントの変なところが追加部分です。分かりやすくていいですね。
729個のcubeです。おそらくこのくらいならドローコール連打でもそこまで遅くないはずです。興味のある人はベンチマーク取ってみてください。自分は忙しいのでやりません。
シェーダー部分
オフセットを作ります。vec3で、シェーダーにインスタンス変数を追加しておきます。
layout (location = 2) in vec3 aOffsetPosition;
...
uniform float uScale;
...
p *= uScale;
p += aOffsetPosition;
ついでにスケールもいじれるようにしました。調整がめんどくさい...あとカメラ、目線の位置をいじってあります。この2番スロットをインスタンスに使わせてもらいます。実は2番はp5が勝手に予約してしまう番号なんですが、今回p5の描画はしないのでここはフリーですし、どうせVAOで切り分けるので、問題ないですね。
データ用意
ここにデータをぶち込む準備をします。特にランダム性を持たせる必要は無いので格子状です。
const data = new Float32Array(729*3);
let offset = 0;
for(let z=-4; z<=4; z++){
for(let y=-4; y<=4; y++){
for(let x=-4; x<=4; x++){
data[offset++] = x * 30;
data[offset++] = y * 30;
data[offset++] = z * 30;
}
}
}
いつものようにバッファリングします。STATIC_DRAWでいいですね。固定です。
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
VAOの用意
ここからが問題なんですが、実はmodel関数の仕様は全く詳しくないです。わかっていることは、現在の仕様では描画が終了してもIBOのバインドはどうやら解除されていないようです(完全上書き)。そういうわけなので、VAOで挟めば記録されます。このデータとアトリビュートさえあればドローコール一本でジオメトリは復元できます。そういうわけで堂々とVAOサンドイッチでmodelを挟みます。ついでにアトポン、enable,divisor,OK.準備完了です。
いけ!
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
model(geom);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.enableVertexAttribArray(2);
gl.vertexAttribDivisor(2, 1);
gl.bindVertexArray(null);
ドローコール
いけ!
gl.bindVertexArray(vao);
//model(geom);
//gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
gl.drawElementsInstanced(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0, 729);
gl.bindVertexArray(null);
おわりに
インスタンシングはお手軽に大量の複製コピーを作れるので楽しいですねぇ。
ここまでお読みいただいてありがとうございました。
これ使うとインスタンシングできるらしい;
