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:
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 {