simutrans

画像変換システム(PNG ⇔ Binary)

概要

Simutrans の 画像変換システムは、PNG などの標準画像フォーマットと Simutrans 独自のバイナリスプライト形式の間で変換を行う仕組みです。makeobj ツールが PNG 画像を pak ファイルに変換する際、および Simutrans が pak ファイルからスプライトを読み込む際に使用されます。

このシステムは、Run-Length Encoding(RLE)圧縮特殊色処理(プレイヤーカラー、ライト)、透過処理を含む高度な画像エンコーディングを実装しています。

目的

画像変換システムは以下の目的で設計されています:


システムアーキテクチャ

コンポーネント構成

PNG 画像ファイル
    ↓
raw_image_t (生画像データ)
    ↓
image_writer_t (エンコーダ)
    ↓
image_t (RLE 圧縮スプライト)
    ↓
PAK ファイル
    ↓
image_reader_t (デコーダ)
    ↓
レンダリングシステム

関連ファイル:


画像読み込み(PNG → raw_image_t)

raw_image_t クラス

生の画像データを保持するクラスです。

class raw_image_t {
public:
    enum format_t {
        FMT_INVALID  = 0,
        FMT_RGBA8888 = 1,  // 32ビット RGBA
        FMT_RGB888   = 2,  // 24ビット RGB
        FMT_GRAY8    = 3   // 8ビット グレースケール
    };

private:
    uint8 *data;      // ピクセルデータ
    uint32 width;     // 画像幅
    uint32 height;    // 画像高さ
    uint8 fmt;        // フォーマット
    uint8 bpp;        // ビット/ピクセル
};

PNG 読み込みプロセス

1. ファイルフォーマットの検出

bool raw_image_t::read_from_file(const char *filename) {
    file_info_t finfo;
    const file_classify_status_t status = classify_image_file(filename, &finfo);

    if (status != FILE_CLASSIFY_OK) {
        return false;
    }

    switch (finfo.file_type) {
        case file_info_t::TYPE_PNG: return read_png(filename);
        case file_info_t::TYPE_BMP: return read_bmp(filename);
        case file_info_t::TYPE_PPM: return read_ppm(filename);
        default: return false;
    }
}

2. PNG ヘッダーの読み込み

bool raw_image_t::read_png_data(FILE *file) {
    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    png_infop info_ptr = png_create_info_struct(png_ptr);

    // エラーハンドリング
#ifdef PNG_SETJMP_SUPPORTED
    if (setjmp(png_jmpbuf(png_ptr))) {
        dbg->error("raw_image_t::read_png_data", "Fatal error in %s.", filename_.c_str());
        png_destroy_read_struct(&png_ptr, &info_ptr, (png_info**)0);
        return false;
    }
#endif

    // PNG ファイルの読み込み
    png_init_io(png_ptr, file);
    png_read_info(png_ptr, info_ptr);

    // IHDR チャンクから情報取得
    png_uint_32 new_width;
    png_uint_32 new_height;
    int bit_depth;
    int color_type;

    png_get_IHDR(png_ptr, info_ptr, &new_width, &new_height,
                 &bit_depth, &color_type, 0, 0, 0);
}

3. PNG データの正規化

Simutrans は RGBA8888RGB888GRAY8 のみをサポートするため、PNG データを変換します:

/* 16 ビット/色を 8 ビットに削減 */
png_set_strip_16(png_ptr);

/* 1、2、4 ビットを 8 ビットに展開 */
png_set_packing(png_ptr);

/* パレットを RGB に展開 */
if (color_type == PNG_COLOR_TYPE_PALETTE) {
    png_set_expand(png_ptr); // tRNS をアルファに変換

    if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
        // アルファチャンネルがない場合は 0xFF(不透明)を追加
        png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
    }

    color_type = PNG_COLOR_TYPE_RGBA;
}
else if (color_type == PNG_COLOR_TYPE_RGB) {
    // RGB に不透明アルファチャンネルを追加
    png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
    color_type = PNG_COLOR_TYPE_RGBA;
}
else if (color_type == PNG_COLOR_TYPE_GA) {
    // グレースケール+アルファはアルファを削除
    png_set_strip_alpha(png_ptr);
    color_type = PNG_COLOR_TYPE_GRAY;
}

4. ピクセルデータの読み込み

// 行ポインタの配列を作成
png_bytep *row_pointers = MALLOCN(png_bytep, new_height);

row_pointers[0] = MALLOCN(png_byte, rowbytes * new_height);
for (uint32 row = 1; row < new_height; row++) {
    row_pointers[row] = row_pointers[row - 1] + rowbytes;
}

/* 画像全体を一度に読み込み */
png_read_image(png_ptr, row_pointers);

// フォーマットを決定
format_t new_fmt = FMT_INVALID;

switch (color_type) {
    case PNG_COLOR_TYPE_RGBA: new_fmt = FMT_RGBA8888; break;
    case PNG_COLOR_TYPE_RGB:  new_fmt = FMT_RGB888;   break;
    case PNG_COLOR_TYPE_GRAY: new_fmt = FMT_GRAY8;    break;
}

// メモリに格納
uint8 *dst = data;
for (uint32 y = 0; y < height; y++) {
    for (uint32 x = 0; x < width * (bpp/CHAR_BIT); x++) {
        *dst++ = row_pointers[y][x];
    }
}

free(row_pointers[0]);
free(row_pointers);
png_read_end(png_ptr, info_ptr);
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);

画像エンコーディング(raw_image_t → image_t)

ピクセル取得

生画像からピクセルを読み取り、内部 RGB 形式に変換します:

uint32 image_writer_t::block_getpix(int x, int y) {
    const uint8 *pixel_data = input_img.access_pixel(x, y);

    switch (input_img.get_format()) {
        case raw_image_t::FMT_GRAY8: {
            const uint8 gray_level = pixel_data[0];
            return
                gray_level <<  0 |  // B
                gray_level <<  8 |  // G
                gray_level << 16;   // R
        }
        case raw_image_t::FMT_RGBA8888: {
            const uint32 pixel =
                (pixel_data[2] <<  0) + // B
                (pixel_data[1] <<  8) + // G
                (pixel_data[0] << 16) + // R
                (pixel_data[3] << 24);  // A

            // アルファチャンネルを反転(0 = 不透明)
            return pixel ^ 0xFF000000;
        }
        case raw_image_t::FMT_RGB888: {
            return
                (pixel_data[2] <<  0) | // B
                (pixel_data[1] <<  8) | // G
                (pixel_data[0] << 16);  // R
        }
    }
}

注意: Simutrans は内部的に アルファ値を反転します(0 = 不透明、255 = 透明)。


色変換(RGB → PIXVAL)

特殊色の定義

Simutrans には 31 種類の特殊色が定義されています:

const uint32 image_t::rgbtab[SPECIAL] = {
    // プレイヤーカラー 1 (8 段階)
    0x244B67, 0x395E7C, 0x4C7191, 0x6084A7,
    0x7497BD, 0x88ABD3, 0x9CBEE9, 0xB0D2FF,

    // プレイヤーカラー 2 (8 段階)
    0x7B5803, 0x8E6F04, 0xA18605, 0xB49D07,
    0xC6B408, 0xD9CB0A, 0xECE20B, 0xFFF90D,

    // ライトと窓(15 種類)
    0x57656F, // ダークウィンドウ(夜は黄色く光る)
    0x7F9BF1, // ライトウィンドウ(夜は青く光る)
    0xFFFF53, // 黄色ライト
    0xFF211D, // 赤ライト
    0x01DD01, // 緑ライト
    0x6B6B6B, 0x9B9B9B, 0xB3B3B3, 0xC9C9C9, 0xDFDFDF, // グレー(メニュー用、暗くならない)
    0xE3E3FF, // ほぼ白(夜は黄色)
    0xC1B1D1, // ウィンドウ(黄色く光る)
    0x4D4D4D, // ウィンドウ(黄色く光る)
    0xFF017F, // 紫ライト
    0x0101FF, // 青ライト
};

pixrgb_to_pixval() 関数

RGB 値を Simutrans の 16 ビット PIXVAL 形式に変換します:

static uint16 pixrgb_to_pixval(uint32 rgb) {
    uint16 pix;

    // アルファ値を確認(透明度の閾値)
    #define ALPHA_THRESHOLD (0xF8000000u)
    assert(rgb < ALPHA_THRESHOLD);

    // アルファを 32 段階に変換(Simutrans は反転形式)
    int alpha = 30 - (rgb >> 24) / 8; // 0 ... 30

    // 半透明ピクセル(アルファ > 0)
    if (rgb > 0x00FFFFFF) {
        // 特殊色(プレイヤーカラー、ライト)かチェック
        for (int i = 0; i < SPECIAL; i++) {
            if (image_t::rgbtab[i] == (uint32)(rgb & 0x00FFFFFF)) {
                // 特殊色 + 透明度
                pix = 0x8020 + i * 31 + alpha;
                return endian(pix);
            }
        }

        // 一般的な色 + 透明度(RGB 334 フォーマット)
        // R: 3 ビット、G: 4 ビット、B: 3 ビット
        pix = ((rgb >> 14) & 0x0380) | // R
              ((rgb >>  9) & 0x0078) | // G
              ((rgb >>  5) & 0x0007);  // B
        pix = 0x8020 + 31 * 31 + pix * 31 + alpha;
        return pix;
    }

    // 完全不透明ピクセル

    // 特殊色かチェック
    for (int i = 0; i < SPECIAL; i++) {
        if (image_t::rgbtab[i] == (uint32)rgb) {
            pix = 0x8000 + i;
            return pix;
        }
    }

    // 一般的な色(RGB 555 フォーマット)
    const int r = (rgb >> 16);
    const int g = (rgb >>  8) & 0xFF;
    const int b = (rgb >>  0) & 0xFF;

    // RGB 555: R5G5B5
    pix = ((r & 0xF8) << 7) | ((g & 0xF8) << 2) | ((b & 0xF8) >> 3);
    return pix;
}

PIXVAL エンコーディング形式

16 ビット PIXVAL の構造:

不透明な一般色:
  0b0RRRRRGGGGGBBBBB  (RGB555)

不透明な特殊色:
  0b1000000000000000 + 特殊色インデックス (0-30)

半透明な特殊色:
  0b1000000000100000 + 特殊色インデックス * 31 + アルファ値 (0-30)

半透明な一般色:
  0b1000000000100000 + 31 * 31 + RGB334 * 31 + アルファ値 (0-30)

メモリマップ:


Run-Length Encoding(RLE)圧縮

RLE アルゴリズム

Simutrans は行ごとの RLE 圧縮を使用してスプライトデータを圧縮します。

エンコード形式

各行は以下の形式で保存されます:

[透明ピクセル数] [色付きピクセル数 | フラグ] [色データ...] [透明...] [色付き...] ... [0]

エンコードの実装

uint16 *image_writer_t::encode_image(int x, int y, dimension* dim, int* len) {
    int line;
    uint16 *dest;
    uint16 *dest_base = new uint16[img_size * img_size * 2];
    uint16 *colored_run_counter;

    dest = dest_base;

    x += dim->xmin;
    y += dim->ymin;

    const int img_width  = dim->xmax - dim->xmin + 1;
    const int img_height = dim->ymax - dim->ymin + 1;

    for (line = 0; line < img_height; line++) {
        int row_px_count = 0;
        uint16 clear_colored_run_pair_count = 0;

        uint32 pix = block_getpix(x + row_px_count, y + line);
        row_px_count++;

        do { // 1 行を読み取る
            uint16 count = 0;

            // 透明ピクセルを読み取る
            while (is_transparent(pix)) {
                count++;
                if (row_px_count >= img_width) {
                    break;
                }
                pix = block_getpix(x + row_px_count, y + line);
                row_px_count++;
            }
            // 透明ピクセル数を書き込み
            *dest++ = endian(count);

            // 色付きピクセル数を書き込む位置を記憶
            colored_run_counter = dest++;
            count = 0;

            PIXVAL has_transparent = 0;
            while (!is_transparent(pix)) {
                // 色付きピクセルを書き込み
                PIXVAL pixval = pixrgb_to_pixval(pix);

                // 半透明ピクセルの検出
                if (pixval >= 0x8020 && !has_transparent) {
                    // 新しいランを開始(半透明フラグを設定)
                    if (count) {
                        *colored_run_counter = endian(uint16(count));
                        *dest++ = endian(uint16(0x8000)); // セパレータ
                        colored_run_counter = dest++;
                        count = 0;
                    }
                    has_transparent = 0x8000;
                }
                else if (pixval < 0x8020 && has_transparent) {
                    // 不透明ピクセルに戻る
                    if (count) {
                        *colored_run_counter = endian(uint16(count + has_transparent));
                        *dest++ = endian(uint16(0x8000)); // セパレータ
                        colored_run_counter = dest++;
                        count = 0;
                    }
                    has_transparent = 0;
                }

                *dest++ = endian(uint16(pixval));
                count++;

                if (row_px_count >= img_width) {
                    break;
                }
                pix = block_getpix(x + row_px_count, y + line);
                row_px_count++;
            }

            // 色付きランが空の場合は削除
            if (clear_colored_run_pair_count > 0 && count == 0) {
                dest -= 2;
            }
            else {
                *colored_run_counter = endian(uint16(count + has_transparent));
                clear_colored_run_pair_count++;
            }
        } while (row_px_count < img_width);

        *dest++ = 0; // 行の終端
    }

    *len = dest - dest_base;

    return dest_base;
}

RLE 圧縮の例

元の画像データ(1 行)

[透明][透明][赤][赤][赤][透明][青][透明][透明]

エンコード後のデータ

[2]          // 透明ピクセル 2 個
[3]          // 色付きピクセル 3 個
[赤PIXVAL]   // 赤
[赤PIXVAL]   // 赤
[赤PIXVAL]   // 赤
[1]          // 透明ピクセル 1 個
[1]          // 色付きピクセル 1 個
[青PIXVAL]   // 青
[2]          // 透明ピクセル 2 個
[0]          // 行の終端
[0]          // 行の終端

透明度の判定

is_transparent() 関数

inline bool is_transparent(const uint32 pix) {
    return (pix & 0x00FFFFFF) == SPECIAL_TRANSPARENT  // 特殊透明色
           || (pix >= ALPHA_THRESHOLD);               // 高アルファ値
}

#define SPECIAL_TRANSPARENT (0x00E7FFFF)
#define ALPHA_THRESHOLD (0xF8000000u)

透明判定:

  1. RGB 値が 0x00E7FFFF (マゼンタ相当) → レガシー透明色
  2. アルファ値 ≥ 248 (約 97% 透明) → 透明として扱う

バウンディングボックスの計算

init_dim() 関数

画像の実際の描画領域(透明でない部分)を計算します:

static void init_dim(uint32 *image, dimension *dim, int img_size) {
    int x, y;
    bool found = false;

    dim->ymin = dim->xmin = img_size;
    dim->ymax = dim->xmax = 0;

    for (y = 0; y < img_size; y++) {
        for (x = 0; x < img_size; x++) {
            if (!is_transparent(image[x + y * img_size])) {
                if (x < dim->xmin) dim->xmin = x;
                if (y < dim->ymin) dim->ymin = y;
                if (x > dim->xmax) dim->xmax = x;
                if (y > dim->ymax) dim->ymax = y;
                found = true;
            }
        }
    }

    if (!found) {
        // 完全に透明な画像
        dim->xmin = 1;
        dim->ymin = 1;
    }
}

用途:


画像デコーディング(PAK → メモリ)

image_reader_t

PAK ファイルから画像データを読み込みます。

obj_desc_t *image_reader_t::read_node(FILE *fp, obj_node_info_t &node) {
    array_tpl<char> desc_buf(node.size);
    if (fread(desc_buf.begin(), node.size, 1, fp) != 1) {
        return NULL;
    }

    char *p = desc_buf.begin() + 6;

    // バージョンを読み取る
    uint8 version = decode_uint8(p);
    p = desc_buf.begin();

    image_t *desc = new image_t();

    if (version == 0) {
        // 旧フォーマット(8 ビットサイズ)
        desc->x = decode_uint8(p);
        desc->w = decode_uint8(p);
        desc->y = decode_uint8(p);
        desc->h = decode_uint8(p);
        desc->alloc(decode_uint32(p)); // len
        desc->imageid = IMG_EMPTY;
        p += 2; // dummys
        desc->zoomable = decode_uint8(p);

        uint16* dest = desc->data;
        p = desc_buf.begin() + 12;

        if (desc->h > 0) {
            for (uint i = 0; i < desc->len; i++) {
                uint16 data = decode_uint16(p);
                // 旧バージョンのプレイヤーカラーオフセットを修正
                if (data >= 0x8000u && data <= 0x800Fu) {
                    data++;
                }
                *dest++ = data;
            }
        }
    }
    else if (version <= 2) {
        // 中間フォーマット(16 ビットサイズ)
        desc->x = decode_sint16(p);
        desc->y = decode_sint16(p);
        desc->w = decode_uint8(p);
        desc->h = decode_uint8(p);
        p++; // skip version information
        desc->alloc(decode_uint16(p)); // len
        desc->zoomable = decode_uint8(p);
        desc->imageid = IMG_EMPTY;

        uint16* dest = desc->data;
        if (desc->h > 0) {
            for (uint i = 0; i < desc->len; i++) {
                *dest++ = decode_uint16(p);
            }
        }
    }
    else if (version == 3) {
        // 現行フォーマット
        desc->x = decode_sint16(p);
        desc->y = decode_sint16(p);
        desc->w = decode_sint16(p);
        p++; // skip version information
        desc->h = decode_sint16(p);
        desc->alloc((node.size - 10) / 2); // len
        desc->zoomable = decode_uint8(p);
        desc->imageid = IMG_EMPTY;

        uint16* dest = desc->data;
        if (desc->h > 0) {
            for (uint i = 0; i < desc->len; i++) {
                *dest++ = decode_uint16(p);
            }
        }
    }

    return desc;
}

PAK ファイル構造

ノード構造

PAK ファイルは階層的なノード構造を持ちます:

obj_node_t {
    uint32 type;        // ノードタイプ(画像、車両、建物など)
    uint32 size;        // データサイズ
    uint16 children;    // 子ノード数
    uint16 reserved;
    uint8 data[...];    // ノードデータ
}

画像ノードのデータ構造(version 3)

struct image_node_t {
    sint16 x;          // X オフセット
    sint16 y;          // Y オフセット
    sint16 w;          // 幅
    uint8  version;    // バージョン番号
    sint16 h;          // 高さ
    uint8  zoomable;   // ズーム可能フラグ
    uint16 data[...];  // RLE 圧縮ピクセルデータ
};

合計サイズ: 10 バイト + データ長


エンディアン変換

endian() マクロ

#ifdef SIM_BIG_ENDIAN
#define endian(x) (uint16)((((x) & 0xFF) << 8) | (((x) >> 8) & 0xFF))
#else
#define endian(x) (x)
#endif

PAK ファイルはリトルエンディアン形式で保存されます。ビッグエンディアンシステム(PowerPC Mac など)では自動的に変換されます。


最適化とパフォーマンス

1. 画像キャッシング

std::string image_writer_t::last_img_file;
raw_image_t image_writer_t::input_img;

bool image_writer_t::block_load(const char *fname) {
    // 最後に読み込んだファイルをキャッシュ
    if (last_img_file == fname) {
        return true;
    }
    else if (load_image_from_file(fname)) {
        // タイルサイズの確認
        if ((input_img.get_width() % img_size != 0) ||
            (input_img.get_height() % img_size != 0)) {
            dbg->error("image_writer_t::block_load",
                       "Cannot load image file '%s': Size not divisible by %d.",
                       fname, img_size);
            last_img_file = "";
            return false;
        }

        last_img_file = fname;
        return true;
    }

    last_img_file = "";
    return false;
}

利点:

2. RLE 圧縮率

典型的な Simutrans スプライトの圧縮率:

:

64×64 ピクセル = 4096 ピクセル
未圧縮: 4096 × 2 バイト = 8192 バイト
RLE 圧縮後: 1000-2000 バイト(平均)
圧縮率: 約 75-87%

3. メモリ配置

// ギャップなしのメモリ配置
access_pixel(x, y+1) == access_pixel(x, y) + get_width() * (get_bpp()/CHAR_BIT)

利点:


デバッグとトラブルシューティング

DEBUG モードの出力

makeobj DEBUG PAK output.pak input.dat

出力例:

Source:  Obj=vehicle
Source:  Name=bus_example
Image:   buses.png
X:       0
Y:       0
Off X:   -20
Off Y:   -30
Width:   64
Height:  32
Zoom:    1
Encoding image...
  Line 0: 5 clear, 15 colored, 10 clear, 20 colored
  Line 1: 3 clear, 25 colored, 8 clear
  ...
Encoded length: 1234 bytes

一般的なエラー

1. アルファチャンネルの問題

症状: 画像が真っ黒になる

原因: アルファチャンネルが反転している

対処法:

2. 特殊色の誤認識

症状: プレイヤーカラーが正しく機能しない

原因: RGB 値が特殊色テーブルと厳密に一致していない

対処法:

3. 画像サイズエラー

エラー: Size not divisible by 64

原因: 画像サイズがタイルサイズの倍数でない

対処法:

# PAK64 の場合、画像サイズは 64 の倍数である必要がある
# 例: 64×64, 128×64, 64×128, 128×128

# PAK128 の場合
makeobj PAK128 output.pak input.dat
# 画像サイズは 128 の倍数

高度なトピック

プレイヤーカラーのマスク

プレイヤーカラーは 8 段階の明るさを持ちます:

// プレイヤーカラー 1(青系)
0x244B67,  // 最も暗い
0x395E7C,
0x4C7191,
0x6084A7,
0x7497BD,
0x88ABD3,
0x9CBEE9,
0xB0D2FF,  // 最も明るい

使用方法:

  1. 車両や建物の PNG に特殊色を使用
  2. ゲーム内でプレイヤーの選択した色に動的に置き換え
  3. 夜間モードでは明るさを調整

ライトエフェクト

0xFFFF53, // 黄色ライト → 夜間に明るく光る
0xFF211D, // 赤ライト   → 信号機、テールライト
0x01DD01, // 緑ライト   → 信号機

仕組み:

窓の表現

0x57656F, // ダークウィンドウ → 夜間に黄色く光る
0x7F9BF1, // ライトウィンドウ → 夜間に青く光る

使い方:


パフォーマンス測定

エンコード速度

典型的なベンチマーク(現代的な CPU):

64×64 画像:   0.1-0.5 ms / 画像
128×128 画像: 0.5-2 ms / 画像
256×256 画像: 2-8 ms / 画像

Pakset 全体(10,000 画像): 5-30 秒

メモリ使用量

// 生画像データ(PNG 読み込み後)
64×64 RGBA:  64 × 64 × 4 = 16,384 バイト

// RLE 圧縮後(典型的な圧縮率 75%)
64×64 RLE:    1,000-2,000 バイト

// 圧縮率: 87-93%

PNG 書き出し(逆変換)

Simutrans は PAK ファイルから PNG への変換もサポートしています(デバッグ用):

bool raw_image_t::write_png(const char *file_name) const {
    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    png_infop info_ptr = png_create_info_struct(png_ptr);

    FILE *fp = dr_fopen(file_name, "wb");
    if (!fp) {
        return false;
    }

    png_init_io(png_ptr, fp);

#if PNG_LIBPNG_VER_MAJOR <= 1 && PNG_LIBPNG_VER_MINOR < 5
    // 最高の圧縮レベルを設定
    png_set_compression_level(png_ptr, Z_BEST_COMPRESSION);
#endif

    // ヘッダーを出力
    int color_type;

    switch (fmt) {
        case FMT_RGBA8888: color_type = PNG_COLOR_TYPE_RGBA; break;
        case FMT_RGB888:   color_type = PNG_COLOR_TYPE_RGB;  break;
        case FMT_GRAY8:    color_type = PNG_COLOR_TYPE_GRAY; break;
        default:
            dbg->fatal("raw_image_t::write_png", "Unsupported source format");
    }

    png_set_IHDR(png_ptr, info_ptr, width, height, 8, color_type,
                 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
                 PNG_FILTER_TYPE_DEFAULT);
    png_write_info(png_ptr, info_ptr);

    // ピクセルデータを書き込み
    for (uint32 y = 0; y < height; ++y) {
        const uint8 *row = access_pixel(0, y);
        png_write_row(png_ptr, const_cast<png_bytep>(row));
    }

    png_write_end(png_ptr, info_ptr);
    png_destroy_write_struct(&png_ptr, &info_ptr);

    fclose(fp);
    return true;
}

関連ファイル

画像処理

エンコーディング/デコーディング

ツール


参考リンク


まとめ

Simutrans の画像変換システムは、効率性と柔軟性を兼ね備えた高度な設計になっています:

主な特徴:

この変換システムにより、Simutrans は数万枚のスプライトを効率的に管理し、リアルタイムで滑らかなゲーム体験を提供しています。

Pakset 制作者は、このシステムを理解することで、より最適化された、視覚的に美しいコンテンツを作成できます。