FNT — 1ビットのビットマップフォント
Total Annihilationには、画面上の無線のやり取り(
comix.fnt)からユニットのブリーフィング画面(armbrief.fnt)まで、さまざまな用途の.fntファイルが一通り同梱されています。どれも小さな可変幅の1ピクセル1ビットのビットマップフォントで、8ビットの文字コードで引くようになっています。要するに、ルックアップテーブルの付いたスプライトシートです。
kbot fnt sheetで16列のスプライトシートとして描画したfonts/comix.fnt
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バイト+詰められたビット) |