Все страницы форматов ↓
Графика и медиа

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

Все флаги описаны в справочнике по 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 следует трём задокументированным соглашениям, в порядке приоритета:

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

  2. .pal с тем же базовым именем в palettes/. Встречается редко: несколько стандартных ресурсов, для которых Cavedog выгрузила сырую палитру на 1024 байта.

  3. Палитра стороны по префиксу имени файла. GAF юнитов и объектов называются с трёхбуквенным префиксом стороны и используют палитру этой стороны:

    Префикс Сторона Файл палитры
    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 (командная строка не запускает механизм выбора через 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. Для движка отрисовки TA байт TransparencyIndex из раздела Кадр имеет решающее значение, но авторы контента 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 ведёт себя так же: правило игры, но для папки 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 бывает двух видов. Палитровый .taf kbot читает как обычный 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.