第2章:3D幾何学と交差判定

はじめに

レイトレーシングの核心は、レイと3Dオブジェクトの交差判定です。本章では、各プリミティブ(球、平面、円柱)との交差判定アルゴリズムを学びます。

---

1. 交差判定の基礎

1.1 問題の定式化

レイと物体の交差点を求める問題:

レイ: P(t) = O + t * D
     O = 始点
     D = 方向(正規化)
     t = パラメータ(t > 0 で前方)

求めるもの:
- 交差するか?
- 交差点 P(最も近い t)
- 法線 N(照明計算に必要)

1.2 交差情報の構造体

typedef struct s_hit {
    int     valid;      // 交差したか
    double  t;          // パラメータ値
    t_vec3  point;      // 交差点
    t_vec3  normal;     // 法線(外向き)
    t_color color;      // 物体の色
    // ボーナス用
    double  u;          // テクスチャ座標
    double  v;
} t_hit;

// 初期化
t_hit hit_miss(void) {
    return (t_hit){.valid = 0, .t = INFINITY};
}

---

2. 球との交差判定

2.1 数学的導出

球の方程式:

|P - C|² = r²

P = 球上の点
C = 球の中心
r = 半径

レイの方程式を代入:

|O + t*D - C|² = r²

ベクトル OC = O - C として:
|OC + t*D|² = r²

展開:
(OC + t*D) · (OC + t*D) = r²
|OC|² + 2t(OC · D) + t²|D|² = r²

|D| = 1(正規化済み)なので:
t² + 2t(OC · D) + |OC|² - r² = 0

これは t の二次方程式:
at² + bt + c = 0

a = 1
b = 2(OC · D)
c = |OC|² - r²

判別式: Δ = b² - 4ac
       = 4(OC · D)² - 4(|OC|² - r²)
       = 4[(OC · D)² - |OC|² + r²]

2.2 実装

typedef struct s_sphere {
    t_vec3  center;
    double  radius;
} t_sphere;

t_hit sphere_intersect(t_sphere *sphere, t_ray *ray) {
    t_vec3 oc = vec3_sub(ray->origin, sphere->center);

    // 二次方程式の係数
    double a = vec3_dot(ray->direction, ray->direction);  // = 1
    double half_b = vec3_dot(oc, ray->direction);
    double c = vec3_dot(oc, oc) - sphere->radius * sphere->radius;

    // 判別式
    double discriminant = half_b * half_b - a * c;

    if (discriminant < 0)
        return hit_miss();

    // 最も近い交差点を選択
    double sqrt_d = sqrt(discriminant);
    double t = (-half_b - sqrt_d) / a;

    // t が負なら遠い方を試す
    if (t < EPSILON) {
        t = (-half_b + sqrt_d) / a;
        if (t < EPSILON)
            return hit_miss();
    }

    // 交差情報を構築
    t_hit hit;
    hit.valid = 1;
    hit.t = t;
    hit.point = ray_at(ray, t);
    hit.normal = vec3_normalize(
        vec3_sub(hit.point, sphere->center)
    );

    return hit;
}

2.3 法線の方向

// 法線が常に外向きになるよう調整
void fix_normal(t_hit *hit, t_ray *ray) {
    if (vec3_dot(ray->direction, hit->normal) > 0) {
        // レイが内側から当たっている
        hit->normal = vec3_scale(hit->normal, -1);
    }
}

外側からの入射:        内側からの入射:
       ↘                    ↗
        \   N              /
         \  ↑             /
          \ |            /
           \|           /|
            ●          ● |
           球          球 N(反転後)

---

3. 平面との交差判定

3.1 数学的導出

平面の方程式:

(P - P₀) · N = 0

P  = 平面上の点
P₀ = 平面上の既知の点
N  = 平面の法線

レイの方程式を代入:

(O + t*D - P₀) · N = 0
(O - P₀) · N + t(D · N) = 0

t = [(P₀ - O) · N] / (D · N)

3.2 実装

typedef struct s_plane {
    t_vec3  point;   // 平面上の点
    t_vec3  normal;  // 法線(正規化済み)
} t_plane;

t_hit plane_intersect(t_plane *plane, t_ray *ray) {
    double denom = vec3_dot(ray->direction, plane->normal);

    // レイが平面と平行(または後ろ向き)
    if (fabs(denom) < EPSILON)
        return hit_miss();

    t_vec3 p0_o = vec3_sub(plane->point, ray->origin);
    double t = vec3_dot(p0_o, plane->normal) / denom;

    // 後ろの交差は無視
    if (t < EPSILON)
        return hit_miss();

    t_hit hit;
    hit.valid = 1;
    hit.t = t;
    hit.point = ray_at(ray, t);
    hit.normal = plane->normal;

    // 法線の向きを調整
    if (denom > 0)
        hit.normal = vec3_scale(hit.normal, -1);

    return hit;
}

3.3 無限平面の描画

        N
        ↑
        |
━━━━━━━━●━━━━━━━━ 平面
       P₀

特徴:
- 無限に広がる
- チェッカーボードパターンでよく使われる

// ボーナス:チェッカーボードパターン
t_color checkerboard(t_hit *hit, double scale) {
    int x = (int)floor(hit->point.x * scale);
    int z = (int)floor(hit->point.z * scale);

    if ((x + z) % 2 == 0)
        return color_new(1, 1, 1);  // 白
    else
        return color_new(0, 0, 0);  // 黒
}

---

4. 円柱との交差判定

4.1 無限円柱

無限円柱の方程式(Z軸に沿った円柱):

x² + y² = r²

一般的な向きの円柱:

|P - C - ((P - C) · A) * A|² = r²

C = 円柱の軸上の点
A = 軸の方向(正規化)
r = 半径

4.2 数学的導出

レイ: P(t) = O + t*D

V = O - C(始点から円柱中心へのベクトル)

円柱表面上の条件:
|V + t*D - ((V + t*D) · A) * A|² = r²

展開して整理すると二次方程式:
at² + bt + c = 0

a = |D - (D·A)A|²
b = 2(D - (D·A)A) · (V - (V·A)A)
c = |V - (V·A)A|² - r²

4.3 有限円柱の実装

typedef struct s_cylinder {
    t_vec3  center;     // 底面の中心
    t_vec3  axis;       // 軸の方向(正規化)
    double  radius;
    double  height;
} t_cylinder;

t_hit cylinder_intersect(t_cylinder *cyl, t_ray *ray) {
    // 軸方向の成分を除去
    t_vec3 v = vec3_sub(ray->origin, cyl->center);
    t_vec3 d_perp = vec3_sub(ray->direction,
        vec3_scale(cyl->axis, vec3_dot(ray->direction, cyl->axis)));
    t_vec3 v_perp = vec3_sub(v,
        vec3_scale(cyl->axis, vec3_dot(v, cyl->axis)));

    // 二次方程式の係数
    double a = vec3_dot(d_perp, d_perp);
    double b = 2.0 * vec3_dot(d_perp, v_perp);
    double c = vec3_dot(v_perp, v_perp) - cyl->radius * cyl->radius;

    double discriminant = b * b - 4 * a * c;
    if (discriminant < 0)
        return hit_miss();

    // 解を計算
    double sqrt_d = sqrt(discriminant);
    double t1 = (-b - sqrt_d) / (2 * a);
    double t2 = (-b + sqrt_d) / (2 * a);

    // 有限円柱の高さチェック
    t_hit hit = hit_miss();
    hit.t = INFINITY;

    // t1をチェック
    if (t1 > EPSILON) {
        t_vec3 p = ray_at(ray, t1);
        double h = vec3_dot(vec3_sub(p, cyl->center), cyl->axis);
        if (h >= 0 && h <= cyl->height) {
            hit.valid = 1;
            hit.t = t1;
        }
    }

    // t2をチェック(t1が無効な場合)
    if (!hit.valid && t2 > EPSILON) {
        t_vec3 p = ray_at(ray, t2);
        double h = vec3_dot(vec3_sub(p, cyl->center), cyl->axis);
        if (h >= 0 && h <= cyl->height) {
            hit.valid = 1;
            hit.t = t2;
        }
    }

    if (!hit.valid)
        return hit_miss();

    // 交差情報を構築
    hit.point = ray_at(ray, hit.t);

    // 法線を計算
    t_vec3 cp = vec3_sub(hit.point, cyl->center);
    double h = vec3_dot(cp, cyl->axis);
    t_vec3 axis_point = vec3_add(cyl->center,
                                  vec3_scale(cyl->axis, h));
    hit.normal = vec3_normalize(vec3_sub(hit.point, axis_point));

    return hit;
}

4.4 円柱のキャップ

// 上下のキャップ(円形の蓋)
t_hit cylinder_cap_intersect(t_cylinder *cyl, t_ray *ray, int is_top) {
    // キャップの平面
    t_vec3 cap_center = is_top ?
        vec3_add(cyl->center, vec3_scale(cyl->axis, cyl->height)) :
        cyl->center;
    t_vec3 cap_normal = is_top ? cyl->axis :
        vec3_scale(cyl->axis, -1);

    // 平面との交差
    double denom = vec3_dot(ray->direction, cap_normal);
    if (fabs(denom) < EPSILON)
        return hit_miss();

    t_vec3 p0_o = vec3_sub(cap_center, ray->origin);
    double t = vec3_dot(p0_o, cap_normal) / denom;

    if (t < EPSILON)
        return hit_miss();

    // 交差点が円内にあるか確認
    t_vec3 p = ray_at(ray, t);
    t_vec3 v = vec3_sub(p, cap_center);
    double dist2 = vec3_dot(v, v);

    if (dist2 > cyl->radius * cyl->radius)
        return hit_miss();

    t_hit hit;
    hit.valid = 1;
    hit.t = t;
    hit.point = p;
    hit.normal = cap_normal;

    return hit;
}

---

5. 交差判定の最適化

5.1 バウンディングボックス

複雑な物体の前に、単純な箱で判定:

typedef struct s_aabb {
    t_vec3 min;
    t_vec3 max;
} t_aabb;

int aabb_intersect(t_aabb *box, t_ray *ray) {
    double tmin = -INFINITY;
    double tmax = INFINITY;

    for (int i = 0; i < 3; i++) {
        double invD = 1.0 / (&ray->direction.x)[i];
        double t0 = ((&box->min.x)[i] - (&ray->origin.x)[i]) * invD;
        double t1 = ((&box->max.x)[i] - (&ray->origin.x)[i]) * invD;

        if (invD < 0) {
            double tmp = t0;
            t0 = t1;
            t1 = tmp;
        }

        tmin = t0 > tmin ? t0 : tmin;
        tmax = t1 < tmax ? t1 : tmax;

        if (tmax < tmin)
            return 0;
    }

    return tmax > 0;
}

5.2 バウンディング球

typedef struct s_bsphere {
    t_vec3 center;
    double radius;
} t_bsphere;

int bsphere_intersect(t_bsphere *bs, t_ray *ray) {
    t_vec3 oc = vec3_sub(ray->origin, bs->center);
    double b = vec3_dot(oc, ray->direction);
    double c = vec3_dot(oc, oc) - bs->radius * bs->radius;

    // b > 0 かつ c > 0 なら、レイは球の外で遠ざかっている
    if (b > 0 && c > 0)
        return 0;

    double discriminant = b * b - c;
    return discriminant >= 0;
}

5.3 早期終了

t_hit find_closest_hit(t_scene *scene, t_ray *ray) {
    t_hit closest = hit_miss();
    closest.t = INFINITY;

    for (int i = 0; i < scene->object_count; i++) {
        t_object *obj = &scene->objects[i];

        // バウンディング球で早期判定
        if (!bsphere_intersect(&obj->bsphere, ray))
            continue;

        t_hit hit;
        switch (obj->type) {
            case OBJ_SPHERE:
                hit = sphere_intersect(&obj->data.sphere, ray);
                break;
            case OBJ_PLANE:
                hit = plane_intersect(&obj->data.plane, ray);
                break;
            case OBJ_CYLINDER:
                hit = cylinder_intersect(&obj->data.cylinder, ray);
                break;
        }

        // より近い交差点を記録
        if (hit.valid && hit.t < closest.t) {
            closest = hit;
            closest.color = obj->color;
        }
    }

    return closest;
}

---

6. 数値安定性

6.1 イプシロン

浮動小数点誤差への対処:

#define EPSILON 1e-6

// 自己交差を防ぐ
t_ray create_shadow_ray(t_hit *hit, t_vec3 light_dir) {
    // 交差点をわずかに法線方向にオフセット
    t_vec3 origin = vec3_add(hit->point,
                             vec3_scale(hit->normal, EPSILON));

    return (t_ray){origin, light_dir};
}

6.2 t = 0 付近の問題

t_hit sphere_intersect_safe(t_sphere *sphere, t_ray *ray) {
    // ... 二次方程式を解く ...

    // 非常に小さいtは無視
    double t = (-half_b - sqrt_d) / a;
    if (t < EPSILON) {
        t = (-half_b + sqrt_d) / a;
        if (t < EPSILON)
            return hit_miss();
    }

    // ... 残りの処理 ...
}

---

まとめ

本章で学んだこと:

  • 交差判定の基礎: レイと物体の交点、法線
  • 球との交差: 二次方程式、判別式
  • 平面との交差: 線形方程式
  • 円柱との交差: 有限円柱、キャップ
  • 最適化: バウンディングボリューム
  • 数値安定性: イプシロン、自己交差

次章では、ライティングとシェーディングを学びます。