three.jsのInstanceMeshでパーティクルシステム

three.jsの[InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh)でパーティクルシステムを作りたいと思います。
![screenshot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/7da90ed6-588b-259d-32a6-649d8ec11b1c.gif)
three.jsのバージョンはr135です。

コード全文と動くサンプルは次のリンクから確認できます。
[aadebdeb/ThreeInstancedMeshParticleSystemExample: Example of Three\.js InstancedMesh Particle System](https://github.com/aadebdeb/ThreeInstancedMeshParticleSystemExample)

`InstancedMesh`を利用したパーティクルシステムのベースは以下のようになります。あらかじめパーティクルの最大数`capacity`で`InstancedMesh`を作成しておき、`update`ごとに生存しているパーティクルの状態だけを`InstancedMesh`の配列に先頭から詰め直しています。`update`の最後で`InstancedMesh.count`を設定することで生存しているパーティクルだけを描画するようにしています。このクラスではパーティクルの生成・廃棄だけを管理して、具体的なパーティクルの挙動はサブクラスで`createParticle`と`updateParticle`を実装して決めるようにしてます。

```js
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`引数の値を更新することで各パーティクルのトランスフォームと色を更新しています。


```js
// 単位球内に一様分布するランダムな点を生成する関数
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);
    }
  }
}
```

作成したパーティクルシステムは以下のように使用します。

```js
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](https://qiita.com/aa_debdeb/items/3ab85f8d2a472c93f7ea)