모든 형식 페이지 ↓
그래픽·미디어

FNT — 1비트 비트맵 글꼴

출처: coreprime/kbot @ 0ab97bf · GitHub에서 편집

Total Annihilation에는 화면의 무전 대화(comix.fnt)부터 유닛 브리핑 화면(armbrief.fnt)까지 온갖 곳에 쓰이는 .fnt 파일이 여럿 들어 있습니다. 8비트 문자 코드로 인덱싱하는, 작고 가변 폭인 픽셀당 1비트 비트맵 글꼴입니다 — 사실상 조회 테이블이 딸린 스프라이트 시트입니다.

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을 하나의 불투명한 "플래그" 워드로 읽었지만, 이 둘은 별개의 필드입니다.


글리프 읽기

글리프 페이로드는 끊기지 않는 비트 스트림으로, 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의 처음 열 문자에 대한 오프셋 테이블:

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 "..." 렌더링할 문자열: 한 줄이며, 게임과 마찬가지로 첫 줄바꿈에서 그리기를 멈춥니다.
--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은 플래그 워드가 아니라 기준선과 첫 문자 코드입니다. 첫 문자 코드가 0이 아니면 오프셋 테이블이 짧아집니다.
  • 문자는 유니코드 코드 포인트가 아니라 바이트입니다. €는 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.