Learn Ray Tracing One Weekend

Learn Ray Tracing One Weekend

Ray Tracing in One Weekend 是学习光追的优秀教程,接下来跟着一步一步实现一个小型的光追渲染器。数学公式显示存在问题,更好阅读体验见Learn Ray Tracing One Weekend

01 Create Image

  • 使用ppm格式

  • 没啥特别好说的,是将渲染的图像保存成ppm格式,win10下去ppmViewer打开

  • 01_create_image.cpp

  • run

    01_create_image.exe > 01_create_image.ppm
    

    去ppmViewer打开01_create_image.ppm

02 vec3 Class

  • 实现一个vec3工具类,具备大部分向量的功能,头文件见vec3.h

  • 值得注意的地方:

    • x(), y(), z()返回各三分量

    • operator + []/+=/*=//=, 重载常用的运算符

    • length(), length_sqarend(), 返回vec3向量的长度和长度平方

    • write_color(), 将vec3向量表示成rgb颜色值[0 ~ 255]写到std::cout

    • random() 生成取值[0, 1]的随机vec3向量

    • near_zero() 判断vec3向量三分量是否都是接近0

    • dot(), cross() vec3向量点乘,叉乘

    • unit_vector() 返回vec3 v的单位向量

    • random_unit_vector(), 生成取值[0, 1]的单位vec3向量

    • reflect()返回反射向量

03 Ray Color

  • 实现一个射线Ray类,$\mathbf{\vec{P}} = \mathbf{\vec{O}} + t\mathbf{\vec{d}}$. 头文件 -> ray.h

  • origdir对应射线原点$\mathbf{\vec{O}}$和射线方向$\mathbf{\vec{d}}$

  • at(double t) -> 实现$\mathbf{\vec{P}} = \mathbf{\vec{O}} + t\mathbf{\vec{d}}$

  • 光线追踪coarse框架

    • 以右手坐标系为例,z-轴背向画布朝外

    • Ray为从原点(0, 0, 0)往画布打出的射线,所以$\mathbf{\vec{O}} = (0, 0, 0)$, $\mathbf{\vec{d}}$为图中红色的向量所示

    • 方向向量$\mathbf{\vec{d}}$表示方式为:lower_left_corner + u * horizontal + v * vertical - origin

      • horizontal和vertical向量表示:

        aspect_radio = 16.0 / 9.0
        viewport_height = 2.0 # 画布范围[-1, 1]长度2
        viewport_width = aspect_radio * viewport_height
        
        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) # focal_length 相机到画布距离,换个说法画布在z-轴位置
        
    • 光追coarse框架代码

      def ray_color(ray, scene, t_min, t_max):
          closest_so_far = t_max
          pixel_color = (0, 0, 0)
          for object in scene:
              if ray.hit(object) == True:
                  if t_min < (ray.t at hit place) < closest_so_far:
                      pixel_color = surface color at place where ray hit object
          return pixel_color
      
      for i in range(image_width):
          for j in range(image_height):
              u = i / (image_width - 1)
              v = j / (image_height - 1)
              dir = lower_left_corner + u * horizontal + v * vertical - origin
              ray <- Ray(origin, dir)
              pixel_color = ray_color(ray, scene, 0, infinity)
      

04 Hit Sphere

  • 与球体求交,理论公式推导部分参考5.1 Ray-Sphere Intersection

  • 求交问题转换为求$\mathbf{\vec{P}} = \mathbf{\vec{O}} + t\mathbf{\vec{d}}$的$t$, 球体最后求解方程为:

    $$ \mathbf{\vec{d}} \cdot \mathbf{\vec{d}} t^2 + 2 \mathbf{\vec{d}} \cdot (\mathbf{\vec{O}} - \mathbf{\vec{C}}) t + (\mathbf{\vec{O}} - \mathbf{\vec{C}}) \cdot (\mathbf{\vec{O}} - \mathbf{\vec{C}}) - r^2 = 0$$

  • 上述方程的$\mathbf{\vec{C}}$和$r$为一个球体的球心和半径。其实这就是$ax^2 + bx + c = 0$一元二次方程,自变量为$t$

  • 因此当上述方程有两个根表示hit两次,一个根表示hit一次,无根表示不会相交

  • hit处返回的ray_color

    // 求解一元二次方程,判断是否相交,返回相交最近处的时间t
    double hit_sphere(const point3& center, double radius, const ray& r)
    {
        vec3 oc = r.origin() - center;
        // auto a = dot(r.direction(), r.direction());
        // auto b = 2.0 * dot(oc, r.direction());
        // auto c = dot(oc, oc) - radius * radius;
        // auto discriminant = b * b - 4 * a * c;
        // b = 2h 简化后
        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 -1.0;
        else
            return (-half_b - sqrt(discriminant)) / a;
    }
    
    auto t = hit_sphere(point3(0, 0, -1), 0.5, r);
    if (t > 0.0)
    {
        vec3 N = unit_vector(r.at(t) - vec3(0, 0, -1));
        return 0.5 * color(N.x() + 1, N.y() + 1, N.z() + 1);
    }
    

05 Hittable Objects

  • 上述的sphere的hit是怎针对场景中一个球体进行的,当场景里面有多个物体,在计算求交时候,可以考虑设计一个抽象类hittable,让会有hit的物体都继承自这个类,同时需要重写hit虚函数

    class hittable
    {
    public:
        virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
    };
    
    
  • 举个例子,sphere继承hittable类,重写hit虚函数,代码见sphere.h

    class sphere: public hittable // 继承hittable抽象类
    {
    public:
        vec3 center;
        double radius;
       
        ...
    
        // 重写hit函数
        virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override;
    };
    
    // Sphere的hit函数
    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;
    
        ...
        return false;
    }
    
  • 值得注意到,hit函数传入了一个hit_record结构体,用来记录相交时候的有关参数(位置p, 法向n, 相交时间t, 材质mat)

    // 相交记录结构体
    struct hit_record
    {
        vec3 p; // 相交位置 p = o + t * d
        vec3 normal; // 交点处面法向
       
        double t; // 相交处的时间 t
        bool front_face; // 射线ray与法向normal是否法向一致
        shared_ptr<Material> mat_ptr; // 材质的智能指针
        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_list: public hittable
    {
    public:
        std::vector<shared_ptr<hittable> > objects; // 记录相交的物体们
       
        ...
    
        void clear() {objects.clear();} // 清空
        void add(shared_ptr<hittable> object) {objects.push_back(object);} // 增加
    
        virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override;
    };
    
    // 重写hit函数,for循环下,对数组内每个物体进行求交
    bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const
    {
        hit_record temp_rec;
        bool hit_anything = false;
        auto closest_so_far = t_max;
    
        for (const auto& object: objects) // 对场景内每个物体
        {
            if (object->hit(r, t_min, closest_so_far, temp_rec)) // 在时间 t_min < t < closest_so_far内发生hit
            {
                hit_anything = true;
                closest_so_far = temp_rec.t; // 更新closest_so_far
                rec = temp_rec;
            }
        }
    
        return hit_anything;
    }
    
  • 至此,一个光追雏形代码可以如下写出,具备光追渲染器的基本组成,代码见05_hittable_objects

    #include "InWeekend/rtweekend.h"
    
    #include "InWeekend/hittable_list.h"
    #include "InWeekend/sphere.h"
    
    #include <iostream>
    
    // 射线透过画布打到场景返回的颜色
    color ray_color(const ray& r, const hittable& world)
    {
        hit_record rec;
        if (world.hit(r, 0, infinity, rec))
        {
            return 0.5 * (rec.normal + color(1, 1, 1)); // 暂时用法向替代rgb颜色
        }
        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()
    {
        // 图像(画布)参数
        const auto aspect_ratio = 16.0 / 9.0;
        const int image_width = 400;
        const int image_height = static_cast<int>(image_width / aspect_ratio);
    
        // 场景内物体加入数组中
        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));
    
        // 相机参数
        auto viewport_height = 2.0;
        auto viewport_width = aspect_ratio * viewport_height;
        auto focal_length = 1.0;
    
        auto origin = point3(0, 0, 0);
        auto horizontal = vec3(viewport_width, 0, 0);
        auto vertical = vec3(0, viewport_height, 0);
        auto lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
    
        // 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)
            {
                auto u = double(i) / (image_width - 1);
                auto v = double(j) / (image_height - 1);
                ray r(origin, lower_left_corner + u * horizontal + v * vertical - origin); // 射线 o + t * d
                color pixel_color = ray_color(r, world);
                pixel_color.write_color(std::cout);
            }
        }
    
        std::cerr << "\nDone.\n";
    }
    
  • 运行效果:

06 Antialiasing

  • 一般的抗锯齿都会想到MSAA等多重采样方案,光追其实也可以多采样完成抗锯齿

  • 只不过这里的抗锯齿是多重采样从原点$\mathbf{\vec{O}}$发射出去的射线,这就用到random_double()生成多个[-1, 1]范围的vec3,给每个画布上的像素点加个扰动构成多重采样射线

    for (int j = image_height-1; j >= 0; --j)
    {
        std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
        for (int i = 0; i < image_width; ++i)
        {
            // 对每个像素额外采样samples_per_pixel个射线
            // 获取到光追颜色后再取平均
            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);
            }
            pixel_color.write_color(std::cout, samples_per_pixel);
        }
    }
    
    

07 Diffuse Materials

  • 漫反射:当一个光线打到漫反射材质物体表面上,发生反射的方向是随机的。

  • Ray Tracing in One Weekend这本书中介绍了三种反射出去的方向计算方式,这里挑其中一个说明

  • 如图所示,ray在$\mathbf{P}$处发生hit,$\mathbf{\vec{N}}$为面法向,从球心出发向外。那么设计漫反射方向$\mathbf{\vec{PS}}$,其中$\mathbf{S}$是以$\mathbf{P} + \mathbf{\vec{N}}$为球心的单位圆上,也就是$\mathbf{\vec{PS}} = \mathbf{\vec{PN}} + \mathbf{\vec{NS}}$

  • 代码如下:

    // 返回[-1, 1]随机vec3向量
    vec3 random_unit_vector()
    {
        return unit_vector(random_in_unit_sphere());
    }
    
    // 递归函数,模拟反射过程
    color ray_color(const ray& r, const hittable& world, int depth)
    {
        // 递归终点,反射超过50次,就此返回全黑(0, 0, 0)
        if (depth <= 0)
            return color(0, 0, 0);
    
        hit_record rec;
        if (world.hit(r, 0.001, infinity, rec))
        {
            point3 target = rec.p + rec.normal + random_unit_vector(); // point S
            return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth - 1);
            // target - rec.p = PS向量
        }
        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);
    }
    
    
  • gamma校正,对渲染出来的图像进行gamma校正来修正图像,gamma校正理论见图像处理之gamma校正

  • 经过gamma校正后的最终效果:

08 Metal Materials

  • Materials Abstract Class

    • 材质抽象类设计目的:用以被各不同材质类继承,重写scatter函数实现不同材质对光线反射
    class material {
    public:
        virtual bool scatter(
                const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
            ) const = 0;
    };
    
  • Metal Material

    • 金属材质特性:镜面反射

    refection

    绿色光线v入射,红色光线出射,C++代码表示

    vec3 reflect(const vec3& v, const vec3& n) {
        return v - 2 * dot(v, n) * n; // dot(v, n) * n -- B vector
    }
    
  • Ray at Metal surface

    • 当光线打到金属表面时,发生如上镜面反射。对于Metal类来说,重写scatter函数:
    class Metal: public Material
    {
    public:
        color albedo;
        Metal(const color& a): albedo(a) {}
    
        virtual bool scatter(const ray& ray_in, const hit_record& rec, color& attenuation, ray& scattered) const override
        {
            vec3 reflected = reflect(unit_vector(ray_in.direction()), rec.normal);
            scattered = ray(rec.p, reflected); // 反射入射光线
            attenuation = albedo; // 衰减
            return (dot(scattered.direction(), rec.normal) > 0);
        }
    };
    
    • 光追上色,ray_color函数
    color ray_color(const ray& r, const hittable& world, int depth)
    {
        ...
    
        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);
        }
        
        ...
    }
    
    • 效果:

  • Fuzz Metal

    • 带有粗糙质感的金属:给反射方向加个随机扰动,扰动半径fuzz取值[0, 1]之间,取值越大,表面粗糙质感越强烈

    • 扰动加入
    class Metal: public Material
    {
    public:
        
        ... 
    
        virtual bool scatter(const ray& ray_in, const hit_record& rec, color& attenuation, ray& scattered) const override
        {
            vec3 reflected = reflect(unit_vector(ray_in.direction()), rec.normal);
            scattered = ray(rec.p, reflected + fuzz * random_in_unit_sphere()); // fuzz 表示粗糙质感的扰动半径,半径越大粗糙感越强
            attenuation = albedo;
            return (dot(scattered.direction(), rec.normal) > 0);
        }
    };
    
    • 效果:
    // in 08_metal_materials.cpp
    // World
    ...
    auto material_left   = make_shared<Metal>(color(0.8, 0.8, 0.8), 0.3); // fuzz = 0.3
    auto material_right  = make_shared<Metal>(color(0.8, 0.6, 0.2), 1.0); // fuzz = 1.0
    ...
    

09 Dielectrics

  • 绝缘体:一条光线打过来,会发生折射反射

  • Snell Law

    • 斯奈尔定律:$\eta \cdot \sin{\theta} = \eta' \cdot \sin{\theta'}$

    • 空气 $\eta = 1.0$

  • 折射方向

    $$ \vec{t} = \frac{\eta}{\eta'} (\vec{v} - (\vec{t} \cdot \mathbf{n}) \mathbf{n} ) - \mathbf{n} \sqrt{1 - (\frac{\eta}{\eta'} (\vec{v} - (\vec{v} \cdot \mathbf{n}) \mathbf{n}))^2}, $$

    • 全反射:注意,并不是所有的光线打过来都会有折射,当入射方向的角度超过一定范围后,是全反射了,这个范围按照如下公式确定:

    $$ \sin{\theta} = \sqrt{1 - \vec{v} \cdot \mathbf{n}} \leq 1.0. $$

    • 综上,折射方向C++表示如下
    class Dielectric : public Material {
    public:
        double ir; // 当前材质的\eta
        Dielectric(double index_of_refraction): ir(index_of_refraction) {}
    
        virtual bool scatter(const ray& ray_in, const hit_record& rec, color& attenuation, ray& scattered) const override 
        {
            attenuation = color(1.0, 1.0, 1.0);
            double refraction_ratio  = rec.front_face? (1.0/ir): ir; // \eta / \eta'
    
            vec3 unit_direction = unit_vector(ray_in.direction());
            double cos_theta = ffmin(dot(-unit_direction, rec.normal), 1.0);
            double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
            
            bool cannot_refract = refraction_ratio * sin_theta > 1.0; // 是否能够折射还是全反射了
            vec3 direction;
            if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_double())
                direction = reflect(unit_direction, rec.normal); // 反射方向
            else
                direction = refract(unit_direction, rec.normal, refraction_ratio); // 折射方向
    
            scattered = ray(rec.p, direction);
            return true;
        }
    }
    
    // 折射方向
    vec3 refract(const vec3& ray_in, const vec3& n, double etai_over_etat) 
    {
        auto cos_theta = fmin(dot(-ray_in, n), 1.0);
        vec3 r_out_vertical =  etai_over_etat * (ray_in + cos_theta * n); // R_vertical
        vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_vertical.length_squared())) * n; // R_parallel
        return r_out_vertical + r_out_parallel; 
    }
    
  • reflect probability -- 反射概率

    • What is: 对于一个绝缘体来说,当满足折射条件(不发生全反射)后,并不是一定发生折射,会有一个概率发生反射,这个反射的概率跟很多因素(折射率、入射角度)有关,采用schlick简化之后:

    $$ Rp = R_0 + (1 - R_0) (1 - \cos{\theta})^5, R_0 = (\frac{\eta - \eta'}{\eta + \eta'})^2. $$

    static double reflectance(double cosine, double ref_idx)
    {
        auto r0 = (1 - ref_idx) / (1 + ref_idx);
        r0 = r0 * r0;
        return r0 + (1 - r0) * pow((1 - cosine), 5);
    }
    
    • 上色:一个像素打出去N多个射线,接着计算发生折射和反射的着色,最后求这N条光线返回颜色的平均。
    virtual bool scatter(const ray& ray_in, const hit_record& rec, color& attenuation, ray& scattered) const override 
    {
        ...
        
        bool cannot_refract = refraction_ratio * sin_theta > 1.0;
        vec3 direction;
        // 随机生成数,判断是否在反射概率范围内
        if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_double())
            direction = reflect(unit_direction, rec.normal);
        else
            direction = refract(unit_direction, rec.normal, refraction_ratio);
    
        ...
    }
    
    // in 09_Dielectrics.cpp
    ...
    
    for (int j = image_height-1; j >= 0; --j) 
    {
        for (int i = 0; i < image_width; ++i) 
        {
            color pixel_color(0, 0, 0);
            // 一个像素打出去N多个射线,最后求平均
            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);
            }
            pixel_color.write_color(std::cout, samples_per_pixel);
        }
    }
    
    ...
    
    
    • 效果:

10 LookAt & Depth-of-field

  • LookAt

    • What is: 当相机摆放在世界空间下的任意位置,面朝着渲染场景,LookAt就是来描述这个面朝的“姿态”。

    • 坐标轴描述:实际上面朝可以理解为在相机处建立一套新的局部坐标系,这个坐标系的三个坐标轴u-v-g,表示相机朝向的姿态。

      • g-axis: 从场景原点$\mathbf{O}$指向相机中心方向

      • v-axis: 垂直g-axisup向量组成的平面的向量,可以通过g x up获得。其中up在这里可以用(0, 1, 0)

      • u-axis: 垂直g-axisv-axis向量组成的平面的向量,v x g获得。

      origin = point3(0, 0, 0);
      g_ = unit_vector(origin - camera_pos);
      v_ = unit_vector(cross(g_, up));
      u_ = unit_vector(cross(v_, g_));
      
    • 形象化理解,就像人脸对着场景内物体(比如茶杯子)看,鼻子正对着茶杯看做是g-axis, 右耳朵看做是v-axis, 天灵盖朝上看做u-axis, 这三个轴可以完整反映出人脸对着茶杯物体姿态。

    • LookAt矩阵:上述描述的是相机看场景的“姿态”,还缺少一个重要信息:相机位置$\mathbf{p}$。加上这个信息,可以将场景内物体从世界坐标系转换到该相机坐标系中。

      • 相机位置$\mathbf{P} = (P_x, P_y, P_z)$.

      • 相机面朝局部坐标系uvg: $[\mathbf{\vec{v}}, \mathbf{\vec{u}}, \mathbf{\vec{g}}]$

      • LookAt矩阵:

      $$ \mathbf{M}_{lookAt} =
      \begin{bmatrix}
      v_x & v_y & v_z & 0 \
      u_x & u_y & u_z & 0 \
      g_x & g_y & g_z & 0 \
      0 & 0 & 0 & 1
      \end{bmatrix}
      \begin{bmatrix}
      1 & 0 & 0 & -P_x \
      0 & 1 & 0 & -P_y \
      0 & 0 & 1 & -P_z \
      0 & 0 & 0 & 1
      \end{bmatrix}
      $$

      • World -> Camera: $\mathbf{X}{cam} = \mathbf{M} \mathbf{X}_{world}$
  • fov & aspect

    • fov

      • field-of-view, 反映相机能够看到场景范围,弧度表示,值越大看的场景范围越大。

      • $\tan(fov) = \frac{h}{2d}$, h为视口高度,d为场景原点$\mathbf{O}$到相机中心点(相机位置)$\mathbf{P}$的距离。

    • aspect

      • 宽高比

      • aspect = (view_width / view_height) = (image_width / image_height)

    • 用途:

      • 已知图像aspect和相机fov,来计算视口大小:
      double theta = degree_to_radius(fov);
      double camera_origin_dist = (origin - camera_pos).length();
      double viewport_height = 2.0 * tan(theta / 2.0) * camera_origin_dist;
      double viewport_width = aspect * viewport_height;
      
  • Ray color

    • ray from any camera position: 从相机中心$\mathbf{P}$打出的射线ray,方向朝向场景原点$\mathbf{O}$
    Camera(double f, double a, point3 camera_pos, vec3 up): fov(f), aspect(a), camera_position(camera_pos)
    {
        double theta = degree_to_radius(fov);
        double camera_origin_dist = (origin - camera_pos).length();
        double viewport_height = 2.0 * tan(theta / 2.0) * camera_origin_dist;
        double viewport_width = aspect * viewport_height;
    
        origin = point3(0, 0, 0);
        g_ = unit_vector(origin - camera_pos);
        v_ = unit_vector(cross(g_, up));
        u_ = unit_vector(cross(v_, g_));
    
        horizontal = viewport_width * v_;
        vertical = viewport_height * u_;
        lower_left_corner = origin - horizontal / 2 - vertical / 2; // 视口左下角
    }
    
    ray get_ray(double u, double v) const
    {
        // 射线起点相机中心,朝向场景原点
        return ray(camera_position, lower_left_corner + u * horizontal + v * vertical - camera_position); 
    }
    
    double fov = 90.0;
    point3 camera_pos = point3(3, 3, 2);
    Camera cam(fov, aspect_ratio, aperture, camera_pos, vec3(0, 1, 0));
    
    for (int j = image_height-1; j >= 0; --j) 
    {
        for (int i = 0; i < image_width; ++i) 
        {
            ...
            pixel_color.write_color(std::cout, samples_per_pixel);
        }
    }
    
    • color效果:

  • depth-of-field

    • 景深:对于一个相机,能够清晰地看到场景内物体是有一定范围的,这个范围就是景深(depth-of-field),超过景深范围,物体就会看做模糊。

    上两张图来源12.4 景深(Depth of Field)

    • 影响因素:光圈(aperture)-- 简单理解,就是个Lens, 其直径大小为aperture

      • 光圈越小,景深越大,焦点处深度越大,能够看到场景物体越清晰,反之是模糊

    • C++描述

    vec3 random_in_unit_disk()
    {
        while (true)
        {
            auto p = vec3(random_double(-1, 1), random_double(-1, 1), 0);
            if (p.length_squared() >= 1)
                continue;
            return p;
        }
    }
    
    vec3 rd = len_radius * random_in_unit_disk(); // len_radius = aperture / 2.0
    vec3 offset = v_ * rd.x() + u_ * rd.y();
    ray(camera_position + offset, lower_left_corner + u * horizontal + v * vertical - camera_position - offset);
    

    说白了,就是射线ray的起点会有个随机扰动offset,扰动的半径为aperture / 2.0

    • 超过景深模糊效果:
    ...
    
    // Camera
    double aperture = 2.0;
    double fov = 20.0;
    point3 camera_pos = point3(3, 3, 2);
    Camera cam(fov, aspect_ratio, aperture, camera_pos, vec3(0, 1, 0));
    
    ...
    

12 Final Render

  • 结合上面11节的内容,渲染一张包含非常多的spheres, 并且使用Lambertian, metal, dielectric材质效果:

posted @   XiWJ  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示