第2章:線形代数の基礎

はじめに

本章では、レイキャスティングに必要な線形代数の基礎を学びます。

---

1. ベクトル

1.1 ベクトルの定義

ベクトルは、大きさと方向を持つ量です。

typedef struct s_vec2 {
    double x;
    double y;
} t_vec2;

// ベクトルの作成
t_vec2 vec2_new(double x, double y) {
    return (t_vec2){x, y};
}

1.2 ベクトル演算

// 加算
t_vec2 vec2_add(t_vec2 a, t_vec2 b) {
    return vec2_new(a.x + b.x, a.y + b.y);
}

// 減算
t_vec2 vec2_sub(t_vec2 a, t_vec2 b) {
    return vec2_new(a.x - b.x, a.y - b.y);
}

// スカラー倍
t_vec2 vec2_scale(t_vec2 v, double s) {
    return vec2_new(v.x * s, v.y * s);
}

// 長さ(ノルム)
double vec2_length(t_vec2 v) {
    return sqrt(v.x * v.x + v.y * v.y);
}

// 正規化(単位ベクトル)
t_vec2 vec2_normalize(t_vec2 v) {
    double len = vec2_length(v);
    if (len == 0) return vec2_new(0, 0);
    return vec2_new(v.x / len, v.y / len);
}

1.3 内積

// 内積
double vec2_dot(t_vec2 a, t_vec2 b) {
    return a.x * b.x + a.y * b.y;
}

// 用途:
// - 2つのベクトルの角度を求める
// - 投影の計算
// cos(θ) = (a · b) / (|a| * |b|)

1.4 外積(2D)

// 2D外積(擬似的、結果はスカラー)
double vec2_cross(t_vec2 a, t_vec2 b) {
    return a.x * b.y - a.y * b.x;
}

// 用途:
// - 左右の判定
// - 面積の計算

---

2. 回転

2.1 回転行列

2Dでの回転は、以下の行列で表現できます:

|cos(θ)  -sin(θ)|   |x|   |x'|
|               | × | | = |  |
|sin(θ)   cos(θ)|   |y|   |y'|

2.2 実装

// ベクトルを回転
t_vec2 vec2_rotate(t_vec2 v, double angle) {
    double cos_a = cos(angle);
    double sin_a = sin(angle);
    return vec2_new(
        v.x * cos_a - v.y * sin_a,
        v.x * sin_a + v.y * cos_a
    );
}

// プレイヤーの回転
void rotate_player(t_player *p, double angle) {
    // 方向ベクトルを回転
    p->dir = vec2_rotate(p->dir, angle);
    // カメラ平面も回転
    p->plane = vec2_rotate(p->plane, angle);
}

2.3 マウスによる回転

#define MOUSE_SENSITIVITY 0.003

void mouse_move(int x, int y, t_game *game) {
    static int prev_x = -1;

    if (prev_x == -1) {
        prev_x = x;
        return;
    }

    int dx = x - prev_x;
    double angle = dx * MOUSE_SENSITIVITY;

    rotate_player(&game->player, angle);
    prev_x = x;
}

---

3. レイの計算

3.1 カメラ平面

カメラ平面は、視野角(FOV)を決定します:

// FOV 66度の場合(tan(33°) ≈ 0.66)
player.planeX = 0.0;
player.planeY = 0.66;

// 各レイの方向を計算
for (int x = 0; x < screenWidth; x++) {
    // カメラ平面上の位置 (-1 to 1)
    double cameraX = 2 * x / (double)screenWidth - 1;

    // レイの方向
    double rayDirX = dirX + planeX * cameraX;
    double rayDirY = dirY + planeY * cameraX;
}

3.2 視野角の関係

FOV = 2 * atan(planeLength)

例:
planeLength = 0.66
FOV = 2 * atan(0.66) ≈ 66度

|planeLength| < 1 → FOV < 90度
|planeLength| = 1 → FOV = 90度
|planeLength| > 1 → FOV > 90度

---

4. 距離計算

4.1 ユークリッド距離 vs 垂直距離

ユークリッド距離(使わない):
dist = sqrt((wallX - posX)² + (wallY - posY)²)

問題: 魚眼効果が発生

垂直距離(使う):
perpDist = (mapX - posX + (1 - stepX) / 2) / rayDirX
      または
perpDist = (mapY - posY + (1 - stepY) / 2) / rayDirY

4.2 魚眼効果の回避

+--------------------+
|     魚眼効果       |
|   /‾‾‾‾‾‾‾‾‾\     |
|  |           |    |
|   \_________/     |
|                   |
+--------------------+

修正後:
+--------------------+
|  正しいレンダリング |
|  _______________  |
| |               | |
| |               | |
| |_______________| |
+--------------------+

垂直距離を使うことで、壁が正しく直線に見える

4.3 実装

double get_perp_wall_dist(t_ray *ray, t_player *player) {
    if (ray->side == 0) {  // 東西の壁
        return (ray->mapX - player->posX +
                (1 - ray->stepX) / 2) / ray->dirX;
    } else {               // 南北の壁
        return (ray->mapY - player->posY +
                (1 - ray->stepY) / 2) / ray->dirY;
    }
}

---

5. 座標変換

5.1 マップ座標とピクセル座標

// マップ座標 → ピクセル座標
int pixelX = (int)(mapX * TILE_SIZE);
int pixelY = (int)(mapY * TILE_SIZE);

// ピクセル座標 → マップ座標
double mapX = (double)pixelX / TILE_SIZE;
double mapY = (double)pixelY / TILE_SIZE;

5.2 画面座標

// 壁の高さを計算
int lineHeight = (int)(screenHeight / perpWallDist);

// 描画開始・終了位置
int drawStart = -lineHeight / 2 + screenHeight / 2;
int drawEnd = lineHeight / 2 + screenHeight / 2;

if (drawStart < 0) drawStart = 0;
if (drawEnd >= screenHeight) drawEnd = screenHeight - 1;

---

6. 三角関数

6.1 基本関数

#include <math.h>

// 正弦・余弦
double sin_val = sin(angle);  // ラジアン
double cos_val = cos(angle);

// 逆三角関数
double angle = atan2(y, x);   // 4象限対応
double angle = acos(cos_val);
double angle = asin(sin_val);

// tan
double tan_val = tan(angle);

6.2 よく使う値

#define PI      3.14159265358979323846
#define PI_2    (PI / 2)     // 90度
#define PI_4    (PI / 4)     // 45度
#define DEG_90  PI_2
#define DEG_180 PI
#define DEG_270 (3 * PI / 2)
#define DEG_360 (2 * PI)

---

7. 衝突判定の数学

7.1 点と矩形

bool point_in_rect(double px, double py,
                   double rx, double ry,
                   double rw, double rh) {
    return px >= rx && px < rx + rw &&
           py >= ry && py < ry + rh;
}

7.2 線分と線分

// 2つの線分が交差するか判定
bool lines_intersect(t_vec2 p1, t_vec2 p2,
                     t_vec2 p3, t_vec2 p4) {
    double d1 = direction(p3, p4, p1);
    double d2 = direction(p3, p4, p2);
    double d3 = direction(p1, p2, p3);
    double d4 = direction(p1, p2, p4);

    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
        return true;
    }
    return false;
}

double direction(t_vec2 a, t_vec2 b, t_vec2 c) {
    return vec2_cross(vec2_sub(c, a), vec2_sub(b, a));
}

---

まとめ

本章で学んだこと:

  • ベクトル演算: 加算、減算、内積、外積
  • 回転: 回転行列、プレイヤー回転
  • レイ計算: カメラ平面、レイ方向
  • 距離計算: 垂直距離、魚眼効果の回避
  • 座標変換: マップ座標とピクセル座標

次章では、レイキャスティングの実装を学びます。