From 64cad7eda6c516d30c1b4b55bedf3569247e7f32 Mon Sep 17 00:00:00 2001 From: Djairo Date: Sun, 31 May 2026 13:51:34 +0200 Subject: [PATCH] ft: moved random scene code to function; cleaned up tri/quad/cube hit logic; made camera attributes private --- src/camera.rs | 12 +-- src/main.rs | 98 +-------------------- src/objects/hit.rs | 50 ++++++++++- src/objects/materials/texture.rs | 7 +- src/objects/quad.rs | 46 ++++------ src/objects/traits.rs | 3 +- src/objects/triangle.rs | 77 +++++------------ src/raytracer.rs | 23 ++--- src/scenes/material_def.rs | 2 +- src/scenes/scene.rs | 143 ++++++++++++++++++++++++++++--- src/vec3.rs | 19 ++-- 11 files changed, 259 insertions(+), 221 deletions(-) diff --git a/src/camera.rs b/src/camera.rs index 933d0b4..757f4f5 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,11 +1,11 @@ -use std::{f32::consts::PI}; +use std::f32::consts::PI; use crate::{ray::Ray, vec3::Vec3}; #[derive(Debug)] pub struct Camera { // raytracing - pub anti_alias_rate: u32, + anti_alias_rate: u32, pixel00_loc: Vec3, pixel_delta_u: Vec3, pixel_delta_v: Vec3, @@ -83,7 +83,7 @@ impl Camera { } } - pub fn init(&mut self, w: f32, hi: f32) { + pub fn init(&mut self, width: u32, height: u32) { if !self.dirty { return; } @@ -97,13 +97,13 @@ impl Camera { // viewport let viewport_height = 2. * h * self.focus_dist; - let viewport_width = viewport_height * (w / hi); + let viewport_width = viewport_height * (width as f32 / height as f32); let viewport_u = viewport_width * self.u; let viewport_v = viewport_height * -self.v; // variables - self.pixel_delta_u = viewport_u / w; - self.pixel_delta_v = viewport_v / hi; + self.pixel_delta_u = viewport_u / width; + self.pixel_delta_v = viewport_v / height; self.pixel00_loc = self.look_from - (self.focus_dist * self.w) - viewport_u / 2. - viewport_v / 2.; self.dirty = false; diff --git a/src/main.rs b/src/main.rs index c35bdd4..698be0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,26 +8,19 @@ mod scenes; mod vec3; use std::{env, fs}; -use std::sync::Arc; -use crate::camera::Camera; -use crate::objects::materials::dielectric::Dielectric; -use crate::objects::materials::lambertian::{Lambertian, Metal}; -use crate::objects::sphere::Sphere; -use crate::objects::traits::Hittable; use crate::ray::Ray; use crate::raytracer::render; use crate::scenes::scene::Scene; use crate::vec3::Vec3; use dotenv::dotenv; -use rand::RngExt; // TODO: implement scene serialization fn main() { dotenv().ok(); pretty_env_logger::init(); - // TODO: better cli parsing + // TODO: better cli parsing || add random flag to generate random scene let mut json_file = "./scenes/scene.json"; let args: Vec = env::args().collect(); if args.len() > 1 { @@ -36,93 +29,6 @@ fn main() { let json_str = fs::read_to_string(json_file).expect("Reading specified scene file failed!"); let mut scene: Scene = serde_json::from_str(&json_str).unwrap(); + let mut scene: Scene = Scene::random(); render(&mut scene); - return; - - // random spheres code; thought: make this available as cli flag? - let ground = Lambertian::rgb(-2.5, 0.5, 0.5, 1.); - let mut world: Vec> = vec![Arc::new(Sphere::xyz( - 0., - -1000., - 0., - 1000., - Arc::new(ground), - ))]; - - let mut rng = rand::rng(); - let point = Vec3::new(4., 0.2, 0.); - for a in -11..11 { - for b in -11..11 { - let mat = rng.random_range((0.)..1.); - let center = Vec3::new( - a as f32 + 0.9 * rng.random_range((0.)..1.), - 0.2, - b as f32 + 0.9 * rng.random_range((0.)..1.), - ); - - if (center - point).length() > 0.9 { - if mat < 0.8 { - // diffuse - world.push(Arc::new(Sphere::new( - center, - 0.2, - Arc::new(Lambertian::new(Vec3::random() * Vec3::random(), 1.)), - ))); - } else if mat < 0.95 { - // metal - world.push(Arc::new(Sphere::new( - center, - 0.2, - Arc::new(Metal::rgb( - rng.random_range(0.5..1.), - rng.random_range(0.5..1.), - rng.random_range(0.5..1.), - 1., - rng.random_range((0.)..0.5), - )), - ))); - } else { - // glass - world.push(Arc::new(Sphere::new( - center, - 0.2, - Arc::new(Dielectric::new(1.5)), - ))); - } - } - } - } - - world.push(Arc::new(Sphere::xyz( - 0., - 1., - 0., - 1., - Arc::new(Dielectric::new(1.5)), - ))); - world.push(Arc::new(Sphere::xyz( - -4., - 1., - 0., - 1., - Arc::new(Lambertian::rgb(0.4, 0.2, 0.1, 1.)), - ))); - world.push(Arc::new(Sphere::xyz( - 4., - 1., - 0., - 1., - Arc::new(Metal::rgb(0.7, 0.6, 0.5, 1., 0.)), - ))); - - let mut c = Camera::new(); - c.set_fov(20.); - c.set_anti_alias_rate(23); - c.set_vup(Vec3::new(0., 1., 0.)); - c.set_look_from(Vec3::new(13., 2., 3.)); - c.set_look_at(Vec3::new(0., 0., 0.)); - c.add_defocus_blur(0.6, 10.); - - let mut s = Scene::new(c, world, "output.png".to_string(), 1920, 1080, 50); - render(&mut s); } diff --git a/src/objects/hit.rs b/src/objects/hit.rs index 9284603..b2540a4 100644 --- a/src/objects/hit.rs +++ b/src/objects/hit.rs @@ -17,7 +17,15 @@ pub struct Hit { } impl Hit { - pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc, front_face: bool, u: f32, v: f32) -> Self { + pub fn new( + t: f32, + p: Vec3, + n: Vec3, + mat: Arc, + front_face: bool, + u: f32, + v: f32, + ) -> Self { Self { t, p, @@ -25,7 +33,26 @@ impl Hit { mat, front_face, u, - v + v, + } + } + + pub fn from_minimal( + mh: MinimalHit, + n: Vec3, + mat: Arc, + front_face: bool, + u: f32, + v: f32, + ) -> Self { + Self { + t: mh.t, + p: mh.p, + n, + mat, + front_face, + u, + v, } } @@ -69,3 +96,22 @@ impl Hit { closest } } + +pub struct MinimalHit { + t: f32, + p: Vec3, +} + +impl MinimalHit { + pub fn new(t: f32, p: Vec3) -> Self { + Self { t, p } + } + + pub fn t(&self) -> f32 { + self.t + } + + pub fn p(&self) -> &Vec3 { + &self.p + } +} diff --git a/src/objects/materials/texture.rs b/src/objects/materials/texture.rs index b4dae8e..87dc559 100644 --- a/src/objects/materials/texture.rs +++ b/src/objects/materials/texture.rs @@ -7,6 +7,11 @@ use crate::{ vec3::Vec3, }; +// TODO: overhaul texture implementation so we can support textures for arbitrary materials. +// Basically remove this struct, and instead make lambertian/metal/dielectric/take either a texture +// or a flat colour +// I think we can use an enum ColourSource with a function to fetch a specific colour depending on +// if we have a texture/flat colour. and yeah. #[derive(Debug)] pub struct Texture { source: DynamicImage, @@ -48,7 +53,7 @@ impl Texture { } fn _idx(&self, u: f32, v: f32) -> usize { - let x: usize = (u * ((self.width - 1) as f32)) as usize; // TODO: check these calcs work + let x: usize = (u * ((self.width - 1) as f32)) as usize; let y: usize = (v * ((self.height - 1) as f32)) as usize; y * self.width as usize + x diff --git a/src/objects/quad.rs b/src/objects/quad.rs index fe77756..b9be635 100644 --- a/src/objects/quad.rs +++ b/src/objects/quad.rs @@ -13,6 +13,10 @@ pub struct Quad { p4: Vec3, material: Arc, normal: Vec3, + + // helpers + t1: Triangle, + t2: Triangle, } impl Quad { @@ -22,27 +26,12 @@ impl Quad { p2, p3, p4, - material, + material: material.clone(), normal: (p2 - p1).cross(&(p4 - p1)).get_unit(), + t1: Triangle::new(p1, p2, p4, material.clone()), + t2: Triangle::new(p2, p3, p4, material), } } - - pub fn hit( - p1: Vec3, - p2: Vec3, - p3: Vec3, - p4: Vec3, - material: Arc, - normal: Vec3, - r: &Ray, - ) -> Option { - let isct1 = Triangle::hit(p1, p2, p4, material.clone(), normal, r); - let isct2 = Triangle::hit(p2, p3, p4, material.clone(), normal, r); - if isct1.is_some() { - return isct1; - } - isct2 - } } impl Debug for Quad { @@ -57,20 +46,19 @@ impl Debug for Quad { } } -// FIXME: texture mapping does not work properly for quads impl Hittable for Quad { fn hit(&self, r: &Ray) -> Option { - let hit = Quad::hit( - self.p1, - self.p2, - self.p3, - self.p4, - self.material.clone(), - self.normal, - r, - ); + let mh = self.t1.calculate_hit(r).or(self.t2.calculate_hit(r))?; + let uvw = self.to_uv(mh.p()); - return hit + Some(Hit::from_minimal( + mh, + self.normal, + self.material.clone(), + false, + *uvw.x(), + *uvw.y(), + )) } fn to_uv(&self, point: &Vec3) -> Vec3 { diff --git a/src/objects/traits.rs b/src/objects/traits.rs index b65803b..5e332bb 100644 --- a/src/objects/traits.rs +++ b/src/objects/traits.rs @@ -5,7 +5,6 @@ use crate::objects::hit::Hit; use crate::vec3::Vec3; pub trait Hittable: Debug + Send + Sync { + fn to_uv(&self, point: &Vec3) -> Vec3; fn hit(&self, r: &Ray) -> Option; - fn to_uv(&self, point: &Vec3) -> Vec3; // TODO: overhaul; remove u,v in Hit and change Mat ref to - // Hittable ref. Then call this function to calculate u,v coords as necessary. } diff --git a/src/objects/triangle.rs b/src/objects/triangle.rs index 9c25ce3..fd98090 100644 --- a/src/objects/triangle.rs +++ b/src/objects/triangle.rs @@ -1,4 +1,4 @@ -use crate::objects::hit::Hit; +use crate::objects::hit::{Hit, MinimalHit}; use crate::objects::traits::Hittable; use crate::ray::Ray; use crate::{objects::materials::traits::Material, vec3::Vec3}; @@ -19,6 +19,7 @@ pub struct Triangle { d01: f32, d11: f32, denom: f32, + a4: f32, } impl Triangle { @@ -29,6 +30,7 @@ impl Triangle { let d00 = v0.dot(&v0); let d01 = v0.dot(&v1); let d11 = v1.dot(&v1); + let a4 = (p3 - p1).cross(&(p2 - p1)).length(); Self { p1, @@ -42,69 +44,33 @@ impl Triangle { d01, d11, denom: d00 * d11 - d01 * d01, + a4, } } - // TODO: make the global Triangle::hit function not take precomped u,v somehow - // FIXME/DEBT: this function recalculates values for each pass that can be cached on the - // triangle struct itself. REFACTOR to not use this function - pub fn to_uv(p1: &Vec3, p2: &Vec3, p3: &Vec3, point: &Vec3) -> Vec3 { - let v0 = *p2 - p1; - let v1 = *p3 - p1; - let v2 = *point - p1; - let d00 = v0.dot(&v0); - let d01 = v0.dot(&v1); - let d11 = v1.dot(&v1); - let denom = d00 * d11 - d01 * d01; - - let d20 = v2.dot(&v0); - let d21 = v2.dot(&v1); - - let v = (d11 * d20 - d01 * d21) / denom; - let w = (d00 * d21 - d01 * d20) / denom; - Vec3::new(1. - v - w, v, w) - } - - pub fn hit( - p1: Vec3, - p2: Vec3, - p3: Vec3, - material: Arc, - normal: Vec3, - r: &Ray, - ) -> Option { + pub fn calculate_hit(&self, r: &Ray) -> Option { // check if ray parallel to plane - let dot = normal.dot(r.dir()); + let dot = self.normal.dot(r.dir()); if dot == 0.0 { return None; } - let d = (-normal).dot(&p1); + let d = (-self.normal).dot(&self.p1); // hitpoint on plane - let t = -(normal.dot(&(r.origin() + d))) / dot; + let t = -(self.normal.dot(&(r.origin() + d))) / dot; // hits behind camera if t < 0. { return None; }; - let uvw = Triangle::to_uv(&p1, &p2, &p3, &r.at(t)); let p = r.at(t); - let a4 = (p3 - p1).cross(&(p2 - p1)).length(); - let a3 = (p3 - p).cross(&(p2 - p)).length(); - let a2 = (p3 - p).cross(&(p1 - p)).length(); - let a1 = (p2 - p).cross(&(p1 - p)).length(); + let a3 = (self.p3 - p).cross(&(self.p2 - p)).length(); + let a2 = (self.p3 - p).cross(&(self.p1 - p)).length(); + let a1 = (self.p2 - p).cross(&(self.p1 - p)).length(); - let diff = (a4 - a1 - a2 - a3).abs(); + let diff = (self.a4 - a1 - a2 - a3).abs(); if diff < 0.001 { - Some(Hit::new( - t, - p, - normal, - material, - normal.dot(r.dir()) < 0., - *uvw.x(), - *uvw.y(), - )) + Some(MinimalHit::new(t, p)) } else { None } @@ -124,14 +90,17 @@ impl Debug for Triangle { impl Hittable for Triangle { fn hit(&self, r: &Ray) -> Option { - Triangle::hit( - self.p1, - self.p2, - self.p3, - self.material.clone(), + let mh = self.calculate_hit(r)?; + let uvw = self.to_uv(mh.p()); + + Some(Hit::from_minimal( + mh, self.normal, - r, - ) + self.material.clone(), + self.normal.dot(r.dir()) < 0., + *uvw.x(), + *uvw.y(), + )) } fn to_uv(&self, point: &Vec3) -> Vec3 { diff --git a/src/raytracer.rs b/src/raytracer.rs index 5f60cec..821c49a 100644 --- a/src/raytracer.rs +++ b/src/raytracer.rs @@ -27,9 +27,9 @@ pub fn render(scene: &mut Scene) { scene.init(); // TODO: currently splits per vertical line, but could be more granular (per chunk) - let mut pixels = vec![0_u8; (scene.image_width * scene.image_height * 3) as usize]; + let mut pixels = vec![0_u8; (scene.get_image_width() * scene.get_image_height() * 3) as usize]; let scanlines: Vec<(usize, &mut [u8])> = pixels - .chunks_mut(scene.image_width as usize * 3) + .chunks_mut(scene.get_image_width() as usize * 3) .enumerate() .collect(); @@ -37,14 +37,18 @@ pub fn render(scene: &mut Scene) { scanlines.into_par_iter().for_each(|(i, chunk)| { render_chunk(chunk, scene, i as u32); }); - info!("rendering took {}.{} seconds.", now.elapsed().as_secs(), now.elapsed().subsec_nanos()); + info!( + "rendering took {}.{} seconds.", + now.elapsed().as_secs(), + now.elapsed().subsec_nanos() + ); info!("Writing image file..."); write_image( - &scene.filename, + &scene.get_filename(), &pixels, - scene.image_width, - scene.image_height, + scene.get_image_width() as u32, + scene.get_image_height() as u32, ); } @@ -52,19 +56,18 @@ pub fn render_chunk(chunk: &mut [u8], scene: &Scene, y: u32) { let camera = &scene.get_camera(); let aa_rate = &scene.get_camera().get_anti_alias_rate(); - for i in 0..scene.image_width { + for i in 0..scene.get_image_width() { let pixel_tl = camera.get_pixel_tl(i, y); let mut pixel_colour = Colour::default(); for y in 1..(aa_rate + 1) { for x in 1..(aa_rate + 1) { let r = camera.get_ray(pixel_tl, x, y); - pixel_colour += ray_colour(&scene.objects, &r, scene.max_depth); + pixel_colour += ray_colour(&scene.get_objects(), &r, scene.get_max_depth()); } } - let (r, g, b) = - (pixel_colour / (aa_rate * aa_rate) as f32).output(); + let (r, g, b) = (pixel_colour / (aa_rate * aa_rate) as f32).output(); chunk[(i * 3) as usize] = r; chunk[(i * 3) as usize + 1] = g; chunk[(i * 3) as usize + 2] = b; diff --git a/src/scenes/material_def.rs b/src/scenes/material_def.rs index 2618a7b..87a73f6 100644 --- a/src/scenes/material_def.rs +++ b/src/scenes/material_def.rs @@ -22,7 +22,7 @@ impl MaterialDef { MaterialDef::Metal(m) => Arc::new(m), MaterialDef::Dielectric(d) => Arc::new(d), MaterialDef::Normal(n) => Arc::new(n), - MaterialDef::Texture(t) => Arc::new(Texture::new(&t.source)), // FIXME: error handling + MaterialDef::Texture(t) => Arc::new(Texture::new(&t.source)), } } } diff --git a/src/scenes/scene.rs b/src/scenes/scene.rs index ef9c8b1..9a5f909 100644 --- a/src/scenes/scene.rs +++ b/src/scenes/scene.rs @@ -1,23 +1,35 @@ use std::sync::Arc; +use rand::RngExt; use serde::Deserialize; use crate::{ camera::Camera, - objects::{materials::traits::Material, traits::Hittable}, + objects::{ + materials::{ + dielectric::Dielectric, + lambertian::{Lambertian, Metal}, + traits::Material, + }, + sphere::Sphere, + traits::Hittable, + }, scenes::{hittable_def::HittableDef, material_def::MaterialDef, raw_camera::RawCamera}, + vec3::Vec3, }; #[derive(Debug)] pub struct Scene { - pub camera: Camera, // FIXME: should not be public - pub objects: Vec>, + camera: Camera, + objects: Vec>, + // image - pub filename: String, - pub image_width: u32, - pub image_height: u32, + filename: String, + image_width: u32, + image_height: u32, + // raytracing // TODO: think about organisation of these vars, also in Camera - pub max_depth: u32, + max_depth: u32, } impl Scene { @@ -39,20 +51,127 @@ impl Scene { } } + pub fn random() -> Self { + let ground = Lambertian::rgb(-2.5, 0.5, 0.5, 1.); + let mut world: Vec> = vec![Arc::new(Sphere::xyz( + 0., + -1000., + 0., + 1000., + Arc::new(ground), + ))]; + + let mut rng = rand::rng(); + let point = Vec3::new(4., 0.2, 0.); + for a in -11..11 { + for b in -11..11 { + let mat = rng.random_range((0.)..1.); + let center = Vec3::new( + a as f32 + 0.9 * rng.random_range((0.)..1.), + 0.2, + b as f32 + 0.9 * rng.random_range((0.)..1.), + ); + + if (center - point).length() > 0.9 { + if mat < 0.8 { + // diffuse + world.push(Arc::new(Sphere::new( + center, + 0.2, + Arc::new(Lambertian::new(Vec3::random() * Vec3::random(), 1.)), + ))); + } else if mat < 0.95 { + // metal + world.push(Arc::new(Sphere::new( + center, + 0.2, + Arc::new(Metal::rgb( + rng.random_range(0.5..1.), + rng.random_range(0.5..1.), + rng.random_range(0.5..1.), + 1., + rng.random_range((0.)..0.5), + )), + ))); + } else { + // glass + world.push(Arc::new(Sphere::new( + center, + 0.2, + Arc::new(Dielectric::new(1.5)), + ))); + } + } + } + } + + world.push(Arc::new(Sphere::xyz( + 0., + 1., + 0., + 1., + Arc::new(Dielectric::new(1.5)), + ))); + world.push(Arc::new(Sphere::xyz( + -4., + 1., + 0., + 1., + Arc::new(Lambertian::rgb(0.4, 0.2, 0.1, 1.)), + ))); + world.push(Arc::new(Sphere::xyz( + 4., + 1., + 0., + 1., + Arc::new(Metal::rgb(0.7, 0.6, 0.5, 1., 0.)), + ))); + + let mut c = Camera::new(); + c.set_fov(20.); + c.set_anti_alias_rate(2); + c.set_vup(Vec3::new(0., 1., 0.)); + c.set_look_from(Vec3::new(13., 2., 3.)); + c.set_look_at(Vec3::new(0., 0., 0.)); + c.add_defocus_blur(0.6, 10.); + + Self { + camera: c, + objects: world, + filename: "random.png".to_string(), + image_width: 1920, + image_height: 1080, + max_depth: 50, + } + } + pub fn get_camera(&self) -> &Camera { &self.camera } - pub fn get_image_width(&self) -> f32 { - self.image_width as f32 + pub fn get_image_width(&self) -> u32 { + self.image_width } - pub fn get_image_height(&self) -> f32 { - self.image_height as f32 + pub fn get_image_height(&self) -> u32 { + self.image_height + } + + pub fn get_objects(&self) -> &Vec> { + &self.objects + } + + pub fn get_max_depth(&self) -> u32 { + self.max_depth + } + + pub fn get_filename(&self) -> &String { + &self.filename } pub fn init(&mut self) { - self.camera.init(self.get_image_width(), self.get_image_height()); + self.camera + .init(self.get_image_width(), self.get_image_height()); } } diff --git a/src/vec3.rs b/src/vec3.rs index a2466cb..48ec86a 100644 --- a/src/vec3.rs +++ b/src/vec3.rs @@ -33,7 +33,7 @@ impl Vec3 { Self { x: r as f32 / 255., y: g as f32 / 255., - z: b as f32/ 255., + z: b as f32 / 255., } } @@ -444,16 +444,20 @@ impl Div for Vec3 { } } } + +impl Div for Vec3 { + type Output = Self; + + fn div(self, u: u32) -> Self::Output { + self / u as f32 + } +} + impl Div for Vec3 { type Output = Self; fn div(self, i: i32) -> Self::Output { - let f: f32 = i as f32; - Self { - x: self.x / f, - y: self.y / f, - z: self.z / f, - } + self / i as f32 } } @@ -500,4 +504,3 @@ impl Display for Vec3 { write!(f, "({}, {}, {})", self.x, self.y, self.z) } } -