带你从零开始徒手撸光线追踪代码(3)—— Ray Tracing in One Weekend

前言

   这是本系列文章的最后部分,对应的是《Ray Tracing in One Weekend》的第10-13章的内容,这部分内容比较少,所以博客内容也会相对短一些。

本系列文章链接

      带你从零开始徒手撸光线追踪代码(1)—— Ray Tracing in One Weekend
      带你从零开始徒手撸光线追踪代码(2)—— Ray Tracing in One Weekend
      带你从零开始徒手撸光线追踪代码(3)—— Ray Tracing in One Weekend

玻璃材质

   我们知道诸如水,玻璃等是透明材质,当一束光线达到这种材质的物体上的时候,光纤会发生反射与折射,此外我们还知道,透过这种透明的物体看到的世界是颠倒的,你可以现在拿起一个勺子或者玻璃球去看,你会看到世界是颠倒的。

斯涅耳定律

   学过物理的同学对斯涅耳定律一定不陌生,斯涅尔定律是用来描述光线在不同介质中发生折射的物理现象,并用表达式表达出折射光线与入射光线的方向关系。公式描述如下:
η ⋅ s i n θ = η ′ ⋅ s i n θ ′ \eta·sin\theta= \eta'·sin\theta ' ηsinθ=ηsinθ
   其中 η \eta η η ′ \eta' η是两种不同介质的折射率(空气中是1.0,玻璃中是1.3-1.7,钻石中是2.4), θ \theta θ θ ′ \theta' θ 是两种介质中各自光纤与发现的夹角。下图是光纤折射的示意图。
在这里插入图片描述

光纤折射示意图

    由此,我们就可以根据两者的介质以及入射光线与法线的夹角得到反射光纤与入射夹角的正弦值。

s i n θ ′ = η η ′ ⋅ s i n θ sin\theta'= \frac {\eta} {\eta'}·sin\theta sinθ=ηηsinθ

    之后我们将光线分解为平行于法线方向的向量和垂直于法线方向的向量。如下图所示,推导过程参考[2],但是个人感觉其中的图上标注的有点小瑕疵,此处重新作一下图。
在这里插入图片描述
    下面让我们一起来推导一下原书中的结论。首先让我们来规定一下, r ⃗ \vec{r} r 是一个单位向量,同时 n ⃗ \vec{n} n 也就是法线,那么我们就会得到 ∣ G ⃗ ∣ = ∣ r ⃗ ∣ ⋅ s i n θ |\vec{G}|=|\vec{r}|·sin\theta G =r sinθ,也就是 ∣ G ⃗ ∣ = s i n θ |\vec{G}|=sin\theta G =sinθ。现在我们知道了 ∣ G ⃗ ∣ |\vec{G}| G ,还差方向就能得到一个垂直于法线的单位向量了,那么这个方向我们就利用向量的四则运算,那既然如此,首先我们需要解算出 r ⃗ / / \vec{r}_{//} r //,这个平行向量的模为 c o s θ cos\theta cosθ,方向为 N ⃗ \vec{N} N ,于是根据三角形法则,我们看左半边三角形便很快就能写出 G ⃗ = − r ⃗ − c o s θ N ⃗ \vec{G}=-\vec{r}-cos\theta\vec{N} G =r cosθN 。我们再将其变为单位向量记为 g ⃗ = G ⃗ / ∣ G ⃗ ∣ \vec{g}=\vec{G}/|\vec{G}| g =G /∣G ,即可得到 g ⃗ = − r ⃗ − c o s θ N ⃗ s i n θ \vec{g}=\frac {-\vec{r}-cos\theta\vec{N}} {sin\theta} g =sinθr cosθN

    到目前位置,一切准备就绪,现在我们假设折射光线也是单位向量,那么就可以得到其垂直分量的值为 s i n θ ′ sin\theta' sinθ,方向自然就是 − g ⃗ -\vec{g} g ,因为 g ⃗ \vec{g} g 我们之前也已经论证过是单位向量了。因此我们可以得到如下式子:

r ⃗ ⊥ = − g ⃗ ⋅ s i n θ ′ = − g ⃗ ⋅ η η ′ ⋅ s i n θ = η η ′ ⋅ ( r ⃗ + c o s θ N ⃗ ) \vec{r}_{\bot} = -\vec{g}·sin\theta' = -\vec{g} · \frac {\eta} {\eta'}·sin\theta = \frac {\eta} {\eta'}·(\vec{r}+cos\theta\vec{N}) r =g sinθ=g ηηsinθ=ηη(r +cosθN )

    求得垂直分量后,我们知道折射光线的模为1,因此平行分量的值只需要用三角关系即可得到。

r ⃗ / / = − 1 − ∣ r ⃗ ⊥ ∣ 2 ⋅ N ⃗ \vec{r}_{//} = -\sqrt{1 - |\vec{r}_{\bot}|^ 2}·\vec{N} r //=1r 2 N

    当然,我们要想直接获取 c o s θ cos\theta cosθ是有一定的难度的,但我们知道,两个向量相乘会产生一个 c o s θ cos\theta cosθ,只需要我们将两个相乘的向量设置为单位向量即可,那么我们想求 c o s θ cos\theta cosθ的话直接计算两个单位向量的点乘。于是我们可以改写上面的 r ⊥ r_{\bot} r成如下形式:

r ⃗ ⊥ = η η ′ ⋅ ( r ⃗ + ( − r ⃗ ⋅ N ⃗ ) N ⃗ ) \vec{r}_{\bot} = \frac {\eta} {\eta'}·(\vec{r}+(-\vec{r}·\vec{N})\vec{N}) r =ηη(r +(r N )N )

    其中有个负号的原因是我们的 r ⃗ \vec{r} r 是射向内的,这样得到的夹角是我们想要的角的补交,因此要加个 - 来修正一下。根据以上我们的分析,我们就可以写出一个函数交refract专门用来生成我们的折射向量,代码如下:

vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
    
    
    auto cos_theta = fmin(dot(-uv, n), 1.0);
    vec3 r_out_perp =  etai_over_etat * (uv + cos_theta*n);
    vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n;
    return r_out_perp + r_out_parallel;
}
vec.h的内容

    现在我们来创建一个类,由于是玻璃材质,因此当然也是继承自材料类。成员函数只需要一个折射率。然后我们重写scatter方法,运用我们上面的refract函数进行折射。

    需要注意的是,我们的折射率是材料本身的属性,光线打过来的时候是从空气折射进物体还是物体折射出空气当然是不一样的。所以我们需要判断一下,如果是光线打入物体发生的折射,那么理应是空气的折射率除以材质的,由于空气的折射率是1.0,因此取倒数就行;光线从物体打入空气的话就是物体的除以空气的,由于空气的值为1.0,因此直接用折射率就行。

    至于如何判断,我们上一篇博客已经将球的法线方向分了一下,详情可以翻看我前一篇博客,链接在上面有。此处简单再解释一下,我们将球体的法线从向着球体外都改成了逆着光线的方向,因此光线从空气到物体的时候,法线方向是不变的,因为向球体外的方向就是逆着光线方向的,而光线从物体到空气的时候,此时如果还是向着球体外,就是顺着光线方向,因此我们当时取了反,所以此处我们只需要根据我们记录中的front_face属性就可以选择折射率的问题了。

class dielectric : public material {
    
    
    public:
        dielectric(double index_of_refraction) : ir(index_of_refraction) {
    
    }

        virtual bool scatter(
            const ray& r_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;

            vec3 unit_direction = unit_vector(r_in.direction());
            vec3 refracted = refract(unit_direction, rec.normal, refraction_ratio);

            scattered = ray(rec.p, refracted);
            return true;
        }

    public:
        double ir; // Index of Refraction
};
material.h的内容

    最后让我们创建几个物体尝试一下。

auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<dielectric>(1.5);
auto material_left   = make_shared<dielectric>(1.5);
auto material_right  = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
main.cpp的内容

在这里插入图片描述

全内反射

    我们都知道材质有自己的折射率,并且会符合斯涅尔定律,那么此处有个问题,我们如果是从低折射率到高折射率的还好,假设是从高折射率到低折射率呢,我们下面来看看会发生什么。
    我们就假设光线是从玻璃中折射到空气中的,玻璃的折射率 η = 1.5 \eta=1.5 η=1.5,空气的折射率 η ′ = 1.0 \eta'=1.0 η=1.0,那么根据斯涅尔定律,我们的折射光线角度的正弦值应该如下:

s i n θ ′ = 1.5 1.0 s i n θ sin\theta' = \frac{1.5}{1.0} sin\theta sinθ=1.01.5sinθ

    我们发现这个值是有可能大于1的,但是正弦值是不可能大于1的,于是这里就出现问题。对于此,我们的做法是当计算结果是大于1的时候,我们将光线进行反射,而非折射,这我们就称之为全内反射。同时这也能解释为什么我们在水下的时候水与空气的分界面充当了一个镜子的作用。

    至于此处的 s i n θ sin\theta sinθ我们当然可以直接运用三角函数直接获取,即

s i n θ = 1 − c o s 2 θ sin\theta = \sqrt{1-cos^2\theta} sinθ=1cos2θ

    下面是我们经过以上分析后,对dielectric类进行修改的代码,主要修改的内容是添加了反射的部分:

class dielectric : public material {
    
    
    public:
        dielectric(double index_of_refraction) : ir(index_of_refraction) {
    
    }

        virtual bool scatter(
            const ray& r_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;

            vec3 unit_direction = unit_vector(r_in.direction());
            double cos_theta = fmin(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)
                direction = reflect(unit_direction, rec.normal);
            else
                direction = refract(unit_direction, rec.normal, refraction_ratio);

            scattered = ray(rec.p, direction);
            return true;
        }

    public:
        double ir; // Index of Refraction
};
dielectric .h的内容

    好了,现在我们拥有漫反射,镜面反射以及折射,我们现在来创建三个物体来看看效果。

auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));	// 地面的球
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));	// 中间的球——漫反射
auto material_left   = make_shared<dielectric>(1.5);					// 左边的球——折射
auto material_right  = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0);	// 右边的球——镜面反射
main.cpp的内容

    效果如下所示:

在这里插入图片描述

Schlick’s approximation

    实际上真正的玻璃,其反射率随角度的变化而变化,举个例子,你看倾斜的窗户,它就会变成一面镜子,我们要做到这个就是使用的Schlick’s approximation,其方程式如下所示(详情可看参考文献3):

R ( θ ) = R 0 + ( 1 − R 0 ) ( 1 − c o s ( θ ) ) 5 R(\theta)=R_0+(1-R_0)(1-cos(\theta))^5 R(θ)=R0+(1R0)(1cos(θ))5
    其中
R 0 = ( η 1 − η 2 η 1 + η 2 ) 2 R_0=(\frac{\eta_1-\eta_2}{\eta_1+\eta_2})^2 R0=(η1+η2η1η2)2

    于是我们根据此公式可以修改我们的玻璃类。要注意的是,我们在reflectance函数中计算 R 0 R_0 R0时,分子分母同时除了一个 η 1 \eta1 η1,这样直接用折射率的比值即可,更方便代买书写。

class dielectric : public material {
    
    
    public:
        dielectric(double index_of_refraction) : ir(index_of_refraction) {
    
    }

        virtual bool scatter(
            const ray& r_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;

            vec3 unit_direction = unit_vector(r_in.direction());
            double cos_theta = fmin(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;
        }

    public:
        double ir; // Index of Refraction

    private:
        static double reflectance(double cosine, double ref_idx) {
    
    	// 改动的地方
            // Use Schlick's approximation for reflectance.
            auto r0 = (1-ref_idx) / (1+ref_idx);
            r0 = r0*r0;
            return r0 + (1-r0)*pow((1 - cosine),5);
        }
};

中空玻璃球

    下面我们展示一下如何创建一个中空的玻璃球。其实很简单,我们把半径改为复数即可。

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.4, material_left));  // 此处是中控玻璃球
world.add(make_shared<sphere>(point3( 1.0,    0.0, -1.0),   0.5, material_right));
main.cpp的内容

    下面是实现的效果图:
在这里插入图片描述

摄像机的位置变换

    我们不能光是这一个视角观察物体,我们最好是可以将摄像机的视角可以发生变化。因此这一部分我们来将摄像机的位置进行移动,从不同的视角观察物体。

摄像机查看几何图形

    首先来看一个概念——视场(Filed of view, fov):可视距离,也就是屏幕上会显示出的画面大小,fov越小显示器中显示的范围越小,你能看到的物品也就越大;fov越大,显示器中所显示的画面越大,你能看到的物品也就越小,类似于手机的广角摄像头所拍摄出来的画面。

    下面就是我们的示意图,假设我们的摄像机还是再原点,视口放在z = -1处,其中的 θ \theta θ就是我们的fov,就是我们看视口的上方与下方的夹角。
在这里插入图片描述
    由于我们视口是再z = -1,根据三角函数, h = t a n ( f o v / 2 ) h=tan(fov/2) h=tan(fov/2)。那么我们整个是口的高是2h。得到了高,再根据宽高比就可以得到视口的宽,于是我们可以将摄像机类先改成这样:

class camera {
    
    
    public:
        camera(
            double vfov, 
            double aspect_ratio
        ) {
    
    
            auto theta = degrees_to_radians(vfov);		// 计算视口的地方
            auto h = tan(theta/2);
            auto viewport_height = 2.0 * h;
            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;
};
camera.h的内容

    下面我们简单修改一下main函数来看看我们对摄像机类修改的效果,我们设置fov是90°,宽高比还是之前设置的16:9:

int main() {
    
    
    ...
    // World

    auto R = cos(pi/4);
    hittable_list world;

    auto material_left  = make_shared<lambertian>(color(0,0,1));
    auto material_right = make_shared<lambertian>(color(1,0,0));

    world.add(make_shared<sphere>(point3(-R, 0, -1), R, material_left));
    world.add(make_shared<sphere>(point3( R, 0, -1), R, material_right));

    // Camera

    camera cam(90.0, aspect_ratio);

    // Render

    std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";

    for (int j = image_height-1; j >= 0; --j) {
    
    
    ...
main.cpp的内容

   于是我们就得到如下的结果:
在这里插入图片描述

相机的定位

   鉴于我们摄像机要移动了,于是我们重新定义一下两个点,一个是我们看的点,即摄像机,叫做lookfrom,另一个是我们要看的点,叫lookat。示意图如下:
在这里插入图片描述
   现在还有一个问题,就是我们需要规定一个向上的方向,为什么需要呢,因为我们知道,我们现在是只规定了摄像机的位置,但是摄像机本身在这个位置上还有三个自由度的旋转。举个例子,我们的脑袋固定在了一定的位置,但是我们的头还是可以围绕各个方向进行旋转。为了解决这个问题,我们就需要定义一个向上的方向。一般情况下,我们规定这个向上的方向叫"view up",简称vup,一直指向上,即(0, 1, 0)。

   现在我们在世界坐标系以外,我们对摄像机也定义一个坐标系,首先是w轴,我们规定的是lookfrom与lookat的连线,然后我们就根据w轴和vup,决定我们的v轴,v轴是要垂直于w轴和vup的,最后就是u轴,u轴就是要垂直于w轴和v轴。这样我们就建立起一个两两之间相互垂直的坐标系(u, v, w)。示意图如下所示:
在这里插入图片描述
   之后我们要做的就是和之前一样,寻找左下角。不过这里找左下角的方式有一些些区别, 那就是我们摄像机经过了变换了,那么我们找horizontal 和 vertical 的时候也要将坐标进行变换,从原来的世界坐标系中变到(u, v, w)中。其实这个变换也很简单,那就是我们已经求出了viewport,那么分别乘以单位向量u和v,就成功转换过去啦,具体实现代码如下:

class camera {
    
    
    public:
        camera(
            point3 lookfrom,
            point3 lookat,
            vec3   vup,
            double vfov, // vertical field-of-view in degrees
            double aspect_ratio
        ) {
    
    
            auto theta = degrees_to_radians(vfov);
            auto h = tan(theta/2);
            auto viewport_height = 2.0 * h;
            auto viewport_width = aspect_ratio * viewport_height;

            auto w = unit_vector(lookfrom - lookat);
            auto u = unit_vector(cross(vup, w));
            auto v = cross(w, u);

            origin = lookfrom;
            horizontal = viewport_width * u;
            vertical = viewport_height * v;
            lower_left_corner = origin - horizontal/2 - vertical/2 - w;
        }

        ray get_ray(double s, double t) const {
    
    
            return ray(origin, lower_left_corner + s*horizontal + t*vertical - origin);
        }

    private:
        point3 origin;
        point3 lower_left_corner;
        vec3 horizontal;
        vec3 vertical;
};

   最后我们在main函数中重新构建世界并定义相机,相机的位置在世界坐标系中是(-2, 2, 1),观测点的坐标是(0, 0, -1),vup是(0, 1, 0),fov是90°,宽高比还是16:9

hittable_list world;

auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left   = make_shared<dielectric>(1.5);
auto material_right  = make_shared<metal>(color(0.8, 0.6, 0.2), 0.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.45, material_left));
world.add(make_shared<sphere>(point3( 1.0,    0.0, -1.0),   0.5, material_right));

// 定义摄像机,参数的含义分别是lookfrom,lookat,vup,fov,宽高比
camera cam(point3(-2,2,1), point3(0,0,-1), vec3(0,1,0), 90, aspect_ratio);
main.cpp的内容

   结果如下所示:
在这里插入图片描述
    现在我们改变一下fov,让其变小,我们看一下效果

camera cam(point3(-2,2,1), point3(0,0,-1), vec3(0,1,0), 20, aspect_ratio);
main.cpp的内容

    我们可以看到物体被放大了,也符合我们之前定义的fov,并且我们还可以看到玻璃球明显是中空的感觉。因为我们在定义左边球的时候就是设置的半径为负数。
在这里插入图片描述

散焦模糊

   摄影师朋友又喜欢叫它景深。其实这就是一个焦距的问题。

   上面的情况中,我们都假设的是光从一个点发出来的,那这样的话我们在视口上就会看到一个点,就如下图所示一样(图片来源参考文献4):
在这里插入图片描述
   而实际情况下,我们并不是一个点,而是会有一个光圈,然后光线是从这些光圈中出来的,这样打到物体上同一个点的时候就不是一个像素,而会有多个像素,就像下图一样(图片来源参考文献4在这里插入图片描述
   我们可以发现,球上同一个点会经过视口上不同的像素点成像,这样这个物体就会产生了模糊,因为这个点不在焦距上。如果物体都在焦距上,那同一个点还是会成像在一个像素上的,如下图所示:
在这里插入图片描述
   所以实现散焦模糊的关键在于这个光圈,我们在半径为1的圆内进行偏移,生成各种光线。

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.h的内容

   然后我们就要修改我们的camera类,给它加上光圈,我们在构造函数中需要添加新的成员aperture,用于表示光圈大小,用focus_dist表示成像屏幕与摄像机之间的距离。光圈半径等于光圈大小除以2。
   可能大家最好奇的就是计算horizontal,vertical的时候为什么要乘以focus_dist。是这样的,我们之前都是假设平面再w方向上与镜头的距离是1,现在我们希望能调控这个成像平面,于是引入了focus_dist,于是我们原来计算horizontal,vertical的时候都是乘以了1,现在是自由距离,自然就要乘以这个值了。
   最重要的是,我们在 get_ray 函数中获取光线的时候,不能像以前一样,直接从左下角算坐标然后连线即可,因为我们的光线原点不再是一个点,而是一个范围,因此我们的光线原点需要发生偏移,偏移量就是光圈的大小再乘以我们先前定义的random_in_unit_disk函数。于是

class camera {
    
    
    public:
        camera(
            point3 lookfrom,
            point3 lookat,
            vec3   vup,
            double vfov, // vertical field-of-view in degrees
            double aspect_ratio,
            double aperture,
            double focus_dist
        ) {
    
    
            auto theta = degrees_to_radians(vfov);
            auto h = tan(theta/2);
            auto viewport_height = 2.0 * h;
            auto viewport_width = aspect_ratio * viewport_height;

            w = unit_vector(lookfrom - lookat);
            u = unit_vector(cross(vup, w));
            v = cross(w, u);

            origin = lookfrom;
            horizontal = focus_dist * viewport_width * u;
            vertical = focus_dist * viewport_height * v;
            lower_left_corner = origin - horizontal/2 - vertical/2 - focus_dist*w;

            lens_radius = aperture / 2;
        }
		
        ray get_ray(double s, double t) const {
    
    
            vec3 rd = lens_radius * random_in_unit_disk();
            vec3 offset = u * rd.x() + v * rd.y();

            return ray(
                origin + offset,
                lower_left_corner + s*horizontal + t*vertical - origin - offset
            );
        }

    private:
        point3 origin;
        point3 lower_left_corner;
        vec3 horizontal;
        vec3 vertical;
        vec3 u, v, w;
        double lens_radius;
};
camera.h的内容

   下面我们采用一个大光圈定义一个摄像机:

point3 lookfrom(3,3,2);
point3 lookat(0,0,-1);
vec3 vup(0,1,0);
auto dist_to_focus = (lookfrom-lookat).length();
auto aperture = 2.0;

camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);
main.cpp的内容

   效果如下所示,我们可以看到左右两个球都变得模糊了。
在这里插入图片描述

最终结果

   最后我们创建一个函数,用于创建我们的场景,首先我们创建一个很大的漫反射的球作为地面,之后我们采用循环的方式来生成各种材质的小球,小球的材质完全是随机确定的,最后我们在整幅图的中间及其左右分别放置三种不同材质的球。相机的位置我们也是可以在创建相机对象的时候自由选择的。

hittable_list random_scene() {
    
    
    hittable_list world;

    auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5));
    world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material));

    for (int a = -11; a < 11; a++) {
    
    
        for (int b = -11; b < 11; b++) {
    
    
            auto choose_mat = random_double();
            point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double());

            if ((center - point3(4, 0.2, 0)).length() > 0.9) {
    
    
                shared_ptr<material> sphere_material;

                if (choose_mat < 0.8) {
    
    
                    // diffuse
                    auto albedo = color::random() * color::random();
                    sphere_material = make_shared<lambertian>(albedo);
                    world.add(make_shared<sphere>(center, 0.2, sphere_material));
                } else if (choose_mat < 0.95) {
    
    
                    // metal
                    auto albedo = color::random(0.5, 1);
                    auto fuzz = random_double(0, 0.5);
                    sphere_material = make_shared<metal>(albedo, fuzz);
                    world.add(make_shared<sphere>(center, 0.2, sphere_material));
                } else {
    
    
                    // glass
                    sphere_material = make_shared<dielectric>(1.5);
                    world.add(make_shared<sphere>(center, 0.2, sphere_material));
                }
            }
        }
    }

    auto material1 = make_shared<dielectric>(1.5);
    world.add(make_shared<sphere>(point3(0, 1, 0), 1.0, material1));

    auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1));
    world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2));

    auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0);
    world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3));

    return world;
}

int main() {
    
    

    // Image

    const auto aspect_ratio = 3.0 / 2.0;
    const int image_width = 1200;
    const int image_height = static_cast<int>(image_width / aspect_ratio);
    const int samples_per_pixel = 500;
    const int max_depth = 50;

    // World

    auto world = random_scene();

    // Camera

    point3 lookfrom(13,2,3);
    point3 lookat(0,0,0);
    vec3 vup(0,1,0);
    auto dist_to_focus = 10.0;
    auto aperture = 0.1;

    camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);

    // Render

    std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

    for (int j = image_height-1; j >= 0; --j) {
    
    
        ...
}
main.cpp的内容

  最后的结果如下所示,这个运行过程比较漫长,请耐心等待。
在这里插入图片描述

总结

    至此,本系列完结撒花,《Ray Tracing in One Weekend》也完结撒花,感谢各位看官的支持,如果本系列哪里有错误,希望各位在评论或者私信我指出,有疑问也欢迎提问。说实话我没想到前两期居然还有人看,我以为都不会有人看,十分感谢支持!之后会继续在CG专栏更新一些其他内容,欢迎各位推荐!

参考文献

   [1] Ray Tracing in One Weekend
   [2] RayTracingInOneWeekend学习记录
   [3] Schlick’s approximation
   [4] 散焦模糊的实现

猜你喜欢

转载自blog.csdn.net/qq_43419761/article/details/128202352