Take you from scratch to ray tracing code by hand (2) - Ray Tracing in One Weekend

foreword

Links to this series of articles

      Take you to ray tracing code from scratch (1) - Ray Tracing in One Weekend
      take you to ray tracing code from scratch (2) - Ray Tracing in One Weekend
      take you to ray tracing code from scratch (3)——Ray Tracing in One Weekend

   The last time I wrote, Liyue at that time, I'm sorry, I got off stage. The last book said that we defined our own three-dimensional vector class, defined the light, and put two balls into the scene. Our main purpose in this section is anti-aliasing and solving material problems. This part corresponds to Chapters 7 to 9 of "Ray Tracing in One Weekend".

anti-aliasing

random function

   The question of why we talk about random numbers will be discussed later. One of the most basic methods for us to generate random numbers is to use the rand() function. To get a number between [0, 1), you only need Use the relational expression: rand() / (RAND_MAX + 1.0), if you want to get a random number between any two numbers, use the relational expression: min + (max-min)*random_num, where random_num is a [0 , a random number between 1).
   However, this traditional C++ method of generating random numbers is not a standard random number, which does not conform to a uniform distribution. Therefore, we use the <random> header file of the C++11 standard to implement it, and use uniform_real_distribution to generate a uniform distribution. Using mt19973 Generate random numbers so that we can get a random number that conforms to a uniform distribution. To get random numbers in any interval, use the relational formula: min + (max-min) * random_num, where random_num is the uniformly distributed random number we just generated. Regarding uniform_real_distribution and mt19973, you can go to the reference ([2]-[4]) to see the reference blog link.

#include <random>

inline double random_double() {
    
    
    static std::uniform_real_distribution<double> distribution(0.0, 1.0);
    static std::mt19937 generator;
    return distribution(generator);
}
Contents of rtweekend.h

MSAA anti-aliasing

   We all know that in computers, we display images through pixels, which is called rasterization. However, this will lead to a problem, that is, jagged. Intuitively, there will be a stepped look and feel on the edge of the object, which is not silky enough. slip. This is a result of insufficient sampling rate. In theory, when we have enough pixels, we will not feel jagged in terms of perception. But sometimes there are not enough pixels, so we need to artificially increase the sampling rate to reduce the poor look and feel caused by this kind of aliasing as much as possible. The specific method is to sample multiple times in a pixel, and then combine the values ​​of these samples to make an average, and the obtained average value is the color of the pixel position. Although this method cannot achieve the silky smooth effect like adding pixels, it can be slightly improved. For example, the color of some parts of the edge will be lighter to varying degrees, so as to weaken the jagged feeling. The specific method is shown in the schematic diagram below:
insert image description here
   Here we can take a break for a while and define the camera as a class, named camera.h. Among them, the constructor is mainly to define the viewport, and to find the point in the lower left corner. Then there is also a need for a function to obtain light, that is, starting from the position of the camera, the line connecting the position of the point in the picture and the camera is the light in the direction.

#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
Contents of camera.h

   In order to prevent crossing the boundary, we need a function to limit it. When it exceeds, it returns the maximum value, and when it is less than it returns the minimum value. This function is put in the toolkit.

inline double clamp(double x, double min, double max) {
    
    
    if (x < min) return min;
    if (x > max) return max;
    return x;
}
Contents of rtweekend.h

   The main update of the write_color function is that it divides the obtained rgb value of the pixel by the number of sampling points, calculates an average, and obtains the true color of the point.

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';
}
Contents of color.h

   The last is the main function. Let’s update it. We added an extra loop to the image. The number of loops is samples_per_pixel, which indicates how many sampling points we divide a pixel into. In the loop, we also use the random function we wrote before to generate a number between [0, 1), which means that we sample in the tiny square composed of this pixel, and then get a The ray from the camera to the point. After traversing the sampling points of a pixel, pass in the write_color function to write the calculated real pixel of the point.

#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";
}
Contents of main.cpp

   The final effect is shown below. This is the effect of local enlargement. The left side is without anti-aliasing, and the right side is the result of anti-aliasing.

insert image description here

diffuse material

   From here, we gradually step into our main topic - ray tracing. The so-called ray tracing is to put it bluntly that we simulate the trajectory of light, which direction the light follows, what object it hits, and what reflection or refraction occurs. .

diffuse reflection

   Now that we have multiple rays per pixel, we can make some realistic objects, such as some rough-looking objects, which have diffuse reflection of light. The so-called diffuse reflection is the reflection of light on rough surfaces.
   Diffuse reflection objects themselves do not emit light, they just show the color of the surrounding environment, and then use their own color to mediate. When the light hits these objects, the reflection direction of the light is random. The schematic diagram is as follows .
insert image description here
   So how do we describe such a phenomenon? Next, let's analyze it in detail.
   First, we follow the light of the camera, and then the light hits the object. We also get an intersection point, and then at this intersection point, we should also get a normal vector at this intersection point. which is n ⃗ \vec{n}n , so we can follow this normal to get a sphere center p + n ⃗ p+\vec{n}p+n , we form a unit sphere along the center of the sphere, randomly generate a point s in this unit sphere, and finally we connect s and p to get a direction vector, we use this direction vector as the diffuse reflection of our light passing through this point backward direction. Then take the intersection point p as the source, and the direction is s ⃗ − p ⃗ \vec{s}-\vec{p}s p Get a new ray, and then use the trajectory of the ray to perform the next intersection and reflection. Repeating this can simulate the result of diffuse reflection between the ray and the object.
   A schematic diagram of the process is shown below.
insert image description here   First of all, let's sort out our ideas. We first need to use s ⃗ − p ⃗ \vec{s}-\vec{p}s p As the center of the sphere, create a unit sphere, and randomly generate a point located in this unit sphere. But to be honest, it is a bit troublesome. The process of creating a unit sphere with a specific point as the center is very troublesome. Then we might as well change our thinking. Our ultimate goal is to require s ⃗ − p ⃗ \vec{s}- \ vec{p}s p , using the properties of vectors, we can obtain p + n ⃗ + [ s ⃗ − ( p + n ⃗ ) ] p + \vec{n} + [\vec{s}-(p + \vec{n}) ]p+n +[s (p+n )],而s ⃗ − ( p + n ⃗ ) \vec{s}-(p + \vec{n})s (p+n ) is actually a vector from a random point to the center of the sphere, and we know that the vector will not change during the movement, so we can simplify the problem to find a random point in a unit sphere, and then connect the random point and the origin available.

   Let's define a function that generates a random vector in 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));
    }
Contents of vec3.h

   Then we define a random point in the unit sphere, and the process will roughly loop until the mode of the random three-dimensional vector we generate is less than 1, and then return this point.

vec3 random_in_unit_sphere() {
    
    
    while (true) {
    
    
        auto p = vec3::random(-1,1);
        if (p.length_squared() >= 1) continue;
        return p;
    }
}
Contents of vec3.h

   Afterwards, we update the function that returns the light color, turning it into a recursive function, and set the focus of the recursion, that is, the dept variable we set. When our depth becomes 0, it returns (0, 0, 0), Or when the reflected ray does not encounter an object, return the color of the environment.

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);
}
Contents of main.cpp

   Finally, the main function, here mainly increases the maximum depth, that is, the maximum number of light reflections, and modifies the call of the ray_color function.

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";
}
Contents of main.cpp

   The effect is shown below, we can see that the whole object looks rough and has shadows.
insert image description here

Correct color intensity with gamma

   We can see that the above effect looks like the ball is very dim. This is because we are not displaying the real value. In fact, the object should be a light gray. For example, if the brightness stored in your computer is 0.5 (Brightness range is 0~1), the output brightness of the CRT display is not 0.5, but approximately equal to 0.218. At this time, we need to perform gamma correction to correct the displayed result to the real color. This gamma value is generally 2.2. For the gradient, we directly use 2 as the gamma value at this time, that is, our color should change from the original I to x \sqrt xx , that is, the original 1/2 power. Therefore, we need to find our write_color function and modify the rgb value to the form of a root sign.

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';
}
Contents of color.h

   The corrected result is as follows:
insert image description here
   Finally, we can also solve the problem of shadow distortion, that is, we can change t_min from 0 to a value close to 0 but greater than 0 when calling the object's hit function.

if (world.hit(r, 0.001, infinity, rec)) 
Contents of main.cpp

insert image description here

lambert reflex

   I don’t understand this part very well. Readers who do understand can private message or discuss in the comment area, but the operation of this part is simple. It is to standardize the random vector in the unit sphere we obtained above and become it on the unit sphere. The random vectors, schematic and code are as follows:
insert image description here

inline vec3 random_in_unit_sphere() {
    
    
    ...
}
vec3 random_unit_vector() {
    
    
    return unit_vector(random_in_unit_sphere());
}
Contents of vec3.h
    After that, we also need to update in ray_color, replacing the original random_in_unit_sphere function with random_unit_vector function
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);
}
Contents of main.cpp

   The final effect is as shown below, which is not much different from the previous one. If we insist on the difference, the shadow is lighter. For example, the shadow directly below the sphere is more obvious. The shadow part of the previous section will be a little darker. It's all too obvious that both spheres have become shallower.
insert image description here

hemispherical scattering

   To be honest, this part of the bloggers is still quite confused, and I will add this part after I know more about it. The specific method here is that we randomly generate a vector in the unit sphere, and then we Filter out the ones that are in the same hemisphere as our normal vector, that is, the value of the point product of two vectors is greater than 0, otherwise it will be reversed. Anyway, the final result must be the vector that falls in the same hemisphere as the normal vector.

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;
}
Contents of vec3.h
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);
}
Contents of main.cpp

   The result is shown below, and we can see that since we only kept random vectors from the same hemisphere as the normal vector, the shadows are clearly lighter.
insert image description here

metallic material

Material abstract class

   The material above us is a diffuse reflection material, now we want to create a material with a metallic feel, these are two completely different material types, but they have one thing in common, that is, they are all object materials, then we can naturally Create an abstract class for materials, and then generate subclasses for different materials.
   The conditions that need to be satisfied in the abstract class of this material are: firstly, a scattered light can be produced, and secondly, how much the light is attenuated if scattering occurs. Accordingly, we designed a virtual function in which the parameters are mainly incident light, intersection point, light absorption rate, and scattered light.

#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
Contents of material.h

   Now that we have defined the material class, and the material is the property of the object, we can add an attribute to the hit_record called the material of the object, and we use a smart pointer to point to this class. In this way, when our light hits this object, we can set the material of the intersection point according to the material we originally assigned to this object, so as to package and proceed to the next step.

#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;
    }
};
Contents of hittable.h

   At the same time, we also need to add such a material attribute in our object class, and assign the material attribute to the intersection point when we find the intersection point.

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;
}
Contents of sphere.h

Lambert reflection class

   Now that we already have a material class, we can inherit the Lambert reflection mentioned in the diffuse reflection in the previous section, and then rewrite the scatter function to generate scattered light. In this class, we can define a reflectivity , that is, the attenuation of light, we have set it to 0.5 very simply and rudely before.
   Of course, before this, we need to pay attention to one more thing, that is, when we generate a random unit vector, if it happens to be opposite to our normal vector, it will cancel each other out and become 0 when added, so that It is thought that there will be no light scattering, and there will be big problems later (infinity or NAN). Since judging whether a vector will be 0 is a feature of a vector, we can judge whether the vector is a vector close to 0 in the vec3 class. The method of judgment is as follows.

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);
    }
    ...
};

Contents of vec3.h

   The following is the class that defines Lambert reflection. It inherits the material class. The member function is its reflectivity, and the attenuation of light is the refractive index. Everything else is consistent with our previous content.

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;
};
Contents of material.h

specular light reflection

   We know that metal has a certain specular light reflection, which simply means that it can reflect a part of the environment's objects and colors on its surface. So how do we achieve this effect?
   In contrast to diffuse reflection, this specular light reflection does not randomly scatter. So how do we calculate the reflected light, we can see the schematic diagram below. We can see that the red vector we requested is the reflection vector, it should be v ⃗ + 2 B ⃗ \vec{v}+2\vec{B}v +2B .
insert image description here
   Let's analyze how to ask in detail below. This part of the original book is just a brief introduction, so the following is the blogger's own understanding. In the diagram below, our incident ray is v ⃗ \vec{v}v , the normal vector is n ⃗ \vec{n}n , the reflected ray is v ⃗ ′ \vec{v}'v ' . We know thatn ⃗ \vec{n}n is a unit vector, which we have dealt with, but v ⃗ \vec{v}v no. Since the incident angle of light is equal to the reflection angle, the relationship − 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ = − v ⃗ + v ⃗ ′ -2(\vec{v}·\vec{n}) \vec {n}= -\vec{v} +\vec{v}'2(v n )n =v +v , therefore− 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ + v ⃗ = v ⃗ ′ -2(\vec{v}·\vec{n}) \vec{n} + \vec{v} = \vec {in}'2(v n )n +v =v ' .
   Explain here− 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ -2(\vec{v}·\vec{n}) \vec{n}2(v n )n , according to the quadrilateral rule, we know − v ⃗ + v ⃗ ′ -\vec{v} +\vec{v}'v +v will get the vector of the diagonal, butn ⃗ \vec{n}n Is a unit vector, if we want to get the correct result, we should calculate v ⃗ \vec{v}v at n ⃗ \vec{n}n The size of the projection in the direction, multiplied by 2 times to get the size of the diagonal, and then multiplied by n ⃗ \vec{n}n to get the diagonal vector. And the minus sign in front is because v ⃗ \vec{v}v is inward, the angle we calculate is (π - α \alphaα ),而cos(π -α \alphaα ) = -cos(α \alphaα ), but we only need positive values, so add a negative sign to offset it.
insert image description here
   In summary, we can add a reflective function to vec3.h, and the returned vector is− 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;
}
Contents of vec3.h

   Next, we define a metal class, inherited from the material class, which is roughly the same as the Lambert reflection class, except that the reflection changes from diffuse reflection to specular reflection. In addition, in the returned result, we also need to ensure that our reflection result and the normal of the intersection point are on the same hemisphere, that is, the result of multiplying the two vectors is greater than 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;
};
Contents of material.h
   Finally, we correct the ray_color function, so that when we hit the intersection point, we use our current method, combined with materials, and attenuation to calculate the real light color.
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);
}
Contents of main.cpp
   Having said so much above, we can create a scene application below. We create two diffuse reflection and two specular reflection objects in the scene.
...

#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";
}
Contents of main.cpp

   The following is our result. There are two specular reflection balls on the left and right sides, and diffuse reflection balls in the middle and bottom. We can see that other objects can be seen on the specular reflection.
insert image description here

blurred reflection

   We can add some frosted texture to this metal, that is, add some blur. The way to add it is to add a unit ball to the reflection, and then randomly point the ball in it, and connect the intersection point and random point to form a new reflected light. The schematic diagram is as follows:
insert image description here
   We can also add a fuzzy parameter to control the fuzziness

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;		// 添加模糊度参数
};
Contents of mateial.h
   In the main function, it is natural to modify the definition of the metal material object, because we have an extra blur as a parameter
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);
    ...
}
Contents of main.cpp

   The final effect is as shown below, with an extra layer of matte texture
insert image description here

Summarize

   This part is mainly about anti-aliasing and ray tracing. Anti-aliasing is simply to artificially increase the sampling rate, so that as many color values ​​as possible can be collected in one pixel. The ray tracing part is to simulate the path of light, and then we also simulated diffuse reflection and specular reflection, and defined two objects, two for diffuse reflection and two for specular reflection. So far, this series is coming to an end, and the plan is to end the last issue, thank you for watching.

references

   [1] Ray Tracing in One Weekend
   [2] C++11 Practice Guide (2. Important features - more powerful functions: regular expressions; timing tools; random distribution generator) [
   3] C++ STL-- mt19937
   [4 ] C++11 random number learning
   [5] Gamma Correction (Gamma Correction)

Guess you like

Origin blog.csdn.net/qq_43419761/article/details/128139487