From b756cc394acbc03460bce2d8c76c39e4607f94ec Mon Sep 17 00:00:00 2001 From: djairoh Date: Thu, 16 Apr 2026 14:04:55 +0200 Subject: [PATCH] ft: 10: metals --- Cargo.lock | 10 ++++ Cargo.toml | 1 + src/camera.rs | 15 +++--- src/main.rs | 19 +++++-- src/objects.rs | 1 + src/objects/hit.rs | 22 ++++++-- src/objects/materials.rs | 2 + src/objects/materials/lambertian.rs | 79 +++++++++++++++++++++++++++++ src/objects/materials/traits.rs | 5 ++ src/objects/sphere.rs | 20 ++++++-- src/ray.rs | 12 ++--- src/vec3.rs | 54 ++++++++++++++------ 12 files changed, 199 insertions(+), 41 deletions(-) create mode 100644 src/objects/materials.rs create mode 100644 src/objects/materials/lambertian.rs create mode 100644 src/objects/materials/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 31a7e18..936d2e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,6 +674,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_close" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d49e01f14871e71a0dce6f09eb72308bca74a4adb1bab5a8c34ee26979a8a2" +dependencies = [ + "num-traits", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1219,6 +1228,7 @@ name = "raytracing" version = "0.1.0" dependencies = [ "image", + "is_close", "log", "ops", "pretty_env_logger", diff --git a/Cargo.toml b/Cargo.toml index dc98c47..425f8a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] image = "0.25.10" +is_close = "0.1.3" log = "0.4.29" ops = "0.6.0" pretty_env_logger = "0.5.0" diff --git a/src/camera.rs b/src/camera.rs index 15782f2..0047283 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -20,10 +20,7 @@ pub struct Camera { } impl Camera { - pub fn new(aspect_ratio: f32, image_width: u32) -> Camera { - Camera::new_aa(aspect_ratio, image_width, 1) - } - pub fn new_aa(aspect_ratio: f32, image_width: u32, anti_alias_rate: u32) -> Camera { + pub fn new(aspect_ratio: f32, image_width: u32, anti_alias_rate: u32, max_depth: u32) -> Self { let image_height = max(1, (image_width as f32 / aspect_ratio) as u32); //camera @@ -42,11 +39,11 @@ impl Camera { let pixel00_loc = camera_center - Vec3::new(0., 0., focal_length) - viewport_u / 2 - viewport_v / 2; - Camera { + Self { image_width: image_width, image_height: image_height, anti_alias_rate: anti_alias_rate, - max_depth: 50, + max_depth: max_depth, center: camera_center, pixel00_loc: pixel00_loc, pixel_delta_u: pixel_delta_u, @@ -93,8 +90,10 @@ impl Camera { let closest = Hit::hit_list(hittables, r); if let Some(hit) = closest { - let dir = *hit.n() + Vec3::random_unit_hemisphere(hit.n()); - return 0.5 * self.ray_colour(hittables, &Ray::new(*hit.p(), dir), depth - 1); + if let Some((scattered, att)) = hit.mat().scatter(&hit, r) { + return att * self.ray_colour(hittables, &scattered, depth - 1); + } + return Colour::nil(); } // background diff --git a/src/main.rs b/src/main.rs index e3ba50d..12af2af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,10 @@ mod objects; mod ray; mod vec3; +use std::sync::Arc; + use crate::camera::Camera; +use crate::objects::materials::lambertian::{Lambertian, Metal}; use crate::objects::sphere::Sphere; use crate::ray::Ray; use crate::vec3::Vec3; @@ -15,10 +18,18 @@ fn main() { pretty_env_logger::init(); // setup objects - let mut world = vec![Sphere::new(Vec3::new(0., 0., -1.), 0.5)]; - world.push(Sphere::new(Vec3::new(0., 0., -0.1), 0.01)); - world.push(Sphere::new(Vec3::new(0., -100.5, -1.), 100.)); + let metal = Arc::new(Metal::rgb(0.7, 0.4, 0.2, 1., 0.1)); + let ground = Arc::new(Lambertian::rgb(0.8, 0.8, 0., 1.0)); + let center = Arc::new(Lambertian::rgb(0.1, 0.2, 0.5, 1.)); + let left = Arc::new(Metal::rgb(0.8, 0.8, 0.8, 1., 0.3)); + let right = Arc::new(Metal::rgb(0.8, 0.6, 0.2, 1., 1.0)); - let c = Camera::new_aa(16. / 9., 400, 3); + let mut world = vec![Sphere::xyz(0., 0.5, -0.8, 0.1, metal.clone())]; + world.push(Sphere::xyz(0., -100.5, -1., 100., ground.clone())); + world.push(Sphere::xyz(0., 0., -1.2, 0.5, center.clone())); + world.push(Sphere::xyz(-1., 0., -1.0, 0.5, left.clone())); + world.push(Sphere::xyz(1., 0., -1.0, 0.5, right.clone())); + + let c = Camera::new(16. / 9., 800, 2, 50); c.render(&world); } diff --git a/src/objects.rs b/src/objects.rs index b7c1286..be146ee 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -1,3 +1,4 @@ pub mod hit; +pub mod materials; pub mod sphere; pub mod traits; diff --git a/src/objects/hit.rs b/src/objects/hit.rs index 529bd58..633510e 100644 --- a/src/objects/hit.rs +++ b/src/objects/hit.rs @@ -1,14 +1,26 @@ -use crate::{objects::traits::Hittable, ray::Ray, vec3::Vec3}; +use std::sync::Arc; + +use crate::{ + objects::{materials::traits::Material, traits::Hittable}, + ray::Ray, + vec3::Vec3, +}; pub struct Hit { t: f32, p: Vec3, n: Vec3, + mat: Arc, } impl Hit { - pub fn new(t: f32, p: Vec3, n: Vec3) -> Hit { - Hit { t: t, p: p, n: n } + pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc) -> Self { + Self { + t: t, + p: p, + n: n, + mat: mat, + } } pub fn t(&self) -> &f32 { @@ -23,6 +35,10 @@ impl Hit { &self.n } + pub fn mat(&self) -> Arc { + self.mat.clone() + } + pub fn hit_list(hittables: &Vec, r: &Ray) -> Option { let mut closest: Option = None; for hittable in hittables { diff --git a/src/objects/materials.rs b/src/objects/materials.rs new file mode 100644 index 0000000..39a34e0 --- /dev/null +++ b/src/objects/materials.rs @@ -0,0 +1,2 @@ +pub mod lambertian; +pub mod traits; diff --git a/src/objects/materials/lambertian.rs b/src/objects/materials/lambertian.rs new file mode 100644 index 0000000..127298e --- /dev/null +++ b/src/objects/materials/lambertian.rs @@ -0,0 +1,79 @@ +use rand::RngExt; + +use crate::{ + objects::{hit::Hit, materials::traits::Material}, + ray::Ray, + vec3::{Colour, Vec3}, +}; + +pub struct Lambertian { + albedo: Colour, + prob: f32, +} + +impl Lambertian { + pub fn new(albedo: Colour, prob: f32) -> Self { + Self { + albedo: albedo, + prob: prob, + } + } + + pub fn rgb(r: f32, g: f32, b: f32, prob: f32) -> Self { + Self { + albedo: Colour::new(r, g, b), + prob: prob, + } + } +} + +impl Material for Lambertian { + fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)> { + let mut rng = rand::rng(); + if self.prob >= rng.random::() { + let mut dir = *hit.n() + Vec3::random_unit(); + if dir.near_zero() { + dir = *hit.n(); + } + let scattered = Ray::new(*hit.p(), dir); + return Some((scattered, self.albedo)); + } + return None; + } +} + +pub struct Metal { + albedo: Colour, + prob: f32, + fuzz: f32, +} + +impl Metal { + pub fn new(albedo: Colour, prob: f32, fuzz: f32) -> Self { + Self { + albedo: albedo, + prob: prob, + fuzz: fuzz, + } + } + + pub fn rgb(r: f32, g: f32, b: f32, prob: f32, fuzz: f32) -> Self { + Self { + albedo: Colour::new(r, g, b), + prob: prob, + fuzz: fuzz, + } + } +} + +impl Material for Metal { + fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)> { + let mut rng = rand::rng(); + if self.prob >= rng.random::() { + let mut refl = ray.dir().reflect(hit.n()); + refl = refl.get_unit() + self.fuzz * Vec3::random_unit(); + return Some((Ray::new(*hit.p(), refl), self.albedo)); + } + return None; + } +} diff --git a/src/objects/materials/traits.rs b/src/objects/materials/traits.rs new file mode 100644 index 0000000..a2024ea --- /dev/null +++ b/src/objects/materials/traits.rs @@ -0,0 +1,5 @@ +use crate::{objects::hit::Hit, ray::Ray, vec3::Colour}; + +pub trait Material { + fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)>; +} diff --git a/src/objects/sphere.rs b/src/objects/sphere.rs index ab52fb9..19bdbee 100644 --- a/src/objects/sphere.rs +++ b/src/objects/sphere.rs @@ -1,19 +1,31 @@ use core::f32::math::sqrt; +use std::sync::Arc; use crate::objects::hit::Hit; +use crate::objects::materials::traits::Material; use crate::objects::traits::Hittable; use crate::Vec3; pub struct Sphere { center: Vec3, radius: f32, + material: Arc, } impl Sphere { - pub fn new(center: Vec3, radius: f32) -> Sphere { - Sphere { + pub fn new(center: Vec3, r: f32, mat: Arc) -> Self { + Self { center: center, - radius: radius, + radius: r, + material: mat, + } + } + + pub fn xyz(x: f32, y: f32, z: f32, r: f32, mat: Arc) -> Self { + Self { + center: Vec3::new(x, y, z), + radius: r, + material: mat, } } } @@ -31,7 +43,7 @@ impl Hittable for Sphere { } else { let t = (h - sqrt(d)) / a; let p = r.at(t); - Some(Hit::new(t, p, self.normal_at(&p))) + Some(Hit::new(t, p, self.normal_at(&p), self.material.clone())) } } diff --git a/src/ray.rs b/src/ray.rs index 80f23da..7cfea1e 100644 --- a/src/ray.rs +++ b/src/ray.rs @@ -6,17 +6,17 @@ pub struct Ray { } impl Ray { - pub fn at(&self, t: f32) -> Vec3 { - self.origin + t * self.dir - } - - pub fn new(origin: Vec3, dir: Vec3) -> Ray { - Ray { + pub fn new(origin: Vec3, dir: Vec3) -> Self { + Self { origin: origin, dir: dir, } } + pub fn at(&self, t: f32) -> Vec3 { + self.origin + t * self.dir + } + pub fn origin(&self) -> &Vec3 { &self.origin } diff --git a/src/vec3.rs b/src/vec3.rs index a1b3a15..6b355b9 100644 --- a/src/vec3.rs +++ b/src/vec3.rs @@ -1,11 +1,11 @@ use core::f32::math::sqrt; +use is_close::default; +use rand::RngExt; use std::{ fmt::Display, ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; -use rand::RngExt; - #[derive(Copy, Clone)] pub struct Vec3 { r: f32, @@ -16,11 +16,11 @@ pub struct Vec3 { pub type Colour = Vec3; impl Vec3 { - pub fn new(r: f32, g: f32, b: f32) -> Vec3 { + pub fn new(r: f32, g: f32, b: f32) -> Self { Self { r: r, g: g, b: b } } - pub fn nil() -> Vec3 { + pub fn nil() -> Self { Self { r: 0., g: 0., @@ -28,26 +28,26 @@ impl Vec3 { } } - pub fn random() -> Vec3 { + pub fn random() -> Self { let mut rng = rand::rng(); - Vec3 { + Self { r: rng.random_range(-1.0..1.), g: rng.random_range(-1.0..1.), b: rng.random_range(-1.0..1.), } } - pub fn random_unit() -> Vec3 { + pub fn random_unit() -> Self { loop { - let v = Vec3::random(); + let v = Self::random(); if v.length_squared() <= 1. { return v / v.length(); } } } - pub fn random_unit_hemisphere(n: &Vec3) -> Vec3 { - let v = Vec3::random_unit(); + pub fn random_unit_hemisphere(n: &Self) -> Self { + let v = Self::random_unit(); if n.dot(&v) > 0.0 { v } else { @@ -75,12 +75,12 @@ impl Vec3 { (self.r * self.r + self.g * self.g + self.b * self.b) as f32 } - pub fn dot(&self, other: &Vec3) -> f32 { + pub fn dot(&self, other: &Self) -> f32 { self.r * other.r + self.g * other.g + self.b * other.b } - pub fn cross(&self, other: &Vec3) -> Vec3 { - Vec3 { + pub fn cross(&self, other: &Self) -> Self { + Self { r: self.g * other.b - self.b * other.g, g: self.b * other.r - self.r * other.b, b: self.r * other.g - self.g * other.r, @@ -91,10 +91,20 @@ impl Vec3 { self /= self.length(); } - pub fn get_unit(self) -> Vec3 { + pub fn get_unit(self) -> Self { self / self.length() } + pub fn near_zero(&self) -> bool { + default().is_close(self.r, 0.) + && default().is_close(self.g, 0.) + && default().is_close(self.b, 0.) + } + + pub fn reflect(&self, o: &Vec3) -> Vec3 { + *self - 2. * self.dot(o) * o + } + pub fn output(self) -> image::Rgb { // gamma correction let r = if self.r > 0. { @@ -120,8 +130,8 @@ impl Vec3 { image::Rgb([ir, ig, ib]) } - pub fn clone(&self) -> Vec3 { - Vec3 { + pub fn clone(&self) -> Self { + Self { r: self.r, g: self.g, b: self.b, @@ -289,6 +299,18 @@ impl Mul for f32 { } } +impl Mul<&Vec3> for f32 { + type Output = Vec3; + + fn mul(self, rhs: &Vec3) -> Self::Output { + Vec3 { + r: rhs.r * self, + g: rhs.g * self, + b: rhs.b * self, + } + } +} + impl MulAssign for Vec3 { fn mul_assign(&mut self, rhs: Self) { *self = Self {