Todas as páginas de formatos ↓
Gráficos e multimédia

GAF — Graphics Animation Format

Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub

O GAF guarda animações de sprites em cor indexada. Quase todos os elementos 2D animados do Total Annihilation são GAF: cursores, ponteiros do rato, explosões, efeitos das unidades, o balançar das árvores, os ícones das armas, as bolhas de lava, tudo. O TA: Kingdoms reutiliza o mesmo contentor com a extensão .taf para as suas animações com paleta (a paleta vem de um ficheiro auxiliar). O TA:K inclui também uma variante truecolor do .taf, que guarda píxeis ARGB de 16 bits em vez de índices; essa tem página própria, TAF / TSF.

sequência cursormove de cursors.gaf    um único fotograma de cursorselect

Tip

Experimente por si.

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

Todas as opções estão na referência de kbot gaf da linha de comandos.

A partir de Go. Use 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))
}

Visão geral

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

Um GAF é uma lista simples de sequências; cada sequência é uma lista de fotogramas; cada fotograma é um mapa de bits de largura × altura com índices da paleta, um ponto de origem e, opcionalmente, séries de transparência ao estilo RLE. Os fotogramas podem ter "subfotogramas", para que um passo lógico da animação seja a composição de várias camadas sobrepostas.


Cabeçalho (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 em todas as ferramentas de terceiros. Os ficheiros anims/terrain.gaf e anims/vismasks.gaf da Cavedog vêm com Version == 0. O jogo não verifica esta palavra; o kbot lê qualquer valor (um valor desconhecido é assinalado com um aviso) e escreve 0x00010100.
SequenceCount Seguido imediatamente de SequenceCount × uint32 deslocamentos absolutos no ficheiro, cada um a apontar para um SequenceHeader. O jogo usa apenas os 16 bits inferiores, lidos como valor com sinal: 1–32767 sequências, e nenhuma para zero ou um valor negativo.
Unknown1 Sempre zero; escreva zero ao gravar.

Sequência (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;

O jogo repete a sequência em ciclo quando o byte inferior de LoopFlags está definido, e reprodu-la uma só vez (e para no último fotograma) quando é 0. Todas as sequências originais guardam 1. O kbot conserva as duas palavras quando reescreve um ficheiro, e kbot gaf build dá o valor original 1 a uma sequência sem sequence.csv.

Seguem-se FrameCount × FrameListItem:

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

O jogo lê apenas os 16 bits inferiores de Duration e mostra cada fotograma durante max(Duration, 1) ticks do seu relógio de animação de 30 Hz.

Os nomes das sequências contam: o jogo procura as animações pelo nome (por exemplo, UnitInfo refere corpyro_smoke do script corpyro.gaf). Ao compará-los, trate-os como ASCII, sem distinguir maiúsculas de minúsculas.


Fotograma (24 bytes de informação + 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 indicam o ponto de ancoragem usado ao copiar (blit) o fotograma para o mundo. Num sprite de explosão, a origem é normalmente o centro da explosão; num cursor, é o píxel do ponto ativo (hotspot).

Fotogramas em camadas

Quando LayerCount > 0, PtrFrameData é um vetor de LayerCount × uint32 ponteiros para FrameInfo aninhados, e não dados de píxeis. Cada camada é desenhada pela ordem do vetor (de trás para a frente), com o seu ponto ativo sobre o ponto ativo do fotograma exterior. Uma camada com o byte +11 definido é desenhada através da tabela de translucidez em vez de ser copiada (nenhum ficheiro original o define). O byte +10 e o byte +11 são campos distintos: lê-los como uma única contagem de 16 bits transforma um byte +11 definido em 256 camadas.

Dados de píxeis

TransparencyIndex é de cada fotograma, e não do ficheiro inteiro: o GAF dos cursores, por exemplo, usa o índice 9 (o rosa do sistema) nalguns fotogramas e o índice 0 noutros.

Não comprimido (Compressed == 0)

Width × Height bytes, de cima para baixo e da esquerda para a direita. O jogo desenha todos os píxeis exceto os iguais a TransparencyIndex. As texturas das unidades (textures/*.gaf) e as máscaras de visão (anims/vismasks.gaf) são lidas como simples vetores de píxeis, por isso os seus fotogramas têm de estar em bruto; todos os fotogramas originais desses ficheiros estão.

Comprimido (Compressed != 0) — codificação RLE do TA

Os dados de píxeis são uma sucessão de Height linhas independentes. Cada linha começa com um uint16 lineLength, seguido de um fluxo de bytes codificado por RLE com exatamente lineLength bytes. Dentro desse fluxo, cada bloco começa com uma máscara de 1 byte cujos dois bits inferiores indicam o tipo de bloco:

Teste da máscara Tipo de bloco Comprimento (píxeis) Bytes seguintes
mask & 0x01 Série transparente count = mask >> 1 (nenhum: os píxeis são saltados)
mask & 0x02 Série repetida count = (mask >> 2) + 1 1 byte: o índice de cor a repetir
nenhum dos dois Cópia literal count = (mask >> 2) + 1 count bytes: os píxeis literais

Assim, cada bloco tem no máximo 127 píxeis transparentes, 64 píxeis repetidos ou 64 píxeis literais. Só os píxeis saltados são transparentes: um píxel repetido ou literal é desenhado mesmo quando o seu valor é igual a TransparencyIndex, e o índice 0 da paleta é um preto opaco como outro qualquer. Se a linha codificada terminar antes de Width, o kbot preenche-a com píxeis transparentes e assinala-o; o jogo continuaria a ler os bytes seguintes. As camadas podem ser mais pequenas do que o fotograma exterior e são posicionadas pelos seus próprios OriginX/Y.

Referência: formats/gaf/gaf.go readCompressed e formats/gaf/writer.go compressRow.

Important

O GAF não tem paleta. O ficheiro só guarda índices de 8 bits. Para renderizar a cores, precisa da paleta global do TA (palettes/palette.pal) ou, no TA: Kingdoms, de uma paleta própria do recurso, que fica fora do GAF. Veja [Resolução da paleta no TA: Kingdoms] (#ta-kingdoms--palette-resolution) e Peculiaridade da transparência no TA: Kingdoms para o mecanismo próprio do TAK. kbot gaf export usa por omissão a paleta do TA incorporada e a regra de transparência do jogo; ambas podem ser substituídas.


TA: Kingdoms — resolução da paleta

Note

Esta secção trata só do que muda no TA:K. O renderizador do TA usa sempre palettes/palette.pal; a disposição dos bytes do GAF é idêntica no TA e no TAK, mas o TAK passou a usar uma paleta própria de cada recurso.

O TA: Kingdoms usa o mesmo contentor .gaf (por vezes .taf) que o TA, mas vai buscar a paleta a um ficheiro auxiliar de cada recurso. A instalação comercial do TAK segue três convenções documentadas, por ordem de prioridade:

  1. .pcx com o mesmo nome, ao lado do .gaf. Os recursos da interface e das animações trazem um ficheiro PCX de 1×1 com a paleta pretendida incorporada como bloco PLTE do PCX. anims/actionbuttons.gaf é renderizado com anims/actionbuttons.pcx. Cerca de 38% dos ficheiros anims/*.gaf do TAK têm um ficheiro auxiliar destes.

  2. .pal com o mesmo nome base em palettes/. Raro; abrange uns poucos recursos originais para os quais a Cavedog exportou uma paleta em bruto de 1024 bytes.

  3. Paleta do lado, pelo prefixo do nome do ficheiro. Os GAF de unidades e de elementos têm no nome um prefixo de 3 letras que indica o lado e usam a paleta desse lado:

    Prefixo Lado Ficheiro 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

Se nenhuma destas resultar, o resolvedor recorre a palettes/palette.pal e, por fim, à paleta do TA incorporada. Na versão comercial do TAK, ambas são idênticas byte a byte entre si e à do TA, pelo que, nos GAF de unidades ou de elementos que não correspondem a nenhum prefixo de lado, as cores ficam erradas sem uma substituição explícita.

O resolvedor do kbot

internal/palettepick implementa a cadeia acima. Todos os pontos de entrada que renderizam um GAF passam por ele:

  • Explorador web. As rotas /png/… e /apng/… fazem a deteção automática por omissão; o visualizador de GAF mostra uma lista pendente de paletas com todas as candidatas que o resolvedor encontra, mais uma entrada auto. Passe ?palette=<vfs-path> para forçar uma delas (a lista pendente faz isso por si).
  • kbot gaf export / kbot gaf dump. Passe --palette <path.pcx|path.pal> para a substituir; caso contrário, é usada a paleta do TA incorporada (a linha de comandos não executa o resolvedor do VFS, porque não tem contexto de VFS, a menos que a envolva com kbot mount).
  • Ferramenta MCP gaf_export. Aceita um argumento palette opcional (um caminho dentro da pasta game-data configurada); sem ele, executa o resolvedor completo.

Portar um GAF do TA para o TA: Kingdoms

A disposição dos bytes do GAF é idêntica no TA e no TAK; só muda a origem da paleta. Para portar um .gaf do TA:

  1. Crie um PCX com paleta de 1×1, com a paleta do TA incorporada.
  2. Guarde-o ao lado do GAF com o mesmo nome base (por exemplo, MyGaf.gaf + MyGaf.pcx).

Isto basta para o TAK o renderizar com as cores originais do TA.


TA: Kingdoms — peculiaridade da transparência

Note

Esta secção trata só do que muda no TA:K. O byte TransparencyIndex da secção Fotograma é determinante para o renderizador do TA, mas os autores dos conteúdos do TAK não o mantiveram coerente com os dados de píxeis reais dos fotogramas não comprimidos.

Muitos GAF de atlas de texturas do TAK (tudo o que está em textures/*.gaf e uma boa parte de anims/*.gaf) têm um fotograma não comprimido cujo metadado TransparencyIndex indica uma constante (normalmente 9) que nunca aparece nos dados de píxeis reais; entretanto, o artista preencheu a margem "transparente" com outro índice da paleta (muitas vezes 5). Uma leitura literal dos metadados produz um fundo colorido opaco onde o renderizador do TA teria produzido transparência.

O renderizador do TA nunca adivinha: usa a chave guardada nos fotogramas em bruto e os comandos de salto nos comprimidos. Para os atlas do TAK, o kbot oferece uma heurística de deteção pelos cantos, opcional (TransparencyModeHeuristic, veja EffectiveTransparencyIndex em formats/gaf do kbot-io), que só se aplica a fotogramas em bruto:

  1. Se TransparencyIndex aparecer em qualquer ponto do buffer de píxeis, confia nos metadados.
  2. Caso contrário, lê os quatro cantos. Se os quatro tiverem o mesmo valor, usa esse valor como índice transparente efetivo. Isto salva os fotogramas não comprimidos do TAK em que o artista desenhou uma margem uniforme.
  3. Caso contrário, recorre ao TransparencyIndex guardado no disco (o que preserva a renderização de texturas que ocupam toda a área, como dungwormB1, cujos cantos são píxeis da unidade; aí a deteção automática, e bem, não substitui nada).

Os fotogramas comprimidos e compostos usam sempre os seus píxeis saltados. O byte no disco nunca é reescrito: quem volta a escrever o ficheiro vê o valor original. Um fotograma do TA totalmente opaco com uma margem uniforme perderia essa margem com a heurística, por isso as renderizações do TA nunca a usam.

Substituir a transparência ao renderizar

gaf.RenderOptions disponibiliza estes modos:

Modo Comportamento Utilização
TransparencyModeAuto / TransparencyModeMetadata A regra do jogo: os píxeis da chave de um fotograma em bruto e os píxeis saltados de um fotograma comprimido são transparentes; o índice 0 é preto opaco (o valor zero e a predefinição em todo o kbot para o TA). Visualização, dumps, idas e voltas.
TransparencyModeHeuristic Palpite pelos cantos nos fotogramas em bruto (ver acima), a regra do jogo nos restantes. É o que o estúdio usa nos sprites do TA: Kingdoms. Atlas de texturas do TAK.
TransparencyModeNone Todos os píxeis são opacos. Inspecionar a cor-chave.
TransparencyModeIndex Usa um índice indicado por quem chama. Substituição na interface; depuração.

Todas as entradas da paleta exportada são opacas, exceto uma posição transparente por exportação, normalmente a chave do fotograma. Quando um fotograma desenha píxeis cujo valor é igual à sua chave (ou os fotogramas de uma animação usam as chaves uns dos outros como cores), a posição transparente passa a ser um índice que nenhum píxel desenhado usa, para que a imagem conserve todos os índices da paleta.

O visualizador de GAF do explorador de recursos oferece Default / Game rule / Corner guess / None numa lista pendente ao lado do seletor de paleta, e passa a escolha como ?transparency=game|heuristic|none|<N> em cada pedido PNG/GIF/APNG. A predefinição é a regra do jogo nas instalações do TA e o palpite pelos cantos nas do TA: Kingdoms. A chave da cache inclui a etiqueta da transparência, por isso mudar de modo não devolve renderizações desatualizadas.

A ferramenta MCP gaf_export segue a mesma predefinição: a regra do jogo, exceto numa pasta game-data registada como TA: Kingdoms (um contexto takingdoms do kbot), que recebe o palpite pelos cantos; o resultado indica o modo usado. kbot gaf export lê um ficheiro simples, sem contexto de jogo, por isso usa por omissão a regra do jogo; passe --transparency heuristic nos atlas do TA: Kingdoms. kbot gaf dump usa sempre a regra do jogo, porque kbot gaf build volta a ler as chaves guardadas a partir das imagens.


Exemplo comentado — 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
...

Um 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)

Construir um GAF

kbot gaf dump escreve uma estrutura de pastas que kbot gaf build volta a ler:

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

As imagens dos fotogramas são PNG indexados (ou GIF, com --format gif) desenhados com a regra do jogo, por isso cada píxel mantém o seu índice da paleta e só os píxeis transparentes são marcados. A construção copia de volta esses índices e transforma os píxeis transparentes na chave do fotograma; as imagens RGBA, por sua vez, são aproximadas à cor mais próxima da paleta que não seja a chave.

  • sequence.csv repõe a ordem da sequência, o seu nome exato, a palavra de ciclo e a palavra +4. Sem ele, o nome da pasta passa a ser o nome da sequência, e a sequência repete em ciclo, como todas as sequências originais.
  • storage (raw ou compressed) e blend (o byte +11) repõem o cabeçalho de cada fotograma. Os fotogramas sem storage ficam comprimidos, exceto os escritos em textures/*.gaf ou anims/vismasks.gaf, que ficam todos em bruto, porque o jogo os lê como simples vetores de píxeis. --storage raw|compressed sobrepõe-se a ambos.
  • Os fotogramas compostos (em camadas) são extraídos como uma única imagem achatada e reconstruídos como fotogramas simples; kbot gaf roundtrip indica quantos.
  • Um fotograma sem píxeis (largura ou altura 0) não tem imagem; basta a sua linha em frames.csv para o reconstruir.
  • Uma subpasta sem frames.csv nem sequence.csv é ignorada, com um aviso. Qualquer outra subpasta que não possa ser construída (uma imagem em falta ou ilegível, um valor inválido no CSV) interrompe a construção, para que nunca fique uma sequência de fora sem aviso.

Note

Os GAF do TA e do TA:K são idênticos byte a byte. O contentor é idêntico; só muda a origem da paleta. Veja Resolução da paleta no TA: Kingdoms acima, com a cadeia completa e a receita comentada para portar um GAF para o TAK.


Armadilhas

Warning

Uma sequência com o byte de ciclo a 0 é reproduzida uma vez e para. As ferramentas que escrevem a palavra +2 a zero congelam todos os elementos animados em que tocam. O kbot conserva a palavra, e as sequências novas construídas pelo kbot repetem em ciclo.

  • Algumas sequências vêm com fotogramas de duração 0. O jogo mostra esses fotogramas durante um tick, como se a duração fosse 1. Não os "corrija" para um valor predefinido.
  • As linhas de píxeis comprimidas podem ser mais curtas do que Width. O jogo continua a ler os bytes seguintes como comandos; o kbot preenche a linha com píxeis transparentes e assinala-o. Nenhum ficheiro original tem linhas assim.
  • As origens das camadas são pontos ativos: cada camada é colocada de modo que a sua origem coincida com a origem do fotograma exterior.
  • O .taf do TA: Kingdoms tem duas variantes. O kbot lê um .taf com paleta como um GAF normal, mas o TA:K também inclui ficheiros .taf truecolor (píxeis ARGB de 16 bits, sem paleta). Verifique o byte do formato do fotograma para os distinguir e consulte TAF / TSF para a variante truecolor.
  • anims/terrain.gaf e anims/vismasks.gaf têm Version == 0. O jogo ignora a palavra da versão.
  • O número de fotogramas por sequência não tem limite: cursorteleport tem 46 fotogramas. Não assuma um máximo fixo ao dimensionar buffers.

Tamanhos típicos

Recurso Intervalo observado nos GAF da Cavedog
anims/cursors.gaf ~30 KB, 22 sequências, 1–46 fotogramas cada
Efeito de unidade (por exemplo, o clarão do disparo) 5–20 KB, 1 sequência, 4–12 fotogramas
Explosão (família anims/explosions.gaf) 50–250 KB por ficheiro
anims/textures.gaf (a folha de texturas principal) ~12 MB (o maior GAF do TA)
anims/terrain.gaf ~1.5 MB
Dados por fotograma Normalmente <1 KB; os fotogramas grandes (explosões de 256×256) chegam a 10+ KB

Exemplos reais no catálogo de referência

GAF notáveis da instalação do TA; extraia qualquer um com kbot hpi extract e depois use kbot gaf list / kbot gaf export:

  • anims/cursors.gaf — 22 sequências, cada uma com uma animação de cursor da interface. As sequências mais pequenas (cursorgrn, cursorred) têm um único fotograma; a maior (cursorteleport) tem 46 fotogramas ao longo de 460 ticks. Aparece na secção do exemplo comentado, acima.
  • anims/textures.gaf — ~12 MB, a folha de mosaicos principal referida pelas cadeias de caracteres OffsetToTextureName de todos os modelos 3DO. Explore-a com kbot gaf list anims/textures.gaf | head.
  • Família anims/explosions.gaf — GAF de efeitos típicos: lasersht.gaf, acid.gaf, bigexpls.gaf; cada um contém várias sequências de explosão com nome.
  • anims/armcom.gaf / corcom.gaf — os efeitos do Comandante de cada lado (círculo de seleção, laser de construção).
  • anims/terrain.gaf + anims/vismasks.gaf — os únicos GAF originais com Version == 0 no cabeçalho (convém sabê-lo quando se escreve um analisador).

Ver também

  • TAF / TSF — a variante truecolor .taf do TA: Kingdoms, construída sobre este mesmo contentor, e a sua forma de texto editável.
  • PAL — a tabela de cores necessária para renderizar os fotogramas GAF.
  • PCX — como o TA:K fornece uma paleta a um .taf com paleta.
  • 3DO — quando um "sprite" é na verdade um modelo 3D texturizado (a maioria das explosões é GAF; a maioria dos projéteis é 3DO + clarão do disparo em GAF).
  • Glossário — subfotograma, origem, tick.