GAF — Graphics Animation Format
Джерело: coreprime/kbot @ 0ab97bf · Редагувати на GitHub
GAF зберігає анімації спрайтів з індексованими кольорами. Майже кожен анімований 2D-елемент у Total Annihilation — це GAF: курсори, вказівники миші, вибухи, деталі юнітів, погойдування дерев, значки зброї, бульбашки лави — геть усе. TA: Kingdoms використовує той самий контейнер під розширенням
.tafдля своїх палітрових анімацій (палітра береться із супровідного файлу). У TA:K є також повнокольоровий варіант.taf, що зберігає 16-бітні пікселі ARGB замість індексів, — йому присвячено окрему сторінку, TAF / TSF.
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
дотримується трьох задокументованих домовленостей, у порядку пріоритету:
Однойменний
.pcxпоруч із.gaf. Ресурси інтерфейсу й анімацій постачаються з PCX-файлом 1×1, у який потрібну палітру вбудовано як фрагмент PLTE цього PCX.anims/actionbuttons.gafрендериться з палітроюanims/actionbuttons.pcx. Близько 38% файлівanims/*.gafу TAK мають такий супровідний файл..palз тим самим базовим іменем уpalettes/. Трапляється рідко; стосується кількох стандартних ресурсів, для яких Cavedog експортувала сиру 1024-байтову палітру.Палітра сторони за префіксом імені файлу. GAF юнітів і об'єктів мають у назві 3-літерний префікс сторони й використовують палітру відповідної сторони:
Префікс Сторона Файл палітри ara*Aramon palettes/aramon.pcxtar*Taros palettes/taros.pcxver*Veruna palettes/veruna.pcxzon*Zhon palettes/zhon.pcxaid*Aiden (Iron Plague) palettes/aiden.pcxcre*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:
- Створіть палітровий PCX розміром 1×1 із вбудованою палітрою TA.
- Збережіть його поруч із 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); її треба ввімкнути явно, і вона застосовується
лише до нестиснених кадрів:
- Якщо
TransparencyIndexтрапляється будь-де в піксельному буфері, довіряти метаданим. - Інакше взяти зразки з чотирьох кутів. Якщо всі чотири мають однакове значення, використати його як фактичний прозорий індекс. Це рятує нестиснені кадри TAK, де художник намалював однорідну рамку.
- Інакше повернутися до
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.