2
2

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のPointsでパーティクルシステム

Posted at

以前の記事でthree.jsのInstanceMeshでパーティクルシステムを作る方法について書きました。
three.jsのInstanceMeshでパーティクルシステム - Qiita

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

ソースコード全文とデモは以下から確認できます。
aadebdeb/ThreePointsParticleSystemExample: Example of Three.js Points Particle System

パーティクルシステムのベースは次のようになります。THREE.Pointsに使用するジオメトリの配列をパーティクルの最大数capacityの分だけ作成しておきます。updateでの更新ごとにそのジオメトリに生存しているパーティクルの情報だけを先頭から詰めて、最後にBufferGeometry.setDrawRangeで生存しているパーティクルだけを描画するようにしています。PointsParticleSystemではパーティクルの生成と寿命の管理だけをして、挙動はサブクラスでcreateParticleupdateParticleを実装することで決めるようにして汎用性を高めています。

class PointsParticleSystem {
  constructor(
    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._geometry = new THREE.BufferGeometry();
    this._geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(3 * capacity), 3));
    this._geometry.setAttribute('pScale', new THREE.Float32BufferAttribute(new Float32Array(capacity), 1));
    if (useColor) {
      this._geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(3 * capacity), 3));
    }
    this.points = new THREE.Points(this._geometry, material);

    // パーティクルの状態を管理する配列
    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._scales = 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();
      this._scales[i] = 0.0;
      if (useColor) {
        this._colors[i] = new THREE.Color();
      }
    }
  }

  get capacity() {
    return this._capacity;
  }

  get useColor() {
    return this._useColor;
  }

  update(deltaSeconds) {
    let aliveCount = 0;
    let birthNum = this.birthRate * deltaSeconds;

    const output = {
      position: new THREE.Vector3(),
      scale: 0.0,
      color: this._useColor ? new THREE.Color() : null,
    };

    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(output, {
            index: i,
            age: currentAge,
            life: life,
            position: this._positions[i],
            scale: this._scales[i],
            color: this._useColor ? this._colors[i] : null,
            useColor: this._useColor,
            deltaSeconds: deltaSeconds,
          });

          this._copyOutput(i, aliveCount, output);
          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(output, {
          index: i,
          useColor: this._useColor,
        });

        this._copyOutput(i, aliveCount, output);
        aliveCount += 1;
      }
    }

    // Pointsの配列を更新する
    this._geometry.setDrawRange(0, aliveCount);
    this._geometry.attributes.position.needsUpdate = true;
    this._geometry.attributes.pScale.needsUpdate = true;
    if (this._useColor) {
      this._geometry.attributes.color.needsUpdate = true;
    }
  }

  _copyOutput(index, aliveCount, output) {
    this._positions[index].copy(output.position);
    this._scales[index] = output.scale;
    this._geometry.attributes.position.array[aliveCount * 3] = output.position.x;
    this._geometry.attributes.position.array[aliveCount * 3 + 1] = output.position.y;
    this._geometry.attributes.position.array[aliveCount * 3 + 2] = output.position.z;
    this._geometry.attributes.pScale.array[aliveCount] = output.scale;
    if (this._useColor) {
      this._colors[index].copy(output.color);
      this._geometry.attributes.color.array[aliveCount * 3] = output.color.r;
      this._geometry.attributes.color.array[aliveCount * 3 + 1] = output.color.g;
      this._geometry.attributes.color.array[aliveCount * 3 + 2] = output.color.b;  
    }
  }

  // パーティクルの初期化はサブクラスで実装する
  createParticle(output, parameters) {
    console.error('This method must be implemented in subclass.');
  }

  // パーティクルの更新はサブクラスで実装する
  updateParticle(output, parameters) {
    console.error('This method must be implemented in subclass.');
  }
}

PointsPointsMaterialを組み合わせて使うことが多いですが、PointsMaterialは頂点ごとにスケールを設定することができません。以下のようにPointsMaterialを拡張することで頂点ごとのスケールに対応させます。拡張方法はこの記事を参考にしました。

class ParticlePointsMaterial extends THREE.PointsMaterial {
  constructor(options) {
    super(options);
    this.onBeforeCompile = this._onBeforeCompile;
  }

  _onBeforeCompile(shader) {
    shader.vertexShader = shader.vertexShader.replace(
      '#include <color_pars_vertex>',
      `
      #include <color_pars_vertex>
      attribute float pScale;
      `,
    );

    shader.vertexShader = shader.vertexShader.replace(
      'gl_PointSize = size;',
      `
      gl_PointSize = size * pScale;
      `
    );
  }
}

PointsParticleSystemを継承したクラスを作ってみます。ここでは上に載せたような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 GridPointsParticleSystem extends PointsParticleSystem {
  constructor(material, capacity, options) {
    super(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._velocities = new Array(capacity);
    for (let i = 0; i < capacity; ++i) {
      this._velocities[i] = new THREE.Vector3();
    }

    this._velocity = 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);

    output.position.set(
      x * this._gridSpacing - this._gridSizeHalf,
      y * this._gridSpacing - this._gridSizeHalf,
      z * this._gridSpacing - this._gridSizeHalf,
    );
    output.scale = 0.0;
    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._velocity.copy(this._velocities[parameters.index]);
    this._velocity.multiplyScalar(2.0 * parameters.deltaSeconds);
    output.position.copy(parameters.position);
    output.position.add(this._velocity);
    output.scale = Math.sin(Math.PI * parameters.age / parameters.life);
    if (this.useColor) {
      output.color.copy(parameters.color);
    }
  }
}

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

const material = new ParticlePointsMaterial({
  size: 2.0,
  vertexColors: true,
});
const particleSystem = new GridPointsParticleSystem(material, 10000, {
  birthRate: 500,
  lifeExpectancy: 3.0,
  lifeVariance: 0.5,
  useColor: true,
});
scene.add(particleSystem.points);

const clock = new THREE.Clock();
// 以下、レンダリングループ内
const deltaSeconds = Math.min(0.1, clock.getDelta());
particleSystem.update(deltaSeconds);
2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?