Skip to content

Commit bdd965e

Browse files
committed
Add raytracer runtime benchmark
Adapted from https://github.com/jorendorff/rust-raytrace. Slightly modified for edition 2021 and made deterministic by removing randomness.
1 parent 2f15679 commit bdd965e

File tree

8 files changed

+1061
-0
lines changed

8 files changed

+1061
-0
lines changed

collector/runtime-benchmarks/raytracer/Cargo.lock

Lines changed: 530 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "raytrace"
3+
version = "0.1.0"
4+
authors = ["Jason Orendorff <jason.orendorff@gmail.com>"]
5+
edition = "2021"
6+
7+
[dependencies]
8+
lodepng = "0.10"
9+
benchlib = { path = "../../benchlib" }
10+
11+
[workspace]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::vec::{random_in_unit_disc, Ray, Vec3};
2+
3+
use std::f32::consts::PI;
4+
5+
#[derive(Clone, Copy, Debug)]
6+
pub struct Camera {
7+
origin: Vec3,
8+
u: Vec3, // unit vector in direction of x coordinates
9+
v: Vec3, // unit vector in dirction of y coordinates
10+
lower_left_corner: Vec3,
11+
horizontal: Vec3,
12+
vertical: Vec3,
13+
lens_radius: f32,
14+
}
15+
16+
impl Camera {
17+
pub fn new(
18+
lookfrom: Vec3,
19+
lookat: Vec3,
20+
vup: Vec3,
21+
vfov_degrees: f32,
22+
aspect: f32,
23+
aperture: f32,
24+
focus_distance: f32,
25+
) -> Camera {
26+
let theta = vfov_degrees * PI / 180.0;
27+
let half_height = (theta / 2.0).tan();
28+
let half_width = aspect * half_height;
29+
let w = (lookfrom - lookat).to_unit_vector();
30+
let u = vup.cross(w).to_unit_vector();
31+
let v = w.cross(u);
32+
Camera {
33+
origin: lookfrom,
34+
u,
35+
v,
36+
lower_left_corner: lookfrom - focus_distance * (half_width * u + half_height * v + w),
37+
horizontal: focus_distance * 2.0 * half_width * u,
38+
vertical: focus_distance * 2.0 * half_height * v,
39+
lens_radius: aperture / 2.0,
40+
}
41+
}
42+
43+
pub fn get_ray(&self, u: f32, v: f32) -> Ray {
44+
let Vec3(du, dv, _) = self.lens_radius * random_in_unit_disc();
45+
let origin = self.origin + du * self.u + dv * self.v;
46+
Ray::new(
47+
origin,
48+
self.lower_left_corner + u * self.horizontal + v * self.vertical - origin,
49+
)
50+
}
51+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use benchlib::benchmark::run_benchmark_group;
2+
use camera::Camera;
3+
use materials::{Dielectric, Lambertian, Material, Metal};
4+
use model::{Model, Sphere};
5+
use std::rc::Rc;
6+
use vec::{random_in_unit_disc, Vec3};
7+
8+
mod camera; // translate 2D pixel coordinates to 3D rays
9+
mod materials; // reflective properties of surfaces
10+
mod model; // geometry of objects in the scene
11+
mod render;
12+
mod vec; // basic 3D vector math // the core ray-tracing algorithm
13+
14+
/// Generate a Model containing a bunch of randomly placed spheres.
15+
fn create_scene() -> Box<dyn Model> {
16+
let mut spheres: Vec<Sphere> = vec![
17+
Sphere {
18+
center: Vec3(0.0, 0.0, -1000.0),
19+
radius: 1000.0,
20+
material: Box::new(Lambertian {
21+
albedo: Vec3(1.0, 0.6, 0.5),
22+
}),
23+
},
24+
Sphere {
25+
center: Vec3(-4.0, 0.0, 2.0),
26+
radius: 2.0,
27+
material: Box::new(Lambertian {
28+
albedo: Vec3(0.6, 0.2, 0.2),
29+
}),
30+
},
31+
Sphere {
32+
center: Vec3(0.0, 0.0, 2.0),
33+
radius: 2.0,
34+
material: Box::new(Dielectric { index: 1.5 }),
35+
},
36+
Sphere {
37+
center: Vec3(4.0, 0.0, 2.0),
38+
radius: 2.0,
39+
material: Box::new(Metal {
40+
albedo: Vec3(0.85, 0.9, 0.7),
41+
fuzz: 0.0,
42+
}),
43+
},
44+
];
45+
46+
fn random_material() -> Box<dyn Material> {
47+
Box::new(Lambertian {
48+
albedo: Vec3(0.5, 0.5, 0.5),
49+
})
50+
}
51+
52+
for _ in 0..500 {
53+
let r = 0.4;
54+
let Vec3(x, y, _) = random_in_unit_disc();
55+
let pos = 20.0 * Vec3(x, y, 0.0) + Vec3(0.0, 0.0, r);
56+
if spheres
57+
.iter()
58+
.all(|s| (s.center - pos).length() >= s.radius + r)
59+
{
60+
spheres.push(Sphere {
61+
center: pos,
62+
radius: r,
63+
material: random_material(),
64+
});
65+
}
66+
}
67+
68+
let world: Vec<Box<dyn Model>> = spheres
69+
.into_iter()
70+
.map(|s| Box::new(s) as Box<dyn Model>)
71+
.collect();
72+
Box::new(world)
73+
}
74+
75+
fn main() {
76+
const WIDTH: usize = 400;
77+
const HEIGHT: usize = 200;
78+
79+
const NSAMPLES: usize = 100;
80+
81+
run_benchmark_group(|group| {
82+
// Performs raytracing on a simple scene.
83+
// Adapted from https://github.com/jorendorff/rust-raytrace.
84+
group.register_benchmark("raytracer", || {
85+
let scene = create_scene();
86+
let lookfrom = Vec3(20.0 * 0.47f32.cos(), 20.0 * 0.47f32.sin(), 3.0);
87+
let lookat = Vec3(0.0, 0.0, 1.0);
88+
let vup = Vec3(0.0, 0.0, 1.0);
89+
let focus_distance = (lookfrom - lookat).length();
90+
let aperture = 0.3;
91+
let camera = Rc::new(Camera::new(
92+
lookfrom,
93+
lookat,
94+
vup,
95+
20.0,
96+
WIDTH as f32 / HEIGHT as f32,
97+
aperture,
98+
focus_distance,
99+
));
100+
move || render::render(&*scene, &camera, WIDTH, HEIGHT, NSAMPLES)
101+
});
102+
});
103+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use crate::model::Hit;
2+
use crate::vec::{random_in_unit_sphere, Ray, Vec3};
3+
4+
#[derive(Clone, Copy, Debug)]
5+
pub struct Scatter {
6+
pub color: Vec3,
7+
pub ray: Option<Ray>,
8+
}
9+
10+
pub trait Material {
11+
fn scatter(&self, r_in: &Ray, rec: &Hit) -> Scatter;
12+
}
13+
14+
pub struct Lambertian {
15+
pub albedo: Vec3,
16+
}
17+
18+
impl Material for Lambertian {
19+
fn scatter(&self, _r_in: &Ray, hit: &Hit) -> Scatter {
20+
let target = hit.p + hit.normal + random_in_unit_sphere();
21+
Scatter {
22+
color: self.albedo,
23+
ray: Some(Ray::new(hit.p, target - hit.p)),
24+
}
25+
}
26+
}
27+
28+
fn reflect(v: Vec3, n: Vec3) -> Vec3 {
29+
v - 2.0 * v.dot(n) * n
30+
}
31+
32+
pub struct Metal {
33+
pub albedo: Vec3,
34+
pub fuzz: f32,
35+
}
36+
37+
impl Material for Metal {
38+
fn scatter(&self, r_in: &Ray, hit: &Hit) -> Scatter {
39+
let reflected = reflect(r_in.direction, hit.normal);
40+
let scattered = Ray::new(hit.p, reflected + self.fuzz * random_in_unit_sphere());
41+
42+
Scatter {
43+
color: self.albedo,
44+
ray: if scattered.direction.dot(hit.normal) <= 0.0 {
45+
None
46+
} else {
47+
Some(scattered)
48+
},
49+
}
50+
}
51+
}
52+
53+
pub struct Dielectric {
54+
// Technically, this is not the index of refaction but the ratio of the
55+
// index of refraction inside the material to the index of refraction
56+
// outside. But if the material outside is air, its index of refraction is
57+
// 1 and so it amounts to the same thing.
58+
pub index: f32,
59+
}
60+
61+
fn refract(v: Vec3, n: Vec3, ni_over_nt: f32) -> Option<Vec3> {
62+
let uv = v.to_unit_vector();
63+
64+
let dt = uv.dot(n);
65+
let discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt);
66+
if discriminant > 0.0 {
67+
Some(ni_over_nt * (uv - dt * n) - discriminant.sqrt() * n)
68+
} else {
69+
None
70+
}
71+
}
72+
73+
const WHITE: Vec3 = Vec3(1.0, 1.0, 1.0);
74+
75+
impl Material for Dielectric {
76+
fn scatter(&self, r_in: &Ray, hit: &Hit) -> Scatter {
77+
let outward_normal: Vec3;
78+
let ni_over_nt: f32;
79+
80+
if r_in.direction.dot(hit.normal) > 0.0 {
81+
outward_normal = -hit.normal;
82+
ni_over_nt = self.index;
83+
} else {
84+
outward_normal = hit.normal;
85+
ni_over_nt = 1.0 / self.index;
86+
}
87+
88+
let _ = refract(r_in.direction, outward_normal, ni_over_nt);
89+
90+
Scatter {
91+
color: WHITE,
92+
ray: Some(Ray::new(hit.p, reflect(r_in.direction, hit.normal))),
93+
}
94+
}
95+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use crate::materials::Material;
2+
use crate::vec::{Ray, Vec3};
3+
4+
#[derive(Clone, Copy)]
5+
pub struct Hit<'obj> {
6+
pub t: f32,
7+
pub p: Vec3,
8+
pub normal: Vec3,
9+
pub material: &'obj dyn Material,
10+
}
11+
12+
pub trait Model {
13+
fn hit(&self, r: &Ray) -> Option<Hit>;
14+
}
15+
16+
pub struct Sphere {
17+
pub center: Vec3,
18+
pub radius: f32,
19+
pub material: Box<dyn Material>,
20+
}
21+
22+
/// Minimum distance a ray must travel before we'll consider a possible hit.
23+
///
24+
/// If we try to use 0 here, we get a really strange bug. When a ray hits an object
25+
/// and bounces, we'll sometimes register another hit on the same sphere,
26+
/// at some tiny but positive distance, due to floating-point error.
27+
///
28+
const T_MIN: f32 = 0.0001;
29+
30+
impl Model for Sphere {
31+
fn hit<'a>(&'a self, r: &Ray) -> Option<Hit<'a>> {
32+
let oc = r.origin - self.center;
33+
let a = r.direction.dot(r.direction);
34+
let hb = oc.dot(r.direction);
35+
let c = oc.dot(oc) - self.radius * self.radius;
36+
let discriminant = hb * hb - a * c;
37+
if discriminant > 0.0 {
38+
let t = (-hb - discriminant.sqrt()) / a;
39+
if t >= T_MIN {
40+
let p = r.point_at_parameter(t);
41+
return Some(Hit {
42+
t: t,
43+
p: p,
44+
normal: (p - self.center) / self.radius,
45+
material: &*self.material,
46+
});
47+
}
48+
let t = (-hb + discriminant.sqrt()) / a;
49+
if t >= T_MIN {
50+
let p = r.point_at_parameter(t);
51+
return Some(Hit {
52+
t: t,
53+
p: p,
54+
normal: (p - self.center) / self.radius,
55+
material: &*self.material,
56+
});
57+
}
58+
}
59+
None
60+
}
61+
}
62+
63+
impl Model for Vec<Box<dyn Model>> {
64+
fn hit(&self, r: &Ray) -> Option<Hit> {
65+
let mut best = None;
66+
for child in self {
67+
if let Some(hit) = child.hit(r) {
68+
match best {
69+
None => best = Some(hit),
70+
Some(prev) => {
71+
if hit.t < prev.t {
72+
best = Some(hit)
73+
}
74+
}
75+
}
76+
}
77+
}
78+
best
79+
}
80+
}

0 commit comments

Comments
 (0)