1、综述
在《Ray Tracing in one weekend》中,我们制作了一个简单的暴力路径追踪器。在这个教程中,我们将增加纹理、体积(比如雾),矩形,实例、灯光,以及对物体使用BVH,我们将得到一个“正真的”光线追踪器。
在光线追踪器中的一个启示就是:许多优化虽然使代码变得复杂,但是没有对程序加速太多。作者强烈建议不要过早地优化代码,如果它在执行时间配置文件中没有那么明显,那么等到所有特性都被支持的时候才进行优化。可以在https://in1weekend.blogspot.com/中阅读和参考更复杂的方法。
书中最难的两个部分分别是BVH和Perlin纹理。你可以不用按顺序阅读章节,可以把他们放到最后阅读,没有BVH和Perlin纹理也可以做出康纳盒。
2 动态模糊
当您决定使用射线跟踪时,您认为视觉质量比运行时更有价值。在模糊反射和散焦模糊中,每个像素需要多个样本(发射多条射线通过该像素)。当你开始做时,有一个好消息:所有的效果都可以暴力实现。动态模糊就是其中之一。在一台真正的相机中,快门打开并保持打开一段时间,在这段时间内相机和物体可能会移动。它实际上是摄像机在我们想要的时间间隔内所看到的平均值。
2.1 时空光线追踪的介绍
我们可以通过在快门打开的某个随机时间发送每条射线来得到一个随机估计。只要物体在那个时候应该在的位置,我们就能得到正确的平均答案,只要射线恰好在同一时间。这就是为什么随机射线跟踪很简单的根本原因。
其基本思想是,当快门打开并与模型相交时,随机产生光线。通常的做法是让摄像机移动,物体移动,但每条光线都在同一时间存在。这样,射线追踪器的“引擎”就可以确保物体在它们需要的位置,并且交叉的部分不会发生太大的变化。
为了实现这个,我们首先需要存储一条光线存在的时间存储在(double)tm中:
// 网页上的代码 [ray.h] Ray with time information
class ray {
public:
ray() {}
ray(const point3& origin, const vec3& direction, double time = 0.0)
: orig(origin), dir(direction), tm(time)
{}
point3 origin() const { return orig; }
vec3 direction() const { return dir; }
double time() const { return tm; }
point3 at(double t) const {
return orig + t*dir;
}
public:
point3 orig;
vec3 dir;
double tm;
};
来自 <https://raytracing.github.io/books/RayTracingTheNextWeek.html>
2.2 升级摄像头模拟运动模糊
现在我们需要修改相机以生成在time1~time2内的随机时间的光线。对于这个时间,有两种考虑:
- 由相机类跟踪time1和tiem2
- 由创建相机的用户跟踪time1和time2
可以根据个人喜好,这里选择第一种。(When in doubt, I like to make constructors complicated if it makes calls simple)
由于这里的相机是固定的,不需要改变太多:
- 在类中增加两个时间属性time0,time1
- 在get_ray方法中更新返回的构造函数参数
// 网页上的代码 [camera.h] Camera with time information
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,
double t0 = 0,
double t1 = 0
) {
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;
time0 = t0;
time1 = t1;
}
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,
random_double(time0, time1)
);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
vec3 u, v, w;
double lens_radius;
double time0, time1; // shutter open/close times
};
2.3 添加移动球体
现在添加一个移动的物体。我们创建一个移动球类moving_sphere,在time0时刻球心位于center0,在time1时刻球心位于center1,线性移动;在这个时间之外它继续移动,不需要配合光圈的开启和关闭。
moving_sphere类:
- 数据:center0,center1,time0, time1,球的半径,材质智能指针
- 方法:
- 默认初始化和带参初始化所有元素
- hit函数
- 利用前面推到出的公式计算射线与小球是否相交,这里的oc的c是在time0到time1(定义在类中,是自带的属性)时刻内的任一时刻的小球的球心的位置。
- 如果相交,计算相交点的信息,包括此时交点对应到射线上的t值,交点位置,交点处的表面法向量,反射光线所在面的法向量,球面材质。这里计算交点处的表面法向量用到的球心是time0到time1(定义在类中,是自带的属性)时刻内的任一时刻的小球的球心的位置。
3. 计算球心在time时刻的位置
// 网页上的代码 time0到time1(定义在类中,是自带的属性)时刻内的任一时刻的小球的球心的位置。
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) {
auto root = sqrt(discriminant);
auto temp = (-half_b - root)/a;
if (temp < t_max && temp > t_min) {
rec.t = temp;
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;
}
temp = (-half_b + root) / a;
if (temp < t_max && temp > t_min) {
rec.t = temp;
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;
}
}
return false;
}
来自 <https://raytracing.github.io/books/RayTracingTheNextWeek.html>
// 网页上的的代码 [moving_sphere.h] A moving sphere
class moving_sphere : public hittable {
public:
moving_sphere() {}
moving_sphere(
point3 cen0, point3 cen1, double t0, double t1, double r, shared_ptr<material> m)
: center0(cen0), center1(cen1), time0(t0), time1(t1), radius(r), mat_ptr(m)
{};
virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const;
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);
}
2.4 跟踪射线交点时间
确保在材料中散射的光线与入射光线的时间一致。
// 网页上的代码[material.h] Lambertian matrial for moving objects
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 {
vec3 scatter_direction = rec.normal + random_unit_vector();
scattered = ray(rec.p, scatter_direction, r_in.time());
attenuation = albedo;
return true;
}
color albedo;
};
2.5 把所有东西放在一起
下面的代码采用了上一本书结尾场景中的漫射球的例子,并使它们在图像渲染期间移动。(想象一台相机,快门在0时刻开启,在1时刻关闭。)每个球在t=0时刻从其中心C移动到t=1时刻的C+(0,r/2,0),其中r为[0,1)中的随机数:
// 网页上的代码 [main.cc] Last book's final scene, but with moving spheres
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 - vec3(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);
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) {
// 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;
}
来自 <https://raytracing.github.io/books/RayTracingTheNextWeek.html>
和下面的观测参数:
// 网页上的代码 [main.cc] Viewing parameters
const auto aspect_ratio = double(image_width) / image_height;
...
point3 lookfrom(13,2,3);
point3 lookat(0,0,0);
vec3 vup(0,1,0);
auto dist_to_focus = 10.0;
auto aperture = 0.0;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0);
来自 <https://raytracing.github.io/books/RayTracingTheNextWeek.html>
我们将得到:
其实这里我有一个疑问,为什么那个大的金属球里的反射的小球没有一个的移动的。
答:金属材质在返回反射光线的时候没有带上入射光线的时间,导致新赋值的反射光线的time被默认初始化为0.0了,所以此时金属反射的运动小球是0.0时刻小球的位置。
只要在metal类中修改scatter函数,加入入射射线的时间就可以获得正常结果:
正常结果:地面是个巨大的金属球,fuzz=0.0,可以看到运动小球在金属球面上的正确倒影
(2020/09/03)
PS: 第一次运行的时候我在camera里面忘记初始化time0和time1了,结果变成这样: