エンジニア引き止めセミナー資料
エンジニア引き止めセミナー資料

Threejsを使って画像からパーティクルアニメーションを実装する

くりちゃん

こんにちは、くりちゃんです。

今回はThree.jsを使って画像からパーティクルアニメーションを実装したいと思います。パーティクルアニメーションを実装するうえでパーティクル一個一個の位置情報は絶対必要になりますよね。前回の記事ではジオメトリーを元に位置情報を設定していましたが、今回は画像から位置情報を抜き取ります。


そこでCanvasAPIのgetImageDataというメソッドを使用してcanvas要素からピクセル情報をImageDataオブジェクトの形式で取り出します。

試しにLIGのロゴ画像を用意してピクセル情報を取得してみます。

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = 868;
const height = 464;
canvas.width = width;
canvas.width = width;
const image = new Image();
image.src = '/path/lig.png';
image.addEventListener('load', render);

function render(){
  ctx.drawImage(image, 0, 0);
  const data = ctx.getImageData(0, 0, width, height).data;
  console.logo(data);
}

console.log()で結果を見ると、

Unit8ClampedArrayという特殊な配列で返されていることがわかります。この配列は「値を上限と下限の間に制限する8bit符号なし整数」で、0~255の範囲で値が表され、それ以外の数値が入ってきたときにはこの範囲の値に制限されます。

1ピクセルのRGBAが順番に格納されていて、ロゴの画像の左上は透過箇所なので、0が返ってきていますが色のついた箇所は0~255の値が入っています。

次にwebglでデータを扱いやすいように整理して、関数にします。

function ImagePixel(path, w, h, ratio) {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  const width = w;
  const height = h;
  canvas.width = width;
  canvas.height = height;

  ctx.drawImage(path, 0, 0);
  const data = ctx.getImageData(0, 0, width, height).data;
  const position = [];
  const color = [];
  const alpha = [];

  for (let y = 0; y < height; y += ratio) {
    for (let x = 0; x < width; x += ratio) {
      const index = (y * width + x) * 4;
      const r = data[index] / 255;
      const g = data[index + 1] / 255;
      const b = data[index + 2] / 255;
      const a = data[index + 3] / 255;

      const pX = x - width / 2;
      const pY = -(y - height / 2);
      const pZ = 0;

      position.push(pX, pY, pZ), color.push(r, g, b), alpha.push(a);
    }
  }

  return { position, color, alpha };
}

まず、配列内の任意の[x、y]ピクセルの位置を以下の式で取得します。

const index = (y * width + x) * 4;

webglでは色を0~1の範囲で表現するので、255で割って数値を0~1の範囲に変換します。

const r = data[index] / 255;
const g = data[index + 1] / 255;
const b = data[index + 2] / 255;
const a = data[index + 3] / 255;

また、webglは原点が中心となり、xは右がプラス左がマイナス。yは上がプラス下がマイナスなので、これも変換します。

const pX = x - width / 2;
const pY = -(y - height / 2);
const pZ = 0;

これでthree.jsでパーティクルを表示するまでの下準備は完了です。このデータを基にThree.jsを使用してブラウザ上にパーティクルを表示していきます。

結果がこちら。

See the Pen
Image Pixel
by Hisami Kurita (@hisamikurita)
on CodePen.

画像の読み込みとデータの取得が完了した後にメッシュの作成が行われ、this.imageList[0]には任意のピクセルの色と位置と透明度の値が返されます。

その値をシェーダーに送るために、各頂点に情報をセットし、表示させます。

const img = new Image();
img.src = image;

img.addEventListener('load', () => {
  this.imageList.push(ImagePixel(img, img.width, img.height, 4.0));
});
const geometry = new BufferGeometry();
const position = new BufferAttribute(new Float32Array(this.imageList[0].position), 3);
const color = new BufferAttribute(new Float32Array(this.imageList[0].color), 3);
const alpha = new BufferAttribute(new Float32Array(this.imageList[0].alpha), 1);
geometry.setAttribute('position', position);
geometry.setAttribute('color', color);
geometry.setAttribute('alpha', alpha);

const material = new RawShaderMaterial({
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  transparent: true
});
this.mesh = new Points(geometry, material);
this.stage.scene.add(this.mesh);

▼頂点シェーダー

attribute vec3 position;
attribute vec3 color;
attribute float alpha;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec3 v_color;
varying float v_alpla;

void main() {
    v_color = color;
    v_alpla = alpha;

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0 );
    gl_PointSize = 6.0;
}

▼フラグメントシェーダー

precision mediump float;

varying vec3 v_color;
varying float v_alpla;

void main() {
    vec2 temp = gl_PointCoord - vec2(0.5);
    float f = dot(temp, temp);
    if (f > 0.25 ) {
        discard;
    }

    gl_FragColor = vec4(v_color, v_alpla);
}

無事ブラウザ上に画像からパーティクルを生成し配置することができましたね。ですが、実際のクライアントワークではパーティクルをただ表示すれば良い案件はおそらくあまりなく、何かしらのアニメーションやインタラクションとセットになって使われることが多いと思います。

せっかくなので色々な演出案やクオリティを上げる方法を考えてみました。コードもちょっとだけ解説します。

拡散

See the Pen
Image Pixel #02
by Hisami Kurita (@hisamikurita)
on CodePen.

まずは、シンプルにパーティクルを各頂点に向かって動かしてみました。glslにはビルドインの関数が用意されています。今回はnormalize関数を使用して、各頂点のxy座標の正規化した値を取得します。

またgsapを使用してu_ratioを0~1の値で変化するように設定します。positionに変化する値をプラスすることでu_ratioが0のときに、positionのみの値、u_ratioが1のときに拡散時の値に変化しますよね。

float power = 800.0;
vec3 vertexDirection = vec3(normalize(position.xy), 0.0);
vec3 finalPosition = position + vertexDirection * power * u_ratio;

拡散+ノイズ

See the Pen
Image Pixel #04
by Hisami Kurita (@hisamikurita)
on CodePen.

人間は決まった動きだと予測できて飽きてしまうので、シンプレックスノイズという有名なノイズ関数を使ってランダム性を持たせます。codepenには仕様上、とても長い行数で書かれた関数を直接貼り付けてしまっているのですが、実際に使用しているのはこの箇所だけです。

vec3 noise = position * snoise(vec3(vertexDirection)) * 5.0 * u_ratio;

JavaScriptのMath.random()で取得するランダムな値とは違い、綺麗なランダムな値が取得できます。ランダムな値を実装するときはdat.guiなどを使用して、数値を動かしながら実際に動きを把握していくことが多いです。

パーティクルごとに違う動きを取り入れる

See the Pen
Image Pixel #03
by Hisami Kurita (@hisamikurita)
on CodePen.

パーティクルを実装するときは常に動かしたり半径を変化させたりすることが多いです。機械的な動きではなく、パーティクルがそれぞれ生きているような感じがしますよね。パーティクルごとに違う動きをさせたいときはattribute変数にランダムな値を持たせて、実装することが多いです。

const rand = [];
const vertices = this.imageList[0].position.length / 3;
for (let i = 0; i < vertices; i++) {
  rand.push((Math.random() - 1.0) * 2.0, (Math.random() - 1.0) * 2.0);
}
const rands = new BufferAttribute(new Float32Array(rand), 2);
geometry.setAttribute('rand', rands);

色を塗る

See the Pen
Image Pixel #05
by Hisami Kurita (@hisamikurita)
on CodePen.

LIGは様々な個性が混ざり合う会社。前回までは、画像から色を抽出していましたが、今回はコードでパーティクルごとに異なる色を塗ってみました。色の塗り方についてはこちらのサイトのロジックを参考にさせていただきました!

三角関数を使用して値を変化させてるようですが、良い色が出ますね。

vec3 pal( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) {
 return a + b *cos(6.28318 * (c * t + d));
}

まとめ

いかがでしたでしょうか。会社のロゴを勝手にパーティクルにして動かしたり色を塗ったりして、えらい人に怒られないか心配です……。無事この記事が公開されることを祈ります。