Search

Metal

Created
2021/03/27
Tags
Chapter 9

1. An Abstract Class for Materials

이전 Chatper에서는 난반사를 일으키는 Object에 대해서 구현을 해보았다. Object의 질감이 난반사를 일으키는 것만 있는 것이 아니니, 다른 재료로 만들어진 Object를 원한다면 어떻게 해야할까? 보편적으로 작용하는 하나의 재료를 두고 여러 인자를 받아 각기 다른 재료를 나타내도록 할 수도 있겠지만, Abstract Class로 캡슐화하여 재료에 대한 행위들을 정의하는 것이 조금 더 낫겠다. 따라서 material이라고 하는 Class를 정의할 것인데, 여기에는 다음과 같은 2가지 특성이 들어가야 한다.
1.
흩어진 ray를 만들어 낼 것
2.
만일 흩어졌다면, ray가 얼마나 감쇄해야 하는지 정할 것
material.h의 코드는 아래와 같다.
#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
C++

2. A Data Structure to Describe Ray-Object Intersections

Pixel에 존재하는 hittable 객체에 대해서 ray가 닿았을 때, 필요한 정보들을 hit_record를 이용해서 많은 인자들을 넘기는 상황을 피할 수 있었다. 물론 필요한 인자들을 모두 넘기는 방법을 이용할 수도 있었겠지만, 우리는 현재 상태를 유지하여 Object가 가진 materialhit_recordmaterial로 기록시켜서 ray의 변화를 재질에 따라서 다르게 줄 수 있도록 할 것이다.
모든 Object (물체)는 특정 Material (재료)로 만들어진다. 따라서 hittable 객체 내에 material이라는 객체를 가질 수 있도록 할 것이다. 이것이 곧 어떤 재료로 만들어졌는지에 대한 표현이 되겠다. 또한 Projection Space에 존재하는 이 hittable 객체를 각 Pixel에 따라서 처리할 때 hit_record를 이용 했었는데, hittableray가 닿을 시 hittablematerial에 따라서 ray의 변화를 줄 수 있어야하므로 hit_recordhittablematerial을 받아서 ray를 처리하게 된다. 따라서 material에 대한 정보는 shared_ptr로 공유할 수 있도록 한다. (shared_ptr로 처리한 이유는 곧 알 수 있다.)
hittable.h는 다음과 같이 수정한다.
#ifndef HITTABLE_H # define HITTABLE_H # 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 hittable { public: virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0; }; #endif
C++
우리가 지금까지 준비한 것들은 materialObject 표면에 어떻게 작용하게 될 지 정할 수 있도록 한 것이다. Object에 닿았을 때 ray를 처리하기 위한 모든 정보들을 hit_record가 갖고 있기 때문에 hit_record만 인자로 넘겨주면 처리가 가능하다. 구체와 같은 Objectray가 닿았다고 한다면, hit_record 구조체를 인자로 넘겼기 때문에 Object 객체가 갖고 있는 material 정보를 hit_recordmaterial로 기록해두는 식으로 처리하면 된다.
material 역시 Class로 구현되어 있는데 Object들 간에 동일한 material로 되어 있는 경우 굳이 중복으로 여럿 만들 필요가 없기 때문에 중복되지 않는 하나의 material만 두고 공유할 수 있도록 hittablehit_record에서 materialshared_ptr로 처리한 것이다.
hit_record에 기록된 정보에 따라 ray_color 함수에서 Pixel의 색상을 처리하려는 경우, hittablematerial을 갖고 있는 hit_recordmaterial 정보를 이용하여 ray의 흩어짐에 따라 색상을 처리하도록 두면 된다. 이것이 가능하도록 기존에 작성한 sphere Classmaterial을 갖도록 수정하고, spherehit 함수에서 hit_record에 자신이 갖고 있는 material을 기록할 수 있도록 수정해주면 된다. 코드는 아래와 같다.
#ifndef SPHERE_H # define SPHERE_H # include "hittable.h" # include "vec3.h" class sphere : public hittable { public: sphere() {} sphere(point3 cen, double r, shared_ptr<material> m) : center(cen), radius(r), mat_ptr(m) {} virtual ~sphere() {} 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 { vec3 oc = r.origin() - center; auto a = r.direction().length_squared(); auto half_b = dot(oc, r.direction()); auto c = oc.length_squared() - radius * radius; auto discriminant = half_b * half_b - a * c; if (discriminant < 0) return (false); auto sqrtd = sqrt(discriminant); // Find the nearest root that lies in the accpetable range. auto root = (-half_b - sqrtd) / a; if (root < t_min || t_max < root) { root = (-half_b + sqrtd) / a; if (root < t_min || t_max < root) return (false); } 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); } #endif
C++

3. Modeling Light Scatter and Reflectance

난반사에서 이용했던 Lambertian (혹은 Hemisphere Scattering)을 material로써 구현해보자. 이에 대해서 난반사되는 ray가 반사율 RR에 따라서 감쇄하거나 감쇄하지 않더라도 1R1-R로 색을 흡수하거나 혹은 이 두가지 모두를 사용할 수 있다는 것을 이미 알고 있다. 아래 코드를 보면 난반사 발생 시에 별도의 감쇄 없이 albedo라는 색을 그대로 이용했는데, 원한다면 확률 pp 감쇄가 일어날 때 albedo / pp로 색상을 주어도 된다.
#ifndef MATERIAL_H # define MATERIAL_H # include "rtweekend.h" # include "vec3.h" struct hit_record; class material { public: virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0; }; 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(); scattered = ray(rec.p, scatter_direction); attenuation = albedo; return (true); } public: color albedo; }; #endif
C++
위 코드는 ClassWrapping만 했기 떄문에 이전 코드와 별반 다를 것이 없는데, 이전부터 우리는 한 가지 오류 발생 가능성을 캐치하지 못한 채로 이용을 해왔다. 코드를 유심히 살펴보면 random_unit_vector는 수 많은 경우 중에 Normal과 정확히 반대 방향을 이루는 Vector를 만들어 낼 수도 있다는 점이다. 이렇게 되면 두 Vector를 더했을 때 Zero Vector가 나오면서 빛의 흩어짐을 만들어내지 못하고 infinity 혹은 NaN과 같은 결과를 얻게 되면서 의도치 못한 오류로 이어질 수 있다. 따라서 이에 대해 방어해줄 수 있는 코드를 작성해줘야 한다. 이 코드를 만들어 낼 때는 정확히 Zero Vector가 나왔는지 등호로써 비교하여 결과를 얻어내는 것보다, 0에 근사한 부동 소수점을 주고 그 소수점보다 작은지 판단하여 처리하는 것이 더 낫다. 이 함수를 near_zero라고 하고 vec3 Class에 추가해준다. 코드는 아래와 같다.
#ifndef VEC3_H # define VEC3_H # include "rtweekend.h" # include <cmath> # include <iostream> using std::sqrt; class vec3 { public: vec3() : e{0,0,0} {} vec3(double e0, double e1, double e2) : e{e0, e1, e2} {} double x() const { return e[0]; } double y() const { return e[1]; } double z() const { return e[2]; } vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } double operator[](int i) const { return e[i]; } double& operator[](int i) { return e[i]; } vec3& operator+=(const vec3 &v) { e[0] += v.e[0]; e[1] += v.e[1]; e[2] += v.e[2]; return *this; } vec3& operator*=(const double t) { e[0] *= t; e[1] *= t; e[2] *= t; return *this; } vec3& operator/=(const double t) { return *this *= 1/t; } double length() const { return sqrt(length_squared()); } double length_squared() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } 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)); } 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))); } public: double e[3]; }; // Type aliases for vec3 using point3 = vec3; // 3D point using color = vec3; // RGB color // vec3 Utility Functions inline std::ostream& operator<<(std::ostream &out, const vec3 &v) { return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2]; } inline vec3 operator+(const vec3 &u, const vec3 &v) { return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]); } inline vec3 operator-(const vec3 &u, const vec3 &v) { return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]); } inline vec3 operator*(const vec3 &u, const vec3 &v) { return vec3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]); } inline vec3 operator*(double t, const vec3 &v) { return vec3(t*v.e[0], t*v.e[1], t*v.e[2]); } inline vec3 operator*(const vec3 &v, double t) { return t * v; } inline vec3 operator/(vec3 v, double t) { return (1/t) * v; } inline double dot(const vec3 &u, const vec3 &v) { return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]; } inline vec3 cross(const vec3 &u, const vec3 &v) { return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1], u.e[2] * v.e[0] - u.e[0] * v.e[2], u.e[0] * v.e[1] - u.e[1] * v.e[0]); } inline vec3 unit_vector(vec3 v) { return v / v.length(); } inline vec3 random_in_unit_sphere() { while (true) { auto p = vec3::random(-1, 1); if (p.length_squared() >= 1) continue; return (p); } } vec3 random_unit_vector() { return (unit_vector(random_in_unit_sphere())); } 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); return (-in_unit_sphere); } #endif
C++
따라서 vec3.hnear_zero를 반영하여 material.h를 다시 작성해보면 아래와 같이 (Normal과 정확히 반대 방향이어서 Zero Vector가 나온 것이므로) Normal로 만들어 주는 구문이 추가된 것을 볼 수 있다.
#ifndef MATERIAL_H # define MATERIAL_H # include "rtweekend.h" # include "vec3.h" struct hit_record; class material { public: virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0; }; 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(); if (scatter_direction.near_zero()) scatter_direction = rec.normal; scattered = ray(rec.p, scatter_direction); attenuation = albedo; return (true); } public: color albedo; }; #endif
C++

4. Mirrored Light Reflection

Metal에도 여러 재질이 있지만, 그 중에서도 굉장히 매끄러운 Full-Metal임을 가정하고 생각해보자. Full-Metal은 난반사처럼 무작위로 ray를 퍼뜨리지 않는다. Full-Metal에서는 ray가 거울 같이 반사되는데, 이를 어떻게 구할 수 있는지 유심히 생각해봐야 한다. 이를 Vector로 생각하면 굉장히 쉽다.
우리는 위 그림에서 붉은 색의 반사각을 나타내는 Vector를 구해야하는데, 입사각으로 들어온 VV를 연장하고 BB를 이용하면 쉽게 구할 수 있다. 아래 쪽에 위치한 VV의 방향이 만약에 반대를 가리키고 있다면, 2B2B에서 해당 벡터를 뺀 것이 반사각 Vector가 된다. (Vector의 뺄셈) 따라서 VV가 반대 방향일 때 2B2B에서 Vector의 뺄셈을 했던 것이기 때문에 현재 VV의 방향이라면 2B2B에서 Vector의 덧셈을 해주면 반사각 Vector가 된다. 이에 따라 반사각 VectorV+2BV + 2B가 된다.
VVrayVector이므로 ray의 시작점과 방향을 알고 있고 물체에 닿은 사실에 따라 거리를 구해 그 크기도 쉽게 구할 수 있다. 따라서 V+2BV+2B라는 수식에서 BB라는 VectorVV로 풀어낼 수 있으면 쉬운 수식이 되는 것을 알 수 있다. 이는 ray가 닿은 지점의 Normal nn을 이용하면 쉽다. nnray가 닿은 지점에서 수직을 이루고 BB 역시 그러하다는 것이 힌트가 될 수 있다. (다만 nnUnit Vector인데 반해 BBUnit Vector임을 보장하지 않는다.) 바로 VVnnDot Product을 이용한 것이다. 아래의 풀이에서는 VV의 방향이 반대를 가리키고 있다는 것을 가정으로 작성한 것임을 유의하여 살펴보자.
Vn=V×n×cosθV \cdot n = |V| \times |n| \times cos\theta
V×n×cosθ=V×n×BV=B|V| \times |n| \times cos\theta = |V| \times |n| \times \frac{|B|} {|V|} = |B|
BB의 길이가 곧 VnV \cdot n이므로 Vector BBn×B=n×(Vn)n \times |B| = n \times (V \cdot n)이 된다. 주어진 값은 V의 방향이 반대일 때 구한 값이므로 올바른 방향에 대해서 정정해보면, BB는 마이너스 기호가 붙은 n×(Vn)-n\times(V\cdot n)이 된다. 따라서 반사각 VectorV2n×(Vn)V - 2n \times (V \cdot n)이다.
난반사를 lambertian 객체의 scatter 함수로 정의하여 ray가 어떻게 흩어지는지 코드로 작성했던 것처럼 위 수식을 이용하여 정반사도 metal 객체의 scatter 함수로 정의해보자. 수정된 material.h의 코드는 아래와 같다.
#ifndef MATERIAL_H # define MATERIAL_H # include "rtweekend.h" # include "vec3.h" struct hit_record; class material { public: virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0; }; class lambertian : public material { public: lambertian(const color& a) : albedo(a) {} virtual ~lambertian() {} virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { auto nothing = r_in.direction(); unit_vector(nothing); auto scatter_direction = rec.normal + random_unit_vector(); if (scatter_direction.near_zero()) scatter_direction = rec.normal; scattered = ray(rec.p, scatter_direction); attenuation = albedo; return (true); } public: color albedo; }; class metal : public material { public: metal(const color &a) : albedo(a) {} virtual ~metal() {} 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; }; #endif
C++
위 코드에 작성된 reflect 함수는 vec3.h에 정의해준다.
#ifndef VEC3_H # define VEC3_H # include "rtweekend.h" # include <cmath> # include <iostream> using std::sqrt; class vec3 { public: vec3() : e{0,0,0} {} vec3(double e0, double e1, double e2) : e{e0, e1, e2} {} double x() const { return e[0]; } double y() const { return e[1]; } double z() const { return e[2]; } vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } double operator[](int i) const { return e[i]; } double& operator[](int i) { return e[i]; } vec3& operator+=(const vec3 &v) { e[0] += v.e[0]; e[1] += v.e[1]; e[2] += v.e[2]; return *this; } vec3& operator*=(const double t) { e[0] *= t; e[1] *= t; e[2] *= t; return *this; } vec3& operator/=(const double t) { return *this *= 1/t; } double length() const { return sqrt(length_squared()); } double length_squared() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } 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)); } 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))); } public: double e[3]; }; // Type aliases for vec3 using point3 = vec3; // 3D point using color = vec3; // RGB color // vec3 Utility Functions inline std::ostream& operator<<(std::ostream &out, const vec3 &v) { return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2]; } inline vec3 operator+(const vec3 &u, const vec3 &v) { return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]); } inline vec3 operator-(const vec3 &u, const vec3 &v) { return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]); } inline vec3 operator*(const vec3 &u, const vec3 &v) { return vec3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]); } inline vec3 operator*(double t, const vec3 &v) { return vec3(t*v.e[0], t*v.e[1], t*v.e[2]); } inline vec3 operator*(const vec3 &v, double t) { return t * v; } inline vec3 operator/(vec3 v, double t) { return (1/t) * v; } inline double dot(const vec3 &u, const vec3 &v) { return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]; } inline vec3 cross(const vec3 &u, const vec3 &v) { return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1], u.e[2] * v.e[0] - u.e[0] * v.e[2], u.e[0] * v.e[1] - u.e[1] * v.e[0]); } inline vec3 unit_vector(vec3 v) { return v / v.length(); } inline vec3 random_in_unit_sphere() { while (true) { auto p = vec3::random(-1, 1); if (p.length_squared() >= 1) continue; return (p); } } vec3 random_unit_vector() { return (unit_vector(random_in_unit_sphere())); } 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); return (-in_unit_sphere); } vec3 reflect(const vec3& v, const vec3& n) { return (v - 2 * dot(v, n) * n); } #endif
C++
lambertianmetal이라는 material을 상속 받은 Class를 정의함에 따라 material을 이용하여 색상을 결정할 수 있도록 ray_color 함수도 수정해준다. main.cpp는 아래와 같다.
#include "rtweekend.h" #include "color.h" #include "hittable_list.h" #include "sphere.h" #include "camera.h" #include "vec3.h" #include <iostream> color ray_color(const ray& r, const hittable& world, int depth) { hit_record rec; // If we've exceeded the ray bound 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); } int main(void) { // 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; 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, max_depth); } write_color(std::cout, pixel_color, samples_per_pixel); } } std::cerr << "\nDone.\n"; return (0); }
C++

5. A Scene with Metal Spheres

위에서 metal이라는 Class를 정의함에 따라 hittable로 만들어진 ObjectFull-Metal 재질을 가질 수 있게 되었다. 이에 따라 main.cpp에 해당 Object를 생성해보자. 코드는 아래와 같다. (단순히 사용하려는 material을 선언 후에 hittable_listworldmaterial을 지정한 hittable 객체만 추가해준다.)
#include "rtweekend.h" #include "color.h" #include "hittable_list.h" #include "sphere.h" #include "camera.h" #include "vec3.h" #include "material.h" #include <iostream> color ray_color(const ray& r, const hittable& world, int depth) { hit_record rec; // If we've exceeded the ray bound 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); } int main(void) { // 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"; return (0); }
C++
실행 결과로 난반사 Object 1개와 정반사 Object 2개를 world에서 확인할 수 있다.

6. Fuzz Reflection

Metal 재질에는 Full-Metal 뿐 아니라 Fuzz (부스럼)과 같은 표면을 가진 Metal도 존재한다. Fuzz-Metal은 완벽한 정반사를 하는 것이 아닌, 정반사 지점이 약간의 무작위성을 띄어 일부 난반사의 특징을 갖는다. 난반사의 특징은 정반사 지점에 구체를 두고 구체 내부를 포함한 공간에서 임의의 점을 잡아 새로운 점을 두어 만들 수 있다. 정반사 지점에 두는 구체의 크기가 클수록 Fuzzier하게 만들 수 있어 난반사 특징을 더 갖게 할 수도 있다. 여기서 구체의 크기를 직접 조정하는 방식 대신에 단위 구체의 반지름을 계수로 조정할 수 있는 Parameter를 두어 Fuzziness를 반영하도록 구현하면 수훨하다. (만일 Parameter 값이 0이라면 이는 Perturbation (섭동)을 의미하게 된다.)
따라서 metalfuzz 값을 추가한 material.h의 코드는 아래와 같다.
#ifndef MATERIAL_H # define MATERIAL_H # include "rtweekend.h" # include "vec3.h" struct hit_record; class material { public: virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0; }; class lambertian : public material { public: lambertian(const color& a) : albedo(a) {} virtual ~lambertian() {} virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override { auto nothing = r_in.direction(); unit_vector(nothing); auto scatter_direction = rec.normal + random_unit_vector(); if (scatter_direction.near_zero()) scatter_direction = rec.normal; scattered = ray(rec.p, scatter_direction); attenuation = albedo; return (true); } public: color albedo; }; class metal : public material { public: metal(const color &a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {} virtual ~metal() {} 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; }; #endif
C++
Full-Metal을 포함한 Fuzz-Metal을 만들 수 있는 코드로 간단히 수정되었다면, main.cpp에서 metal 객체를 이용하는 구체의 생성자 함수로 들어가는 인자를 수정해보자.
#include "rtweekend.h" #include "color.h" #include "hittable_list.h" #include "sphere.h" #include "camera.h" #include "vec3.h" #include "material.h" #include <iostream> color ray_color(const ray& r, const hittable& world, int depth) { hit_record rec; // If we've exceeded the ray bound 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); } int main(void) { // 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), 0.15); auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0); 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"; return (0); }
C++
실행하면 좌측은 Fuzz가 상대적으로 약하여 Full-Metal과 유사하고, 우측은 Fuzz가 최대치로 나타나 난반사 특징을 다수 갖고 있는 형태인 것을 확인할 수 있다.