动态模糊
“带时间的光线”划开了新旧篇章的界限,新的维度下,新的可能性在孕育。
这一章中就来看看,“时间”会给这个世界带来什么变化。本章我们会创建继“球”之后,第二个实体物体类:“移动的球”。呃,抱歉,它还是球。
匀速运动的球
创建新文件moving_sphere.h,并敲入如下代码,其实其中大部分代码是从球类中复制过来的:
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
| #ifndef MOVING_SPHERE_H #define MOVING_SPHERE_H
#include "rtweekend.h" #include "hittable.h"
class moving_sphere : public hittable { public: moving_sphere() {} moving_sphere( point3 cen0, point3 cen1, double _time0, double _time1, double r, shared_ptr<material> m) : center0(cen0), center1(cen1), time0(_time0), time1(_time1), radius(r), mat_ptr(m) {};
virtual bool hit( const ray& r, double t_min, double t_max, hit_record& rec) const override;
point3 center(double time) const;
public: point3 center0, center1; double time0, time1; double radius; shared_ptr<material> mat_ptr; };
point3 moving_sphere::center(double time) const { return center0 + ((time - time0) / (time1 - time0))*(center1 - center0); }
#endif
|
center函数的参数time不一定非要处于time0和time1之间,它也可以小于time0或者大于time1。从另一个角度来说,time0,time1和其对应的球心位置center0,center1并不是球运动的边界,而是用于确定球运动方向和速度的参数。
总结来说,这颗球沿着空间中的某条直线做匀速运动,它在time0时刻处于直线上的center0位置,time1时间下处于center1位置。
接下来补完moving_sphere类,它还差一个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 27
| bool moving_sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { vec3 oc = r.origin() - center(r.time()); 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);
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); auto outward_normal = (rec.p - center(r.time())) / radius; rec.set_face_normal(r, outward_normal); rec.mat_ptr = mat_ptr;
return true; }
|
材质类对应修改
每次递归都会产生新的光线,得确保时间信息可以传递到新光线上,修改材质类:
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
| class lambertian : public material { ... virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const override { ... scattered = ray(rec.p, scatter_direction, r_in.time()); ... } ... };
class metal : public material { ... virtual bool scatter( ... ) const override { ... scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere(), r_in.time()); ... } ... };
class dielectric : public material { ... virtual bool scatter( ... scattered = ray(rec.p, direction, r_in.time()); ... } ... };
|
修改初篇最终场景
我们尝试把初篇最终场景中的所有漫反射小球都替换成移动的球,并且让他们移动的方向都为y轴正方向,为了做出差异性,让所有的小球移动的速度都不同,我们要利用随机数给定不同的参数。
假设相机镜头在 time0=0 时刻打开镜头,time1=1 时刻关闭镜头,且time0时刻这些移动的球正好都在y = 0.2 平面上(这也是我们以前最终场景中的设计),也就是说center0.y() 是 0.2 ,那么center1就可以根据center0加上一个随机长度的y轴正方向向量得到,具体如下:
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
| ... #include "moving_sphere.h"
... hittable_list random_scene() { ...
for (int a = -11; a < 11; a++) { for (int b = -11; b < 11; b++) { ...
if (choose_mat < 0.8) { auto albedo = color::random() * color::random(); sphere_material = make_shared<lambertian>(albedo); auto center2 = center + vec3(0, random_double(0,.5), 0); world.add(make_shared<moving_sphere>( center, center2, 0.0, 1.0, 0.2, sphere_material)); } else if (choose_mat < 0.95) { ...
}
|
再修改相机和图片参数,为了不等待太久,适当降低了分辨率
1 2 3 4 5 6 7 8 9 10 11 12 13
| int main() { auto aspect_ratio = 16.0 / 9.0; int image_width = 400; int samples_per_pixel = 100;
...
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0);
|
会得到:

虽然这些移动的球的运动轨迹是“直破云霄”往天上去的,但是看起来就好像是球在平台上弹跳一样。这是因为无论是向下运动还是向上运动,动态模糊看上去都是一个样子的。
以下内容为个人探索(需要先完成初篇第十六章)。
配置ffmpeg*
现在来休息一下,暂停对技术的探索,一起来做一些工程方向的趣事。让我们来生成一系列的图片,并且把它串成视频。你不想看到自己的小球真的动起来吗?如果答案为肯定,那就来继续探索时空光线追踪的极限吧。
图片按序列帧转视频这件差事如果要靠代码完成可得费一番功夫,幸好伟大的前辈们早就写好了统一解决方案——ffmpeg!一个伟大的开源程序!它可以用来记录、转换数字音频、视频,并能将其转化为流,总之,它在音视频方面几乎无所不精无所不能。
但本篇并不是ffmpeg的教程贴,而且为了整体项目的精简,把ffmpeg接入光追项目暂时来看是没有必要。我们仅仅只利用ffmpeg作为外部工具生成视频,或者更准确来说,仅仅只用到一个命令。
一:先需要下载ffmpeg,直接点击该链接进行下载:https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z。
如果上方链接失效,可以通过以下方法:
- 先上官网:https://ffmpeg.org/download.html
- 因为我是windows系统,就按下图所示进入链接(其他系统下的ffmpeg安装远比windows下安装简单,具体请自行搜索):

进入之后,第一行就是最新的git分支的压缩包,俩个分别是只包含基础功能的以及全部功能的压缩包,随便选一个,因为我还使用ffmpeg做其他项目,所以安装的是full版本,但是应对本项目,essential应该就足够了。

你会下载到和刚刚我给你的链接里一样的安装包。
二:把下载的压缩包解压到某个目录,我将其解压到了D:\ffmpeg文件夹下。
三:配置环境变量。我的电脑右键属性-高级系统设置-环境变量-再编辑path-新建一个path为ffmpeg目录下的bin文件夹。(本部分我简单描述,环境变量配置为程序员必修课,如果不会的话去看教程,这里贴一个百度经验教程https://jingyan.baidu.com/article/a17d5285c9b0c48099c8f26a.html),配置完成你的path应该会大概如下多出一行:

四:配置完毕之后就可以通过命令行测试是否配置成功,在任意目录下输入ffmpeg -version,如果出现版本信息,即表示配置成功:

多线程输出序列帧*
切换代码为多线程模式,修改多线程的main所在文件如下所示:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| #if MULTITHREAD
#define _CRT_SECURE_NO_WARNINGS ... #include "moving_sphere.h"
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 200;
const int image_height = static_cast<int>(image_width / aspect_ratio); const int samples_per_pixel = 100; const int max_depth = 50;
... hittable_list random_scene() { ...
if (choose_mat < 0.8) { ...
auto center2 = center + vec3(0, random_double(0, 50), 0);
world.add(make_shared<moving_sphere>( center, center2, 0.0, 1.0, 0.2, sphere_material)); } else if (choose_mat < 0.95) { ... ... }
int main() { int video_frame_num = 10; std::string video_folder_path = "video";
world = random_scene(); 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;
std::string command; command = "mkdir " + video_folder_path; system(command.c_str()); char filename[50]; for (int i = 0; i < video_frame_num; i++) {
std::cerr << "LineIndex:" << i << std::endl;
sprintf(filename, "./%s/videoframe%04d.ppm", video_folder_path.c_str(), i);
buf = new imageoutput(image_width, image_height, filename); std::vector<std::thread> threads;
cam = new camera(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, pow(i / (float)video_frame_num, 5), pow((i + 1) / (float)video_frame_num, 5));
for (int k = 0; k < numThread; k++) { threads.push_back(std::thread(ray_tracing_by_thread, k)); } for (auto& thread : threads) { thread.detach(); }
while (thread_remain > 0) { std::cerr << "\rScanlines remaining : " << line_remain - 1 << ' ' << std::flush; }
line_remain = image_height; thread_remain = numThread;
std::cerr << "\nDone.\n"; buf->buffer_2_file();
delete cam; delete buf; }
return 0; }
#endif
|
移动的球类是一些匀速运动的小球,但这个匀速运动也是有条件的,那就是时间流速是匀速不变的。
我们将均匀增加的自变量i(每次循环加一),套进
函数,并将其映射到(0,1)区间内(和移动的球在创建之初指定的时间区间保持同步),即可实现球速逐渐加快效果,因为
在此区间为凹函数。可以简单理解为,套用该函数之后,时间流速不再均匀,而是会逐渐加快。
运行代码,如果你是在命令行中运行exe文件,就可以在exe所在目录下找到video文件夹。如果是vs内运行生成,这个文件夹会在代码所在目录下。

ffmpeg命令生成视频*
打开命令行,走到video目录,然后输入以下命令:
ffmpeg -r 10 -i videoframe%04d.ppm -q:v 1 output.avi
如图所示:

我们分开看看这个命令讲了什么:
ffmpeg 表示运行的exe的名字,系统会在环境变量里面的那些目录去找叫这个名字的exe,显然,它要找的就是ffmpeg/bin文件夹下的那个exe。
-r 10 表示我们要以一秒10帧的速度安排这些图片,最后生成的图片每秒钟会有十张图片播放。
-i videoframe%04d.ppm 表示图片输入,ffmpeg会找指定格式化输入的%d从0开始,这样它就可以找到目录下全部的10张图片。
-q:v 1 表示我们希望最后生成的图像质量高一些,但可惜的是,即使是这样它还是会失去一些清晰度。
output.avi 输出文件目录,没有前缀的话它就会存放在和图片同样的目录里。
在一阵提示输出之后,你会得到视频:

它的时长是1秒。我们做到了三个时间统一:移动的球类指定时间,相机区间和最终生成图片时间区间。
时间统一有很多好处,它可以让时间轴一目了然。
当然它们三个也可以不完全统一,而是某种映射关系,这取决于你的设计。
拓展
可以尝试通过修改代码,达到以下效果:


参考文献
https://raytracing.github.io/books/RayTracingTheNextWeek.html
参考自《Ray Tracing: The Next Week》第2.2节到第2.5节。