Усі сторінки форматів ↓
Графіка й медіа

GAF — Graphics Animation Format

Джерело: coreprime/kbot @ 0ab97bf · Редагувати на GitHub

GAF зберігає анімації спрайтів з індексованими кольорами. Майже кожен анімований 2D-елемент у Total Annihilation — це GAF: курсори, вказівники миші, вибухи, деталі юнітів, погойдування дерев, значки зброї, бульбашки лави — геть усе. TA: Kingdoms використовує той самий контейнер під розширенням .taf для своїх палітрових анімацій (палітра береться із супровідного файлу). У TA:K є також повнокольоровий варіант .taf, що зберігає 16-бітні пікселі ARGB замість індексів, — йому присвячено окрему сторінку, TAF / TSF.

послідовність cursormove з cursors.gaf    один кадр із cursorselect

Tip

Спробуйте самі.

kbot gaf list   anims/cursors.gaf                                      # sequences, frame counts, loop flag
kbot gaf export anims/cursors.gaf --format gif --sequence 0 --target cursor.gif
kbot gaf dump   anims/cursors.gaf --target ./cursors                   # one PNG per frame

Усі прапорці див. у довіднику CLI kbot gaf.

З Go. Скористайтеся пакетом formats/gaf:

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

r, _ := gaf.LoadFromFile("anims/cursors.gaf")
defer r.Close()
sequences, _ := r.ReadSequences()
for _, seq := range sequences {
    fmt.Println(seq.Name, len(seq.Frames))
}

Коротко

┌─ Header (12B) ─┐
│ ver  N  pad    │
└────────┬───────┘
         ▼
┌─ Sequence offset table ─┐  N × uint32 absolute offsets
└───┬─────────────────────┘
    ▼
┌─ SequenceHeader (40B) ─┐
│ frameCount, loop word, │
│ +4 word, name[32]      │
└─────────┬──────────────┘
          ▼
┌─ FrameListItem (8B) × frameCount ─┐  ptr + tick duration
└───┬───────────────────────────────┘
    ▼
┌─ FrameInfo (24B) ─┐
│ size, origin,     │
│ transparency,     │
│ compressed?,      │
│ layers, +11 byte  │
│ ptrPixelData      │
└─────┬─────────────┘
      ▼
┌─ Pixel data: raw or run-encoded ─┐
└──────────────────────────────────┘

GAF — це плаский масив послідовностей; кожна послідовність — масив кадрів; кожен кадр — растр індексів палітри розміром ширина × висота з точкою прив'язки (origin) і (необов'язково) серіями прозорості в стилі RLE. Кадри можуть містити «підкадри», тож один логічний крок анімації може бути композицією кількох накладених шарів.


Заголовок (12 байтів)

typedef struct {
    uint32 Version;        // 0x00010100 (or 0 for a handful of stock GAFs)
    uint32 SequenceCount;  // Number of animations in the file
    uint32 Unknown1;       // Always 0
} GAFHeader;
Поле Примітки
Version 0x00010100 в усіх сторонніх інструментах. anims/terrain.gaf і anims/vismasks.gaf від Cavedog постачаються з Version == 0. Гра це слово не перевіряє; kbot читає будь-яке значення (про незнайоме повідомляє попередженням) і записує 0x00010100.
SequenceCount Одразу за ним ідуть SequenceCount × uint32 абсолютних зміщень у файлі, кожне з яких вказує на SequenceHeader. Гра використовує лише молодші 16 бітів, прочитані як знакове значення: 1–32767 послідовностей; за нуля чи від'ємного значення — жодної.
Unknown1 Завжди нуль; під час запису пишіть нуль.

Послідовність (40 байтів)

typedef struct {
    uint16 FrameCount;
    uint16 LoopFlags;      // +2: low byte non-zero = the sequence loops
    uint32 Unknown4;       // +4: not used by the game; stock files store 0
    char   Name[32];       // NUL-padded, case-preserved
} GAFSequence;

Гра зациклює послідовність, коли молодший байт LoopFlags установлено, і відтворює її один раз (зупиняючись на останньому кадрі), коли він 0. У кожній стандартній послідовності тут записано 1. kbot зберігає обидва слова, коли переписує файл, а kbot gaf build присвоює послідовності без sequence.csv стандартне значення 1.

Далі йдуть FrameCount × FrameListItem:

typedef struct {
    uint32 PtrFrameInfo;   // → FrameInfo
    uint32 Duration;       // Game ticks (30 ticks = 1 second); low 16 bits used
} FrameListItem;

Гра читає лише молодші 16 бітів Duration і показує кадр протягом max(Duration, 1) тіків свого таймера анімації з частотою 30 Гц.

Імена послідовностей важливі: гра шукає анімації за іменем (наприклад, UnitInfo посилається на corpyro_smoke зі скрипту corpyro.gaf). Під час зіставлення вважайте їх ASCII-рядками без урахування регістру.


Кадр (24-байтовий опис + дані)

typedef struct {
    uint16 Width;
    uint16 Height;
    int16  OriginX;            // Render anchor, may be negative
    int16  OriginY;
    uint8  TransparencyIndex;  // Key: transparent in raw frames
    uint8  Compressed;         // 0 = raw bitmap, non-zero = run-encoded
    uint8  LayerCount;         // +10: 0 = simple frame, >0 = composite of layers
    uint8  Blend;              // +11: set on a layer = drawn translucently
    uint32 Unknown2;           // +12: editor scratch, not used by the game
    uint32 PtrFrameData;       // → pixel bytes OR layer pointer table
    uint32 Unknown3;           // Always 0
} FrameInfo;

OriginX / OriginY описують точку прив'язки, яка використовується під час виведення (blit) кадру у світ гри. Для спрайта вибуху це зазвичай центр вибуху; для курсора — піксель гарячої точки (hotspot).

Шаруваті кадри

Коли LayerCount > 0, PtrFrameData — це масив із LayerCount × uint32 вказівників на вкладені FrameInfo, а не піксельні дані. Шари малюються в порядку масиву (від дальнього до ближнього), і гаряча точка кожного шару суміщається з гарячою точкою зовнішнього кадру. Шар, у якого встановлений байт +11, малюється через таблицю напівпрозорості замість простого копіювання (жоден стандартний файл його не встановлює). Байти +10 і +11 — окремі поля: якщо прочитати їх як одне 16-бітне число, установлений байт +11 перетвориться на 256 шарів.

Піксельні дані

TransparencyIndex задається для кожного кадру, а не для всього файлу: наприклад, GAF курсорів в одних кадрах використовує індекс 9 (системний рожевий), а в інших — індекс 0.

Без стиснення (Compressed == 0)

Width × Height байтів, згори донизу, зліва направо. Гра малює кожен піксель, крім тих, що дорівнюють TransparencyIndex. Текстури юнітів (textures/*.gaf) і маски видимості (anims/vismasks.gaf) читаються як прості масиви пікселів, тож їхні кадри мають бути нестиснені; усі стандартні кадри там саме такі.

Зі стисненням (Compressed != 0) — кодування довжин серій TA

Піксельні дані — це послідовність із Height незалежних рядків. Кожен рядок починається з uint16 lineLength, за яким іде потік байтів, закодований довжинами серій (RLE), рівно з lineLength байтів. У цьому потоці кожен фрагмент починається з 1-байтової маски, два молодші біти якої визначають вид фрагмента:

Перевірка маски Вид фрагмента Довжина (пікселів) Наступні байти
mask & 0x01 Прозора серія count = mask >> 1 (немає — пікселі пропускаються)
mask & 0x02 Серія повторів count = (mask >> 2) + 1 1 байт — індекс кольору, що повторюється
жодна Літеральна копія count = (mask >> 2) + 1 count байтів — самі пікселі

Отже, максимальні довжини серій — 127 прозорих пікселів, 64 пікселі повтору й 64 літеральні пікселі на фрагмент. Прозорі лише пропущені пікселі: повторений чи літеральний піксель малюється, навіть якщо його значення дорівнює TransparencyIndex, а індекс палітри 0 — звичайний непрозорий чорний. Якщо закодований рядок закінчується раніше за Width, kbot доповнює його прозорими пікселями й повідомляє про це; гра ж читала б далі наступні байти. Шари можуть бути меншими за зовнішній кадр і розташовуються за власними OriginX/Y.

Довідка: formats/gaf/gaf.go readCompressed і formats/gaf/writer.go compressRow.

Important

GAF не містить палітри. Файл зберігає лише 8-бітні індекси. Щоб відрендерити кадр у кольорі, потрібна або глобальна палітра TA (palettes/palette.pal), або, для TA: Kingdoms, окрема палітра ресурсу, що лежить поза GAF. Механізми, специфічні для TAK, див. у розділах [Визначення палітри в TA: Kingdoms] (#ta-kingdoms--palette-resolution) і Особливість прозорості в TA: Kingdoms. kbot gaf export за замовчуванням використовує вбудовану палітру TA й ігрове правило прозорості; обидва можна перевизначити.


TA: Kingdoms — визначення палітри

Note

Цей розділ описує лише відмінності TA:K. Рендерер TA завжди використовує palettes/palette.pal; структура байтів GAF у TA і TAK ідентична, але TAK запровадила конвеєр з окремою палітрою для кожного ресурсу.

TA: Kingdoms використовує той самий контейнер .gaf (іноді .taf), що й TA, але бере палітру із супровідного файлу ресурсу. Релізна інсталяція TAK дотримується трьох задокументованих домовленостей, у порядку пріоритету:

  1. Однойменний .pcx поруч із .gaf. Ресурси інтерфейсу й анімацій постачаються з PCX-файлом 1×1, у який потрібну палітру вбудовано як фрагмент PLTE цього PCX. anims/actionbuttons.gaf рендериться з палітрою anims/actionbuttons.pcx. Близько 38% файлів anims/*.gaf у TAK мають такий супровідний файл.

  2. .pal з тим самим базовим іменем у palettes/. Трапляється рідко; стосується кількох стандартних ресурсів, для яких Cavedog експортувала сиру 1024-байтову палітру.

  3. Палітра сторони за префіксом імені файлу. GAF юнітів і об'єктів мають у назві 3-літерний префікс сторони й використовують палітру відповідної сторони:

    Префікс Сторона Файл палітри
    ara* Aramon palettes/aramon.pcx
    tar* Taros palettes/taros.pcx
    ver* Veruna palettes/veruna.pcx
    zon* Zhon palettes/zhon.pcx
    aid* Aiden (Iron Plague) palettes/aiden.pcx
    cre* Creon (Iron Plague) palettes/creon.pcx

Якщо жоден із цих способів не спрацьовує, механізм визначення повертається до palettes/palette.pal і зрештою до вбудованої палітри TA — у релізній TAK обидві байт у байт збігаються з палітрою TA, тож для GAF юнітів і об'єктів, які не відповідають префіксу сторони, кольори без явного перевизначення будуть неправильні.

Механізм визначення в kbot

internal/palettepick реалізує описаний вище ланцюжок. Його використовує кожна точка входу, що рендерить GAF:

  • Веб-оглядач. Маршрути /png/… і /apng/… за замовчуванням визначають палітру автоматично; переглядач GAF показує випадний список з усіма кандидатами, яких знаходить механізм визначення, а також пункт auto. Передайте ?palette=<vfs-path>, щоб примусово обрати конкретну (випадний список робить це за вас).
  • kbot gaf export / kbot gaf dump. Щоб перевизначити палітру, передайте --palette <path.pcx|path.pal>; інакше використовується вбудована палітра TA (CLI не запускає механізм визначення на основі VFS, бо не має контексту VFS, якщо не обгорнути його в kbot mount).
  • Інструмент MCP gaf_export. Приймає необов'язковий аргумент palette (шлях усередині налаштованої теки game-data); якщо його не вказати, запускається повний механізм визначення.

Перенесення GAF із TA в TA: Kingdoms

Структури байтів GAF у TA і TAK ідентичні — відрізняється лише джерело палітри. Щоб перенести наявний .gaf з TA:

  1. Створіть палітровий PCX розміром 1×1 із вбудованою палітрою TA.
  2. Збережіть його поруч із GAF під тим самим базовим іменем (наприклад, MyGaf.gaf + MyGaf.pcx).

Цього достатньо, щоб TAK рендерила його в оригінальних кольорах TA.


TA: Kingdoms — особливість прозорості

Note

Цей розділ описує лише відмінності TA:K. Байт TransparencyIndex у розділі Кадр є визначальним для рендерера TA, але автори вмісту TAK не стежили, щоб у нестиснених кадрах він відповідав справжнім піксельним даним.

Багато GAF-атласів текстур у TAK (усе в textures/*.gaf і частина anims/*.gaf) містять нестиснений кадр, у метаданих якого TransparencyIndex має якесь стале значення (зазвичай 9), що ніколи не трапляється в самих піксельних даних, — тим часом художник заповнив «прозору» рамку іншим індексом палітри (часто 5). Буквальне прочитання метаданих дає непрозорий кольоровий фон там, де рендерер TA дав би прозорість.

Рендерер TA ніколи не вгадує: для нестиснених кадрів він використовує збережений ключ, а для стиснених — команди пропуску. Для атласів TAK kbot пропонує евристику визначення за кутами (TransparencyModeHeuristic, див. EffectiveTransparencyIndex у formats/gaf з kbot-io); її треба ввімкнути явно, і вона застосовується лише до нестиснених кадрів:

  1. Якщо TransparencyIndex трапляється будь-де в піксельному буфері, довіряти метаданим.
  2. Інакше взяти зразки з чотирьох кутів. Якщо всі чотири мають однакове значення, використати його як фактичний прозорий індекс. Це рятує нестиснені кадри TAK, де художник намалював однорідну рамку.
  3. Інакше повернутися до TransparencyIndex з диска (це зберігає правильний рендеринг текстур на весь кадр на кшталт dungwormB1, кути яких — пікселі юніта; автовизначення слушно відмовляється від перевизначення).

Стиснені й складені кадри завжди використовують свої пропущені пікселі. Байт на диску ніколи не перезаписується — інструменти, що записують файл назад без змін (round-trip), бачать оригінальне значення. Повністю непрозорий кадр TA з однорідною рамкою втратив би цю рамку під дією евристики, тому рендеринг TA її ніколи не використовує.

Перевизначення прозорості під час рендерингу

gaf.RenderOptions надає такі режими:

Режим Поведінка Коли використовувати
TransparencyModeAuto / TransparencyModeMetadata Ігрове правило: прозорі пікселі-ключі нестисненого кадру й пропущені пікселі стисненого; індекс 0 — непрозорий чорний (нульове значення й режим за замовчуванням для TA всюди в kbot). Показ, дампи, round-trip.
TransparencyModeHeuristic Вгадування за кутами для нестиснених кадрів (див. вище), в інших випадках — ігрове правило. Цим режимом студія користується для спрайтів TA: Kingdoms. Атласи текстур TAK.
TransparencyModeNone Кожен піксель непрозорий. Дослідження ключового кольору.
TransparencyModeIndex Використати індекс, заданий під час виклику. Перевизначення в інтерфейсі; налагодження.

Кожен експортований елемент палітри непрозорий, крім одного прозорого слота на експорт — зазвичай ключа кадру. Коли в кадрі малюються пікселі, значення яких дорівнює його ключу (або кадри однієї анімації використовують ключі одне одного як кольори), слотом стає індекс, якого не використовує жоден намальований піксель, тож зображення зберігає кожен індекс палітри.

Переглядач GAF в оглядачі ресурсів пропонує поруч із вибором палітри випадний список Default / Game rule / Corner guess / None і передає вибір як ?transparency=game|heuristic|none|<N> у кожному запиті PNG/GIF/APNG. За замовчуванням для TA діє ігрове правило, а для інсталяцій TA: Kingdoms — вгадування за кутами. Ключ кешу містить мітку прозорості, тож після зміни режиму застарілі рендери не віддаються.

Інструмент MCP gaf_export має те саме значення за замовчуванням: ігрове правило, за винятком теки з даними гри, зареєстрованої як TA: Kingdoms (контекст kbot takingdoms), — вона отримує вгадування за кутами; у результаті інструмента вказано використаний режим. kbot gaf export читає звичайний файл без контексту гри, тож за замовчуванням застосовує ігрове правило; для атласів TA: Kingdoms передайте --transparency heuristic. kbot gaf dump завжди використовує ігрове правило, бо kbot gaf build зчитує збережені ключі назад із зображень.


Розібраний приклад — cursors.gaf

$ kbot gaf list anims/cursors.gaf
GAF: 22 sequence(s), version 0x00010100

#   Name             Frames  Loops  Duration (ticks)  Duration (sec)
─   ────             ──────  ─────  ────────────────  ──────────────
0   cursormove       8       yes    64                2.13
1   cursorgrn        1       yes    10                0.33
2   cursorselect     2       yes    12                0.40
...

Типовий кадр cursormove:

Width:       32
Height:      32
OriginX:     16        ← hotspot in the middle of the cursor
OriginY:     16
Transparency: 0        ← this frame's key (compressed, so only skips are transparent)
Compressed:  1         ← run-encoded
LayerCount:  0         ← flat (no sub-frames)

Збирання GAF

kbot gaf dump записує структуру каталогів, яку kbot gaf build читає назад:

cursors/
├── cursormove/
│   ├── sequence.csv    ← index,name,loop_flags,unknown4
│   ├── frames.csv      ← frame,width,height,origin_x,origin_y,transparency,
│   │                     duration_ticks,duration_sec,storage,blend
│   ├── animated.png    ← preview only; not read back
│   ├── 0.png
│   ├── 1.png
│   └── ...
└── ...
kbot gaf dump  anims/cursors.gaf --target ./cursors
# edit some PNGs / tweak frames.csv or sequence.csv
kbot gaf build ./cursors --target ./cursors-rebuilt.gaf
kbot gaf roundtrip anims/                     # check dump/build keeps what the game reads

Зображення кадрів — це індексовані PNG (або GIF із --format gif), намальовані за ігровим правилом, тож кожен піксель зберігає свій індекс палітри, а позначено лише прозорі пікселі. Під час збирання ці індекси копіюються назад, а прозорі пікселі перетворюються на ключ кадру; зображення RGBA натомість зіставляються з найближчим кольором палітри, оминаючи ключ.

  • sequence.csv відновлює порядок послідовностей, точне ім'я, слово зациклення та слово +4. Без нього ім'ям послідовності стає ім'я теки, а сама послідовність зациклюється, як кожна стандартна.
  • storage (raw або compressed) і blend (байт +11) відновлюють заголовок кожного кадру. Кадри без storage стискаються; виняток — кадри, що записуються в textures/*.gaf або anims/vismasks.gaf: вони завжди нестиснені, бо гра читає ці файли як прості масиви пікселів. --storage raw|compressed перевизначає обидва випадки.
  • Складені (шаруваті) кадри вивантажуються як одне сплощене зображення й збираються назад як прості кадри; kbot gaf roundtrip повідомляє, скільки їх.
  • Кадр без пікселів (ширина чи висота 0) не має зображення; його відновлює лише рядок у frames.csv.
  • Підтека, у якій немає ні frames.csv, ні sequence.csv, пропускається з попередженням. Будь-яка інша підтека, яку не вдається зібрати (відсутнє чи нечитабельне зображення, неправильне значення в CSV), зупиняє збирання, тож послідовність ніколи не випадає мовчки.

Note

GAF у TA і TA:K ідентичні до байта. Контейнер той самий; відрізняється лише конвеєр палітр. Повний ланцюжок і покроковий рецепт перенесення GAF у TAK див. вище, у розділі Визначення палітри в TA: Kingdoms.


Підводні камені

Warning

Послідовність, у якої байт зациклення дорівнює 0, відтворюється один раз і зупиняється. Інструменти, що записують у слово +2 нуль, заморожують кожен анімований об'єкт, якого торкаються. kbot зберігає це слово, а нові послідовності, зібрані kbot, зациклюються.

  • Деякі послідовності постачаються з тривалістю кадрів 0. Гра показує такий кадр один тік, як кадр із тривалістю 1. Не «виправляйте» їх на значення за замовчуванням.
  • Стиснені рядки пікселів можуть бути коротшими за Width. Гра читає далі наступні байти як команди; kbot доповнює рядок прозорими пікселями й повідомляє про це. У стандартних файлах таких рядків немає.
  • Точки прив'язки шарів — це гарячі точки: кожен шар розміщується так, щоб його точка прив'язки збіглася з точкою прив'язки зовнішнього кадру.
  • .taf у TA: Kingdoms буває двох видів. kbot читає палітровий .taf як звичайний GAF, але в TA:K є також повнокольорові файли .taf (16-бітні пікселі ARGB, без палітри). Розрізнити їх можна за байтом формату кадру; повнокольоровий варіант описано на сторінці TAF / TSF.
  • anims/terrain.gaf і anims/vismasks.gaf мають Version == 0. Гра ігнорує слово версії.
  • Кількість кадрів у послідовності не обмежена — cursorteleport має 46 кадрів. Не закладайте фіксовану межу, коли визначаєте розміри буферів.

Типові розміри

Ресурс Діапазон у GAF від Cavedog
anims/cursors.gaf ~30 КБ, 22 послідовності, по 1–46 кадрів
Деталь юніта (наприклад, дульний спалах) 5–20 КБ, 1 послідовність, 4–12 кадрів
Вибух (сімейство anims/explosions.gaf) 50–250 КБ на файл
anims/textures.gaf (головний аркуш текстур) ~12 МБ (найбільший GAF у TA)
anims/terrain.gaf ~1,5 МБ
Дані одного кадру Зазвичай <1 КБ; великі кадри (вибухи 256×256) сягають 10+ КБ

Живі приклади в довідковому каталозі

Примітні GAF в інсталяції TA — будь-який можна видобути через kbot hpi extract, а потім переглянути kbot gaf list / kbot gaf export:

  • anims/cursors.gaf — 22 послідовності, кожна з яких — анімація курсора інтерфейсу. Найменші послідовності (cursorgrn, cursorred) складаються з одного кадру; найбільша (cursorteleport) — 46 кадрів на 460 тіків. Показано в розібраному прикладі вище.
  • anims/textures.gaf — ~12 МБ, головний аркуш тайлів, на який посилаються рядки OffsetToTextureName кожної моделі 3DO. Перегляньте командою kbot gaf list anims/textures.gaf | head.
  • Сімейство anims/explosions.gaf — типові GAF з ефектами: lasersht.gaf, acid.gaf, bigexpls.gaf — кожен містить кілька іменованих послідовностей вибухів.
  • anims/armcom.gaf / corcom.gaf — деталі командирів кожної сторони (коло виділення, будівельний промінь).
  • anims/terrain.gaf + anims/vismasks.gaf — єдині стандартні GAF з Version == 0 у заголовку (це варто знати, коли пишете парсер).

Див. також

  • TAF / TSF — повнокольоровий варіант .taf з TA: Kingdoms, побудований на тому самому контейнері, а також його редагована текстова форма.
  • PAL — таблиця кольорів, потрібна для рендерингу кадрів GAF.
  • PCX — як TA:K надає палітру палітровому .taf.
  • 3DO — коли «спрайт» насправді є текстурованою 3D-моделлю (більшість вибухів — GAF; більшість снарядів — 3DO + дульний спалах у GAF).
  • Глосарій — sub-frame, origin, tick.