Tutte le pagine dei formati ↓
Grafica e media

GAF — Graphics Animation Format

Fonte: coreprime/kbot @ 0ab97bf · Modifica su GitHub

GAF memorizza animazioni di sprite a colori indicizzati. Quasi ogni elemento 2D animato di Total Annihilation è un GAF: cursori, puntatori del mouse, esplosioni, parti delle unità, alberi che ondeggiano, icone delle armi, bolle di lava, e via dicendo. TA: Kingdoms riutilizza lo stesso contenitore con l'estensione .taf per le sue animazioni con palette (la palette viene presa da un file di accompagnamento). TA:K distribuisce anche una variante .taf truecolor che memorizza pixel ARGB a 16 bit invece di indici: quella ha una pagina tutta sua, TAF / TSF.

sequenza cursormove da cursors.gaf    singolo frame di cursorselect

Tip

Provalo tu stesso.

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

Consulta il riferimento CLI di kbot gaf per tutte le opzioni.

Da Go. Usa 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))
}

In breve

┌─ 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 è un array piatto di sequenze; ogni sequenza è un array di frame; ogni frame è una bitmap larghezza × altezza di indici della palette, con un punto di origine e (facoltativamente) run di trasparenza in stile RLE. I frame possono contenere dei "sotto-frame", così che un singolo passo logico dell'animazione sia la composizione di più livelli sovrapposti.


Intestazione (12 byte)

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;
Campo Note
Version 0x00010100 per tutti gli strumenti di terze parti. anims/terrain.gaf e anims/vismasks.gaf di Cavedog sono distribuiti con Version == 0. Il gioco non controlla questa parola; kbot legge qualsiasi valore (uno non riconosciuto viene segnalato come avviso) e scrive 0x00010100.
SequenceCount È seguito subito da SequenceCount × uint32 offset assoluti nel file, ciascuno dei quali punta a un SequenceHeader. Il gioco usa solo i 16 bit bassi, letti come valore con segno: da 1 a 32767 sequenze, nessuna per zero o per un valore negativo.
Unknown1 Sempre zero; in scrittura usa zero.

Sequenza (40 byte)

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;

Il gioco ripete in ciclo una sequenza quando il byte basso di LoopFlags è impostato, e la riproduce una volta sola (fermandosi poi sull'ultimo frame) quando vale 0. Tutte le sequenze originali contengono 1. kbot conserva entrambe le parole quando riscrive un file, e kbot gaf build assegna a una sequenza senza sequence.csv il valore originale 1.

Seguono FrameCount × FrameListItem:

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

Il gioco legge solo i 16 bit bassi di Duration e mostra un frame per max(Duration, 1) tick del suo orologio di animazione a 30 Hz.

I nomi delle sequenze contano: il gioco cerca le animazioni per nome (ad es. UnitInfo fa riferimento a corpyro_smoke dallo script corpyro.gaf). Quando li confronti, trattali come ASCII senza distinguere maiuscole e minuscole.


Frame (info di 24 byte + payload)

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 descrivono il punto di ancoraggio usato quando il frame viene copiato (blit) nel mondo. Per lo sprite di un'esplosione l'origine è di solito il centro dello scoppio; per un cursore è il pixel del suo hotspot.

Frame a livelli

Quando LayerCount > 0, PtrFrameData è un array di LayerCount × uint32 puntatori a FrameInfo annidati, non a dati di pixel. Ogni livello viene disegnato nell'ordine dell'array (dal fondo verso il primo piano) con il suo hotspot sull'hotspot del frame esterno. Un livello con il byte +11 impostato viene disegnato attraverso la tabella di traslucenza invece di essere copiato (nessun file originale lo imposta). Il byte +10 e il byte +11 sono campi separati: leggerli come un unico conteggio a 16 bit trasforma un byte +11 impostato in 256 livelli.

Dati dei pixel

TransparencyIndex vale per singolo frame, non per tutto il file: il GAF dei cursori, per esempio, usa l'indice 9 (il rosa di sistema) in alcuni frame e l'indice 0 in altri.

Non compresso (Compressed == 0)

Width × Height byte, dall'alto in basso, da sinistra a destra. Il gioco disegna ogni pixel tranne quelli uguali a TransparencyIndex. Le texture delle unità (textures/*.gaf) e le maschere di visibilità (anims/vismasks.gaf) vengono lette come semplici array di pixel, quindi i loro frame devono essere grezzi; lo sono tutti i frame originali di quei file.

Compresso (Compressed != 0) — Run-Length Encoding di TA

I dati dei pixel sono una sequenza di Height righe indipendenti. Ogni riga inizia con un uint16 lineLength seguito da un flusso di byte codificato RLE, lungo esattamente lineLength byte. All'interno di quel flusso, ogni blocco inizia con un byte di maschera i cui due bit bassi scelgono il tipo di blocco:

Test sulla maschera Tipo di blocco Lunghezza (pixel) Byte che seguono
mask & 0x01 Run trasparente count = mask >> 1 (nessuno: i pixel vengono saltati)
mask & 0x02 Run ripetuto count = (mask >> 2) + 1 1 byte: l'indice di colore da ripetere
nessuno dei due Copia letterale count = (mask >> 2) + 1 count byte: i pixel letterali

Le lunghezze massime per blocco sono quindi 127 pixel trasparenti, 64 pixel ripetuti e 64 pixel letterali. Solo i pixel saltati sono trasparenti: un pixel ripetuto o letterale viene disegnato anche quando il suo valore è uguale a TransparencyIndex, e l'indice 0 della palette è un normale nero opaco. Se la riga codificata finisce prima di Width, kbot la completa con pixel trasparenti e lo segnala; il gioco continuerebbe a leggere nei byte successivi. I livelli possono essere più piccoli del frame esterno e vengono posizionati con il proprio OriginX/Y.

Riferimento: formats/gaf/gaf.go readCompressed e formats/gaf/writer.go compressRow.

Important

Un GAF non contiene la palette. Il file memorizza solo indici a 8 bit. Per visualizzarlo a colori ti serve la palette globale di TA (palettes/palette.pal) oppure, per TA: Kingdoms, una palette per singolo asset che si trova fuori dal GAF. Vedi [Risoluzione della palette in TA: Kingdoms] (#ta-kingdoms--palette-resolution) e Stranezza della trasparenza in TA: Kingdoms per i meccanismi specifici di TAK. kbot gaf export usa per impostazione predefinita la palette di TA incorporata e la regola di trasparenza del gioco; entrambe si possono sostituire.


TA: Kingdoms — risoluzione della palette

Note

Questa sezione riguarda solo le differenze di TA:K. Il renderer di TA usa sempre palettes/palette.pal; la struttura dei byte di un GAF è identica tra TA e TAK, ma TAK ha introdotto una pipeline di palette per singolo asset.

TA: Kingdoms distribuisce lo stesso contenitore .gaf (a volte .taf) di TA, ma prende la palette da un file di accompagnamento proprio di ciascun asset. L'installazione originale di TAK segue tre convenzioni documentate, in ordine di priorità:

  1. Un .pcx con lo stesso nome accanto al .gaf. Gli asset dell'interfaccia e delle animazioni sono accompagnati da un file PCX 1×1 con la palette desiderata incorporata come chunk PLTE del PCX. anims/actionbuttons.gaf viene visualizzato con anims/actionbuttons.pcx. Circa il 38% dei file anims/*.gaf di TAK ha un file di accompagnamento del genere.

  2. Un .pal con lo stesso nome di base in palettes/. Raro; riguarda una manciata di asset originali per cui Cavedog ha esportato una palette grezza di 1024 byte.

  3. Palette della fazione in base al prefisso del nome del file. I GAF di unità ed elementi hanno un prefisso di fazione di 3 lettere e usano la palette della fazione corrispondente:

    Prefisso Fazione File della 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

Se nessuna di queste regole trova una palette, il risolutore ripiega su palettes/palette.pal e infine sulla palette di TA incorporata; entrambe sono identiche byte per byte nel TAK originale e uguali a quella di TA, quindi per i GAF di unità ed elementi che non corrispondono a un prefisso di fazione i colori saranno sbagliati senza una sostituzione esplicita.

Il risolutore di kbot

internal/palettepick implementa la catena qui sopra. Ogni punto d'ingresso che visualizza un GAF la utilizza:

  • Esploratore web. Le route /png/… e /apng/… rilevano automaticamente la palette per impostazione predefinita; il visualizzatore GAF offre un menu a tendina con tutte le palette candidate trovate dal risolutore, più una voce auto. Passa ?palette=<vfs-path> per imporne una specifica (il menu a tendina lo fa per te).
  • kbot gaf export / kbot gaf dump. Passa --palette <path.pcx|path.pal> per sostituirla; altrimenti viene usata la palette di TA incorporata (la CLI non esegue il risolutore sul VFS perché non ha un contesto VFS, a meno che tu non la avvolga con kbot mount).
  • Strumento MCP gaf_export. Accetta un argomento facoltativo palette (un percorso all'interno della cartella game-data configurata); se lo ometti, esegue il risolutore completo.

Portare un GAF di TA in TA: Kingdoms

Le strutture dei byte dei GAF di TA e di TAK sono identiche: cambia solo la provenienza della palette. Per portare un .gaf di TA esistente:

  1. Crea un PCX 1×1 con palette in cui sia incorporata quella di TA.
  2. Salvalo accanto al GAF con lo stesso nome di base (ad es. MyGaf.gaf + MyGaf.pcx).

Questo basta perché TAK lo visualizzi con i colori originali di TA.


TA: Kingdoms — stranezza della trasparenza

Note

Questa sezione riguarda solo le differenze di TA:K. Il byte TransparencyIndex nella sezione Frame fa fede per il renderer di TA, ma gli autori dei contenuti di TAK non lo hanno mantenuto allineato ai dati dei pixel effettivi nei frame non compressi.

Molti GAF di TAK usati come atlanti di texture (tutto ciò che sta sotto textures/*.gaf e una parte di anims/*.gaf) contengono un frame non compresso in cui il metadato TransparencyIndex riporta una costante (di solito 9) che non compare mai nei dati dei pixel, mentre l'artista ha riempito il bordo "trasparente" con un altro indice della palette (spesso 5). Una lettura letterale del metadato produce uno sfondo colorato opaco dove il renderer di TA avrebbe prodotto trasparenza.

Il renderer di TA non tira mai a indovinare: usa la chiave memorizzata per i frame grezzi e i comandi di salto per quelli compressi. Per gli atlanti di TAK, kbot offre un'euristica di rilevamento degli angoli da attivare esplicitamente (TransparencyModeHeuristic, vedi EffectiveTransparencyIndex in formats/gaf di kbot-io), che si applica solo ai frame grezzi:

  1. Se TransparencyIndex compare in un punto qualsiasi del buffer dei pixel, fidati del metadato.
  2. Altrimenti campiona i quattro angoli. Se tutti e quattro concordano su un valore, usa quel valore come indice trasparente effettivo. Così si recuperano i frame non compressi di TAK in cui l'artista ha disegnato un bordo uniforme.
  3. Altrimenti ripiega sul TransparencyIndex memorizzato su disco (il che preserva la visualizzazione delle texture che arrivano fino ai bordi, come dungwormB1, i cui angoli sono pixel dell'unità: il rilevamento automatico, giustamente, rinuncia a sostituirlo).

I frame compressi e quelli composti usano sempre i loro pixel saltati. Il byte su disco non viene mai sovrascritto: chi riscrive il file in round-trip ritrova il valore originale. Un frame di TA completamente opaco con un bordo uniforme perderebbe quel bordo con l'euristica, quindi il rendering per TA non la usa mai.

Sostituire la trasparenza al momento del rendering

gaf.RenderOptions espone queste modalità:

Modalità Comportamento Caso d'uso
TransparencyModeAuto / TransparencyModeMetadata La regola del gioco: i pixel chiave di un frame grezzo e i pixel saltati di un frame compresso sono trasparenti; l'indice 0 è nero opaco (il valore zero, e l'impostazione predefinita in tutto kbot per TA). Visualizzazione, dump, round-trip.
TransparencyModeHeuristic Ipotesi basata sugli angoli per i frame grezzi (vedi sopra), altrimenti la regola del gioco. È quella che lo studio usa per gli sprite di TA: Kingdoms. Atlanti di texture di TAK.
TransparencyModeNone Ogni pixel è opaco. Esaminare il colore chiave.
TransparencyModeIndex Usa un indice fornito da chi chiama. Sostituzione dall'interfaccia; debug.

Ogni voce della palette esportata è opaca tranne uno slot trasparente per esportazione, normalmente la chiave del frame. Quando un frame disegna pixel il cui valore è uguale alla sua chiave (o i frame di una stessa animazione usano come colori le chiavi l'uno dell'altro), lo slot diventa invece un indice che nessun pixel disegnato usa, così l'immagine conserva ogni indice della palette.

Il visualizzatore GAF dell'esploratore di asset offre Default / Game rule / Corner guess / None in un menu a tendina accanto al selettore della palette, e passa la scelta come ?transparency=game|heuristic|none|<N> in ogni richiesta PNG/GIF/APNG. Il valore predefinito è la regola del gioco per TA e l'ipotesi basata sugli angoli per le installazioni di TA: Kingdoms. La chiave della cache include l'etichetta della trasparenza, quindi cambiando modalità non vengono serviti rendering obsoleti.

Lo strumento MCP gaf_export segue la stessa impostazione predefinita: la regola del gioco, salvo per una cartella game-data registrata come TA: Kingdoms (un contesto kbot takingdoms), che usa l'ipotesi basata sugli angoli; il suo risultato indica la modalità usata. kbot gaf export legge un semplice file senza contesto di gioco, quindi usa per impostazione predefinita la regola del gioco; passa --transparency heuristic per gli atlanti di TA: Kingdoms. kbot gaf dump usa sempre la regola del gioco, perché kbot gaf build rilegge le chiavi memorizzate dalle immagini.


Esempio svolto — 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
...

Un tipico frame di 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)

Costruire un GAF

kbot gaf dump scrive una struttura di directory che kbot gaf build sa rileggere:

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

Le immagini dei frame sono PNG indicizzati (o GIF con --format gif) disegnati con la regola del gioco, così ogni pixel conserva il suo indice della palette e solo i pixel trasparenti sono marcati. La fase di build ricopia quegli indici e trasforma i pixel trasparenti nella chiave del frame; le immagini RGBA vengono invece associate al colore più vicino della palette, evitando la chiave.

  • sequence.csv ripristina l'ordine delle sequenze, il nome esatto, la sua parola di loop e la sua parola +4. Senza questo file, il nome della cartella diventa il nome della sequenza e la sequenza va in loop, come ogni sequenza originale.
  • storage (raw o compressed) e blend (il byte +11) ripristinano l'intestazione di ciascun frame. I frame senza valore di storage vengono compressi, tranne quelli scritti in textures/*.gaf o anims/vismasks.gaf, che sono sempre grezzi perché il gioco legge quei file come semplici array di pixel. --storage raw|compressed sostituisce entrambi.
  • I frame composti (a livelli) vengono esportati come un'unica immagine appiattita e ricostruiti come frame semplici; kbot gaf roundtrip riporta quanti sono.
  • Un frame senza pixel (larghezza o altezza 0) non ha un'immagine; basta la sua riga in frames.csv per ricostruirlo.
  • Una sottocartella senza frames.csv né sequence.csv viene saltata con un avviso. Qualsiasi altra sottocartella che non si può costruire (un'immagine mancante o illeggibile, un valore CSV non valido) interrompe la build, così una sequenza non viene mai esclusa in silenzio.

Note

I GAF di TA e TA:K sono identici byte per byte. Il contenitore è identico; cambia solo la pipeline della palette. Vedi Risoluzione della palette in TA: Kingdoms più sopra per la catena completa e la procedura passo passo per portare un GAF in TAK.


Insidie

Warning

Una sequenza il cui byte di loop vale 0 viene riprodotta una volta e si ferma. Gli strumenti che scrivono la parola +2 a zero congelano ogni elemento animato che toccano. kbot conserva la parola, e le nuove sequenze costruite da kbot vanno in loop.

  • Alcune sequenze sono distribuite con durate dei frame pari a 0. Il gioco mostra un frame del genere per un tick, come con una durata di 1. Non "correggerle" con un valore predefinito.
  • Le righe di pixel compresse possono essere più corte di Width. Il gioco continua a leggere i byte successivi come comandi; kbot completa la riga con pixel trasparenti e lo segnala. Nessun file originale ha righe del genere.
  • Le origini dei livelli sono hotspot: ogni livello viene posizionato in modo che la sua origine cada sull'origine del frame esterno.
  • Il .taf di TA: Kingdoms esiste in due varianti. kbot legge un .taf con palette come un normale GAF, ma TA:K distribuisce anche file .taf truecolor (pixel ARGB a 16 bit, senza palette). Controlla il byte del formato del frame per distinguerli e vedi TAF / TSF per la variante truecolor.
  • anims/terrain.gaf e anims/vismasks.gaf hanno Version == 0. Il gioco ignora la parola di versione.
  • Il numero di frame per sequenza non è limitato: cursorteleport ha 46 frame. Non presupporre un tetto fisso quando dimensioni i buffer.

Dimensioni tipiche

Elemento Intervallo osservato nei GAF di Cavedog
anims/cursors.gaf ~30 KB, 22 sequenze, da 1 a 46 frame ciascuna
Parte di un'unità (ad es. lampo di sparo) 5–20 KB, 1 sequenza, 4–12 frame
Esplosione (famiglia di anims/explosions.gaf) 50–250 KB per file
anims/textures.gaf (il foglio di texture principale) ~12 MB (il GAF più grande di TA)
anims/terrain.gaf ~1,5 MB
Dati per frame Di solito <1 KB; i frame grandi (esplosioni 256×256) arrivano a oltre 10 KB

Esempi reali nel catalogo di riferimento

GAF degni di nota nell'installazione di TA: estraine uno qualsiasi con kbot hpi extract e poi usa kbot gaf list / kbot gaf export:

  • anims/cursors.gaf: 22 sequenze, ciascuna un'animazione di un cursore dell'interfaccia. Le sequenze più piccole (cursorgrn, cursorred) sono frame singoli; la più grande (cursorteleport) ha 46 frame in 460 tick. Mostrato nell'esempio svolto qui sopra.
  • anims/textures.gaf: ~12 MB, il foglio di tile principale a cui fanno riferimento le stringhe OffsetToTextureName di ogni modello 3DO. Sfoglialo con kbot gaf list anims/textures.gaf | head.
  • Famiglia di anims/explosions.gaf: tipici GAF di effetti (lasersht.gaf, acid.gaf, bigexpls.gaf), ciascuno con più sequenze di esplosioni con nome.
  • anims/armcom.gaf / corcom.gaf: parti grafiche del comandante di ciascuna fazione (cerchio di selezione, laser di costruzione).
  • anims/terrain.gaf + anims/vismasks.gaf: gli unici GAF originali con Version == 0 nell'intestazione (utile saperlo quando scrivi un parser).

Vedi anche

  • TAF / TSF: la variante .taf truecolor di TA: Kingdoms, costruita su questo stesso contenitore, più la sua forma testuale modificabile.
  • PAL: la tabella dei colori necessaria per visualizzare i frame GAF.
  • PCX: come TA:K fornisce una palette a un .taf con palette.
  • 3DO: quando uno "sprite" è in realtà un modello 3D con texture (la maggior parte delle esplosioni è GAF; la maggior parte dei proiettili è 3DO + lampo di sparo GAF).
  • Glossario: sotto-frame (sub-frame), origine (origin), tick.