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
.tafpara sus animaciones con paleta (la paleta sale de un archivo acompañante). TA:K incluye también una variante truecolor de.tafque guarda píxeles ARGB de 16 bits en lugar de índices; esa tiene su propia página, TAF / TSF.
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:
Un
.pcxcon 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.gafse dibuja conanims/actionbuttons.pcx. Alrededor del 38 % de los archivosanims/*.gafde TAK llevan un archivo acompañante así.Un
.palcon el mismo nombre base enpalettes/. Es raro; cubre un puñado de recursos originales para los que Cavedog exportó una paleta en bruto de 1024 bytes.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.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 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 entradaauto. 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 conkbot mount).- Herramienta MCP
gaf_export. Acepta un argumentopaletteopcional (una ruta dentro de la carpetagame-dataconfigurada); 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:
- Crea un PCX indexado de 1×1 que lleve incrustada la paleta de TA.
- 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:
- Si
TransparencyIndexaparece en algún punto del búfer de píxeles, se confía en los metadatos. - 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.
- Si no, se recurre al
TransparencyIndexdel disco (con lo que se dibujan bien las texturas a sangre comodungwormB1, 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.csvrestaura 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(rawocompressed) yblend(el byte +11) restauran la cabecera de cada fotograma. Los fotogramas que no indican storage se comprimen, salvo los que se escriben entextures/*.gafoanims/vismasks.gaf, que van sin comprimir porque el juego los lee como simples arrays de píxeles.--storage raw|compressedse impone a ambos.- Los fotogramas compuestos (por capas) se vuelcan como una sola imagen
aplanada y se reconstruyen como fotogramas simples;
kbot gaf roundtripindica cuántos hay. - Un fotograma sin píxeles (ancho o alto 0) no tiene imagen; basta su fila
de
frames.csvpara reconstruirlo. - Una subcarpeta sin
frames.csvnisequence.csvse 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
.tafde TA: Kingdoms tiene dos variantes. kbot lee un.tafcon paleta como un GAF normal, pero TA:K también incluye archivos.taftruecolor (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.gafyanims/vismasks.gaftienenVersion == 0. El juego ignora la palabra de versión.- El número de fotogramas por secuencia no está acotado:
cursorteleporttiene 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 cadenasOffsetToTextureNamede todos los modelos 3DO. Explórala conkbot 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 conVersion == 0en la cabecera (conviene saberlo al escribir un analizador).
Véase también
- TAF / TSF: la variante truecolor
.tafde 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
.tafcon 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.