5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-12-12

three.jsのInstancedMeshでパーティクルシステムを作りたいと思います。
screenshot.gif
three.jsのバージョンはr135です。

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

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

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次元のグリッドに順々にパーティクルを生成するパーティクルシステムを作ると以下のようになります。createParticleupdateParticleでは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

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?