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

# はじめに

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

わたしは昨年から[株式会社ノックノート](https://knocknote.co.jp/)というところで、今年の春先くらいに出したスマートフォン向けブラウザゲームのクライアント・サーバー・インフラの開発などを担当しています。  
ブラウザゲームといっても、従来のFlashゲームのようなものではなく、ネイティブアプリと比べても遜色ないものにするというコンセプトのもと、PWAを意識して開発していたため、当然描画もDOMベースではなく canvas(WebGL)で行う方針となり、当時 WebGL でまともに2Dゲームを作れそうな描画エンジンを探したところ [pixi.js](http://www.pixijs.com) が良さそうだということになったため、これを開発に採用したという経緯があります。

わたしが入社したときにはすでに pixi.js を使って複数のタイトルの開発が進んでいたのですが、pixi.js はもともとゲームエンジンとして作られたものではないため、ゲームを作るために必要な機能がいろいろ足りていませんでした。  
その中のひとつであるアニメーション開発コストも問題で、 [pixi-tween](https://github.com/Nazariglez/pixi-tween) を使って表現できるものはまだいいのですが、アニメーターさんが作ってくださった AfterEffects アニメーションをそもそも実装(再現)すること自体が難しいケースもありました。  

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

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

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

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

# pixi-after-effects

ライブラリは pixi.js のプラグインとして実装しており、 [pixi-after-effects](https://github.com/knocknote/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` )を用意しています。

```javascript
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);` を呼んでいます。


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

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

## 要素の置き換え

アニメーション再生前に、テンプレート的に用意したアニメーションデータに対してプログラム側でいろいろいじりたい場合があると思います。  
( 例えば、 AfterEffectsアニメーション内で null オブジェクトで表現しておき、その要素を Spine のオブジェクトで置き換えるとか、画像をさしかえるとか )

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

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

ふたつめは、 `PIXI.AEDataInterceptor` コンポーネントを利用したデータの差し替えです。  
デモ中でも行っていますが、こちらを使うと、JSONデータを読み込んだ時点をフックして指定したパラメータでJSONの内容を置き換えることができます。例えばデモの例だと

```javascript
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) => {
    // 差し替え後のデータになっている
});
```

のように利用していますが、 `00` や `0` や `D` というのが `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報告も心よりお待ちしております。