three.jsのInstancedMeshでパーティクルシステムを作りたいと思います。
three.jsのバージョンはr135です。
コード全文と動くサンプルは次のリンクから確認できます。
aadebdeb/ThreeInstancedMeshParticleSystemExample: Example of Three.js InstancedMesh Particle System
InstancedMesh
を利用したパーティクルシステムのベースは以下のようになります。あらかじめパーティクルの最大数capacity
でInstancedMesh
を作成しておき、update
ごとに生存しているパーティクルの状態だけをInstancedMesh
の配列に先頭から詰め直しています。update
の最後でInstancedMesh.count
を設定することで生存しているパーティクルだけを描画するようにしています。このクラスではパーティクルの生成・廃棄だけを管理して、具体的なパーティクルの挙動はサブクラスでcreateParticle
とupdateParticle
を実装して決めるようにしてます。
class InstancedMeshParticleSystem {
constructor(
geometry, // 使用するGeometry
material, // 使用するMaterial
capacity, // インスタンスの最大数
{
birthRate = 100, // 1秒当たりに生成するパーティクル数
lifeExpectancy = 1.0, // パーティクルの寿命(秒)
lifeVariance = 0.0, // パーティクルの寿命の分散, [0, 1]
useColor = false, // パーティクルごとに色を設定するか
} = {})
{
this._capacity = capacity;
this.birthRate = birthRate;
this.lifeExpectancy = lifeExpectancy;
this.lifeVariance = lifeVariance;
this._useColor = useColor;
this.mesh = new THREE.InstancedMesh(geometry, material, capacity);
if (useColor) {
this.mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(3 * capacity), 3);
}
// パーティクルの状態を管理する配列
this._ages = new Array(this._capacity); // パーティクルの年齢(秒)
this._lifes = new Array(this._capacity); // パーティクルの寿命
this._alives = new Array(this._capacity); // パーティクルの生死
this._positions = new Array(this._capacity); // パーティクルの位置
this._colors = useColor ? new Array(this._capacity) : null; // パーティクルの色
for (let i = 0; i < this._capacity; ++i) {
this._ages[i] = 0.0;
this._lifes[i] = 0.0;
this._alives[i] = false;
this._positions[i] = new THREE.Vector3();
if (useColor) {
this._colors[i] = new THREE.Color();
}
}
// 再利用する変数
this._reuseMatrix = new THREE.Matrix4();
this._reuseColor = new THREE.Color();
}
get capacity() {
return this._capacity;
}
get useColor() {
return this._useColor;
}
update(deltaSeconds) {
let aliveCount = 0; // 生存しているパーティクルの数
let birthNum = this.birthRate * deltaSeconds; // 生成するパーティクルの数
for (let i = 0; i < this._capacity; ++i) {
if (this._alives[i]) {
const age = this._ages[i];
const life = this._lifes[i];
const currentAge = age + deltaSeconds;
if (currentAge <= life) {
// 生存しているパーティクルで、かつ寿命に達していないものは更新する
this._ages[i] = currentAge;
this.updateParticle(
{
matrix: this._reuseMatrix,
color: this._useColor ? this._reuseColor : null,
},
{
index: i,
age: currentAge,
life: life,
position: this._positions[i],
color: this._useColor ? this._colors[i] : null,
useColor: this._useColor,
deltaSeconds: deltaSeconds,
}
);
this.mesh.setMatrixAt(aliveCount, this._reuseMatrix);
this._positions[i].setFromMatrixPosition(this._reuseMatrix);
if (this._useColor) {
this.mesh.setColorAt(aliveCount, this._reuseColor);
this._colors[i].copy(this._reuseColor);
}
aliveCount += 1;
} else {
// 生存しているパーティクルで、かつ寿命を過ぎたものは殺す
this._alives[i] = false;
}
} else if (birthNum >= 1.0 || (birthNum > 0 && Math.random() <= birthNum)) {
// 死んでいるパーティクルで、かつまだ十分にパーティクルが作られていない場合は生成する
birthNum -= 1.0;
this._alives[i] = true;
this._ages[i] = 0.0;
this._lifes[i] = this.lifeExpectancy * (1.0 + (Math.random() * 2.0 - 1.0) * this.lifeVariance);
this.createParticle(
{
matrix: this._reuseMatrix,
color: this._useColor ? this._reuseColor : null,
},
{
index: i,
useColor: this._useColor,
}
);
this.mesh.setMatrixAt(aliveCount, this._reuseMatrix);
this._positions[i].setFromMatrixPosition(this._reuseMatrix);
if (this._useColor) {
this.mesh.setColorAt(aliveCount, this._reuseColor);
this._colors[i].copy(this._reuseColor);
}
aliveCount += 1;
}
}
// IntancedMeshの利用範囲を生存しているパーティクルだけにする
this.mesh.count = aliveCount;
this.mesh.instanceMatrix.needsUpdate = true;
if (this._useColor) {
this.mesh.instanceColor.needsUpdate = true;
}
}
// パーティクル生成のメソッドはサブクラスで実装する
createParticle(outputs, params) {
console.error('this method must be implemented in subclass.');
}
// パーティクル更新のメソッドはサブクラスで実装する
updateParticle(outputs, params) {
console.error('this method must be implemented in subclass.');
}
dispose() {
this.mesh.dispose();
}
}
InstancedMeshParticleSystem
を継承したクラスの一例として、この記事の最初にあるような3次元のグリッドに順々にパーティクルを生成するパーティクルシステムを作ると以下のようになります。createParticle
とupdateParticle
ではoutput
引数の値を更新することで各パーティクルのトランスフォームと色を更新しています。
// 単位球内に一様分布するランダムな点を生成する関数
function randomInSphere() {
const cosTheta = -2.0 * Math.random() + 1.0;
const sinTheta = Math.sqrt(1.0 - cosTheta * cosTheta);
const phi = 2.0 * Math.PI * Math.random();
const radius = Math.pow(Math.random(), 1.0 / 3.0);
return new THREE.Vector3(radius * sinTheta * Math.cos(phi), radius * sinTheta * Math.sin(phi), radius * cosTheta);
}
class GridInstancedMeshParticleSystem extends InstancedMeshParticleSystem {
constructor(geometry, material, capacity, options) {
super(geometry, material, capacity, options);
this._idCount = 0;
this._gridSize = 40;
this._gridSizeHalf = 0.5 * this._gridSize;
this._gridDivision = 20;
this._gridDivision2 = this._gridDivision * this._gridDivision;
this._gridSpacing = this._gridSize / this._gridDivision;
this._position = new THREE.Vector3();
this._quaternion = new THREE.Quaternion();
this._scale = new THREE.Vector3();
this._velocity = new THREE.Vector3();
this._velocities = new Array(capacity);
for (let i = 0; i < capacity; ++i) {
this._velocities[i] = new THREE.Vector3();
}
}
createParticle(output, parameters) {
const id = this._idCount++;
const x = id % this._gridDivision;
const y = Math.floor(id / this._gridDivision2 % this._gridDivision);
const z = Math.floor(id % this._gridDivision2 / this._gridDivision);
this._position.set(
x * this._gridSpacing - this._gridSizeHalf,
y * this._gridSpacing - this._gridSizeHalf,
z * this._gridSpacing - this._gridSizeHalf,
);
this._scale.setScalar(0);
output.matrix.compose(this._position, this._quaternion, this._scale);
if (this.useColor) {
output.color.setRGB(x / (this._gridSize - 1), y / (this._gridSize - 1), z / (this._gridSize - 1));
}
this._velocities[parameters.index].copy(randomInSphere());
}
updateParticle(output, parameters) {
this._position.copy(parameters.position);
this._velocity.copy(this._velocities[parameters.index]);
this._velocity.multiplyScalar(2.0 * parameters.deltaSeconds);
this._position.add(this._velocity);
this._scale.setScalar(Math.sin(Math.PI * parameters.age / parameters.life));
output.matrix.compose(this._position, this._quaternion, this._scale);
if (this.useColor) {
output.color.copy(parameters.color);
}
}
}
作成したパーティクルシステムは以下のように使用します。
const geometry = new THREE.SphereGeometry();
const material = new THREE.MeshLambertMaterial();
const particleSystem = new GridInstancedMeshParticleSystem(geometry, material, 10000, {
birthRate: 300,
lifeExpectancy: 2.0,
lifeVariance: 0.5,
useColor: true,
});
scene.add(particleSystem.mesh);
const clock = new THREE.Clock();
// 以下はレンダリングループ内での処理
const deltaSeconds = Math.min(0.1, clock.getDelta());
particleSystem.update(deltaSeconds);
THREE.Pointsでも同じようなパーティクルシステムを作る方法について書きました。
three.jsのPointsでパーティクルシステム - Qiita