第4章:カメラと投影

はじめに

カメラは3Dシーンを2D画像に変換する重要な要素です。本章では、カメラモデル、視野角、アンチエイリアシングを学びます。

---

1. カメラの数学

1.1 座標系

右手座標系(OpenGL, miniRT):
    Y ↑
      |
      |
      +------→ X
     /
    /
   ↙ Z(手前)

カメラのデフォルト:
- 位置: 原点 (0, 0, 0)
- 向き: -Z方向を見る
- 上方向: +Y

1.2 ビューポート

ビューポートは、カメラの前に置かれた仮想的なスクリーンです:

typedef struct s_camera {
    t_vec3  origin;          // カメラ位置
    t_vec3  direction;       // 視線方向(正規化)
    double  fov;             // 視野角(度)

    // 計算されるパラメータ
    t_vec3  horizontal;      // ビューポートの横幅ベクトル
    t_vec3  vertical;        // ビューポートの縦幅ベクトル
    t_vec3  lower_left;      // ビューポートの左下コーナー
} t_camera;

                  Viewport
            +---------------+
            |       ↑       |
            |       |       |
   Camera   |       |vertical
      ●-----+-------●-------+
      |     |       |       |
      |     |←─horizontal─→|
   focal    |               |
  distance  +---------------+
            lower_left

レイは Camera から Viewport の各点を通過

1.3 カメラの初期化

void camera_init(t_camera *cam, t_vec3 origin, t_vec3 direction,
                 double fov, double aspect_ratio) {
    cam->origin = origin;
    cam->direction = vec3_normalize(direction);
    cam->fov = fov;

    // ビューポートのサイズを計算
    double theta = fov * M_PI / 180.0;
    double half_height = tan(theta / 2.0);
    double half_width = aspect_ratio * half_height;

    // カメラのローカル座標系を構築
    t_vec3 world_up = vec3_new(0, 1, 0);

    // 視線方向と上方向が平行な場合の処理
    if (fabs(vec3_dot(cam->direction, world_up)) > 0.999) {
        world_up = vec3_new(0, 0, 1);
    }

    // 右方向(X軸)
    t_vec3 right = vec3_normalize(vec3_cross(cam->direction, world_up));

    // 真の上方向(Y軸)
    t_vec3 up = vec3_cross(right, cam->direction);

    // ビューポートのベクトル
    cam->horizontal = vec3_scale(right, 2.0 * half_width);
    cam->vertical = vec3_scale(up, 2.0 * half_height);

    // 左下コーナー
    cam->lower_left = vec3_sub(
        vec3_sub(
            vec3_sub(cam->origin, vec3_scale(cam->horizontal, 0.5)),
            vec3_scale(cam->vertical, 0.5)
        ),
        cam->direction  // focal_distance = 1
    );
}

1.4 レイの生成

t_ray camera_get_ray(t_camera *cam, double u, double v) {
    // u, v は 0.0 〜 1.0 の範囲

    // ビューポート上の点
    t_vec3 point = vec3_add(
        vec3_add(
            cam->lower_left,
            vec3_scale(cam->horizontal, u)
        ),
        vec3_scale(cam->vertical, v)
    );

    // レイの方向
    t_vec3 direction = vec3_normalize(vec3_sub(point, cam->origin));

    return (t_ray){cam->origin, direction};
}

---

2. 視野角(FOV)

2.1 FOVの意味

FOV = 90°の場合:
              Screen
            +-------+
            |       |
            |       |
     Camera ●-------+
            |       |
            |       |
            +-------+
            45° + 45° = 90°

FOV = 60°の場合(狭い):
            +---+
            |   |
     Camera ●---+
            |   |
            +---+
           ズームイン効果

FOV = 120°の場合(広い):
        +-------------+
        |             |
 Camera ●-------------+
        |             |
        +-------------+
           魚眼効果

2.2 FOVとビューポートサイズの関係

// 垂直FOVからビューポートの高さを計算
double viewport_height = 2.0 * tan(fov * M_PI / 360.0);
double viewport_width = viewport_height * aspect_ratio;

tan(FOV/2) = (viewport_height/2) / focal_distance

focal_distance = 1 と仮定:
viewport_height = 2 * tan(FOV/2)

典型的なFOV:
- 60° : 標準的なゲーム
- 70° : miniRTのデフォルト
- 90° : 広角(FPSゲーム)
- 120°: 超広角(VR)

---

3. カメラの配置

3.1 ルックアット(LookAt)

特定の点を見るようにカメラを配置:

void camera_look_at(t_camera *cam, t_vec3 from, t_vec3 at, t_vec3 up) {
    cam->origin = from;

    // 視線方向
    cam->direction = vec3_normalize(vec3_sub(at, from));

    // カメラ座標系を構築
    t_vec3 right = vec3_normalize(vec3_cross(cam->direction, up));
    t_vec3 true_up = vec3_cross(right, cam->direction);

    // ... ビューポートを設定
}

3.2 回転

// オイラー角でカメラを回転
void camera_rotate(t_camera *cam, double pitch, double yaw, double roll) {
    // Yaw(左右の回転)
    double cos_yaw = cos(yaw);
    double sin_yaw = sin(yaw);

    t_vec3 dir = cam->direction;
    cam->direction.x = dir.x * cos_yaw - dir.z * sin_yaw;
    cam->direction.z = dir.x * sin_yaw + dir.z * cos_yaw;

    // Pitch(上下の回転)
    double cos_pitch = cos(pitch);
    double sin_pitch = sin(pitch);

    dir = cam->direction;
    cam->direction.y = dir.y * cos_pitch - dir.z * sin_pitch;
    cam->direction.z = dir.y * sin_pitch + dir.z * cos_pitch;

    // ビューポートを再計算
    camera_update_viewport(cam);
}

---

4. アンチエイリアシング

4.1 ジャギーの問題

ピクセル単位でサンプリングすると、エッジがギザギザに見えます:

ジャギーあり:        アンチエイリアシング後:
████████             ████████
████                 ▓▓██
██                   ░▓██
                     ░░▓█

4.2 スーパーサンプリング

各ピクセルで複数のレイを発射し、平均を取ります:

#define SAMPLES_PER_PIXEL 16

t_color render_pixel(t_scene *scene, t_camera *cam, int x, int y,
                     int width, int height) {
    t_color total = color_new(0, 0, 0);

    for (int s = 0; s < SAMPLES_PER_PIXEL; s++) {
        // ピクセル内でランダムにオフセット
        double u = (x + random_double()) / (width - 1);
        double v = (y + random_double()) / (height - 1);

        t_ray ray = camera_get_ray(cam, u, v);
        t_color sample = trace_ray(scene, &ray, 0);

        total = color_add(total, sample);
    }

    return color_scale(total, 1.0 / SAMPLES_PER_PIXEL);
}

4.3 ストラティファイドサンプリング

より均一なサンプル分布:

t_color render_pixel_stratified(t_scene *scene, t_camera *cam,
                                 int x, int y, int width, int height,
                                 int sqrt_samples) {
    t_color total = color_new(0, 0, 0);

    for (int sy = 0; sy < sqrt_samples; sy++) {
        for (int sx = 0; sx < sqrt_samples; sx++) {
            // 各サブピクセル内でランダム
            double u = (x + (sx + random_double()) / sqrt_samples) /
                       (width - 1);
            double v = (y + (sy + random_double()) / sqrt_samples) /
                       (height - 1);

            t_ray ray = camera_get_ray(cam, u, v);
            t_color sample = trace_ray(scene, &ray, 0);

            total = color_add(total, sample);
        }
    }

    int total_samples = sqrt_samples * sqrt_samples;
    return color_scale(total, 1.0 / total_samples);
}

ランダムサンプリング:      ストラティファイド:
+--+--+--+--+            +--+--+--+--+
|* |  | *|  |            |* |* |* |* |
+--+--+--+--+            +--+--+--+--+
|  |* |  | *|            | *| *| *| *|
+--+--+--+--+            +--+--+--+--+

* = サンプル位置
ストラティファイドの方が均一

4.4 乱数生成

#include <stdlib.h>

// 0.0 〜 1.0 のランダム値
double random_double(void) {
    return rand() / (RAND_MAX + 1.0);
}

// 範囲指定
double random_double_range(double min, double max) {
    return min + (max - min) * random_double();
}

// シード初期化
void init_random(void) {
    srand(time(NULL));
}

---

5. ガンマ補正

5.1 なぜ必要か

モニターは入力を非線形に表示するため、補正が必要です:

物理的な光量 → モニター出力

補正なし:
入力 0.5 → 出力 約0.22(暗すぎる)

ガンマ補正後:
入力 0.5^(1/2.2) ≈ 0.73 → 出力 0.5(正しい)

5.2 実装

// ガンマ補正(ガンマ = 2.2)
t_color gamma_correct(t_color c) {
    double gamma = 1.0 / 2.2;
    return color_new(
        pow(c.r, gamma),
        pow(c.g, gamma),
        pow(c.b, gamma)
    );
}

// 出力時に適用
unsigned int color_to_int_gamma(t_color c) {
    c = color_clamp(c);
    c = gamma_correct(c);

    return ((int)(c.r * 255) << 16) |
           ((int)(c.g * 255) << 8) |
           (int)(c.b * 255);
}

---

6. 被写界深度(ボーナス)

6.1 原理

実際のカメラは、焦点距離にあるものだけがシャープに写ります:

typedef struct s_camera_dof {
    t_vec3  origin;
    t_vec3  direction;
    double  fov;
    double  aperture;      // 絞り(ボケの量)
    double  focus_dist;    // 焦点距離

    t_vec3  horizontal;
    t_vec3  vertical;
    t_vec3  lower_left;
    t_vec3  u, v, w;       // カメラ座標系
} t_camera_dof;

6.2 実装

// 単位円内のランダム点
t_vec3 random_in_unit_disk(void) {
    while (1) {
        t_vec3 p = vec3_new(
            random_double_range(-1, 1),
            random_double_range(-1, 1),
            0
        );
        if (vec3_length_squared(p) < 1)
            return p;
    }
}

t_ray camera_get_ray_dof(t_camera_dof *cam, double s, double t) {
    // 絞りによるランダムオフセット
    t_vec3 rd = vec3_scale(random_in_unit_disk(),
                           cam->aperture / 2);
    t_vec3 offset = vec3_add(
        vec3_scale(cam->u, rd.x),
        vec3_scale(cam->v, rd.y)
    );

    // 焦点面上の点
    t_vec3 focus_point = vec3_add(
        vec3_add(
            cam->lower_left,
            vec3_scale(cam->horizontal, s)
        ),
        vec3_scale(cam->vertical, t)
    );

    // オフセットされた始点から焦点面へ
    t_vec3 origin = vec3_add(cam->origin, offset);
    t_vec3 direction = vec3_normalize(
        vec3_sub(focus_point, origin)
    );

    return (t_ray){origin, direction};
}

Aperture(絞り)が大きい → ボケが強い

      ●       ← ボケた背景
     /|\
    / | \
   /  |  \
  ●---●---●   ← 焦点面(シャープ)
   \  |  /
    \ | /
     \|/
      ○       ← レンズ(絞り)
      |
   Camera

---

7. 解像度とアスペクト比

7.1 一般的な解像度

| 名前 | 解像度 | アスペクト比 | |------|--------|--------------| | VGA | 640×480 | 4:3 | | HD | 1280×720 | 16:9 | | Full HD | 1920×1080 | 16:9 | | 4K | 3840×2160 | 16:9 |

7.2 実装

#define WIDTH 1280
#define HEIGHT 720

int main(void) {
    double aspect_ratio = (double)WIDTH / HEIGHT;

    t_camera cam;
    camera_init(&cam, origin, direction, 70.0, aspect_ratio);

    // レンダリングループ
    for (int y = HEIGHT - 1; y >= 0; y--) {
        for (int x = 0; x < WIDTH; x++) {
            double u = (double)x / (WIDTH - 1);
            double v = (double)y / (HEIGHT - 1);

            t_ray ray = camera_get_ray(&cam, u, v);
            t_color color = trace_ray(&scene, &ray, 0);

            put_pixel(x, HEIGHT - 1 - y, color_to_int(color));
        }
    }
}

---

まとめ

本章で学んだこと:

  • カメラの数学: ビューポート、座標系
  • 視野角(FOV): ズームとの関係
  • カメラ配置: LookAt、回転
  • アンチエイリアシング: スーパーサンプリング
  • ガンマ補正: 正しい色再現
  • 被写界深度: 絞りと焦点

次章では、実装の詳細とシーン管理を学びます。