Todas las páginas de formatos ↓
Gráficos y multimedia

GAF: Graphics Animation Format

Fuente: coreprime/kbot @ 0ab97bf · Editar en GitHub

GAF guarda animaciones de sprites en color indexado. Casi todos los gráficos 2D animados de Total Annihilation son GAF: cursores, punteros del ratón, explosiones, gadgets de las unidades, el balanceo de los árboles, iconos de armas, las burbujas de lava, todo. TA: Kingdoms reutiliza el mismo contenedor con la extensión .taf para sus animaciones con paleta (la paleta sale de un archivo acompañante). TA:K incluye también una variante truecolor de .taf que guarda píxeles ARGB de 16 bits en lugar de índices; esa tiene su propia página, TAF / TSF.

secuencia cursormove de cursors.gaf    un fotograma de cursorselect

Tip

Pruébalo tú mismo.

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 la referencia de kbot gaf de la CLI para ver todas las opciones.

Desde 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))
}

De un vistazo

┌─ 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 es un array plano de secuencias; cada secuencia es un array de fotogramas; cada fotograma es un mapa de bits de ancho × alto hecho de índices de la paleta, con un punto de origen y (opcionalmente) tramos de transparencia al estilo RLE. Los fotogramas pueden llevar «subfotogramas», de modo que un paso lógico de la animación sea la composición de varias capas superpuestas.


Cabecera (12 bytes)

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 Notas
Version 0x00010100 en todas las herramientas de terceros. Los anims/terrain.gaf y anims/vismasks.gaf de Cavedog vienen con Version == 0. El juego no comprueba esta palabra; kbot lee cualquier valor (uno desconocido se señala con un aviso) y escribe 0x00010100.
SequenceCount Le siguen inmediatamente SequenceCount × uint32 desplazamientos absolutos en el archivo, cada uno de los cuales apunta a un SequenceHeader. El juego solo usa los 16 bits bajos, leídos como valor con signo: de 1 a 32 767 secuencias, y ninguna si es cero o negativo.
Unknown1 Siempre cero; al escribir, pon cero.

Secuencia (40 bytes)

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;

El juego repite en bucle una secuencia cuando el byte bajo de LoopFlags no es cero, y la reproduce una sola vez (y se detiene en el último fotograma) cuando vale 0. Todas las secuencias originales guardan 1. kbot conserva las dos palabras al reescribir un archivo, y kbot gaf build da a una secuencia sin sequence.csv el valor original 1.

A continuación vienen FrameCount × FrameListItem:

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

El juego solo lee los 16 bits bajos de Duration y muestra un fotograma durante max(Duration, 1) ticks de su reloj de animación de 30 Hz.

Los nombres de las secuencias importan: el juego busca las animaciones por nombre (p. ej., UnitInfo hace referencia a corpyro_smoke del script corpyro.gaf). Trátalos como ASCII sin distinguir mayúsculas al compararlos.


Fotograma (información de 24 bytes + datos)

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 describen el punto de anclaje que se usa al volcar el fotograma en el mundo. En el sprite de una explosión, el origen suele ser el centro de la explosión; en un cursor, es el píxel activo (hotspot).

Fotogramas por capas

Cuando LayerCount > 0, PtrFrameData es un array de LayerCount × uint32 punteros a FrameInfo anidados, no datos de píxeles. Cada capa se dibuja en el orden del array (de atrás hacia delante), con su hotspot sobre el hotspot del fotograma exterior. Una capa con el byte +11 activado se dibuja a través de la tabla de translucidez en lugar de copiarse (ningún archivo original lo activa). El byte +10 y el byte +11 son campos distintos: si se leen como un único recuento de 16 bits, un byte +11 activado se convierte en 256 capas.

Datos de píxeles

TransparencyIndex es propio de cada fotograma, no de todo el archivo: el GAF de los cursores, por ejemplo, usa el índice 9 (el rosa del sistema) en algunos fotogramas y el índice 0 en otros.

Sin comprimir (Compressed == 0)

Width × Height bytes, de arriba abajo y de izquierda a derecha. El juego dibuja todos los píxeles salvo los que valen TransparencyIndex. Las texturas de las unidades (textures/*.gaf) y las máscaras de visión (anims/vismasks.gaf) se leen como simples arrays de píxeles, así que sus fotogramas deben estar sin comprimir; todos los fotogramas originales de esos archivos lo están.

Comprimido (Compressed != 0): codificación RLE de TA

Los datos de píxeles son una secuencia de Height filas independientes. Cada fila empieza con un uint16 lineLength seguido de un flujo de bytes codificado por longitud de tramos (RLE) de exactamente lineLength bytes. Dentro de ese flujo, cada chunk empieza con un byte de máscara cuyos dos bits bajos indican el tipo de chunk:

Prueba de la máscara Tipo de chunk Longitud (píxeles) Bytes que siguen
mask & 0x01 Tramo transparente count = mask >> 1 (ninguno: los píxeles se saltan)
mask & 0x02 Tramo repetido count = (mask >> 2) + 1 1 byte: el índice de color que se repite
ninguna Copia literal count = (mask >> 2) + 1 count bytes: los píxeles literales

Así, las longitudes máximas por chunk son 127 píxeles transparentes, 64 píxeles repetidos y 64 píxeles literales. Solo los píxeles saltados son transparentes: un píxel repetido o literal se dibuja aunque su valor coincida con TransparencyIndex, y el índice 0 de la paleta es un negro opaco normal. Si la fila codificada termina antes de Width, kbot la rellena con píxeles transparentes y lo señala; el juego seguiría leyendo los bytes siguientes. Las capas pueden ser más pequeñas que el fotograma exterior y se colocan según su propio OriginX/Y.

Referencia: readCompressed de formats/gaf/gaf.go y compressRow de formats/gaf/writer.go.

Important

GAF no lleva paleta. El archivo solo guarda índices de 8 bits. Para mostrarlo en color necesitas la paleta global de TA (palettes/palette.pal) o, en TA: Kingdoms, una paleta propia de cada recurso que está fuera del GAF. Consulta [Resolución de la paleta en TA: Kingdoms] (#ta-kingdoms--palette-resolution) y La peculiaridad de la transparencia en TA: Kingdoms para el mecanismo específico de TAK. kbot gaf export usa por defecto la paleta de TA integrada y la regla de transparencia del juego; ambas se pueden cambiar.


Resolución de la paleta en TA: Kingdoms

Note

Esta sección recoge solo las diferencias de TA:K. El renderizador de TA siempre usa palettes/palette.pal; la estructura de bytes de GAF es idéntica en TA y en TAK, pero TAK introdujo un sistema de paletas propias de cada recurso.

TA: Kingdoms distribuye el mismo contenedor .gaf (a veces .taf) que TA, pero toma la paleta de un archivo acompañante propio de cada recurso. La instalación comercial de TAK sigue tres convenciones documentadas, por orden de prioridad:

  1. Un .pcx con el mismo nombre junto al .gaf. Los recursos de interfaz y de animación incluyen un archivo PCX de 1×1 con la paleta deseada incrustada como chunk PLTE del PCX. anims/actionbuttons.gaf se dibuja con anims/actionbuttons.pcx. Alrededor del 38 % de los archivos anims/*.gaf de TAK llevan un archivo acompañante así.

  2. Un .pal con el mismo nombre base en palettes/. Es raro; cubre un puñado de recursos originales para los que Cavedog exportó una paleta en bruto de 1024 bytes.

  3. Una paleta específica del bando según el prefijo del nombre de archivo. Los GAF de unidades y elementos llevan en el nombre un prefijo de 3 letras del bando y usan la paleta de ese bando:

    Prefijo Bando Archivo de paleta
    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 ninguna de esas se resuelve, el resolutor recurre a palettes/palette.pal y, en último término, a la paleta de TA integrada; en el TAK comercial ambas son idénticas byte a byte entre sí y a la de TA, así que, en los GAF de unidades y elementos que no encajan con ningún prefijo de bando, los colores saldrán mal sin una sustitución explícita.

El resolutor de kbot

internal/palettepick implementa la cadena anterior. Todos los puntos de entrada que dibujan un GAF pasan por él:

  • Explorador web. Las rutas /png/… y /apng/… la detectan automáticamente por defecto; el visor de GAF ofrece un desplegable de paletas con todos los candidatos que encuentra el resolutor, más una entrada auto. Pasa ?palette=<vfs-path> para forzar una concreta (el desplegable lo hace por ti).
  • kbot gaf export / kbot gaf dump. Pasa --palette <path.pcx|path.pal> para sustituirla; si no, se usa la paleta de TA integrada (la CLI no ejecuta el resolutor del VFS porque no tiene un contexto de VFS salvo que la envuelvas con kbot mount).
  • Herramienta MCP gaf_export. Acepta un argumento palette opcional (una ruta dentro de la carpeta game-data configurada); si se omite, ejecuta el resolutor completo.

Portar un GAF de TA a TA: Kingdoms

Las estructuras de bytes de los GAF de TA y de TAK son idénticas; solo cambia el origen de la paleta. Para portar un .gaf de TA existente:

  1. Crea un PCX indexado de 1×1 que lleve incrustada la paleta de TA.
  2. Guárdalo junto al GAF con el mismo nombre base (p. ej., MyGaf.gaf + MyGaf.pcx).

Con eso basta para que TAK lo dibuje con los colores originales de TA.


La peculiaridad de la transparencia en TA: Kingdoms

Note

Esta sección recoge solo las diferencias de TA:K. El byte TransparencyIndex de la sección Fotograma es el que manda para el renderizador de TA, pero los autores de contenido de TAK no lo mantuvieron sincronizado con los datos de píxeles reales en los fotogramas sin comprimir.

Muchos GAF de atlas de texturas de TAK (todo lo que hay en textures/*.gaf y una buena parte de anims/*.gaf) llevan un fotograma sin comprimir cuyo TransparencyIndex en los metadatos contiene alguna constante (normalmente 9) que no aparece nunca en los datos de píxeles reales; mientras tanto, el artista rellenó el borde «transparente» con otro índice de la paleta (a menudo 5). Una lectura literal de los metadatos produce un fondo de color opaco donde el renderizador de TA habría producido transparencia.

El renderizador de TA nunca adivina: usa el color clave almacenado en los fotogramas sin comprimir y las órdenes de salto en los comprimidos. Para los atlas de TAK, kbot ofrece una heurística de detección por las esquinas opcional (TransparencyModeHeuristic; consulta EffectiveTransparencyIndex en formats/gaf de kbot-io), que solo se aplica a los fotogramas sin comprimir:

  1. Si TransparencyIndex aparece en algún punto del búfer de píxeles, se confía en los metadatos.
  2. Si no, se muestrean las cuatro esquinas. Si las cuatro coinciden en un valor, se usa ese valor como índice transparente efectivo. Esto salva los fotogramas sin comprimir de TAK en los que el artista dibujó un borde uniforme.
  3. Si no, se recurre al TransparencyIndex del disco (con lo que se dibujan bien las texturas a sangre como dungwormB1, cuyas esquinas son píxeles de la unidad: la detección automática se abstiene correctamente de sustituirlo).

Los fotogramas comprimidos y compuestos siempre usan sus píxeles saltados. El byte del disco nunca se sobrescribe: los escritores de ida y vuelta ven el valor original. Un fotograma de TA totalmente opaco con un borde uniforme perdería ese borde con la heurística, así que los renderizados de TA nunca la usan.

Cómo cambiar la transparencia al renderizar

gaf.RenderOptions ofrece estos modos:

Modo Comportamiento Caso de uso
TransparencyModeAuto / TransparencyModeMetadata La regla del juego: los píxeles del color clave de un fotograma sin comprimir y los píxeles saltados de uno comprimido son transparentes; el índice 0 es negro opaco (es el valor cero, y el predeterminado en todo kbot para TA). Visualización, volcados, viajes de ida y vuelta.
TransparencyModeHeuristic Detección por las esquinas en los fotogramas sin comprimir (ver arriba); la regla del juego en los demás. Es lo que usa el studio para los sprites de TA: Kingdoms. Atlas de texturas de TAK.
TransparencyModeNone Todos los píxeles son opacos. Examinar el color clave.
TransparencyModeIndex Usa un índice proporcionado por quien llama. Sustitución desde la interfaz; depuración.

Todas las entradas de la paleta exportada son opacas salvo una posición transparente por exportación, que normalmente es el color clave del fotograma. Cuando un fotograma dibuja píxeles cuyo valor coincide con su color clave (o los fotogramas de una misma animación usan como colores las claves de los otros), la posición pasa a ser un índice que no usa ningún píxel dibujado, de modo que la imagen conserva todos los índices de la paleta.

El visor de GAF del explorador de recursos ofrece Default / Game rule / Corner guess / None en un desplegable junto al selector de paleta, y pasa la elección como ?transparency=game|heuristic|none|<N> en cada petición PNG/GIF/APNG. El valor por defecto es la regla del juego en las instalaciones de TA y la detección por las esquinas en las de TA: Kingdoms. La clave de la caché incluye la etiqueta de transparencia, así que al cambiar de modo la caché no devuelve renderizados obsoletos.

La herramienta MCP gaf_export sigue el mismo criterio por defecto: la regla del juego, salvo en una carpeta game-data registrada como TA: Kingdoms (un contexto de kbot takingdoms), que recibe la detección por las esquinas; su resultado indica el modo usado. kbot gaf export lee un archivo suelto sin contexto de juego, así que usa por defecto la regla del juego; pasa --transparency heuristic para los atlas de TA: Kingdoms. kbot gaf dump siempre usa la regla del juego, porque kbot gaf build vuelve a leer de las imágenes los colores clave almacenados.


Ejemplo desarrollado: 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 fotograma típico 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)

Crear un GAF

kbot gaf dump escribe una estructura de directorios que kbot gaf build vuelve a leer:

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

Las imágenes de los fotogramas son PNG indexados (o GIF con --format gif) dibujados con la regla del juego, de modo que cada píxel conserva su índice de la paleta y solo se marcan los píxeles transparentes. El paso de construcción vuelve a copiar esos índices y convierte los píxeles transparentes en el color clave del fotograma; las imágenes RGBA, en cambio, se ajustan al color más cercano de la paleta, sin usar el color clave.

  • sequence.csv restaura el orden de la secuencia, su nombre exacto, su palabra de bucle y su palabra +4. Sin él, el nombre de la carpeta es el nombre de la secuencia y la secuencia se repite en bucle, como todas las secuencias originales.
  • storage (raw o compressed) y blend (el byte +11) restauran la cabecera de cada fotograma. Los fotogramas que no indican storage se comprimen, salvo los que se escriben en textures/*.gaf o anims/vismasks.gaf, que van sin comprimir porque el juego los lee como simples arrays de píxeles. --storage raw|compressed se impone a ambos.
  • Los fotogramas compuestos (por capas) se vuelcan como una sola imagen aplanada y se reconstruyen como fotogramas simples; kbot gaf roundtrip indica cuántos hay.
  • Un fotograma sin píxeles (ancho o alto 0) no tiene imagen; basta su fila de frames.csv para reconstruirlo.
  • Una subcarpeta sin frames.csv ni sequence.csv se omite con un aviso. Cualquier otra subcarpeta que no se pueda construir (una imagen que falta o no se puede leer, un valor CSV incorrecto) detiene la construcción, de modo que nunca se pierde una secuencia sin avisar.

Note

Los GAF de TA y de TA:K son idénticos byte a byte. El contenedor es idéntico; solo cambia el sistema de paletas. Consulta Resolución de la paleta en TA: Kingdoms, más arriba, para la cadena completa y la receta para portar un GAF a TAK.


Escollos

Warning

Una secuencia cuyo byte de bucle es 0 se reproduce una vez y se detiene. Las herramientas que ponen a cero la palabra +2 congelan todos los elementos animados que tocan. kbot conserva la palabra, y las secuencias nuevas que crea kbot se repiten en bucle.

  • Algunas secuencias vienen con fotogramas de duración 0. El juego muestra ese fotograma durante un tick, como si la duración fuera 1. No los «corrijas» a un valor por defecto.
  • Las filas de píxeles comprimidas pueden ser más cortas que Width. El juego sigue leyendo los bytes siguientes como órdenes; kbot rellena la fila con píxeles transparentes y lo señala. Ningún archivo original tiene filas así.
  • Los orígenes de las capas son hotspots: cada capa se coloca de modo que su origen caiga sobre el origen del fotograma exterior.
  • El .taf de TA: Kingdoms tiene dos variantes. kbot lee un .taf con paleta como un GAF normal, pero TA:K también incluye archivos .taf truecolor (píxeles ARGB de 16 bits, sin paleta). Comprueba el byte de formato del fotograma para distinguirlos y consulta TAF / TSF para la variante truecolor.
  • anims/terrain.gaf y anims/vismasks.gaf tienen Version == 0. El juego ignora la palabra de versión.
  • El número de fotogramas por secuencia no está acotado: cursorteleport tiene 46 fotogramas. No supongas un tope fijo al dimensionar los búferes.

Tamaños habituales

Recurso Rango observado en los GAF de Cavedog
anims/cursors.gaf ~30 KB, 22 secuencias, de 1 a 46 fotogramas cada una
Gadget de unidad (p. ej., fogonazo del cañón) 5–20 KB, 1 secuencia, 4–12 fotogramas
Explosión (familia anims/explosions.gaf) 50–250 KB por archivo
anims/textures.gaf (la hoja maestra de texturas) ~12 MB (el GAF más grande de TA)
anims/terrain.gaf ~1,5 MB
Datos por fotograma Normalmente <1 KB; los fotogramas grandes (explosiones de 256×256) llegan a más de 10 KB

Ejemplos reales en el catálogo de referencia

GAF destacables de la instalación de TA; extrae cualquiera con kbot hpi extract y después usa kbot gaf list / kbot gaf export:

  • anims/cursors.gaf: 22 secuencias, cada una con una animación de cursor de la interfaz. Las secuencias más pequeñas (cursorgrn, cursorred) son de un solo fotograma; la más grande (cursorteleport) tiene 46 fotogramas en 460 ticks. Aparece en el ejemplo desarrollado de más arriba.
  • anims/textures.gaf: ~12 MB, la hoja maestra de texturas a la que hacen referencia las cadenas OffsetToTextureName de todos los modelos 3DO. Explórala con kbot gaf list anims/textures.gaf | head.
  • Familia anims/explosions.gaf: GAF de efectos típicos (lasersht.gaf, acid.gaf, bigexpls.gaf); cada uno contiene varias secuencias de explosión con nombre.
  • anims/armcom.gaf / corcom.gaf: los gadgets del comandante de cada bando (círculo de selección, láser de construcción).
  • anims/terrain.gaf + anims/vismasks.gaf: los únicos GAF originales con Version == 0 en la cabecera (conviene saberlo al escribir un analizador).

Véase también

  • TAF / TSF: la variante truecolor .taf de TA: Kingdoms, construida sobre este mismo contenedor, y su forma de texto editable.
  • PAL: la tabla de colores necesaria para dibujar los fotogramas GAF.
  • PCX: cómo proporciona TA:K una paleta a un .taf con paleta.
  • 3DO: cuando un «sprite» es en realidad un modelo 3D texturizado (la mayoría de las explosiones son GAF; la mayoría de los proyectiles son 3DO + un fogonazo GAF).
  • Glosario: sub-frame, origin, tick.