JavaScript
WebGL
npm
AfterEffects
pixi.js

AfterEffectsアニメーションをWebGLで動かす

はじめに

AfterEffectsで作成したアニメーションを動かすツールとして有名なものに Airbnb の Lottie があります。
もともとは AfterEffectsで作ったアニメーションをHTML5ベースで動かせる bodymovin というソフトウェアがあり、これを iOS/Android用にポートしたものが Lottie というプロジェクトだったと思います。今では bodymovin の作者も Lottie 側の開発にジョインして、 https://github.com/airbnb/lottie-web という名前で開発を続けているようです。
調べてみたところ、canvas(WebGL) で動くようにするプロジェクトは存在しないように見受けられました。

わたしは昨年から株式会社ノックノートというところで、今年の春先くらいに出したスマートフォン向けブラウザゲームのクライアント・サーバー・インフラの開発などを担当しています。

ブラウザゲームといっても、従来のFlashゲームのようなものではなく、ネイティブアプリと比べても遜色ないものにするというコンセプトのもと、PWAを意識して開発していたため、当然描画もDOMベースではなく canvas(WebGL)で行う方針となり、当時 WebGL でまともに2Dゲームを作れそうな描画エンジンを探したところ pixi.js が良さそうだということになったため、これを開発に採用したという経緯があります。

わたしが入社したときにはすでに pixi.js を使って複数のタイトルの開発が進んでいたのですが、pixi.js はもともとゲームエンジンとして作られたものではないため、ゲームを作るために必要な機能がいろいろ足りていませんでした。

その中のひとつであるアニメーション開発コストも問題で、 pixi-tween を使って表現できるものはまだいいのですが、アニメーターさんが作ってくださった AfterEffects アニメーションをそもそも実装(再現)すること自体が難しいケースもありました。

そこで、AfterEffects アニメーションを WebGL (というか pixi.js )で動かせるツールを探したのですが、前述のとおり HTML5 で動かすことはできても、WebGL で動かすツールは存在しないようでした。また、アニメーション内で画像を利用することがあまり想定されていないようで(モノリシックかつ軽量なデータでアニメーションを実現できることをコンセプトにしている?)、HTML5で利用する場合であっても、画像がからむとうまく再生できないといったことがありました。

さらに、ゲームで利用する場合はただアニメーションを再生するだけでなく、特定のオブジェクトだけ別のものに置き換えたり、あるアニメーションはループで再生しておいて、何かイベント(タップとか)があったら次のアニメーションに移るなど、アニメーション再生処理の制御なども行いたい場合があります。

こういった事情から、既存のものを改修していくようなアプローチではなく、いちから pixi.js 用の AfterEffects アニメーション再生ライブラリを開発することにしました。ライブラリは、当初は先に挙げた春先にリリースしたタイトルとは別のタイトルで利用するために実験的に開発していたのですが、予想よりも完成度高く作ることができたため、先のタイトルでも採用することになりました。

前置きが長くなってしまいましたが、次から開発したライブラリでどんなことができるか解説したいと思います。

pixi-after-effects

ライブラリは pixi.js のプラグインとして実装しており、 pixi-after-effects として公開しています。

実装にあたって、 https://www.lottiefiles.com のような既存の AfterEffects アニメーションの資産を有効活用したいという思いがあり、 https://github.com/airbnb/lottie-web#plugin-installation で公開している Lottie(bodymovin)用の AfterEffects プラグインを使って出力された JSONファイルを入力として扱う方針としました。

ですので、AfterEffectsアニメーションからライブラリがサポートするデータ形式に変換する手順はLottie側の解説におまかせしたいと思います(日本語のプラグイン導入記事もいくつかあるようです)。

ためしに、プラグインを使って出力したJSONファイルを bodymovin.json として保存して使うといった例として https://knocknote.github.io/pixi-after-effects/dist を用意しました。

( examples/bodymovin.json の中身は https://github.com/airbnb/lottie-web/tree/master/demo/bodymovin と同じものを利用しています )

次から、このデモで利用しているソースコードを使って、機能の紹介をしていきたいと思います。

JSONを読み込む

アニメーションを再生するにあたり、まずは AfterEffects からプラグインを使って変換したJSONファイルを読み込まなければなりません。 pixi-after-effects では専用のローダー( PIXI.AEDataLoader )を用意しています。

new PIXI.AEDataLoader().loadJSON('examples/bodymovin.json').then((data) => {
    // アニメーション再生に必要な追加リソース(画像など)がある場合は、そのリソースをすべて読みこんでからここにくる
});

上記のようなコードで利用することができます。コメントにも書いてあるとおり、アニメーションの中には画像を利用しているケースも少なくありません。

AfterEffectsプラグインで書き出す際、プラグイン側で利用している画像の書き出しとその画像へのパスをJSONファイルに書き込むこともやってくれるので、 pixi-after-effects ではこの画像パスをもとに依存するリソースをダウンロードします。すべての依存リソースのダウンロードが終わり、アニメーション再生準備が整ってから Promise が解決されます。 ( 内部で PIXI.loaders.Loader を利用しています )

再生する

事前に、PIXIのレンダラに stage という名前の PIXI.Container が追加されていたとします。
( 描画対象にするPIXIのオブジェクトは、stage に追加すればいいという状況です )

その上で下記のように PIXI.AEDataLoader のロード完了後のコールバックで PIXI.AfterEffects オブジェクトを作成して stage に追加すれば準備完了です。
また、レンダリングループのなかで ae オブジェクトの更新をおこないたいため requestAnimationFrame を使ってメインループを作ってその中で ae.update(nowTime); を呼んでいます。

let renderer = new PIXI.WebGLRenderer(2000, 600, { antialias: true, backgroundColor: 0xffffff });
document.body.appendChild(renderer.view);

const stage = new PIXI.Container();

let ae = null;
new PIXI.AEDataLoader().loadJSON('examples/bodymovin.json').then((data) => {
    ae = PIXI.AfterEffects.fromData(data);
    stage.addChild(ae);
});

requestAnimationFrame(animate);
function animate(nowTime) {
    if (ae) { ae.update(nowTime); }
    renderer.render(stage);
    requestAnimationFrame(animate);
}

この状態で、 ae.play() を呼ぶとアニメーションが再生されます ( ループ再生したい場合は ae.play(true) )。

再生制御

PIXI.AfterEffectsPIXI.Container を継承して作られており、PIXI.AfterEffects が内包している要素もすべて (PIXI.Container|PIXI.Sprite|PIXI.Graphics)のいずれかで実装されています。また、要素それぞれを個別にアニメーションさせることができる設計になっているので、アニメーションを構成する要素のうち、この要素だけをアニメーションさせたいといったことが可能です。

再生制御用のメソッドとして、 play , stop , pause , resume を用意しました。これらは PIXI.AfterEffects オブジェクトに対して実行できるだけでなく、 PIXI.AfterEffects.find(name) を用いて取得した AfterEffectsアニメーション内の子要素に対しても適応することができます。

要素の置き換え

アニメーション再生前に、テンプレート的に用意したアニメーションデータに対してプログラム側でいろいろいじりたい場合があると思います。

( 例えば、 AfterEffectsアニメーション内で null オブジェクトで表現しておき、その要素を Spine のオブジェクトで置き換えるとか、画像をさしかえるとか )

そのために使える機能を2つ用意しました。  

ひとつめは、 PIXI.AfterEffects.find(name) です。

AfterEffectsの各要素には名前をつけることができ、これはプラグインで書き出したデータにも引き継がれます。これを利用して要素を検索し取得することで、 PIXI のオブジェクトに対して好きな変更を行うことができます。

ふたつめは、 PIXI.AEDataInterceptor コンポーネントを利用したデータの差し替えです。

デモ中でも行っていますが、こちらを使うと、JSONデータを読み込んだ時点をフックして指定したパラメータでJSONの内容を置き換えることができます。例えばデモの例だと

let interceptor = new PIXI.AEDataInterceptor({
    '00': { //B
        events: {
            click: (event) => {
                event.target.play();
            },
        }
    },
    '0': { //O
        events: {
            click: (event) => {
                event.target.play(true);
            },
        }
    },
    'D': { //D
        events: {
            click: (event) => {
                event.target.play();
            },
            completed: () => {
                console.log("completed D");
            }
        }
    },
});
const loader = new PIXI.AEDataLoader();
loader.loadJSONWithInterceptor('./examples/bodymovin.json', interceptor).then((data) => {
    // 差し替え後のデータになっている
});

のように利用していますが、 000D というのが bodymovin.json 中で表現されている各要素の名前で、デモ中の B , O , D の要素にそれぞれ該当しています。

これらのデータを上記の用に書き換えることで、

  • B の要素をタップしたときに、その要素だけアニメーションを再生する
  • O の要素をタップしたときに、その要素だけアニメーションをループ再生する
  • D の要素をタップしたときに、その要素だけアニメーションを再生し、かつその完了通知を受けとる

という意味になります。 Developer Consoleを開いて期待する挙動になるか見ていただければと思います。 

この他にも差し替えられる値はいろいろあります。詳しくは https://knocknote.github.io/pixi-after-effects/docs/PIXI.AEDataInterceptor.html を参照ください

既知の問題

pixi-after-effects は現在も本番環境で利用されています。

しかし不具合がないわけではなく、いくつか利用側で調整しながら運用している部分があるのでそれらをご紹介します。 ここで紹介したものは、できるだけ今後のアップデートで対応したいと考えています。

1. 書き出した透過画像の透過部分がくすむ

https://github.com/bigxixi/SaveFrameAsPNG-Plus でも言及されていますが、 plugin内で利用されている画像書き出し処理に saveFrameToPng() が利用されているため、そのままだとキレイな画像が出力されません。

開発当時はこの挙動のため、書き出された画像を別途置き換える作業が必要でしたが、今調べてみたところ今年の5月の時点で https://github.com/bodymovin/bodymovin-extension/pull/3 のような対応が入ったようなので、もしかすると今では直っているかもしれません。

2. 加算・減算ブレンドが使えない

加算ブレンドに関しては、プラグイン側に出力するためのソースコードが存在しないため、そもそも書き出されません。

減算ブレンドは、pixi.js 側に減算ブレンドのサポートがそもそもないためできません。

これらはプラグイン側、pixi.js側の改修によって利用できる可能性があるため調査中です。
開発時は、アニメーション側でこれらのブレンドモードを利用しないことで対応しています。

3. トリミング処理

こちらは単純にゲーム内で利用しているアニメーションに該当のものがなかったので実装していなかったのですが、今後サポート予定です。

まとめ

WebGLでAfterEffectsアニメーションを実行するためのライブラリとして、pixi.js用のプラグイン pixi-after-effects を開発しました。

アニメーション内の要素を個別に再生制御したり、特定の要素をあとから置き換えたりする機能があります。

ライブラリはすでに本番利用されており、継続的に開発していく予定です。
ぜひ使っていただいて、感想を寄せていただければ幸いです。 Issue報告も心よりお待ちしております。