0%

【RayTracer】(七)景深效果

到目前为止,我们的简易光线追踪器就差不多了,接下来对相机进行一些改进,使得我们能在任何角度观察场景并且模拟镜头相机的景深效果。

1 更强大的相机

到目前为止我们的相机基本上还是固定的,我们希望能够让相机更强大一些。比如可以调节视场角大小以扩充我们的视野,以及能够移动相机,从不同的角度观察整个场景。

1.1 自定义视场

对于视场角,一般使用垂直视场角,我们将视场角加到相机类的构造函数中,通过视场角和宽高比来计算视口尺寸,这是一个简单的几何计算:

fig-1.14-cam-view-geom

我们假设视口平面在 z = -1 处,于是视口的高度 h 即为:
$$
h = tan(\frac{\theta}{2})
$$
由此我们可以修改相机类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
相机类camera
*/
#pragma once

#ifndef CAMERA_H
#define CAMERA_H

#include "utilities.h"

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

#endif

1.2 自定义指向和位置

接下来我们希望摄像机能在任何位置任何角度观察场景。首先回顾之前图形学中学习的如何固定一个相机位置和姿态,我们需要一个位置、观察方向,以及一个 up 向量,这个 up 向量指定了相机旋转的角度,然后我们要根据这些值来计算在这个对应的角度下视口平面是怎样的,这可以通过两次叉乘得到:

  • up 向量和观察方向 w 叉乘得到视口平面 u 方向
  • u 方向和 w 方向叉乘得到视口平面 v 方向

其中观察方向 w 可以通过给定的相机位置 lookfrom 和要观察的点 lookat 得到,我们沿着 -w 方向观察,如下图:

fig-1.16-cam-view-up

于是我们可以继续修改相机类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
相机类camera
*/
#pragma once

#ifndef CAMERA_H
#define CAMERA_H

#include "utilities.h"

class camera {
public:
camera(point3 lookfrom, point3 lookat, vec3 vup, 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 w = normalize(lookfrom - lookat);
auto u = normalize(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;
};

#endif

然后修改主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <string>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include "hittable_list.h"
#include "sphere.h"
#include "color.h"
#include "camera.h"

int main()
{
/****图片保存,保存为png格式****/
std::string SavePath = "D:\\TechStack\\ComputerGraphics\\Ray Tracing in One Weekend Series\\Results\\";
std::string filename = "CameraView.png";
std::string filepath = SavePath + filename;

/*******图片属性*******/
// 宽高比
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
// 使用static_cast可以明确告诉编译器,这种损失精度的转换是在知情的情况下进行的
// 也可以让阅读程序的其他程序员明确你转换的目的而不是由于疏忽
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int channel = 3;
// 每个像素的采样数量
const int samples_per_pixel = 100;
// 光线至少弹射次数
const int min_bounce = 5;
// 俄罗斯轮盘赌算法生存概率
const double RR = 0.9;

/*******创建相机*******/
camera cam(point3(-2, 2, 1), point3(0, 0, -1), vec3(0, 1, 0), 90, aspect_ratio);

/*******创建场景*******/
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.3);
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));

/******渲染部分*****/
// 3通道图像存在一维数组中
unsigned char* odata = (unsigned char*)malloc(image_width * image_height * channel);
unsigned char* p = odata;
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, min_bounce, RR);
}
write_color(p, pixel_color, samples_per_pixel);
}
}
stbi_write_png(filepath.c_str(), image_width, image_height, channel, odata, 0);
std::cerr << "\nDone.\n";
}

得到的效果如下:

CameraView

调整视场角为 30 度,得到的效果如下:

CameraViewTiny

2 散焦模糊

之前我们投射光线都假设相机的镜头是一个只容纳一根光线的针孔,一根光线投射出去会打到场景中的一个点,但实际相机的镜头是一个透镜,透镜会将场景中多束光线汇聚到一个点上,如下图:

20200301194134436

对于树的顶点P,其传入到成像屏幕的范围,从之前的一条光线,扩大到 L1 到 L2 两条光线之间的部分,尽管采样的光线变多了,但并不影响这一棵树的清晰成像,因为目前这棵树到相机的距离,刚好是新的屏幕到相机的距离,即焦点距离。焦点距离不等同于焦距,焦距是投影点到图像平面的距离。

但是如果我们的相机向前移动一点,原本能采样到树顶的像素颜色,变成了多条光线采样值的混合色,也就是树顶部下面一片区域的颜色,从而导致这个像素变模糊,越往前移动,越模糊,因为 L1 和 L2 的区间会扩大更多;而如果我们的相机向后移动一点,并延长光线L1 和 L2 至树的纵切平面,则会采样天空和树头顶的颜色的混合色,同样实现模糊。越往后,L1 和 L2 的区间将会扩大,从而越模糊。

因此,只要物体到相机的距离不等于焦点距离,就会出现模糊,光圈越大,采样射线的跨度越大,模糊效果越明显,这就是散焦模糊的原理。我们为了简单的模拟这一过程,可以将原本相机位置的一个点,变为相机所在位置为中心的一个圆盘内的点,然后从圆盘内的多个点出发,投射光线到场景中并采样求平均,就可以实现上述镜头景深的效果。

首先先增加一个生成单位圆盘内随机点的函数:

1
2
3
4
5
6
7
8
// 生成单位圆盘内随机一点
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) continue;
return p;
}
}

然后修改相机类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
相机类camera
*/
#pragma once

#ifndef CAMERA_H
#define CAMERA_H

#include "utilities.h"

class camera {
public:
camera(
point3 lookfrom,
point3 lookat,
vec3 vup,
double vfov,
double aspect_ratio,
double aperture, // 光圈大小,光圈为0就是之前的针孔相机
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 = normalize(lookfrom - lookat);
u = normalize(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;
};

#endif

然后修改相机参数:

1
2
3
4
5
6
7
8
/*******创建相机*******/
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);

得到的效果如下:

DefoucsBlur

减小光圈将会降低模糊:

1
2
auto aperture = 1.0;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);

DefoucsBlur1.0

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。