WebGL2のTransform Feedbackを調べると大量のパーティクルを動かす例をよく見かけますが、これにGeometry Instancingを組み合わせると大量のメッシュを動かすことができるのではと思い、試してみました。
作ったものは以下のようになります。球メッシュに重力で力を加えて地面と壁で反射するようにしてまいます。
https://aadebdeb.github.io/Sample_WebGL2_GeometryInstancing_with_TransformFeedback/index.html
ソースコードはGithubに置いておきました。
https://github.com/aadebdeb/Sample_WebGL2_GeometryInstancing_with_TransformFeedback
最初に初期化処理おこないます。
まず、球メッシュの頂点の位置と法線を格納するVBOと頂点インデックスを格納するIBOを作成します。この球メッシュをGeometry Instancingで大量に描画します。
sphere = createSphere(data.sphere.radius, data.sphere.thetaSegment, data.sphere.phiSegment);
sphereIbo = createIbo(gl, sphere.indices);
vertexPositionVbo = createVbo(gl, sphere.positions);
vertexNormalVbo = createVbo(gl, sphere.normals);
その後、各球の位置と速度、色を格納するVBOを作成します。位置と速度はTransform Feedbackで更新するのでVBOを2つ作成しておきます。R
のほうが読み込み用(read)、W
のほうが書き込み用(write)です。
Transform Feedbackで位置と速度の更新をおこなうたびにswapParticleVbos
関数で書き込み用と読み込み用のVBOを入れ替えます。
sphereNum = data.num;
const positions = new Float32Array(Array.from({length: sphereNum * 3}, () => {
return (2.0 * Math.random() - 1.0);
}));
const velocities = new Float32Array(sphereNum * 3);
for (let i = 0; i < sphereNum; i++) {
velocities[3 * i] = (2.0 * Math.random() - 1.0) * 0.5;
velocities[3 * i + 1] = 0.0;
velocities[3 * i + 2] = (2.0 * Math.random() - 1.0) * 0.5;
}
const colors = new Float32Array(sphereNum * 3);
for (let i = 0; i < sphereNum; i++) {
const rgb = hsvToRgb(Math.floor(Math.random() * 360.99), 1, 1);
colors[3 * i] = rgb[0];
colors[3 * i + 1] = rgb[1];
colors[3 * i + 2] = rgb[2];
}
positionVboR = createVbo(gl, positions, gl.DYNAMIC_COPY);
velocityVboR = createVbo(gl, velocities, gl.DYNAMIC_COPY);
positionVboW = createVbo(gl, new Float32Array(sphereNum * 3), gl.DYNAMIC_COPY);
velocityVboW = createVbo(gl, new Float32Array(sphereNum * 3), gl.DYNAMIC_COPY);
colorVbo = createVbo(gl, colors);
const swapParticleVbos = function() {
const tmpP = positionVboR;
const tmpV = velocityVboR;
positionVboR = positionVboW;
velocityVboR = velocityVboW;
positionVboW = tmpP;
velocityVboW = tmpV;
}
初期化処理を行った後にレンダリングループでTransform Feedbackによる各球の位置と速度の更新、Geometry Instancingによるレンダリングを繰り返しおこないます。
Transform Feedbackで位置と速度の更新をおこなうJavaScriptのコードと頂点シェーダーは以下のようになります。positionVboR
とvelocityVboR
を入力にして、positionVboW
とvelocityVboW
が出力になるようにします。更新が終了したらswapParticleVbos
でVBOを入れ替えます。
gl.useProgram(updateProgram);
gl.uniform1f(updateUniforms['u_deltaTime'], elapsedTime);
[positionVboR, velocityVboR].forEach((vbo, i) => {
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.enableVertexAttribArray(i);
gl.vertexAttribPointer(i, 3, gl.FLOAT, false, 0, 0);
});
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, positionVboW);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, velocityVboW);
gl.enable(gl.RASTERIZER_DISCARD);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, sphereNum);
gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
swapParticleVbos();
#version 300 es
layout (location = 0) in vec3 i_position;
layout (location = 1) in vec3 i_velocity;
out vec3 o_position;
out vec3 o_velocity;
uniform float u_deltaTime;
vec3 GRAVITY_FORCE = vec3(0.0, -9.8, 0.0);
float MASS = 10.0;
void main(void) {
vec3 velocity = i_velocity;
vec3 position = i_position;
velocity += u_deltaTime * GRAVITY_FORCE / MASS;
position += u_deltaTime * velocity;
if (position.x <= -1.0 || position.x >= 1.0) {
velocity.x *= -1.0;
position.x += u_deltaTime * velocity.x;
}
if (position.y <= -1.0 || position.y >= 1.0) {
velocity.y *= -1.0;
position.y += u_deltaTime * velocity.y;
}
if (position.z <= -1.0 || position.z >= 1.0) {
velocity.z *= -1.0;
position.z += u_deltaTime * velocity.z;
}
o_position = position;
o_velocity = velocity;
}
Geometry Instancingでレンダリングを行うためのJavaScriptのコードと頂点シェーダーは以下のようになっています。Transform Feedbackで先ほど出力先にしたpositionVboR
(すでにswap済みなのでW
ではなくR
)をそのままレンダリングに利用しています。頂点シェーダーではposition
に球メッシュの各頂点の位置(vertexPositionVbo内の値)が、instancePosition
に球の中心位置(positionVboR
内の値)が入っているので、加算して最終的な頂点位置を求めています。
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0.0, 0.0, canvas.width, canvas.height);
gl.useProgram(renderProgram);
gl.uniformMatrix4fv(renderUniforms['u_mvpMatrix'], false, mvpMatrix.elements);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereIbo);
[vertexPositionVbo, vertexNormalVbo, colorVbo, positionVboR].forEach((vbo, i) => {
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.enableVertexAttribArray(i);
gl.vertexAttribPointer(i, 3, gl.FLOAT, false, 0, 0);
});
gl.vertexAttribDivisor(2, 1);
gl.vertexAttribDivisor(3, 1);
gl.drawElementsInstanced(gl.TRIANGLES, sphere.indices.length, gl.UNSIGNED_SHORT, 0, sphereNum);
#version 300 es
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec3 color;
layout (location = 3) in vec3 instancePosition;
out vec3 v_color;
out vec3 v_normal;
uniform mat4 u_mvpMatrix;
void main(void) {
v_color = color;
v_normal = (u_mvpMatrix * vec4(normal, 0.0)).xyz;
vec3 pos = position + instancePosition * 500.0;
gl_Position = u_mvpMatrix * vec4(pos, 1.0);
}
パフォーマンスの比較用に同じ操作をGeometry Instancingのみ、Transform Feedbackのみ、Geometry InstancingもTransform Feedbackもなしで行うバージョンを作りました。
こうしてみるとGeometry Instancingの有無はパフォーマンスに影響しますが、Transform Feedbackはそこまで変わらないですね...