5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++レイトレーサー自作シリーズ

Part1 レイ/ベクトル Part2 交差判定 Part3 反射/屈折 Part4 マテリアル Part5 BVH Part6 パストレ
👈 Now - - - - -

はじめに

3DCGってどうやって作られてるか知ってる?

実は、光の物理シミュレーションをしてるだけ。

┌────────────────────────────────────────────────────────────┐
│ レイトレーシングの原理                                      │
│                                                            │
│   👁 カメラ                                                │
│    \                                                       │
│     \ レイ(光線)                                         │
│      \                                                     │
│       \→ 🔴 物体にヒット → 色を計算                       │
│          \                                                 │
│           \→ 💡 光源に向かって影を計算                    │
│                                                            │
└────────────────────────────────────────────────────────────┘

このシリーズでは、C++でレイトレーサーをゼロから作りながら、3DCGの基礎を学んでいくよ。

必要な数学

ベクトル

レイトレーシングでは3次元ベクトルを多用する。

struct Vec3 {
    double x, y, z;
    
    Vec3() : x(0), y(0), z(0) {}
    Vec3(double x, double y, double z) : x(x), y(y), z(z) {}
    
    // 加算
    Vec3 operator+(const Vec3& v) const {
        return Vec3(x + v.x, y + v.y, z + v.z);
    }
    
    // 減算
    Vec3 operator-(const Vec3& v) const {
        return Vec3(x - v.x, y - v.y, z - v.z);
    }
    
    // スカラー乗算
    Vec3 operator*(double t) const {
        return Vec3(x * t, y * t, z * t);
    }
    
    // スカラー除算
    Vec3 operator/(double t) const {
        return *this * (1.0 / t);
    }
    
    // 反転
    Vec3 operator-() const {
        return Vec3(-x, -y, -z);
    }
    
    // 長さの2乗(sqrtを避ける)
    double length_squared() const {
        return x*x + y*y + z*z;
    }
    
    // 長さ
    double length() const {
        return std::sqrt(length_squared());
    }
};

// 外部演算子
Vec3 operator*(double t, const Vec3& v) {
    return v * t;
}

内積と外積

内積(dot product): 2つのベクトルがどれくらい同じ方向を向いているか

double dot(const Vec3& a, const Vec3& b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}
cos(θ) = dot(a, b) / (|a| * |b|)

dot > 0 → 同じ方向
dot = 0 → 直角
dot < 0 → 反対方向

外積(cross product): 2つのベクトルに垂直なベクトルを計算

Vec3 cross(const Vec3& a, const Vec3& b) {
    return Vec3(
        a.y * b.z - a.z * b.y,
        a.z * b.x - a.x * b.z,
        a.x * b.y - a.y * b.x
    );
}

正規化

ベクトルを長さ1にする(単位ベクトル)。

Vec3 normalize(const Vec3& v) {
    return v / v.length();
}

なぜ正規化が重要?

// 法線ベクトルは常に長さ1であるべき
Vec3 normal = normalize(surface_normal);

// 反射計算で使う
Vec3 reflected = direction - 2 * dot(direction, normal) * normal;
// ↑ normalが長さ1でないと正しく計算できない

レイ(光線)

レイは始点と方向で定義される。

struct Ray {
    Vec3 origin;     // 始点
    Vec3 direction;  // 方向(正規化されてなくてもOK)
    
    Ray() {}
    Ray(const Vec3& origin, const Vec3& direction)
        : origin(origin), direction(direction) {}
    
    // パラメータtの位置を計算
    Vec3 at(double t) const {
        return origin + t * direction;
    }
};
レイの方程式: P(t) = origin + t * direction

t = 0  → 始点
t = 1  → 方向ベクトルの先端
t > 0  → 始点より前方
t < 0  → 始点より後方

最初の画像を生成

PPM形式で画像を出力してみよう。

#include <iostream>
#include <fstream>

void write_ppm(const std::string& filename, int width, int height) {
    std::ofstream file(filename);
    
    // PPMヘッダー
    file << "P3\n" << width << " " << height << "\n255\n";
    
    for (int j = height - 1; j >= 0; --j) {
        for (int i = 0; i < width; ++i) {
            // 色をグラデーションで計算
            double r = static_cast<double>(i) / (width - 1);
            double g = static_cast<double>(j) / (height - 1);
            double b = 0.25;
            
            int ir = static_cast<int>(255.999 * r);
            int ig = static_cast<int>(255.999 * g);
            int ib = static_cast<int>(255.999 * b);
            
            file << ir << " " << ig << " " << ib << "\n";
        }
    }
}

int main() {
    write_ppm("gradient.ppm", 256, 256);
    return 0;
}

これで赤緑のグラデーション画像が生成される。

カメラの設定

レイトレーシングでは、カメラからレイを飛ばす

┌──────────────────────────────────────────────────────────────┐
│ カメラモデル                                                 │
│                                                              │
│     ┌───────────────────────────────────┐ ← 画像平面        │
│     │ (0,1)                       (1,1) │                    │
│     │                                   │                    │
│     │                                   │                    │
│     │ (0,0) ←──────────────────→ (1,0)  │                    │
│     └───────────────────────────────────┘                    │
│                      ↑                                       │
│                      │                                       │
│                   👁 カメラ(原点)                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘
class Camera {
public:
    Camera() {
        // 画像のアスペクト比
        auto aspect_ratio = 16.0 / 9.0;
        auto viewport_height = 2.0;
        auto viewport_width = aspect_ratio * viewport_height;
        auto focal_length = 1.0;  // カメラから画像平面までの距離
        
        origin_ = Vec3(0, 0, 0);
        horizontal_ = Vec3(viewport_width, 0, 0);
        vertical_ = Vec3(0, viewport_height, 0);
        lower_left_corner_ = origin_ 
            - horizontal_ / 2 
            - vertical_ / 2 
            - Vec3(0, 0, focal_length);
    }
    
    Ray get_ray(double u, double v) const {
        // (u, v) は 0〜1 の範囲
        return Ray(
            origin_,
            lower_left_corner_ + u * horizontal_ + v * vertical_ - origin_
        );
    }

private:
    Vec3 origin_;
    Vec3 lower_left_corner_;
    Vec3 horizontal_;
    Vec3 vertical_;
};

背景色をレンダリング

まだ何もオブジェクトがないけど、レイの向きに応じた背景色を描こう。

Vec3 ray_color(const Ray& r) {
    // レイの方向を正規化
    Vec3 unit_direction = normalize(r.direction);
    
    // y成分を0〜1にマッピング
    auto t = 0.5 * (unit_direction.y + 1.0);
    
    // 白と青のグラデーション
    Vec3 white(1.0, 1.0, 1.0);
    Vec3 blue(0.5, 0.7, 1.0);
    
    return (1.0 - t) * white + t * blue;
}

線形補間(lerp)

lerp(a, b, t) = (1 - t) * a + t * b

t = 0 → a
t = 1 → b
t = 0.5 → aとbの中間

メイン関数

#include <iostream>
#include <fstream>

int main() {
    // 画像サイズ
    const int image_width = 400;
    const int image_height = static_cast<int>(image_width / (16.0 / 9.0));
    
    // カメラ
    Camera camera;
    
    // 出力
    std::ofstream file("output.ppm");
    file << "P3\n" << image_width << " " << image_height << "\n255\n";
    
    for (int j = image_height - 1; j >= 0; --j) {
        std::cerr << "\rScanlines remaining: " << j << " " << std::flush;
        
        for (int i = 0; i < image_width; ++i) {
            auto u = static_cast<double>(i) / (image_width - 1);
            auto v = static_cast<double>(j) / (image_height - 1);
            
            Ray r = camera.get_ray(u, v);
            Vec3 color = ray_color(r);
            
            write_color(file, color);
        }
    }
    
    std::cerr << "\nDone.\n";
    return 0;
}

void write_color(std::ostream& out, Vec3 color) {
    int r = static_cast<int>(255.999 * color.x);
    int g = static_cast<int>(255.999 * color.y);
    int b = static_cast<int>(255.999 * color.z);
    
    out << r << " " << g << " " << b << "\n";
}

出力されるのは青から白へのグラデーション。空みたいな色になる。

Colorクラスを分ける

Vec3をそのまま色に使ってるけど、意図を明確にするためにエイリアスを作っておこう。

using Color = Vec3;
using Point3 = Vec3;

こうすると、関数のシグネチャが分かりやすくなる。

// ❌ 何がなんだかわからない
Vec3 ray_color(const Ray& r, const Vec3& background);

// ⭕ 意図が明確
Color ray_color(const Ray& r, const Color& background);

ユーティリティ関数

よく使う数学関数をまとめておこう。

#include <cmath>
#include <limits>
#include <random>

// 定数
const double infinity = std::numeric_limits<double>::infinity();
const double pi = 3.1415926535897932385;

// 度数法 → ラジアン
inline double degrees_to_radians(double degrees) {
    return degrees * pi / 180.0;
}

// クランプ
inline double clamp(double x, double min, double max) {
    if (x < min) return min;
    if (x > max) return max;
    return x;
}

// 乱数(0〜1)
inline double random_double() {
    static std::mt19937 gen(42);
    static std::uniform_real_distribution<double> dist(0.0, 1.0);
    return dist(gen);
}

// 乱数(min〜max)
inline double random_double(double min, double max) {
    return min + (max - min) * random_double();
}

ここまでのまとめ

トピック ポイント
ベクトル 位置・方向を表す。内積と外積が重要
レイ 始点 + t * 方向 で任意の点を表す
カメラ 各ピクセルに対してレイを生成
PPM 簡単な画像フォーマット

次回は球と平面の交差判定を実装して、実際にオブジェクトを描画するよ。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?