Ray Tracing in One Weekend 中译版(下)

208 阅读14分钟

11. 电介质

  类似水,玻璃和钻石等透明的材质都是电介质。当光线碰到它们时,会分成反射光线和折射(透射)光线。我们将在反射和折射之间随机选择来处理这个问题,每次交互只生成一条散射射线。

  这里快速回顾一些术语,反射光线指的是光线入射表面,然后从新的方向“反弹”离开表面。

  折射光线在从一种介质进入另一种介质(如玻璃或水)时方向会发生改变。这就是为什么铅笔部分插入水中时看起来会弯曲的原因。

  折射光线弯曲程度是由介质的折射率决定的。一般地,这是一个用来描述从光线从真空中进入介质时,光线的弯曲程度的值。玻璃的折射率在1.5-1.7之间,钻石在2.4附近,而空气的折射率是1.000293。

  当光线从一种透明介质嵌入到另一种透明介质中时,可以用相对折射率来描述折射现象:即使用包围介质的折射率除以被包围介质的折射率。例如,当想要渲染一个水下的玻璃球时,玻璃球的相对折射率是1.125。这是由水的折射率(1.333)除以玻璃的折射率(1.5)得到的。

  关于大多数常见介质的折射率,可以通过网上搜索得到。

11.1. 折射

  最难调试的部分是折射光线。如果存在折射光线,我通常会先让所有光线发生折射。在这个项目中,我尝试在场景中放置两个玻璃球,结果是这样的(我还没有告诉你这样做的对错,但很快就会告诉你!):

Image 15: 玻璃优先

  是这样吗?玻璃球在现实生活中看起来很奇怪。但不,这是不对的。世界应该是颠倒的,不应该有奇怪的黑色东西。我刚把射线从图像中间直接打印出来,结果明显不对。这通常就能解决问题。

11.2. 折射定律(Shell Law)

  折射规律可以通过斯涅尔定律描述:

ηsin(θ)=ηsin(θ)\eta \sin(\theta) = \eta^{'}\sin(\theta^{'})

  其中θ\thetaθ\theta^{'}分别是入射光线与法线的夹角以及折射光线与法线的夹角。η\etaη\eta^{'}分别是第一介质和第二介质的折射率。(通常空气=1.0,玻璃=1.3-1.7,钻石=2.4)。光路图如下:

Figure 17: 光线折射

  为了确定折射光线的方向,我们必须求解sin(θ)\sin(\theta^{'})

sin(θ)=ηηsin(θ)\sin(\theta ^{'})=\frac{\eta}{\eta^{'}}\cdot\sin(\theta)

  在接触表面折射的这一侧,有一条折射光线RR^{'}和法线nn,它们之间的角度是θ\theta^{'}。我们可以把RR^{'}分解成平行于nn和垂直于nn的部分:

R=R+RR^{'}=R^{'}_{\bot}+R^{'}_{\parallel}

  如果我们求解RR^{'}_{\bot}RR^{'}_{\parallel},可得:*(译者注:这里有必要解释一下,RR^{'}_{\bot}是与法线垂直的部分。RR^{'}_{\parallel}是与法线平行的部分。

R=ηη(R+cos(θ)n)R=n1R2R^{'}_{\bot} = \frac{\eta}{\eta^{'}}(R+\cos(\theta)n)\\ R^{'}_{\parallel} = -n\sqrt{1-|R^{'}_{\bot}|^2}

  如果你想的话,你可以自己证明这个等式,但我们将把它当作事实并继续。本书的其余部分不需要你理解这个证明。

  我们知道右侧每个项的值,除了cosθ\cos\theta。众所周知,两个向量的点积可以用它们之间的夹角的余弦来解释:

ab=abcos(θ)a\cdot b = |a||b|\cos(\theta)

  如果我们限制aabb为单位向量,则:

ab=cos(θ)a\cdot b = \cos(\theta)

  我们现在可以用已知量重写RR^{'}_{\bot}的求解公式:

R=ηη(R+(Rn)n)R^{'}_{\bot} = \frac{\eta}{\eta^{'}}(R+(-R\cdot n)n)

  当我们将它们重新组合在一起时,我们可以编写一个函数来计算RR^{'}

...

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

inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
    auto cos_theta = std::fmin(dot(-uv, n), 1.0);
    vec3 r_out_perp =  etai_over_etat * (uv + cos_theta*n);
    vec3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n;
    return r_out_perp + r_out_parallel;
}

  发生折射的材质可以实现如下:

...

class metal : public material {
    ...
};

class dielectric : public material {
  public:
    dielectric(double refraction_index) : refraction_index(refraction_index) {}

    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 ri = rec.front_face ? (1.0/refraction_index) : refraction_index;

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

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

  private:
// Refractive index in vacuum or air, or the ratio of the material's refractive index over
// the refractive index of the enclosing media
    double refraction_index;
};

  现在我们将更新场景,将左侧的球体更改为折射率为1.5的玻璃:

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.50);
auto material_right  = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);

  渲染结果如下:

Image 16: 总是折射的玻璃球体

11.3. 全反射

  折射的一个麻烦的问题是,对于某些光线角度,使用斯涅尔定律无法求解。当光线以足够的入射角进入折射率较低的介质时,它可以以大于90°的角度进行折射。如果我们回顾一下斯涅尔定律和sinθ\sin\theta^{'}的推导:

sin(θ)=ηηsin(θ)\sin(\theta ^{'})=\frac{\eta}{\eta^{'}}\cdot\sin(\theta)

  如果光线在玻璃内,而外部介质是空气(η=1.5\eta = 1.5 以及 η=1.0\eta' = 1.0

sin(θ)=1.51.0sin(θ)\sin(\theta ^{'})=\frac{1.5}{1.0}\cdot\sin(\theta)

  而sinθ\sin\theta'的值不能大于1.0

1.51.0sinθ>1.0\frac{1.5}{1.0}\sin\theta \gt 1.0

  方程两边之间的相等性被打破,解不存在。如果不存在解决方案,则玻璃无法折射,因此必须反射光线

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

  在这里,所有的光都被反射了,因为在实践中,这通常在固体物体内部,所以它被称为全内反射。这就是为什么有时当您浸入水中时,水与空气的边界就像一面完美的镜子——如果你在水下仰望,你可以看到水面上的东西,但是当你靠近水面朝着与水面较为平行的方向看时,水面看起来像一面镜子。

  我们可以使用三角函数的特性求解sinθ\sin\theta:

sinθ=1cos2θ\sin\theta =\sqrt{1-\cos^2\theta}

  以及:

cosθ=Rn\cos \theta = R\cdot n
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);

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

  而总是发生折射的电介质材质(在可能的情况下)修改如下:

class dielectric : public material {
  public:
    dielectric(double refraction_index) : refraction_index(refraction_index) {}

    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 ri = rec.front_face ? (1.0/refraction_index) : refraction_index;

        vec3 unit_direction = unit_vector(r_in.direction());
        double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
        double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);

        bool cannot_refract = ri * sin_theta > 1.0;
        vec3 direction;

        if (cannot_refract)
            direction = reflect(unit_direction, rec.normal);
        else
            direction = refract(unit_direction, rec.normal, ri);

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

  private:
    // Refractive index in vacuum or air, or the ratio of the material's refractive index over
    // the refractive index of the enclosing media
    double refraction_index;
};

  衰减始终为1——玻璃材质不吸收任何光线。

  如果我们使用新的dielectric::scatter()函数渲染前一个场景,我们会看到 ...没有变化。怎么回事呢?

  嗯,事实证明,给定一个折射率大于空气的材质球体,无论是在射入球的入口点还是在射线出口处都不会发生全反射。这是由于球体的几何形状决定的,因为入射光线将始终弯曲到较小的角度,然后在出口时弯曲回原始角度。

  因此,我们如何表现全反射呢?如果球体的折射率小于它所在的介质,那么我们可以用与入射平面较小角度的入射光线撞击它,获得全反射。这应该足以观察效果。

  我们假设整个世界充满了水(折射率大约是1.33),改变球体的材质为空气(折射率为1.0)——Oh,一个泡泡!为了做到这一点,依据下面的公式修改左边球体的折射率:

空气的折射率水的折射率\frac{空气的折射率}{水的折射率}
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.00 / 1.33);
auto material_right  = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);

  修改后渲染效果如下:

Image 17: 空气泡泡有时折射,有时反射

  你可以看到或多或少的直达光线会折射,而与平面夹角较小的光线会反射。

11.4. Schlick近似

  真正的玻璃具有随观察角度变化的反射率——以与观察表面较平行的方向看窗户时,它就会变成一面镜子。有一个大而丑陋的方程式可以描述这一特点,但几乎每个人都使用Christophe Schlick的一个性能较优且非常准确的多项式近似。基于此可以实现我们的玻璃材质:

class dielectric : public material {
  public:
    dielectric(double refraction_index) : refraction_index(refraction_index) {}

    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 ri = rec.front_face ? (1.0/refraction_index) : refraction_index;

        vec3 unit_direction = unit_vector(r_in.direction());
        double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
        double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);

        bool cannot_refract = ri * sin_theta > 1.0;
        vec3 direction;

        if (cannot_refract || reflectance(cos_theta, ri) > random_double())
            direction = reflect(unit_direction, rec.normal);
        else
            direction = refract(unit_direction, rec.normal, ri);

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

  private:
    // Refractive index in vacuum or air, or the ratio of the material's refractive index over
    // the refractive index of the enclosing media
    double refraction_index;

    static double reflectance(double cosine, double refraction_index) {
        // Use Schlick's approximation for reflectance.
        auto r0 = (1 - refraction_index) / (1 + refraction_index);
        r0 = r0*r0;
        return r0 + (1-r0)*std::pow((1 - cosine),5);
    }
};

11.5. 模拟空心玻璃球

  下面让我们渲染一个空心玻璃球。这是一个具有一定厚度的球体,里面有另一个空气球体。如果考虑光线穿过此类对象的路径,它将撞击外部球体、折射、撞击内部球体(假设我们确实击中它)、第二次折射,并穿过内部的空气。然后它将继续前进,撞击内球体的内表面,折射回来,然后撞击外球体的内表面,最后折射并退出到场景大气中。

  外球体使用标准玻璃球体建模,折射率约为 1.50(模拟从外部空气到玻璃中的折射)。内球体有点不同,因为它的折射率应该是相对于外球体的材质的,是玻璃到内空气的过渡。

  这实际上很容易指定,因为介质的refraction_index参数可以解释为物体的折射率除以外包围介质的折射率之比。在这种情况下,内球体的折射率是空气(内球体材质)的折射率除以玻璃(外包围介质)的折射率,即11.5=0.67\frac{1}{1.5}=0.67

  下面是代码实现:

...
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.50);
auto material_bubble = make_shared<dielectric>(1.00 / 1.50);
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.2),   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_bubble));
world.add(make_shared<sphere>(point3( 1.0,    0.0, -1.0),   0.5, material_right));
...

  渲染结果如下:

Image 18: 内部有空腔的球体

12. 可定位相机

  相机和电介质一样难以调试,所以我总是逐步开发我自己的相机。首先,让我们与许一个可调整的视野(fov)。这是从渲染图像的某一边到对边的视角。由于我们的图像不是正方形的,因此fov在水平和垂直方向上是不同的。我总是使用垂直 fov。我通常也以度数为单位指定它,并在构造函数中更改为弧度——这纯属个人喜好。

12.1. 相机视锥的几何表示

  首先,我们保持射线从原点发出,并朝向z=1z=-1的平面。如下图所示,只要我们让它和hh成比例,我们可以朝任意zz平面发射光线。

Figure 18: 相机视锥几何示意图

  如果z=1z=-1,这意味着h=tanθ2h=\tan\frac{\theta}{2}。相机的实现如下:

class camera {
  public:
    double aspect_ratio      = 1.0;  // Ratio of image width over height
    int    image_width       = 100;  // Rendered image width in pixel count
    int    samples_per_pixel = 10;   // Count of random samples for each pixel
    int    max_depth         = 10;   // Maximum number of ray bounces into scene

    double vfov = 90;  // Vertical view angle (field of view)

    void render(const hittable& world) {
    ...

  private:
    ...

    void initialize() {
        image_height = int(image_width / aspect_ratio);
        image_height = (image_height < 1) ? 1 : image_height;

        pixel_samples_scale = 1.0 / samples_per_pixel;

        center = point3(0, 0, 0);

        // Determine viewport dimensions.
        auto focal_length = 1.0;
        auto theta = degrees_to_radians(vfov);
        auto h = std::tan(theta/2);
        auto viewport_height = 2 * h * focal_length;
        auto viewport_width = viewport_height * (double(image_width)/image_height);

        // Calculate the vectors across the horizontal and down the vertical viewport edges.
        auto viewport_u = vec3(viewport_width, 0, 0);
        auto viewport_v = vec3(0, -viewport_height, 0);

        // Calculate the horizontal and vertical delta vectors from pixel to pixel.
        pixel_delta_u = viewport_u / image_width;
        pixel_delta_v = viewport_v / image_height;

        // Calculate the location of the upper left pixel.
        auto viewport_upper_left =
            center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2;
        pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
    }

    ...
};

  我们将会使用简单的场景来测试上面的代码,场景中有两个相切的球,并设置fov为90度。

int main() {
    hittable_list world;

    auto R = std::cos(pi/4);

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

    cam.aspect_ratio      = 16.0 / 9.0;
    cam.image_width       = 400;
    cam.samples_per_pixel = 100;
    cam.max_depth         = 50;

    cam.vfov = 90;

    cam.render(world);
}

  渲染效果如下:

Image 19: 较宽的相机视图

12.2. 相机的位置和朝向

  为了获得一个任意的视点,让我们首先说出我们关心的点。我们将放置摄像机的位置称为lookfrom,将我们注视的点称为lookat。(后续可以定义为要看的方向,而不是要看的点)。

  我们还需要一种指定相机的滚动或侧向倾斜的方法:围绕lookat-lookfrom轴的旋转。另一种思考方式是,即使你保持lookfromlookat不变,你仍然可以将头绕着鼻子旋转。我们需要一种方法来为相机指定“up”向量。

Figure 19: 相机观察方向

  我们可以指定任何我们想要的上方向,只要它不平行于视图方向即可。将此上方向向量投影到与视线方向正交的平面上,以获得相对于摄像机的上方向向量。我使用的常见惯例将其命名为“view up”(vup)向量。经过一些叉积和向量归一化,我们现在有一个完整的正交基 (uvw)(u,v,w)来描述相机的方向。uu是指向相机右侧的单位向量,vv是指向相机向上的单位向量,ww是指向视图方向相反的单位向量(因为我们使用右侧坐标),相机中心位于原点。

Figure 20: 相机的上方向

  和以前一样,当我们的固定相机面向Z−Z时,我们的任意视图相机面向w−w。请记住,我们可以,但不是必须使用world up(0,1,0)来指定vup。这很方便,并且会自然地使您的相机保持水平,直到您决定尝试疯狂的相机角度。

class camera {
  public:
    double aspect_ratio      = 1.0;  // Ratio of image width over height
    int    image_width       = 100;  // Rendered image width in pixel count
    int    samples_per_pixel = 10;   // Count of random samples for each pixel
    int    max_depth         = 10;   // Maximum number of ray bounces into scene

    double vfov     = 90;              // Vertical view angle (field of view)
    point3 lookfrom = point3(0,0,0);   // Point camera is looking from
    point3 lookat   = point3(0,0,-1);  // Point camera is looking at
    vec3   vup      = vec3(0,1,0);     // Camera-relative "up" direction

    ...

  private:
    int    image_height;         // Rendered image height
    double pixel_samples_scale;  // Color scale factor for a sum of pixel samples
    point3 center;               // Camera center
    point3 pixel00_loc;          // Location of pixel 0, 0
    vec3   pixel_delta_u;        // Offset to pixel to the right
    vec3   pixel_delta_v;        // Offset to pixel below
    vec3   u, v, w;              // Camera frame basis vectors

    void initialize() {
        image_height = int(image_width / aspect_ratio);
        image_height = (image_height < 1) ? 1 : image_height;

        pixel_samples_scale = 1.0 / samples_per_pixel;

        center = lookfrom;

        // Determine viewport dimensions.
        auto focal_length = (lookfrom - lookat).length();
        auto theta = degrees_to_radians(vfov);
        auto h = std::tan(theta/2);
        auto viewport_height = 2 * h * focal_length;
        auto viewport_width = viewport_height * (double(image_width)/image_height);

        // Calculate the u,v,w unit basis vectors for the camera coordinate frame.
        w = unit_vector(lookfrom - lookat);
        u = unit_vector(cross(vup, w));
        v = cross(w, u);

        // Calculate the vectors across the horizontal and down the vertical viewport edges.
        vec3 viewport_u = viewport_width * u;    // Vector across viewport horizontal edge
        vec3 viewport_v = viewport_height * -v;  // Vector down viewport vertical edge

        // Calculate the horizontal and vertical delta vectors from pixel to pixel.
        pixel_delta_u = viewport_u / image_width;
        pixel_delta_v = viewport_v / image_height;

        // Calculate the location of the upper left pixel.
        auto viewport_upper_left = center - (focal_length * w) - viewport_u/2 - viewport_v/2;
        pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
    }

    ...

  private:
};

  我们把场景改回去,并使用新的视角渲染:

int main() {
    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.50);
    auto material_bubble = make_shared<dielectric>(1.00 / 1.50);
    auto material_right  = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);

    world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
    world.add(make_shared<sphere>(point3( 0.0,    0.0, -1.2),   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_bubble));
    world.add(make_shared<sphere>(point3( 1.0,    0.0, -1.0),   0.5, material_right));

    camera cam;

    cam.aspect_ratio      = 16.0 / 9.0;
    cam.image_width       = 400;
    cam.samples_per_pixel = 100;
    cam.max_depth         = 50;


    cam.vfov     = 90;
    cam.lookfrom = point3(-2,2,1);
    cam.lookat   = point3(0,0,-1);
    cam.vup      = vec3(0,1,0);

    cam.render(world);
}

  渲染效果如下:

Image 20: 有一定距离的观察视角

  修改FOV参数为20:

    cam.vfov     = 20;

  渲染后得到:

Image 21: 放大

13. 焦散模糊(Defocus Blur)

  我们最后一个功能:焦散模糊。摄影师们通常将其称为“景深”,所以请确保只在学习光线追踪的朋友中使用“焦散模糊”的术语。

  真实相机中出现散焦模糊的原因是因为它们需要一个比较大光圈(而不仅仅是一个针孔)来聚焦光线。大光圈会使所有物体失焦,但如果我们在胶片/传感器前面放一个镜头,在一定的距离内所有物体都会聚焦。位于该距离的物体将会清晰可见,距离越远,物体就会显得越模糊。您可以想象这样的镜头:来自焦距处特定点的所有光线(照射到镜头上)都会弯曲回到图像传感器上的某个点。

  我们将相机中心与让所有物体完美对焦的平面之间的距离称为“焦距”。请注意,焦距通常与注视距离(focal length)不同——注视距离是相机中心与图像平面之间的距离。然而,对于我们的模型,这两个值将相同,因为我们将把像素网格平面放在焦平面上,与相机中心的距离为一个“焦距”。

  在物理相机中,焦距由镜头和胶片/传感器之间的距离控制。这就是为什么当你改变焦点时,你会看到镜头相对于相机移动的原因(这在你的手机相机中也可能发生,但却是传感器在移动)。“光圈”是一个孔,用于控制镜头的有效大小。对于真正的相机,如果你需要更多的光线,你可以把光圈调大,这样远离焦距的物体就会变得更加模糊。对于我们的虚拟相机,我们可以有一个完美的传感器,并且永远不需要更多的光线,所以我们只在想要散焦模糊时使用光圈。

13.1. 薄镜片的近似

  真正的相机有一个复杂的复合镜头。对于我们的代码,我们可以模拟以下顺序:传感器,然后是镜头,然后是光圈。然后我们可以找出将光线发送到哪里,并在计算后翻转图像(图像在胶片上是倒过来的)。然而,图形人员通常使用薄透镜近似:

Figure 21: 相机透镜示意图

  我们不需要模拟相机内部的任何部分,因为那将带来不必要的复杂性,只需要关注相机外部需要渲染的图像。相反,我通常会从无限薄的圆形“透镜”开始发出光线,并将它们发送到位于焦平面上相应的像素(与透镜的距离为“focal_length”),3D世界中该平面上的所有内容都完美对焦。

  实际上,我们通过将视口放置在焦平面上来实现这一点。把所有内容总结如下:

  1. 焦平面(forcus plane)与相机视线方向正交。
  2. 焦距(forcus distance)是相机中心与焦平面之间的距离。
  3. 视口位于焦平面上,以相机视线方向矢量为中心。
  4. 像素格子位于视口内(3D 世界中)
  5. 从当前像素位置周围的区域中随机选择样本位置
  6. 相机从镜头(lens)上的随机点发射射线,穿过当前图像采样位置。

Figure 22: 相机注视平面

  译者注:焦散模糊是这么理解的,光线原本聚焦于相机内部一点,而焦散模糊的时候,光线打在相机内部一个平面上的(上图中的 lens )。

13.2. 生成采样光线

  如果没有焦散模糊,所有场景光线都源自相机中心(或“lookfrom”)。为了实现焦散模糊,我们构建了一个以相机中心为中心的圆盘。半径越大,焦散模糊越大。您可以将我们的原始相机想象为具有半径为零的焦散圆盘(完全没有模糊),因此所有光线都源自圆盘中心(lookfrom)。

  那么,焦散盘应该有多大?由于此盘的大小控制着我们获得的焦散模糊程度,因此它应该是相机类的一个参数。我们可以将盘的半径作为相机参数,但模糊会根据投影距离而变化。一个稍微简单的参数是指定锥体的角度,其顶点位于视口中心,底部(焦散盘)位于相机中心。当您改变给定镜头的焦距时,这应该会为您提供更一致的结果。

  由于我们将从焦散盘中选择随机点,因此我们需要一个函数来执行此操作:random_in_unit_disk()。此函数使用的方法与我们在random_in_unit_sphere()中使用的方法相同,区别在于它只针对二维平面。

...

inline vec3 unit_vector(const vec3& u) {
    return v / v.length();
}

inline vec3 random_in_unit_disk() {
    while (true) {
        auto p = vec3(random_double(-1,1), random_double(-1,1), 0);
        if (p.length_squared() < 1)
            return p;
    }
}

...

  现在让我们更新相机以从焦散盘发出光线:

class camera {
  public:
    double aspect_ratio      = 1.0;  // Ratio of image width over height
    int    image_width       = 100;  // Rendered image width in pixel count
    int    samples_per_pixel = 10;   // Count of random samples for each pixel
    int    max_depth         = 10;   // Maximum number of ray bounces into scene

    double vfov     = 90;              // Vertical view angle (field of view)
    point3 lookfrom = point3(0,0,0);   // Point camera is looking from
    point3 lookat   = point3(0,0,-1);  // Point camera is looking at
    vec3   vup      = vec3(0,1,0);     // Camera-relative "up" direction

    double defocus_angle = 0;  // Variation angle of rays through each pixel
    double focus_dist = 10;    // Distance from camera lookfrom point to plane of perfect focus

    ...

  private:
    int    image_height;         // Rendered image height
    double pixel_samples_scale;  // Color scale factor for a sum of pixel samples
    point3 center;               // Camera center
    point3 pixel00_loc;          // Location of pixel 0, 0
    vec3   pixel_delta_u;        // Offset to pixel to the right
    vec3   pixel_delta_v;        // Offset to pixel below
    vec3   u, v, w;              // Camera frame basis vectors
    vec3   defocus_disk_u;       // Defocus disk horizontal radius
    vec3   defocus_disk_v;       // Defocus disk vertical radius

    void initialize() {
        image_height = int(image_width / aspect_ratio);
        image_height = (image_height < 1) ? 1 : image_height;

        pixel_samples_scale = 1.0 / samples_per_pixel;

        center = lookfrom;

        // Determine viewport dimensions.
        auto focal_length = (lookfrom - lookat).length();
        auto theta = degrees_to_radians(vfov);
        auto h = std::tan(theta/2);
        auto viewport_height = 2 * h * focus_dist;
        auto viewport_width = viewport_height * (double(image_width)/image_height);

        // Calculate the u,v,w unit basis vectors for the camera coordinate frame.
        w = unit_vector(lookfrom - lookat);
        u = unit_vector(cross(vup, w));
        v = cross(w, u);

        // Calculate the vectors across the horizontal and down the vertical viewport edges.
        vec3 viewport_u = viewport_width * u;    // Vector across viewport horizontal edge
        vec3 viewport_v = viewport_height * -v;  // Vector down viewport vertical edge

        // Calculate the horizontal and vertical delta vectors to the next pixel.
        pixel_delta_u = viewport_u / image_width;
        pixel_delta_v = viewport_v / image_height;

        // Calculate the location of the upper left pixel.
        auto viewport_upper_left = center - (focus_dist * w) - viewport_u/2 - viewport_v/2;
        pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);

        // Calculate the camera defocus disk basis vectors.
        auto defocus_radius = focus_dist * std::tan(degrees_to_radians(defocus_angle / 2));
        defocus_disk_u = u * defocus_radius;
        defocus_disk_v = v * defocus_radius;
    }

    ray get_ray(int i, int j) const {
        // Construct a camera ray originating from the defocus disk and directed at a randomly
        // sampled point around the pixel location i, j.

        auto offset = sample_square();
        auto pixel_sample = pixel00_loc
                          + ((i + offset.x()) * pixel_delta_u)
                          + ((j + offset.y()) * pixel_delta_v);

        auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample();
        auto ray_direction = pixel_sample - ray_origin;

        return ray(ray_origin, ray_direction);
    }

    vec3 sample_square() const {
        ...
    }

    point3 defocus_disk_sample() const {
        // Returns a random point in the camera defocus disk.
        auto p = random_in_unit_disk();
        return center + (p[0] * defocus_disk_u) + (p[1] * defocus_disk_v);
    }

    color ray_color(const ray& r, int depth, const hittable& world) const {
        ...
    }
};

  使用大光圈:

int main() {
    ...

    camera cam;

    cam.aspect_ratio      = 16.0 / 9.0;
    cam.image_width       = 400;
    cam.samples_per_pixel = 100;
    cam.max_depth         = 50;

    cam.vfov     = 20;
    cam.lookfrom = point3(-2,2,1);
    cam.lookat   = point3(0,0,-1);
    cam.vup      = vec3(0,1,0);

    cam.defocus_angle = 10.0;
    cam.focus_dist    = 3.4;

    cam.render(world);
}

  渲染结果如下:

Image 22: 带景深的球体渲染结果

14. 那么接下来呢?

14.1. 最后的场景

  让我们制作这本书封面上的图像——许多随机的球体。

int main() {
    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));

    camera cam;

    cam.aspect_ratio      = 16.0 / 9.0;
    cam.image_width       = 1200;
    cam.samples_per_pixel = 500;
    cam.max_depth         = 50;

    cam.vfov     = 20;
    cam.lookfrom = point3(13,2,3);
    cam.lookat   = point3(0,0,0);
    cam.vup      = vec3(0,1,0);

    cam.defocus_angle = 0.6;
    cam.focus_dist    = 10.0;

    cam.render(world);
}

  (请注意,上述代码与项目示例代码略有不同:上面的 samples_per_pixel 设置为 500,以获得需要相当长一段时间才能渲染的高质量图像。项目源代码使用值10,以便在开发和验证时获得合理的运行时间。)

  渲染结果如下:

Image 23: 最终场景

  你可能会注意到一件有趣的事情,那就是玻璃球实际上没有阴影,这使得它们看起来像是漂浮着的。这不是Bug——你在现实生活中很少看到玻璃球,它们看起来也有点奇怪,而且在阴天确实看起来像是漂浮着的。玻璃球下大球体上的点仍然有大量的光线照射到它,因为有大量光线仍然来自天空。

14.2. 下一步计划

  现在,你拥有了一个很酷的光线追踪渲染器,那么接下来呢?

14.2.1. 第二本书: Ray Tracing: The Next Week

  该系列的第二本书,将会基于这本书开发的光线追踪渲染器进行。将会包括以下新的功能实现:

  • 运动模糊——真实地渲染运动中的物体
  • BVH——加速渲染场景的速度
  • 纹理映射——在物体表面放置纹理
  • 柏林噪声——对于很对技术而言,一个随机的噪声生成器会很有用
  • 四边形——除了球体之外,再渲染更多的其他物体。此外,它是实现磁盘、三角形、环或几乎任何其他二维图元的基础。
  • 光照——往场景中增加光源
  • Transforms——对于放置和旋转物体很有用
  • 体渲染——渲染烟,云和其他气态的体积
14.2.2. 第三本书: Ray Tracing: The Rest of Your Life

  这本书再次扩展第二本书的内容。这本书的很多内容都是关于提高渲染图像质量和渲染器性能,并侧重于生成正确的射线并适当地累积它们。

  这本书适合那些对编写专业级光线跟踪非常感兴趣,和/或对实现次表面散射或嵌套电介质等高级效果的基础感兴趣的读者。

14.2.3. 其他指导

  您可以从这里获取很多其他指导,包括我们在本系列中尚未介绍的技术。这些包括:

  • 三角形——大多数酷炫的模型都是三角形网格。模型I/O是最糟糕的,几乎每个人都试图让别人的代码来做这件事。这还包括高效处理大型三角形网格的技术,这本身颇具挑战。
  • 并行性——使用不同的随机种子在N个核心上运行N份代码。对N次运行取平均值。取平均值也可以分层进行,其中可以对 N/2 对图像取平均值以获得N/4个图像,然后对这些图像取平均值。这种并行方法应该可以很好地扩展到数千个核心,并且只需很少的编码。
  • 阴影射线——当向光源发射光线时,您可以准确确定特定点的阴影情况。这样,您可以渲染清晰或柔和的阴影,为场景增添另一层真实感。

  玩得开心,请将你的精彩图片发送给我!

译者:寒江雪