Ray Tracing: The Next Week
2. Motion Blur
- 在前面,反走样是通过像素内取多个路径实现的,此外多条路径的选择也跟后面的漫反射、模糊反射、散焦模糊等一系列随机过程有关,如果继续暴力解法,也可以实现运动模糊。
- 在真实的相机中,照片的形成是对一段时间内光线的记录,与快门有关,因此,为了模拟摄影,加入运动模糊。
- 首先运动模糊采取随机生成快门开启时间内某个时刻的光线,在此之后该光路上的其他反射或者折射光线都是同一时刻的光线,对应的是同一个时刻的物体位置。因此,修改ray类,为其加入时间成员属性,时间属性默认为0,即统一所有光线为初始时刻,不进行运动模糊。
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;
};
- 然后,在camera类中加入time0与time1两个属性,作为快门开启时间起始点。这两个属性用于在getray时为返回的ray类在该区间内随机生成光线发射时间。
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 _time0 = 0,
double _time1 = 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 = _time0;
time1 = _time1;
}
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
};
- 到目前为止,就得到了光线时间随机生成的功能。紧接着,需们需要实现运动的物体,此处为运动球体moving_sphere类。moving_sphere与sphere类似,需要定义球心坐标、半径和材质,后两者不需要修改,但是球心坐标需要改变。原本的sphere类的球心坐标是point3数据成员,可以被hit函数直接使用,而在moving_sphere类中球心坐标通过定义成员函数moving_sphere::center实现,为了实现某个时刻球心坐标的计算,定义了四个成员变量
point3 center0, center1;double time0, time1;
,利用线性方式求解球心位置(此处为线性匀速运动)。进一步的,在hit函数的重写中,只需要将center换为center(r.time()),与之前的对应起来了,即利用光线的时间属性来得到物体的位置,基于此计算相交结果。
#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);
}
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);
// Find the nearest root that lies in the acceptable range.
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;
}
#endif
- 最后,修改材质中的反射光线ray。
class lambertian : public material {
...
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
auto scatter_direction = rec.normal + random_unit_vector();
// Catch degenerate scatter direction
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction, r_in.time());
attenuation = albedo;
return true;
}
...
};
class metal : public material {
...
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere(), r_in.time());
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
...
};
class dielectric : public material {
...
virtual bool scatter(
...
scattered = ray(rec.p, direction, r_in.time());
return true;
}
...
};
- 将diffuse小球给予y向运动:
...
#include "moving_sphere.h"
...
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;
}
int main() {
// Image
auto aspect_ratio = 16.0 / 9.0;
int image_width = 400;
int samples_per_pixel = 100;
const int max_depth = 50;
...
// Camera
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;
int image_height = static_cast<int>(image_width / aspect_ratio);
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0);
效果如下:
3. Bounding Volume Hierarchies
- 在aabb.h中定义AABB类,主要内容是两个对角的点坐标,此外给出了相交检测。AABB类会作为BVH节点的属性。
#ifndef AABB_H
#define AABB_H
#include "rtweekend.h"
class aabb {
public:
aabb() {}
aabb(const point3& a, const point3& b) { minimum = a; maximum = b;}
point3 min() const {return minimum; }
point3 max() const {return maximum; }
bool hit(const ray& r, double t_min, double t_max) const {
for (int a = 0; a < 3; a++) {
auto t0 = fmin((minimum[a] - r.origin()[a]) / r.direction()[a],
(maximum[a] - r.origin()[a]) / r.direction()[a]);
auto t1 = fmax((minimum[a] - r.origin()[a]) / r.direction()[a],
(maximum[a] - r.origin()[a]) / r.direction()[a]);
t_min = fmax(t0, t_min);
t_max = fmin(t1, t_max);
if (t_max <= t_min)
return false;
}
return true;
}
point3 minimum;
point3 maximum;
};
#endif
- 对于hittable类,除了之前的相交检测之外,定义一个bounding_box函数,用于得到一个该类对象的aabb。
#include "aabb.h"
...
class hittable {
public:
...
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const = 0;
...
};
- sphere:球体的boundingbox是他的外切。
class sphere : public hittable {
public:
...
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override;
...
};
...
bool sphere::bounding_box(double time0, double time1, aabb& output_box) const {
output_box = aabb(
center - vec3(radius, radius, radius),
center + vec3(radius, radius, radius));
return true;
}
- moving sphere的boundingbox是其起止点boundingbox的surroundingbox
...
#include "aabb.h"
...
class moving_sphere : public hittable {
public:
...
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(
double _time0, double _time1, aabb& output_box) const override;
...
};
...
bool moving_sphere::bounding_box(double _time0, double _time1, aabb& output_box) const {
aabb box0(
center(_time0) - vec3(radius, radius, radius),
center(_time0) + vec3(radius, radius, radius));
aabb box1(
center(_time1) - vec3(radius, radius, radius),
center(_time1) + vec3(radius, radius, radius));
output_box = surrounding_box(box0, box1);
return true;
}
- hittablelist的boundingbox是其所有object的boundingbox的surroundingbox。
...
#include "aabb.h"
...
class hittable_list : public hittable {
public:
...
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(
double time0, double time1, aabb& output_box) const override;
...
};
...
bool hittable_list::bounding_box(double time0, double time1, aabb& output_box) const {
if (objects.empty()) return false;
aabb temp_box;
bool first_box = true;
for (const auto& object : objects) {
if (!object->bounding_box(time0, time1, temp_box)) return false;
output_box = first_box ? temp_box : surrounding_box(output_box, temp_box);
first_box = false;
}
return true;
}
- surroundingbox是对两个box的两个顶点分别取最小值的最小值和最大值的最大值,即外边界的顶点。
aabb surrounding_box(aabb box0, aabb box1) {
point3 small(fmin(box0.min().x(), box1.min().x()),
fmin(box0.min().y(), box1.min().y()),
fmin(box0.min().z(), box1.min().z()));
point3 big(fmax(box0.max().x(), box1.max().x()),
fmax(box0.max().y(), box1.max().y()),
fmax(box0.max().z(), box1.max().z()));
return aabb(small,big);
}
- 在bvh.h中定义了bvh_node类,这是一个BVH二叉树的节点类型;在一个二叉树中,非叶子节点是bvh_node类型,叶子节点是一个物体。因此,一种统一的方法是将bvh_node继承hittable类。作为一个二叉树结点,其主要内容是aabb与左右节点的指针shared_ptr
。此外,其继承了hittable的两个纯虚函数,hittable::hit与hittaable::bounding_box。不同于物体或者hittable_list,此处的hittaable::bounding_box并不是计算aabb的,而是取出bvh_node的aabb成员属性,而aabb成员的定义是通过初始化函数实现的。
#ifndef BVH_H
#define BVH_H
#include "rtweekend.h"
#include "hittable.h"
#include "hittable_list.h"
class bvh_node : public hittable {
public:
bvh_node();
bvh_node(const hittable_list& list, double time0, double time1)
: bvh_node(list.objects, 0, list.objects.size(), time0, time1)
{}
bvh_node(
const std::vector<shared_ptr<hittable>>& src_objects,
size_t start, size_t end, double time0, double time1);
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override;
public:
shared_ptr<hittable> left;
shared_ptr<hittable> right;
aabb box;
};
bool bvh_node::bounding_box(double time0, double time1, aabb& output_box) const {
output_box = box;
return true;
}
#endif
- bvh_node的hit函数重写。不同于物体的hit是对自身的相交检测,并给出相交信息,bvh_node的hit是对以他为根节点的树做前序遍历,首先利用节点的aabb成员检测光线相交,如果相交则检查两个子节点,并将相交结果交给子节点来提供,直到到达叶子节点,则使用某种物体的hit函数进行对该物体的相交检测,并修改hit_record。此外,这里使用了前序遍历,每一个left节点使用该节点的tmin和tmax,如果发生物体相加则会被记录在hit_record中,相应的right节点的tmax修改为相交时间hit_record::t,如此依赖就实现了对叶子节点(可以进入父节点box的)从左到右进行相交性检测,并进行最近相交节点的维护。
bool bvh_node::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
if (!box.hit(r, t_min, t_max))
return false;
bool hit_left = left->hit(r, t_min, t_max, rec);
bool hit_right = right->hit(r, t_min, hit_left ? rec.t : t_max, rec);
return hit_left || hit_right;
}
- bvh_node的构造函数定义了box、left与right,如果该节点对应的hittable_list的片选内容只有一个object,则将左右节点都指向该object,如果有两个object则进行一次比较,left指向位置更小的object,如果有多个object,则将该节点对应的hittable_list的片选内容进行位置排序(排序的维度是随机生成的),并均分成两份,分别利用make_shared<bvh_node>创建一个对象,分别对应半份片选,然后绑定在left与right上。至于aabb box是对左右节点由bounding_box得到的aabb进行surroundingbox得到。因为需要进行排序,所以对该节点的vector输入创建一个副本。可见,创建一个节点实则是创建以该节点为根节点的树,是一个递归函数。其中,每次递归都先将list分成两份,绑定在left与right上,如果list个数大于两个,就会进行递归,直到绑定到物体上,然后,反向的从物体的bounding_box计算得到aabb并向上层反馈,在上层进行surroundingbox。
#include <algorithm>
...
bvh_node::bvh_node(
std::vector<shared_ptr<hittable>>& src_objects,
size_t start, size_t end, double time0, double time1
) {
auto objects = src_objects; // Create a modifiable array of the source scene objects
int axis = random_int(0,2);
auto comparator = (axis == 0) ? box_x_compare
: (axis == 1) ? box_y_compare
: box_z_compare;
size_t object_span = end - start;
if (object_span == 1) {
left = right = objects[start];
} else if (object_span == 2) {
if (comparator(objects[start], objects[start+1])) {
left = objects[start];
right = objects[start+1];
} else {
left = objects[start+1];
right = objects[start];
}
} else {
std::sort(objects.begin() + start, objects.begin() + end, comparator);
auto mid = start + object_span/2;
left = make_shared<bvh_node>(objects, start, mid, time0, time1);
right = make_shared<bvh_node>(objects, mid, end, time0, time1);
}
aabb box_left, box_right;
if ( !left->bounding_box (time0, time1, box_left)
|| !right->bounding_box(time0, time1, box_right)
)
std::cerr << "No bounding box in bvh_node constructor.\n";
box = surrounding_box(box_left, box_right);
}
- 比较部分:利用bvh_node的box属性或者其他hittable的boundingbox计算结果进行比较,这里采取了利用aabb的最小值节点的某一个维度进行比较。按照比较维度的不同,得到三个函数。
inline bool box_compare(const shared_ptr<hittable> a, const shared_ptr<hittable> b, int axis) {
aabb box_a;
aabb box_b;
if (!a->bounding_box(0,0, box_a) || !b->bounding_box(0,0, box_b))
std::cerr << "No bounding box in bvh_node constructor.\n";
return box_a.min().e[axis] < box_b.min().e[axis];
}
bool box_x_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) {
return box_compare(a, b, 0);
}
bool box_y_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) {
return box_compare(a, b, 1);
}
bool box_z_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) {
return box_compare(a, b, 2);
}
- 随机整数:
inline int random_int(int min, int max) {
// Returns a random integer in [min,max].
return static_cast<int>(random_double(min, max+1));
}
- 至此,得到了bvh_node类型,因此,将world(hittable_list)生成bvh_node,利用bvh_node的hit得到相交信息,实现加速。
- 经过测试,原耗时是4-5倍。
4. Solid Textures
- 在texture.h中创建texture类,是一个抽象类,定义了纯虚函数texture::value,用于通过纹理坐标(u,v)和相交点坐标p返回此处的color值。所以在此处,texture只是用于给出相交处颜色的,即吸收率attenuation。继承texture的类是作为material类的成员属性,然后再其scatter成员函数中按照hit_record中的信息(uvp)来给出color。
#ifndef TEXTURE_H
#define TEXTURE_H
#include "rtweekend.h"
class texture {
public:
virtual color value(double u, double v, const point3& p) const = 0;
};
- 然后,在texture.h中定义了solid_color类,继承了texture类,对于solid_color类而言,其主要内容是一个color,即所有地方的颜色都是一样的,相应的,value直接返回color成员就可以了。
class solid_color : public texture {
public:
solid_color() {}
solid_color(color c) : color_value(c) {}
solid_color(double red, double green, double blue)
: solid_color(color(red,green,blue)) {}
virtual color value(double u, double v, const vec3& p) const override {
return color_value;
}
private:
color color_value;
};
#endif
- 将纹理坐标uv作为相交信息记录在hit_record中,具体的uv计算实现在hittable物体类中的hit函数中。
struct hit_record {
vec3 p;
vec3 normal;
shared_ptr<material> mat_ptr;
double t;
double u;
double v;
bool front_face;
...
- 对于球体,从空间坐标向纹理坐标映射:
首先,利用\(\phi\),\(\theta\)来直接映射
然后,需要利用空间坐标得到球坐标
其中,atan2
是利用二维坐标来获得角度,从参数二轴向逆时针为正方向,但是在一个周期内变化是从0到pi,然后从-pi到0,不符合0到2pi的需求,所以将当前点取反到对称点位置,则是从-pi到pi,然后加pi得到目标范围。所以:
以上的转换过程是对于球心在原点的球面坐标转换规则。在此处只需要将hit_record中记录的球面法线就可以了。在sphere类中实现get_sphere_hittable函数,作为静态成员函数,实现该过程,通过法线来给出纹理坐标。
class sphere : public hittable {
...
private:
static void get_sphere_uv(const point3& p, double& u, double& v) {
// p: a given point on the sphere of radius one, centered at the origin.
// u: returned value [0,1] of angle around the Y axis from X=-1.
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
auto theta = acos(-p.y());
auto phi = atan2(-p.z(), p.x()) + pi;
u = phi / (2*pi);
v = theta / pi;
}
};
- 然后,在hit中调用sphere::get_sphere_uv,给出hit_record中的uv坐标。
bool sphere::hit(...) {
...
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
get_sphere_uv(outward_normal, rec.u, rec.v);
rec.mat_ptr = mat_ptr;
return true;
}
- 在漫反射材质中加入texture属性,在其scatter中的attenuation通过texture的value给出。
#include "texture.h"
...
class lambertian : public material {
public:
lambertian(const color& a) : albedo(make_shared<solid_color>(a)) {}
lambertian(shared_ptr<texture> a) : albedo(a) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
auto scatter_direction = rec.normal + random_unit_vector();
// Catch degenerate scatter direction
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction, r_in.time());
attenuation = albedo->value(rec.u, rec.v, rec.p);
return true;
}
public:
shared_ptr<texture> albedo;
};
- 在solid_texture基础上可以实现checker_texture。即有选择地按照某种solid_texture着色。
class checker_texture : public texture {
public:
checker_texture() {}
checker_texture(shared_ptr<texture> _even, shared_ptr<texture> _odd)
: even(_even), odd(_odd) {}
checker_texture(color c1, color c2)
: even(make_shared<solid_color>(c1)) , odd(make_shared<solid_color>(c2)) {}
virtual color value(double u, double v, const point3& p) const override {
auto sines = sin(10*p.x())*sin(10*p.y())*sin(10*p.z());
if (sines < 0)
return odd->value(u, v, p);
else
return even->value(u, v, p);
}
public:
shared_ptr<texture> odd;
shared_ptr<texture> even;
};
- 因此当创建物体时,由material->hittable到texture->material(指漫反射)->hittable,当然,由漫反射材质类的定义可知,可以将solid_texture的参数在定义lambertian时实现,那么原先的实现方法也可以被兼容,当然不支持复杂的材质,对于这些应该先定义材质。
hittable_list random_scene() {
hittable_list world;
auto checker = make_shared<checker_texture>(color(0.2, 0.3, 0.1), color(0.9, 0.9, 0.9));
world.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(checker)));
for (int a = -11; a < 11; a++) {
...
- 选择world
hittable_list two_spheres() {
hittable_list objects;
auto checker = make_shared<checker_texture>(color(0.2, 0.3, 0.1), color(0.9, 0.9, 0.9));
objects.add(make_shared<sphere>(point3(0,-10, 0), 10, make_shared<lambertian>(checker)));
objects.add(make_shared<sphere>(point3(0, 10, 0), 10, make_shared<lambertian>(checker)));
return objects;
}
// World
hittable_list world;
point3 lookfrom;
point3 lookat;
auto vfov = 40.0;
auto aperture = 0.0;
switch (0) {
case 1:
world = random_scene();
lookfrom = point3(13,2,3);
lookat = point3(0,0,0);
vfov = 20.0;
aperture = 0.1;
break;
default:
case 2:
world = two_spheres();
lookfrom = point3(13,2,3);
lookat = point3(0,0,0);
vfov = 20.0;
break;
}
// Camera
vec3 vup(0,1,0);
auto dist_to_focus = 10.0;
int image_height = static_cast<int>(image_width / aspect_ratio);
camera cam(lookfrom, lookat, vup, vfov, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0);
...
5. Perlin Noise
- 在perlin.h中定义perlin类,首先定义randfloat属性,提供一系列随机生成的double值;此外定义了三个整数数组,内部的值是randfloat的索引,三个数组都是被打乱的。perlin类在定义时会生成randfloat、perm_x、perm_y、perm_z,即一个perlin类的noise特征。在perlin类中,定义了noise函数,通过点坐标来得到noise,此处将三个维度的值乘以4(提高频率),变成整型值(作为索引),然后与255做按位与(保持索引范围0-255)。然后,将三个维度得到的索引按位异或,作为randfloat的索引,最终得到noise值。
#ifndef PERLIN_H
#define PERLIN_H
#include "rtweekend.h"
class perlin {
public:
perlin() {
ranfloat = new double[point_count];
for (int i = 0; i < point_count; ++i) {
ranfloat[i] = random_double();
}
perm_x = perlin_generate_perm();
perm_y = perlin_generate_perm();
perm_z = perlin_generate_perm();
}
~perlin() {
delete[] ranfloat;
delete[] perm_x;
delete[] perm_y;
delete[] perm_z;
}
double noise(const point3& p) const {
auto i = static_cast<int>(4*p.x()) & 255;
auto j = static_cast<int>(4*p.y()) & 255;
auto k = static_cast<int>(4*p.z()) & 255;
return ranfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]];
}
private:
static const int point_count = 256;
double* ranfloat;
int* perm_x;
int* perm_y;
int* perm_z;
static int* perlin_generate_perm() {
auto p = new int[point_count];
for (int i = 0; i < perlin::point_count; i++)
p[i] = i;
permute(p, point_count);
return p;
}
static void permute(int* p, int n) {
for (int i = n-1; i > 0; i--) {
int target = random_int(0, i);
int tmp = p[i];
p[i] = p[target];
p[target] = tmp;
}
}
};
#endif
- 定义纹理noise_texture,其主要内容是一个perlin类成员,在该纹理类实例化的时候自行生成作为该纹理的固化了的随机噪声,然后其color函数是通过其perlin成员返回的噪声来修改颜色并返回,此处是得到黑白灰color。
class noise_texture : public texture {
public:
noise_texture() {}
virtual color value(double u, double v, const point3& p) const override {
return color(1,1,1) * noise.noise(p);
}
public:
perlin noise;
};
画图
hittable_list two_perlin_spheres() {
hittable_list objects;
auto pertext = make_shared<noise_texture>();
objects.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(pertext)));
objects.add(make_shared<sphere>(point3(0, 2, 0), 2, make_shared<lambertian>(pertext)));
return objects;
}
int main() {
...
switch (0) {
...
case 2:
...
default:
case 3:
world = two_perlin_spheres();
lookfrom = point3(13,2,3);
lookat = point3(0,0,0);
vfov = 20.0;
break;
}
...
效果如下。可见,这是空间中的立方体色块,这是因为将点坐标进行了ADC采样为整形索引值。此外,之前将坐标值乘以4,所以空间坐标以0.25为一个整形索引区段,即立方体色块的边长是0.25。
- 线性插值做平滑:修改noise函数,改变利用点坐标p得到一个randfloat值,而是得到这个坐标周围的八个整形空间坐标对应的八个值,记录在数组中,然后利用余数进行线性插值。
class perlin {
public:
...
double noise(point3 vec3& p) const {
auto u = p.x() - floor(p.x());
auto v = p.y() - floor(p.y());
auto w = p.z() - floor(p.z());
auto i = static_cast<int>(floor(p.x()));
auto j = static_cast<int>(floor(p.y()));
auto k = static_cast<int>(floor(p.z()));
double c[2][2][2];
for (int di=0; di < 2; di++)
for (int dj=0; dj < 2; dj++)
for (int dk=0; dk < 2; dk++)
c[di][dj][dk] = ranfloat[
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
return trilinear_interp(c, u, v, w);
}
...
private:
...
static double trilinear_interp(double c[2][2][2], double u, double v, double w) {
auto accum = 0.0;
for (int i=0; i < 2; i++)
for (int j=0; j < 2; j++)
for (int k=0; k < 2; k++)
accum += (i*u + (1-i)*(1-u))*
(j*v + (1-j)*(1-v))*
(k*w + (1-k)*(1-w))*c[i][j][k];
return accum;
}
}
效果如下,可见消除了明显的采样痕迹。
- 为了进一步的消除剩余的方形踪迹mash band,采用Hermite cubic来平滑线性插值。
u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);
效果如下,不是很明显。
- 在之前在坐标上乘以四,这是改变频率;这个应当定义在纹理中,作为纹理特征。
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const override {
return color(1,1,1) * noise.noise(scale * p);
}
public:
perlin noise;
double scale;
};
- 使用值索引随机浮点数还是存在整数采样问题,此处改变为索引随机矢量,然后对矢量做一次处理。来进一步减少方形。
class perlin {
public:
perlin() {
ranvec = new vec3[point_count];
for (int i = 0; i < point_count; ++i) {
ranvec[i] = unit_vector(vec3::random(-1,1));
}
perm_x = perlin_generate_perm();
perm_y = perlin_generate_perm();
perm_z = perlin_generate_perm();
}
~perlin() {
delete[] ranvec;
delete[] perm_x;
delete[] perm_y;
delete[] perm_z;
}
...
private:
static const int point_count = 256;
vec3* ranvec;
int* perm_x;
int* perm_y;
int* perm_z;
...
}
class perlin {
public:
...
double noise(const point3& p) const {
auto u = p.x() - floor(p.x());
auto v = p.y() - floor(p.y());
auto w = p.z() - floor(p.z());
auto i = static_cast<int>(floor(p.x()));
auto j = static_cast<int>(floor(p.y()));
auto k = static_cast<int>(floor(p.z()));
vec3 c[2][2][2];
for (int di=0; di < 2; di++)
for (int dj=0; dj < 2; dj++)
for (int dk=0; dk < 2; dk++)
c[di][dj][dk] = ranvec[
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
return perlin_interp(c, u, v, w);
}
...
}
class perlin {
...
private:
...
static double perlin_interp(vec3 c[2][2][2], double u, double v, double w) {
auto uu = u*u*(3-2*u);
auto vv = v*v*(3-2*v);
auto ww = w*w*(3-2*w);
auto accum = 0.0;
for (int i=0; i < 2; i++)
for (int j=0; j < 2; j++)
for (int k=0; k < 2; k++) {
vec3 weight_v(u-i, v-j, w-k);
accum += (i*uu + (1-i)*(1-uu))
* (j*vv + (1-j)*(1-vv))
* (k*ww + (1-k)*(1-ww))
* dot(c[i][j][k], weight_v);
}
return accum;
}
...
}
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const override {
return color(1,1,1) * 0.5 * (1.0 + noise.noise(scale * p));
}
public:
perlin noise;
double scale;
};
- Turbulence:对于一个点,不仅索引该点对应的噪声,而且按照一定步长索引与该点相关的其他点的噪声(此处为倍乘),并且按照一定的衰减率做加权和(0.5倍衰减)。
class perlin {
...
public:
...
double turb(const point3& p, int depth=7) const {
auto accum = 0.0;
auto temp_p = p;
auto weight = 1.0;
for (int i = 0; i < depth; i++) {
accum += weight*noise(temp_p);
weight *= 0.5;
temp_p *= 2;
}
return fabs(accum);
}
...
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const override {
return color(1,1,1) * noise.turb(scale * p);
}
public:
perlin noise;
double scale;
};
- 将Turbulence混合正弦函数使用,可以得到有规律的Turbulence,此处按照坐标的z维度分量进行正弦变化,并且将Turbulence的噪声值作为随机相位偏置,如此的到大理石条纹。
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const override {
return color(1,1,1) * 0.5 * (1 + sin(scale*p.z() + 10*noise.turb(p)));
}
public:
perlin noise;
double scale;
};
6. Image Texture Mapping
- 定义image_texture类,其主要内容是与image相关的参数,如unsigned char用于记录图像的信息,width是图像的宽度(pixel数量),height是图像的高度(pixel数量),bytes_per_scanline是每行的字节数(每个unsigned char是一个字节),bytes_per_pixel是每个pixel的字节数(如RGB格式是三个字节),这两个参数是用于从连续分布的data中索引对应行列位置的pixel起始位置,并找到相应的信息(如RGB)。读取图片数据及其参数的函数是利用stb_image实现的。color函数是用于返回image中对应像素的RGB。因为color的参数uv是从左下角为原点的,而image的data是从左上角开始按行保存的,所以行坐标u保持而列坐标v取反(1-v)。uv坐标是[0,1)的,那么相应的乘以width或者height就可以得到对应的image中的像素索引。然后,利用该索引和bytes_per_pixel、bytes_per_scanline,以data地址为标准得到对应像素信息的起始地址unsigned char pixel。相应的pixel、pixel+1、pixel+2就是RGB信息。此处读取出来的RGB信息是[0,255]的,为了得到[0,1],需要做归一化。最终,从texture中映射出了信息。
#include "rtweekend.h"
#include "rtw_stb_image.h"
#include "perlin.h"
#include <iostream>
...
class image_texture : public texture {
public:
const static int bytes_per_pixel = 3;
image_texture()
: data(nullptr), width(0), height(0), bytes_per_scanline(0) {}
image_texture(const char* filename) {
auto components_per_pixel = bytes_per_pixel;
data = stbi_load(
filename, &width, &height, &components_per_pixel, components_per_pixel);
if (!data) {
std::cerr << "ERROR: Could not load texture image file '" << filename << "'.\n";
width = height = 0;
}
bytes_per_scanline = bytes_per_pixel * width;
}
~image_texture() {
delete data;
}
virtual color value(double u, double v, const vec3& p) const override {
// If we have no texture data, then return solid cyan as a debugging aid.
if (data == nullptr)
return color(0,1,1);
// Clamp input texture coordinates to [0,1] x [1,0]
u = clamp(u, 0.0, 1.0);
v = 1.0 - clamp(v, 0.0, 1.0); // Flip V to image coordinates
auto i = static_cast<int>(u * width);
auto j = static_cast<int>(v * height);
// Clamp integer mapping, since actual coordinates should be less than 1.0
if (i >= width) i = width-1;
if (j >= height) j = height-1;
const auto color_scale = 1.0 / 255.0;
auto pixel = data + j*bytes_per_scanline + i*bytes_per_pixel;
return color(color_scale*pixel[0], color_scale*pixel[1], color_scale*pixel[2]);
}
private:
unsigned char *data;
int width, height;
int bytes_per_scanline;
};
hittable_list earth() {
auto earth_texture = make_shared<image_texture>("earthmap.jpg");
auto earth_surface = make_shared<lambertian>(earth_texture);
auto globe = make_shared<sphere>(point3(0,0,0), 2, earth_surface);
return hittable_list(globe);
}
int main() {
...
switch (0) {
...
case 3:
...
default:
case 4:
world = earth();
lookfrom = point3(13,2,3);
lookat = point3(0,0,0);
vfov = 20.0;
break;
}
...
7. Rectangles and Lights
- 自发光材质。对于一个材质,由渲染方程可知,其光线包括自发光、直接反射和间接反射。再没有定义自发光物体时,是以天空作为光源,因此只定义了直接或者间接反射scatter,而没有定义自发光项。因此,首先在material类中定义自发光项emitter纯虚函数,其返回值也是一个color,默认的是(0,0,0)即不发光。
class material {
public:
virtual color emitted(double u, double v, const point3& p) const {
return color(0,0,0);
}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const = 0;
};
- 基于此,定义自发光材质diffuse_light,该类对于纯虚函数scatter是return false,即不考虑反射光。该类的主要内容是一个纹理属性,可以绑定任意纹理,也可以直接给定color绑定solidcolor。对于emitter的重写,是直接通过纹理emit查询。
class diffuse_light : public material {
public:
diffuse_light(shared_ptr<texture> a) : emit(a) {}
diffuse_light(color c) : emit(make_shared<solid_color>(c)) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
return false;
}
virtual color emitted(double u, double v, const point3& p) const override {
return emit->value(u, v, p);
}
public:
shared_ptr<texture> emit;
};
- 改写raycolor函数:反射次数达到最大值则返回黑色(光路太深,吸收完全);加入backgroung(color类),作为背景色,如果不再相交则返回背景色;查询自发光emitter得到自发光项;查询反射,如果该材质不可反射,则直接返回自发光项,否则返回自发光项加反射项,并迭代。
color ray_color(const ray& r, const color& background, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
// If the ray hits nothing, return the background color.
if (!world.hit(r, 0.001, infinity, rec))
return background;
ray scattered;
color attenuation;
color emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
if (!rec.mat_ptr->scatter(r, rec, attenuation, scattered))
return emitted;
return emitted + attenuation * ray_color(scattered, background, world, depth-1);
}
- Rectangle Objects。此处定义了AA Rectangle,以平行于xy平面为例。
与rectange所在的z平面的相交时间:
相交检测:
- 在aarect.h中定义了xy_rect类,其主要内容是xy坐标x0、x1、y0、y1以及z轴位置k,此外还有纹理。考虑到xy_rect是没有厚度的,所以其aabb在xy轴上利用x0、x1、y0、y1界定,而z向则在k上加上微小偏移。
#ifndef AARECT_H
#define AARECT_H
#include "rtweekend.h"
#include "hittable.h"
class xy_rect : public hittable {
public:
xy_rect() {}
xy_rect(double _x0, double _x1, double _y0, double _y1, double _k,
shared_ptr<material> mat)
: x0(_x0), x1(_x1), y0(_y0), y1(_y1), k(_k), mp(mat) {};
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override {
// The bounding box must have non-zero width in each dimension, so pad the Z
// dimension a small amount.
output_box = aabb(point3(x0,y0, k-0.0001), point3(x1, y1, k+0.0001));
return true;
}
public:
shared_ptr<material> mp;
double x0, x1, y0, y1, k;
};
#endif
- 其hit函数利用上述逻辑实现相交检测结果。其对应的纹理坐标uv是利用在xy_rect上的xy坐标做归一化实现,其以z轴正向单位矢量作为外向法线,然后利用set_face_normal得到实际相交面法线与内外关系。
bool xy_rect::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
auto t = (k-r.origin().z()) / r.direction().z();
if (t < t_min || t > t_max)
return false;
auto x = r.origin().x() + t*r.direction().x();
auto y = r.origin().y() + t*r.direction().y();
if (x < x0 || x > x1 || y < y0 || y > y1)
return false;
rec.u = (x-x0)/(x1-x0);
rec.v = (y-y0)/(y1-y0);
rec.t = t;
auto outward_normal = vec3(0, 0, 1);
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mp;
rec.p = r.at(t);
return true;
}
画个图:
hittable_list simple_light() {
hittable_list objects;
auto pertext = make_shared<noise_texture>(4);
objects.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(pertext)));
objects.add(make_shared<sphere>(point3(0,2,0), 2, make_shared<lambertian>(pertext)));
auto difflight = make_shared<diffuse_light>(color(4,4,4));
objects.add(make_shared<xy_rect>(3, 5, 1, 3, -2, difflight));
return objects;
}
#include "rtweekend.h"
#include "camera.h"
#include "color.h"
#include "hittable_list.h"
#include "material.h"
#include "moving_sphere.h"
#include "sphere.h"
#include "aarect.h"
#include <iostream>
...
int main() {
...
switch (0) {
...
default:
case 5:
world = simple_light();
samples_per_pixel = 400;
background = color(0,0,0);
lookfrom = point3(26,3,6);
lookat = point3(0,2,0);
vfov = 20.0;
break;
}
...
- 画一个Cornell box
hittable_list cornell_box() {
hittable_list objects;
auto red = make_shared<lambertian>(color(.65, .05, .05));
auto white = make_shared<lambertian>(color(.73, .73, .73));
auto green = make_shared<lambertian>(color(.12, .45, .15));
auto light = make_shared<diffuse_light>(color(15, 15, 15));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red));
objects.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 555, white));
objects.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white));
return objects;
}
default:
case 6:
world = cornell_box();
aspect_ratio = 1.0;
image_width = 600;
samples_per_pixel = 200;
background = color(0,0,0);
lookfrom = point3(278, 278, -800);
lookat = point3(278, 278, 0);
vfov = 40.0;
break;
噪声很严重,这是因为光源很小,不容易命中。
8. Instances
- 在box.h中创建box类,其主要内容是最小与最大两个顶点,用于提供aabb;此外还有hittable_list成员,用于实现hit函数;在构造函数中,利用最大最小两个顶点值创建八个aarect,并且共享材质。
#ifndef BOX_H
#define BOX_H
#include "rtweekend.h"
#include "aarect.h"
#include "hittable_list.h"
class box : public hittable {
public:
box() {}
box(const point3& p0, const point3& p1, shared_ptr<material> ptr);
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override {
output_box = aabb(box_min, box_max);
return true;
}
public:
point3 box_min;
point3 box_max;
hittable_list sides;
};
box::box(const point3& p0, const point3& p1, shared_ptr<material> ptr) {
box_min = p0;
box_max = p1;
sides.add(make_shared<xy_rect>(p0.x(), p1.x(), p0.y(), p1.y(), p1.z(), ptr));
sides.add(make_shared<xy_rect>(p0.x(), p1.x(), p0.y(), p1.y(), p0.z(), ptr));
sides.add(make_shared<xz_rect>(p0.x(), p1.x(), p0.z(), p1.z(), p1.y(), ptr));
sides.add(make_shared<xz_rect>(p0.x(), p1.x(), p0.z(), p1.z(), p0.y(), ptr));
sides.add(make_shared<yz_rect>(p0.y(), p1.y(), p0.z(), p1.z(), p1.x(), ptr));
sides.add(make_shared<yz_rect>(p0.y(), p1.y(), p0.z(), p1.z(), p0.x(), ptr));
}
bool box::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
return sides.hit(r, t_min, t_max, rec);
}
#endif
画个图:
- Instance Translation。对于物体平移,因为原本的物体是AArect,非常容易计算相交点,但是对其进行旋转就会使得计算变得困难,因此比起创造变动的物体,通过改动光线实现更简单。translate类继承hittable,其主要内容是一个平移offset(point3),与一个绑定的平移物体。对于boundingbox函数,其通过查询绑定的hittable的boundingbox然后做平移实现。其hit函数通过将入射光线的原点反向平移,然后利用被绑定hittable物体的hit函数实现。最终得到的相交点需要做平移得到hittable物体平移后的对应位置。对于内外法线的判断,原本的书中内容是利用旋转后的光线与计算得到的旋转后的法线作比较,这里修改为使用原本的光线与旋转后的法线作比较。
class translate : public hittable {
public:
translate(shared_ptr<hittable> p, const vec3& displacement)
: ptr(p), offset(displacement) {}
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override;
public:
shared_ptr<hittable> ptr;
vec3 offset;
};
bool translate::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
ray moved_r(r.origin() - offset, r.direction(), r.time());
if (!ptr->hit(moved_r, t_min, t_max, rec))
return false;
rec.p += offset;
rec.set_face_normal(r, rec.normal);
return true;
}
bool translate::bounding_box(double time0, double time1, aabb& output_box) const {
if (!ptr->bounding_box(time0, time1, output_box))
return false;
output_box = aabb(
output_box.min() + offset,
output_box.max() + offset);
return true;
}
- 旋转:
- 在hittable.h中实现了绕y轴旋转的rotate_y类。其主要内容除了被绑定的hittable物体,还有旋转的正弦与余弦值,是否有aabb(hasbox)以及aabb(bbox)。这些属性是在构造时实现,相应的boundingbox函数是直接查询bbox以及hasbox。
class rotate_y : public hittable {
public:
rotate_y(shared_ptr<hittable> p, double angle);
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override {
output_box = bbox;
return hasbox;
}
public:
shared_ptr<hittable> ptr;
double sin_theta;
double cos_theta;
bool hasbox;
aabb bbox;
};
- 构造函数。对于aabb属性,首先通过被绑定hittable物体的boundingbox获得其aabb,然后利用aabb的对应最大以及最小坐标得到八个顶点,对着八个顶点进行旋转,然后得到从八个顶点的坐标的三个维度中获得最大最小顶点坐标的三个维度的值。
rotate_y::rotate_y(shared_ptr<hittable> p, double angle) : ptr(p) {
auto radians = degrees_to_radians(angle);
sin_theta = sin(radians);
cos_theta = cos(radians);
hasbox = ptr->bounding_box(0, 1, bbox);
point3 min( infinity, infinity, infinity);
point3 max(-infinity, -infinity, -infinity);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
auto x = i*bbox.max().x() + (1-i)*bbox.min().x();
auto y = j*bbox.max().y() + (1-j)*bbox.min().y();
auto z = k*bbox.max().z() + (1-k)*bbox.min().z();
auto newx = cos_theta*x + sin_theta*z;
auto newz = -sin_theta*x + cos_theta*z;
vec3 tester(newx, y, newz);
for (int c = 0; c < 3; c++) {
min[c] = fmin(min[c], tester[c]);
max[c] = fmax(max[c], tester[c]);
}
}
}
}
bbox = aabb(min, max);
}
- 其hit函数通过将ray的原点与方向都做反向旋转,然后与被绑定物体做hit,然后将hit得到的hit_record的相交点p与法线normal都做旋转得到物体旋转后的对应结果。对于内外法线的判断,原本的书中内容是利用旋转后的光线与计算得到的旋转后的法线作比较,这里修改为使用原本的光线与旋转后的法线作比较。
bool rotate_y::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
auto origin = r.origin();
auto direction = r.direction();
origin[0] = cos_theta*r.origin()[0] - sin_theta*r.origin()[2];
origin[2] = sin_theta*r.origin()[0] + cos_theta*r.origin()[2];
direction[0] = cos_theta*r.direction()[0] - sin_theta*r.direction()[2];
direction[2] = sin_theta*r.direction()[0] + cos_theta*r.direction()[2];
ray rotated_r(origin, direction, r.time());
if (!ptr->hit(rotated_r, t_min, t_max, rec))
return false;
auto p = rec.p;
auto normal = rec.normal;
p[0] = cos_theta*rec.p[0] + sin_theta*rec.p[2];
p[2] = -sin_theta*rec.p[0] + cos_theta*rec.p[2];
normal[0] = cos_theta*rec.normal[0] + sin_theta*rec.normal[2];
normal[2] = -sin_theta*rec.normal[0] + cos_theta*rec.normal[2];
rec.p = p;
rec.set_face_normal(r, normal);
return true;
}
9. Volumes 参与介质
- 参与介质。参与介质的特殊之处在于会发生内部光线相交而不是表面相交,此外其反射方向是各个方向的而非漫反射材质的半球面。对于是否发生相交的判断,不仅使用参与介质的外表面判断是否会进入介质内部,而且使用穿过介质的路径长度来判断相交的可能性;此外,相交的可能性还与介质密度有关。因为参与介质的相交检测以及反射方式都有特殊之处,所以需要建立新的hittable类以及material类,至于texture暂时不需要。在constant_medium.h中创建constant_medium类,其主要内容是boundary以及neg_inv_density,前者是一个hittable的sharedptr,用于界定介质的边界,即介质的boundingbox直接通过boundary的boundingbox给出,且hit中的边界相交通过boundary的hit实现;后者是一个密度相关的系数,即-1/密度,用于hit函数中的内部相交检测;基本上,包围面与密度可确定一个参与介质的物理属性。此外,其理所应当的还有一个材质属性,用于后续的反射光线确定。
#ifndef CONSTANT_MEDIUM_H
#define CONSTANT_MEDIUM_H
#include "rtweekend.h"
#include "hittable.h"
#include "material.h"
#include "texture.h"
class constant_medium : public hittable {
public:
constant_medium(shared_ptr<hittable> b, double d, shared_ptr<texture> a)
: boundary(b),
neg_inv_density(-1/d),
phase_function(make_shared<isotropic>(a))
{}
constant_medium(shared_ptr<hittable> b, double d, color c)
: boundary(b),
neg_inv_density(-1/d),
phase_function(make_shared<isotropic>(c))
{}
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
virtual bool bounding_box(double time0, double time1, aabb& output_box) const override {
return boundary->bounding_box(time0, time1, output_box);
}
public:
shared_ptr<hittable> boundary;
shared_ptr<material> phase_function;
double neg_inv_density;
};
#endif
- 在material.h中创建了isotropic类,即各向同性,其主要内容是将scatter函数重写为反射光线的方向是单位球体内部的随机方向。
class isotropic : public material {
public:
isotropic(color c) : albedo(make_shared<solid_color>(c)) {}
isotropic(shared_ptr<texture> a) : albedo(a) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
scattered = ray(rec.p, random_in_unit_sphere(), r_in.time());
attenuation = albedo->value(rec.u, rec.v, rec.p);
return true;
}
public:
shared_ptr<texture> albedo;
};
- constant_medium的hit函数。首先将光线与其boundary进行两次相交检测,如果相交则会得到两个hit_record,记录了两个相交点的t,然后根据tmin和tmax更新这两个t,最后考虑到光线可能是从介质内部发出的,将近处的相交t与0作比较。这样一来,得到了光线在介质内部的传播起始点,利用光线的方向矢量就可以得到传播路径长度。然后,利用密度参数与log随机数生成一个随机的相交路径长度,比较这两个长度来判断是否相交。若相交,则按照随机相交路径长度与光线方向矢量的长度相应的得到了相交时刻t。
bool constant_medium::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
// Print occasional samples when debugging. To enable, set enableDebug true.
const bool enableDebug = false;
const bool debugging = enableDebug && random_double() < 0.00001;
hit_record rec1, rec2;
if (!boundary->hit(r, -infinity, infinity, rec1))
return false;
if (!boundary->hit(r, rec1.t+0.0001, infinity, rec2))
return false;
if (debugging) std::cerr << "\nt_min=" << rec1.t << ", t_max=" << rec2.t << '\n';
if (rec1.t < t_min) rec1.t = t_min;
if (rec2.t > t_max) rec2.t = t_max;
if (rec1.t >= rec2.t)
return false;
if (rec1.t < 0)
rec1.t = 0;
const auto ray_length = r.direction().length();
const auto distance_inside_boundary = (rec2.t - rec1.t) * ray_length;
const auto hit_distance = neg_inv_density * log(random_double());
if (hit_distance > distance_inside_boundary)
return false;
rec.t = rec1.t + hit_distance / ray_length;
rec.p = r.at(rec.t);
if (debugging) {
std::cerr << "hit_distance = " << hit_distance << '\n'
<< "rec.t = " << rec.t << '\n'
<< "rec.p = " << rec.p << '\n';
}
rec.normal = vec3(1,0,0); // arbitrary
rec.front_face = true; // also arbitrary
rec.mat_ptr = phase_function;
return true;
}
画个图:
#include "constant_medium.h"
...
hittable_list cornell_smoke() {
hittable_list objects;
auto red = make_shared<lambertian>(color(.65, .05, .05));
auto white = make_shared<lambertian>(color(.73, .73, .73));
auto green = make_shared<lambertian>(color(.12, .45, .15));
auto light = make_shared<diffuse_light>(color(7, 7, 7));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red));
objects.add(make_shared<xz_rect>(113, 443, 127, 432, 554, light));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 555, white));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white));
objects.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white));
shared_ptr<hittable> box1 = make_shared<box>(point3(0,0,0), point3(165,330,165), white);
box1 = make_shared<rotate_y>(box1, 15);
box1 = make_shared<translate>(box1, vec3(265,0,295));
shared_ptr<hittable> box2 = make_shared<box>(point3(0,0,0), point3(165,165,165), white);
box2 = make_shared<rotate_y>(box2, -18);
box2 = make_shared<translate>(box2, vec3(130,0,65));
objects.add(make_shared<constant_medium>(box1, 0.01, color(0,0,0)));
objects.add(make_shared<constant_medium>(box2, 0.01, color(1,1,1)));
return objects;
}
...
int main() {
...
switch (0) {
...
default:
case 7:
world = cornell_smoke();
aspect_ratio = 1.0;
image_width = 600;
samples_per_pixel = 200;
lookfrom = point3(278, 278, -800);
lookat = point3(278, 278, 0);
vfov = 40.0;
break;
...
}
10. A Scene Testing All New Features
- 画出目前为止的效果。一个特殊的地方在于,将参与介质包在一个玻璃球中会有有趣的效果。
...
#include "bvh.h"
...
hittable_list final_scene() {
hittable_list boxes1;
auto ground = make_shared<lambertian>(color(0.48, 0.83, 0.53));
const int boxes_per_side = 20;
for (int i = 0; i < boxes_per_side; i++) {
for (int j = 0; j < boxes_per_side; j++) {
auto w = 100.0;
auto x0 = -1000.0 + i*w;
auto z0 = -1000.0 + j*w;
auto y0 = 0.0;
auto x1 = x0 + w;
auto y1 = random_double(1,101);
auto z1 = z0 + w;
boxes1.add(make_shared<box>(point3(x0,y0,z0), point3(x1,y1,z1), ground));
}
}
hittable_list objects;
objects.add(make_shared<bvh_node>(boxes1, 0, 1));
auto light = make_shared<diffuse_light>(color(7, 7, 7));
objects.add(make_shared<xz_rect>(123, 423, 147, 412, 554, light));
auto center1 = point3(400, 400, 200);
auto center2 = center1 + vec3(30,0,0);
auto moving_sphere_material = make_shared<lambertian>(color(0.7, 0.3, 0.1));
objects.add(make_shared<moving_sphere>(center1, center2, 0, 1, 50, moving_sphere_material));
objects.add(make_shared<sphere>(point3(260, 150, 45), 50, make_shared<dielectric>(1.5)));
objects.add(make_shared<sphere>(
point3(0, 150, 145), 50, make_shared<metal>(color(0.8, 0.8, 0.9), 1.0)
));
auto boundary = make_shared<sphere>(point3(360,150,145), 70, make_shared<dielectric>(1.5));
objects.add(boundary);
objects.add(make_shared<constant_medium>(boundary, 0.2, color(0.2, 0.4, 0.9)));
boundary = make_shared<sphere>(point3(0, 0, 0), 5000, make_shared<dielectric>(1.5));
objects.add(make_shared<constant_medium>(boundary, .0001, color(1,1,1)));
auto emat = make_shared<lambertian>(make_shared<image_texture>("earthmap.jpg"));
objects.add(make_shared<sphere>(point3(400,200,400), 100, emat));
auto pertext = make_shared<noise_texture>(0.1);
objects.add(make_shared<sphere>(point3(220,280,300), 80, make_shared<lambertian>(pertext)));
hittable_list boxes2;
auto white = make_shared<lambertian>(color(.73, .73, .73));
int ns = 1000;
for (int j = 0; j < ns; j++) {
boxes2.add(make_shared<sphere>(point3::random(0,165), 10, white));
}
objects.add(make_shared<translate>(
make_shared<rotate_y>(
make_shared<bvh_node>(boxes2, 0.0, 1.0), 15),
vec3(-100,270,395)
)
);
return objects;
}
int main() {
...
switch (0) {
...
default:
case 8:
world = final_scene();
aspect_ratio = 1.0;
image_width = 800;
samples_per_pixel = 10000;
background = color(0,0,0);
lookfrom = point3(478, 278, -600);
lookat = point3(278, 278, 0);
vfov = 40.0;
break;
...
}
来源
本文来自博客园,作者:ETHERovo,转载请注明原文链接:https://www.cnblogs.com/etherovo/p/17343266.html