Ray Tracing in One Week系列笔记
Chapter 1~2 : Output an image and Vec3
输出到PPM图片文件
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 #include <iostream> #include <fstream> #include "vec3.h" int main () { int nx = 200 ; int ny = 100 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { vec3 col (float (i) / float (nx), float (j) / float (ny), 0.2 ) ; int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
输出结果:
Chapter 3 :Rays, a simple camera, and background
射线的构建
所有的Ray Trace都以Ray类为基础,光线追踪器的核心是发送光线穿过像素,并计算在这些光线的方向上看到什么颜色。
它的形式是计算光线从眼睛到一个像素,计算该光线的相交,并计算出该交点的颜色。
一条射线的表示方式:
p ( t ) = A + t ∗ B p(t) = A + t * B
p ( t ) = A + t ∗ B
因此ray类如下:
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 #ifndef RAYTRACINGTOY_RAY_H #define RAYTRACINGTOY_RAY_H #include "vec3.h" class ray {public : ray () {} ray (const vec3 &a, const vec3 &b) { A = a; B = b; } vec3 origin () const { return A; } vec3 direction () const { return B; } vec3 point_at_parameter (float t) const { return A + B * t; } vec3 A; vec3 B; }; #endif
构建一个视角,摄像机或眼睛放在原点
线性插值lerp:
b l e n d e d _ v a l u e = ( 1 − t ) ∗ s t a r t _ v a l u e + t ∗ e n d _ v a l u e blended\_value = (1-t)*start\_value + t*end\_value
b l e n d e d _ v a l u e = ( 1 − t ) ∗ s t a r t _ v a l u e + t ∗ e n d _ v a l u e
利用ray去计算指向点的颜色,修改过的main.cpp如下:
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 #include <iostream> #include <fstream> #include "vec3.h" #include "ray.h" vec3 color (const ray &r) { vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0f - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } int main () { int nx = 200 ; int ny = 100 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; vec3 lower_left_corner (-2.0 , -1.0 , -1.0 ) ; vec3 horizontal (4.0 , 0.0 , 0.0 ) ; vec3 vertical (0.0 , 2.0 , 0.0 ) ; vec3 origin (0.0 , 0.0 , 0.0 ) ; for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { float u = float (i) / float (nx); float v = float (j) / float (ny); ray r (origin, lower_left_corner + u * horizontal + v * vertical) ; vec3 col = color (r); int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
输出的结果:
Chapter 4 :Adding a sphere
球心在原点,且半径为R R R 的球体可以表示为:
x ∗ x + y ∗ y + z ∗ z = R ∗ R x*x + y * y + z*z = R * R
x ∗ x + y ∗ y + z ∗ z = R ∗ R
球心在( c x , c y , c z ) (cx,cy,cz) ( c x , c y , c z ) ,半径为R R R 的球体可以表示为:
( x − c x ) ∗ ( x − c x ) + ( y − c y ) ∗ ( y − c y ) + ( z − c z ) ∗ ( z − c z ) = R ∗ R (x - cx)*(x - cx) + (y - cy) * (y - cy) + (z - cz)*(z - cz) = R*R
( x − c x ) ∗ ( x − c x ) + ( y − c y ) ∗ ( y − c y ) + ( z − c z ) ∗ ( z − c z ) = R ∗ R
当球心表示为
C = ( c x , c y , c z ) C = (cx,cy,cz)
C = ( c x , c y , c z )
球体上任一点表示为
p = ( x , y , z ) p = (x,y,z)
p = ( x , y , z )
而C C C 指向p p p 是( p − C ) (p-C) ( p − C ) ,因此
d o t ( ( p − C ) , ( p − C ) ) = ( x − c x ) ∗ ( x − c x ) + ( y − c y ) ∗ ( y − c y ) + ( z − c z ) ∗ ( z − c z ) dot((p-C),(p-C)) = (x - cx)*(x - cx) + (y - cy) * (y - cy) + (z - cz)*(z - cz)
d o t ( ( p − C ) , ( p − C ) ) = ( x − c x ) ∗ ( x − c x ) + ( y − c y ) ∗ ( y − c y ) + ( z − c z ) ∗ ( z − c z )
即球体方程可以表示为:
d o t ( ( p − C ) , ( p − C ) ) = R ∗ R dot((p-C),(p-C)) = R*R
d o t ( ( p − C ) , ( p − C ) ) = R ∗ R
等价于:
d o t ( ( A + t ∗ B − C ) , ( A + t ∗ B − C ) ) = R ∗ R dot((A + t*B - C),(A + t*B - C)) = R*R
d o t ( ( A + t ∗ B − C ) , ( A + t ∗ B − C ) ) = R ∗ R
展开可以得到:
t ∗ t ∗ d o t ( B , B ) + 2 ∗ t ∗ d o t ( A − C , B ) + d o t ( A − C , A − C ) − R ∗ R = 0 t*t*dot(B,B) + 2*t*dot(A-C,B) + dot(A-C,A-C) - R*R = 0
t ∗ t ∗ d o t ( B , B ) + 2 ∗ t ∗ d o t ( A − C , B ) + d o t ( A − C , A − C ) − R ∗ R = 0
A , B , C , R A,B,C,R A , B , C , R 均为已知量,因此得到关于t t t 的一元二次方程,关于t t t 的解的数量,有如下关系:
对是否相交进行判断,并修改color函数:
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 bool hit_sphere (const vec3 ¢er, float radius, const ray &r) { vec3 oc = r.origin () - center; float a = dot (r.direction (), r.direction ()); float b = 2.0 * dot (oc, r.direction ()); float c = dot (oc, oc) - radius * radius; float discriminant = b * b - 4 * a * c; return (discriminant > 0 ); } vec3 color (const ray &r) { if (hit_sphere (vec3 (0 , 0 , -1 ), 0.5 , r)) { return vec3 (1 , 0 , 0 ); } vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0f - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); }
最终输出结果如下:
修改后的m a i n . c p p main.cpp m a i n . c p p 如下:
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 <iostream> #include <fstream> #include "vec3.h" #include "ray.h" bool hit_sphere (const vec3 ¢er, float radius, const ray &r) { vec3 oc = r.origin () - center; float a = dot (r.direction (), r.direction ()); float b = 2.0 * dot (oc, r.direction ()); float c = dot (oc, oc) - radius * radius; float discriminant = b * b - 4 * a * c; return (discriminant > 0 ); } vec3 color (const ray &r) { if (hit_sphere (vec3 (0 , 0 , -1 ), 0.5 , r)) { return vec3 (1 , 0 , 0 ); } vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0f - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } int main () { int nx = 200 ; int ny = 100 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; vec3 lower_left_corner (-2.0 , -1.0 , -1.0 ) ; vec3 horizontal (4.0 , 0.0 , 0.0 ) ; vec3 vertical (0.0 , 2.0 , 0.0 ) ; vec3 origin (0.0 , 0.0 , 0.0 ) ; for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { float u = float (i) / float (nx); float v = float (j) / float (ny); ray r (origin, lower_left_corner + u * horizontal + v * vertical) ; vec3 col = color (r); int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
Chapter 5 : Surface normals and multiple objects
球体的表面的法线向量:由球心指向接触点
我们先求得交点处的法线单位向量N N N ,再将其映射到[ 0 , 1 ] [0,1] [ 0 , 1 ]
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 float hit_sphere (const vec3 ¢er, float radius, const ray &r) { vec3 oc = r.origin () - center; float a = dot (r.direction (), r.direction ()); float b = 2.0 * dot (oc, r.direction ()); float c = dot (oc, oc) - radius * radius; float discriminant = b * b - 4 * a * c; if (discriminant < 0 ) { return -1.0 ; } else { return (-b - sqrt (discriminant) / (2.0 * a)); } } vec3 color (const ray &r) { float t = hit_sphere (vec3 (0 , 0 , -1 ), 0.5 , r); if (t > 0.0 ) { vec3 N = unit_vector (r.point_at_parameter (t) - vec3 (0 , 0 , -1 )); return 0.5 * vec3 (N.x () + 1 , N.y () + 1 , N.z () + 1 ); } vec3 unit_direction = unit_vector (r.direction ()); t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0f - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); }
输出结果如下:
现在考虑多个可被击中的物体。
构建一个Hitable的抽象类,包含抽象方法hit判断是否击中物体,以及记录hit到的数据,包括hit的位置,hit点的法向,以及距离参数t。
设定t t t 的区间t m i n < t < t m a x tmin < t < tmax t m i n < t < t m a x ,可以控制在此区域内的hit才计数。在计算hit时,只需要最接近的事物的法线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef RAYTRACINGTOY_HITABLE_H #define RAYTRACINGTOY_HITABLE_H #include "ray.h" struct hit_record { float t; vec3 p; vec3 normal; }; class hitable {public : virtual bool hit (const ray &r, float t_min, float t_max, hit_record &rec) const = 0 ; }; #endif
对于sphere类基础hitable抽象类,实现自己的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 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 #ifndef RAYTRACINGTOY_SPHERE_H #define RAYTRACINGTOY_SPHERE_H #include "hitable.h" class sphere : public hitable {public : sphere () {} sphere (vec3 cen, float r) : center (cen), radius (r) {}; virtual bool hit (const ray &r, float tmin, float tmax, hit_record &rec) const ; vec3 center; float radius; }; bool sphere::hit (const ray &r, float tmin, float tmax, hit_record &rec) const { vec3 oc = r.origin () - center; float a = dot (r.direction (), r.direction ()); float b = dot (oc, r.direction ()); float c = dot (oc, oc) - radius * radius; float discriminant = b * b - a * c; if (discriminant > 0 ) { float temp = (-b - sqrt (b * b - a * c)) / a; if (temp < tmax && temp > tmin) { rec.t = temp; rec.p = r.point_at_parameter (rec.t); rec.normal = (rec.p - center) / radius; return true ; } temp = (-b + sqrt (b * b - a * c)) / a; if (temp < tmax && temp > tmin) { rec.t = temp; rec.p = r.point_at_parameter (rec.t); rec.normal = (rec.p - center) / radius; return true ; } } return false ; } #endif
还需要一个hitable list去记录击中所有的物体,也是继承hitable类,实现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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #ifndef RAYTRACINGTOY_HITABLELIST_H #define RAYTRACINGTOY_HITABLELIST_H #include "hitable.h" class hitable_list : public hitable {public : hitable_list () {} hitable_list (hitable **l, int n) { list = l; list_size = n; } virtual bool hit (const ray &r, float tmin, float tmax, hit_record &rec) const ; hitable **list; int list_size; }; bool hitable_list::hit (const ray &r, float tmin, float tmax, hit_record &rec) const { hit_record temp_rec; bool hit_anything = false ; double closet_so_far = tmax; for (int i = 0 ; i < list_size; i++) { if (list[i]->hit (r, tmin, closet_so_far, temp_rec)) { hit_anything = true ; closet_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } #endif
更新过的main.cpp如下:
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 #include <iostream> #include <fstream> #include <float.h> #include "vec3.h" #include "ray.h" #include "sphere.h" #include "hitablelist.h" vec3 color (const ray &r, hitable *world) { hit_record rec; if (world->hit (r, 0.0 , FLT_MAX, rec)) { return 0.5 * vec3 (rec.normal.x () + 1 , rec.normal.y () + 1 , rec.normal.z () + 1 ); } else { vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0 - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } } int main () { int nx = 200 ; int ny = 100 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; vec3 lower_left_corner (-2.0 , -1.0 , -1.0 ) ; vec3 horizontal (4.0 , 0.0 , 0.0 ) ; vec3 vertical (0.0 , 2.0 , 0.0 ) ; vec3 origin (0.0 , 0.0 , 0.0 ) ; hitable *list[2 ]; list[0 ] = new sphere (vec3 (0 , 0 , -1 ), 0.5 ); list[1 ] = new sphere (vec3 (0 , -100.5 , -1 ), 100 ); hitable *world = new hitable_list (list, 2 ); for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { float u = float (i) / float (nx); float v = float (j) / float (ny); ray r (origin, lower_left_corner + u * horizontal + v * vertical) ; vec3 p = r.point_at_parameter (2.0 ); vec3 col = color (r, world); int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
最终的结果:
Chapter 6 : Antialiasing
本节目标是实现抗锯齿,首先需要实现在一定范围内的随机采样,因此我们需要一个随机函数。我们用到random头文件,利用rand()函数。
要取得 [a,b) 的随机整数,使用 (rand() % (b-a))+ a;
要取得 [a,b] 的随机整数,使用 (rand() % (b-a+1))+ a;
要取得 (a,b] 的随机整数,使用 (rand() % (b-a))+ a + 1;
通用公式: a + rand() % n;其中的 a 是起始值,n 是整数的范围。
要取得 a 到 b 之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。
要取得 0~1 之间的浮点数,可以使用 rand() / double(RAND_MAX)。
因此我们可以在main.cpp定义一个宏,用来生成[ a . b ] [a.b] [ a . b ] 之间的随机数。
1 #define random(a, b) (rand()%(b-a+1)+a)
当然,缺点是每此生成的随机结果都是一样的。
对于给定的一个像素,我们有好几个随机的采样点,对每个采样点进行ray tracer,然后再平均结果。
然后我们实现一个camera类。
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 #ifndef RAYTRACINGTOY_CAMERA_H #define RAYTRACINGTOY_CAMERA_H #include "ray.h" class camera {public : camera () { lower_left_corner = vec3 (-2.0 , -1.0 , -1.0 ); horizontal = vec3 (4.0 , 0.0 , 0.0 ); vertical = vec3 (0.0 , 2.0 , 0.0 ); origin = vec3 (0.0 , 0.0 , 0.0 ); } ray get_ray (float u, float v) { return ray (origin, lower_left_corner + u * horizontal + v * vertical - origin); } vec3 origin; vec3 lower_left_corner; vec3 horizontal; vec3 vertical; }; #endif
最终的main.cpp
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 #include <iostream> #include <fstream> #include <float.h> #include "vec3.h" #include "ray.h" #include "sphere.h" #include "hitablelist.h" #include "camera.h" #include "random" #define random(a, b) (rand()%(b-a+1)+a) vec3 color (const ray &r, hitable *world) { hit_record rec; if (world->hit (r, 0.0 , FLT_MAX, rec)) { return 0.5 * vec3 (rec.normal.x () + 1 , rec.normal.y () + 1 , rec.normal.z () + 1 ); } else { vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0 - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } } int main () { int nx = 200 ; int ny = 100 ; int ns = 100 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; camera cam; hitable *list[2 ]; list[0 ] = new sphere (vec3 (0 , 0 , -1 ), 0.5 ); list[1 ] = new sphere (vec3 (0 , -100.5 , -1 ), 100 ); hitable *world = new hitable_list (list, 2 ); for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { vec3 col (0 , 0 , 0 ) ; for (int s = 0 ; s < ns; s++) { float u = float ((i + random (0 , 100 ) / 100 ) / float (nx)); float v = float ((j + random (0 , 100 ) / 100 ) / float (ny)); ray r = cam.get_ray (u, v); vec3 p = r.point_at_parameter (2.0 ); col += color (r, world); } col /= float (ns); int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
Chapter7 : Diffuse Materials
本章的目标是实现漫反射。
通常在渲染中,每个物体都有自己的材质。
对于不发光的物体,漫反射是吸收周围的颜色,并与自身的颜色混合调节。
从漫反射物体表面反射的光线的方向是随机的。因此,如下图,如果我们向两个漫反射物体形成的夹缝中射入3条光线,会有不同的反射路径结果。
物体表面越暗,看起来越像是光线被吸收了,吸收之后的表面更像是是一个哑光的表面。
我们选择表面上一点hitpoint点P P P ,在该点做一个与表面相切的单位球,这个球的球心是( P + N ) (P+N) ( P + N ) ,我们从球体中随机选择一个点S S S ,从hitpoint点P P P 处,发射一条由P P P 指向S S S 的射线作为漫反射的方向。
我们构造一个方法来从单位半径球体中选择一个随机点:首先,选择一个单位立方体中的随机点,其中x,y,z的范围都在-1到+1之间。如果这个点在球体外,则重新选择一个随机点。
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 vec3 random_in_unit_sphere () { vec3 p; do { p = 2.0 * vec3 (random1, random1, random1) - vec3 (1 , 1 , 1 ); } while (dot (p, p) >= 1.0 ); return p; } vec3 color (const ray &r, hitable *world) { hit_record rec; if (world->hit (r, 0.0 , FLT_MAX, rec)) { vec3 target = rec.p + rec.normal + random_in_unit_sphere (); return 0.5 * color (ray (rec.p, target - rec.p), world); } else { vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0 - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } }
此时我们得到的结果:
可以看出球体底部过暗,因此我们可以进行gamma补偿。
1 2 3 4 5 6 7 col /= float (ns); col = vec3 (sqrt (col[0 ]), sqrt (col[1 ]), sqrt (col[2 ])); int ir = int (255.99 * col[0 ]);int ig = int (255.99 * col[1 ]);int ib = int (255.99 * col[2 ]);
得到的补偿后的结果:
最终的mian.cpp:
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 #include <iostream> #include <fstream> #include <float.h> #include "vec3.h" #include "ray.h" #include "sphere.h" #include "hitablelist.h" #include "camera.h" #include "random" #include <iostream> #define random(a, b) (rand()%(b-a+1)+a) #define random1 (float((rand() % 100) / 100.f)) vec3 random_in_unit_sphere () { vec3 p; do { p = 2.0 * vec3 (random1, random1, random1) - vec3 (1 , 1 , 1 ); } while (dot (p, p) >= 1.0 ); return p; } vec3 color (const ray &r, hitable *world) { hit_record rec; if (world->hit (r, 0.0 , FLT_MAX, rec)) { vec3 target = rec.p + rec.normal + random_in_unit_sphere (); return 0.5 * color (ray (rec.p, target - rec.p), world); } else { vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0 - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } } int main () { int nx = 200 ; int ny = 100 ; int ns = 200 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; camera cam; hitable *list[2 ]; list[0 ] = new sphere (vec3 (0 , 0 , -1 ), 0.5 ); list[1 ] = new sphere (vec3 (0 , -100.5 , -1 ), 100 ); hitable *world = new hitable_list (list, 2 ); for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { vec3 col (0 , 0 , 0 ) ; for (int s = 0 ; s < ns; s++) { float u = float ((i + random (0 , 100 ) / 100 ) / float (nx)); float v = float ((j + random (0 , 100 ) / 100 ) / float (ny)); ray r = cam.get_ray (u, v); vec3 p = r.point_at_parameter (2.0 ); col += color (r, world); } col /= float (ns); col = vec3 (sqrt (col[0 ]), sqrt (col[1 ]), sqrt (col[2 ])); int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
如果我们需要不同的物体拥有不同的材质,一种本方法是:我们可以定义一个具有很多参数的材质,每种不同的材质就是不同参数的组合。
或者我们可以定义一个材质的抽象类:
我们定义的材质需要能够做到:
产生散射线(或者说它吸收了入射线)
如果发生散射,则应该确定光线究竟衰减了多少
定义一个如下的material抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #ifndef RAYTRACINGTOY_MATERIAL_H #define RAYTRACINGTOY_MATERIAL_H #include "ray.h" #include "hitable.h" class material {public : virtual bool scatter (const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const = 0 ; }; #endif
在我们计算碰撞的信息时,我们需要获得碰撞处的材质,因此我们在h i t _ r e c o r d hit\_record h i t _ r e c o r d 类中添加一个指向材质的指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef RAYTRACINGTOY_HITABLE_H #define RAYTRACINGTOY_HITABLE_H #include "ray.h" #include "material.h" struct hit_record { float t; vec3 p; vec3 normal; material *mat_ptr; }; class hitable {public : virtual bool hit (const ray &r, float t_min, float t_max, hit_record &rec) const = 0 ; }; #endif
对于我们已经拥有的Lambertian(漫反射)情况,它既可以总是散射,也可以通过其反射率R R R 衰减,或者它可以在没有衰减的情况下散射但吸收部分1 − R 1-R 1 − R 射线,或者混合使用这些策略。对于Lambertian材质,我们可以设计这样一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 class lambertian : public material {public : lambertian (const vec3 &a) : albedo (a) {} virtual bool scatter (const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const { vec3 target = rec.p + rec.normal + random_in_unit_sphere (); scattered = ray (rec.p, target - rec.p); attenuation = albedo; return true ; } vec3 albedo; };
我们也可以仅按照某个概率p p p 进行散射,并且衰减为a l b e d o / p albedo/p a l b e d o / p 。
对于光滑 的表面,光线不会随机散射。光滑表面的光线反射物理规律是出射角等于入射角,会发生镜面反射 。
图中红色的是反射光线,向量表示为( v + 2 B ) (v+2B) ( v + 2 B ) ,N N N 是单位法向量,v v v 是入射光线的方向向量,B B B 的模是v v v 和N N N 的点乘d o t ( v , N ) dot(v,N) d o t ( v , N ) 。公式为:
1 2 3 4 5 6 7 vec3 reflect (const vec3 &v, const vec3 &n) { return v - 2 * dot (v, n) * n; }
接下来我们就可以利用镜面反射来构造一个金属材质(m a t e l matel m a t e l ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class metal : public material {public : metal (const vec3 &a) : albedo (a) {} virtual bool scatter (const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const { vec3 reflected = reflect (unit_vector (r_in.direction ()), rec.normal); scattered = ray (rec.p, reflected); attenuation = albedo; return (dot (scattered.direction (), rec.normal) > 0 ); } vec3 albedo; };
然后我们对c o l o r ( ) color() c o l o r ( ) 函数进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 vec3 color (const ray &r, hitable *world, int depth) { hit_record rec; if (world->hit (r, 0.0001 , FLT_MAX, rec)) { ray scattered; vec3 attenuation; if (depth < 50 && rec.mat_ptr->scatter (r, rec, attenuation, scattered)) { return attenuation * color (scattered, world, depth + 1 ); } else { return vec3 (0 , 0 , 0 ); } } else { vec3 unit_direction = unit_vector (r.direction ()); float t = 0.5 * (unit_direction.y () + 1.0 ); return (1.0 - t) * vec3 (1.0 , 1.0 , 1.0 ) + t * vec3 (0.5 , 0.7 , 1.0 ); } }
也需要对s p h e r e sphere s p h e r e 类进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 class sphere : public hitable {public : sphere () {} sphere (vec3 cen, float r, material *mat) : center (cen), radius (r), mat_ptr (mat) {}; virtual bool hit (const ray &r, float tmin, float tmax, hit_record &rec) const ; vec3 center; float radius; material *mat_ptr; };
在m a i n main m a i n 中新建两个金属球:
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 int main () { int nx = 600 ; int ny = 300 ; int ns = 500 ; std::ofstream outfile; outfile.open ("color.ppm" ); outfile << "P3\n" << nx << " " << ny << "\n255\n" ; camera cam; hitable *list[2 ]; list[0 ] = new sphere (vec3 (0 , 0 , -1 ), 0.5 , new lambertian (vec3 (0.8 , 0.3 , 0.3 ))); list[1 ] = new sphere (vec3 (0 , -100.5 , -1 ), 100 , new lambertian (vec3 (0.8 , 0.8 , 0.8 ))); list[2 ] = new sphere (vec3 (1 , 0 , -1 ), 0.5 , new metal (vec3 (0.8 , 0.6 , 0.2 ))); list[3 ] = new sphere (vec3 (-1 , 0 , -1 ), 0.5 , new metal (vec3 (0.8 , 0.8 , 0.8 ))); hitable *world = new hitable_list (list, 4 ); for (int j = ny - 1 ; j >= 0 ; j--) { for (int i = 0 ; i < nx; i++) { vec3 col (0 , 0 , 0 ) ; for (int s = 0 ; s < ns; s++) { float u = float ((i + random (0 , 100 ) / 100 ) / float (nx)); float v = float ((j + random (0 , 100 ) / 100 ) / float (ny)); ray r = cam.get_ray (u, v); vec3 p = r.point_at_parameter (2.0 ); col += color (r, world, 0 ); } col /= float (ns); col = vec3 (sqrt (col[0 ]), sqrt (col[1 ]), sqrt (col[2 ])); int ir = int (255.99 * col[0 ]); int ig = int (255.99 * col[1 ]); int ib = int (255.99 * col[2 ]); outfile << ir << " " << ig << " " << ib << "\n" ; } } outfile.close (); return 0 ; }
此时的结果:
对于m e t a l metal m e t a l 的材质,也可以用一个随机性的反射方向,来做微量的偏移,相当于在一个小球上选择一个e n d p o i n t endpoint e n d p o i n t ,而f u z z i n s s fuzzinss f u z z i n s s 就相当于这个小球的半径,可以决定反射偏移的多少,f u z z i n s s fuzzinss f u z z i n s s 的取值在[ 0 , 1 ] [0,1] [ 0 , 1 ] 之间。
Chapter9:Dielectrics
透明的材料,如水,玻璃和钻石是电介质。
当一束光线照射到它们时,它会分裂成一束反射光线和一束折射(透射)光线。我们将通过在反射和折射之间随机选择并且每次相互作用只产生一个散射光来处理这个问题。
最难调试的部分是折射光。通常我们假设所有的折射,只有一条折射光线。我们先尝试在场景中放入两个玻璃球。得到如下图:
但是看起来并不对,现实生活中没有黑色的东西。
光从一种介质进入另一种介质时,实际上,有一部分光会折射进入另一种介质,有另一部分光则会反射回来。反射系数=反射光振幅(能量)/入射光振幅(能量)。
折射满足斯涅尔定律(Snell’s law):
n ∗ s i n ( t h e t a ) = n ′ ∗ s i n ( t h e t a ′ ) n * sin(theta) = n' * sin(theta')
n ∗ s i n ( t h e t a ) = n ′ ∗ s i n ( t h e t a ′ )
其中n
和n'
是折射率。一般空气的折射率为1,玻璃的折射率为1.3-1.7,钻石的折射率为2.4。
一个棘手的实际问题是,当光现在具有较高折射率的材料中时,斯涅尔定律并没有真正的解决方案,不可能存在折射。所有的光线都在其内部反射,因为实际上这些光线通常在固体物体内部,所以被称为全内反射 。这就是为什么有时当你被淹没在水中时,水-空气的界限就像一面完美的镜子。因此,折射的代码要比反射的代码复杂一些。
折射的部分代码:
1 2 3 4 5 6 7 8 9 10 11 bool refract (const vec3 &v, const vec3 &n, float ni_over_nt, vec3 &refracted) { vec3 uv = unit_vector (v); float dt = dot (uv, n); float discriminant = 1.0 - ni_over_nt * ni_over_nt * (1 - dt * dt); if (discriminant > 0 ) { refracted = ni_over_nt * (v - n * dt) - n * sqrt (discriminant); return true ; } else { return false ; } }
电介质材质总是会发生折射,因此我们在材质类中派生dielectrics材质:
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 class dielectric : public material {public : dielectric (float ri) : ref_idx (ri) {} virtual bool scatter (const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const { vec3 outward_normal; vec3 reflected = reflect (r_in.direction (), rec.normal); float ni_over_nt; attenuation = vec3 (1.0 , 1.0 , 0.0 ); vec3 refracted; if (dot (r_in.direction (), rec.normal) > 0 ) { outward_normal = -rec.normal; ni_over_nt = ref_idx; } else { outward_normal = rec.normal; ni_over_nt = 1.0 / ref_idx; } if (refract (r_in.direction (), outward_normal, ni_over_nt, refracted)) { scattered = ray (rec.p, refracted); } else { scattered = ray (rec.p, reflected); return false ; } return true ; } float ref_idx; };
Attenuation(衰减)总为1,玻璃材质的表面不吸收任何光线。
我们将球4材质改为玻璃球。
1 list[3 ] = new sphere (vec3 (-1 , 0 , -1 ), 0.5 , new dielectric (1.5 ));
真正的玻璃具有随角度变化的反射率 ,从陡峭的角度看窗户,它就变成了一面镜子。这里一个简陋但是大家都在用的,由Christophe Schlick提出的多项式近似:
1 2 3 4 5 float schlick (float cosine, float ref_idx) { float r0 = (1 - ref_idx) / (1 + ref_idx); r0 = r0 * r0; return r0 + (1 - r0) * pow ((1 - cosine), 5 ); }
因此完成后的电介质材质:
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 class dielectric : public material {public : dielectric (float ri) : ref_idx (ri) {} virtual bool scatter (const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const { vec3 outward_normal; vec3 reflected = reflect (r_in.direction (), rec.normal); float ni_over_nt; attenuation = vec3 (1.0 , 1.0 , 1.0 ); vec3 refracted; float reflect_prob; float cosine; if (dot (r_in.direction (), rec.normal) > 0 ) { outward_normal = -rec.normal; ni_over_nt = ref_idx; cosine = ref_idx * dot (r_in.direction (), rec.normal) / r_in.direction ().length (); } else { outward_normal = rec.normal; ni_over_nt = 1.0 / ref_idx; cosine = -dot (r_in.direction (), rec.normal) / r_in.direction ().length (); } if (refract (r_in.direction (), outward_normal, ni_over_nt, refracted)) { reflect_prob = schlick (cosine, ref_idx); } else { scattered = ray (rec.p, reflected); reflect_prob = 1.0 ; } if (drand48 () < reflect_prob) { scattered = ray (rec.p, reflected); } else { scattered = ray (rec.p, refracted); } return true ; } float ref_idx; };