すべての形式のページ ↓
グラフィックとメディア

FNT — 1ビットのビットマップフォント

出典:coreprime/kbot @ 0ab97bf · GitHubで編集

Total Annihilationには、画面上の無線のやり取り(comix.fnt)からユニットのブリーフィング画面(armbrief.fnt)まで、さまざまな用途の.fntファイルが一通り同梱されています。どれも小さな可変幅の1ピクセル1ビットのビットマップフォントで、8ビットの文字コードで引くようになっています。要するに、ルックアップテーブルの付いたスプライトシートです。

comix.fntのグリフシート
kbot fnt sheetで16列のスプライトシートとして描画したfonts/comix.fnt

黄色で描画した「kbot」
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"

Tip

自分で試す

kbot fnt info     fonts/comix.fnt                    # one-line summary
kbot fnt describe fonts/comix.fnt --list             # full metadata + glyph list
kbot fnt sheet    fonts/comix.fnt -o sheet.png       # all glyphs as a grid
kbot fnt render   fonts/comix.fnt --text "Commander" --fg "#ffff00" --bg transparent -o caption.png
kbot fnt dump     fonts/comix.fnt -t ./glyphs        # one PNG per character

CLIのkbot fntのリファレンスを参照してください。

Goから使う場合は、formats/fntを使います。

import "github.com/coreprime/kbot/formats/fnt"

f, _ := os.Open("fonts/comix.fnt")
defer f.Close()
font, _ := fnt.LoadFromReader(f)
fmt.Println(font.Height, "px,", font.GlyphCount(), "glyphs")
if g := font.Glyphs['A']; g != nil {
    fmt.Println("A is", g.Width, "px wide")
}

ディスク上の配置

┌─ 4-byte header ──────────┐
│ uint8   Height           │   byte 0: shared by every glyph; the line height
│ uint8   (ignored)        │   byte 1: not part of the height
│ int8    Baseline         │   byte 2: rows the glyphs extend above the pen
│ uint8   FirstChar        │   byte 3: code of the first offset-table entry
├─ (256 − FirstChar) × u16 ┤   file offsets; 0 = glyph not defined
├─ Glyph data ─────────────┤   for each defined glyph:
│   uint8  Width           │   pixel width (1..255), also the pen advance
│   ⌈Width × Height/8⌉     │   1bpp pixels, MSB-first bit stream
└──────────────────────────┘

製品版のフォントはどれもFirstChar = 0なので、ヘッダーとオフセットテーブルは4 + 256 × 2 = 516バイトで、グリフのデータはその直後から始まります。

フィールド 備考
Height バイト0だけ。フォント内のすべてのグリフに共通するピクセル単位の高さ(製品版のフォントでは9〜17)。
Baseline バイト2、符号付き。ペンを行yに置いて描いた文字は、グリフの一番上の行がy − Baselineに来ます。製品版のフォントでは1〜3。
FirstChar バイト3。文字cはオフセットテーブルのエントリc − FirstCharを使い、それより小さいコードにはグリフがありません。製品版のフォントでは0。
Offsets[] 文字のバイトで引きます(製品版のゲームのテキストはWindows-1252)。0はこの文字のグリフはないという意味です。0以外の値は、フォントの先頭からのオフセットで、グリフのWidthバイトを指します。
グリフのWidth 1〜255。幅が0だと、ゲームは0だけ進めてでたらめなものを描きます。kbotはそうしたグリフを読み飛ばし、報告します。

古いツールはバイト2〜3を1つの中身のわからない「flags」ワードとして読んでいましたが、これは別々の2つのフィールドです。


グリフの読み方

グリフのペイロードは、MSBファーストの途切れのないビットストリームで、左から右へ、次に上から下へと詰められています。つまり、行優先の走査です。ビット数はWidth × Height、格納されているバイト数は⌈(Width × Height) / 8⌉です。

def read_glyph(file, offset, height):
    file.seek(offset)
    width = file.read(1)[0]
    if width == 0 or width > 128:
        return None
    n_bits  = width * height
    n_bytes = (n_bits + 7) // 8
    bits    = file.read(n_bytes)
    pixels  = []
    for i in range(n_bits):
        bit = (bits[i // 8] >> (7 - (i % 8))) & 1
        pixels.append(bool(bit))
    return Glyph(width, height, pixels)

カーニングのデータはなく、Widthとは別の送り幅もありません。ゲームはテキストを次のようにレイアウトし、kbot fnt render、MCPのfnt_renderツール、アセットエクスプローラーのテキストプレビューも同じようにします。

  • テキストの各バイトが、そのコードのグリフを選びます(UTF-8のテキストは先にゲームのコードページに変換してください。kbot fnt render --codepageの既定値はWindows-1252です)。
  • グリフはペンをちょうどその幅だけ進め、間隔は空けません。
  • グリフのない文字は何も描かず、0だけ進みます。
  • 最初のNULまたは改行で描画が止まります。

そのため、Aの幅が10ピクセルでスペースのグリフがない13行のフォントでは、"A A"は幅20ピクセルで描かれます。

Note

行の間に埋め草はありません。各走査線は、同じバイトの中で前の走査線が終わったところから続きます。幅5ピクセル、高さ7ピクセルのグリフは⌈35/8⌉ = 5バイトを使い、末尾の5ビットは無視されます。


実例 — comix.fnt

$ kbot fnt describe fonts/comix.fnt
Height:        14 px
Baseline:      1 (glyph rows start this many rows above the pen)
First char:    0x00
Glyphs:        94 / 256 defined
Glyph width:   min=3 max=13 mean=5.9
Ranges:        0x20-0x7D

これは、表示可能なASCII(スペースから}まで)を収めた、高さ14ピクセルのフォントだということです。幅は3(i、l)から13(M、W)までさまざまです。定義されていない162個の枠の大半は表示できない制御文字で、残りはゲームが表示しないLatin-1の追加文字です。

comix.fntの最初の10文字のオフセットテーブル:

char  offset    width  bytes  meaning
0x20  0x021C    4      7     space
0x21  0x0223    3      5     '!'
0x22  0x0228    5      9     '"'
0x23  0x0231    6     11     '#'
0x24  0x023C    5      9     '$'
...

kbot fnt describe --listで同じ内容を出力できます。


描画のヒント

kbot fnt renderは次のオプションに対応しています。

フラグ 効果
--text "..." 描画する文字列。1行だけで、ゲームと同じく最初の改行で描画が止まります。
--codepage NAME UTF-8のテキストの変換先のコードページ:cp1252(既定値。製品版のゲームのもの)、cp1250、cp1251、cp437、cp850、iso-8859-1、iso-8859-15、またはバイトをそのまま渡すraw。対応するバイトのない文字は?になります。
--fg #rrggbb[aa] 前景色。既定値は白。
--bg #rrggbb[aa] | transparent 背景。既定値は透過。
--target PATH 出力するPNGのパス。指定しなければ標準出力。

フォントにない文字は、ゲームと同じく何も描かず、場所も取りません。


注意点

Warning

Offsets[c] == 0は「グリフなし」という意味で、「オフセット0にあるグリフ」ではありません。オフセット0はヘッダーの途中に当たり、構造上ありえない位置です。Cavedogはこの番兵値を未定義の意味で使い回しています。オフセットが0なら、常にグリフがないものとして扱ってください。

  • ビットの順序は、各バイトの中でMSBファーストです。最近のビットマップフォントではLSBファーストのほうが一般的な慣習なので、思い込まないでください。
  • ビットストリームは行の間で途切れません。各走査線の終わりで、バイトの境界まで埋め草を入れることはありません。
  • バイト2と3は、ベースラインと最初の文字コードです。flagsのワードではありません。最初の文字コードが0でなければ、オフセットテーブルは短くなります。
  • 文字はバイトであって、Unicodeのコードポイントではありません。€はWindows-1252ではバイト0x80です。コードポイントを256で割った余りを使うと、間違ったグリフを選んでしまいます。
  • どの文字に対応しているかを示すメタデータはありません。グリフの一覧を知るには、オフセットテーブルを順に調べる必要があります。

典型的なサイズ

アセット Cavedogのfonts/*.fntで見られる範囲
ファイルサイズ 700 B – 4 KB
ヘッダー+オフセットテーブル 常に516バイト
グリフの高さ 9–17 px
グリフの幅 3–13 px
フォントあたりの定義済みグリフ数 60–120(大半は表示可能なASCIIだけ)
グリフあたりのデータ 通常5–30バイト(幅の1バイト+詰められたビット)

関連項目

  • PAL:フォントはパレットを持たないので、描画するには前景色と背景色を選ぶか、TAパレットを自分で使う必要があります。
  • 用語集:1bpp、MSB-first bit stream(MSBファーストのビットストリーム)。