0%

Ray tracing

根据《Ray Tracing in a Weekend》这本书做的。

光线追踪简单实现

输出格式

.ppm的图片格式,用VS的话可以直接进项目属性页->调试中把到控制台的输出重定向到文件中。

ppm格式示例:

P3

3 2

255

255 0 0 0 255 0 0 0 255

255 255 0 255 255 255 0 0 0

ppm格式的起始两个字节为P3或者P6,第二行为图像大小,第三行描述像素的最大颜色组成,最后为图像数据。

向量类

这个类没啥好说的,实现基础的向量运算就行。实在嫌麻烦可以直接用glm里的vec3。

射线类

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
class ray
{
private:
vec3 A;
vec3 B;
public :
ray() {}
ray(vec3 origin, vec3 direction) : A(origin), B(direction)
{

}
vec3 origin() const
{
return A;
}
vec3 direction() const
{
return B;
}
vec3 point_at_parameter(float t) const
{
return A + t * B;
}

};

射线由起点和方向组成,并需要一个方法–能根据输入参数t返回一个射线路径上的点。

光线撞击判定

这个程序只进行球类的判定。设球心坐标为(xc, yc, zc),则点是否在球上的判定式为(x - xc)^2 + (y - yc)^2 + (z - zc)^2 = R^2

转换为向量形式:dot((A + t B - C), (A + t B - C)) = R * R

=> t t dot(B, B) + 2 t dot(B, A - C) + dot(A - C, A - C) - R * R = 0

A为射线起点,B为射线方向,C为球心。

所求的为关于t的一元二次方程,根据delta不同有三种根结构。

同时,需要记录碰撞点的信息:一个根t, 碰撞点p,碰撞点处的法线normal,材质信息material。

最终把其抽象为hitable的抽象类,继承他的类必须实现hit方法。

下为球的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
struct hit_record
{
float t;
vec3 p;
vec3 normal;
material* material_ptr;
};

bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const
{
vec3 oc = r.origin() - center;
float a = dot(r.direction(), r.direction());
float b = 2.0f * dot(r.direction(), oc);
float c = dot(oc, oc) - radius * radius;
float delta = b * b - 4 * a * c;
if (delta < 0)
{
return false;
}
else
{
float x = (-b - sqrt(delta)) / (2 * a);
if (x < t_max && x > t_min)
{
rec.t = x;
rec.p = r.point_at_parameter(x);
rec.normal = (rec.p - center) / radius;
rec.material_ptr = m;
return true;
}
x = (-b + sqrt(delta)) / (2 * a);
if (x < t_max && x > t_min)
{
rec.t = x;
rec.p = r.point_at_parameter(x);
rec.normal = (rec.p - center) / radius;
rec.material_ptr = m;
return true;
}
return false;
}
}

光线追踪时,一条光线需要获取到整个场景的信息,所以创建一个hitable_list的类继承hitable来管理场景内所有可以碰撞的球。

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 hitable_list : public hitable
{
private :
hitable** list;
int list_size;
public :
hitable_list() {}
hitable_list(hitable** l, int size)
{
list = l;
list_size = size;
}
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const
{
hit_record temp;
bool hit_result = false;
double closest_so_far = t_max;
for (int i = 0; i < list_size; i++)
{
if (list[i]->hit(r, t_min, closest_so_far, temp))
{
hit_result = true;
closest_so_far = temp.t;
rec = temp;
}
}
return hit_result;
}
};

相机类

为了输出图像,创建一个相机类来观察场景

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
class camera
{
private :
vec3 origin;
vec3 lower_left;
vec3 horizontal;
vec3 vertical;
float lens_radius;
vec3 u, w, v;
public :
camera(vec3 lookfrom, vec3 lookat, vec3 vUp, float fov, float aspect, float aperture, float focus_distance)
{
lens_radius = aperture / 2.0f;
float theta = fov * PI / 180;
float half_height = tan(theta / 2);
float half_width = aspect * half_height;
w = normalized_vector(lookfrom - lookat);
u = normalized_vector(cross(vUp, w));
v = cross(w, u);
origin = lookfrom;
lower_left = origin - focus_distance * half_width * u - focus_distance * half_height * v - focus_distance * w;
horizontal = 2 * focus_distance * half_width * u;
vertical = 2 * focus_distance * half_height * v;
}

ray get_ray(float s, float t)
{
vec3 rd = lens_radius * random_disk_point();
vec3 offset = u * rd.x() + v * rd.y();
return ray(origin + offset, lower_left + s * horizontal + t * vertical - origin - offset);
}

vec3 random_disk_point() const
{
vec3 p;
do
{
p = 2.0f * vec3(random_double(), random_double(), 0.0f) - vec3(1.0f, 1.0f, 0.0f);
} while (dot(p, p) >= 1.0);
return p;
}
};

材质

漫反射

当射线与表面撞击后会进行随机发散出一条射线,发散方向由一个单位球的随机点生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
virtual bool scatter(const ray& ray_in, hit_record& rec, vec3& attenuation, ray& scattered) const
{
vec3 next_direction = rec.p + rec.normal + random_sphere_point();
scattered = ray(rec.p, next_direction - rec.p);
attenuation = albedo;
return true;
}

vec3 random_sphere_point() const
{
vec3 p;
do
{
p = 2.0f * vec3(random_double(), random_double(), random_double()) - vec3(1.0f, 1.0f, 1.0f);
} while (p.squared_length() >= 1.0f);
return p;
}

金属

金属材质就为纯反射材质,根据碰撞点法线方向计算射线反射方向,再根据表面光滑度加上随机方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
vec3 reflect(vec3 in, vec3 normal) const
{
vec3 result = in + 2 * dot(-in, normal) * normal;
return result;
}

virtual bool scatter(const ray& ray_in, hit_record& rec, vec3& attenuation, ray& scattered) const
{
vec3 next_direction = reflect(normalized_vector(ray_in.direction()), rec.normal);
scattered = ray(rec.p, next_direction + fuzz * random_sphere_point());
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}

玻璃

斯涅尔定律:n_{1}\sin \theta _{1}=n_{2}\sin \theta _{2}

n1,n2为两种介质折射率;θ1,θ2为入射光与折射光与法线夹角。

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
bool refract(const vec3& in, const vec3& normal, float ni_over_no, vec3& refracted) const
{
vec3 nin = normalized_vector(in);
float dt = dot(nin, normal);
float discriminant = 1.0f - ni_over_no * ni_over_no * (1.0f - dt * dt);
if (discriminant > 0)
{
refracted = ni_over_no * (nin - normal * dt) - normal * sqrt(discriminant);
return true;
}
return false;
}

virtual bool scatter(const ray& ray_in, hit_record& rec, vec3& attenuation, ray& scattered) const
{
vec3 outward_normal;
float ni_over_no;
float cosine;
float reflect_prob;
vec3 reflected = reflect(ray_in.direction(), rec.normal);
attenuation = vec3(1.0f, 1.0f, 1.0f);
vec3 refracted;
if (dot(ray_in.direction(), rec.normal) > 0)
{
outward_normal = -rec.normal;
ni_over_no = ref_idx;
cosine = dot(ray_in.direction(), rec.normal) / ray_in.direction().length();
cosine = sqrt(1 - ref_idx * ref_idx * (1 - cosine * cosine));
}
else
{
outward_normal = rec.normal;
ni_over_no = 1.0f / ref_idx;
cosine = -dot(ray_in.direction(), rec.normal) / ray_in.direction().length();
}
if (refract(ray_in.direction(), outward_normal, ni_over_no, refracted))
{
reflect_prob = schlick(cosine, ref_idx);
}
else
{
reflect_prob = 1.0f;
}
if (random_double() < reflect_prob)
{
scattered = ray(rec.p, reflected);
}
else
{
scattered = ray(rec.p, refracted);
}
return true;
}

float schlick(float cosine, float ref_idx) const
{
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
vec3 paint(const ray& r, hitable* world, int depth)
{
hit_record rec;
if (world->hit(r, 0.001, FLT_MAX, rec))
{
ray scattered;
vec3 attenuation;
if (depth < 50 && rec.material_ptr->scatter(r, rec, attenuation, scattered)) {
return attenuation * paint(scattered, world, depth + 1);
}
else
{
return vec3(0, 0, 0);
}
}
else
{
vec3 unit_direction = normalized_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);
}
}