计算机图形学——Ray Tracing in One Weekend[1]

概述

本系列文章从0开始学习光线追踪,参考资料为光线追踪三部曲:
1.Ray Tracing in One Weekend
2.Ray Tracing: The Next Week
3.Ray Tracing: The Rest of Your Life

记录并翻译部分内容。

Ray Tracing in One Weekend 概述

总共分为以下几章节内容:
一.内容
二.输出一张图片
三.向量类的定义
四.光线,一个简单的相机,和背景
五.添加一个球体
六.法线与多个物体
七.反走样
八.漫反射材质
九.金属
十.介质
十一.相机移动
十二.焦散模糊
十三.下一章

[One Weekend 内容(一)]

略 这里简单介绍了Ray Tracing

[One Weekend 输出一张图片(二)]

2.1.PPM格式
我们开始渲染的时候,需要看到渲染的结果。最直接的方式是把结果写道文件中。我们用ppm图片来存储渲染结果。以下是PPM的描述。
在这里插入图片描述
代码:以下代码输出一张256 * 256 像素的内容,如果想要输出图片

#include <iostream>
int main() {
    // Image
    const int image_width = 256;
    const int image_height = 256;
    // Render
    std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

    for (int j = image_height-1; j >= 0; --j) {
        for (int i = 0; i < image_width; ++i) {
            auto r = double(i) / (image_width-1);
            auto g = double(j) / (image_height-1);
            auto b = 0.25;
            int ir = static_cast<int>(255.999 * r);
            int ig = static_cast<int>(255.999 * g);
            int ib = static_cast<int>(255.999 * b);
            std::cout << ir << ' ' << ig << ' ' << ib << '\n';
        }
    }
}

需要说明的几个点:
1.左上角为(0, 0)点
2.为了方便,红/绿/蓝的范围是0.0~1.0.我们后续有可能会引入HDR,但是目前我们不会做什么改变

2.2.输出一张图片
上述的C++代码其实是在命令行输出字符串,怎么做才能输出图片呢?
答案:执行的时候加上 >> 命令 或者 > 命令
在这里插入图片描述
输出结果:
在这里插入图片描述
2.3添加进度指标
我们的图片使用标准的std::cout,所以我们用std::cerr输出进度信息

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 r = double(i) / (image_width - 1);
        auto g = double(j) / (image_height - 1);
        auto b = 0.25;
        int ir = static_cast<int>(255.999 * r);
        int ig = static_cast<int>(255.999 * g);
        int ib = static_cast<int>(255.999 * b);
        std::cout << ir << ' ' << ig << ' ' << ib << '\n';
    }
}

[One Weekend 向量(三)]

我们需要一些类来存储几何向量,位置,颜色。在很多系统中,这些向量是4D的(一般情况下我们用齐次坐标表示,颜色(RGBA)有ALPHA项,代表透明度,也是4D)。但是目前对于我们来讲3D的坐标就够用了。我们用Vec3不光能用来表示向量,也能用来表示颜色
3.1变量和方法
数据的定义double或者float都是可以的,全看个人喜好

#ifndef VEC3_H
#define VEC3_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];
        }
    public:
        double e[3];
};
// Type aliases for vec3
using point3 = vec3;   // 3D point
using color = vec3;    // RGB color
#endif

3.2 Vec3的实用函数

// 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();
}

3.3颜色的实用函数
以下方法用来输出一个颜色值

#ifndef COLOR_H
#define COLOR_H

#include "vec3.h"

#include <iostream>

void write_color(std::ostream &out, color pixel_color) {
    // Write the translated [0,255] value of each color component.
    out << static_cast<int>(255.999 * pixel_color.x()) << ' '
        << static_cast<int>(255.999 * pixel_color.y()) << ' '
        << static_cast<int>(255.999 * pixel_color.z()) << '\n';
}
#endif

现在我们可以把Main函数改成下面的样子了。

#include "color.h"
#include "vec3.h"

#include <iostream>

int main() {
    // Image
    const int image_width = 256;
    const int image_height = 256;
    // 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(double(i)/(image_width-1), double(j)/(image_height-1), 0.25);
            write_color(std::cout, pixel_color);
        }
    }
    std::cerr << "\nDone.\n";
}

[One Weekend 光线,一个简单的相机,和背景(四)]

4.1 光线的定义
我们定义一个和时间相关的函数P(t)=A+tb t表示时间。P光线上的一个位置(3D).A表示光线的起点。b表示光线的方向。t是一个实数,可以理解为时间t,经过t时间,P(t) 表示光线传输到的位置。
在这里插入图片描述
实现光线:

#ifndef RAY_H
#define RAY_H

#include "vec3.h"

class ray {
    public:
        ray() {}
        ray(const point3& origin, const vec3& direction)
            : orig(origin), dir(direction)
        {}
        point3 origin() const  { return orig; }
        vec3 direction() const { return dir; }
        point3 at(double t) const {
            return orig + t*dir;
        }
    public:
        point3 orig;
        vec3 dir;
};  
#endif

4.2向场景发射光线
现在我们需要向场景中发射一条光线。分为以下三个步骤:
1.计算从眼睛(相机)到像素(屏幕)的一条光线
2.计算哪个物体和这条光线相交
3.计算交点的颜色。
我们现在假设相机在(0, 0, 0)位置,向上方向为Y正方向,向右方向X正方向,负Z方向朝向屏幕里面。我们从屏幕矩阵(可以理解为值为Vec3的二维数组)。在屏幕上找到一个点P(x, y, z). 其中z是固定的。(p-原点).norm() 就是光线的方向。那么我们就能发射一条光线 o + td
在这里插入图片描述

#include "color.h"
#include "ray.h"
#include "vec3.h"

#include <iostream>

color ray_color(const ray& r) {
    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() {
    // 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);

    // Camera

    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);
            color pixel_color = ray_color(r);
            write_color(std::cout, pixel_color);
        }
    }

    std::cerr << "\nDone.\n";
}

ray_color 函数从白色到蓝色渐变,找个白蓝渐变依赖的是高度变化,也就是三维空间的Y值,再次强调,我们的Y是续向上方向。
blendedValue = (1-t)*startValue + t.endValue;
当t=0的时候是白色,t=1的时候是蓝色。最总我们可以绘制出蓝白色的背景。
在这里插入图片描述

[One Weekend 添加一个球体(五)]

让我们来给场景中添加一个物体吧。人们经常使用球体来做光线追踪,因为计算光线是否和球体相交是十分容易的。
5.1光线和球体相交
我们先看一下球体的方程,球体的方程式如下,其中
(x−Cx)2+(y−Cy)2+(z−Cz)2=r2
现在我们用向量的方式表示球体的方程式,如下,C代表球心坐标,P代表球面上的任意一点

在这里插入图片描述
也就是:
在这里插入图片描述
那么还记得我们的光线是怎么表示的呢
在这里插入图片描述
如果光线和球体相交,那么我们一定可以解出有效的时间t,使得
在这里插入图片描述
带入后可得:
在这里插入图片描述
展开后可得:
在这里插入图片描述
那么上述函数式有三种情况,一种是:
不相交
相切
相交
在这里插入图片描述
5.2创建我们第一个光线追踪的图像
计算和光线和球体相交的方法:

bool 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;
    //这里求解的是t,如果t >0 ,则代表有有效值,光线和球体相交
    auto discriminant = b*b - 4*a*c;
    return (discriminant > 0);
}

渲染球体:

color ray_color(const ray& r) {
    if (hit_sphere(point3(0,0,-1), 0.5, r))
        return color(1, 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);
}

在这里插入图片描述

我们的第一个球体渲染出来了。

[One Weekend 表面法线和多个物体(六)]

6.1渲染球体的法线
现在,让我们来获取我们表面的法线。法线和是球体表面点垂直的。法线有2个特点:
1.法线是单位向量,这里的意思是向量的长度是1
在这里插入图片描述
2.对于球体来说,法线方向和,球面上的某一点P减去球体的圆形O的单位向量。现在我们修改一部分代码,将法线渲染出来。

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;
    //这个是不相交的请情况
    if (discriminant < 0) {
        return -1.0;
    } else {
    	// 如果相交,我们求取第一个交点的时间t
        return (-b - sqrt(discriminant) ) / (2.0*a);
    }
}

根据给定的时间t,计算法线的颜色。

color ray_color(const ray& r) {
    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);
    }
    //如果不相交,渲染渐变的颜色
    vec3 unit_direction = unit_vector(r.direction());
    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);
}

6.2简化球体和光线相交的代码
将b=2h带入后,光线和球体相交的代码简化如下

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;

    if (discriminant < 0) {
        return -1.0;
    } else {
        return (-b - sqrt(discriminant) ) / (2.0*a);
    }
}

6.3定义相交的抽象算法
我们定义了一个hittable 类,这个类有一个抽象的方法hit,会计算出相交的交点p,交点的法线normal,和相交时

#ifndef HITTABLE_H
#define HITTABLE_H

#include "ray.h"

struct hit_record {
    point3 p;
    vec3 normal;
    double t;
};

class hittable {
    public:
        virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
#endif

再将光线和球体相交的算法重新提取:

#ifndef SPHERE_H
#define SPHERE_H

#include "hittable.h"
#include "vec3.h"

class sphere : public hittable {
    public:
        sphere() {}
        sphere(point3 cen, double r) : center(cen), radius(r) {};

        virtual bool hit(
            const ray& r, double t_min, double t_max, hit_record& rec) const override;

    public:
        point3 center;
        double radius;
};

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 acceptable 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);
    rec.normal = (rec.p - center) / radius;

    return true;
}

#endif

6.4正面和背面
这里有个问题需要搞明白,法线是不是总是指向物体外。之前,我们的法线总是指向外部,球体上的法线反方向总是从球心到球面上的某一点。我们现在假设如果光线和球体相交在外面,法线向外,如果光线和球体相交在内部,法线方向为向内,也就是从交点到球心的方向。
在这里插入图片描述
我们需要选择出这种情况,我们必须知道光线是从哪边过来的。这种情况有利于我们渲染2边不同的物体,例如2面不同文字的纸张,例如玻璃。
如果我们决定让2边的法线都是向外的,那么我们需要在着色的时候知道光线是在正面相交还是i背面相交。我们可以通过比较光线和法线得出结果。如果光线和法线在相同的方向,光线在背面,也就是内部。如果光线和法线在不同的方向,光线在物体外部。可以用点成来判断。tips:这里的相同方向和不同方向,准确的描述应该是光线和法线夹角>90 度,我们认为是不同方向,夹角小于90度,认为是在相同的方向。

if (dot(ray_direction, outward_normal) > 0.0) {
    // ray is inside the sphere
    ...
} else {
    // ray is outside the sphere
    ...
}

如果我们总是假设光线和法线方向相反(相反指大于90度的教教),我们需要额外的数据来标记光线和物体相交的面是正面还是背面

bool front_face;
if (dot(ray_direction, outward_normal) > 0.0) {
    // ray is inside the sphere
    normal = -outward_normal;
    front_face = false;
} else {
    // ray is outside the sphere
    normal = outward_normal;
    front_face = true;
}

我们可以假设法线总是和交点方向向外,或者总是和光线进来的方向相反(这里指的光线和法线的夹角小于90度)。这取决于你是否在物体存在的的时候确认正面还是反面,还是在着色的时候才确认正反面。在这里我们会有很多种材质类型,为了省事,我们给物体标记出正面和反面。光线相交的时候,我们给出是正面还是反面。

struct hit_record {
    point3 p;
    vec3 normal;
    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;
    }
};
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);
    return true;
}

6.5多个物体的相交
写出光线和场景中多个物体相交情况的代码

#ifndef HITTABLE_LIST_H
#define HITTABLE_LIST_H

#include "hittable.h"

#include <memory>
#include <vector>

using std::shared_ptr;
using std::make_shared;

class hittable_list : public hittable {
    public:
        hittable_list() {}
        hittable_list(shared_ptr<hittable> object) { add(object); }
        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;
    public:
        std::vector<shared_ptr<hittable>> objects;
};

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)) {
            hit_anything = true;
            closest_so_far = temp_rec.t;
            rec = temp_rec;
        }
    }
    return hit_anything;
}

#endif

6.6一些C++特征
略,这里介绍的是C++语法
6.7常量和通用函数
rtweekend.h 定义了一些常量

#ifndef RTWEEKEND_H
#define RTWEEKEND_H

#include <cmath>
#include <limits>
#include <memory>


// Usings

using std::shared_ptr;
using std::make_shared;
using std::sqrt;

// Constants

const double infinity = std::numeric_limits<double>::infinity();
const double pi = 3.1415926535897932385;

// Utility Functions

inline double degrees_to_radians(double degrees) {
    return degrees * pi / 180.0;
}

// Common Headers

#include "ray.h"
#include "vec3.h"

#endif

修改新的main函数:
#include “rtweekend.h”

#include "color.h"
#include "hittable_list.h"
#include "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));
    }
    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() {

    // 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);

    // 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

    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);
            color pixel_color = ray_color(r, world);
            write_color(std::cout, pixel_color);
        }
    }

    std::cerr << "\nDone.\n";
}

在这里插入图片描述

[One Weekend 反走样(七)]

本质上来讲,走样是因为对函数(信号)的采样频率不足,或者说信号的变化速率远大于采样的频率,简单来说,在图形学中无法呈现所设想的效果,即称为走样。在计算机渲染中,由于绘制的图形在数学上是连续的,而渲染的像素点是离散的,从而导致在光栅化的三角形遍历阶段,将图形打散为像素时,会不可避免的产生锯齿(Jaggies),这种现象不可避免,只能减轻。
7.1一些随机数的定义
定义随机数生成的方法,产生随机数。
C语言的方式

#include <cstdlib>
...
inline double random_double() {
    // Returns a random real in [0,1).
    return rand() / (RAND_MAX + 1.0);
}
inline double random_double(double min, double max) {
    // Returns a random real in [min,max).
    return min + (max-min)*random_double();
}

C++的方式

#include <random>

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

7.2对一个像素进行多次采样
在这里插入图片描述
现在我们创建一个相机类来管理虚拟相机和采样。

#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

为了支持多次采样,我们需要修改write_color()函数。
实现clamp函数,限制范围。

inline double clamp(double x, double min, double max) {
    if (x < min) return min;
    if (x > max) return max;
    return x;
}

write_color 函数的改动其实是添加了采样的次数samples_per_pixel,每个像素的光线数目,那么每条光线的对该像素产生的颜色为贡献为1.0/samples_per_pixel。

void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
    auto r = pixel_color.x();
    auto g = pixel_color.y();
    auto b = pixel_color.z();

    // Divide the color by the number of samples.
    auto scale = 1.0 / samples_per_pixel;
    r *= scale;
    g *= scale;
    b *= scale;

    // Write the translated [0,255] value of each color component.
    out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
        << static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
        << static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}

再次修改main函数:

#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";
 }

结果:下图是每个像素200条光线产生的结果
在这里插入图片描述

[One Weekend 漫反射材质(八)]

8.1 一个简单的漫反射材质
漫反射材质不会直接发光,它们显示的颜色是和周围环境颜色相关,根据环境光的颜色调节它们自身的颜色。光线打到漫反射物体后会反射到西面八方(这里原文描述是随机的,个人理解可能有点不太准确)。如果我们向2个漫反射物体发射光线, 它们的反射方向如下所示:
在这里插入图片描述
它们大部分都会被吸收而不是被反射。颜色越暗,越容易吸收。一个完全真实的漫反射算法会让物体表面看起来很粗糙。我们现在用一个简单的方法来实现漫反射(这里原文描述是一个简单的hack,使用兰伯特光照模型)。
(这里描述的是一个简单的兰伯特光照模型,正确的兰伯特光照比我们目前使用的要多很多东西)
这里有2个单位球体相切在p点,2个球体体的中心是(p+n)和(p-n),n是表面的法向量。这个球心在(p-n)的我们认为法线实在表面外的,相切的球体在光线(这里应该指的眼睛)的同一侧,那我们可以随机选取一个在同一侧的点s,发出一条光线从p点到s点。
我们需要写出一个算法去获取球面上的一个点。随机生成-1~1之间的x, y, z。

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

如果随机在了球体外,则重新随机。

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

更新ray_color() 函数使用新的随机方向。

color ray_color(const ray& r, const hittable& world) {
    hit_record rec;

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

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

还有一个问题,我们需要限制光线弹射的次数,不然会无限执行下去

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

...

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";
}

下图是200条光线/像素的渲染结果
在这里插入图片描述
8.3使用伽马矫正获取更加准确的颜色
关于什么是伽马矫正以及怎么做伽马矫正,可以看以下链接
https://learnopenglcn.github.io/05%20Advanced%20Lighting/02%20Gamma%20Correction/
伽马矫正有多种方式,这里我们使用 “gamma 2” ,这意味着将颜色提升为( 1 / G a m m a ) (1/Gamma)(1/Gamma)的幂,即( R G B ) ½ (RGB)^{½}(RGB) ½。

void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
    auto r = pixel_color.x();
    auto g = pixel_color.y();
    auto b = pixel_color.z();

    // Divide the color by the number of samples and gamma-correct for gamma=2.0.
    auto scale = 1.0 / samples_per_pixel;
    r = sqrt(scale * r);
    g = sqrt(scale * g);
    b = sqrt(scale * b);

    // Write the translated [0,255] value of each color component.
    out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
        << static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
        << static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}

在这里插入图片描述
8.4修复细微的错误
有些光线并不是在t=0的地方发生反射,而是t=-0.000000001或者t=-0.0000000001,或者更接近的地方。所以我们需要忽略接近0的。

if (world.hit(r, 0.001, infinity, rec)) {

在这里插入图片描述
8.5兰伯特光照
这里我们对之前的向量进行单位化。使得结果更加准确。这里的代码和原文描述的兰伯特光照不太一致,只是做了单位化一件事情。原文描述的算法应该没有给出code.

inline vec3 random_in_unit_sphere() {
    ...
}
vec3 random_unit_vector() {
    return unit_vector(random_in_unit_sphere());
}
color ray_color(const ray& r, const hittable& world, int depth) {
    hit_record rec;

    // If we've exceeded the ray bounce limit, no more light is gathered.
    if (depth <= 0)
        return color(0,0,0);

    if (world.hit(r, 0.001, infinity, rec)) {
        point3 target = rec.p + rec.normal + random_unit_vector();
        return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
    }

    vec3 unit_direction = unit_vector(r.direction());
    auto t = 0.5*(unit_direction.y() + 1.0);
    return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}

在这里插入图片描述

8.6另外一种漫反射公式
在法线所在的半球做反射。8.5和8.6只是对漫反射方式不同算法的描述,本文这里不深究此处

vec3 random_in_hemisphere(const vec3& normal) {
    vec3 in_unit_sphere = random_in_unit_sphere();
    if (dot(in_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal
        return in_unit_sphere;
    else
        return -in_unit_sphere;
}
color ray_color(const ray& r, const hittable& world, int depth) {
    hit_record rec;
    // If we've exceeded the ray bounce limit, no more light is gathered.
    if (depth <= 0)
        return color(0,0,0);
    if (world.hit(r, 0.001, infinity, rec)) {
        point3 target = rec.p + random_in_hemisphere(rec.normal);
        return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
    }
    vec3 unit_direction = unit_vector(r.direction());
    auto t = 0.5*(unit_direction.y() + 1.0);
    return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}

在这里插入图片描述

[One Weekend 金属材质(九)]

9.1对金属的抽象描述
如果我们希望的不同的物体拥有不同的材质,需要对现有的代码进行改进。我们可以拥有一种通用材质,但是有很多不同的参数。或者我们可以提取一个材质的抽象类。使得其它类型的材质都继承自它。对于材质,我们需要做2件事情:
1.产生散射的射线
2.如果发生散射,那么吸收了多少,反射了多少

#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

9.2描述交点的数据结构
这里添加了交点的材质信息

#include "rtweekend.h"

class material;

struct hit_record {
    point3 p;
    vec3 normal;
    shared_ptr<material> mat_ptr; // 交点的材质
    double t;
    bool front_face;

    inline void set_face_normal(const ray& r, const vec3& outward_normal) {
        front_face = dot(r.direction(), outward_normal) < 0;
        normal = front_face ? outward_normal :-outward_normal;
    }
};

当光线打到材质的时候,我们需要带上材质的信息

class sphere : public hittable {
    public:
        sphere() {}
        sphere(point3 cen, double r, shared_ptr<material> m)
            : center(cen), radius(r), mat_ptr(m) {};

        virtual bool hit(
            const ray& r, double t_min, double t_max, hit_record& rec) const override;

    public:
        point3 center;
        double radius;
        shared_ptr<material> mat_ptr;
};

bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
    ...

    rec.t = root;
    rec.p = r.at(rec.t);
    vec3 outward_normal = (rec.p - center) / radius;
    rec.set_face_normal(r, outward_normal);
    rec.mat_ptr = mat_ptr;

    return true;
}

9.3描述光线的入射和散射
对于兰伯特(漫反射)的材质来说,我们已经有了,它们可以散射并衰减。简单来讲,下面的兰伯特计算了光线入射到某个点hit_record,然后然后随机反射到某个方向的ray和衰减后的color。

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

现在我们以某种概率p散射,并使得衰减为albedo/p。如果我们仔细看上述代码,还是会有一点点的小瑕疵。如果我们随机的向量和法线方向相反,那么rec.normal + random_unit_vector()的结果接近0.这会导致后面的糟糕情况和无穷,所以我们要事先避免这种情况。
我们会创建一个新的方法,判断向量是否接近零。

class vec3 {
    ...
    bool near_zero() const {
        // Return true if the vector is close to zero in all dimensions.
        const auto s = 1e-8;
        return (fabs(e[0]) < s) && (fabs(e[1]) < s) && (fabs(e[2]) < s);
    }
    ...
};

修改算法

class lambertian : public material {
    public:
        lambertian(const color& a) : albedo(a) {}

        virtual bool scatter(
            const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
        ) const override {
            auto scatter_direction = rec.normal + random_unit_vector();

            // Catch degenerate scatter direction
            if (scatter_direction.near_zero())
                scatter_direction = rec.normal;

            scattered = ray(rec.p, scatter_direction);
            attenuation = albedo;
            return true;
        }

    public:
        color albedo;
};

9.4镜面反射
对于一个光滑的表面来说,光线不会随机的散射。我们来看光线如何在镜面反射的。
在这里插入图片描述
反射的方向是红色的v+2b。在我们的设计中,n是单位向量,但是v不一定是。b的长度是v.n。v是入射光线

vec3 reflect(const vec3& v, const vec3& n) {
    return v - 2*dot(v,n)*n;
}

金属材质通过上面的公式反射光线

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

我们需要修改ray_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);
}

9.5含有金属球体的场景

#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";
}

在这里插入图片描述
9.6模糊反射
我们可以随机的反射光线方向。
球体越大,反射将变得越模糊。 建议添加一个模糊度参数,该参数仅是球体的半径。 要注意的是,对于大球体或掠食性射线,我们可能会散射到表面以下, 我们可以让表面吸收
掉。

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

我们可以尝试添加0.3和1.0的模糊在金属球上面:

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

在这里插入图片描述

[One Weekend 介质(十)]

透明的物体如水,玻璃,钻石都是介质。当光线打进来后,它们有部分被反射,有部分被折射。我们打算在场景中放入2个玻璃球。
10.1 折射
折射是用斯奈尔定律描述的
10.2 斯奈尔定律
在这里插入图片描述
在这里插入图片描述
为了确定折射的方向,我们选择解下述方程:
在这里插入图片描述
我们将折射方向的光线拆分为垂直和平行方向:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样,我们可以计算出折射光:

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

添加材质的折射:

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

在这里插入图片描述
10.3完全的内部反射
如果光线从一个折射率比较高的地方到折射率比较低的地方:
在这里插入图片描述
如果是从玻璃到外部的空气(η=1.5 and η′=1.0):
在这里插入图片描述
由于sinθ′不可能大于1,所以,如果
在这里插入图片描述
我们的折射就不存在,玻璃不会存在折射,但是会反射光线:

if (refraction_ratio * sin_theta > 1.0) {
    // Must Reflect
    ...
} else {
    // Can Refract
    ...
}

现在我们所有的光线都被反射了,常规来讲,光线是在物体的内部,所以叫做"内部全反射".这就是为什么水-空气表面看起来像是完美的镜面反射一样。

在这里插入图片描述
在这里插入图片描述

double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);

if (refraction_ratio * sin_theta > 1.0) {
    // Must Reflect
    ...
} else {
    // Can Refract
    ...
}

修改材质

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

在这里插入图片描述
10.4 施利克近似
真正的玻璃的反射率随角度的变化而变化——看一扇倾斜角度的窗户,它就变成了一面镜子。这有一个丑陋的大方程,但几乎每个人都使用克里斯托弗·施里克(Christophe Schlick)的廉价而精确的多项式近似。这就产生了我们的全玻璃材料:

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

10.4 空心玻璃球建模
我们很容易使用半径为负数的,创建空心玻璃球

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

在这里插入图片描述

[One Weekend 可以移动的相机(十一)]

相机,类似介质,是很难调试的。所以,我经常渐进式开发。首先,我们允许调节的FOV(视角场)。
11.1相机视角
我们首先假设光线从源点过来,打向z=-1的平面。
在这里插入图片描述
h=tan(θ/2).我们的相机现在变成了

class camera {
    public:
        camera(
            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 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;
};

我们将相机参数设置为 cam(90, aspect_ratio)。

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) {
    ...

在这里插入图片描述
11.2 位置和定向相机
我们可以指定相机的位置,以及相机朝向。我们将相机所在的位置表示为lookfrom,我们看向的位置表示为lookat。我们绕着lookat-lookfrom轴的朝向,被定义为lookup。lookup应该和视线方向垂直。
在这里插入图片描述
我们可以使用和视线方向垂直的任何向量表示向上方向。我们使用(u,v,w)表示相机的方向。在这里插入图片描述

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

camera cam(point3(-2,2,1), point3(0,0,-1), vec3(0,1,0), 90, aspect_ratio);

在这里插入图片描述
如果我们改变视角

camera cam(point3(-2,2,1), point3(0,0,-1), vec3(0,1,0), 20, aspect_ratio);

在这里插入图片描述

[One Weekend 焦散模糊(十二)]

现在我们最后一步,就是焦散模糊。摄影师朋友称之为"景深"。

我们在真实的相机中会碰到焦散模糊是因为我们需要一个很大的孔(而不是小孔)才能吸收到足够的光。这回焦散所有物体,但是如果我们在孔中插入一个镜头,所有的东西都在焦点上。你可以这样想:所有的光线都来自特定焦距的一个点——光线打到镜头上——将被弯曲并且传送到图像传感器上。

我们将投影点和完美聚焦的平面之间的距离称之为焦距。

对于一个真实的相机来讲,焦距可以由镜头和成像平面控制。这是为什么移动镜头可以调整焦距。光圈是一个可以有效控制镜头大小的孔,如果你需要更多的光,你就把光圈调大,这样会得到更多的焦散模糊。

12.1 薄透镜近似
真正的相机有一个复杂的复合镜头。对于我们的代码,我们可以模拟顺序:传感器,镜头,光圈。然后我们可以找出将光线发送到哪里,并在计算后翻转图像(图像倒置投影到胶片上)。人们通常使用薄透镜近似值:在这里插入图片描述
我们不需要模拟相机内部的所有操作。渲染一张不在相机内的图像,并不是那么复杂。相反,我们经常在镜头前发射一条光线,光线的方向朝向焦平面,这样,平面上的所有物体都是完美聚焦的。
在这里插入图片描述
12.2 生成光线
通常来讲,场景中所有的光线都是空lookfrom的位置发出的。为了实现焦散模糊,我们在以lookfrom点为圆盘的内部随机生成光线。半径越大,越容易焦散模糊。

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

在这里插入图片描述

[One Weekend 下一步是什么(十三)]

现在,让我们开始渲染一个场景。

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

在这里插入图片描述

参考资料:
https://raytracing.github.io/books/RayTracingInOneWeekend.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值