2018年04月17日

シェーダでの3Dパーティクルアニメーション実装に便利なライブラリ THREE.BAS

背景

昨年、弊社採用サイトをリニューアルしました。トップページは 3D のパーティクルアニメーションとなっており、 WebGL で実装しています。実装するにあたって使用するライブラリには PixiJS 等も検討しましたが、今回は 3D ということで three.js を選びました。

始めは一つ一つのパーティクルを THREE.Mesh で生成していましたが、特にテキスト側の画面はパーティクルの数が多くかなり重くなってしまいました。

改善するために別の実装方法を探したところ、 Takumi Hasegawa さんの記事を見つけ、どうやらシェーダを使うと大量のパーティクルでも重くならずに描画できると書いてあったので試すことにしました。

シェーダで実装してみた

上記の記事を参考にシェーダで実装してみたところ軽くはなりましたが、パーティクルが画面から出ていって消えた後に反対側から再び表示させるという処理をするときに、頂点ごとに画面外判定をしてしまうと四角形の4頂点のうち一つだけが反対側に移動して四角形が崩れてしまうという問題にぶち当たりました。シェーダはパフォーマンスを落とさずにアニメーションを描画できますが、パーティクルのようなオブジェクトを移動させるには複雑な計算が必要になるという欠点がありました。

これを楽にできないかなぁと調べていたら、素敵なテキストのパーティクルアニメーションを見つけました。

See the Pen THREE Text Animation #6 by Szenia Zadvornykh (@zadvorsky) on CodePen.

大量のパーティクルをアニメーションさせているにもかかわらず全くもたつきを感じないので、是非これを真似してみたいと思いコードを読んでいたら、 THREE.BAS というライブラリを使用していることに気が付きました。

THREE.BAS について

THREE.BAS は three.js の拡張ライブラリで、簡単に言うと、頂点シェーダのアニメーションロジックを簡単に書けるようにするものです。

THREE.BAS の機能の一つとして、 three.js のビルトインジオメトリをパーティクルの一つ (prefab) として扱い、そのジオメトリの一つ一つの頂点を意識することなく prefab 一つに対し一つの座標を指定するだけでパーティクルを移動させることができます。回転なども通常は行列計算が必要ですが、 THREE.BAS が提供する関数を使えば軸と角度を指定するだけで実現できます。

採用サイトのパーティクルは PlaneGeometry を縦長にしたものを prefab として設定し、一つ一つ異なる位置、角度を指定しています。

// PlaneGeometry を prefab とする (他のジオメトリも可)
const prefab = new THREE.PlaneGeometry(prefabWidth, prefabHeight)

// prefab を prefabCount 数の分だけ増やしたジオメトリを生成
const geometry = new BAS.PrefabBufferGeometry(prefab, prefabCount)

// attribute 変数初期化
// .createAttribute([変数名], [ベクトル成分数])
// 位置
const aPosition = geometry.createAttribute('aPosition', 3)
// 位置 (終点)
const aEndPosition = geometry.createAttribute('aEndPosition', 3)
// 回転
const aAxisAngle = geometry.createAttribute('aAxisAngle', 4)

// 各 prefab ごとに attribute 変数値を設定
for (let i = 0; i < prefabCount; i++) {
  // 位置 (XYZ座標)
  // 値は任意
  geometry.setPrefabData(aPosition, i, [x, y, z])
  geometry.setPrefabData(aEndPosition, i, [x, y, z])
  // 回転 (軸、角度)
  // 値は任意
  geometry.setPrefabData(aAxisAngle, i, [x, y, z, angle])
}

// THREE.BAS 用マテリアル生成
const material = new BAS.BasicAnimationMaterial({
  // シェーダで使う uniform 変数を指定
  uniforms: {
    uTime: { value: 0.0 }
  },
  // 頂点シェーダで使う THREE.BAS のビルトイン関数を指定
  vertexFunctions: [
    // easeCubicInOut
    BAS.ShaderChunk['ease_cubic_in_out'],
    // quatFromAxisAngle, rotateVector
    BAS.ShaderChunk['quaternion_rotation']
  ],
  // 頂点シェーダで使う変数を指定 (GLSL)
  vertexParameters: [
    'uniform float uTime;',
    'attribute vec3 aPosition;',
    'attribute vec3 aEndPosition;',
    'attribute vec4 aAxisAngle;'
  ],
  // 頂点シェーダで使う値の初期化 (GLSL)
  vertexInit: [
    // quatFromAxisAngle: 軸と角度から回転を表すクォータニオン算出 (THREE.BAS 提供関数)
    'vec4 tQuat = quatFromAxisAngle(aAxisAngle.xyz, aAxisAngle.w);',
    // 一般的な Easing 関数は THREE.BAS で提供されている
    'float tProgress = easeCubicInOut(uTime);'
  ],
  // prefab の位置を計算する (GLSL)
  vertexPosition: [
    // transformed: prefab の位置 (three.js の ShaderChunk で定義されているもの)
    // 最初は原点の状態で、その値を変更することで移動させることができる
    // 回転
    // rotateVector: クォータニオンを基に回転後の transformed を算出
    'transformed = rotateVector(tQuat, transformed);',
    // 位置
    'transformed += mix(aPosition, aEndPosition, tProgress);'
  ]
})

// 上記 geometry と material を基に three.js のメッシュを生成してシーンに追加
const mesh = new THREE.Mesh(geometry, material)
const scene = new THREE.Scene()
scene.add(mesh)

// uniform 変数の値は JS で変化させる
material.uniforms['uTime'].value = time

このように、アニメーションロジックに必要な値は通常の JS で書く場合とあまり変わらないので、実装難易度を高めずにシェーダで高速化することができます。アニメーションロジック部分は GLSL で書く必要があるので知識は必要になりますが、簡単なアニメーションであれば GLSL で書く部分は数行で済むのでハードルはそこまで高くないかもしれません。

ただし、日本語の解説記事がなかったり、THREE.BAS の公式ドキュメントの情報も乏しいため、実際に使いこなせるようになるまでは苦労します。筆者は Examples のコードや GitHub のソースコードを読んで少しずつ理解しました。

こうして THREE.BAS を使うことで、採用サイトのテキストパーティクルアニメーションをスマホでもヌルヌル動くように実装することができました。ただ、カラフルな画面のほうのパーティクルはクリックイベントに対応しなければならず、シェーダでのクリックイベント対応が分からなかったため、そちらはシェーダでの実装を断念しました。

作例

公式 Examples に作例がいくつかありますが、シェーダで描画されているのでどれも軽快なアニメーションで気持ち良く感じます。他にも THREE.BAS 作者の作品が CodePen で見れます。 THREE.BAS 以外の作品もありますが、どれもレベルの高いものばかりなので是非ご覧ください。実は最初に紹介した CodePen の作品は THREE.BAS 作者が作ったものでした。

僭越ながら、筆者が作成した作品も紹介します。

Crumbling Image

See the Pen Crumbling Image by Ko.Yelie (@ko-yelie) on CodePen.

1枚の画像を1ピクセルごとに PlaneGeometry に分割したものを prefab としてアニメーションさせています。画像の解像度が高くなるほど処理が重くなってしまいますが、このくらいの大きさであればスマホでもヌルヌル動きます。

The Flow of Time

See the Pen The Flow of Time by Ko.Yelie (@ko-yelie) on CodePen.

上の作例を応用させて砂時計のような作品も作りました。

House Explosion

See the Pen House Explosion by Ko.Yelie (@ko-yelie) on CodePen.

こちらは画像全体を1つの PlaneGeometry として用意し、 THREE.BAS のユーティリティメソッド BAS.Utils.separateFaces を使って PlaneGeometry で指定した segments の数だけ三角形に分割し、その一つ一つをパーティクルとしてアニメーションさせています。パーティクルが三角形で問題なければこの方法が楽です。

まとめ

シェーダを使うことで大量のパーティクルアニメーションを高速に描画できますが、パーティクルのようなオブジェクトのアニメーションをシェーダで実装するには複雑な計算が必要になり大変です。それを改善するためのライブラリ THREE.BAS について紹介しました。筆者が覚えた機能はライブラリの一部のみですが、それだけでも十分恩恵を受けることができます。

アニメーションが複雑になるほどパフォーマンスが悪くなる傾向にあり、実際に閲覧するユーザーが重いと感じてしまってはせっかくの作品が台無しになってしまいます。そうならないためにもこういったライブラリを活用してより良い作品を作っていきたいと思います。

RELATED POSTS