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
Все флаги описаны в справочнике по 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 во всех сторонних инструментах. Файлы Cavedog anims/terrain.gaf и anims/vismasks.gaf поставляются с 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 задают точку привязки, по которой кадр выводится в
игровой мир. У спрайта взрыва это обычно центр взрыва, у курсора —
пиксель его активной точки (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) — RLE-кодирование TA
Пиксельные данные — это Height независимых строк. Каждая строка
начинается с uint16 lineLength, за которым идёт RLE-поток ровно из
lineLength байт. Внутри потока каждый фрагмент (chunk) начинается с
однобайтовой маски, два младших бита которой задают вид фрагмента:
| Проверка маски | Вид фрагмента | Длина (в пикселях) | Следующие байты |
|---|---|---|---|
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 которого встроена нужная палитра.anims/actionbuttons.gafотрисовывается с палитрой изanims/actionbuttons.pcx. Такой сопутствующий файл есть примерно у 38 % файловanims/*.gafв TAK..palс тем же базовым именем вpalettes/. Встречается редко: несколько стандартных ресурсов, для которых Cavedog выгрузила сырую палитру на 1024 байта.Палитра стороны по префиксу имени файла. GAF юнитов и объектов называются с трёхбуквенным префиксом стороны и используют палитру этой стороны:
Префикс Сторона Файл палитры 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 (командная строка не запускает механизм выбора через 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. Для движка отрисовки TA байт
TransparencyIndex из раздела Кадр
имеет решающее значение, но авторы контента 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 ведёт себя так же: правило игры, но для
папки game-data, зарегистрированной как 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
Последовательность с нулевым байтом цикла проигрывается один раз и останавливается. Инструменты, которые записывают слово по смещению +2 как ноль, замораживают каждый анимированный объект, к которому прикасаются. kbot сохраняет это слово, а новые последовательности, собранные kbot, зациклены.
- У некоторых последовательностей длительность кадров равна
0. Игра показывает такой кадр один тик, как при длительности 1. Не «исправляйте» их на значение по умолчанию. - Сжатые строки пикселей могут быть короче
Width. Игра продолжает читать следующие байты как команды; kbot дополняет строку прозрачными пикселями и сообщает об этом. В стандартных файлах таких строк нет. - Точки привязки слоёв — это активные точки: каждый слой располагается так, чтобы его точка привязки совпала с точкой привязки внешнего кадра.
.tafв TA: Kingdoms бывает двух видов. Палитровый.tafkbot читает как обычный GAF, но в TA:K есть и полноцветные файлы.taf(16-битные пиксели ARGB, без палитры). Различить их можно по байту формата кадра; полноцветный вариант описан на странице TAF / TSF.- У
anims/terrain.gafиanims/vismasks.gafVersion == 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.