第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、回転
- アンチエイリアシング: スーパーサンプリング
- ガンマ補正: 正しい色再現
- 被写界深度: 絞りと焦点
次章では、実装の詳細とシーン管理を学びます。