本文主要参照 Ray Tracing: The Next Week,其中只是主要精炼光追相关理论,具体实现可参照原文。
一、自发光
1.1 自发光材质类
首先,定义自发光材质,该材质不提供散射,仅提供一个发光函数emitted()。
class diffuse_light : public material {
public:
texture *emit;
diffuse_light(texture *a) : emit(a) {}
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const {
return false;
}
virtual vec3 emitted(float u, float v, const vec3& p) const {
return emit->value(u, v, p);
}
};
material基类也加上emitted()函数:
class material {
public:
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const = 0;
virtual vec3 emitted(float u, float v, const vec3& p) const {
return vec3(0,0,0);
}
};
1.2 颜色采样修改
新版color()函数,跟旧版color函数的区别如下:
- 取消了“天空盒”,当射线没有命中任何物体的时候,直接返回纯黑色(r,g,b均为0)。这也说明,之前的“天空盒”的本质,其实是一个铺满场景的巨型发光材质,将其取消了之后,四周一片漆黑。
- 如果递归深度超过50,或者命中点没有发生散射(例如射线命中发光材质了),则color返回发光材质emitted()函数的返回值(即发光材质表面某个被射线所命中的点,所对应的发射光的颜色),然后乘以attenuation,叠加到上一层递归的的emitted中(如果有emitted的话,即物体自发光的话),作为上一层color()的返回值。
vec3 color(const ray& r, hittable *world, int depth) {
hit_record rec;
if (world->hit(r, 0.001, MAXFLOAT, rec)) {
ray scattered;
vec3 attenuation;
vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
if (depth < 50 && rec.mat_ptr->scatter(r, rec, attenuation, scattered))
return emitted + attenuation*color(scattered, world, depth+1);
else
return emitted;
}
else
return vec3(0,0,0);
}
即,光源照到物体表面材质的颜色 = 材质表面颜色(即各通道反射率)* 光源颜色
二、轴对齐矩形
如上图所示,假设在xy平面有个一矩形,我们只需要一个z参数就能够确定该矩形的所在的平面,例如z=k,其边由以下4个变量确定:
x=x0,x = x1,y = y0,y=y1;
判断一条射线是否命中一个xy平面上的轴对齐矩形,我们可以先计算该射线和该矩形所在的平面的交点。
射线方程为:
p(t) = a + t * b
单独将z轴的分量抽离出来,则有:
z(t)=az+t * bz
将z(t) = k 代入上式,有:
t=(k−az)/bz
有了t的值,可以计算出交点的x和y值:
x = ax + t * bx
y = ay + t * by
则有射线命中轴对齐矩形的判断条件为:x0 < x < x1 且 y0 < y < y1 ,将xy平面的轴对齐矩形,写成代码如下:
class xy_rect: public hittable {
public:
xy_rect() {}
xy_rect(float _x0, float _x1, float _y0, float _y1, float _k, material *mat) : x0(_x0), x1(_x1), y0(_y0), y1(_y1), k(_k), mp(mat) {};
virtual bool hit(const ray& r, float t0, float t1, hit_record& rec) const;
virtual bool bounding_box(float t0, float t1, aabb& box) const {
box = aabb(vec3(x0,y0, k-0.0001), vec3(x1, y1, k+0.0001));
return true; }
material *mp;
float x0, x1, y0, y1, k;
};
bool xy_rect::hit(const ray& r, float t0, float t1, hit_record& rec) const {
float t = (k-r.origin().z()) / r.direction().z();
if (t < t0 || t > t1) return false;
float x = r.origin().x() + t*r.direction().x();
float 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;
rec.mat_ptr = mp;
rec.p = r.point_at_parameter(t);
rec.normal = vec3(0, 0, 1);
return true;
}
xz平面和yz平面的定义原理一样,详见代码。
在场景中使用diffuse_ligh材质定义一个发光的球和一个发光的轴对齐矩形:
hittable *random_scene() {
texture *pertext = new noise_texture();
hittable **list = new hittable*[4];
int i = 0;
list[i++] = new sphere(vec3(0, -1000, 0), 1000, new lambertian(new constant_texture(vec3(0.7, 0.5, 0.3))));
int nn;
unsigned char *Cristiano = stbi_load("F://Cristiano.jpg", &nx, &ny, &nn, 0);
material *CristianoJpg = new lambertian(new image_texture(Cristiano, nx, ny));
list[i++] = new sphere(vec3(0, 2, 0), 2, CristianoJpg);
list[i++] = new sphere(vec3(0, 6, 0), 1, new diffuse_light(new constant_texture(vec3(4, 4, 4))));
list[i++] = new xy_rect(3, 5, 1, 3, -2, new diffuse_light(new constant_texture(vec3(4, 4, 4))));
return new bvh_node(list, i, 0.0, 1.0);
}
效果如下(每个像素采样2000次):
三、Cornell Box
xz平面和yz平面的定义原理一样,详见代码:
class xz_rect : public hittable {
public:
xz_rect() {}
xz_rect(float _x0, float _x1, float _z0, float _z1, float _k, material *mat) :x0(_x0), x1(_x1), z0(_z0), z1(_z1), k(_k), mp(mat) {};
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
virtual bool bounding_box(float time0, float time1, aabb& output_box) const override {
// The bounding box must have non-zero width in each dimension, so pad the Y
// dimension a small amount.
output_box = aabb(vec3(x0, k - 0.0001, z0), vec3(x1, k + 0.0001, z1));
return true;
}
material *mp;
double x0, x1, z0, z1, k;
};
bool xz_rect::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
auto t = (k - r.origin().y()) / r.direction().y();
if (t < t_min || t > t_max)
return false;
auto x = r.origin().x() + t * r.direction().x();
auto z = r.origin().z() + t * r.direction().z();
if (x < x0 || x > x1 || z < z0 || z > z1)
return false;
rec.u = (x - x0) / (x1 - x0);
rec.v = (z - z0) / (z1 - z0);
rec.t = t;
rec.normal = vec3(0, 1, 0);
rec.mat_ptr = mp;
rec.p = r.point_at_parameter(t);
return true;
}
class yz_rect : public hittable {
public:
yz_rect(float _y0, float _y1, float _z0, float _z1, float _k,material *mat): y0(_y0), y1(_y1), z0(_z0), z1(_z1), k(_k), mp(mat) {};
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
virtual bool bounding_box(float time0, float time1, aabb& output_box) const override {
// The bounding box must have non-zero width in each dimension, so pad the X
// dimension a small amount.
output_box = aabb(vec3(k - 0.0001, y0, z0), vec3(k + 0.0001, y1, z1));
return true;
}
material *mp;
double y0, y1, z0, z1, k;
};
bool yz_rect::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
auto t = (k - r.origin().x()) / r.direction().x();
if (t < t_min || t > t_max)
return false;
auto y = r.origin().y() + t * r.direction().y();
auto z = r.origin().z() + t * r.direction().z();
if (y < y0 || y > y1 || z < z0 || z > z1)
return false;
rec.u = (y - y0) / (y1 - y0);
rec.v = (z - z0) / (z1 - z0);
rec.t = t;
rec.normal = vec3(1, 0, 0);
rec.mat_ptr = mp;
rec.p = r.point_at_parameter(t);
return true;
}
因为法线是写死在轴对齐矩形里面的,所以接下来定义一个新的hittable,作用是将另外一个hittable的法线置反。
class flip_normals : public hittable {
public:
flip_normals(hittable *p) : ptr(p) {}
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
if (ptr->hit(r, t_min, t_max, rec)) {
rec.normal = -rec.normal;
return true;
}
else
return false;
}
virtual bool bounding_box(float t0, float t1, aabb& box) const {
return ptr->bounding_box(t0, t1, box);
}
hittable *ptr;
};
在场景中增加5个普通矩形,1个发光矩形,一个纹理圆形(其中使用flip_normals,将三个对面的法线取反):
hittable *random_scene() {
//cam.SetCamParams(vec3(278, 278, -800), vec3(278, 278, 0), vec3(0, 1, 0), 40, float(nx) / float(ny), 0.0, 10.0, 0.0, 1.0);
hittable **list = new hittable*[7];
int i = 0;
material *red = new lambertian(new constant_texture(vec3(0.65, 0.05, 0.05)));
material *white = new lambertian(new constant_texture(vec3(0.73, 0.73, 0.73)));
material *green = new lambertian(new constant_texture(vec3(0.12, 0.45, 0.15)));
material *light = new diffuse_light(new constant_texture(vec3(15, 15, 15)));
list[i++] = new flip_normals(new yz_rect(0, 555, 0, 555, 555, green));
list[i++] = new yz_rect(0, 555, 0, 555, 0, red);
list[i++] = new xz_rect(213, 343, 227, 332, 554, light);
list[i++] = new flip_normals(new xz_rect(0, 555, 0, 555, 555, white));
list[i++] = new xz_rect(0, 555, 0, 555, 0, white);
list[i++] = new flip_normals(new xy_rect(0, 555, 0, 555, 555, white));
int nn;
unsigned char *Cristiano = stbi_load("F://Cristiano.jpg", &nx, &ny, &nn, 0);
material *CristianoJpg = new lambertian(new image_texture(Cristiano, nx, ny));
//list[i++] = new sphere(vec3(0, 2, 0), 2, new lambertian(pertext));
list[i++] = new sphere(vec3(200, 130, 330), 130, CristianoJpg);
return new bvh_node(list, i, 0.0, 1.0);
}
每像素采样1000次效果如下: