JavaScriptでクリエイティブコーディング
テキストを分解しパーティクルにする演出

61
36

パーティクルとは粒子のこと。パーティクルを表現に取り入れると、印象的な演出に役立ちます。JavaScriptやWebGLを使うことで、ウェブの技術でもパーティクル表現の制作が可能です。本記事では題材にパーティクル表現の制作に役立つアイデアや着眼点を紹介します。

作例の紹介

本記事のチュートリアルの完成形はこちらになります。

この記事で学べること

  • 2Dテキストを粒子化して動かす表現
  • パーリンノイズによる空気感の再現
  • GSAPによる大量のトゥイーン制御
  • WebGLの高速化(PixiJSの応用)

制作の技術

本作例を制作するにあたり、利用しているウェブの技術の概要を紹介します。

WebGL

画面表示はWebGLを利用します。ウェブのレンダリング技術において、もっとも高速な描画性能を得られるのがWebGLであるためです。WebGLは3D表現のための技術と思われがちですが、2Dでも利用できます

2D向けWebGLライブラリとしてはPixiJSピクシィ・ジェイエスが有名です。実行性能が極めて高く、Adobe FlashのようなAPIを備えています。本作例ではPixiJSを使いますが、WebGLでレンダリングできればいいので他のJSライブラリを利用しても構いません。

Canvas 2D

Canvas 2Dを利用して、画像からピクセル情報の解析を行います。後述の空白ピクセルの有無を判定するためです。

トゥイーン

キーフレームごとに位置座標を指定し、キーフレームの間を補間することでモーションを実現します。この補間技術をトゥイーンと呼びます。トゥイーンを実現するJavaScriptとして今回はGSAPジーサップを利用します。GSAPは同時実行性能が高いのでオススメしていますが、他のJSライブラリでも問題ありません。

再現性のある乱数

乱数の生成にパーリンノイズを利用するため、JSライブラリ「josephg/noisejs」を利用します。パーリンノイズを実現するライブラリは他にもあるので、別のJSライブラリでも問題ありません。

4段階の手順で制作

以下のアプローチで表現化します。

  1. テキストをCanvasに描画する
  2. テキストを粒子に分解する
  3. 粒子を飛ばす
  4. 飛ばし方に風の要素を加える

大量の粒子が集まってテキスト形状を形成するようにします。

手順1. テキストをCavnvasに描画する

画像文字として、透過PNG画像を用意します。

透過PNG画像をJavaScriptで読み込みます。同期的に読み込みたいので、以下のようにimg要素を作成し、decode()メソッドをawaitで待機します。

// 画像を読み込む
const image = new Image();
image.src = "images/text_2x.png";
await image.decode();

画像要素はcanvasタグの2次元描画APIであるCanvasRenderingContext2D(以下、Canvas 2Dと記載)に転写します。Canvas 2Dに転写することで、透過PNG画像の画素情報を調べることが可能になります。

// 画像のサイズを算出
const imageW = image.width;
const imageH = image.height;

// 画像をメモリ上のcanvasに転写。ピクセル値を取得するため
const canvas = document.createElement("canvas");
canvas.width = imageW;
canvas.height = imageH;
const context = canvas.getContext("2d", {
  // getImageData() を頻繁に読み出すためのヒント
  willReadFrequently: true,
});
context.drawImage(image, 0, 0);

ここで作成したcanvas要素はメモリ上で利用するだけで、document.bodyのDOMツリーには配置しません。

手順2. テキストを粒子に分解する

Canvas 2Dに転写した画像の全ピクセルを調べることで、パーティクルの配置すべき座標を調べます。

Canvas 2DではXY座標を指定すれば、ピクセルのRGBA値を調べられます。for文を使って、画像の全ピクセルを走査します。

const dots = []; // パーティクルの保存先

const lengthW = imageW;
const lengthH = imageH;

for (let i = 0; i < lengthW * lengthH; i++) {
  // カウンタ変数 i から x, y を算出
  const x = i % lengthW;
  const y = Math.floor(i / lengthW);
  // x,y座標の画素情報を取得
  const dotData = context.getImageData(x, y, 1, 1);

ピクセルのRGBA値を調べて、透明ピクセル以外のピクセルを調べます。実行負荷を下げるために生成する粒子は最小限に留めておきたいところです。透明ピクセルでは粒子を生成しないよう、無視するようにします。

// 透過チャンネルを取得(0 = 赤, 1 = 緑, 2 = 青, 3 = アルファ)
const alpha = dotData.data[3];

// 透明であればスプライトは作らないようにする
if (alpha === 0) {
  continue;
}

透明ピクセル以外であれば、粒子を生成します。粒子はただの白い四角形として、PixiJSのスプライトで生成します。

※PixiJSのセットアップ等はコードを省略しています。詳細はGitHubのコードを参照ください。

// パーティクルを生成
const dot = new PIXI.Sprite(PIXI.Texture.WHITE);
dot.x = x;
dot.y = y;
dot.width = 1;
dot.height = 1;
dot.alpha = alpha / 255; // 元画像の透明度を適用
container.addChild(dot);

// 配列に保存
dots.push(dot);

わかりやすく可視化したのが、次の図版です。実際は隙間なく敷き詰めています。

手順3. 粒子を飛ばす

パーティクルが拡散する演出を作成します。パーティクルを拡散させるためには乱数を使って、適当な座標を算出します。

JavaScriptではMath.random()メソッドを使うと、0.01.0の範囲の数値が得られます。ゼロを中心に値を散らかしたいときは、Math.random()の最大値である1.0の半分の値を引き算し、Math.random() - 0.5という計算式とすることで-0.50.5の範囲の値が得られます。これに振幅をかけ算すれば、振幅 * (Math.random() - 0.5)という計算式となり-振幅の半分+振幅の半分の範囲の値が得られます。

// stageW と stageH は画面幅と画面高さ
const randomX = stageW * (Math.random() - 0.5);
const randomY = stageH * (Math.random() - 0.5);

配列に対して乱数を計算し適用します。GSAPのfrom()メソッドを使って出発点となるキーフレームを作成します。パーティクルの1つひとつに個別のトゥイーンを指定しますが、トゥイーンを集約管理するためのGSAPのタイムラインを作成しています。

// GSAPのタイムラインを作成(各トゥイーンを集約管理するため)
const tl = gsap.timeline({ repeat: -1, yoyo: true });

// 画面サイズを取得
const stageW = app.screen.width;
const stageH = app.screen.height;

for (let i = 0; i < dots.length; i++) {
  const dot = dots[i];
  // パーティクルの移動座標を決める

  const randomX = stageW * (Math.random() - 0.5);
  const randomY = stageH * (Math.random() - 0.5);

  tl.from(
    dot,
    {
      x: randomX,
      y: randomY,
      alpha: 0,
      duration: 4,
      ease: "expo.inOut",
    },
    0, // 各トゥイーンは0秒地点を開始とする
  );
}

ここまでの手順でパーティクル演出を作成できました。

手順4. 飛ばし方に風の要素を加える

風のような表現を加えるために、パーリンノイズを利用します。

さきほど説明したMath.random()メソッドは実行するたびに0.01.0の値を規則性なく返します。対して、パーリンノイズは引数によって規則性のある乱数が得られます。連続した2つの引数を与えれば、近い値が得られます。

規則性のあるノイズを利用すると、一例として波打つような描画結果を得られます。

次のデモで、関数に与える引数を変更すると連続した値を得られていることを確認できます。XとYのスライダーをゆっくり動かして確認しましょう。

パーリンノイズは「波」、「風」といった表現に利用できます。応用できることが多いので、クリエイティブコーディングをするなら覚えておきたい手法です。詳しくは記事『パーリンノイズを使いこなせ』を参考ください。

パーリンノイズで求めた値は、GSAPの出発点の座標に適用します。

for (let i = 0; i < dots.length; i++) {
  const dot = dots[i];

  const index = dot.offsetIndex;

  // XとYを正規化 (Normalize X and Y)
  // nx は左辺を基準に 0.0〜1.0の値をとる
  const nx = (index % lengthW) / lengthW;
  // ny は上辺を基準に 0.0〜1.0の値をとる
  const ny = Math.floor(index / lengthW) / lengthH;

  // パーリンノイズでパーティクルの移動座標を決める。
  // パーリンノイズだと連続性が生まれるので、波打つ表現になる。
  // 乗算は周期と考えてもらえばOK。
  const px = noise.perlin2(nx, ny);
  const py = noise.perlin2(nx * 2, ny);

  tl.from(
    dot,
    {
      x: stageW * px,
      y: stageH * py,
      alpha: 0,
      duration: 4,
      ease: "expo.inOut",
    },
    0, // 各トゥイーンは0秒地点を開始とする
  );
}

ぐにゃっと曲がるような表現になりました。

水平方向に流れるような演出

画面の左側から流れるように表示したいと思います。トゥイーンの遅延時間にX座標を加味してみましょう。

// 画面左側から遅延
// nxは左端が0.0、右側が1.0の正規化された数値
const delay = nx * 1.0;

// (一部省略)

tl.from(
  dot,
  {
    x,
    y,
    alpha: 0,
    duration: 4,
    ease: "expo.inOut",
  },
  delay, // 各トゥイーンは0秒地点を開始とする
);

パーティクルの出現と退場に「流れ」が生まれてきました。

さらに乱数を加えて味付け

パーリンノイズで計算した座標をそのまま使うと規則性が強くでてしまいます。もう一段階、適当な乱数を加算しています。

const px = noise.perlin2(nx, ny * 2);
const py = noise.perlin2(nx * 2, ny);

// 画面左側から遅延
const delay = nx * 1;

// 画面左側から拡散度合いを強くする
const spread = (1 - nx) * 100 + 100;
// 追加で乱数で拡散
const x = stageW * px + Math.random() * spread;
const y = stageH * py + Math.random() * spread;

tl.from(
  dot,
  {
    x,
    y,
    alpha: 0,
    duration: 4,
    ease: "expo.inOut",
  },
  delay, // 各トゥイーンは0秒地点を開始とする
);

これで規則正しい雰囲気を抑制できました。

完成形

最後にいろいろと味付けをします。出現時と退場時のイージングを異なるものを指定して余韻を調整したり、少しずつ手前側に近寄ってくるような演出を加えています。

本記事で伝えたかったチュートリアルは以上となります。ここからは、とくに読まなくてよい内容ですが、より深く技術のことを知りたい方向けに書きます。

コラム: 大量の粒子を効率よく表示する

WebGLには大量の要素を同時に動かすには、CPU・GPUの性能を引き出すことが必要です。一般的にドローコール(描画命令)を少なくすることが、取り組みやすい最適化です。

PixiJSだと、ParticleContainerクラスを使うと、ドローコールが最適化されます(公式ドキュメント)。数万個の粒子を負荷少なめで描画できるようになるので、表現制作においては強力な機能です。

性能が向上するならParticleContainerクラスを常に使いたくなりますが、実はそのような簡単な話ではありません。ParticleContainerクラスだと子どものネストができなかったり、マスクやフィルターが使えないなど、多くの制約があります。また位置座標のみ、回転のみと制約が多ければ多いほど性能が向上します。制約と誓約を課すほど、より強い力を得られるような中二病仕様です。

ドローコールの最適化は記事『WebGLのドローコール最適化手法』で解説しているので、詳しく知りたい方は読んでみてください。

コラム:パーリンノイズとシンプレックスノイズ

パーリンノイズを開発したのがケン・パーリンさん。映画『トロン』(1982年)の作成のために生み出された技術です。長い間、パーリンノイズはさまざまな場面で利用されていました。たとえば、Adobe After Effectsのフラクタルノイズやタービュレントノイズもパーリンノイズが使われています。Adobe FlashのBitmapDataクラスのperlinNoize()メソッドは名前の通りパーリンノイズそのものです。

そのパーリンさんが2001年に発表したのがシンプレックスノイズです。パーリンノイズよりシンプレックスノイズのほうが、計算量が少なく高速です。本記事の解説を、パーリンノイズとシンプレックスノイズのどちらで説明するか迷ったのですが、知名度からパーリンノイズを利用しました。使い方はあまり変わらないですしね。

パーリンノイズとシンプレックスノイズは出現する値に微妙な違いがあることを、次の2021年の記事で紹介されています。シンプレックスノイズのほうが偏りは少ないらしいです。

私は、この傾向の違いが表現に与えるインパクトまでわからないのですが、もしパッと見でパーリンノイズかシンプレックスノイズのどちらで表現されているか判別できる方がいたらすごいと思います。

余談ですが、パーリンノイズとシンプレックスノイズの違いの記事を書いたキース・ピータース氏は、Flash界隈では神の書と言われる『Foundation ActionScript 3 Animation』を書いた方です。Flash Playerが終了した時代でも、Flashを扱っていた達人は健在だなと思いました。

コラム:なぜ画像文字で用意したか

テキストを用意するのに、手間がかからないように画像文字で用意しました。なぜ画像文字を使用したのでしょうか?

Canvas 2Dにはテキストを描画するためのfillText()メソッドが存在します。Canvas 2Dでテキストを描いてもいいのですが、総称フォントsans-serifだと実行環境によってフォントが異なり、期待する結果が得られません。

対策としては、ウェブフォントを使うのがよさそうです。ただし、Canvas 2Dでウェブフォントを使うには、フォントの読み込みが描画前に完了している必要があります。ウェブフォントの読み込み状況は検知しづらく、WebFontLoader等のJSライブラリを検討する必要があります。詳しくは記事『HTML5 CanvasとWebGLでウェブフォントを扱う方法』を参照ください。

つまり、実現したいことに対してあまりにもコードが複雑になるため、画像文字で代用することにしました。

おまけ:WebGLとJSライブラリを使わずに挑戦してみる

WebGL等の技術ではなく、divタグと標準のWeb Animations APIを使って同じような表現を作ってみました。

(動作が重たいので、別タブで再生ください)

実行時の負荷が高く粒子の数を増やすのは、厳しそうです。また、アニメーションのコードが乱雑になっています。Web Animations APIはシンプルな動きを作ることには適していますが、複数の動きを組み合わせるようなモーションは苦手だと思います。

WebGLを使う方が描画は高速ですが、技術選定の際に比較検証デモを作って、どの技術が適切なのか判断することは大事です。DOMで作ったらどうなるかは予想できていましたが、エビデンスを作成すること自体は大切なことだと思います。

まとめ

この記事で伝えたかったポイントとしては、モーションの実装はちょっとした技術を組み合わせることで実現できる工夫をして表現を追求するのは楽しいということです。クリエイティブコーディングに興味を持って頂けたら幸いです。

池田 泰延

ICS代表。筑波大学 非常勤講師。ICS MEDIA編集長。個人実験サイト「ClockMaker Labs」のようなビジュアルプログラミングとUIデザインが得意分野です。

この担当の記事一覧