序文
この一連の記事へのリンク
レイ トレーシング コードをゼロから学ぶ (1) - レイ トレーシング in One Weekend で
レイ トレーシング コードをゼロから学ぶ (2) - レイ トレーシング in One Weekend で
レイ トレーシング コードをゼロから学ぶ (3) ——レイ トレーシングある週末
最後に書いたとき、そのときのLiyue、ごめんなさい、ステージから降りました。前回の本では、独自の 3 次元ベクトル クラスを定義し、ライトを定義し、シーンに 2 つのボールを配置したと述べていました。このセクションの主な目的は、アンチエイリアシングとマテリアルの問題の解決です。この部分は、「週末のレイ トレーシング」の第 7 章から第 9 章に相当します。
コンテンツ
アンチエイリアシング
ランダム関数
なぜ乱数について話すのかという問題は、後ほど説明します。乱数を生成するための最も基本的な方法の 1 つは、 rand() 関数を使用することです。[0, 1) の間の数値を取得するには、次の関数を使用するだけです。関係式: rand() / (RAND_MAX + 1.0)。任意の 2 つの数値の間の乱数を取得したい場合は、関係式: min + (max-min)*random_num を使用します。random_num は [0 , a 1) の間の乱数。
ただし、この従来の C++ 乱数生成方法は、一様分布に準拠しない標準的な乱数ではないため、C++11 標準の <random> ヘッダー ファイルを使用して実装し、uniform_real_distribution を使用します。 mt19973 を使用する 一様分布に従う乱数を得るために乱数を生成します。任意の間隔で乱数を取得するには、次の関係式を使用します: min + (max-min) * random_num。random_num は、生成したばかりの一様分布乱数です。uniform_real_distribution と mt19973 については、リファレンス ([2]-[4]) にアクセスして、リファレンス ブログのリンクを参照してください。
#include <random>
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}
MSAA アンチエイリアシング
コンピュータでは、ラスタライゼーションと呼ばれるピクセルを介して画像を表示することは誰もが知っていますが、これにより問題が発生します。さらさら感が足りません。これはサンプリング レートが不十分な結果ですが、理論的には、十分なピクセルがあれば、知覚の点でギザギザを感じることはありません。ただし、十分なピクセルがない場合があるため、サンプリング レートを人為的に上げて、この種のエイリアシングによって引き起こされる外観の悪さをできるだけ減らす必要があります。具体的な方法は、ピクセル内で複数回サンプリングし、これらのサンプルの値を組み合わせて平均を出し、得られた平均値がピクセル位置の色になります。この方法では、ピクセルを追加するような滑らかな効果は得られませんが、エッジの一部の色を程度の差はあれ明るくするなど、ギザギザ感を弱めるなど、若干の改善は可能です。具体的な方法を以下の概略図に示します。
ここで少し休憩し、カメラを Camera.h という名前のクラスとして定義します。このうち、コンストラクターは主にビューポートを定義し、左下隅の点を見つけるために使用されます。次に、光を取得する機能も必要になります。つまり、カメラの位置を起点として、写真内の点の位置とカメラを結んだ線がその方向の光になります。
#ifndef CAMERA_H
#define CAMERA_H
#include "rtweekend.h"
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 = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0.0, 0.0);
vertical = vec3(0.0, viewport_height, 0.0);
lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
#endif
境界を越えないように制限する関数が必要で、超えた場合は最大値を返し、下回った場合は最小値を返す関数がツールキットに組み込まれています。
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
}
write_color 関数の主な更新は、取得したピクセルの rgb 値をサンプリング ポイントの数で除算し、平均を計算して、ポイントの真の色を取得することです。
void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
// Divide the color by the number of samples.
auto scale = 1.0 / samples_per_pixel;
r *= scale;
g *= scale;
b *= scale;
// Write the translated [0,255] value of each color component.
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
最後は main 関数です。更新しましょう。画像に追加のループを追加しました。ループの数は、samples_per_pixel で、ピクセルを何つのサンプリング ポイントに分割するかを示します。ループでは、以前に作成したランダム関数を使用して [0, 1) の間の数値を生成します。これは、このピクセルで構成される小さな正方形でサンプリングし、カメラからその点への光線を取得することを意味します。 。ピクセルのサンプリング ポイントを走査した後、write_color 関数を渡して、そのポイントの計算された実際のピクセルを書き込みます。
#include "camera.h"
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
// World
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
// Camera
camera cam;
// Render
std::cout << "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) {
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 = cam.get_ray(u, v);
pixel_color += ray_color(r, world);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
最終的な効果は次のとおりです。これは局所的な拡大の効果です。左側はアンチエイリアスなし、右側はアンチエイリアシングの結果です。
拡散マテリアル
ここからは徐々に本題のレイトレーシングに入っていきますが、いわゆるレイトレーシングとは、端的に言うと、光がどの方向に進み、どのような物体に当たり、どのような反射や屈折が起こるのかという光の軌跡をシミュレーションすることです。 . .
拡散反射
ピクセルごとに複数の光線があるため、光が乱反射する、粗い見た目のオブジェクトなどのリアルなオブジェクトを作成できます。いわゆる拡散反射は、粗い表面での光の反射です。
拡散反射物体は、それ自体が発光するのではなく、周囲の環境の色を示し、自らの色を媒介するだけで、これらの物体に光が当たると、光の反射方向はランダムになります。 。
では、そのような現象をどのように説明すればよいのでしょうか? 次に、それを詳細に分析しましょう。
まず、カメラの光をたどり、次に光がオブジェクトに当たります。交点も取得します。この交点では、この交点における法線ベクトルも取得する必要があります。n ⃗ \vec{n}である点nしたがって、この法線に従って球の中心p + n ⃗ p+\vec{n}を取得できます。p+n、球の中心に沿って単位球を形成し、この単位球内に点 s をランダムに生成します。最後に s と p を接続して方向ベクトルを取得します。この方向ベクトルを通過する光の拡散反射として使用します。この点は後方方向です。次に、交点 p をソースとし、方向はs ⃗ − p ⃗ \vec{s}-\vec{p}となります。s−p新しい光線を取得し、その光線の軌跡を利用して次の交差と反射を行うことを繰り返すことで、光線と物体との間の拡散反射の結果をシミュレートできます。
プロセスの概略図を以下に示します。
まずアイデアを整理しましょう。最初にs ⃗ − p ⃗ \vec{s}-\vec{p} を使用する必要がありますs−p球の中心として単位球を作成し、この単位球内に位置する点をランダムに生成します。でも正直、ちょっと面倒です。特定の点を中心とした単位球を作るのが非常に面倒です。それなら考え方を変えた方が良いでしょう。最終目標は s ⃗ − p ⃗ \ を要求することです。 vec{s}- \ vec{p}s−p, ベクトルの性質を利用すると、p + n ⃗ + [ s ⃗ − ( p + n ⃗ ) ] p + \vec{n} + [\vec{s}-(p + \vec{n}) を得ることができます。 】p+n+[s−( p+n)],結果⃗ − ( p + n ⃗ ) \vec{s}-(p + \vec{n})s−( p+n)は、実際にはランダムな点から球の中心に向かうベクトルであり、そのベクトルは移動中に変化しないことがわかっているので、単位球内でランダムな点を見つけて、そのランダムな点を接続するという問題を単純化できます。点と原点が利用可能です。
vec3 でランダムなベクトルを生成する関数を定義しましょう
class vec3 {
public:
...
inline static vec3 random() {
return vec3(random_double(), random_double(), random_double());
}
inline static vec3 random(double min, double max) {
return vec3(random_double(min,max), random_double(min,max), random_double(min,max));
}
次に、単位球内にランダムな点を定義します。プロセスは、生成するランダムな 3 次元ベクトルのモードが 1 未満になるまで大まかにループし、その後この点を返します。
vec3 random_in_unit_sphere() {
while (true) {
auto p = vec3::random(-1,1);
if (p.length_squared() >= 1) continue;
return p;
}
}
その後、明るい色を返す関数を更新して再帰関数に変え、再帰のフォーカス、つまり設定した dept 変数を設定します。深さが 0 になると、(0, 0, 0) を返します。 )、または、反射光線がオブジェクトに遭遇しない場合は、環境の色を返します。
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0, infinity, rec)) {
point3 target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
最後に、メイン関数は、ここでは主に最大深度、つまり光の反射の最大数を増やし、ray_color 関数の呼び出しを変更します。
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;
...
// Render
std::cout << "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) {
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 = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
効果は以下に示されています。オブジェクト全体がざらざらしていて影があることがわかります。
ガンマを使用して色の強度を修正する
上記の効果では、ボールが非常に暗く見えることがわかります。これは、実際の値を表示していないためです。実際、オブジェクトは明るい灰色であるはずです。たとえば、コンピュータに保存されている明るさが 0.5 の場合 (輝度範囲は 0 ~ 1)、CRT ディスプレイの出力輝度は 0.5 ではなく、約 0.218 になります。このとき、表示結果を実際の色に補正するためにガンマ補正を実行する必要があります。このガンマ値は通常 2.2 です。グラデーションの場合、この時点ではガンマ値として 2 を直接使用します。つまり、色は元の I から x \sqrt xに変更されるはずです。バツ、つまり元の 1/2 乗です。したがって、write_color 関数を見つけて、RGB 値をルート記号の形式に変更する必要があります。
void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
// Divide the color by the number of samples and gamma-correct for gamma=2.0.
auto scale = 1.0 / samples_per_pixel;
r = sqrt(scale * r);
g = sqrt(scale * g);
b = sqrt(scale * b);
// Write the translated [0,255] value of each color component.
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
修正された結果は次のとおりです。
最後に、影の歪みの問題も解決できます。つまり、オブジェクトの hit 関数を呼び出すときに、t_min を 0 から 0 に近いが 0 より大きい値に変更できます。
if (world.hit(r, 0.001, infinity, rec))
ランバート反射
この部分がよくわかりません。理解できる読者はプライベートメッセージを送ったり、コメントエリアで議論したりできますが、この部分の操作は簡単です。上で求めた単位球内のランダムベクトルを標準化して、単位球上でのランダム ベクトル、回路図、およびコードは次のとおりです。
inline vec3 random_in_unit_sphere() {
...
}
vec3 random_unit_vector() {
return unit_vector(random_in_unit_sphere());
}
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) {
point3 target = rec.p + rec.normal + random_unit_vector(); // 改动在此处
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
最終的な効果は以下の通りで、前回とあまり変わりませんが、強いて違いをあげるとすれば、影が明るくなったことで、例えば球の真下の影がよりはっきりと見えてきました。両方の球が浅くなっているのは明らかです。
半球散乱
正直に言うと、この部分はブロガーの間でもまだかなり混乱しているので、詳しくわかってから追記します 具体的な方法は、単位球内でランダムにベクトルを生成し、そのベクトルをフィルターで除去するというものですつまり、2 つのベクトルの点積の値が 0 より大きい場合、それ以外の場合は逆転します。いずれにしても、最終結果は、法線ベクトルと同じ半球内にあるベクトルでなければなりません。法線ベクトル。
vec3 random_in_hemisphere(const vec3& normal) {
vec3 in_unit_sphere = random_in_unit_sphere();
if (dot(in_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal
return in_unit_sphere;
else
return -in_unit_sphere;
}
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) {
point3 target = rec.p + random_in_hemisphere(rec.normal); // 改动的地方
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
結果を以下に示します。法線ベクトルと同じ半球からのランダムなベクトルのみを保持しているため、影が明らかに明るくなっていることがわかります。
金属素材
マテリアル抽象クラス
私たちの上にあるマテリアルは拡散反射マテリアルです。今度は金属的な雰囲気を持つマテリアルを作成したいと考えています。これらは 2 つのまったく異なるマテリアル タイプですが、共通点が 1 つあります。それは、すべてオブジェクト マテリアルであるということです。当然、マテリアルの抽象クラスを作成し、さまざまなマテリアルのサブクラスを生成します。
このマテリアルの抽象クラスで満たす必要がある条件は、第一に散乱光を生成できること、第二に散乱が発生した場合に光がどの程度減衰するか、です。そこで、入射光、交点、光吸収率、散乱光を主パラメータとする仮想関数を設計した。
#ifndef MATERIAL_H
#define MATERIAL_H
#include "rtweekend.h"
struct hit_record;
class material {
public:
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const = 0;
};
#endif
マテリアル クラスを定義し、マテリアルがオブジェクトのプロパティになったので、オブジェクトのマテリアルと呼ばれる属性を hit_record に追加し、スマート ポインタを使用してこのクラスをポイントします。このようにして、光がこのオブジェクトに当たると、最初にこのオブジェクトに割り当てたマテリアルに従って交点のマテリアルを設定し、パッケージ化して次のステップに進むことができます。
#include "rtweekend.h"
class material;
struct hit_record {
point3 p;
vec3 normal;
shared_ptr<material> mat_ptr;
double t;
bool front_face;
inline void set_face_normal(const ray& r, const vec3& outward_normal) {
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal :-outward_normal;
}
};
同時に、そのようなマテリアル属性をオブジェクト クラスに追加し、交点が見つかったらその交点にマテリアル属性を割り当てる必要もあります。
class sphere : public hittable {
public:
sphere() {
}
sphere(point3 cen, double r, shared_ptr<material> m) // 构造函数增加了材质
: center(cen), radius(r), mat_ptr(m) {
};
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
public:
point3 center;
double radius;
shared_ptr<material> mat_ptr; // 增加材质属性
};
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
...
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mat_ptr; // 将物体的材质赋给交点
return true;
}
ランバート反射クラス
すでにマテリアル クラスがあるので、前のセクションの拡散反射で説明したランバート反射を継承し、散乱関数を書き換えて散乱光を生成できます。このクラスでは、反射率、つまり、光の減衰については、以前非常に単純かつ無作法に 0.5 に設定しました。
もちろん、その前に、もう 1 つ注意する必要があります。つまり、ランダムな単位ベクトルを生成するときに、それが法線ベクトルと逆の場合、加算すると互いに打ち消し合って 0 になります。そのため、光の散乱はないと考えられており、後で大きな問題が発生します(無限大またはNAN)。ベクトルが0になるかどうかを判断するのはベクトルの特徴なので、vec3クラスではベクトルが0に近いベクトルであるかどうかを判断することができます。判定方法は以下の通りです。
class vec3 {
...
bool near_zero() const {
// Return true if the vector is close to zero in all dimensions.
const auto s = 1e-8;
return (fabs(e[0]) < s) && (fabs(e[1]) < s) && (fabs(e[2]) < s);
}
...
};
以下はランバート反射を定義するクラスです。マテリアル クラスを継承します。メンバー関数は反射率、光の減衰は屈折率です。それ以外はすべて以前の内容と一致しています。
class lambertian : public material {
public:
lambertian(const color& a) : albedo(a) {
}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
auto scatter_direction = rec.normal + random_unit_vector();
// Catch degenerate scatter direction
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
public:
color albedo;
};
鏡面光反射
金属には特定の鏡面光反射があることがわかっています。これは、単に金属がその表面で環境の物体や色の一部を反射する可能性があることを意味します。では、どうすればこの効果を達成できるのでしょうか?
拡散反射とは対照的に、この鏡面光反射はランダムに散乱しません。では、反射光をどのように計算するかについては、以下の模式図を参照してください。要求した赤いベクトルが反射ベクトルであることがわかります。v ⃗ + 2 B ⃗ \vec{v}+2\vec{B}となるはずです。v+2B。
以下で、質問の仕方を詳しく分析してみましょう。原書のこの部分は簡単な紹介にすぎないため、以下はブロガー自身の理解です。以下の図では、入射光線はv ⃗ \vec{v}です。v、法線ベクトルはn ⃗ \vec{n}です。n、反射された光線はv ⃗ '' \vec{v}'ですv'。n ⃗ \vec{n}であることがわかっています。nは単位ベクトルですが、v ⃗ \vec{v}vいいえ。光の入射角は反射角と等しいので、関係− 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ = − v ⃗ + v ⃗ ′ -2(\vec{v}・\vec{n}) \ vec {n}= -\vec{v} +\vec{v}'− 2 (v⋅n)n=−v+v′、したがって− 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ + v ⃗ = v ⃗ ′ -2(\vec{v}·\vec{n}) \vec{n} + \vec{v} = \vec {の}'− 2 (v⋅n)n+v=v'。
ここで説明します− 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ -2(\vec{v}・\vec{n}) \vec{n}− 2 (v⋅n)n、四角形ルールによれば、− v ⃗ + v ⃗ ′ -\vec{v} +\vec{v}' がわかります。−v+v'は対角線のベクトルを取得しますが、n ⃗ \vec{n}nは単位ベクトルです。正しい結果を取得したい場合は、v ⃗ \vec{v}を計算する必要があります。vn ⃗ \vec{n}でn方向の投影のサイズを 2 倍して対角線のサイズを取得し、さらにn ⃗ \vec{n}を掛けます。n対角ベクトルを取得します。そして、前にマイナス記号があるのは、v ⃗ \vec{v}であるためです。vが内側である場合、計算する角度は (π - α \alphaα ),而cos(π -α \alphaα ) = -cos(α \alphaα ) ですが、必要なのは正の値だけなので、それをオフセットするために負の符号を追加します。
要約すると、vec3.h に反射関数を追加でき、返されるベクトルは− 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ + v ⃗ -2(\vec{v}·\vec{n }) \vec となります。 {n} + \vec{v}− 2 (v⋅n)n+v。
vec3 reflect(const vec3& v, const vec3& n) {
return v - 2*dot(v,n)*n;
}
次に、マテリアル クラスから継承したメタル クラスを定義します。これは、反射が拡散反射から鏡面反射に変わることを除けば、Lambert 反射クラスとほぼ同じです。さらに、返された結果では、反射結果と交点の法線が同じ半球上にあること、つまり 2 つのベクトルを乗算した結果が 0 より大きいことも確認する必要があります。
class metal : public material {
public:
metal(const color& a) : albedo(a) {
}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
};
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) {
ray scattered;
color attenuation;
if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) // 此处更改
return attenuation * ray_color(scattered, world, depth-1);
return color(0,0,0);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
...
#include "material.h"
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;
// World
hittable_list world;
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); // 这里修改,添加了物体和材质,参数是颜色
auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8));
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2));
world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right));
// Camera
camera cam;
// Render
std::cout << "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) {
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 = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
結果は以下の通りで、左右に鏡面反射球、中央と下に拡散反射球があり、鏡面反射面には他の物体が映っていることが分かります。
ぼやけた反射
この金属につや消しのテクスチャを追加することができます。つまり、ブラーを追加します。追加する方法は、反射に単位ボールを追加し、その中でボールをランダムにポイントし、交点とランダムなポイントを接続することです。新しい反射光を形成します。模式図は次のとおりです。
ファジーパラメータを追加して、ファジィネスを制御することもできます。
class metal : public material {
public:
metal(const color& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {
} // 修改构造函数
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere()); // 添加随机向量
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
double fuzz; // 添加模糊度参数
};
int main() {
...
// World
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
...
}
最終的な効果は以下のようになり、マット テクスチャの追加レイヤーが追加されます。
要約する
この部分は主にアンチエイリアスとレイ トレーシングに関するものですが、アンチエイリアスとは単純にサンプリング レートを人為的に高めて、できるだけ多くのカラー値を 1 つのピクセルに収集できるようにすることです。レイトレーシングの部分は光の経路をシミュレートするものですが、拡散反射と鏡面反射もシミュレートし、拡散反射用と鏡面反射用の 2 つのオブジェクトを定義しました。この連載は今のところ終わりに近づいており、最終回で終了の予定です、ご覧いただきありがとうございます。
参考文献
[1]レイ トレーシング in One Weekend
[2] C++11 実践ガイド (2. 重要な機能 - より強力な機能: 正規表現、タイミング ツール、ランダム分布ジェネレーター) [3]
C ++ STL-- mt19937
[4] C+ +11乱数学習
[5]ガンマ補正(ガンマ補正)