第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アルゴリズム: グリッド走査の効率的な方法
- レイの初期化: 方向、デルタ距離の計算
- 壁の描画: 高さ計算、色付け
- レンダリングループ: フレームごとの処理
次章では、テクスチャマッピングを学びます。