Todas as páginas de formatos ↓
Gráficos e mídia

GAF: Graphics Animation Format

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

O GAF guarda animações de sprites em cores indexadas. Quase todo elemento 2D animado do Total Annihilation é um GAF: cursores, ponteiros do mouse, explosões, gadgets de unidades, o balanço das árvores, ícones de armas, as bolhas de lava, tudo. O TA: Kingdoms reaproveita o mesmo contêiner com a extensão .taf para suas animações com paleta (a paleta vem de um arquivo acompanhante). O TA:K também traz uma variante truecolor de .taf, que guarda pixels ARGB de 16 bits em vez de índices; essa tem página própria, TAF / TSF.

sequência cursormove de cursors.gaf    um quadro de cursorselect

Tip

Experimente você mesmo.

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 do kbot gaf da linha de comando.

Em 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 é um array simples de sequências. Cada sequência é um array de quadros, e cada quadro é um bitmap de índices da paleta, de largura × altura, com um ponto de origem e (opcionalmente) trechos de transparência no estilo RLE. Os quadros podem ter "subquadros", de modo 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 Observações
Version 0x00010100 em todas as ferramentas de terceiros. Os arquivos anims/terrain.gaf e anims/vismasks.gaf da Cavedog vêm com Version == 0. O jogo não verifica esse valor. O kbot aceita qualquer valor (um desconhecido gera um aviso) e grava 0x00010100.
SequenceCount Logo em seguida vêm SequenceCount × uint32 offsets absolutos no arquivo, cada um apontando para um SequenceHeader. O jogo usa só os 16 bits baixos, lidos com sinal: de 1 a 32.767 sequências, e nenhuma para zero ou um valor negativo.
Unknown1 Sempre zero. Grave zero.

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 loop quando o byte baixo de LoopFlags está definido e a reproduz uma única vez (parando no último quadro) quando ele vale 0. Todas as sequências originais guardam 1. O kbot preserva os dois valores ao regravar um arquivo, e kbot gaf build dá o valor original 1 a uma sequência sem sequence.csv.

Em seguida vêm FrameCount × FrameListItem:

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

O jogo lê só os 16 bits baixos de Duration e mostra um quadro por max(Duration, 1) ticks do seu relógio de animação de 30 Hz.

Os nomes das sequências importam: o jogo procura as animações pelo nome (por exemplo, UnitInfo referencia corpyro_smoke do script corpyro.gaf). Compare-os como ASCII sem diferenciar maiúsculas de minúsculas.


Quadro (informações de 24 bytes + dados)

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 definem o ponto de ancoragem usado ao copiar o quadro para o mundo. Num sprite de explosão, a origem costuma ser o centro da explosão; num cursor, é o pixel do hotspot.

Quadros em camadas

Quando LayerCount > 0, PtrFrameData é um array de LayerCount × uint32 ponteiros para FrameInfos aninhados, e não dados de pixels. Cada camada é desenhada na ordem do array (de trás para a frente), com o seu hotspot sobre o hotspot do quadro externo. Uma camada com o byte +11 definido é desenhada pela tabela de translucidez em vez de ser copiada (nenhum arquivo original o define). O byte +10 e o byte +11 são campos separados: lê-los como uma única contagem de 16 bits transforma um byte +11 definido em 256 camadas.

Dados de pixels

TransparencyIndex vale para cada quadro, não para o arquivo todo: o GAF dos cursores, por exemplo, usa o índice 9 (o rosa do sistema) em alguns quadros e o índice 0 em outros.

Sem compressão (Compressed == 0)

Width × Height bytes, de cima para baixo e da esquerda para a direita. O jogo desenha todos os pixels, 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 arrays simples de pixels, por isso os seus quadros precisam estar sem compressão. Todos os quadros originais ali estão.

Com compressão (Compressed != 0): a codificação RLE do TA

Os dados de pixels são uma sequência de Height linhas independentes. Cada linha começa com um uint16 lineLength, seguido de um fluxo de bytes codificado em RLE com exatamente lineLength bytes. Dentro desse fluxo, cada chunk começa com uma máscara de 1 byte cujos dois bits baixos escolhem o tipo de chunk:

Teste da máscara Tipo de chunk Comprimento (pixels) Bytes seguintes
mask & 0x01 Trecho transparente count = mask >> 1 (nenhum: os pixels são pulados)
mask & 0x02 Trecho repetido 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 pixels literais

Assim, os trechos têm no máximo 127 pixels transparentes, 64 pixels repetidos e 64 pixels literais por chunk. Só os pixels pulados são transparentes: um pixel repetido ou literal é desenhado mesmo quando o seu valor é igual a TransparencyIndex, e o índice 0 da paleta é um preto opaco comum. Se a linha codificada terminar antes de Width, o kbot a completa com pixels transparentes e avisa. O jogo continuaria lendo os bytes seguintes. As camadas podem ser menores que o quadro externo 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 arquivo guarda só índices de 8 bits. Para renderizar em cores, é preciso a paleta global do TA (palettes/palette.pal) ou, no TA: Kingdoms, uma paleta própria de cada recurso, que fica fora do GAF. Veja [Resolução da paleta no TA: Kingdoms] (#ta-kingdoms--palette-resolution) e A peculiaridade da transparência no TA: Kingdoms para o mecanismo específico do TAK. Por padrão, kbot gaf export usa a paleta do TA embutida e a regra de transparência do jogo. As duas podem ser trocadas.


Resolução da paleta no TA: Kingdoms

Note

Esta seção trata só do que muda no TA:K. O renderizador do TA sempre usa 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 para cada recurso.

O TA: Kingdoms traz o mesmo contêiner .gaf (às vezes .taf) do TA, mas tira a paleta de um arquivo acompanhante de cada recurso. A instalação da versão comercial do TAK segue três convenções documentadas, em ordem de prioridade:

  1. Um .pcx com o mesmo nome ao lado do .gaf. Os recursos de interface e de animação trazem um arquivo PCX de 1×1 com a paleta desejada embutida no chunk PLTE do PCX. anims/actionbuttons.gaf é renderizado com anims/actionbuttons.pcx. Cerca de 38% dos arquivos anims/*.gaf do TAK têm um acompanhante assim.

  2. Um .pal com o mesmo nome-base em palettes/. Raro. Cobre uns poucos recursos originais para os quais a Cavedog exportou uma paleta bruta de 1.024 bytes.

  3. Uma paleta de lado, pelo prefixo do nome do arquivo. Os GAFs de unidades e de elementos têm um prefixo de lado de 3 letras e usam a paleta do lado correspondente:

    Prefixo Lado Arquivo 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 delas resolver, o resolvedor recorre a palettes/palette.pal e, por fim, à paleta do TA embutida. As duas são idênticas byte a byte no TAK comercial e iguais à do TA, por isso, nos GAFs de unidades e elementos que não batem com um prefixo de lado, as cores saem 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/… detectam a paleta automaticamente por padrão. O visualizador de GAF mostra uma lista suspensa de paletas com todos os candidatos que o resolvedor encontra, mais uma entrada auto. Passe ?palette=<vfs-path> para forçar uma delas (a lista suspensa faz isso por você).
  • kbot gaf export / kbot gaf dump. Passe --palette <path.pcx|path.pal> para substituir. Caso contrário, é usada a paleta do TA embutida (a linha de comando não roda o resolvedor do VFS, porque não tem contexto de VFS, a não ser que você a envolva com kbot mount).
  • Ferramenta MCP gaf_export. Aceita um argumento palette opcional (um caminho dentro da pasta game-data configurada). Sem ele, o resolvedor completo é executado.

Portar um GAF do TA para o TA: Kingdoms

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

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

Isso basta para o TAK renderizá-lo com as cores originais do TA.


A peculiaridade da transparência no TA: Kingdoms

Note

Esta seção trata só do que muda no TA:K. O byte TransparencyIndex da seção Quadro é a referência para o renderizador do TA, mas os autores do conteúdo do TAK não o mantiveram de acordo com os pixels de fato nos quadros sem compressão.

Muitos GAFs de atlas de texturas do TAK (tudo em textures/*.gaf e uma parte de anims/*.gaf) têm um quadro sem compressão cujo TransparencyIndex nos metadados indica uma constante (em geral 9) que nunca aparece nos pixels. Enquanto isso, o artista preencheu a borda "transparente" com outro índice da paleta (muitas vezes 5). Uma leitura literal dos metadados renderiza um fundo colorido opaco onde o renderizador do TA teria produzido transparência.

O renderizador do TA nunca tenta adivinhar: usa a chave guardada nos quadros sem compressão e os comandos de pulo nos comprimidos. Para os atlas do TAK, o kbot oferece uma heurística de detecção pelos cantos opcional (TransparencyModeHeuristic, veja EffectiveTransparencyIndex em formats/gaf do kbot-io), que vale só para quadros sem compressão:

  1. Se TransparencyIndex aparecer em qualquer ponto do buffer de pixels, confia nos metadados.
  2. Senão, examina os quatro cantos. Se os quatro tiverem o mesmo valor, usa esse valor como índice transparente efetivo. Isso salva os quadros sem compressão do TAK em que o artista desenhou uma borda uniforme.
  3. Senão, recorre ao TransparencyIndex gravado no disco (o que preserva a renderização de texturas que ocupam o quadro inteiro, como dungwormB1, cujos cantos são pixels da unidade; a detecção automática corretamente não substitui nada).

Os quadros comprimidos e os compostos sempre usam os seus pixels pulados. O byte no disco nunca é sobrescrito: quem regrava o arquivo em ida e volta vê o valor original. Um quadro do TA totalmente opaco e com borda uniforme perderia essa borda com a heurística, por isso as renderizações do TA nunca a usam.

Substituir a transparência na hora de renderizar

gaf.RenderOptions oferece estes modos:

Modo Comportamento Uso
TransparencyModeAuto / TransparencyModeMetadata A regra do jogo: os pixels-chave de um quadro sem compressão e os pixels pulados de um quadro comprimido são transparentes; o índice 0 é preto opaco (o valor zero, e o padrão em todo o kbot para o TA). Exibição, extrações, idas e voltas.
TransparencyModeHeuristic Palpite pelos cantos nos quadros sem compressão (acima), e a regra do jogo nos demais. É o que o studio usa para os sprites do TA: Kingdoms. Atlas de texturas do TAK.
TransparencyModeNone Todos os pixels são opacos. Inspecionar a cor-chave.
TransparencyModeIndex Usa um índice fornecido por quem chama. Substituição pela interface, depuração.

Toda entrada de paleta exportada é opaca, exceto uma posição transparente por exportação, normalmente a chave do quadro. Quando um quadro desenha pixels cujo valor é igual à sua chave (ou quando os quadros de uma mesma animação usam as chaves uns dos outros como cores), um índice que nenhum pixel desenhado usa passa a ser essa posição, e assim a imagem preserva todos os índices da paleta.

O visualizador de GAF do explorador de recursos oferece Default / Game rule / Corner guess / None numa lista suspensa ao lado do seletor de paleta e repassa a escolha como ?transparency=game|heuristic|none|<N> em toda requisição de PNG/GIF/APNG. O padrão é a regra do jogo nas instalações do TA e o palpite pelos cantos nas do TA: Kingdoms. A chave do cache inclui a marca de transparência, por isso trocar de modo não serve renderizações antigas.

A ferramenta MCP gaf_export segue o mesmo padrão: a regra do jogo, exceto quando a pasta game-data está registrada como TA: Kingdoms (um contexto do kbot takingdoms), que recebe o palpite pelos cantos. O resultado informa o modo usado. kbot gaf export lê um arquivo avulso sem contexto de jogo, por isso o padrão é a regra do jogo. Passe --transparency heuristic para atlas do TA: Kingdoms. kbot gaf dump sempre usa a regra do jogo, já que kbot gaf build lê as chaves guardadas de volta 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 quadro 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)

Montar um GAF

kbot gaf dump grava uma estrutura de pastas que kbot gaf build lê de volta:

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 quadros são PNGs indexados (ou GIFs com --format gif) desenhados com a regra do jogo, de modo que cada pixel mantém o seu índice da paleta e só os pixels transparentes são marcados. A montagem copia esses índices de volta e transforma os pixels transparentes na chave do quadro. Imagens RGBA são associadas à cor mais próxima da paleta, evitando a chave.

  • sequence.csv restaura a ordem da sequência, o seu nome exato, o seu valor de loop e o seu valor em +4. Sem ele, o nome da pasta vira o nome da sequência e a sequência repete em loop, como todas as originais.
  • storage (raw ou compressed) e blend (o byte +11) restauram o cabeçalho de cada quadro. Os quadros sem storage são comprimidos, exceto que todo quadro gravado em textures/*.gaf ou anims/vismasks.gaf fica sem compressão, porque o jogo os lê como arrays simples de pixels. --storage raw|compressed se sobrepõe aos dois.
  • Os quadros compostos (em camadas) são extraídos como uma única imagem achatada e montados de volta como quadros simples. kbot gaf roundtrip informa quantos.
  • Um quadro sem pixels (largura ou altura 0) não tem imagem. Só a sua linha em frames.csv basta para remontá-lo.
  • Uma subpasta sem frames.csv nem sequence.csv é ignorada com um aviso. Qualquer outra subpasta que não possa ser montada (uma imagem ausente ou ilegível, um valor inválido num CSV) interrompe a montagem, para que nenhuma sequência fique de fora sem aviso.

Note

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


Armadilhas

Warning

Uma sequência cujo byte de loop vale 0 toca uma vez e para. Ferramentas que gravam o valor em +2 como zero congelam todos os elementos animados em que tocam. O kbot preserva o valor, e as novas sequências montadas pelo kbot repetem em loop.

  • Algumas sequências vêm com quadros de duração 0. O jogo mostra esse quadro por um tick, como se a duração fosse 1. Não as "corrija" para um valor padrão.
  • Linhas de pixels comprimidas podem ser mais curtas que Width. O jogo continua lendo os bytes seguintes como comandos. O kbot completa a linha com pixels transparentes e avisa. Nenhum arquivo original tem linhas assim.
  • As origens das camadas são hotspots: cada camada é posicionada de modo que a sua origem caia sobre a origem do quadro externo.
  • O .taf do TA: Kingdoms vem em duas variantes. O kbot lê um .taf com paleta como um GAF comum, mas o TA:K também traz arquivos .taf truecolor (pixels ARGB de 16 bits, sem paleta). Confira o byte de formato do quadro para distingui-los, e veja TAF / TSF para a variante truecolor.
  • anims/terrain.gaf e anims/vismasks.gaf têm Version == 0. O jogo ignora o valor da versão.
  • O número de quadros por sequência não tem limite: cursorteleport tem 46 quadros. Não suponha um teto fixo ao dimensionar buffers.

Tamanhos típicos

Recurso Faixa observada nos GAFs da Cavedog
anims/cursors.gaf ~30 KB, 22 sequências, de 1 a 46 quadros cada
Gadget de unidade (por exemplo, clarão do disparo) 5–20 KB, 1 sequência, 4–12 quadros
Explosão (família anims/explosions.gaf) 50–250 KB por arquivo
anims/textures.gaf (a folha mestra de texturas) ~12 MB (o maior GAF do TA)
anims/terrain.gaf ~1,5 MB
Dados por quadro Em geral <1 KB; quadros grandes (explosões de 256×256) passam de 10 KB

Exemplos reais no catálogo de referência

GAFs 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 a animação de um cursor da interface. As menores sequências (cursorgrn, cursorred) têm um único quadro. A maior (cursorteleport) tem 46 quadros em 460 ticks. Aparece no exemplo comentado acima.
  • anims/textures.gaf: ~12 MB, a folha mestra de blocos referenciada pelas strings OffsetToTextureName de todos os modelos 3DO. Navegue com kbot gaf list anims/textures.gaf | head.
  • Família anims/explosions.gaf: GAFs de efeitos típicos, lasersht.gaf, acid.gaf, bigexpls.gaf, cada um com várias sequências de explosão com nome.
  • anims/armcom.gaf / corcom.gaf: gadgets do Comandante de cada lado (círculo de seleção, laser de construção).
  • anims/terrain.gaf + anims/vismasks.gaf: os únicos GAFs originais com Version == 0 no cabeçalho (bom saber ao escrever um analisador).

Veja também

  • TAF / TSF: a variante truecolor .taf do TA: Kingdoms, construída sobre esse mesmo contêiner, e a sua forma em texto editável.
  • PAL: a tabela de cores necessária para renderizar os quadros de um GAF.
  • PCX: como o TA:K fornece a 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: sub-frame (subquadro), origin (origem), tick.