7
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

はじめに

ここまでで、かなり綺麗な画像が作れるようになった。

でも、間接光(他の物体からの反射光)がまだ足りない。

今回はパストレーシングを実装して、よりリアルな画像を目指そう。

レンダリング方程式

すべてのレンダリング技術の基礎となる方程式。

$$L_o(\mathbf{x}, \omega_o) = L_e(\mathbf{x}, \omega_o) + \int_\Omega f_r(\mathbf{x}, \omega_i, \omega_o) L_i(\mathbf{x}, \omega_i) (\omega_i \cdot \mathbf{n}) d\omega_i$$

Lo = 出力される光(求めたい値)
Le = 自己発光
fr = BRDF(双方向反射率分布関数)
Li = 入射光
n  = 法線
Ω  = 半球

この積分をモンテカルロ法で近似するのがパストレーシング。

モンテカルロ積分

基本原理

積分をランダムサンプリングで近似する。

$$\int f(x) dx \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f(x_i)}{p(x_i)}$$

N = サンプル数
xi = ランダムサンプル
p(xi) = xiのサンプリング確率密度

なぜこれが使えるの?

大数の法則:サンプル数を増やすと真の値に収束する。

N = 10    → ノイズだらけ
N = 100   → なんとなく見える
N = 1000  → 結構綺麗
N = 10000 → ほぼノイズなし

発光マテリアル

まず、光源を実装しよう。

class DiffuseLight : public Material {
public:
    std::shared_ptr<Texture> emit;
    
    DiffuseLight(std::shared_ptr<Texture> a) : emit(a) {}
    DiffuseLight(const Color& c) : emit(std::make_shared<SolidColor>(c)) {}
    
    bool scatter(const Ray& r_in, const HitRecord& rec,
                 Color& attenuation, Ray& scattered) const override {
        return false;  // 発光するだけで散乱しない
    }
    
    Color emitted(double u, double v, const Point3& p) const override {
        return emit->value(u, v, p);
    }
};

Materialクラスの修正

class Material {
public:
    virtual ~Material() = default;
    
    virtual bool scatter(
        const Ray& r_in,
        const HitRecord& rec,
        Color& attenuation,
        Ray& scattered
    ) const = 0;
    
    virtual Color emitted(double u, double v, const Point3& p) const {
        return Color(0, 0, 0);  // デフォルトは発光しない
    }
};

レイカラーの修正

発光を考慮する。

Color ray_color(const Ray& r, const Hittable& world, int depth) {
    if (depth <= 0) {
        return Color(0, 0, 0);
    }
    
    HitRecord rec;
    
    // 何にも当たらなければ背景色(または環境マップ)
    if (!world.hit(r, 0.001, infinity, rec)) {
        return Color(0, 0, 0);  // 黒(光源なし)
    }
    
    Ray scattered;
    Color attenuation;
    Color emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
    
    // 散乱しなければ発光のみ
    if (!rec.mat_ptr->scatter(r, rec, attenuation, scattered)) {
        return emitted;
    }
    
    // 発光 + 反射光
    return emitted + attenuation * ray_color(scattered, world, depth - 1);
}

コーネルボックス

パストレーシングのテストに使われる定番シーン。

HittableList cornell_box() {
    HittableList objects;
    
    auto red   = std::make_shared<Lambertian>(Color(0.65, 0.05, 0.05));
    auto white = std::make_shared<Lambertian>(Color(0.73, 0.73, 0.73));
    auto green = std::make_shared<Lambertian>(Color(0.12, 0.45, 0.15));
    auto light = std::make_shared<DiffuseLight>(Color(15, 15, 15));
    
    // 壁
    objects.add(std::make_shared<Quad>(Point3(555,0,0), Vec3(0,555,0), Vec3(0,0,555), green));   // 右(緑)
    objects.add(std::make_shared<Quad>(Point3(0,0,0), Vec3(0,555,0), Vec3(0,0,555), red));       // 左(赤)
    objects.add(std::make_shared<Quad>(Point3(0,0,0), Vec3(555,0,0), Vec3(0,0,555), white));     // 床
    objects.add(std::make_shared<Quad>(Point3(555,555,555), Vec3(-555,0,0), Vec3(0,0,-555), white)); // 天井
    objects.add(std::make_shared<Quad>(Point3(0,0,555), Vec3(555,0,0), Vec3(0,555,0), white));   // 奥
    
    // 光源
    objects.add(std::make_shared<Quad>(Point3(213,554,227), Vec3(130,0,0), Vec3(0,0,105), light));
    
    // ボックス
    std::shared_ptr<Hittable> box1 = make_box(Point3(0,0,0), Point3(165,330,165), white);
    box1 = std::make_shared<RotateY>(box1, 15);
    box1 = std::make_shared<Translate>(box1, Vec3(265,0,295));
    objects.add(box1);
    
    std::shared_ptr<Hittable> box2 = make_box(Point3(0,0,0), Point3(165,165,165), white);
    box2 = std::make_shared<RotateY>(box2, -18);
    box2 = std::make_shared<Translate>(box2, Vec3(130,0,65));
    objects.add(box2);
    
    return objects;
}

四角形(Quad)クラス

class Quad : public Hittable {
public:
    Point3 Q;    // 頂点
    Vec3 u, v;   // 2辺のベクトル
    std::shared_ptr<Material> mat;
    Vec3 normal;
    double D;
    Vec3 w;
    
    Quad(const Point3& Q, const Vec3& u, const Vec3& v, 
         std::shared_ptr<Material> m)
        : Q(Q), u(u), v(v), mat(m)
    {
        auto n = cross(u, v);
        normal = normalize(n);
        D = dot(normal, Q);
        w = n / dot(n, n);
    }
    
    bool hit(const Ray& r, double t_min, double t_max, 
             HitRecord& rec) const override {
        auto denom = dot(normal, r.direction);
        
        // 平行チェック
        if (std::fabs(denom) < 1e-8) {
            return false;
        }
        
        auto t = (D - dot(normal, r.origin)) / denom;
        if (t < t_min || t > t_max) {
            return false;
        }
        
        // 四角形の内部かチェック
        auto intersection = r.at(t);
        Vec3 planar = intersection - Q;
        auto alpha = dot(w, cross(planar, v));
        auto beta = dot(w, cross(u, planar));
        
        if (alpha < 0 || alpha > 1 || beta < 0 || beta > 1) {
            return false;
        }
        
        rec.t = t;
        rec.p = intersection;
        rec.mat_ptr = mat;
        rec.set_face_normal(r, normal);
        rec.u = alpha;
        rec.v = beta;
        
        return true;
    }
    
    AABB bounding_box() const override {
        return AABB(Q, Q + u + v).pad();
    }
};

重点サンプリング(Importance Sampling)

ランダムにサンプリングするより、光源に向かってサンプリングしたほうが効率的。

┌──────────────────────────────────────────────────────────────┐
│ 重点サンプリング                                             │
│                                                              │
│   一様サンプリング           重点サンプリング                │
│   ↗ ↗ ↑ ↖ ↖                     ↗↗↗                         │
│     ╲ ● ╱                        ╲ ● ╱                       │
│       ↓                           ↓                          │
│                                                              │
│   ほとんどが無駄             光源に集中 → 効率的            │
└──────────────────────────────────────────────────────────────┘

PDF(確率密度関数)

class PDF {
public:
    virtual ~PDF() = default;
    
    virtual double value(const Vec3& direction) const = 0;
    virtual Vec3 generate() const = 0;
};

// コサイン重み付きPDF(Lambertian用)
class CosinePDF : public PDF {
public:
    ONB uvw;
    
    CosinePDF(const Vec3& w) { uvw.build_from_w(w); }
    
    double value(const Vec3& direction) const override {
        auto cosine = dot(normalize(direction), uvw.w);
        return (cosine <= 0) ? 0 : cosine / pi;
    }
    
    Vec3 generate() const override {
        return uvw.local(random_cosine_direction());
    }
};

// 光源に向かうPDF
class HittablePDF : public PDF {
public:
    Point3 origin;
    std::shared_ptr<Hittable> ptr;
    
    HittablePDF(std::shared_ptr<Hittable> p, const Point3& origin)
        : ptr(p), origin(origin) {}
    
    double value(const Vec3& direction) const override {
        return ptr->pdf_value(origin, direction);
    }
    
    Vec3 generate() const override {
        return ptr->random(origin);
    }
};

混合PDF

複数のPDFを組み合わせる。

class MixturePDF : public PDF {
public:
    std::shared_ptr<PDF> p[2];
    
    MixturePDF(std::shared_ptr<PDF> p0, std::shared_ptr<PDF> p1) {
        p[0] = p0;
        p[1] = p1;
    }
    
    double value(const Vec3& direction) const override {
        return 0.5 * p[0]->value(direction) + 0.5 * p[1]->value(direction);
    }
    
    Vec3 generate() const override {
        if (random_double() < 0.5) {
            return p[0]->generate();
        } else {
            return p[1]->generate();
        }
    }
};

レイカラーの改良版

PDFを使った実装。

Color ray_color(const Ray& r, const Hittable& world, 
                const std::shared_ptr<Hittable>& lights, int depth) {
    if (depth <= 0) {
        return Color(0, 0, 0);
    }
    
    HitRecord rec;
    
    if (!world.hit(r, 0.001, infinity, rec)) {
        return Color(0, 0, 0);
    }
    
    ScatterRecord srec;
    Color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
    
    if (!rec.mat_ptr->scatter(r, rec, srec)) {
        return emitted;
    }
    
    if (srec.skip_pdf) {
        // 鏡面反射などはPDFを使わない
        return srec.attenuation * ray_color(srec.skip_pdf_ray, world, lights, depth - 1);
    }
    
    // 混合PDF:光源 + コサイン重み
    auto light_ptr = std::make_shared<HittablePDF>(lights, rec.p);
    MixturePDF mixed_pdf(light_ptr, srec.pdf_ptr);
    
    Ray scattered = Ray(rec.p, mixed_pdf.generate());
    auto pdf_val = mixed_pdf.value(scattered.direction);
    
    double scattering_pdf = rec.mat_ptr->scattering_pdf(r, rec, scattered);
    
    Color sample_color = ray_color(scattered, world, lights, depth - 1);
    
    return emitted + srec.attenuation * scattering_pdf * sample_color / pdf_val;
}

ロシアンルーレット

深い再帰を確率的に打ち切る方法。

Color ray_color(const Ray& r, const Hittable& world, int depth) {
    HitRecord rec;
    
    if (!world.hit(r, 0.001, infinity, rec)) {
        return Color(0, 0, 0);
    }
    
    // ロシアンルーレット
    double continue_probability = 0.8;  // 80%の確率で続行
    if (depth > 3 && random_double() > continue_probability) {
        return rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
    }
    
    // ... 通常の処理
    
    // 打ち切り確率で補正
    return result / continue_probability;
}

これで無限再帰でも正しい期待値が得られる。

NEE(Next Event Estimation)

各ヒットポイントで直接光を明示的にサンプリング。

Color direct_light(const Point3& p, const Vec3& normal, 
                   const std::shared_ptr<Hittable>& lights) {
    Color direct(0, 0, 0);
    
    // 光源上のランダムな点をサンプル
    Vec3 to_light = lights->random(p) - p;
    double distance_squared = to_light.length_squared();
    to_light = normalize(to_light);
    
    // 遮蔽チェック
    Ray shadow_ray(p, to_light);
    HitRecord shadow_rec;
    
    if (world.hit(shadow_ray, 0.001, std::sqrt(distance_squared) - 0.001, shadow_rec)) {
        return Color(0, 0, 0);  // 遮られている
    }
    
    // 光源からの寄与を計算
    double cosine = std::fmax(0, dot(normal, to_light));
    double pdf = lights->pdf_value(p, to_light);
    
    Color light_emission = lights->material->emitted(...);
    
    return light_emission * cosine / (pdf * distance_squared);
}

MIS(Multiple Importance Sampling)

複数のサンプリング戦略を最適に組み合わせる方法。

double balance_heuristic(double pdf_a, double pdf_b) {
    return pdf_a / (pdf_a + pdf_b);
}

double power_heuristic(double pdf_a, double pdf_b, double beta = 2.0) {
    double a = std::pow(pdf_a, beta);
    double b = std::pow(pdf_b, beta);
    return a / (a + b);
}

並列化

レイトレーシングはembarrassingly parallel。各ピクセルを独立に計算できる。

#include <omp.h>

int main() {
    std::vector<Color> framebuffer(image_width * image_height);
    
    #pragma omp parallel for schedule(dynamic, 1)
    for (int j = image_height - 1; j >= 0; --j) {
        for (int i = 0; i < image_width; ++i) {
            Color pixel_color(0, 0, 0);
            
            for (int s = 0; s < samples_per_pixel; ++s) {
                auto u = (i + random_double()) / (image_width - 1);
                auto v = (j + random_double()) / (image_height - 1);
                Ray r = camera.get_ray(u, v);
                pixel_color += ray_color(r, world, lights, max_depth);
            }
            
            framebuffer[j * image_width + i] = pixel_color / samples_per_pixel;
        }
        
        // 進捗表示(スレッドセーフに)
        #pragma omp critical
        {
            std::cerr << "\rScanlines remaining: " << j << " " << std::flush;
        }
    }
    
    // ファイル出力
    // ...
}

最終結果

1000サンプル/ピクセルでレンダリングした結果:

  • 直接光のみ:硬い影、暗い
  • パストレーシング:柔らかい影、自然な色の滲み
  • NEE + MIS:ノイズが少ない、効率的

まとめ

トピック ポイント
レンダリング方程式 全ての光の伝搬を記述
モンテカルロ積分 ランダムサンプリングで近似
重点サンプリング 光源に向かってサンプル
ロシアンルーレット 確率的な再帰の打ち切り
NEE 直接光を明示的にサンプル
MIS 複数戦略の最適な組み合わせ

このシリーズで、レイトレーシングの基礎から応用まで一通り学んだ。

もっと学びたい人へのおすすめ:

ぜひ自分でも実装して、3DCGの世界を楽しんでね!

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

7
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
7
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?