commit 2613c5262757b718b325f79f7fb2b6734db4fe83
parent 3193d1343701297c741155d80e7e7a0106a0c1c8
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Sun, 14 Dec 2025 21:30:38 -0500
Add Hittable trait and Interval struct
Complete Chapter 6
Diffstat:
| A | src/hittable.rs | | | 90 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/interval.rs | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/main.rs | | | 49 | +++++++++++++++++++++++++++++++------------------ |
| M | src/ray.rs | | | 2 | +- |
4 files changed, 172 insertions(+), 19 deletions(-)
diff --git a/src/hittable.rs b/src/hittable.rs
@@ -0,0 +1,90 @@
+use crate::{interval::Interval, ray::Ray, vec3::Vec3};
+
+pub struct HitRecord {
+ _point: Vec3,
+ normal: Vec3,
+ t: f64,
+ _front_face: bool,
+}
+
+impl HitRecord {
+ fn new(t: f64, r: &Ray, outward_normal: &Vec3) -> HitRecord {
+ let point = r.at(t);
+ let outward_normal_unit = outward_normal.unit();
+ let front_face = r.direction().dot(&outward_normal_unit) < 0.;
+ let normal = if front_face {
+ outward_normal_unit
+ } else {
+ -outward_normal_unit
+ };
+ HitRecord {
+ _point: point,
+ normal,
+ t,
+ _front_face: front_face,
+ }
+ }
+ pub fn normal(&self) -> Vec3 {
+ self.normal
+ }
+}
+
+pub trait Hittable {
+ fn hit(&self, r: &Ray, ray_t: Interval) -> Option<HitRecord>;
+}
+
+pub struct Sphere {
+ center: Vec3,
+ radius: f64,
+}
+
+impl Sphere {
+ pub const fn new(center: Vec3, radius: f64) -> Self {
+ Sphere {
+ center,
+ radius: radius.max(0.),
+ }
+ }
+}
+
+impl Hittable for Sphere {
+ fn hit(&self, r: &Ray, ray_t: Interval) -> Option<HitRecord> {
+ let oc = self.center - r.origin();
+ let a = r.direction().length_squared();
+ let h = r.direction().dot(&oc);
+ let c = oc.length_squared() - self.radius.powi(2);
+ let discriminant = h.powi(2) - a * c;
+ if discriminant < 0. {
+ return None;
+ }
+ let sqrt_d = discriminant.sqrt();
+ let root = {
+ let root_negative = (h - sqrt_d) / a;
+ if !ray_t.surrounds(root_negative) {
+ let root_positive = (h + sqrt_d) / a;
+ if !ray_t.surrounds(root_positive) {
+ return None;
+ }
+ root_positive
+ } else {
+ root_negative
+ }
+ };
+ let outward_normal = (r.at(root) - self.center) / self.radius;
+ Some(HitRecord::new(root, r, &outward_normal))
+ }
+}
+
+impl Hittable for &[Box<dyn Hittable>] {
+ fn hit(&self, r: &Ray, ray_t: Interval) -> Option<HitRecord> {
+ let mut best_hit = None;
+ let mut closest_so_far = ray_t.max();
+ for object in self.iter() {
+ if let Some(hit) = object.hit(r, Interval::new(ray_t.min(), closest_so_far)) {
+ closest_so_far = hit.t;
+ best_hit = Some(hit);
+ }
+ }
+ best_hit
+ }
+}
diff --git a/src/interval.rs b/src/interval.rs
@@ -0,0 +1,50 @@
+pub struct Interval {
+ min: f64,
+ max: f64,
+}
+
+impl Default for Interval {
+ fn default() -> Self {
+ Interval::empty()
+ }
+}
+
+impl Interval {
+ pub const fn new(min: f64, max: f64) -> Self {
+ Interval { min, max }
+ }
+
+ pub const fn min(&self) -> f64 {
+ self.min
+ }
+
+ pub const fn max(&self) -> f64 {
+ self.max
+ }
+
+ pub const fn empty() -> Self {
+ Self {
+ min: f64::INFINITY,
+ max: f64::NEG_INFINITY,
+ }
+ }
+
+ pub const fn _full() -> Self {
+ Self {
+ min: f64::NEG_INFINITY,
+ max: f64::INFINITY,
+ }
+ }
+
+ pub const fn _size(&self) -> f64 {
+ (self.max - self.min).max(0.)
+ }
+
+ pub const fn _contains(&self, x: f64) -> bool {
+ self.min <= x && x <= self.max
+ }
+
+ pub const fn surrounds(&self, x: f64) -> bool {
+ self.min < x && x < self.max
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -1,18 +1,17 @@
-use crate::{color::Color, ray::Ray, vec3::Vec3};
+use crate::{
+ color::Color,
+ hittable::{Hittable, Sphere},
+ interval::Interval,
+ ray::Ray,
+ vec3::Vec3,
+};
mod color;
+mod hittable;
+mod interval;
mod ray;
mod vec3;
-fn hit_sphere(center: Vec3, radius: f64, r: Ray) -> bool {
- let oc = center - r.origin();
- let a = r.direction().dot(&r.direction());
- let b = -2. * r.direction().dot(&oc);
- let c = oc.dot(&oc) - radius * radius;
- let discriminant = b * b - 4. * a * c;
- discriminant >= 0.
-}
-
struct Image {
data: Vec<Vec<Color>>,
}
@@ -49,6 +48,21 @@ impl Image {
}
}
+fn ray_color(r: &Ray, world: &[Box<dyn Hittable>]) -> Color {
+ if let Some(hit) = world.hit(r, Interval::new(0., f64::INFINITY)) {
+ let normal = hit.normal();
+ Color::new(
+ normal.x() * 0.5 + 0.5,
+ normal.y() * 0.5 + 0.5,
+ normal.z() * 0.5 + 0.5,
+ )
+ } else {
+ let unit_direction = r.direction().unit();
+ let a = 0.5 * (unit_direction.y() + 1.);
+ (1. - a) * Color::new(1., 1., 1.) + a * Color::new(0.5, 0.7, 1.)
+ }
+}
+
const ASPECT_RATIO: f64 = 16. / 9.;
const IMAGE_WIDTH: usize = 400;
const IMAGE_HEIGHT: usize = {
@@ -71,6 +85,11 @@ fn main() {
CAMERA_CENTER - Vec3::new(0., 0., FOCAL_LENGTH) - VIEWPORT_U / 2. - VIEWPORT_V / 2.;
let pixel_origin: Vec3 = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
+ let world: [Box<dyn Hittable>; 2] = [
+ Box::new(Sphere::new(Vec3::new(0., 0., -1.), 0.5)),
+ Box::new(Sphere::new(Vec3::new(0., -100.5, -1.), 100.)),
+ ];
+
let mut image = Image::new(IMAGE_WIDTH, IMAGE_HEIGHT);
for i in 0..IMAGE_WIDTH {
for j in 0..IMAGE_HEIGHT {
@@ -78,14 +97,8 @@ fn main() {
pixel_origin + (i as f64 * pixel_delta_u) + (j as f64 * pixel_delta_v);
let ray_direction = pixel_center - CAMERA_CENTER;
let ray = Ray::new(CAMERA_CENTER, ray_direction);
- let color = if hit_sphere(Vec3::new(0., 0., -1.), 0.5, ray) {
- Color::new(1., 0., 0.)
- } else {
- let unit_direction = ray.direction().unit();
- let a = 0.5 * (unit_direction.y() + 1.);
- (1. - a) * Color::new(1., 1., 1.) + a * Color::new(0.5, 0.7, 1.)
- };
- image.data[j][i] = color;
+
+ image.data[j][i] = ray_color(&ray, &world);
}
}
println!("{}", image.to_ppm_string());
diff --git a/src/ray.rs b/src/ray.rs
@@ -19,7 +19,7 @@ impl Ray {
self.direction
}
- pub fn _at(&self, t: f64) -> Vec3 {
+ pub fn at(&self, t: f64) -> Vec3 {
self.origin + t * self.direction
}
}