根据《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); }
|
玻璃
斯涅尔定律:
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); } }
|