第3章:レイキャスティングの実装

はじめに

本章では、DDAアルゴリズムを使用したレイキャスティングの実装を学びます。

---

1. DDAアルゴリズム

1.1 概要

DDA(Digital Differential Analyzer)は、レイがグリッド境界を通過するたびに、次の境界までの距離を計算するアルゴリズムです。

      |     |     |     |
------+-----+-----+-----+------
      |     |  *  |     |
------+-----+--/--+-----+------
      |     |/    | #   |
------+-----*-----+-----+------
      |   / |     | #   |
------+--/--+-----+-----+------
      | P   |     |     |
------+-----+-----+-----+------

P = プレイヤー
* = グリッド境界との交点
# = 壁

1.2 アルゴリズムの手順

1. レイの方向を計算
2. デルタ距離を計算(1グリッド移動に必要な距離)
3. 最初のグリッド境界までの距離を計算
4. ステップ方向を決定(+1 or -1)
5. DDAループ:
   - X方向とY方向の近い方を選択
   - その方向に1グリッド進む
   - 壁に当たったら終了

---

2. 実装

2.1 データ構造

typedef struct s_ray {
    double posX;
    double posY;
    double dirX;
    double dirY;

    // 現在のマップセル
    int mapX;
    int mapY;

    // 次のX/Y境界までの距離
    double sideDistX;
    double sideDistY;

    // 1グリッド移動に必要な距離
    double deltaDistX;
    double deltaDistY;

    // ステップ方向
    int stepX;
    int stepY;

    // 壁との衝突情報
    int hit;
    int side;  // 0: 東西, 1: 南北
    double perpWallDist;
} t_ray;

2.2 レイの初期化

void init_ray(t_ray *ray, t_player *player, int x, int screenWidth) {
    // カメラ平面上の位置 (-1 to 1)
    double cameraX = 2 * x / (double)screenWidth - 1;

    // レイの方向
    ray->dirX = player->dirX + player->planeX * cameraX;
    ray->dirY = player->dirY + player->planeY * cameraX;

    // プレイヤー位置
    ray->posX = player->posX;
    ray->posY = player->posY;

    // 現在のマップセル
    ray->mapX = (int)ray->posX;
    ray->mapY = (int)ray->posY;

    // デルタ距離を計算
    // |1 / rayDir| で1グリッド移動に必要な距離
    ray->deltaDistX = (ray->dirX == 0) ? 1e30 : fabs(1 / ray->dirX);
    ray->deltaDistY = (ray->dirY == 0) ? 1e30 : fabs(1 / ray->dirY);

    ray->hit = 0;
}

2.3 ステップと初期サイド距離

void calc_step_and_side_dist(t_ray *ray) {
    if (ray->dirX < 0) {
        ray->stepX = -1;
        ray->sideDistX = (ray->posX - ray->mapX) * ray->deltaDistX;
    } else {
        ray->stepX = 1;
        ray->sideDistX = (ray->mapX + 1.0 - ray->posX) * ray->deltaDistX;
    }

    if (ray->dirY < 0) {
        ray->stepY = -1;
        ray->sideDistY = (ray->posY - ray->mapY) * ray->deltaDistY;
    } else {
        ray->stepY = 1;
        ray->sideDistY = (ray->mapY + 1.0 - ray->posY) * ray->deltaDistY;
    }
}

2.4 DDAループ

void perform_dda(t_ray *ray, char **map) {
    while (ray->hit == 0) {
        // X方向とY方向の近い方を選択
        if (ray->sideDistX < ray->sideDistY) {
            ray->sideDistX += ray->deltaDistX;
            ray->mapX += ray->stepX;
            ray->side = 0;  // 東西の壁
        } else {
            ray->sideDistY += ray->deltaDistY;
            ray->mapY += ray->stepY;
            ray->side = 1;  // 南北の壁
        }

        // 壁に当たったかチェック
        if (map[ray->mapY][ray->mapX] == '1') {
            ray->hit = 1;
        }
    }
}

2.5 垂直距離の計算

void calc_perp_wall_dist(t_ray *ray) {
    if (ray->side == 0) {
        ray->perpWallDist = ray->sideDistX - ray->deltaDistX;
    } else {
        ray->perpWallDist = ray->sideDistY - ray->deltaDistY;
    }
}

---

3. 壁の描画

3.1 壁の高さ計算

typedef struct s_draw {
    int lineHeight;
    int drawStart;
    int drawEnd;
} t_draw;

void calc_wall_height(t_ray *ray, t_draw *draw, int screenHeight) {
    draw->lineHeight = (int)(screenHeight / ray->perpWallDist);

    draw->drawStart = -draw->lineHeight / 2 + screenHeight / 2;
    if (draw->drawStart < 0)
        draw->drawStart = 0;

    draw->drawEnd = draw->lineHeight / 2 + screenHeight / 2;
    if (draw->drawEnd >= screenHeight)
        draw->drawEnd = screenHeight - 1;
}

3.2 単色での描画

void draw_wall_line(t_game *game, int x, t_ray *ray, t_draw *draw) {
    int color;

    // 方向によって色を変える
    if (ray->side == 0) {
        if (ray->stepX > 0)
            color = 0xFF0000;  // 東: 赤
        else
            color = 0x00FF00;  // 西: 緑
    } else {
        if (ray->stepY > 0)
            color = 0x0000FF;  // 南: 青
        else
            color = 0xFFFF00;  // 北: 黄
    }

    // 影をつける(南北の壁は暗く)
    if (ray->side == 1)
        color = (color >> 1) & 0x7F7F7F;

    // 壁を描画
    for (int y = draw->drawStart; y < draw->drawEnd; y++) {
        put_pixel(game, x, y, color);
    }
}

3.3 天井と床

void draw_ceiling_floor(t_game *game, int x, t_draw *draw) {
    // 天井
    for (int y = 0; y < draw->drawStart; y++) {
        put_pixel(game, x, y, game->ceiling_color);
    }

    // 床
    for (int y = draw->drawEnd; y < game->screenHeight; y++) {
        put_pixel(game, x, y, game->floor_color);
    }
}

---

4. レンダリングループ

4.1 1フレームのレンダリング

void render_frame(t_game *game) {
    for (int x = 0; x < game->screenWidth; x++) {
        t_ray ray;
        t_draw draw;

        // レイの初期化
        init_ray(&ray, &game->player, x, game->screenWidth);

        // ステップと初期距離
        calc_step_and_side_dist(&ray);

        // DDA実行
        perform_dda(&ray, game->map);

        // 垂直距離計算
        calc_perp_wall_dist(&ray);

        // 壁の高さ計算
        calc_wall_height(&ray, &draw, game->screenHeight);

        // 描画
        draw_ceiling_floor(game, x, &draw);
        draw_wall_line(game, x, &ray, &draw);
    }

    // 画面に表示
    mlx_put_image_to_window(game->mlx, game->win, game->img, 0, 0);
}

4.2 ゲームループ

int game_loop(t_game *game) {
    // 入力処理
    handle_input(game);

    // 更新
    update_player(game);

    // 描画
    render_frame(game);

    return 0;
}

int main() {
    t_game game;

    init_game(&game);

    // ループ関数を設定
    mlx_loop_hook(game.mlx, game_loop, &game);

    mlx_loop(game.mlx);
    return 0;
}

---

5. 最適化

5.1 不要な計算の削減

// 事前計算
double invDirX = 1.0 / ray->dirX;
double invDirY = 1.0 / ray->dirY;

ray->deltaDistX = fabs(invDirX);
ray->deltaDistY = fabs(invDirY);

5.2 ルックアップテーブル

// 三角関数のテーブル
double sin_table[3600];  // 0.1度刻み
double cos_table[3600];

void init_trig_tables() {
    for (int i = 0; i < 3600; i++) {
        double angle = i * PI / 1800.0;
        sin_table[i] = sin(angle);
        cos_table[i] = cos(angle);
    }
}

---

まとめ

本章で学んだこと:

  • DDAアルゴリズム: グリッド走査の効率的な方法
  • レイの初期化: 方向、デルタ距離の計算
  • 壁の描画: 高さ計算、色付け
  • レンダリングループ: フレームごとの処理

次章では、テクスチャマッピングを学びます。