Pixi.js の OffscreenCanvas を Web Worker で試してみる。

この投稿はKDDIテクノロジーアドベントカレンダーの12/04分の記事です。
他の記事はこちらです。
https://qiita.com/advent-calendar/2024/kddi-technology

いつもお世話になってる [PXI.js](https://pixijs.com) ですが、夏頃にv8を触った時には「人類にはまだ早い！」という感想しかありませんでした。

v2かv3の頃からのPixi.jsユーザーですが、どう考えても書き方変えすぎだし、動作も安定してなかった。

私も一度v8で頑張ってアプリ書いてはみたけどv6で書き直して2024年のさまざまなイベントを乗り切った、というのが実情です。

![can.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/d7e4acb4-3313-9095-1e46-2c64c345b57a.png)
> みんな大好き缶たたき機にはPixi.js v6を使ってます。
> https://capp.mz4u.net/vc/

ただ、v6は安定してはいますが、イベント諸々落ち着いたところで新しいことをやりたくなり、v8をまた試してみることにしました。

- OffscreenCanvasが使いたい。これはv6では対応してなさそう。
- OffscreenCanvasはもちろんWeb Workerで使いたい。v6だとまだWeb Workerには対応してなさそう（実験的な実装はしてた模様）

ということで、再チャレンジです。

と、その前に簡単に今回やることの用語解説を簡単に。

### [Pixi.js](https://pixijs.com) とは？

WebGLとCanvasに対応した描画ライブラリです。WebGLといえば GLSL！という男前な人には不要なものかもしれませんが、気軽にHTMLでアニメーション書きたい！といったライトユーザー向けのライブラリです。
Goodboy Digitalという会社がずっと開発してましたが、2021年にPlayco社に買収されたようです。

WebGLのライブラリといえば [Three.js](https://threejs.org)が有名ですが、Three.jsが3D用ライブラリなのに対して、Pixi.jsは2D用です。

### OffscreenCanvas とは？

[OffscreenCanvas](https://developer.mozilla.org/ja/docs/Web/API/OffscreenCanvas)とは、その名の通りオフスクリーンのcanvasです。canvasは通常 `<canvas>`要素とHTMLコンテンツとして描画される要素の一つに紐づいていて、当然のことながら通常は他のDOM要素と一緒に画面に描画されます。まー、`display:none`とかにすれば描画されないので、それと何が違うんだ！という話ではあるのですが、大きく違うのが Web Workerでも利用できる、という点です。

DOM要素の一種である`<canvas>`は、通常メインスレッドでのみ描画処理を実施できます。
ただ、メインスレッドで実行されるということは、メインスレッドのさまざまな他の処理と処理時間を取り合うことになります。複雑なアニメーションなどを表現すればするほど、シビアなスロットリング制御などの調整が大変になってきます。

この問題を解決するのが OffscreenCanvas です。
OffscreenCanvasは Web Worker というメインスレッドとは独立した別のスレッドで動作させることができます。

> OffscreenCanvasは メインスレッドでも動作させられますが、そのためにOffscreenCanvasを利用するという有効なユースケースが思いつきません。

Pixi.jsがv8になって OffscreenCanvas と Web Worker に対応したようなので、利用方法を確認してみることにしました。

### Web Worker とは？

最後に [Web Worker](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API) について。

Webアプリから別スレッドを作る機能です。
利用できる機能はメインスレッドと違い限られています。例えば、`window` や `document`、`navigator` などのオブジェクトは利用できません。普段何気なくこれらを当たり前に使ってプログラミングしているWebエンジニアにとって、これは大きな制約でしょう。

また、通常、メインスレッドから渡せるデータは `postMessage()` によるコピーのみです。

例外として、[Transfarable Objects](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Transferable_objects) という特別なオブジェクトだけを渡す（委譲）させることができます。

OffscreenCanvasもTransfarable Objectの一種なので、Web Wrokerで利用可能、というわけです。

### Pixi.js + OffscreenCanvas + WebWorkerの example

利用した Pixi.jsのバージョンは [8.6.2](https://github.com/pixijs/pixijs/releases/tag/v8.6.2)です。
OffscreenCanvasの公式のexampleは [ココ](https://pixijs.com/8.x/examples/offscreen-canvas/basic) にあります。 ただ、Workerと組み合わせたサンプルが見当たらない。

というわけで、試行錯誤しながら書いてみました。

#### 1. index.html

```index.html
<!doctype html>
<html lang="jp">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>pixi-worker test</title>
</head>
<body>
<canvas id="cv"></canvas>
<script type="module">
  const $cv = document.querySelector("#cv");
  const offscreen = $cv.transferControlToOffscreen();
  const w = new Worker("./w.mjs",{type:"module"});
  w.postMessage(offscreen,[offscreen]);
</script>
</body>
</html>
```

メインスレッドの処理は`index.html`に直接書いちゃってます。
jsのコード4行なので、まーいいでしょ。ということで。

```.js
$cv.transferControlToOffscreen();
```
ここで、OffscreenCanvasを作っています。

```.js
const w = new Worker("./w.mjs",{type:"module"});
```
Web Workerを作成して

```.js
w.postMessage(offscreen,[offscreen]);
```

OffscreenCanvasを転送しています。

#### 2. Web Worker 側の処理

Web Worker側の処理は、下記のようになります。

```[w.mjs] .js
import * as PIXI from "./webworker-v8.6.2.mjs";

self.imageURL = "/face.png";
self.pixi = {spr:null};

self.initApp = async (offscreen)=>{
  PIXI.DOMAdapter.set(PIXI.WebWorkerAdapter);
  self.pixi.app = new PIXI.Application();
  await self.pixi.app.init({canvas:offscreen, width:1000,height:800});
  self.pixi.container = new PIXI.Container();
  self.pixi.tex = await PIXI.Assets.load(self.imageURL);
  self.pixi.spr = new PIXI.Sprite(self.pixi.tex);
  self.pixi.spr.anchor.set(0.5);
  self.pixi.spr.x = 100;
  self.pixi.spr.y = 100;
  self.pixi.container.addChild(self.pixi.spr);
  self.pixi.app.stage.addChild(self.pixi.container);
  self.pixi.ticker = PIXI.Ticker.shared;
  self.pixi.ticker.add((delta) => {
    update();
  });
}

self.update = () =>{
  if(pixi.spr != null){
    self.pixi.spr.rotation -= 0.01;
  }
};

self.addEventListener("message", async (e) => {
  await self.initApp(e.data);
});
```

Worker側で、Pixi.jsライブラリをインポートしています。

```.js
import * as PIXI from "./webworker-v8.6.2.mjs";
```
と`PIXI`オブジェクトをわざわざ作っちゃってるのは古いPixi.jsとのコード互換性のためです。

`webworker-v8.6.2.mjs` とファイル名にバージョン番号を付けていますが、前述のPixi.jsのリリースに含まれる「`webworker.mjs`」が元のファイルです。

こちらの処理を動かすのにえらい時間がかかってしまいました。
処理自体はPixi.jsで画像を1つ読み込んで`Texture`を作り、そこから`Sprite`を作って回転させているだけ、という基本的な処理です。

ハマりポイントなどを共有したいと思います。

##### 1. `pixi.js` / `pixi.min.js` / `pixi.mjs` / `pixi.min.mjs` は Worker非対応！

これに気づくのに何時間かかったか！ w

正解は `webworker.js` / `webworker.mjs` / `webworker.min.js` / `webworker.min.mjs` を使いましょう、ということです。

どっかのドキュメントに書いてあるのか？全然気づかなかった....

##### 2. workerで動かすときは Assetは絶対pathで指定

相対pathだとなんだか上手く動きませんでした。

こんなところでしょうか。ライブラリのファイルさえ間違えなければ、そんなに難しくはないです。

### まとめ

読み込むファイルが違っていて、何時間もうごかねー！ってなってましたが、正しいファイルを読み込めばうごきます。
ここさえ間違えなければ実は簡単です。

Pixi.js v8を使って手軽にweb workerで動くアニメーションをぜひ作ってみてください！

[ソースコードはこちら](https://github.com/tadfmac/pixijs-worker-example)