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 | 簡単な画像フォーマット |
次回は球と平面の交差判定を実装して、実際にオブジェクトを描画するよ。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!