スプライトと画像処理

画像フォーマットの歴史

デジタル画像の誕生

1957年、Russell KirschはNBS(現NIST)で世界初のデジタル画像スキャナを開発し、息子Waldenの写真をデジタル化しました。この176×176ピクセルの画像は、デジタル画像処理の出発点となりました。

Russell Kirschの最初のデジタル画像(1957年)

解像度: 176 × 176 ピクセル
階調: 1ビット(白黒)
総ピクセル数: 30,976

デジタル画像の基本構造:
┌─────────────────────────────────────┐
│ Image = 2D Array of Pixels          │
│                                     │
│ pixel[y][x] = color_value           │
│                                     │
│ ┌───┬───┬───┬───┬───┐             │
│ │0,0│0,1│0,2│0,3│...│             │
│ ├───┼───┼───┼───┼───┤             │
│ │1,0│1,1│1,2│1,3│...│             │
│ ├───┼───┼───┼───┼───┤             │
│ │...│...│...│...│...│             │
│ └───┴───┴───┴───┴───┘             │
└─────────────────────────────────────┘

X Window Systemと画像フォーマット

1984年、MITで開発されたX Window Systemは、ネットワーク透過なウィンドウシステムとして設計されました。このシステムのために、いくつかの画像フォーマットが開発されました。

X Window System の画像フォーマット

1. XBM (X BitMap) - 1985年
   - 1ビット(白黒)画像
   - Cソースコード形式
   - カーソルやアイコン用

2. XPM (X PixMap) - 1989年
   - カラー画像対応
   - Cソースコード形式
   - 透明度サポート

3. XWD (X Window Dump)
   - スクリーンショット用
   - バイナリ形式

進化:
XBM (1-bit) → XPM1 → XPM2 → XPM3 (現在)

XPM形式の理論

フォーマット構造

XPM(X PixMap)は、Couchot Danielによって1989年に設計されました。Cソースコードとして直接コンパイル可能な点が特徴です。

XPM3フォーマットの構造

/* XPM */
static char *name[] = {
  /* ヘッダー: 幅 高さ 色数 文字数/ピクセル */
  "width height ncolors cpp",

  /* カラーテーブル: 各色の定義 */
  "chars c color",  /* c = color */
  "chars s symbol", /* s = symbolic name */
  "chars m mono",   /* m = monochrome */
  "chars g gray",   /* g = grayscale */

  /* ピクセルデータ: 各行 */
  "pixels...",
  "pixels...",
  ...
};

形式的な定義:
XPM ::= "/* XPM */" NEWLINE
        "static char *" NAME "[] = {" NEWLINE
        HEADER NEWLINE
        COLORS NEWLINE
        PIXELS NEWLINE
        "};"

HEADER ::= "\"" WIDTH " " HEIGHT " " NCOLORS " " CPP "\""
COLORS ::= ("\"" CHARS " c " COLOR "\"" NEWLINE)*
PIXELS ::= ("\"" (CHARS)* "\"" NEWLINE)*

色指定の方式

XPMでは複数の色指定方式がサポートされています:

/* 色指定の例 */

/* 16進RGB */
". c #FF0000",      /* 赤 */
"# c #00FF00",      /* 緑 */
"@ c #0000FF",      /* 青 */

/* X11カラーネーム */
"+ c red",          /* 赤 */
"= c green",        /* 緑 */
"- c blue",         /* 青 */

/* 特殊値 */
"  c None",         /* 透明 */

/* HSV形式(一部の実装) */
"* c %H120S100V100", /* 緑 */

色彩理論の基礎

RGBカラーモデル

1931年、CIE(国際照明委員会)は人間の色覚に基づいた色彩空間を標準化しました。現代のディスプレイで使用されるRGBモデルはこの研究に基づいています。

RGBカラーモデル

加法混色(Additive Color Mixing):
光を混合して色を作る

     Red (255,0,0)
         ╲
          ╲   Yellow (255,255,0)
           ╲ ╱
            ╳     White (255,255,255)
           ╱ ╲
          ╱   Cyan (0,255,255)
         ╱
    Green (0,255,0)───────Blue (0,0,255)
              Magenta (255,0,255)

24ビットカラー:
┌────────┬────────┬────────┐
│   R    │   G    │   B    │
│ 8 bits │ 8 bits │ 8 bits │
└────────┴────────┴────────┘
    = 256 × 256 × 256
    = 16,777,216 色

32ビットカラー(ARGB):
┌────────┬────────┬────────┬────────┐
│ Alpha  │   R    │   G    │   B    │
│ 8 bits │ 8 bits │ 8 bits │ 8 bits │
└────────┴────────┴────────┴────────┘

色の表現

// 色の定数定義

// 16進数形式(0xAARRGGBB)
#define COLOR_BLACK     0x00000000
#define COLOR_WHITE     0x00FFFFFF
#define COLOR_RED       0x00FF0000
#define COLOR_GREEN     0x0000FF00
#define COLOR_BLUE      0x000000FF

// RGBからint変換
int rgb_to_int(int r, int g, int b)
{
    return ((r << 16) | (g << 8) | b);
}

// ARGBからint変換
int argb_to_int(int a, int r, int g, int b)
{
    return ((a << 24) | (r << 16) | (g << 8) | b);
}

// intから各成分を抽出
int get_red(int color)   { return ((color >> 16) & 0xFF); }
int get_green(int color) { return ((color >> 8) & 0xFF); }
int get_blue(int color)  { return (color & 0xFF); }
int get_alpha(int color) { return ((color >> 24) & 0xFF); }

スプライトの歴史

ハードウェアスプライトの誕生

1970年代後半、ビデオゲームの開発者たちは、背景とは独立して動く画像オブジェクトを効率的に処理する必要がありました。Texas Instruments TMS9918(1979年)は、ハードウェアレベルでスプライトをサポートした最初のビデオチップの一つです。

ハードウェアスプライトの原理

従来のアプローチ(ソフトウェア描画):
1. 背景を描画
2. スプライト位置に背景を保存
3. スプライトを描画
4. 移動時:背景を復元 → 新位置に描画
→ CPU負荷が高い

ハードウェアスプライト:
1. スプライトデータをVRAMに格納
2. スプライトレジスタで位置を指定
3. ハードウェアが自動的に合成
→ CPU負荷が軽い

┌─────────────────────────────────────┐
│           Video Output              │
├─────────────────────────────────────┤
│      ┌──────────────────────┐      │
│      │   Sprite Overlay     │      │
│      │   (Hardware Layer)   │      │
│      └──────────────────────┘      │
│              ↓ Composite            │
│      ┌──────────────────────┐      │
│      │   Background Layer   │      │
│      └──────────────────────┘      │
└─────────────────────────────────────┘

タイルベースグラフィックス

メモリが限られた時代、タイルベースのアプローチは効率的なグラフィックス表現を可能にしました。

タイルベースグラフィックスの原理

メモリ効率:
- 全画面を個別ピクセルで持つ: 320×240×8bit = 76,800 bytes
- タイルベース: 40×30 タイルマップ + 256タイル = 1,200 + 16,384 = 17,584 bytes
- 約77%のメモリ削減

タイルセット(8×8ピクセル):
┌───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ ...
├───┼───┼───┼───┤
│ 4 │ 5 │ 6 │ 7 │ ...
└───┴───┴───┴───┘

タイルマップ:
┌───┬───┬───┬───┬───┐
│ 1 │ 1 │ 1 │ 1 │ 1 │
├───┼───┼───┼───┼───┤
│ 1 │ 0 │ 2 │ 0 │ 1 │
├───┼───┼───┼───┼───┤
│ 1 │ 3 │ 0 │ 4 │ 1 │
└───┴───┴───┴───┴───┘

So_longでのタイルマッピング:
'1' → wall.xpm (タイルID: 0)
'0' → floor.xpm (タイルID: 1)
'P' → player.xpm (タイルID: 2)
'C' → collectible.xpm (タイルID: 3)
'E' → exit.xpm (タイルID: 4/5)

グラフィックスパイプライン

2Dレンダリングパイプライン

現代のグラフィックスシステムは、パイプラインアーキテクチャで処理を行います。

2Dレンダリングパイプライン

1. 入力処理
   ┌─────────────────────┐
   │ Game State Update   │
   │ ゲーム状態の更新    │
   └──────────┬──────────┘
              ▼
2. ジオメトリ処理
   ┌─────────────────────┐
   │ Transform & Cull    │
   │ 座標変換・カリング   │
   └──────────┬──────────┘
              ▼
3. ラスタライズ
   ┌─────────────────────┐
   │ Rasterization       │
   │ ピクセル変換        │
   └──────────┬──────────┘
              ▼
4. フラグメント処理
   ┌─────────────────────┐
   │ Texture & Blend     │
   │ テクスチャ・ブレンド │
   └──────────┬──────────┘
              ▼
5. 出力
   ┌─────────────────────┐
   │ Framebuffer Write   │
   │ フレームバッファ書込 │
   └─────────────────────┘

ダブルバッファリング

ダブルバッファリングは、画面のちらつき(テアリング)を防ぐ技術です。

ダブルバッファリングの原理

シングルバッファ(問題あり):
┌─────────────────────┐
│    Framebuffer      │←─ 描画中に表示
│    (表示と描画が同じ) │←─ テアリング発生
└─────────────────────┘

ダブルバッファ(解決策):
┌─────────────────────┐
│   Front Buffer      │←─ 現在表示中
│   (表示用)          │
└─────────────────────┘
         ↕ Swap(描画完了時に交換)
┌─────────────────────┐
│   Back Buffer       │←─ 描画先
│   (描画用)          │
└─────────────────────┘

処理フロー:
1. Back Bufferに描画
2. 描画完了を待機(VSync)
3. Front/Back をスワップ
4. 1に戻る

XPMファイルの実装

基本的なXPMファイル構造

/* wall.xpm - 32×32の壁タイル */

/* XPM */
static char *wall_xpm[] = {
/* width height ncolors cpp */
"32 32 4 1",
/* colors */
"  c #4A4A4A",   /* 濃いグレー(影) */
". c #6A6A6A",   /* グレー(基本色) */
"+ c #8A8A8A",   /* 明るいグレー(ハイライト) */
"# c #3A3A3A",   /* 最も暗い部分 */
/* pixels */
"++++++++++++++++++++++++++++++++",
"+..............................+",
"+..............................+",
"+..####..####..####..####..##..+",
"+..#  #..#  #..#  #..#  #..# ..+",
"+..#  #..#  #..#  #..#  #..# ..+",
"+..####..####..####..####..##..+",
"+..............................+",
"+..............................+",
"+..##..####..####..####..####..+",
"+..# ..#  #..#  #..#  #..#  #..+",
"+..# ..#  #..#  #..#  #..#  #..+",
"+..##..####..####..####..####..+",
"+..............................+",
"+..............................+",
"+..####..####..####..####..##..+",
"+..#  #..#  #..#  #..#  #..# ..+",
"+..#  #..#  #..#  #..#  #..# ..+",
"+..####..####..####..####..##..+",
"+..............................+",
"+..............................+",
"+..##..####..####..####..####..+",
"+..# ..#  #..#  #..#  #..#  #..+",
"+..# ..#  #..#  #..#  #..#  #..+",
"+..##..####..####..####..####..+",
"+..............................+",
"+..............................+",
"+..####..####..####..####..##..+",
"+..#  #..#  #..#  #..#  #..# ..+",
"+..#  #..#  #..#  #..#  #..# ..+",
"+..####..####..####..####..##..+",
"++++++++++++++++++++++++++++++++",
};

プレイヤースプライト

/* player.xpm - プレイヤーキャラクター */

/* XPM */
static char *player_xpm[] = {
"32 32 5 1",
"  c None",        /* 透明 */
". c #FFE4B5",     /* 肌色 */
"+ c #4169E1",     /* 青(服) */
"# c #000000",     /* 黒(輪郭) */
"@ c #8B4513",     /* 茶(靴) */
/* pixels - シンプルなキャラクター */
"                                ",
"                                ",
"            ######              ",
"          ##......##            ",
"         #..........#           ",
"        #....##..##..#          ",
"        #....##..##..#          ",
"        #............#          ",
"         #...####...#           ",
"          ##......##            ",
"            ######              ",
"         ####++####             ",
"        #+++++++++++#           ",
"       #++++++++++++++#         ",
"      #++++++++++++++++#        ",
"      #++++++++++++++++#        ",
"      #++++++++++++++++#        ",
"       #++++++++++++++#         ",
"        #++++++++++++#          ",
"         ##++++++++##           ",
"           ########             ",
"          ##      ##            ",
"         #++#    #++#           ",
"         #++#    #++#           ",
"         #++#    #++#           ",
"         #++#    #++#           ",
"         ####    ####           ",
"        #@@@@#  #@@@@#          ",
"        #@@@@#  #@@@@#          ",
"         ####    ####           ",
"                                ",
"                                ",
};

MiniLibXでの画像処理

テクスチャ管理構造体

typedef struct s_texture
{
    void    *img_ptr;   // MiniLibX画像ポインタ
    char    *addr;      // ピクセルデータアドレス
    int     bpp;        // bits per pixel
    int     line_len;   // 1行のバイト数
    int     endian;     // エンディアン
    int     width;      // 画像幅
    int     height;     // 画像高さ
}   t_texture;

typedef struct s_textures
{
    t_texture   wall;
    t_texture   floor;
    t_texture   player;
    t_texture   collectible;
    t_texture   exit_open;
    t_texture   exit_closed;
    int         tile_size;
}   t_textures;

画像読み込み関数

// 単一テクスチャの読み込み
int load_texture(void *mlx, t_texture *tex, char *path)
{
    tex->img_ptr = mlx_xpm_file_to_image(mlx, path,
                                         &tex->width, &tex->height);
    if (!tex->img_ptr)
        return (0);

    tex->addr = mlx_get_data_addr(tex->img_ptr,
                                   &tex->bpp,
                                   &tex->line_len,
                                   &tex->endian);
    return (1);
}

// 全テクスチャの読み込み
int load_all_textures(t_game *game)
{
    void *mlx;

    mlx = game->mlx_ptr;

    if (!load_texture(mlx, &game->tex.wall, "textures/wall.xpm"))
        return (error_msg("Failed to load wall.xpm"));

    if (!load_texture(mlx, &game->tex.floor, "textures/floor.xpm"))
    {
        destroy_textures(game);
        return (error_msg("Failed to load floor.xpm"));
    }

    if (!load_texture(mlx, &game->tex.player, "textures/player.xpm"))
    {
        destroy_textures(game);
        return (error_msg("Failed to load player.xpm"));
    }

    if (!load_texture(mlx, &game->tex.collectible, "textures/collectible.xpm"))
    {
        destroy_textures(game);
        return (error_msg("Failed to load collectible.xpm"));
    }

    if (!load_texture(mlx, &game->tex.exit_closed, "textures/exit_closed.xpm"))
    {
        destroy_textures(game);
        return (error_msg("Failed to load exit_closed.xpm"));
    }

    if (!load_texture(mlx, &game->tex.exit_open, "textures/exit_open.xpm"))
    {
        destroy_textures(game);
        return (error_msg("Failed to load exit_open.xpm"));
    }

    game->tex.tile_size = game->tex.wall.width;
    return (1);
}

テクスチャの解放

void destroy_texture(void *mlx, t_texture *tex)
{
    if (tex->img_ptr)
    {
        mlx_destroy_image(mlx, tex->img_ptr);
        tex->img_ptr = NULL;
    }
}

void destroy_textures(t_game *game)
{
    void *mlx;

    mlx = game->mlx_ptr;
    destroy_texture(mlx, &game->tex.wall);
    destroy_texture(mlx, &game->tex.floor);
    destroy_texture(mlx, &game->tex.player);
    destroy_texture(mlx, &game->tex.collectible);
    destroy_texture(mlx, &game->tex.exit_open);
    destroy_texture(mlx, &game->tex.exit_closed);
}

ピクセル操作

直接ピクセルアクセス

// ピクセルの取得
unsigned int get_pixel(t_texture *tex, int x, int y)
{
    char *dst;

    if (x < 0 || x >= tex->width || y < 0 || y >= tex->height)
        return (0);

    dst = tex->addr + (y * tex->line_len + x * (tex->bpp / 8));
    return (*(unsigned int *)dst);
}

// ピクセルの設定
void put_pixel(t_texture *tex, int x, int y, unsigned int color)
{
    char *dst;

    if (x < 0 || x >= tex->width || y < 0 || y >= tex->height)
        return;

    dst = tex->addr + (y * tex->line_len + x * (tex->bpp / 8));
    *(unsigned int *)dst = color;
}

画像のコピー(ブリット)

// 画像を別の画像にコピー
void blit_image(t_texture *dst, t_texture *src, int dx, int dy)
{
    int             x;
    int             y;
    unsigned int    color;

    y = 0;
    while (y < src->height)
    {
        x = 0;
        while (x < src->width)
        {
            color = get_pixel(src, x, y);

            // 透明ピクセル(alpha = 0)はスキップ
            if ((color & 0xFF000000) != 0xFF000000)
                put_pixel(dst, dx + x, dy + y, color);

            x++;
        }
        y++;
    }
}

// 透明度を考慮したブリット
void blit_with_transparency(t_texture *dst, t_texture *src, int dx, int dy)
{
    int             x;
    int             y;
    unsigned int    src_color;
    unsigned int    dst_color;
    unsigned int    alpha;

    y = 0;
    while (y < src->height)
    {
        x = 0;
        while (x < src->width)
        {
            src_color = get_pixel(src, x, y);
            alpha = (src_color >> 24) & 0xFF;

            // 完全透明はスキップ
            if (alpha == 0)
            {
                x++;
                continue;
            }

            // 完全不透明はそのまま書き込み
            if (alpha == 255)
            {
                put_pixel(dst, dx + x, dy + y, src_color);
            }
            else
            {
                // アルファブレンディング
                dst_color = get_pixel(dst, dx + x, dy + y);
                put_pixel(dst, dx + x, dy + y,
                          blend_colors(src_color, dst_color, alpha));
            }

            x++;
        }
        y++;
    }
}

アルファブレンディング

// 色のブレンディング
unsigned int blend_colors(unsigned int src, unsigned int dst, int alpha)
{
    int sr, sg, sb;
    int dr, dg, db;
    int r, g, b;

    // 成分の抽出
    sr = (src >> 16) & 0xFF;
    sg = (src >> 8) & 0xFF;
    sb = src & 0xFF;

    dr = (dst >> 16) & 0xFF;
    dg = (dst >> 8) & 0xFF;
    db = dst & 0xFF;

    // ブレンディング式: result = src * alpha + dst * (1 - alpha)
    r = (sr * alpha + dr * (255 - alpha)) / 255;
    g = (sg * alpha + dg * (255 - alpha)) / 255;
    b = (sb * alpha + db * (255 - alpha)) / 255;

    return ((r << 16) | (g << 8) | b);
}

レンダリングシステム

タイルベースレンダリング

// タイルマップからテクスチャを選択
t_texture *select_texture(t_game *game, char tile)
{
    if (tile == WALL)
        return (&game->tex.wall);
    if (tile == PLAYER)
        return (&game->tex.player);
    if (tile == COLLECTIBLE)
        return (&game->tex.collectible);
    if (tile == EXIT)
    {
        if (game->collected == game->total_collectibles)
            return (&game->tex.exit_open);
        return (&game->tex.exit_closed);
    }
    return (&game->tex.floor);
}

// 1タイルの描画
void render_tile(t_game *game, int grid_x, int grid_y)
{
    int         pixel_x;
    int         pixel_y;
    char        tile;
    t_texture   *tex;

    pixel_x = grid_x * game->tex.tile_size;
    pixel_y = grid_y * game->tex.tile_size;
    tile = game->map.grid[grid_y][grid_x];

    // 床を先に描画(背景レイヤー)
    mlx_put_image_to_window(game->mlx_ptr, game->win_ptr,
                            game->tex.floor.img_ptr,
                            pixel_x, pixel_y);

    // 床以外のタイルを上に描画
    if (tile != FLOOR)
    {
        tex = select_texture(game, tile);
        mlx_put_image_to_window(game->mlx_ptr, game->win_ptr,
                                tex->img_ptr,
                                pixel_x, pixel_y);
    }
}

// 全画面の描画
void render_game(t_game *game)
{
    int x;
    int y;

    y = 0;
    while (y < game->map.height)
    {
        x = 0;
        while (x < game->map.width)
        {
            render_tile(game, x, y);
            x++;
        }
        y++;
    }
}

差分レンダリング(最適化)

// 変更されたタイルのみを再描画
void render_tile_update(t_game *game, int old_x, int old_y,
                        int new_x, int new_y)
{
    // 古い位置を再描画
    render_tile(game, old_x, old_y);

    // 新しい位置を再描画
    render_tile(game, new_x, new_y);
}

// プレイヤー移動時の描画更新
void update_player_render(t_game *game, int from_x, int from_y,
                          int to_x, int to_y)
{
    // 古い位置:床またはその他のタイルを描画
    char old_tile = game->map.grid[from_y][from_x];
    if (old_tile == PLAYER)
        game->map.grid[from_y][from_x] = FLOOR;
    render_tile(game, from_x, from_y);

    // 新しい位置:プレイヤーを描画
    game->map.grid[to_y][to_x] = PLAYER;
    render_tile(game, to_x, to_y);
}

バッファレンダリング

オフスクリーンバッファの作成

typedef struct s_buffer
{
    void    *img_ptr;
    char    *addr;
    int     bpp;
    int     line_len;
    int     endian;
    int     width;
    int     height;
}   t_buffer;

// バッファの初期化
int init_buffer(void *mlx, t_buffer *buf, int width, int height)
{
    buf->width = width;
    buf->height = height;
    buf->img_ptr = mlx_new_image(mlx, width, height);

    if (!buf->img_ptr)
        return (0);

    buf->addr = mlx_get_data_addr(buf->img_ptr,
                                   &buf->bpp,
                                   &buf->line_len,
                                   &buf->endian);
    return (1);
}

// バッファの破棄
void destroy_buffer(void *mlx, t_buffer *buf)
{
    if (buf->img_ptr)
    {
        mlx_destroy_image(mlx, buf->img_ptr);
        buf->img_ptr = NULL;
    }
}

バッファを使用したレンダリング

// バッファにゲームを描画
void render_to_buffer(t_game *game, t_buffer *buf)
{
    int     x;
    int     y;
    int     px;
    int     py;
    char    tile;

    y = 0;
    while (y < game->map.height)
    {
        x = 0;
        while (x < game->map.width)
        {
            tile = game->map.grid[y][x];
            px = x * game->tex.tile_size;
            py = y * game->tex.tile_size;

            // バッファに床を描画
            copy_texture_to_buffer(buf, &game->tex.floor, px, py);

            // バッファにタイルを描画
            if (tile != FLOOR)
            {
                t_texture *tex = select_texture(game, tile);
                copy_texture_to_buffer(buf, tex, px, py);
            }

            x++;
        }
        y++;
    }
}

// テクスチャをバッファにコピー
void copy_texture_to_buffer(t_buffer *buf, t_texture *tex, int dx, int dy)
{
    int             x;
    int             y;
    unsigned int    color;
    char            *dst;

    y = 0;
    while (y < tex->height && (dy + y) < buf->height)
    {
        x = 0;
        while (x < tex->width && (dx + x) < buf->width)
        {
            color = get_pixel(tex, x, y);

            // 透明でない場合のみ書き込み
            if ((color >> 24) != 0xFF)
            {
                dst = buf->addr + ((dy + y) * buf->line_len
                                   + (dx + x) * (buf->bpp / 8));
                *(unsigned int *)dst = color;
            }
            x++;
        }
        y++;
    }
}

// ダブルバッファリングを使用した描画
void render_double_buffered(t_game *game)
{
    static t_buffer buffer = {0};
    static int initialized = 0;

    // 初回のみバッファを作成
    if (!initialized)
    {
        int w = game->map.width * game->tex.tile_size;
        int h = game->map.height * game->tex.tile_size;
        init_buffer(game->mlx_ptr, &buffer, w, h);
        initialized = 1;
    }

    // バッファに描画
    render_to_buffer(game, &buffer);

    // バッファを画面に転送
    mlx_put_image_to_window(game->mlx_ptr, game->win_ptr,
                            buffer.img_ptr, 0, 0);
}

ピクセルアートの技法

パレット制限

レトロゲームの雰囲気を出すため、使用色を制限します:

// NES風パレット(53色から選択)
unsigned int nes_palette[] = {
    0x7C7C7C, 0x0000FC, 0x0000BC, 0x4428BC,
    0x940084, 0xA80020, 0xA81000, 0x881400,
    0x503000, 0x007800, 0x006800, 0x005800,
    0x004058, 0x000000, 0x000000, 0x000000,
    0xBCBCBC, 0x0078F8, 0x0058F8, 0x6844FC,
    0xD800CC, 0xE40058, 0xF83800, 0xE45C10,
    0xAC7C00, 0x00B800, 0x00A800, 0x00A844,
    0x008888, 0x000000, 0x000000, 0x000000,
    // ... 続き
};

// 最も近いパレット色を見つける
unsigned int quantize_to_palette(unsigned int color)
{
    int min_dist = INT_MAX;
    int best_idx = 0;
    int i;
    int r1, g1, b1, r2, g2, b2;
    int dist;

    r1 = (color >> 16) & 0xFF;
    g1 = (color >> 8) & 0xFF;
    b1 = color & 0xFF;

    i = 0;
    while (i < 53)
    {
        r2 = (nes_palette[i] >> 16) & 0xFF;
        g2 = (nes_palette[i] >> 8) & 0xFF;
        b2 = nes_palette[i] & 0xFF;

        // ユークリッド距離(近似)
        dist = (r1 - r2) * (r1 - r2)
             + (g1 - g2) * (g1 - g2)
             + (b1 - b2) * (b1 - b2);

        if (dist < min_dist)
        {
            min_dist = dist;
            best_idx = i;
        }
        i++;
    }

    return (nes_palette[best_idx]);
}

まとめ

この章では、スプライトと画像処理について体系的に学びました:

  • 画像フォーマットの歴史: Kirschの最初のデジタル画像からXPMまで
  • XPM形式: Cソースコードとしての画像表現
  • 色彩理論: RGB、アルファブレンディング
  • スプライトの歴史: ハードウェアスプライトとタイルベースグラフィックス
  • グラフィックスパイプライン: 2Dレンダリングの処理フロー
  • ダブルバッファリング: ティアリング防止技術
  • MiniLibX実装: テクスチャ管理、ピクセル操作、レンダリング

これらの概念は、So_longに限らず、2Dゲーム開発全般で活用できる基礎知識です。

次の章では、ゲームロジックと衝突検出について学びます。移動処理、アイテム収集、勝利条件の実装を探求します。