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
.tafpour 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.
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é :
Un
.pcxde 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.gafest rendu avecanims/actionbuttons.pcx. Environ 38 % des fichiersanims/*.gafde TAK ont un tel fichier compagnon.Un
.palde même nom de base danspalettes/. Rare ; concerne une poignée de ressources d'origine pour lesquelles Cavedog a exporté une palette brute de 1 024 octets.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.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
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éeauto. 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 aveckbot mount).- Outil MCP
gaf_export. Accepte un argumentpalettefacultatif (un chemin dans le dossiergame-dataconfiguré) ; 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 :
- Créez un PCX à palette de 1×1 dans lequel la palette de TA est intégrée.
- 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 :
- Si
TransparencyIndexest présent quelque part dans le tampon de pixels, faire confiance à la métadonnée. - 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.
- Sinon, se replier sur le
TransparencyIndexstocké sur le disque (ce qui préserve le rendu des textures à fond perdu commedungwormB1, 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.csvrestitue 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(rawoucompressed) etblend(l'octet +11) restituent l'en-tête de chaque frame. Les frames sansstoragesont compressées, sauf que chaque frame écrite danstextures/*.gafouanims/vismasks.gafest brute, car le jeu lit ces fichiers comme de simples tableaux de pixels.--storage raw|compresseda priorité sur les deux.- Les frames composites (à calques) sont extraites comme une seule image
aplatie et reconstruites comme des frames simples ;
kbot gaf roundtripindique combien il y en a. - Une frame sans pixels (largeur ou hauteur 0) n'a pas d'image ; sa ligne de
frames.csvsuffit à la reconstruire. - Un sous-dossier qui n'a ni
frames.csvnisequence.csvest 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
.tafde TA: Kingdoms existe en deux variantes. kbot lit un.tafà palette comme un GAF ordinaire, mais TA:K livre aussi des fichiers.taftruecolor (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.gafetanims/vismasks.gafontVersion == 0. Le jeu ignore le mot de version.- Le nombre de frames par séquence n'est pas borné :
cursorteleporta 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înesOffsetToTextureNamede chaque modèle 3DO. Parcourez-la aveckbot 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 avecVersion == 0dans l'en-tête (bon à savoir quand on écrit un analyseur).
Voir aussi
- TAF / TSF — la variante truecolor
.tafde 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.