pixi.jsでビットマップフォントを使ってみる

※pixi.jsはv5.x系、WebGL Rendererを使っている前提で話してます。

# はじめに
pixi.jsには標準で好きなフォントでテキストを描画する機能が備わっています。

```js
const text = new PIXI.Text("好きな文字列", {
  fontFamily: "Aldrich"
});
```

とても便利で、静的なテキスト表示であればこれで十分です。しかしテキスト内容を頻繁に更新する場合は注意が必要です。
というのもpixi.jsはテキスト描画の際、内部Canvasに描画=>テクスチャとしてGPUにアップロードするという処理を行っています。

> When text changes, this texture has to be re-generated and re-uploaded to the GPU, taking up time.

*https://pixijs.download/dev/docs/PIXI.Text.html*

GPU上のテクスチャは描き換えられないので、テキスト内容やスタイルを変更するたびCanvas再描画＆再アップロードを行うことになります。
これはそれなりにパフォーマンスに影響を及ぼすので、フレーム単位での更新の多いテキストには**[BitmapText](https://pixijs.download/dev/docs/PIXI.BitmapText.html)**を使うことが推奨されてます。


# BitmapTextとは？
`BitmapText`は予め画像（テクスチャ）化したフォントを、テキスト情報に合わせて並び替えて表示することで擬似的にテキスト描画を行うクラスで、いわばフォント版スプライトシート（テクスチャアトラス）です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81823/fd046133-1423-e92b-f5d9-da89fc77b19b.png)

最初のロードの際に元画像をGPUアップロードしますが、以降はテキスト内容を変更しても頂点や座標等が更新されるだけで、GPUアップロードは一切発生しないため、変更コストはほぼかかりません。

> The primary advantage of this class over Text is that all of your textures are pre-generated and loading, meaning that rendering is fast, and changing text has no performance implications.

*https://pixijs.download/dev/docs/PIXI.BitmapText.html*

また複数のBitmapTextを使っても、同じフォントならテクスチャを使い回せるのでメモリとドローコールの節約にもつながります。

さらに技術的な利点ではありませんが、フォントのライセンス条件の抵触を回避できるという副次的な利点があります。
フォントによっては静的な画像として利用することは許可してても、フォントファイルを直接アプリケーションに埋め込むことを禁止している場合があります。
BitmapTextはその点、アプリケーションに含めるのはあくまでフォント画像（と、マッピングデータ）のみであるため、この種のライセンスに違反しない…はずです。（詳しくはご自身で確認・判断してください）

しかしデメリットももちろんあって

- フォントファイルとはまた別にファイルを用意しないといけない（v5.3以降は不要な場合も。[後述](#BitmapFont.fromを使った方法（v5.3以降）)）
- 文字間隔の調整など、`PIXI.Text`ほど細かいスタイルが設定できない
- あくまでビットマップ画像なので`PIXI.Text`ほど手軽に拡大・縮小できない
- 日本語など文字数が多いフォントには使いづらい（テクスチャデータが肥大化するため）

などがありますが、ゲームのスコア表示など、頻繁に更新＆英数字オンリーなUI部分には使いやすいと思われます。

以下、使い方などを書いていきます。


## 基本的な使用方法
ビットマップフォント用のファイルを事前に用意してそれを表示します。

### 前準備：ビットマップフォントデータの作成
BitmapTextを使うにはまずそれ用のファイル作成が必要になります。こちらは

- フォント画像データ（pngファイル）
- fntファイル（画像と対応する文字の座標データ、その他メタデータを含んだxml形式のファイル）

が必要となります。
手作業で作ることも不可能ではないですが、通常はツールを頼ります。

- windowsなら[bitmap font generator](http://www.angelcode.com/products/bmfont/)、
- macなら[bmglyph](http://www.bmglyph.com/)

が公式のおすすめとして挙げられてます。

筆者の環境はwinなのでbitmap font generatorを使いました。
使い方の詳しい説明は以下の記事に譲ります。こちらで書かれている通りに操作すればpixi.jsでも使えるフォントが作成できるはずです。

[BMFont(Bitmap Font Generator) で NGUI 向けのビットマップフォントを作る方法](http://nakamura001.hatenablog.com/entry/20120910/1347241168)

ただしexport optionsにて`Font discriptor`の設定を「XML」、`Textures`を「png」にすることを忘れないようにします。
（デフォルトではそれぞれText、tgaになってます）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81823/33a63475-4626-ff48-3e52-72ebdd27156d.png)

ここで設定を間違えるとフォントがひっくり返ったりして正しく表示できなくなるので慎重を期しましょう。


### ビットマップフォントを表示
ビットマップフォントが用意できたら後はほぼTextクラスとおなじです。
フォントをロード後、BitmapTextクラスをインスタンス化します。
なお、フォント画像pngとfntファイルは同じ階層に置くようにします。（Loaderはfntファイルに書かれたパスを参照してロードを行います）

```js
const loader = new PIXI.Loader()
loader.add('HeaderFont', './fonts/aldrich-bitmap32x32.fnt')
.load(()=> {
  // ロード完了後
  const bmtext = new PIXI.BitmapText('表示したいテキスト', {
    // フォント指定は文字列・オブジェクトどちらでもOK
    // font: '32px Aldrich', // 文字列で指定する場合
    font: {
      name: 'Aldrich', // fntファイルの'face'に指定されたラベル
      size: 32,
    },
    align: 'center', // 文字の整列位置。複数行のときのみ有効
    tint: 0xFF0000, // 色をつけたいときに指定（16進数指定）
  })
  
  // 子要素として追加
  app.stage.addChild(bmtext)
})
```

コメントにも書いてますが、フォント名は**fntファイルの`face=`で指定されている名前を指定する**ことに注意です。（ファイル名でも、ロード時につけたキー名（ここでは`HeaderFont`）でもありません）

```xml
<info face="Aldrich" size="32" bold="0" italic="0" charset="" unicode="1" stretchH="100" smooth="1" aa="1" padding="0,0,0,0" spacing="1,1" outline="0"/>
```

また文字色は変更できませんが、真っ白なビットマップフォントに`tint`値を設定することで擬似的に色をつけることは可能です。

**[実行結果](https://pentamania.github.io/pixi_bitmap-text_sample/)**
（テキストを設定時より大きくするとややぼやけることから、画像参照であることがより分かりやすいです。）


## BitmapFont.fromを使った方法（pixi.js v5.3以降）
v5.3からになりますが、動的にビットマップフォントを工面する手段が用意されています。

```js
const app = new PIXI.Application();
document.body.appendChild(app.view);

PIXI.BitmapFont.from(
  'myBMfont', // BitmapTextの`fontName`として使用する登録名
  {
    // ビットマップフォントの基本スタイルを指定
    fill: "#333333",
    fontSize: 40,
  }
);

// ビットマップフォントを表示
const bmText = new PIXI.BitmapText(
  'Hello World', // 表示テキスト
  { 
    fontName: 'myBMfont', // 先程のフォント登録名を指定
    fontSize: 80,
  }
);
bmText.position.set(app.renderer.screen.width/2, app.renderer.screen.height/2)
bmText.anchor.set(0.5)
app.stage.addChild(bmText)
```

こちらの例では`BitmapFont.from`で色指定#333333、サイズが40pxのフォント一覧画像を内部canvasであらかじめ作り、それを使ってテキストを表示しています。
上記例ではブラウザデフォルトのフォントが使われますが、スタイル指定のところで`fontFamily`を指定すれば任意のwebフォントが使えます。その場合は事前にフォントがロードされている必要があります。ここでその方法を書くと長くなるので割愛しますが、webfontloader.jsを使った方法を**[こちら](https://github.com/pentamania/pixi_bitmap-text_sample/blob/master/BitmapFont-from.html)**に書いてます。

なおデフォルトでは英数字のみがテクスチャ化されますが、第三引数のオプションで対象文字を指定することもできます。
日本語を含めたい、テクスチャサイズを節約したいときなどに有効です。

```js
PIXI.BitmapFont.from(
  'myHiraganaFont',
  {
    fill: "#333333",
    fontSize: 40,
    fontFamily: "Noto Sans JP"
  },
  {
    chars: "あいうえおかきくけこ…" // テクスチャ化したい文字群
  }
);
```

その他のオプションはドキュメントを参照してください。
https://pixijs.download/dev/docs/PIXI.BitmapFont.html#.from

**※こちらの方法はフォントファイルの埋め込みが必要なため、先述したライセンス条件の回避には使えません**

## webpackで使う場合（仮）
※以下は公式もしくはそれに準ずるサイトに書かれている方法ではなく、我流となります。

webpackではモジュール解決機能をつかうことになると思いますが、バンドル後にfntファイルと画像pngファイルのパスがsrcディレクトリと変わってしまうため、pixi.jsのLoaderをそのまま使うことができません。（これはwebpackの設定次第かも？）
なので個別にファイルをimportして手動で登録処理をする必要があります。  


### 前準備
- pixi.js、raw-loader、url-loaderをインストール
- webpack.config.jsを以下のように設定

```webpack.config.js
module.exports = {
  // ...省略 

  module: {
    rules: [
      // fntファイルを文字列としてインポート
      {
        test: /\.fnt$/i,
        use: 'raw-loader',
      },
      // 画像をdataURIにする（あるいは出力してモジュール解決後のパスを得る）
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
          },
        ],
      },
    ],
  },

  // ...省略 
};
```

### メイン処理

1. raw-loaderでfntファイルを文字列としてimport
2. １の文字列をDOMParcerを使ってXMLDocumentに変換
3. 画像をPIXI.Texture化する
4. BitmapText.registerFontメソッドで２.と３.を組み合わせてビットマップフォントを登録

その後はローダーを使った時と一緒です。

```js
import bitmapFnt from './assets/aldrich-bitmap32x32.fnt'; // raw-loaderで文字列としてインポート
import bitmapFontImage from './assets/aldrich-bitmap32x32_0.png'; // url-loaderによるモジュール解決のパスを取得
import { Application, BitmapText, Texture } from 'pixi.js'
import { BitmapFont } from 'pixi.js' // v5.3以降

// 手動でbitmapFontをパース＆登録
const bitmapFontTexture = Texture.from(bitmapFontImage)
const fntXmlDoc = new DOMParser().parseFromString(bitmapFnt, "application/xml");
BitmapText.registerFont(fntXmlDoc, bitmapFontTexture);

// BitmapText.registerFontはv5.3以降deprecatedとなっている
// もしエラーになるようなら代わりにBitmapFont.installを使う
// BitmapFont.install(fntXmlDoc, bitmapFontTexture);

// ここからメイン処理
document.addEventListener('DOMContentLoaded', function() {
  const app = new Application({
    width: 512,
    height: 512,
    backgroundColor: 0x109090,
  });
  document.body.appendChild(app.view);
  
  // ビットマップフォント表示
  const bmtext = new BitmapText('Hello Bitmap Font', {
    font: {
      name: 'Aldrich', // fntファイルの'face'に指定されたラベル
      size: 32,
    },
    align: 'center',
    tint: 0xFF00FF,
  })
  bmtext.anchor.set(0.5)
  bmtext.position.x = app.view.width / 2
  bmtext.position.y = app.view.height / 2
  app.stage.addChild(bmtext)
})

```

コメントの通り、こちらもv5.3以前・以降で微妙に使うモジュールとメソッドが異なるので注意しましょう。

またテクスチャ用ファイルが複数に渡る場合、配列にして渡すようです。

```js
BitmapText.registerFont(fntXmlDoc, [bitmapFontTexturePage1, bitmapFontTexturePage2])
```

詳しいコードはサンプル用に作ったリポジトリをチェックしてみて下さい。
https://github.com/pentamania/pixi_bitmap-text_sample


# 参考
- [Making and Using a Bitmap Font in PIXI.js v5](https://www.adammarcwilliams.co.uk/creating-bitmap-text-pixi/)