Toutes les pages de formats ↓
Graphismes et médias

GAF — Graphics Animation Format

Source : coreprime/kbot @ 0ab97bf · Modifier sur GitHub

Le GAF stocke des animations de sprites en couleurs indexées. Presque tous les éléments 2D animés de Total Annihilation sont des GAF : curseurs, pointeurs de souris, explosions, gadgets d'unités, balancement des arbres, icônes d'armes, bulles de lave, et tout le reste. TA: Kingdoms réutilise le même conteneur sous l'extension .taf pour ses animations à palette (la palette vient d'un fichier compagnon). TA:K livre aussi une variante truecolor du .taf, qui stocke des pixels ARGB 16 bits au lieu d'index ; celle-ci a sa propre page, TAF / TSF.

séquence cursormove de cursors.gaf    une frame de cursorselect

Tip

Essayez vous-même.

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

Consultez la référence de kbot gaf de la CLI pour toutes les options.

Depuis Go. Utilisez 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))
}

Vue d'ensemble

┌─ 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 ─┐
└──────────────────────────────────┘

Un GAF est un tableau plat de séquences ; chaque séquence est un tableau de frames ; chaque frame est un bitmap d'index de palette de largeur × hauteur, avec un point d'origine et, éventuellement, des plages de transparence de type RLE. Les frames peuvent porter des « sous-frames », de sorte qu'une étape logique de l'animation soit composée de plusieurs calques superposés.


En-tête (12 octets)

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;
Champ Remarques
Version 0x00010100 pour tous les outils tiers. Les fichiers anims/terrain.gaf et anims/vismasks.gaf de Cavedog sont livrés avec Version == 0. Le jeu ne vérifie pas ce mot ; kbot lit n'importe quelle valeur (une valeur inhabituelle est signalée par un avertissement) et écrit 0x00010100.
SequenceCount Immédiatement suivi de SequenceCount × uint32 d'offsets absolus dans le fichier, chacun pointant vers un SequenceHeader. Le jeu n'utilise que les 16 bits de poids faible, lus comme une valeur signée : de 1 à 32 767 séquences, aucune pour zéro ou une valeur négative.
Unknown1 Toujours zéro ; écrire zéro.

Séquence (40 octets)

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;

Le jeu joue une séquence en boucle quand l'octet de poids faible de LoopFlags est non nul, et une seule fois (en s'arrêtant sur la dernière frame) quand il vaut 0. Toutes les séquences d'origine stockent 1. kbot conserve les deux mots quand il réécrit un fichier, et kbot gaf build donne la valeur d'origine 1 à une séquence sans sequence.csv.

Suivi de FrameCount × FrameListItem :

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

Le jeu ne lit que les 16 bits de poids faible de Duration et affiche une frame pendant max(Duration, 1) ticks de son horloge d'animation à 30 Hz.

Les noms de séquences comptent : le jeu cherche les animations par leur nom (par ex. UnitInfo fait référence à corpyro_smoke depuis le script corpyro.gaf). Traitez-les comme de l'ASCII insensible à la casse lors des comparaisons.


Frame (info de 24 octets + charge utile)

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 décrivent le point d'ancrage utilisé pour copier la frame dans le monde. Pour un sprite d'explosion, l'origine est en général le centre de la déflagration ; pour un curseur, c'est le pixel du point chaud (hotspot).

Frames à calques

Quand LayerCount > 0, PtrFrameData est un tableau de LayerCount × uint32 pointeurs vers des FrameInfo imbriqués, et non des données de pixels. Chaque calque est dessiné dans l'ordre du tableau (de l'arrière vers l'avant), avec son point chaud placé sur celui de la frame englobante. Un calque dont l'octet +11 est positionné est dessiné à travers la table de translucidité au lieu d'être copié (aucun fichier d'origine ne le positionne). L'octet +10 et l'octet +11 sont des champs distincts : les lire comme un seul compteur de 16 bits transforme un octet +11 positionné en 256 calques.

Données de pixels

TransparencyIndex est propre à chaque frame, et non global au fichier : le GAF des curseurs, par exemple, utilise l'index 9 (le rose système) sur certaines frames et l'index 0 sur d'autres.

Non compressé (Compressed == 0)

Width × Height octets, de haut en bas et de gauche à droite. Le jeu dessine chaque pixel, sauf ceux qui valent TransparencyIndex. Les textures d'unités (textures/*.gaf) et les masques de vision (anims/vismasks.gaf) sont lus comme de simples tableaux de pixels : leurs frames doivent donc être brutes, et toutes les frames d'origine le sont.

Compressé (Compressed != 0) — codage par plages de TA

Les données de pixels sont une suite de Height lignes indépendantes. Chaque ligne commence par un uint16 lineLength, suivi d'un flux d'octets codé par plages, long de lineLength octets exactement. Dans ce flux, chaque chunk commence par un masque d'un octet dont les deux bits de poids faible déterminent le type de chunk :

Test du masque Type de chunk Longueur (pixels) Octets suivants
mask & 0x01 Plage transparente count = mask >> 1 (aucun — les pixels sont sautés)
mask & 0x02 Plage répétée count = (mask >> 2) + 1 1 octet — l'index de couleur à répéter
ni l'un ni l'autre Copie littérale count = (mask >> 2) + 1 count octets — les pixels littéraux

Les longueurs maximales par chunk sont donc de 127 pixels transparents, 64 pixels répétés et 64 pixels littéraux. Seuls les pixels sautés sont transparents : un pixel répété ou littéral est dessiné même quand sa valeur est égale à TransparencyIndex, et l'index 0 de la palette est un noir opaque ordinaire. Si la ligne codée se termine avant Width, kbot la complète avec des pixels transparents et le signale ; le jeu, lui, continuerait à lire dans les octets suivants. Les calques peuvent être plus petits que la frame englobante et sont positionnés par leurs propres OriginX/Y.

Référence : formats/gaf/gaf.go readCompressed et formats/gaf/writer.go compressRow.

Important

Le GAF n'a pas de palette. Le fichier ne stocke que des index sur 8 bits. Pour l'afficher en couleur, il faut soit la palette globale de TA (palettes/palette.pal), soit, pour TA: Kingdoms, une palette propre à chaque ressource, stockée hors du GAF. Voir [Résolution de la palette de TA: Kingdoms] (#ta-kingdoms--palette-resolution) et Particularité de transparence de TA: Kingdoms pour la mécanique propre à TAK. kbot gaf export utilise par défaut la palette TA intégrée et la règle de transparence du jeu ; les deux peuvent être remplacées.


TA: Kingdoms — résolution de la palette

Note

Cette section ne décrit que les écarts propres à TA:K. Le moteur de rendu de TA utilise toujours palettes/palette.pal ; la disposition des octets du GAF est identique entre TA et TAK, mais TAK a introduit une chaîne de palettes propres à chaque ressource.

TA: Kingdoms livre le même conteneur .gaf (parfois .taf) que TA, mais tire la palette d'un fichier compagnon propre à chaque ressource. L'installation d'origine de TAK suit trois conventions documentées, par ordre de priorité :

  1. Un .pcx de même nom à côté du .gaf. Les ressources d'interface et d'animation sont livrées avec un fichier PCX de 1×1 dont le chunk PLTE contient la palette voulue. anims/actionbuttons.gaf est rendu avec anims/actionbuttons.pcx. Environ 38 % des fichiers anims/*.gaf de TAK ont un tel fichier compagnon.

  2. Un .pal de même nom de base dans palettes/. Rare ; concerne une poignée de ressources d'origine pour lesquelles Cavedog a exporté une palette brute de 1 024 octets.

  3. Une palette propre au camp, d'après le préfixe du nom de fichier. Les GAF d'unités et d'éléments de décor sont nommés avec un préfixe de camp de 3 lettres et utilisent la palette du camp correspondant :

    Préfixe Camp Fichier de palette
    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

Si aucune de ces règles n'aboutit, le résolveur se replie sur palettes/palette.pal, puis en dernier recours sur la palette TA intégrée — deux palettes identiques à l'octet près dans le TAK d'origine, et identiques à celle de TA : pour les GAF d'unités ou d'éléments de décor qui ne correspondent à aucun préfixe de camp, les couleurs seront donc fausses sans remplacement explicite.

Le résolveur de kbot

internal/palettepick met en œuvre la chaîne ci-dessus. Chaque point d'entrée qui affiche un GAF passe par elle :

  • Explorateur web. Les routes /png/… et /apng/… détectent la palette automatiquement par défaut ; la visionneuse GAF propose une liste déroulante de palettes qui recense chaque candidate trouvée par le résolveur, plus une entrée auto. Passez ?palette=<vfs-path> pour en imposer une (la liste déroulante le fait pour vous).
  • kbot gaf export / kbot gaf dump. Passez --palette <path.pcx|path.pal> pour la remplacer ; sinon, c'est la palette TA intégrée qui est utilisée (la CLI n'exécute pas le résolveur VFS, car elle n'a pas de contexte VFS, sauf si vous l'enveloppez avec kbot mount).
  • Outil MCP gaf_export. Accepte un argument palette facultatif (un chemin dans le dossier game-data configuré) ; s'il est omis, le résolveur complet s'exécute.

Porter un GAF de TA vers TA: Kingdoms

Les dispositions des octets des GAF de TA et de TAK sont identiques : seule la source de la palette diffère. Pour porter un .gaf de TA existant :

  1. Créez un PCX à palette de 1×1 dans lequel la palette de TA est intégrée.
  2. Enregistrez-le à côté du GAF avec le même nom de base (par ex. MyGaf.gaf + MyGaf.pcx).

Cela suffit pour que TAK l'affiche avec les couleurs d'origine de TA.


TA: Kingdoms — particularité de transparence

Note

Cette section ne décrit que les écarts propres à TA:K. L'octet TransparencyIndex de la section Frame fait foi pour le moteur de rendu de TA, mais les auteurs de contenu de TAK ne l'ont pas maintenu cohérent avec les données de pixels réelles des frames non compressées.

De nombreux GAF d'atlas de textures de TAK (tout ce qui se trouve sous textures/*.gaf et une bonne partie des anims/*.gaf) contiennent une frame non compressée dont la métadonnée TransparencyIndex indique une constante (souvent 9) qui n'apparaît jamais dans les données de pixels réelles, alors que l'artiste a rempli la bordure « transparente » avec un autre index de palette (fréquemment 5). Une lecture littérale de la métadonnée produit un fond coloré opaque là où le moteur de rendu de TA aurait produit de la transparence.

Le moteur de rendu de TA ne devine jamais : il utilise la clé stockée pour les frames brutes et les commandes de saut pour les frames compressées. Pour les atlas de TAK, kbot propose une heuristique de détection par les coins, à activer explicitement (TransparencyModeHeuristic, voir EffectiveTransparencyIndex dans formats/gaf de kbot-io), qui ne s'applique qu'aux frames brutes :

  1. Si TransparencyIndex est présent quelque part dans le tampon de pixels, faire confiance à la métadonnée.
  2. Sinon, échantillonner les quatre coins. Si les quatre ont la même valeur, l'utiliser comme index transparent effectif. Cela rattrape les frames non compressées de TAK où l'artiste a dessiné une bordure uniforme.
  3. Sinon, se replier sur le TransparencyIndex stocké sur le disque (ce qui préserve le rendu des textures à fond perdu comme dungwormB1, dont les coins sont des pixels de l'unité — la détection automatique renonce à juste titre à remplacer la valeur).

Les frames compressées et composites utilisent toujours leurs pixels sautés. L'octet stocké sur le disque n'est jamais écrasé : les modules d'écriture en aller-retour retrouvent la valeur d'origine. Une frame TA entièrement opaque avec une bordure uniforme perdrait cette bordure avec l'heuristique : les rendus de TA ne l'utilisent donc jamais.

Remplacer la transparence au rendu

gaf.RenderOptions expose ces modes :

Mode Comportement Cas d'usage
TransparencyModeAuto / TransparencyModeMetadata La règle du jeu : les pixels de clé d'une frame brute et les pixels sautés d'une frame compressée sont transparents ; l'index 0 est un noir opaque (la valeur zéro, et le mode par défaut partout dans kbot pour TA). Affichage, extractions, allers-retours.
TransparencyModeHeuristic Estimation par les coins pour les frames brutes (voir ci-dessus), la règle du jeu sinon. C'est ce qu'utilise le studio pour les sprites de TA: Kingdoms. Atlas de textures de TAK.
TransparencyModeNone Tous les pixels sont opaques. Examiner la couleur clé.
TransparencyModeIndex Utilise un index fourni par l'appelant. Remplacement depuis l'interface ; débogage.

Chaque entrée de palette exportée est opaque, sauf un emplacement transparent par export, normalement la clé de la frame. Quand une frame dessine des pixels dont la valeur est égale à sa clé (ou quand les frames d'une même animation utilisent les clés les unes des autres comme couleurs), c'est un index qu'aucun pixel dessiné n'utilise qui devient cet emplacement, pour que l'image conserve tous les index de la palette.

La visionneuse GAF de l'explorateur de ressources propose Default / Game rule / Corner guess / None dans une liste déroulante à côté du sélecteur de palette, et transmet le choix sous la forme ?transparency=game|heuristic|none|<N> à chaque requête PNG/GIF/APNG. Par défaut, c'est la règle du jeu pour TA et l'estimation par les coins pour les installations de TA: Kingdoms. La clé de cache inclut l'étiquette de transparence : changer de mode ne renvoie donc jamais de rendus périmés.

L'outil MCP gaf_export applique le même choix par défaut : la règle du jeu, sauf qu'un dossier game-data enregistré comme TA: Kingdoms (un contexte kbot takingdoms) reçoit l'estimation par les coins ; son résultat indique le mode utilisé. kbot gaf export lit un simple fichier sans contexte de jeu : il utilise donc par défaut la règle du jeu ; passez --transparency heuristic pour les atlas de TA: Kingdoms. kbot gaf dump utilise toujours la règle du jeu, puisque kbot gaf build relit les clés stockées à partir des images.


Exemple commenté — 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
...

Une frame typique de 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)

Construire un GAF

kbot gaf dump écrit une arborescence de répertoires que kbot gaf build sait relire :

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

Les images des frames sont des PNG indexés (ou des GIF avec --format gif) dessinés selon la règle du jeu : chaque pixel garde son index de palette, et seuls les pixels transparents sont marqués. L'étape de construction recopie ces index et transforme les pixels transparents en clé de la frame ; les images RGBA sont, elles, ramenées à la couleur de palette la plus proche, en évitant la clé.

  • sequence.csv restitue l'ordre des séquences, leur nom exact, leur mot de boucle et leur mot +4. Sans ce fichier, le nom du dossier devient le nom de la séquence, et la séquence boucle, comme toutes les séquences d'origine.
  • storage (raw ou compressed) et blend (l'octet +11) restituent l'en-tête de chaque frame. Les frames sans storage sont compressées, sauf que chaque frame écrite dans textures/*.gaf ou anims/vismasks.gaf est brute, car le jeu lit ces fichiers comme de simples tableaux de pixels. --storage raw|compressed a priorité sur les deux.
  • Les frames composites (à calques) sont extraites comme une seule image aplatie et reconstruites comme des frames simples ; kbot gaf roundtrip indique combien il y en a.
  • Une frame sans pixels (largeur ou hauteur 0) n'a pas d'image ; sa ligne de frames.csv suffit à la reconstruire.
  • Un sous-dossier qui n'a ni frames.csv ni sequence.csv est ignoré, avec un avertissement. Tout autre sous-dossier impossible à construire (image manquante ou illisible, valeur CSV incorrecte) arrête la construction : une séquence n'est donc jamais omise en silence.

Note

Les GAF de TA et de TA:K sont identiques à l'octet près. Le conteneur est le même ; seule la chaîne de palettes diffère. Voir Résolution de la palette de TA: Kingdoms plus haut pour la chaîne complète et la recette commentée pour porter un GAF vers TAK.


Pièges

Warning

Une séquence dont l'octet de boucle vaut 0 se joue une fois puis s'arrête. Les outils qui écrivent le mot +2 à zéro figent chaque élément de décor animé qu'ils touchent. kbot conserve ce mot, et les nouvelles séquences construites par kbot bouclent.

  • Certaines séquences sont livrées avec des durées de frame de 0. Le jeu affiche une telle frame pendant un tick, comme une durée de 1. Ne les « corrigez » pas avec une valeur par défaut.
  • Les lignes de pixels compressées peuvent être plus courtes que Width. Le jeu continue à lire les octets suivants comme des commandes ; kbot complète la ligne avec des pixels transparents et le signale. Aucun fichier d'origine ne contient de telles lignes.
  • Les origines des calques sont des points chauds : chaque calque est placé de sorte que son origine tombe sur celle de la frame englobante.
  • Le .taf de TA: Kingdoms existe en deux variantes. kbot lit un .taf à palette comme un GAF ordinaire, mais TA:K livre aussi des fichiers .taf truecolor (pixels ARGB 16 bits, sans palette). Vérifiez l'octet de format de frame pour les distinguer, et consultez TAF / TSF pour la variante truecolor.
  • anims/terrain.gaf et anims/vismasks.gaf ont Version == 0. Le jeu ignore le mot de version.
  • Le nombre de frames par séquence n'est pas borné : cursorteleport a 46 frames. Ne supposez pas de plafond fixe pour dimensionner les tampons.

Tailles typiques

Ressource Plage observée dans les GAF de Cavedog
anims/cursors.gaf ~30 Ko, 22 séquences, 1–46 frames chacune
Gadget d'unité (par ex. éclair de tir) 5–20 Ko, 1 séquence, 4–12 frames
Explosion (famille anims/explosions.gaf) 50–250 Ko par fichier
anims/textures.gaf (la planche de textures principale) ~12 Mo (le plus gros GAF de TA)
anims/terrain.gaf ~1,5 Mo
Données par frame En général <1 Ko ; les grandes frames (explosions de 256×256) atteignent 10 Ko et plus

Exemples concrets dans le catalogue de référence

Quelques GAF notables de l'installation de TA — extrayez-en n'importe lequel avec kbot hpi extract, puis kbot gaf list / kbot gaf export :

  • anims/cursors.gaf — 22 séquences, chacune une animation de curseur de l'interface. Les plus petites séquences (cursorgrn, cursorred) sont des frames uniques ; la plus grande (cursorteleport) compte 46 frames sur 460 ticks. Présenté dans l'exemple commenté plus haut.
  • anims/textures.gaf — ~12 Mo, la planche de tuiles principale référencée par les chaînes OffsetToTextureName de chaque modèle 3DO. Parcourez-la avec kbot gaf list anims/textures.gaf | head.
  • Famille anims/explosions.gaf — des GAF d'effets typiques : lasersht.gaf, acid.gaf, bigexpls.gaf, qui contiennent chacun plusieurs séquences d'explosion nommées.
  • anims/armcom.gaf / corcom.gaf — gadgets du commandant propres à chaque camp (cercle de sélection, laser de construction).
  • anims/terrain.gaf + anims/vismasks.gaf — les seuls GAF d'origine avec Version == 0 dans l'en-tête (bon à savoir quand on écrit un analyseur).

Voir aussi

  • TAF / TSF — la variante truecolor .taf de TA: Kingdoms, bâtie sur ce même conteneur, et sa forme texte modifiable.
  • PAL — la table de couleurs nécessaire pour afficher les frames GAF.
  • PCX — comment TA:K fournit une palette à un .taf à palette.
  • 3DO — quand un « sprite » est en réalité un modèle 3D texturé (la plupart des explosions sont des GAF ; la plupart des projectiles sont des 3DO + un éclair de tir en GAF).
  • Glossaire — sous-frame, origine, tick.