0%

90:初始图形学(13)

自从上个学期之后,就怎么接触过图形学了。但是仔细感受下来,图形学是最让人有收获感的方向。它通过模拟真实的物理规则,通过计算模拟出真实的模拟场景,十分有趣,就是给人一种融汇贯通的感觉,学到的知识能结合在一起,我感觉这才是最有意义的地方。

然后进度的话是接着上一次”图形学(12)“最终的场景,在此之前,我把之前的项目代码熟悉了一下,在WSL环境上重构了一遍,感觉效率相对于原来还是快了一点。现在就正式开始吧

运动模糊

在真实的世界中,当我们按下快门,相机会接受一段时间内的光线信息,在此期间内,世界中的物体可能会发生移动,这样排除来的照片,我称之为运动模糊。为了真实的再现这个效果,需要对我们现有的程序进行补充:

简介

我们可以在快门开启的期间随机选择一个时间点发射一条射线从而得到,这条射线上的光子信息。只要我们能够知道在那个随机时间点上,场景中每个物体的位置和姿态。我们就可以像处理静态场景一样,计算这条光线与物体的交点、着色等。这样,这条光线就能准确反映那个特定瞬间的光照情况。

因此光线追踪的过程只需要,在处理每条光线时,更新物体的位置,就可以是实现对物体动态的追踪了。为了实现这一点,我们需要让每条线携带时间信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ray {
public:
ray() {}
ray(const point3& origin, const vec3& direction, double time)
: orig(origin), dir(direction), tm(time) {}
// 默认初始化时间为0
ray(const point3& origin, const vec3& direction)
: orig(origin), dir(direction), tm(0.0) {}

point3 origin() const { return orig; }
vec3 direction() const { return dir; }
double time() const { return tm; }

point3 at(double t) const {
return orig + t * dir;
}

private:
point3 orig;
vec3 dir;
double tm;
};

时间管理

真实世界中的快门时间是由两部分决定的:

  • 帧间隔:连续两帧画面之间的时间间隔
  • 快门开启时长:在每一帧的帧间隔中,快门实际打开、允许光线进入的时间长度(一般情况下开启时间越长,运动模糊越明显)

但是这里为了简化光线追踪的实现,我们只对一帧的画面进行捕捉。当然,如果想要渲染一个完整的动画,只需要设置好适当的快门时间就可以了。如果世界是静态的,只有相机的位置在移动,那么我们的代码无需做出改变。但如果世界中的物体在进行运动,那么我们需要为hittable设置一种方法,使得每个物体都能感知到当前帧的时间周期,并更新他们此时的位置。

为了简化墨香,我们只渲染一帧,我们把时间设置为从时间t=0到t=1。我们将让相机在时间[0,1]之间随机发射光线,并更新我们的球体类。

更新相机

我们更新相机,使其在开始和结束时间之间随机生成光线,这里我们通过相机类来对光线信息进行管理和生成:

1
2
3
4
5
6
7
8
ray get_ray(int i, int j){
auto offset = sample_pixel_offset();
auto pixel_sample = pixel_origin + (i + offset.x()) * pixel_delta_u + (j + offset.y()) * pixel_delta_v;
auto ray_origin = (defocus_angle > 0.0) ? defocus_disk_sample() : camera_origin;
auto ray_direction = pixel_sample - ray_origin;
auto ray_time = random_double();
return ray(ray_origin, ray_direction, ray_time);
}

球体运动属性

现在我们要让我们的世界动起来,我们需要更新球体类,使其中心在t=0到t=1的时间中,从center1线性速度的移动到center。我们将center属性修改成一个从t=0时刻指向t=1时刻的射线向量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class sphere: public hittable {
public:
// 静态球体
sphere(const point3& center, double radius, shared_ptr<material> mat)
: center(center,vec3(0,0,0)), radius(radius), mat(mat) {}
// 动态球体
sphere(const point3& center1, const point3& center2, double radius, shared_ptr<material> mat)
: center(center1,center2-center1), radius(radius), mat(mat) {}
...

private:
ray center;
double radius;
shared_ptr<material> mat;
};

然后我们需要更新hit方法,需要需要在更新动画中心的位置之后,也能正确的进行相交判断:

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
bool hit(const ray& r, interval t_range, hit_record& rec) const override {
point3 cur_center = center.at(r.time());
vec3 oc = r.origin() - cur_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 = std::sqrt(discriminant);
auto root = (-half_b - sqrtd) / a;
if (!t_range.contains(root)) {
root = (-half_b + sqrtd) / a;
if (!t_range.contains(root)) {
return false;
}
}
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - cur_center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat = mat;
return true;
}

我们只需要根据射线携带的时间信息,更新我们的球心位置,然后正常的进行计算就行了,如果r.time()等于0,就说明位置不变。就算变了,我们也不需要花费额外的计算开销。

追踪光线的相交时间

现在光线有了时间属性,我们也需要记录每次光线和世界相交的时间信息,我们只需要简单的更新一下相交光线的信息就行了:

1
scattered = ray(rec.p, scatter_direction, r_in.time());

最终效果

现在我们想世界中添加球体的运动属性,以实现最终的动态渲染效果,让我们看看怎么样:

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
69
70
#include "utils.h"
#include "hittable_list.h"
#include "sphere.h"
#include "camera.h"
#include "material.h"

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 = -8; a < 8; a++) {
for (int b = -8; b < 8; 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.5) {
// diffuse
auto albedo = color::random() * color::random();
sphere_material = make_shared<lambertian>(albedo);
auto center2 = center + vec3(0, random_double(0,0.2), 0);
world.add(make_shared<sphere>(center, center2, 0.2, sphere_material));
} else if (choose_mat < 0.8) {
// metal
auto albedo = color::random(0.5, 1);
auto fuzz = random_double(0, 0.1);
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);
auto center2 = center + vec3(0, random_double(0,0.1), 0);
world.add(make_shared<sphere>(center, center2, 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 = 25.0 / 16.0;
cam.image_width = 1250;
cam.samples_per_pixel = 100;
cam.max_depth = 30;


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_distance = 10;

cam.render(world);
}

可以看到较为明显的运动痕迹:

image.png