GAF — グラフィックアニメーション形式
GAFはインデックスカラーのスプライトアニメーションを格納する形式です。Total Annihilationで動く2D要素は、ほぼすべてがGAFです。カーソル、マウスポインター、爆発、ユニットのガジェット、木の揺れ、武器アイコン、溶岩の泡など、何から何までそうです。TA: Kingdomsは同じコンテナを
.taf拡張子で流用し、パレット付きのアニメーションに使っています(パレットは別のサイドカーファイルから読み込みます)。TA:Kにはインデックスの代わりに16ビットのARGBピクセルを格納するトゥルーカラー版の.tafもあり、こちらは専用のページTAF / TSFで扱います。
Tip
自分で試す
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
すべてのフラグについては、CLIのkbot gafリファレンスを参照してください。
Goから使う場合は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))
}
概要
┌─ 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 ─┐
└──────────────────────────────────┘
GAFはシーケンスを平らに並べた配列で、各シーケンスはフレームの配列です。各フレームは幅×高さのパレットインデックスのビットマップで、原点を1つ持ち、(任意で)RLE風の透過ランを持ちます。フレームは「サブフレーム」を持つことができ、1つの論理的なアニメーションステップを、重ね合わせた複数のレイヤーの合成にできます。
ヘッダー(12バイト)
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;
| フィールド | 説明 |
|---|---|
Version |
サードパーティ製のツールはすべて0x00010100を使います。Cavedogのanims/terrain.gafとanims/vismasks.gafはVersion == 0で出荷されています。ゲームはこのワードを検査しません。kbotはどの値でも読み込み(見慣れない値は警告として報告します)、書き込みでは0x00010100を使います。 |
SequenceCount |
直後にSequenceCount × uint32個の絶対ファイルオフセットが続き、それぞれがSequenceHeaderを指します。ゲームは下位16ビットだけを符号付きの値として読みます。シーケンス数は1〜32767で、0または負の値ならシーケンスはありません。 |
Unknown1 |
常に0です。書き込み時も0を出力します。 |
シーケンス(40バイト)
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;
ゲームはLoopFlagsの下位バイトがセットされているとシーケンスをループ再生し、0なら1回だけ再生します(その後は最後のフレームで止まります)。標準のシーケンスはすべて1を格納しています。kbotはファイルを書き直すときに両方のワードを保持し、kbot gaf buildはsequence.csvのないシーケンスに標準の値である1を与えます。
この後にFrameCount × FrameListItemが続きます。
typedef struct {
uint32 PtrFrameInfo; // → FrameInfo
uint32 Duration; // Game ticks (30 ticks = 1 second); low 16 bits used
} FrameListItem;
ゲームはDurationの下位16ビットだけを読み、30 Hzのアニメーションクロックでmax(Duration, 1)ティックの間フレームを表示します。
シーケンス名には意味があります。ゲームはアニメーションを名前で検索します(たとえばUnitInfoはcorpyro.gafスクリプトのcorpyro_smokeを参照します)。照合するときは、大文字と小文字を区別しないASCIIとして扱ってください。
フレーム(24バイトの情報とペイロード)
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は、フレームをワールドに転送(ブリット)するときに使うアンカー点を表します。爆発のスプライトなら原点はふつう爆発の中心、カーソルならホットスポットのピクセルです。
レイヤー付きフレーム
LayerCount > 0のとき、PtrFrameDataはピクセルデータではなくLayerCount × uint32個のポインターの配列で、各ポインターは入れ子になったFrameInfo構造体を指します。各レイヤーは配列の順に(奥から手前へ)描かれ、そのホットスポットは外側のフレームのホットスポットに合わせて置かれます。+11バイトがセットされたレイヤーは、コピーされる代わりに半透明テーブルを通して描かれます(これをセットしている標準ファイルはありません)。バイト+10とバイト+11は別々のフィールドです。2つを1つの16ビットの数として読むと、+11バイトがセットされているだけで256レイヤーになってしまいます。
ピクセルデータ
TransparencyIndexはファイル全体ではなくフレームごとの値です。たとえばカーソルのGAFは、あるフレームではインデックス9(システムピンク)を、別のフレームではインデックス0を使っています。
非圧縮(Compressed == 0)
Width × Heightバイトで、上から下、左から右の順に並びます。ゲームはTransparencyIndexと等しいピクセル以外をすべて描画します。ユニットのテクスチャ(textures/*.gaf)と視界マスク(anims/vismasks.gaf)は単純なピクセル配列として読まれるため、これらのフレームは非圧縮でなければなりません。標準のフレームはすべて非圧縮です。
圧縮(Compressed != 0)— TAのランレングス符号化
ピクセルデータはHeight個の独立した行の並びです。各行はuint16 lineLengthで始まり、その後にちょうどlineLengthバイトの、ランレングス符号化されたバイト列が続きます。このバイト列の中で、各チャンクは1バイトのマスクで始まり、その下位2ビットでチャンクの種類が決まります。
| マスクの判定 | チャンクの種類 | 長さ(ピクセル) | 後続のバイト |
|---|---|---|---|
mask & 0x01 |
透過ラン | count = mask >> 1 |
(なし。ピクセルは読み飛ばされます) |
mask & 0x02 |
繰り返しラン | count = (mask >> 2) + 1 |
1バイト。繰り返す色のインデックス |
| どちらでもない | リテラルコピー | count = (mask >> 2) + 1 |
countバイト。リテラルのピクセル |
したがって1チャンクあたりのランの最大長は、透過が127ピクセル、繰り返しが64ピクセル、リテラルが64ピクセルです。透過になるのは読み飛ばされたピクセルだけです。繰り返しやリテラルのピクセルは、値がTransparencyIndexと等しくても描画され、パレットインデックス0はふつうの不透明な黒です。符号化された行がWidthに届く前に終わった場合、kbotは残りを透過ピクセルで埋めて報告します。ゲームのほうはそのまま後続のバイトを読み進めてしまいます。レイヤーは外側のフレームより小さくてもよく、それぞれのOriginX/Yで配置されます。
参照:formats/gaf/gaf.go readCompressedとformats/gaf/writer.go compressRow。
Important
GAFはパレットを持ちません。ファイルには8ビットのインデックスしか格納されていません。カラーで描画するには、TA共通のパレット(palettes/palette.pal)か、TA: Kingdomsの場合はGAFの外にあるアセットごとのパレットが必要です。TAK固有の仕組みについては[TA: Kingdomsのパレットの解決]
(#ta-kingdoms--palette-resolution)とTA: Kingdomsの透過の癖を参照してください。kbot gaf exportは既定で組み込みのTAパレットとゲームの透過規則を使います。どちらも上書きできます。
TA: Kingdoms — パレットの解決
Note
この節はTA:Kだけの差分です。TAのレンダラーは常にpalettes/palette.palを使います。GAFのバイト配置はTAとTAKで同じですが、TAKはアセットごとのパレットを使う仕組みに差し替えています。
TA: KingdomsはTAと同じ.gaf(ときには.taf)コンテナを使いますが、パレットはアセットごとのサイドカーファイルから取得します。製品版TAKのインストールは、優先度の高い順に次の3つの規則に従っています。いずれも文書化されているものです。
.gafと同じ名前で隣に置かれた.pcx。UIやアニメーションのアセットには、目的のパレットをPCXのPLTEチャンクとして埋め込んだ1×1のPCXファイルが付いています。anims/actionbuttons.gafはanims/actionbuttons.pcxのパレットで描画されます。TAKのanims/*.gafファイルのうち約38%に、こうしたサイドカーがあります。palettes/にある、ベース名が同じ.pal。まれなケースで、Cavedogが生の1024バイトのパレットを書き出した少数の標準アセットが該当します。ファイル名の接頭辞による陣営別のパレット。ユニットと地形物のGAFには3文字の陣営接頭辞が付いた名前があり、対応する陣営のパレットを使います。
接頭辞 陣営 パレットファイル 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
どれにも該当しない場合、リゾルバーはpalettes/palette.palに、最後には組み込みのTAパレットにフォールバックします。この2つは製品版TAKでは互いにバイト単位で同一で、TAのものとも同一です。そのため、陣営接頭辞に一致しないユニットや地形物のGAFは、明示的に上書きしない限り色が正しく出ません。
kbotのリゾルバー
internal/palettepickが上記の解決順を実装しています。GAFを描画するすべての入口がこれを通ります。
- Webエクスプローラー。
/png/…と/apng/…のルートは既定で自動判定します。GAFビューアーには、リゾルバーが見つけたすべての候補とautoの項目を並べたパレットのドロップダウンがあります。特定のパレットを強制するには?palette=<vfs-path>を渡します(ドロップダウンがこれを代わりに行います)。 kbot gaf export/kbot gaf dump。上書きするには--palette <path.pcx|path.pal>を渡します。指定しなければ組み込みのTAパレットが使われます(CLIはkbot mountで包まない限りVFSのコンテキストを持たないため、VFSのリゾルバーを実行しません)。- MCPの
gaf_exportツール。任意のpalette引数(設定されたgame-dataフォルダー内のパス)を受け付けます。省略すると、解決順をすべてたどるリゾルバーが実行されます。
TAのGAFをTA: Kingdomsに移植する
TAとTAKのGAFのバイト配置は同じで、違うのはパレットの取得元だけです。既存のTAの.gafを移植するには、次のようにします。
- TAパレットを埋め込んだ1×1のパレット付きPCXを作ります。
- それをGAFの隣に同じベース名で保存します(例:
MyGaf.gafとMyGaf.pcx)。
これだけで、TAKは元のTAの色で描画します。
TA: Kingdoms — 透過の癖
Note
この節はTA:Kだけの差分です。フレームの節にあるTransparencyIndexバイトはTAのレンダラーにとって正式な値ですが、TAKのコンテンツ制作者は、非圧縮フレームでこの値を実際のピクセルデータと合わせていませんでした。
TAKのテクスチャアトラスGAFの多く(textures/*.gafの全部とanims/*.gafの一部)は、メタデータのTransparencyIndexがある定数(多くは9)になっているのに、その値が実際のピクセルデータに一度も現れない非圧縮フレームを持っています。一方でアーティストは、「透過」のつもりの縁を別のパレットインデックス(多くは5)で塗っています。メタデータを文字どおりに読むと、TAのレンダラーなら透過になったはずの部分が不透明な色付きの背景として描画されます。
TAのレンダラーは推測をしません。非圧縮フレームには格納されたキーを、圧縮フレームにはスキップ命令を使います。TAKのアトラス向けに、kbotはオプトインの四隅検出ヒューリスティック(TransparencyModeHeuristic。kbot-ioのformats/gafにあるEffectiveTransparencyIndexを参照)を用意しています。これは非圧縮フレームにだけ適用されます。
TransparencyIndexがピクセルバッファーのどこかにあれば、メタデータを信用します。- なければ四隅をサンプリングします。4つすべてが同じ値なら、その値を実効的な透過インデックスとして使います。これで、アーティストが均一な縁を描いたTAKの非圧縮フレームが救われます。
- それ以外の場合は、ディスク上の
TransparencyIndexにフォールバックします(これにより、四隅がユニットのピクセルになっているdungwormB1のような、全面に絵があるテクスチャの描画が保たれます。自動検出は正しく上書きを見送ります)。
圧縮フレームと合成フレームは、常に読み飛ばされたピクセルを透過として使います。ディスク上のバイトが上書きされることはなく、ラウンドトリップで書き戻すライターには元の値が見えます。均一な縁を持つ完全に不透明なTAのフレームは、このヒューリスティックでは縁を失ってしまうため、TAの描画では決して使いません。
描画時に透過を上書きする
gaf.RenderOptionsには次のモードがあります。
| モード | 動作 | 用途 |
|---|---|---|
TransparencyModeAuto / TransparencyModeMetadata |
ゲームの規則。非圧縮フレームのキーのピクセルと、圧縮フレームの読み飛ばされたピクセルが透過になり、インデックス0は不透明な黒です(ゼロ値で、kbotではTAに対してどこでもこれが既定)。 | 表示、ダンプ、ラウンドトリップ。 |
TransparencyModeHeuristic |
非圧縮フレームでは四隅から推測し(上記)、それ以外ではゲームの規則。スタジオがTA: Kingdomsのスプライトに使うモードです。 | TAKのテクスチャアトラス。 |
TransparencyModeNone |
すべてのピクセルが不透明。 | キーカラーの確認。 |
TransparencyModeIndex |
呼び出し側が指定したインデックスを使う。 | UIでの上書き、デバッグ。 |
書き出されるパレットの項目は、書き出しごとに1つある透過スロット(通常はフレームのキー)を除いて、すべて不透明です。フレームがキーと同じ値のピクセルを描画する場合(あるいは1つのアニメーションのフレーム同士が互いのキーを色として使う場合)は、描画されるどのピクセルも使っていないインデックスが代わりに透過スロットになるので、画像はすべてのパレットインデックスを保ちます。
アセットエクスプローラーのGAFビューアーには、パレットの選択欄の横にDefault / Game rule / Corner guess / Noneのドロップダウンがあり、選んだ値をすべてのPNG/GIF/APNGリクエストに?transparency=game|heuristic|none|<N>として渡します。既定は、TAではゲームの規則、TA: Kingdomsのインストールでは四隅からの推測です。キャッシュキーには透過のタグが含まれるので、モードを切り替えても古い描画結果が返されることはありません。
MCPのgaf_exportツールも同じ既定に従います。基本はゲームの規則ですが、TA: Kingdomsとして登録されたgame-dataフォルダー(takingdomsのkbotコンテキスト)では四隅からの推測を使い、結果には使ったモードが示されます。kbot gaf exportはゲームのコンテキストなしで単体のファイルを読むため、既定はゲームの規則です。TA: Kingdomsのアトラスには--transparency heuristicを渡してください。kbot gaf buildが画像から格納済みのキーを読み戻すため、kbot gaf dumpは常にゲームの規則を使います。
実例 — 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
...
典型的な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)
GAFをビルドする
kbot gaf dumpは、kbot gaf buildが読み戻せるディレクトリ構成を書き出します。
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
フレーム画像はゲームの規則で描画したインデックスカラーのPNG(--format gifならGIF)なので、すべてのピクセルがパレットインデックスを保ち、透過ピクセルにだけ印が付きます。ビルドではそのインデックスをそのまま書き戻し、透過ピクセルはフレームのキーに変えます。RGBA画像の場合は、代わりにキーを避けながら最も近いパレットの色に合わせます。
sequence.csvはシーケンスの順序、正確な名前、ループのワード、+4のワードを復元します。これがない場合はフォルダー名がシーケンス名になり、標準のシーケンスと同じくループします。storage(rawまたはcompressed)とblend(+11バイト)は、各フレームのヘッダーを復元します。storageのないフレームは圧縮されます。ただし、ゲームが単純なピクセル配列として読むため、textures/*.gafやanims/vismasks.gafに書き込むフレームはすべて非圧縮になります。--storage raw|compressedはこの両方を上書きします。- 合成(レイヤー付き)フレームは1枚に平坦化した画像としてダンプされ、単純なフレームとしてビルドし直されます。その数は
kbot gaf roundtripが報告します。 - ピクセルのない(幅または高さが0の)フレームには画像がありません。
frames.csvの行だけで再構築されます。 frames.csvもsequence.csvもないサブフォルダーは、警告を出してスキップします。それ以外でビルドできないサブフォルダー(画像がない、読めない、CSVの値が不正など)があるとビルドは止まるので、シーケンスが黙って抜け落ちることはありません。
Note
TAとTA:KのGAFはバイト単位で同一です。コンテナは同じで、違うのはパレットの仕組みだけです。解決順の全体と、GAFをTAKに移植する手順の実例については、上のTA: Kingdomsのパレットの解決を参照してください。
注意点
Warning
ループのバイトが0のシーケンスは1回再生して止まります。+2のワードを0として書き込むツールは、触れたアニメーション付きの地形物をすべて止めてしまいます。kbotはこのワードを保持し、kbotが新しく作るシーケンスはループします。
- フレームの表示時間が
0になっているシーケンスが、標準で含まれています。ゲームはそのようなフレームを、表示時間1と同じく1ティック表示します。既定値に「修正」しないでください。 - 圧縮されたピクセルの行は
Widthより短いことがあります。ゲームは次のバイトを命令として読み進めますが、kbotは行を透過ピクセルで埋めて報告します。そのような行を持つ標準ファイルはありません。 - レイヤーの原点はホットスポットです。各レイヤーは、その原点が外側のフレームの原点に重なるように置かれます。
- TA: Kingdomsの
.tafには2種類あります。kbotはパレット付きの.tafを通常のGAFとして読みますが、TA:Kにはトゥルーカラーの.tafファイル(16ビットのARGBピクセル、パレットなし)もあります。両者はフレームのフォーマットバイトで見分けられます。トゥルーカラー版についてはTAF / TSFを参照してください。 anims/terrain.gafとanims/vismasks.gafはVersion == 0です。ゲームはバージョンのワードを無視します。- シーケンスあたりのフレーム数に上限はありません。
cursorteleportは46フレームあります。バッファーのサイズを決めるときに、固定の上限を仮定しないでください。
典型的なサイズ
| アセット | CavedogのGAFで観測された範囲 |
|---|---|
anims/cursors.gaf |
約30 KB、22シーケンス、それぞれ1〜46フレーム |
| ユニットのガジェット(例:マズルフラッシュ) | 5〜20 KB、1シーケンス、4〜12フレーム |
爆発(anims/explosions.gaf系) |
1ファイルあたり50〜250 KB |
anims/textures.gaf(マスターのテクスチャシート) |
約12 MB(TAで最大のGAF) |
anims/terrain.gaf |
約1.5 MB |
| フレームごとのデータ | 通常は1 KB未満。大きなフレーム(256×256の爆発)では10 KB以上 |
リファレンスカタログにある実例
TAのインストールに含まれる主なGAFです。どれもkbot hpi extractで取り出してから、kbot gaf list / kbot gaf exportで調べられます。
anims/cursors.gaf— 22シーケンスで、それぞれがUIカーソルのアニメーションです。最も小さいシーケンス(cursorgrn、cursorred)は1フレーム、最も大きいcursorteleportは460ティックにわたる46フレームです。上の実例の節で取り上げています。anims/textures.gaf— 約12 MBのマスタータイルシートで、すべての3DOモデルのOffsetToTextureNameの文字列から参照されます。kbot gaf list anims/textures.gaf | headで中身を見られます。anims/explosions.gaf系 — 典型的なエフェクトのGAFです。lasersht.gaf、acid.gaf、bigexpls.gafなどがあり、どれも名前付きの爆発シーケンスを複数持っています。anims/armcom.gaf/corcom.gaf— 陣営ごとのコマンダーのガジェット(選択サークル、建設レーザー)。anims/terrain.gaf+anims/vismasks.gaf— ヘッダーでVersion == 0になっている標準のGAFは、この2つだけです(パーサーを書くときに知っておく価値があります)。