stocking

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

commit 51fe57edbc27aee94434264e3ac0b673d7f390d9
parent 5f7f9eb5019bbe128d49eba9cfc63171023f38da
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Mon, 15 Dec 2025 20:17:40 -0500

Add Material trait and Lambertian and Metal structs that implement it

Complete Chapter 10

Diffstat:
Msrc/camera.rs | 28+++++++++++++---------------
Msrc/color.rs | 115+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/hittable.rs | 22++++++++++++++++++----
Msrc/main.rs | 35++++++++++++++++++++++++++++++++---
Asrc/material.rs | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/random.rs | 1+
Msrc/vec3.rs | 10+++++++++-
7 files changed, 236 insertions(+), 72 deletions(-)

diff --git a/src/camera.rs b/src/camera.rs @@ -1,14 +1,10 @@ use crate::{ - color::{Color, ColorSpace}, - hittable::Hittable, - interval::Interval, - random::RandomNumberGenerator, - ray::Ray, - vec3::Vec3, + color::LinearColor, hittable::Hittable, interval::Interval, random::RandomNumberGenerator, + ray::Ray, vec3::Vec3, }; struct Image { - data: Vec<Vec<Color>>, + data: Vec<Vec<LinearColor>>, } fn scale_f64_to_u8(value: f64) -> u8 { @@ -18,7 +14,7 @@ fn scale_f64_to_u8(value: f64) -> u8 { impl Image { fn new(width: usize, height: usize) -> Self { Image { - data: vec![vec![Color::default(); width]; height], + data: vec![vec![LinearColor::default(); width]; height], } } fn to_ppm_string(&self) -> String { @@ -97,7 +93,7 @@ impl Camera { let mut image = Image::new(self.image_width, self.image_height); for i in 0..self.image_width { for j in 0..self.image_height { - let mut pixel_color = Color::new(0., 0., 0., ColorSpace::Linear); + let mut pixel_color = LinearColor::new(0., 0., 0.); for _ in 0..self.samples_per_pixel { let r = self.get_ray(i, j); pixel_color += @@ -109,18 +105,20 @@ impl Camera { println!("{}", image.to_ppm_string()); } - fn ray_color(&mut self, r: &Ray, world: &[Box<dyn Hittable>], depth: usize) -> Color { + fn ray_color(&mut self, r: &Ray, world: &[Box<dyn Hittable>], depth: usize) -> LinearColor { if depth == 0 { - return Color::new(0., 0., 0., ColorSpace::Linear); + return LinearColor::new(0., 0., 0.); } if let Some(hit) = world.hit(r, Interval::new(0.001, f64::INFINITY)) { - let direction = hit.normal() + Vec3::random_unit(&mut self.rng); - 0.5 * self.ray_color(&Ray::new(hit.point(), direction), world, depth - 1) + if let Some(scattered_ray) = hit.material().scatter(r, &hit, &mut self.rng) { + scattered_ray.attenuation() * self.ray_color(&scattered_ray.ray(), world, depth - 1) + } else { + LinearColor::new(0., 0., 0.) + } } else { let unit_direction = r.direction().unit(); let a = 0.5 * (unit_direction.y() + 1.); - (1. - a) * Color::new(1., 1., 1., ColorSpace::Linear) - + a * Color::new(0.5, 0.7, 1., ColorSpace::Linear) + (1. - a) * LinearColor::new(1., 1., 1.) + a * LinearColor::new(0.5, 0.7, 1.) } } diff --git a/src/color.rs b/src/color.rs @@ -1,47 +1,54 @@ use std::ops::{Add, AddAssign, Div, Mul}; #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub enum ColorSpace { - #[default] - Linear, - Gamma, +pub struct GammaColor { + red: f64, + green: f64, + blue: f64, +} + +impl GammaColor { + pub const fn red(&self) -> f64 { + self.red + } + + pub const fn green(&self) -> f64 { + self.green + } + + pub const fn blue(&self) -> f64 { + self.blue + } } #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Color { +pub struct LinearColor { red: f64, green: f64, blue: f64, - space: ColorSpace, } -impl Color { +impl LinearColor { const MIN: f64 = 0.; const MAX: f64 = 1.; - pub const fn new(red: f64, green: f64, blue: f64, space: ColorSpace) -> Self { - Color { - red: red.clamp(Color::MIN, Color::MAX), - green: green.clamp(Color::MIN, Color::MAX), - blue: blue.clamp(Color::MIN, Color::MAX), - space, + pub const fn new(red: f64, green: f64, blue: f64) -> Self { + LinearColor { + red: red.clamp(LinearColor::MIN, LinearColor::MAX), + green: green.clamp(LinearColor::MIN, LinearColor::MAX), + blue: blue.clamp(LinearColor::MIN, LinearColor::MAX), } } - pub fn as_gamma(&self) -> Self { - if let ColorSpace::Linear = self.space { - Color { - red: if self.red > 0. { self.red.sqrt() } else { 0. }, - green: if self.green > 0. { - self.green.sqrt() - } else { - 0. - }, - blue: if self.blue > 0. { self.blue.sqrt() } else { 0. }, - space: ColorSpace::Gamma, - } - } else { - *self + pub fn as_gamma(&self) -> GammaColor { + GammaColor { + red: if self.red > 0. { self.red.sqrt() } else { 0. }, + green: if self.green > 0. { + self.green.sqrt() + } else { + 0. + }, + blue: if self.blue > 0. { self.blue.sqrt() } else { 0. }, } } @@ -58,50 +65,60 @@ impl Color { } } -impl Add<Color> for Color { - type Output = Color; +impl Add<LinearColor> for LinearColor { + type Output = LinearColor; - fn add(self, rhs: Color) -> Self::Output { - Color { - red: (self.red + rhs.red).clamp(Color::MIN, Color::MAX), - green: (self.green + rhs.green).clamp(Color::MIN, Color::MAX), - blue: (self.blue + rhs.blue).clamp(Color::MIN, Color::MAX), - space: self.space, + fn add(self, rhs: LinearColor) -> Self::Output { + LinearColor { + red: (self.red + rhs.red).clamp(LinearColor::MIN, LinearColor::MAX), + green: (self.green + rhs.green).clamp(LinearColor::MIN, LinearColor::MAX), + blue: (self.blue + rhs.blue).clamp(LinearColor::MIN, LinearColor::MAX), } } } -impl AddAssign<Color> for Color { - fn add_assign(&mut self, rhs: Color) { +impl AddAssign<LinearColor> for LinearColor { + fn add_assign(&mut self, rhs: LinearColor) { self.red += rhs.red; self.green += rhs.green; self.blue += rhs.blue; } } -impl Mul<f64> for Color { - type Output = Color; +impl Mul<f64> for LinearColor { + type Output = LinearColor; fn mul(self, rhs: f64) -> Self::Output { - Color { - red: (self.red * rhs).clamp(Color::MIN, Color::MAX), - green: (self.green * rhs).clamp(Color::MIN, Color::MAX), - blue: (self.blue * rhs).clamp(Color::MIN, Color::MAX), - space: self.space, + LinearColor { + red: (self.red * rhs).clamp(LinearColor::MIN, LinearColor::MAX), + green: (self.green * rhs).clamp(LinearColor::MIN, LinearColor::MAX), + blue: (self.blue * rhs).clamp(LinearColor::MIN, LinearColor::MAX), } } } -impl Mul<Color> for f64 { - type Output = Color; +impl Mul<LinearColor> for f64 { + type Output = LinearColor; - fn mul(self, rhs: Color) -> Self::Output { + fn mul(self, rhs: LinearColor) -> Self::Output { rhs * self } } -impl Div<f64> for Color { - type Output = Color; +impl Mul<LinearColor> for LinearColor { + type Output = LinearColor; + + fn mul(self, rhs: LinearColor) -> Self::Output { + LinearColor { + red: (self.red * rhs.red).clamp(LinearColor::MIN, LinearColor::MAX), + green: (self.green * rhs.green).clamp(LinearColor::MIN, LinearColor::MAX), + blue: (self.blue * rhs.blue).clamp(LinearColor::MIN, LinearColor::MAX), + } + } +} + +impl Div<f64> for LinearColor { + type Output = LinearColor; fn div(self, rhs: f64) -> Self::Output { self * (1. / rhs) diff --git a/src/hittable.rs b/src/hittable.rs @@ -1,14 +1,17 @@ -use crate::{interval::Interval, ray::Ray, vec3::Vec3}; +use std::rc::Rc; + +use crate::{interval::Interval, material::Material, ray::Ray, vec3::Vec3}; pub struct HitRecord { point: Vec3, normal: Vec3, t: f64, + material: Rc<dyn Material>, _front_face: bool, } impl HitRecord { - fn new(t: f64, r: &Ray, outward_normal: &Vec3) -> HitRecord { + fn new(t: f64, r: &Ray, outward_normal: &Vec3, material: Rc<dyn Material>) -> HitRecord { let point = r.at(t); let outward_normal_unit = outward_normal.unit(); let front_face = r.direction().dot(&outward_normal_unit) < 0.; @@ -21,6 +24,7 @@ impl HitRecord { point, normal, t, + material, _front_face: front_face, } } @@ -30,6 +34,9 @@ impl HitRecord { pub fn point(&self) -> Vec3 { self.point } + pub fn material(&self) -> Rc<dyn Material> { + self.material.clone() + } } pub trait Hittable { @@ -39,13 +46,15 @@ pub trait Hittable { pub struct Sphere { center: Vec3, radius: f64, + material: Rc<dyn Material>, } impl Sphere { - pub const fn new(center: Vec3, radius: f64) -> Self { + pub const fn new(center: Vec3, radius: f64, material: Rc<dyn Material>) -> Self { Sphere { center, radius: radius.max(0.), + material, } } } @@ -74,7 +83,12 @@ impl Hittable for Sphere { } }; let outward_normal = (r.at(root) - self.center) / self.radius; - Some(HitRecord::new(root, r, &outward_normal)) + Some(HitRecord::new( + root, + r, + &outward_normal, + self.material.clone(), + )) } } diff --git a/src/main.rs b/src/main.rs @@ -1,6 +1,10 @@ +use std::rc::Rc; + use crate::{ camera::Camera, + color::LinearColor, hittable::{Hittable, Sphere}, + material::{Lambertian, Metal}, vec3::Vec3, }; @@ -8,15 +12,40 @@ mod camera; mod color; mod hittable; mod interval; +mod material; mod random; mod ray; mod vec3; fn main() { - 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 material_ground = Lambertian::new(LinearColor::new(0.8, 0.8, 0.)); + let material_center = Lambertian::new(LinearColor::new(0.1, 0.2, 0.5)); + let material_left = Metal::new(LinearColor::new(0.8, 0.8, 0.8), 0.3); + let material_right = Metal::new(LinearColor::new(0.8, 0.6, 0.2), 1.0); + + let world: Vec<Box<dyn Hittable>> = vec![ + Box::new(Sphere::new( + Vec3::new(0., -100.5, -1.), + 100., + Rc::new(material_ground), + )), + Box::new(Sphere::new( + Vec3::new(0., 0., -1.2), + 0.5, + Rc::new(material_center), + )), + Box::new(Sphere::new( + Vec3::new(-1., 0., -1.), + 0.5, + Rc::new(material_left), + )), + Box::new(Sphere::new( + Vec3::new(1., 0., -1.), + 0.5, + Rc::new(material_right), + )), ]; + let mut camera = Camera::new(400, 16. / 9., 100, 50); camera.render(&world); } diff --git a/src/material.rs b/src/material.rs @@ -0,0 +1,97 @@ +use crate::{ + color::LinearColor, hittable::HitRecord, random::RandomNumberGenerator, ray::Ray, vec3::Vec3, +}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct ScatteredRay { + ray: Ray, + attenuation: LinearColor, +} + +impl ScatteredRay { + pub const fn ray(&self) -> Ray { + self.ray + } + pub const fn attenuation(&self) -> LinearColor { + self.attenuation + } +} + +pub trait Material { + fn scatter( + &self, + r_in: &Ray, + hit: &HitRecord, + rng: &mut RandomNumberGenerator, + ) -> Option<ScatteredRay>; +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct Lambertian { + albedo: LinearColor, +} + +impl Lambertian { + pub const fn new(albedo: LinearColor) -> Self { + Lambertian { albedo } + } +} + +impl Material for Lambertian { + fn scatter( + &self, + _r_in: &Ray, + hit: &HitRecord, + rng: &mut RandomNumberGenerator, + ) -> Option<ScatteredRay> { + let scatter_direction = hit.normal() + Vec3::random_unit(rng); + Some(ScatteredRay { + ray: Ray::new( + hit.point(), + if scatter_direction.near_zero() { + hit.normal() + } else { + scatter_direction + }, + ), + attenuation: self.albedo, + }) + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct Metal { + albedo: LinearColor, + fuzz: f64, +} + +impl Metal { + pub const fn new(albedo: LinearColor, fuzz: f64) -> Self { + Metal { + albedo, + fuzz: fuzz.clamp(0., 1.), + } + } +} + +impl Material for Metal { + fn scatter( + &self, + r_in: &Ray, + hit: &HitRecord, + rng: &mut RandomNumberGenerator, + ) -> Option<ScatteredRay> { + let ray = Ray::new( + hit.point(), + r_in.direction().reflect(&hit.normal()).unit() + (self.fuzz * Vec3::random_unit(rng)), + ); + if ray.direction().dot(&hit.normal()) > 0. { + Some(ScatteredRay { + ray, + attenuation: self.albedo, + }) + } else { + None + } + } +} diff --git a/src/random.rs b/src/random.rs @@ -1,5 +1,6 @@ use std::time::SystemTime; +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] pub struct RandomNumberGenerator { state: u64, } diff --git a/src/vec3.rs b/src/vec3.rs @@ -12,7 +12,6 @@ pub struct Vec3 { z: f64, } -#[allow(dead_code)] impl Vec3 { pub const fn new(x: f64, y: f64, z: f64) -> Self { Vec3 { x, y, z } @@ -80,6 +79,15 @@ impl Vec3 { pub fn unit(&self) -> Vec3 { *self / self.length() } + + pub fn near_zero(&self) -> bool { + const THRESHOLD: f64 = 1e-8; + self.x().abs() < THRESHOLD && self.y().abs() < THRESHOLD && self.z().abs() < THRESHOLD + } + + pub fn reflect(&self, normal: &Vec3) -> Vec3 { + *self - 2. * self.dot(normal) * *normal + } } impl Add for Vec3 {