Chapter 54

パーティクル

miku
miku
2021.11.16に更新

この章ではエフェクトの表現でよく利用されるパーティクルについて扱う。

基本

const Particle = {
  init: function (x, y, vx, vy, radius) {
    return { x, y, vx, vy, radius };
  }
};

パーティクルに最低限必要なのは位置と速度で、その他描画に必要な情報を入れる。

particles = [];
for (let i = 0; i < 100; i++) {
  const vx = random(-3, 3);
  const vy = random(-3, 3);
  const particle = Particle.init(width / 2, height / 2, vx, vy, 15);
  particles.push(particle);
}

複数のパーティクルオブジェクトを作成し、配列に格納する。

particles.forEach((p) => {
  Particle.update(p); // 更新
  Particle.draw(p); // 描画
});

const Particle = {
  update: function (p) {
    p.x += p.vx;
    p.y += p.vy;
  },

  draw: function (p) {
    circle(p.x, p.y, p.radius * 2);
  },
};

パーティクルの更新と描画を毎フレーム行う。

let particles;

function setup() {
  createCanvas(windowWidth, windowHeight);

  particles = [];
  for (let i = 0; i < 100; i++) {
    const vx = random(-3, 3);
    const vy = random(-3, 3);
    const particle = Particle.init(width / 2, height / 2, vx, vy, 15);
    particles.push(particle);
  }
}

function draw() {
  clear();

  particles.forEach((p) => {
    Particle.update(p);
    Particle.draw(p);
  });
}

const Particle = {
  init: function (x, y, vx, vy, radius) {
    return { x, y, vx, vy, radius };
  },

  update: function (p) {
    p.x += p.vx;
    p.y += p.vy;
  },

  draw: function (p) {
    noStroke();
    fill(240);
    circle(p.x, p.y, p.radius * 2);
  },
};

パーティクルの大きさと透明度を変更

let particles;

function setup() {
  createCanvas(windowWidth, windowHeight);

  particles = [];
  for (let i = 0; i < 100; i++) {
    const vx = random(-3, 3);
    const vy = random(-3, 3);
    const radius = random(4, 30);
    const opacity = random(40, 255);
    const particle = Particle.init(width / 2, height / 2, vx, vy, radius, opacity);
    particles.push(particle);
  }
}

function draw() {
  clear();

  particles.forEach((p) => {
    Particle.update(p);
    Particle.draw(p);
  });
}

const Particle = {
  init: function (x, y, vx, vy, radius, opacity) {
    return { x, y, vx, vy, radius, opacity };
  },

  update: function (p) {
    p.x += p.vx;
    p.y += p.vy;
  },

  draw: function (p) {
    noStroke();
    fill(240, p.opacity);
    circle(p.x, p.y, p.radius * 2);
  },
};

パーティクルの情報に透明度をもたせて、一つ一つのパーティクルの透明度を変更する作例。

一つずつパーティクルを出す

const interval = 10;
const maxParticle = 100;
let particles;

function setup() {
  createCanvas(windowWidth, windowHeight);

  particles = [];
}

function draw() {
  clear();

  if (frameCount % interval === 0 && particles.length < maxParticle) {
    addParticle();
  }

  particles.forEach((p) => {
    Particle.update(p);
    Particle.draw(p);
  });
}

function addParticle() {
  const vx = random(-3, 3);
  const vy = random(-3, 3);
  const radius = random(4, 30);
  const opacity = random(40, 255);
  const particle = Particle.init(width / 2, height / 2, vx, vy, radius, opacity);
  particles.push(particle);
}

const Particle = {
  init: function (x, y, vx, vy, radius, opacity) {
    return { x, y, vx, vy, radius, opacity };
  },

  update: function (p) {
    p.x += p.vx;
    p.y += p.vy;
  },

  draw: function (p) {
    noStroke();
    fill(240, p.opacity);
    circle(p.x, p.y, p.radius * 2);
  },
};

最初に配列にまとめてパーティクルのオブジェクトを追加するのをやめ、パーティクルを生成して配列に追加する部分を関数化する。

if (frameCount % interval === 0 && particles.length < maxParticle) {
  addParticle();
}

あとは定期的にパーティクルを追加するため frameCountinterval の倍数になるたびにという条件と、無尽蔵にパーティクルが増えるのを防ぐため、パーティクルの上限値 maxParticle を定義する。

パーティクルを範囲外で削除する

const interval = 10;
const maxParticle = 100;
let particles;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();

  particles = [];

  textAlign(CENTER);
}

function draw() {
  clear();

  fill(240);
  text(`${particles.length} / ${maxParticle}`, width / 2, height / 2);

  if (frameCount % interval === 0 && particles.length < maxParticle) {
    addParticle();
  }

  for (const p of particles) {
    Particle.update(p);
    Particle.draw(p);
  }

  particles = particles.filter((p) => {
    return p.x + p.radius >= 0 && p.x - p.radius < width && p.y + p.radius >= 0 && p.y - p.radius < height;
  });
}

function addParticle() {
  const vx = random(-3, 3);
  const vy = random(-3, 3);
  const radius = random(4, 30);
  const opacity = random(40, 255);
  const particle = Particle.init(width / 2, height / 2, vx, vy, radius, opacity);
  particles.push(particle);
}

const Particle = {
  init: function (x, y, vx, vy, radius, opacity) {
    return { x, y, vx, vy, radius, opacity };
  },

  update: function (p) {
    p.x += p.vx;
    p.y += p.vy;
  },

  draw: function (p) {
    fill(240, p.opacity);
    circle(p.x, p.y, p.radius * 2);
  },
};

パーティクルが画面外に移動するともはや描画する必要はないので、配列から削除する必要がある。

particles = particles.filter((p) => {
  return p.x + p.radius >= 0 && p.x - p.radius < width && p.y + p.radius >= 0 && p.y - p.radius < height;
});

そのためには Array#filter() を利用する便利だ。filter() に指定した関数の戻り値が false になるならそのオブジェクトが配列から削除されるので、配列に残すための条件、つまり画面内にオブジェクトがあるなら、と記述すればいい。

パーティクルのライフスパン

const interval = 10;
const maxParticle = 100;
let particles;

function setup() {
  createCanvas(windowWidth, windowHeight);
  textAlign(CENTER);
  noStroke();

  particles = [];
}

function draw() {
  clear();

  fill(240);
  text(`${particles.length} / ${maxParticle}`, width / 2, height / 2);

  if (frameCount % interval === 0 && particles.length < maxParticle) {
    addParticle();
  }

  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    Particle.update(p);
    Particle.draw(p);
  }

  particles = particles.filter((p) => p.lifespan > 0);
}

function addParticle() {
  const vx = random(-3, 3);
  const vy = random(-3, 3);
  const radius = random(4, 30);
  const lifespan = 255;
  const damage = random(1, 3);
  const particle = Particle.init(width / 2, height / 2, vx, vy, radius, lifespan, damage);
  particles.push(particle);
}

const Particle = {
  init: function (x, y, vx, vy, radius, lifespan, damage) {
    return { x, y, vx, vy, radius, lifespan, damage };
  },

  update: function (p) {
    p.x += p.vx;
    p.y += p.vy;
    p.lifespan -= p.damage;
    p.lifespan = max(p.lifespan, 0);
  },

  draw: function (p) {
    fill(240, p.lifespan);
    circle(p.x, p.y, p.radius * 2);
  },
};

パーティクルを削除する方法としてパーティクルに寿命を持たせるという方法がある。プロパティに lifespandamage を用意して、毎フレーム lifespan から damage を引く。lifespan0 以下になるとパーティクルが死んだと判定して配列から削除する。なんなら lifespan の値を透明度に設定すると機能と見た目が両立していて都合が良い。

ストロークによるパーティクルの表現

let circles;

function setup() {
  createCanvas(windowWidth, windowHeight);

  circles = [];
  for (let i = 0; i < 50; i++) {
    const c = {};
    reset(c);
    circles.push(c);
  }
}

function reset(c) {
  c.x = random(width);
  c.y = random(height);
  c.r = random(10, 30);
  c.weight = random(10, 30);
}

function draw() {
  clear();

  circles.forEach((c) => {
    c.r += 0.1;
    c.weight -= 0.1;
    if (c.weight <= 0) {
      reset(c);
    }
    drawCircle(c);
  });
}

function drawCircle(c) {
  strokeWeight(c.weight);
  stroke(240);
  noFill();
  circle(c.x, c.y, c.r * 2);
}

オブジェクトを移動させなくても、ストロークの太さを変化させることでパーティクルの表現を行うことができる。