stocking

git clone https://git.bradleygannon.com/stocking.git
Log | Files | Refs

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:
Asrc/hittable.rs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/interval.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 49+++++++++++++++++++++++++++++++------------------
Msrc/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 } }