NovaLogicのComanche地形図をリバースエンジニアリングする
NovaLogicのComancheゲームは地形を偽装形式で保存しており、本物のPCXファイルではありません。本記事ではマップをバイト単位で分解し、RLEデータストリームと末尾に隠されたパレットをデコードして、レンダリング可能な地形を再構築します。
日本語
コピー

NovaLogic の Comanche で使われている Voxel Space 地形レンダリング技術は、私が最も好きなアルゴリズムのひとつだ。この記事では、地形のレンダリングに必要な heightmap と color データを、オリジナル版ゲームのファイルからデコードしていく過程を書く。

2026年9月29日 · 15分で読める

私はずっと 3D 地形レンダリングに、コンピュータグラフィックスが地形を再現するその方法に魅せられてきた。この魅力は、ブラジル南部のパンパで育った経験に由来するのかもしれない——そこでは自然の景色が地平線の果てまでずっと広がっていた。丘、植生、開けた地平線、そして濃淡の異なる緑の林地と Araucaria の木。

Campos de Cima da Serra。ブラジル、Rio Grande do Sul。
一方で、初めて OpenGL で地形をレンダリングしたときのあの高揚感は、別の夢を見ていただけなのではないか、とも思う。手つかずの風景、人間が残した傷跡も、じわじわと迫ってくるいわゆる進歩もない風景を見たいという夢だ。コンピュータグラフィックスの力を使えば、別の世界を想像できる……私の記憶にあるような世界を。目の前で少しずつ崩れていくのを眺めさせられている世界ではなく。

旧式(固定パイプライン)OpenGL 1.4 を使った 3D 地形の例
とはいえ、私が OpenGL で美しい 3D 地形を描くことから始めたわけではない。90年代初頭にはハードウェアアクセラレーションも高解像度ディスプレイもなかった。当時はすべてのピクセルをソフトウェア(CPU)で手作業で描いていた。MS-DOS ゲーム、VGA と SVGA グラフィックス、ローポリゴン、フラットシェーディング、ディザリングされた 3D 世界の黄金時代だった。
当時のフライトシミュレーターの多くは、地形をポリゴンでレンダリングしていた。3D オブジェクトは頂点で構成され、その頂点がポリゴンのシーンを組み立て、投影とラスタライズを経て、スキャンラインごと、ピクセルごとに描画される。

F29 Retaliator by Digital Image Design
ほとんどの 3D ゲームがまだポリゴングラフィックスを使っていた時代に、NovaLogic の Comanche: Maximum Overkill はすでに、うねる山脈、深い峡谷、明暗のある谷をほぼ写真のように描き出し、しかもそれが家庭用の 386 マシンで動いていた。
その裏にある技術が Voxel Space で、Kyle Freeman が生み出した。おそらく私が史上最も好きなアルゴリズムのひとつで、以前の動画でもその仕組みを解説している:
Comanche Terrain Rendering (Voxel Space Algorithm)
私たちの Voxel Space チュートリアルを見たことがあれば、1024×1024 の画像をひと組使ったことを覚えているだろう。カラーマップが1枚と、各レベルごとに1枚のハイトマップだ。
だが、これらの画像がどこから来たのか考えたことはあるだろうか? ゲームに GIF ファイルが同梱されていたことは一度もない。あの GIF を生成するには、元のゲームデータから掘り出す必要があった。
この記事でやるのはまさにそれだ。1992年のゲームに付属していたファイルを開き、生のバイト列を眺め、フォーマットを突き止め、どんな画像ビューアでも開ける BMP 画像に変換する小さな ANSI C プログラムを書く。答えは思っているよりずっと地味だ——そしてそれ自体が教訓になる。
ゴールを定める
ヘックスエディタを開く前に、最終的な結果がどうなるべきかを把握しておくと役に立つ。Voxel Space の地形に必要なデータは2つだけだ:
- ハイトマップ:グリッドの各バイトが、その地点の地形の高さを表す。
- カラーマップ:同じ大きさのグリッドで、各バイトがその地点の地面の色を表し、照明と影はすでに焼き込まれている。

どちらのグリッドも 1024×1024。つまり各画像はちょうど 1,048,576 バイト、1 MB だ。カラーマップは8ビットインデックスカラーなので、各バイトは RGB 値ではなく、256色パレットへのインデックスになる。これは VGA の mode 13h にぴったりで、こちらも256色だ。
この数字を覚えておこう:1,048,576。ちょうどこのサイズに展開できるものを見つけられれば、方向は合っている。
ゲームディレクトリを覗く
オリジナル版のディレクトリは90年代初頭らしく、8.3 形式のファイル名(名前8文字+拡張子3文字)で埋め尽くされている。手元のコピーでは、いくつかの .RLE ファイル(おそらくスプライトと効果音)、.MIS ファイル(おそらくミッションデータ)、そして大量の .DTA ファイルが見える。そのうち8つのファイルは、サイズを見ただけで目を引く:

これらのファイルはペアで存在するらしい。番号は 1 から 4 まで、接頭辞は C と D だ。
最初に浮かんだのは、C は "color"、D は "depth"(ハイトマップ)の頭文字だろうという推測だった。どれも 1 MB 未満なので、中身は圧縮されているに違いない。
ファイルフォーマットをリバースエンジニアリングするとき、まず最初にやるのは十六進エディタで先頭数バイトを覗くことだ。以下が C1.DTA の先頭部分。

最初の 8 バイトをつなげると Kyle DTA になる。Kyle Freeman 本人の署名で、どのマップファイルの先頭にもある。C の 4 ファイルと D の 4 ファイルも同じ始まり方のようだ。
残りの部分からはさらに 3 つの手がかりが得られる。
- オフセット
0x08にあるのはff 03 ff 03。16 ビットリトルエンディアン整数として読むと 1023 と 1023 で、1024×1024 に怪しいほど近い。 - オフセット
0x42にあるのは00 04、すなわち 1024。 - ヘッダはオフセット
0x80(128)で終わっているらしい。ゼロバイトが並んだあと、データの様子が一変する。
ヘッダの直後はこうなっている。
00000080: 01 02 c2 01 02 01 c5 03 01 02 01 03 01 02 c2 01 ................
00000090: 02 04 c2 02 01 02 01 03 c2 01 02 01 c7 03 02 04 ................
C2、C5、C7 のいずれかであるバイトがどれだけ多く、そしてそうしたバイトの後ろに必ず平凡な小さい値が続いているかに注目してほしい。DOS 時代の画像フォーマットを触ったことがある人なら、ここでピンとくるはずだ。
変装した PCX
128 バイトのヘッダ、画像サイズを最大座標で格納する方式(1024 ではなく 1023)、そして上位 2 ビットがしばしば立っているバイト列——これらはすべて PCX の特徴だ。PCX は ZSoft Paintbrush のフォーマットで、80 年代末から 90 年代初頭にかけて DOS 上にあふれていた。

ZSoft PC Paintbrush
注意: PCX は PiCture eXchange の略で、アメリカ・ジョージア州マリエッタの ZSoft 社が開発した——その会社は今はもう存在しない。PC Paintbrush のネイティブファイルフォーマットであり、DOS で最初に広く受け入れられた画像規格のひとつでもあった。その後は当然、BMP、JPEG、PNG といったより進んだ画像フォーマットに取って代わられた。PCX ファイルは通常パレットインデックス画像を格納し、色数は 2 色、4 色から 16 色、256 色までさまざまだが、このフォーマットは後にトゥルーカラー(24 ビット)画像の記録能力も拡張された。
正常な PCX ファイルはバイト 0x0A(ZSoft の「ベンダー」ID)で始まり、続いてバージョン番号、エンコードフラグ、ピクセルあたりのビット数が並ぶ。そのあとに 4 つの 16 ビット座標が続く。xmin、ymin、xmax、ymax だ。これらのフィールドを合計するとちょうど 8 バイトになる。
どうやら NovaLogic はまさにこれをやったらしい。標準的な 8 ビット PCX ファイルを用意し、先頭 8 バイトをテキスト Kyle DTA で上書きしたのだ。残りのフィールドはすべて元の位置にある。
| オフセット | サイズ | PCX フィールド | C1.DTA での値 |
|---|---|---|---|
0x00 | 8 | ベンダー、バージョン、エンコード、ピクセルあたりのビット数、xmin、ymin | Kyle DTA で置き換え |
0x08 | 2 | xmax | 1023 |
0x0A | 2 | ymax | 1023 |
0x0C | 4 | 水平・垂直 DPI | 1024、768(無視される) |
0x10 | 48 | 16 色 EGA パレット | 未使用 |
0x40 | 1 | 予約 | 0 |
0x41 | 1 | 色プレーン数 | 1 |
0x42 | 2 | 走査線あたりのバイト数 | 1024 |
0x44 | 60 | パレット情報とパディング | すべてゼロ |
0x80 | ... | 圧縮されたピクセルデータ | RLE ストリーム |
幅は xmax + 1、高さは ymax + 1、つまり 1024×1024。プレーンが 1 つ、ピクセルあたり 1 バイトということは、各ピクセルが 8 ビットのインデックスに対応する——まさに探していたものだ。
なぜこんなふうに PCX ファイルを隠したのか。推測するしかない。Deluxe Paint でマップを開けないようにするためかもしれないし、ローダーが正しい種類のファイルを掴んだことを確認するためだけかもしれない。
とはいえこの変装は表面的なものにすぎない。標準的な PCX ヘッダの 8 バイトを書き戻し(0a 05 01 08 00 00 00 00)、ファイル名を .PCX に変えれば、PCX に対応した画像ビューアならどれでもこのマップを開ける。

ZSoft PC Paintbrush で C1.DTA のカラーテーブルを開く
面白いことに、DPI フィールドはファイル間で一致していない。C2.DTA には 800×600 と書かれているが、C3.DTA と C4.DTA には 1024×1024 と書かれている。これらの値はおそらく、アーティストがファイルを保存したときに使っていたツールの設定を記録しただけのものだろう。
RLE ストリームをデコードする
PCX ファイル形式が採用しているのは、これ以上ないほど単純な圧縮方式、ランレングス符号化(RLE)だ。
アセンブリで NES をプログラミングする講座を受けたことがあるなら、カートリッジ内のレベルデータを RLE でデコードしたのを覚えているだろう。考え方は単純で、05 05 05 05 05 05 05 をそのまま保存する代わりに 07 05(「05 が 7 個」)を保存する。地形テクスチャでは隣り合うピクセルの値が一致しやすいので、かなりの容量が節約できる見込みがある。デコードのコストもごくわずかで、386 PC ではこれが重要だった。
PCX のルールはこうだ。
- あるバイトの上位 2 ビットが立っていれば(
b >= 0xC0)、そのバイトはカウンタである。下位 6 ビットが、直後のバイトを何回繰り返すかを表す。 - そうでなければ、そのバイトはそのままのピクセル値であり、そのままコピーする。
先ほど見たデータの冒頭、01 02 c2 01 02 01 c5 03 をデコードしてみよう。
01 -> 01
02 -> 02
c2 01 -> 01 01 (0xC2 & 0x3F = 2 copies)
02 -> 02
01 -> 01
c5 03 -> 03 03 03 03 03 (0xC5 & 0x3F = 5 copies)
ここで一つ、引っかかりやすい点がある。単一のピクセルの値が 0xC0 以上だったらどうなるか? デコーダはそれをカウンタと誤認してしまう。エンコーダ側はこれを、そうした値を必ず長さ 1 のランとして書くことで避ける。C1 D7 は値 0xD7 のピクセル 1 個を意味する。1 つのランは最大 63 ピクセルまでしか持てないので、同じ色の長い帯が複数のランに分割されているのも目にするはずだ。
C で書けば、デコーダ全体が 1 つのループで済む。data にファイル全体が入っており、128 バイトのヘッダーの直後から読み始める。
/*****************************************
* PCX run-length decoding routine
* Starts right after the 128-byte header
******************************************/
pos = 128;
n = 0;
while (n < total && pos < size) {
unsigned char b = data[pos++];
if ((b & 0xC0) == 0xC0) {
int count = b & 0x3F;
unsigned char value = data[pos++];
while (count-- && n < total) {
img->pixels[n++] = value;
}
} else {
img->pixels[n++] = b;
}
}
Comanche のマップでは、ランが行をまたぐことはない。だから画像全体を 1 本の長いストリームとしてデコードできる。ちょうど 1,048,576 ピクセルでループが終わり、これはまさに期待通りの数字だ。
末尾に隠されたパレット
100 万バイト分の色インデックスは手に入った。だがそのインデックスは何を指しているのか? ヘッダーにある 48 バイトのパレットは 16 色分しかなく、Comanche のマップはそれを使っていない。
PCX バージョン 5 は、256 色画像を扱うためにパレットをファイルの末尾に置く。形式は単純で、マーカーバイト 0x0C のあとに 256 組の RGB 三つ組(768 バイト)が続く。デコーダはちょうど 1,048,576 ピクセルで止まるが、その時点でファイルには何バイト残っているか? 8 枚の画像すべてで 769 バイト、つまりマーカーとパレットだ。
/***********************************
* 256-color palette: Marker 0x0C.
* Followed by 768 bytes at the end
************************************/
if (size >= 769 && data[size - 769] == 0x0C) {
memcpy(img->palette, data + size - 768, 768);
}
これらの色を実際の VGA ハードウェアで使うつもりなら、一つ注意点がある。PCX は各成分を 8 ビット(0〜255)で持つが、VGA DAC は各成分を 6 ビット(0〜63)しか受け付けない。mode 13h でパレットをポート 0x3C8/0x3C9 に送るときは、各値をまず 2 ビット右シフトする必要がある。
面白い発見がある。ハイトマップのファイルにもパレットが付いているのだ。しかも単純なグレースケールではなく、16 段階ごとの色帯に分かれた疑似カラーパレットで、まず青、次にオレンジ、と続き、さらに上には灰色の帯があり、未使用のエントリはマゼンタで埋められている。このパレットで D ファイルをペイントプログラムで開くと、等高線図が見える。各色帯が 16 高度単位を覆っている。Kyle Freeman 本人に確認してみるつもりだが、NovaLogic のアーティストがこのビューを使って地形ファイルを描き、確認していたであろうことは容易に想像できる。
我々としては、高度用パレットはひとまず無視し、通常のグレースケールに置き換える。こうすればピクセル値がそのまま高度になる。黒が最低点で、明るいほど高い。4 枚の画像の高度は 0 からおよそ 120 までしかないので、画像はかなり暗く見える。これは正常で、これらの値は生の標高であって、人間が見るための画像ではない。
8 ビット BMP を書く
PCX 形式を理解してしまえば、これらのファイルの BMP 版を生成するのは難しくないはずだ。
ピクセルとパレットがメモリ上にあるので、あとは現代のツールが読める形式で保存するだけだ。ここでは BMP を使う。非圧縮の 8 ビット BMP は、手書きする画像形式としてはおそらく最も簡単だからだ。小さなヘッダーが 2 つ、パレットが 1 つ、あとは生のピクセルだけ。
構造はこうなる。
| 部分 | サイズ | 内容 |
|---|---|---|
BITMAP FILEHEADER | 14 バイト | BM シグネチャ、ファイルサイズ、ピクセルデータのオフセット |
BITMAP INFOHEADER | 40 バイト | 幅、高さ、プレーン数 1、ピクセルあたり 8 ビット、無圧縮 |
| カラーテーブル | 256 × 4 バイト | 各色を青、緑、赤、0 の順で |
| ピクセルデータ | 高さ × 行サイズ | ピクセルあたり 1 バイト、行は 4 バイトの倍数にパディング |
初めて書く人が必ず踏む罠が 3 つある。
- BMP は色を BGR で格納する。RGB ではない。しかも各色は 3 バイトではなく 4 バイトを占める。
- 高さが正のとき、行は下から上へ格納される。画像の最終行がファイルの先頭に来る。
- すべてのフィールドはリトルエンディアンだ。ここでは 1 バイトずつ書き込むので、CPU のエンディアンに関係なくコードの挙動は一致する。
まず、16 ビットと 32 ビットの値をリトルエンディアンで書き込む補助関数を 2 つ用意する。
static void write_u16(FILE *f, unsigned int v) {
fputc(v & 0xFF, f);
fputc((v >> 8) & 0xFF, f);
}
static void write_u32(FILE *f, unsigned long v) {
write_u16(f, (unsigned int)(v & 0xFFFF));
write_u16(f, (unsigned int)(v >> 16));
}
ファイル全体を書き出す関数はこうだ。
static int save_bmp(const char *filename, const image_t *img) {
FILE *f;
int i, y;
unsigned long row_size = (img->width + 3) & ~3UL; /* rows padded to 4 bytes */
unsigned long pixel_offset = 14 + 40 + 256 * 4;
unsigned long file_size = pixel_offset + row_size * img->height;
f = fopen(filename, "wb");
if (!f) {
return 0;
}
/* BITMAPFILEHEADER (14 bytes) */
fputc('B', f); fputc('M', f);
write_u32(f, file_size);
write_u32(f, 0); /* reserved */
write_u32(f, pixel_offset);
/* BITMAPINFOHEADER (40 bytes) */
write_u32(f, 40);
write_u32(f, img->width);
write_u32(f, img->height); /* positive = rows stored bottom-up */
write_u16(f, 1); /* planes */
write_u16(f, 8); /* bits per pixel */
write_u32(f, 0); /* no compression */
write_u32(f, row_size * img->height);
write_u32(f, 2835); /* 72 DPI */
write_u32(f, 2835);
write_u32(f, 256); /* colors used */
write_u32(f, 0);
/* palette: BMP wants blue, green, red, reserved */
for (i = 0; i < 256; i++) {
fputc(img->palette[i * 3 + 2], f);
fputc(img->palette[i * 3 + 1], f);
fputc(img->palette[i * 3 + 0], f);
fputc(0, f);
}
/* pixel rows, last row first */
for (y = img->height - 1; y >= 0; y--) {
unsigned long pad;
fwrite(img->pixels + (long)y * img->width, 1, img->width, f);
for (pad = img->width; pad < row_size; pad++) {
fputc(0, f);
}
}
fclose(f);
return 1;
}
我々のマップは幅 1024 ピクセルで、それ自体が 4 の倍数なので、パディングのループは決して実行されない。それでも残してあるのは、任意の幅で関数が正しく動くようにするためだ。
GIF ではだめなのか?
最初の voxel space チュートリアル/コードで使った画像は GIF だった。ならばなぜ GIF を直接書かないのか? GIF は LZW 方式でピクセルを圧縮するが、可変コード長に対応した正しい LZW エンコーダは、それだけで記事が一本書けてしまうからだ。
ちょっとした歴史:90年代の大半を通じて、LZW は Unisys の特許に覆われていた。1994年末に Unisys がライセンス料を取ると発表したときの反発の大きさが、そのまま PNG 形式を生むきっかけになった。
幸いなことに、こちらには必要ない。この BMP は 8 ビットインデックスに 256 色パレットで、GIF とまったく同じだから、変換しても失うものは何もない。ImageMagick のようなツールで片付く。
まとめる
ここまでの部品を小さなコマンドラインツールに貼り合わせる。デコードした画像はそれぞれ単純な struct に収める:
typedef struct {
int width, height;
unsigned char *pixels; /* width * height palette indices */
unsigned char palette[768]; /* 256 RGB triplets, 8 bits each */
} image_t;
ローダーはファイル全体をメモリに読み込み、Kyle DTA シグネチャを確認し、ヘッダから寸法を取り出して、前に見た RLE ループとパレットのコピーを走らせる:
static unsigned int read_u16(const unsigned char *p) {
return p[0] | (p[1] << 8);
}
static int load_dta(const char *filename, image_t *img) {
FILE *f;
long size, pos, n, total;
unsigned char *data;
f = fopen(filename, "rb");
if (!f) {
return 0;
}
fseek(f, 0, SEEK_END);
size = ftell(f);
fseek(f, 0, SEEK_SET);
data = (unsigned char *)malloc(size);
if (fread(data, 1, size, f) != (size_t)size || memcmp(data, "Kyle DTA", 8) != 0) {
fclose(f);
free(data);
return 0;
}
fclose(f);
img->width = read_u16(data + 8) + 1; /* xmax + 1 */
img->height = read_u16(data + 10) + 1; /* ymax + 1 */
total = (long)img->width * img->height;
img->pixels = (unsigned char *)malloc(total);
/* ... RLE decoding loop ... */
/* ... palette copy ... */
free(data);
return 1;
}
最後に main() がマップ番号を受け取り、ペアになる 2 つのファイルを変換し、ハイトマップのパレットをグレースケールのグラデーションに差し替える:
int main(int argc, char *argv[]) {
char in_name[256], out_name[256];
image_t color, height;
int map, i;
if (argc < 2) {
printf("usage: dta2bmp <map number 1-4>\n");
return 1;
}
map = atoi(argv[1]);
sprintf(in_name, "C%d.DTA", map);
if (!load_dta(in_name, &color)) {
printf("Could not read %s\n", in_name);
return 1;
}
sprintf(out_name, "map%d_color.bmp", map);
save_bmp(out_name, &color);
sprintf(in_name, "D%d.DTA", map);
if (!load_dta(in_name, &height)) {
printf("Could not read %s\n", in_name);
return 1;
}
for (i = 0; i < 256; i++) { /* gray ramp: index = height */
height.palette[i * 3 + 0] = (unsigned char)i;
height.palette[i * 3 + 1] = (unsigned char)i;
height.palette[i * 3 + 2] = (unsigned char)i;
}
sprintf(out_name, "map%d_height.bmp", map);
save_bmp(out_name, &height);
printf("map %d: %dx%d color + height maps saved\n", map, color.width, color.height);
free(color.pixels);
free(height.pixels);
return 0;
}
プログラム全体はおよそ 150 行の標準 ANSI C だ。
これが最初のマップの結果で、左がカラーマップ、右がハイトマップ:

Comanche マップ 1:カラーマップ(左)とハイトマップ(右)
2 枚がどう対応しているかは一目瞭然だ。川はハイトマップで最も暗い谷になり、最も明るい領域は雪をかぶった山頂になる。左上にも目を向けてほしい。あの四角い構造物は両方のマップに直接描き込まれた基地で、高さもそれに合わせて持ち上げてある。Voxel Space では、建物は地形の一部だ。
以下がオリジナルのゲームに含まれる 4 枚のカラーマップ地形すべて:

1992 年のオリジナル Comanche に収録された 4 枚のカラーマップ
答え合わせ
うまくいったかどうかを確かめるには? 何年もネットに出回っているマップのセットと比べればいい。あちらは map0 から番号が振られている。こちらのマップ 1 が map0、マップ 2 が map1、以下同様に対応する。
- ハイトマップ
D1.DTAはmap0とバイト単位で一致する。両ファイルの MD5 ハッシュが同じだ。 - カラーマップ
C4.DTAは、すべてのピクセルでmap3とまったく同じ RGB 色を返す。
残りのマップはほぼ同じだ。違いが出るのは水面で、ネットに出回っている版は湖や川をすべてパレットエントリ 251 から 254 で塗っている。このあたりは水面の色循環アニメーションに使われるエントリだろう。高さも 1 単位ずれている箇所がある。こちらのファイルは 1992 年のオリジナル版から取ったもので、ネットの版はおそらく後発のバージョンのゲームから出たものだ。ゲームデータはファイル名が変わらなくてもバージョンごとに変わる、という良い例でもある。
マップファイルはどう作られたのか?
では Kyle Freeman は当時どうやってこれらのマップを作ったのか。1994 年 7 月の彼の雑誌インタビューが一部を明かしている。彼は最初、3D モデリングソフトで自分の世界を組み立てようとしたが、ディテールレベルを 500 万から 600 万ポリゴンに設定しても、求めるリアリティには届かなかった。そこでまともなハッカーなら誰でもすることをした。自分でツールを書いたのだ。Freeman は自作のプログラムで地形を手作業で彫り込み、「形をあちこち押し動かし、粘土をこねるように地形を形作って」いった。
それでも結果は柔らかすぎ、偽物っぽすぎると感じた。彼の解決策は、さらにいくつかのルーチンを書いて、数百万年分の侵食、何度かの地震災害、そして天候による絶え間ない浸食をシミュレートし、ボクセルの世界を変形させることだった。記事によれば、彼のツールの各ボクセルは高さと色だけでなく、その地点の物質密度も保持していた。岩石、土壌の種類、水でそれぞれ異なる。侵食アルゴリズムはこの密度を考慮するので、柔らかい土壌は流され、硬い岩はその場に残り、今もこれらのマップに見られる裂け目や谷が刻まれる。ただし、この追加情報は我々がデコードした .DTA ファイルには入っていない。マップが正式にリリースされる頃には、そうした地質演算はとっくに 2 枚の単純な 8 ビット画像に焼き込まれていた。各点につき色 1 バイト、高さ 1 バイトだ。

Kyle Freeman 本人によれば、この地形ツールは Voxel Space エンジンそのものと同じくらい複雑だったという。実行時に地形をレンダリングするのと同じ技術が、ポスターやその他の販促物の生成にも使われた。
結論と次の一手
これらのファイルを最初に覗いたとき、私はどちらかといえば、何か巧妙な独自圧縮に出くわすだろうと思っていた。何しろこのスタジオは地形を描く新しい方法を発明したばかりなのだから。ところがマップはごく普通の PCX 画像で、先頭 8 バイトにシグネチャが乗っているだけだった。
これこそが本当の教訓だと思う。90 年代初頭のプログラマは何でも自分で作り直したりはしなかった。アーティストがすでに手にしている形式とツール、たとえばペイントプログラムがそのまま書き出す PCX ファイルを使い、賢さは本当に必要な場所、つまりレンダラに残した。そして古いファイル形式をリバースエンジニアリングするときの進め方は、ほとんどいつも同じだ。先頭のバイトを見て、サイズや個数らしきものを見つけ、こう自問する。これは当時のどの一般的な形式に似ている?
マップがただの画像だと分かってしまえば、あとは楽しい部分だ。自分の Voxel Space レンダラに読み込むもよし、DOS に戻って VGA mode 13h と元の 256 色パレットで、1992 年そのままに描画するもよし。

自作の VGA mode 13h ボクセル空間レンダラ
ネタバレ:ここまで話してきた内容をまとめて、VGA mode 13h で DOS 用のボクセル空間レンダラを実装する方法を解説する小さな講座を録ろうかと考えている。実際に作れるかどうかは、やってみてからだ。🤞
Comanche のマップファイルの速習はここまで。この記事について意見があれば、Twitter で罵ってほしい。それから、講座ページでレトロプログラミングの講演もチェックしてみてくれ。
また次回。