From fa31d18c12df55a01a8d544a9003e426eca55c2b Mon Sep 17 00:00:00 2001 From: Adam French Date: Sun, 8 Mar 2026 20:13:34 +0000 Subject: [PATCH] 1. Camera movable with mouse and keyboard 2. GUI runs in separate thread 3. Improvement to GUI widgets 4. Fixes to BVH --- rhai/bvh_test.rhai | 81 ++++++++++++ src/bvh.rs | 85 +++++------- src/camera.rs | 60 +++++++++ src/gui.rs | 190 ++++++++++++++++++-------- src/node.rs | 4 +- src/primitive.rs | 125 ++++++++++-------- src/ray.rs | 60 ++++----- src/state.rs | 322 +++++++++++++++++++++++++++++++-------------- 8 files changed, 629 insertions(+), 298 deletions(-) create mode 100644 rhai/bvh_test.rhai diff --git a/rhai/bvh_test.rhai b/rhai/bvh_test.rhai new file mode 100644 index 0000000..9ec9095 --- /dev/null +++ b/rhai/bvh_test.rhai @@ -0,0 +1,81 @@ +// BVH Stress Test Scene - Grid of spheres +let scene = Scene(); + +// Camera +let camera = Camera(P(0.0, 2.0, 5.0), P(0.0, 0.0, 0.0), V(0.0, 1.0, 0.0)); +scene.addCamera("main", camera); + +// Lights +let light = Light(P(3.0, 5.0, 3.0), V(0.8, 0.8, 0.8), V(0.05, 0.05, 0.05)); +light.active(true); +scene.addLight("key", light); + +let light2 = Light(P(-3.0, 4.0, -2.0), V(0.4, 0.4, 0.6), V(0.05, 0.05, 0.05)); +light2.active(true); +scene.addLight("fill", light2); + +let ambient = Ambient(V(0.1, 0.1, 0.1)); +ambient.active(true); +scene.addLight("ambient", ambient); + +// Materials +let kd = V(0.8, 0.2, 0.2); +let ks = V(0.3, 0.3, 0.3); +let kr = V(0.0, 0.0, 0.0); +let red = Material(kd, ks, kr, 10.0); +scene.addMaterial("red", red); + +let kd = V(0.2, 0.2, 0.8); +let ks = V(0.3, 0.3, 0.3); +let kr = V(0.0, 0.0, 0.0); +let blue = Material(kd, ks, kr, 10.0); +scene.addMaterial("blue", blue); + +let kd = V(0.2, 0.8, 0.2); +let ks = V(0.3, 0.3, 0.3); +let kr = V(0.0, 0.0, 0.0); +let green = Material(kd, ks, kr, 10.0); +scene.addMaterial("green", green); + +let kd = V(0.8, 0.8, 0.2); +let ks = V(0.3, 0.3, 0.3); +let kr = V(0.0, 0.0, 0.0); +let yellow = Material(kd, ks, kr, 10.0); +scene.addMaterial("yellow", yellow); + +let kd = V(0.8, 0.4, 0.8); +let ks = V(0.3, 0.3, 0.3); +let kr = V(0.1, 0.1, 0.1); +let purple = Material(kd, ks, kr, 15.0); +scene.addMaterial("purple", purple); + +// Floor +let floor = RectangleUnit(); +let floor_node = Node(floor, green); +floor_node.rotate(-90.0, 0.0, 0.0); +floor_node.translate(0.0, -1.5, 0.0); +floor_node.scale(5.0, 5.0, 1.0); +scene.addNode("floor", floor_node); + +// Grid of spheres: 5 x 4 x 5 = 100 spheres +let materials = [red, blue, green, yellow, purple]; +let count = 0; + +for x in range(-2, 3) { + for y in range(0, 4) { + for z in range(-2, 3) { + let mat_idx = count % 5; + let mat = materials[mat_idx]; + let sphere = Sphere(P(0.0, 0.0, 0.0), 0.15); + let node = Node(sphere, mat); + let px = x.to_float() * 0.7; + let py = y.to_float() * 0.7 - 1.0; + let pz = z.to_float() * 0.7 - 1.0; + node.translate(px, py, pz); + scene.addNode("s" + count, node); + count += 1; + } + } +} + +scene diff --git a/src/bvh.rs b/src/bvh.rs index 4f287e1..ccd07fb 100644 --- a/src/bvh.rs +++ b/src/bvh.rs @@ -21,8 +21,8 @@ pub struct AABB { impl AABB { // New box with respective coordinates pub fn new(bln: Point3, trf: Point3) -> AABB { - let bln = bln + Vector3::new(EPSILON, EPSILON, EPSILON); - let trf = trf - Vector3::new(EPSILON, EPSILON, EPSILON); + let bln = bln - Vector3::new(EPSILON, EPSILON, EPSILON); + let trf = trf + Vector3::new(EPSILON, EPSILON, EPSILON); let centroid = bln + (trf - bln) / 2.0; AABB { bln, trf, centroid } } @@ -50,50 +50,22 @@ impl AABB { let t1 = (bln - ray.a).component_div(&ray.b); let t2 = (trf - ray.a).component_div(&ray.b); - let tmin = t1.inf(&t2).min(); - let tmax = t1.sup(&t2).max(); + let tmin = t1.inf(&t2).max(); + let tmax = t1.sup(&t2).min(); - if tmax >= tmin { - let intersect = ray.at_t(tmin); - - // Check if the intersection is inside the box - if intersect.x > bln.x - || intersect.x < trf.x - || intersect.y > bln.y - || intersect.y < trf.y - || intersect.z > bln.z - || intersect.z < trf.z - { - return true; // Intersection is outside the box - } - } - false + tmax >= tmin && tmax > 0.0 } - // Intersect way with some epsilon term + // Intersect ray with some epsilon tolerance pub fn intersect_ray_aprox(&self, ray: &Ray) -> bool { let bln = &self.bln; let trf = &self.trf; let t1 = (bln - ray.a).component_div(&ray.b); let t2 = (trf - ray.a).component_div(&ray.b); - let tmin = t1.inf(&t2).min(); - let tmax = t1.sup(&t2).max(); + let tmin = t1.inf(&t2).max(); + let tmax = t1.sup(&t2).min(); - if tmax >= tmin { - let intersect = ray.at_t(tmin); - - // Check if the intersection is inside the box - if intersect.x > bln.x - EPSILON - || intersect.x < trf.x + EPSILON - || intersect.y > bln.y - EPSILON - || intersect.y < trf.y + EPSILON - || intersect.z > bln.z - EPSILON - || intersect.z < trf.z + EPSILON - { - return true; // Intersection is outside the box - } - } - false + tmax >= tmin - EPSILON && tmax > -EPSILON } // Get the center of this bounding box fn get_centroid(&self) -> Point3 { @@ -126,6 +98,7 @@ impl AABB { self.trf.y.max(other.trf.y), self.trf.z.max(other.trf.z), ); + self.centroid = self.bln + (self.trf - self.bln) / 2.0; } //Grow the AABB to contain the cover the point pub fn grow(&self, other: &Point3) -> AABB { @@ -154,6 +127,7 @@ impl AABB { self.trf.y.max(other.y), self.trf.z.max(other.z), ); + self.centroid = self.bln + (self.trf - self.bln) / 2.0; } // Size of AABB pub fn size(&self) -> Vector3 { @@ -304,7 +278,7 @@ impl BVH { // let mut best_pos = 0.0; // let mut best_cost = 1e30; // let first_prim_idx = self.bvh_nodes[index].first_prim; - // for axis in 0..2 { + // for axis in 0..3 { // for i in 0..self.bvh_nodes[index].prim_count { // let node = &self.nodes[first_prim_idx + i]; // //Get the centroid of the bounding box @@ -391,20 +365,25 @@ impl BVH { return None; } if bvh_node.prim_count != 0 { - // Leaf node intersection - let node_idx = bvh_node.first_prim; - let node = &self.nodes[node_idx]; - if !node.active { - return None; - } - if let Some(intersect) = node.intersect_ray(&ray) { - if intersect.distance < EPSILON { - return None; - } else { - return Some((node, intersect)); + // Leaf node intersection — test all primitives in the leaf + let mut closest: Option<(&Node, Intersection)> = None; + let mut closest_dist = f64::MAX; + for i in 0..bvh_node.prim_count { + let node = &self.nodes[bvh_node.first_prim + i]; + if !node.active { + continue; + } + if let Some(intersect) = node.intersect_ray(&ray) { + if intersect.distance < EPSILON { + continue; + } + if intersect.distance < closest_dist { + closest_dist = intersect.distance; + closest = Some((node, intersect)); + } } } - return None; + return closest; } else { //Recurse down the BVH //Recurse down the BVH right node @@ -438,15 +417,15 @@ impl BVH { let aabb = self.nodes[node.first_prim + i].get_world_aabb(); if aabb.trf[axis] < pos { l_count += 1; - l_aabb.grow_mut(&aabb.trf); + l_aabb.join_mut(&aabb); } else { r_count += 1; - r_aabb.grow_mut(&aabb.bln); + r_aabb.join_mut(&aabb); } } let cost = l_count as f64 * l_aabb.area() + r_count as f64 * r_aabb.area(); match cost > 0.0 { - true => 0.0, + true => cost, false => 1e30, } } diff --git a/src/camera.rs b/src/camera.rs index 7098865..b45d878 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -51,6 +51,66 @@ impl Camera { self.recalculate_matrix(); } + /// Get the forward direction vector (from eye toward target) + pub fn forward(&self) -> Vector3 { + (self.target - self.eye).normalize() + } + + /// Get the right direction vector + pub fn right(&self) -> Vector3 { + self.forward().cross(&self.up).normalize() + } + + /// Move the camera forward/backward along its view direction (moves both eye and target) + pub fn move_forward(&mut self, amount: f64) { + let dir = self.forward() * amount; + self.eye += dir; + self.target += dir; + self.recalculate_matrix(); + } + + /// Strafe the camera left/right (moves both eye and target) + pub fn move_right(&mut self, amount: f64) { + let dir = self.right() * amount; + self.eye += dir; + self.target += dir; + self.recalculate_matrix(); + } + + /// Move the camera up/down along the up vector (moves both eye and target) + pub fn move_up(&mut self, amount: f64) { + let dir = self.up.normalize() * amount; + self.eye += dir; + self.target += dir; + self.recalculate_matrix(); + } + + /// Orbit the camera around the target point by yaw (horizontal) and pitch (vertical) angles in radians + pub fn orbit(&mut self, yaw: f64, pitch: f64) { + let offset = self.eye - self.target; + let radius = offset.norm(); + + // Current spherical angles + let current_pitch = (offset.y / radius).asin(); + let current_yaw = offset.z.atan2(offset.x); + + let new_yaw = current_yaw + yaw; + let new_pitch = (current_pitch + pitch).clamp( + -std::f64::consts::FRAC_PI_2 + 0.01, + std::f64::consts::FRAC_PI_2 - 0.01, + ); + + // Convert back to cartesian + let new_offset = Vector3::new( + radius * new_pitch.cos() * new_yaw.cos(), + radius * new_pitch.sin(), + radius * new_pitch.cos() * new_yaw.sin(), + ); + + self.eye = self.target + new_offset; + self.recalculate_matrix(); + } + /// Recalculate the view and inverse view matrices based on the current eye, target, and up vectors fn recalculate_matrix(&mut self) { self._view = Matrix4::look_at_lh(&self.eye, &self.target, &self.up); diff --git a/src/gui.rs b/src/gui.rs index 1ac3032..616057f 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -11,7 +11,7 @@ use imgui::*; use nalgebra::{Point3, Vector3}; use pixels::{wgpu, PixelsContext}; use rhai::Engine; -use std::time::Instant; +use std::time::{Duration, Instant}; //BUFFER CONSTANTS const BUFFER_PROPORTION_MIN: f32 = 0.1; @@ -38,22 +38,16 @@ const MIN_DIFFUSE_COEFFICIENT: f32 = 0.0; const MAX_DIFFUSE_COEFFICIENT: f32 = 1.0; //MATERIAL CONSTANTS -const MIN_D: f32 = 0.0; -const MIN_S: f32 = 0.0; const MIN_SHINE: f32 = 0.0; -const MAX_D: f32 = 1.0; -const MAX_S: f32 = 1.0; const MAX_SHINE: f32 = 50.0; //TRANSFORMATION CONSTANTS -const MIN_COLOUR: f32 = 0.0; const MIN_FALLOFF: f32 = 0.0; const MIN_SCALE: f64 = 0.0; //const MIN_POSITION: f64 = -10.0; const MIN_ROTATION: f64 = -180.0; const MIN_TRANSLATE: f64 = -10.0; //-- -const MAX_COLOUR: f32 = 1.0; const MAX_FALLOFF: f32 = 1.0; const MAX_SCALE: f64 = 3.0; //const MAX_POSITION: f64 = 10.0; @@ -81,6 +75,9 @@ pub struct Gui { pub event: Option, + render_start: Option, + render_elapsed: Option, + script_filename: String, script: String, engine: Engine, @@ -141,6 +138,9 @@ impl Gui { last_cursor: None, event: None, + render_start: None, + render_elapsed: None, + script_filename: String::from(INIT_FILE), script: String::new(), engine: init_engine(), @@ -171,6 +171,17 @@ impl Gui { gui } + pub fn start_render_timer(&mut self) { + self.render_start = Some(Instant::now()); + self.render_elapsed = None; + } + + pub fn stop_render_timer(&mut self) { + if let Some(start) = self.render_start.take() { + self.render_elapsed = Some(start.elapsed()); + } + } + /// Prepare Dear ImGui. pub fn prepare( &mut self, @@ -216,25 +227,42 @@ impl Gui { &mut self.raytracing_option.threads, ); // Numbers of rays to render per pass - ui.slider( - "Rays Per Pass", - RAYS_MIN, - RAYS_MAX, - &mut self.raytracing_option.pixels_per_thread, - ); + Drag::new("Rays Per Pass") + .range(RAYS_MIN, RAYS_MAX) + .speed(50.0) + .build(ui, &mut self.raytracing_option.pixels_per_thread); // Proportion of the window the buffer occupies - ui.slider( - "% Buffer: ", - BUFFER_PROPORTION_MIN, - BUFFER_PROPORTION_MAX, - &mut self.raytracing_option.buffer_proportion, - ); + Drag::new("% Buffer: ") + .range(BUFFER_PROPORTION_MIN, BUFFER_PROPORTION_MAX) + .speed(0.005) + .display_format("%.2f") + .build(ui, &mut self.raytracing_option.buffer_proportion); //Clear colour for scene - ui.slider_config("Clear Colour", 0, 255) - .build_array(&mut self.raytracing_option.clear_color); + let mut clear_f32 = [ + self.raytracing_option.clear_color[0] as f32 / 255.0, + self.raytracing_option.clear_color[1] as f32 / 255.0, + self.raytracing_option.clear_color[2] as f32 / 255.0, + self.raytracing_option.clear_color[3] as f32 / 255.0, + ]; + if ui.color_edit4_config("Clear Colour", &mut clear_f32).alpha_bar(true).build() { + self.raytracing_option.clear_color = [ + (clear_f32[0] * 255.0) as u8, (clear_f32[1] * 255.0) as u8, + (clear_f32[2] * 255.0) as u8, (clear_f32[3] * 255.0) as u8, + ]; + } //Clear colour if no intersect - ui.slider_config("Pixel Clear Colour", 0, 255) - .build_array(&mut self.raytracing_option.pixel_clear); + let mut pixel_clear_f32 = [ + self.raytracing_option.pixel_clear[0] as f32 / 255.0, + self.raytracing_option.pixel_clear[1] as f32 / 255.0, + self.raytracing_option.pixel_clear[2] as f32 / 255.0, + self.raytracing_option.pixel_clear[3] as f32 / 255.0, + ]; + if ui.color_edit4_config("Pixel Clear Colour", &mut pixel_clear_f32).alpha_bar(true).build() { + self.raytracing_option.pixel_clear = [ + (pixel_clear_f32[0] * 255.0) as u8, (pixel_clear_f32[1] * 255.0) as u8, + (pixel_clear_f32[2] * 255.0) as u8, (pixel_clear_f32[3] * 255.0) as u8, + ]; + } //Ray depth slider ui.slider( "Ray Depth", @@ -250,12 +278,11 @@ impl Gui { &mut self.raytracing_option.ray_samples, ); //Ray randomness - ui.slider( - "Ray Randomness", - MIN_RANDOM, - MAX_RANDOM, - &mut self.raytracing_option.ray_randomness, - ); + Drag::new("Ray Randomness") + .range(MIN_RANDOM, MAX_RANDOM) + .speed(5.0) + .display_format("%.1f") + .build(ui, &mut self.raytracing_option.ray_randomness); //Number of diffuse rays ui.slider( "Diffuse Rays", @@ -264,12 +291,11 @@ impl Gui { &mut self.raytracing_option.diffuse_rays, ); //Diffuse Coefficient - ui.slider( - "Diffuse Coefficient", - MIN_DIFFUSE_COEFFICIENT, - MAX_DIFFUSE_COEFFICIENT, - &mut self.raytracing_option.diffuse_coefficient, - ); + Drag::new("Diffuse Coefficient") + .range(MIN_DIFFUSE_COEFFICIENT, MAX_DIFFUSE_COEFFICIENT) + .speed(0.005) + .display_format("%.3f") + .build(ui, &mut self.raytracing_option.diffuse_coefficient); // Fov of the buffer ui.slider( "fov", @@ -283,6 +309,15 @@ impl Gui { ui.checkbox("Enable Reflections", &mut self.raytracing_option.reflect); ui.checkbox("Enable Specular", &mut self.raytracing_option.specular); ui.checkbox("Enable Diffuse", &mut self.raytracing_option.diffuse); + // Render timer display + ui.separator(); + if let Some(start) = &self.render_start { + let elapsed = start.elapsed().as_secs_f64(); + ui.text(format!("Rendering: {:.2}s", elapsed)); + } else if let Some(elapsed) = &self.render_elapsed { + ui.text(format!("Render time: {:.2}s", elapsed.as_secs_f64())); + } + ui.separator(); // Apply stored changes if ui.button("Apply") { self.event = Some(GuiEvent::RaytracerOption(self.raytracing_option.clone())); @@ -292,12 +327,21 @@ impl Gui { if CollapsingHeader::new("Camera").build(ui) { // Eye, target and up vector inputs ui.text("Camera options:"); - ui.slider_config("Eye", MIN_TRANSLATE, MAX_TRANSLATE) - .build_array(self.camera.eye.coords.as_mut_slice()); - ui.slider_config("Target", MIN_TRANSLATE, MAX_TRANSLATE) - .build_array(self.camera.target.coords.as_mut_slice()); - ui.slider_config("Up", 0.0, 1.0) - .build_array(self.camera.up.as_mut_slice()); + Drag::new("Eye") + .range(MIN_TRANSLATE, MAX_TRANSLATE) + .speed(0.05) + .display_format("%.2f") + .build_array(ui, self.camera.eye.coords.as_mut_slice()); + Drag::new("Target") + .range(MIN_TRANSLATE, MAX_TRANSLATE) + .speed(0.05) + .display_format("%.2f") + .build_array(ui, self.camera.target.coords.as_mut_slice()); + Drag::new("Up") + .range(0.0, 1.0) + .speed(0.005) + .display_format("%.3f") + .build_array(ui, self.camera.up.as_mut_slice()); if ui.button("Apply Camera") { println!("Camera changed"); self.event = Some(GuiEvent::CameraUpdate(self.camera.clone())); @@ -361,12 +405,21 @@ impl Gui { ui.checkbox(format!("##active{label}"), &mut node.active); ui.same_line(); if let Some(_t) = ui.tree_node(label) { - ui.slider_config("Translation", MIN_TRANSLATE, MAX_TRANSLATE) - .build_array(&mut node.translation); - ui.slider_config("Rotation", MIN_ROTATION, MAX_ROTATION) - .build_array(&mut node.rotation); - ui.slider_config("Scale", MIN_SCALE, MAX_SCALE) - .build_array(&mut node.scale); + Drag::new("Translation") + .range(MIN_TRANSLATE, MAX_TRANSLATE) + .speed(0.05) + .display_format("%.2f") + .build_array(ui, &mut node.translation); + Drag::new("Rotation") + .range(MIN_ROTATION, MAX_ROTATION) + .speed(1.0) + .display_format("%.1f") + .build_array(ui, &mut node.rotation); + Drag::new("Scale") + .range(MIN_SCALE, MAX_SCALE) + .speed(0.01) + .display_format("%.3f") + .build_array(ui, &mut node.scale); } } } @@ -374,11 +427,19 @@ impl Gui { if let Some(_t) = ui.tree_node("Materials") { for (label, material) in &mut self.scene.materials { if let Some(_t) = ui.tree_node(label) { - ui.slider_config("ks", MIN_D, MAX_D) - .build_array(material.ks.as_mut_slice()); - ui.slider_config("kd", MIN_S, MAX_S) - .build_array(material.kd.as_mut_slice()); - ui.slider("shine", MIN_SHINE, MAX_SHINE, &mut material.shininess); + let mut ks_arr: [f32; 3] = material.ks.into(); + if ui.color_edit3("ks", &mut ks_arr) { + material.ks = Vector3::from(ks_arr); + } + let mut kd_arr: [f32; 3] = material.kd.into(); + if ui.color_edit3("kd", &mut kd_arr) { + material.kd = Vector3::from(kd_arr); + } + Drag::new("shine") + .range(MIN_SHINE, MAX_SHINE) + .speed(0.5) + .display_format("%.1f") + .build(ui, &mut material.shininess); } } } @@ -388,12 +449,20 @@ impl Gui { ui.checkbox(format!("##activelight{label}"), &mut light.active); ui.same_line(); if let Some(_t) = ui.tree_node(label) { - ui.slider_config("Colour", MIN_COLOUR, MAX_COLOUR) - .build_array(light.colour.as_mut_slice()); - ui.slider_config("Position", MIN_TRANSLATE, MAX_TRANSLATE) - .build_array(light.position.coords.as_mut_slice()); - ui.slider_config("Falloff", MIN_FALLOFF, MAX_FALLOFF) - .build_array(light.falloff.as_mut_slice()); + let mut colour_arr: [f32; 3] = light.colour.into(); + if ui.color_edit3("Colour", &mut colour_arr) { + light.colour = Vector3::from(colour_arr); + } + Drag::new("Position") + .range(MIN_TRANSLATE, MAX_TRANSLATE) + .speed(0.05) + .display_format("%.2f") + .build_array(ui, light.position.coords.as_mut_slice()); + Drag::new("Falloff") + .range(MIN_FALLOFF, MAX_FALLOFF) + .speed(0.005) + .display_format("%.3f") + .build_array(ui, light.falloff.as_mut_slice()); } } } @@ -430,6 +499,11 @@ impl Gui { ) } + /// Update the GUI's camera to reflect external changes (e.g. from keyboard/mouse movement) + pub fn update_camera(&mut self, camera: &Camera) { + self.camera = camera.clone(); + } + /// Handle any outstanding events. pub fn handle_event( &mut self, diff --git a/src/node.rs b/src/node.rs index 1dd7791..489a44c 100644 --- a/src/node.rs +++ b/src/node.rs @@ -99,17 +99,19 @@ impl Node { // Compute the inverse model matrix by inverting the model matrix self.inv_model = self.model.try_inverse().unwrap(); self.inv_transpose_model = self.inv_model.transpose().remove_row(3).remove_column(3); + self.aabb = self.primitive.get_aabb(); self.aabb.transform_mut(&self.model); } // Intersection of a ray, will convert to model coords and check pub fn intersect_ray(&self, ray: &Ray) -> Option { + let world_origin = ray.a; // Save world-space origin before transform let ray = ray.transform(&self.inv_model); //Transform from world coordinates if let Some(mut intersect) = self.primitive.intersect_ray(&ray) { if intersect.distance < EPSILON { return None; } intersect.transform_mut(&self.model, &self.inv_transpose_model); //Transform to world coords - intersect.distance = distance(&intersect.point, &ray.a); + intersect.distance = distance(&intersect.point, &world_origin); return Some(intersect); } return None; diff --git a/src/primitive.rs b/src/primitive.rs index b4569a8..2c21a57 100644 --- a/src/primitive.rs +++ b/src/primitive.rs @@ -47,14 +47,12 @@ impl Primitive for Sphere { Roots::No(_) => return None, Roots::One([x1]) => x1, Roots::Two([x1, x2]) => { - if x1 <= 0.0 && x2 <= 0.0 { - return None; + if x1 > EPSILON { + x1 + } else if x2 > EPSILON { + x2 } else { - if x1.abs() < x2.abs() { - x1 - } else { - x2 - } + return None; } } _ => return None, @@ -124,7 +122,7 @@ impl Primitive for Circle { let n_dot_b = ray.b.dot(&self.normal); let t = (self.constant - n_dot_a) / n_dot_b; - if t > INFINITY { + if t < EPSILON || t > INFINITY { return None; }; @@ -197,14 +195,12 @@ impl Primitive for Cylinder { Roots::No(_) => return None, Roots::One([x1]) => Some(x1), Roots::Two([x1, x2]) => { - if x1 <= 0.0 && x2 <= 0.0 { - return None; + if x1 > EPSILON { + Some(x1) + } else if x2 > EPSILON { + Some(x2) } else { - if x1.abs() < x2.abs() { - Some(x1) - } else { - Some(x2) - } + return None; } } _ => return None, @@ -325,14 +321,12 @@ impl Primitive for Cone { Roots::No(_) => None, Roots::One([x1]) => Some(x1), Roots::Two([x1, x2]) => { - if x1 <= 0.0 && x2 <= 0.0 { - None + if x1 > EPSILON { + Some(x1) + } else if x2 > EPSILON { + Some(x2) } else { - if x1.abs() < x2.abs() { - Some(x1) - } else { - Some(x2) - } + None } } _ => None, @@ -359,7 +353,15 @@ impl Primitive for Cone { (None, None) => None, (Some(cone_intersect), None) => Some(cone_intersect), (None, Some(circle_intersect)) => Some(circle_intersect), - (Some(cone_intersect), Some(_)) => Some(cone_intersect), + (Some(cone_intersect), Some(circle_intersect)) => { + let cone_distance = distance(&ray.a, &cone_intersect.point); + let circle_distance = distance(&ray.a, &circle_intersect.point); + if cone_distance < circle_distance { + Some(cone_intersect) + } else { + Some(circle_intersect) + } + } } } @@ -395,7 +397,7 @@ impl Primitive for RectangleXY { let az = ray.a.z; let bz = ray.b.z; let t = (z - az) / bz; - if t > INFINITY { + if t < EPSILON || t > INFINITY { return None; } let intersect = ray.at_t(t); @@ -470,21 +472,28 @@ impl Primitive for Cube { return None; // Intersection is outside the box } - //Get normal of intersection point - //t1 is bln t2 is trf - let normal = if tmin == t1.x { - Vector3::new(-1.0, 0.0, 0.0) - } else if tmin == t1.y { - Vector3::new(0.0, -1.0, 0.0) - } else if tmin == t1.z { - Vector3::new(0.0, 0.0, -1.0) - } else if tmin == t2.x { - Vector3::new(1.0, 0.0, 0.0) - } else if tmin == t2.y { - Vector3::new(0.0, 1.0, 0.0) - } else { - Vector3::new(0.0, 0.0, 1.0) - }; + // Determine which face was hit by finding the t-value closest to tmin + let diffs = [ + (t1.x - tmin).abs(), + (t1.y - tmin).abs(), + (t1.z - tmin).abs(), + (t2.x - tmin).abs(), + (t2.y - tmin).abs(), + (t2.z - tmin).abs(), + ]; + let normals = [ + Vector3::new(-1.0, 0.0, 0.0), + Vector3::new(0.0, -1.0, 0.0), + Vector3::new(0.0, 0.0, -1.0), + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(0.0, 0.0, 1.0), + ]; + let min_idx = diffs.iter() + .enumerate() + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) + .unwrap().0; + let normal = normals[min_idx]; Some(Intersection { point: intersect, @@ -645,9 +654,9 @@ impl Mesh { let u = vertices[v1 - 1]; let v = vertices[v2 - 1]; let w = vertices[v3 - 1]; - let uv = u - v; - let uw = w - v; - let normal = uv.cross(&uw).normalize(); + let uv = v - u; + let uw = w - u; + let normal = uw.cross(&uv).normalize(); triangles.push(Triangle { u, v, w, normal }); } } @@ -805,9 +814,9 @@ impl Primitive for Torus { } fn get_aabb(&self) -> AABB { - //TODO! - let trf = Point3::new(1.0, 1.0, 1.0); - let bln = Point3::new(-1.0, -1.0, -1.0); + let extent = self.inner_rad + self.outer_rad; + let bln = Point3::new(-extent, -self.outer_rad, -extent); + let trf = Point3::new(extent, self.outer_rad, extent); AABB::new(bln, trf) } } @@ -846,19 +855,19 @@ impl Gnonom { impl Primitive for Gnonom { fn intersect_ray(&self, ray: &Ray) -> Option { - match self.x_cube.intersect_ray(ray) { - Some(intersect) => return Some(intersect), - None => (), - }; - match self.y_cube.intersect_ray(ray) { - Some(intersect) => return Some(intersect), - None => (), - }; - match self.z_cube.intersect_ray(ray) { - Some(intersect) => return Some(intersect), - None => (), - }; - None + let mut closest: Option = None; + let mut closest_dist = f64::MAX; + + for cube in [&self.x_cube, &self.y_cube, &self.z_cube] { + if let Some(intersect) = cube.intersect_ray(ray) { + let dist = distance(&ray.a, &intersect.point); + if dist < closest_dist { + closest_dist = dist; + closest = Some(intersect); + } + } + } + closest } fn get_aabb(&self) -> AABB { diff --git a/src/ray.rs b/src/ray.rs index 7b6a277..1de90e3 100644 --- a/src/ray.rs +++ b/src/ray.rs @@ -168,6 +168,28 @@ impl Ray { // Compute the ambient light component and set it as base colour let mut colour = Vector3::zeros(); + // Reflection is view-dependent, not light-dependent — compute once + let mut reflect = Vector3::zeros(); + if options.reflect { + let reflect_dir = incidence - 2.0 * incidence.dot(&normal) * normal; + let reflect_ray = Ray::new(*point, reflect_dir); + if let Some(col) = reflect_ray.shade_ray(scene, depth + 1, options, bvh) { + reflect += col.component_mul(&material.kr) + } + } + + // Indirect diffuse (global illumination samples) — compute once + let mut indirect = Vector3::zeros(); + if options.diffuse { + for _ in 0..options.diffuse_rays { + let diffuse_dir = random_unit_vec(); + let diffuse_ray = Ray::new(point.clone(), diffuse_dir + normal); + if let Some(col) = diffuse_ray.shade_ray(scene, depth + 1, options, bvh) { + indirect += col * options.diffuse_coefficient; + } + } + } + for (_, light) in &scene.lights { if !light.active { continue; @@ -192,27 +214,10 @@ impl Ray { let n_dot_l = normal.dot(&to_light).max(0.0) as f32; - //Reflected component - let mut reflect = Vector3::zeros(); - if options.reflect { - let reflect_dir = incidence - 2.0 * incidence.dot(&normal) * normal; - let reflect_ray = Ray::new(*point, reflect_dir); - if let Some(col) = reflect_ray.shade_ray(scene, depth + 1, options, bvh) { - reflect += col.component_mul(&material.kr) - } - } - - //Diffuse component (Lambertian) + //Direct diffuse component (Lambertian) let mut diffuse = Vector3::zeros(); if options.diffuse { diffuse += material.kd * n_dot_l; - for _ in 0..options.diffuse_rays { - let diffuse_dir = random_unit_vec(); - let diffuse_ray = Ray::new(point.clone(), diffuse_dir + normal); - if let Some(col) = diffuse_ray.shade_ray(scene, depth + 1, options, bvh) { - diffuse += col * options.diffuse_coefficient; - } - } } //Specular component @@ -234,10 +239,13 @@ impl Ray { + light.falloff[2] * light_distance * light_distance); } - let intensity = light.colour.component_mul(&(diffuse + reflect + specular)) * falloff; + let intensity = light.colour.component_mul(&(diffuse + specular)) * falloff; colour += &intensity; } + // Add light-independent terms + colour += reflect + indirect; + colour } @@ -246,18 +254,8 @@ impl Ray { match bvh { Some(bvh) => { //We have a bvh so use bvh traversal - for (_, node) in &scene.nodes { - if !node.active { - continue; - } - match bvh.traverse(self, 0) { - Some((_, intersect)) => { - if intersect.distance < light_distance { - return true; - } - } - None => continue, - } + if let Some((_, intersect)) = bvh.traverse(self, 0) { + return intersect.distance < light_distance; } return false; } diff --git a/src/state.rs b/src/state.rs index 8db5c68..63cee88 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,6 +5,7 @@ use crate::camera::Camera; use crate::ray::Ray; use crate::{gui::Gui, scene::Scene}; use crate::{gui::GuiEvent, log_error}; +use std::collections::HashSet; use std::path::Path; use std::thread; @@ -13,12 +14,15 @@ use rand::seq::SliceRandom; use rand::{random, thread_rng}; use std::error::Error; -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; use anyhow::Result; use pixels::{Pixels, SurfaceTexture}; use winit::dpi::{LogicalSize, PhysicalSize}; -use winit::event::{Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent}; +use winit::event::{ + ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent, +}; use winit::event_loop::{ControlFlow, EventLoop}; use winit::window::{Window, WindowBuilder}; @@ -72,6 +76,9 @@ impl RaytracingOption { } } +const CAMERA_MOVE_SPEED: f64 = 0.15; +const CAMERA_ORBIT_SPEED: f64 = 0.005; + pub struct State { scene: Arc, bvh: Arc>, @@ -85,8 +92,17 @@ pub struct State { gui: Gui, rays: Arc>, - ray_queue: Vec, + ray_queue: Arc>>, raytracing_options: Arc, + + result_rx: mpsc::Receiver>, + render_active: Arc, + rendering: bool, + + keys_pressed: HashSet, + right_mouse_down: bool, + last_mouse_pos: Option<(f64, f64)>, + camera_dirty: bool, } impl State { @@ -96,6 +112,7 @@ impl State { let pixels = pixels; let camera = Camera::unit(); let rays = Arc::new(Vec::new()); + let (_tx, rx) = mpsc::channel(); Self { scene, @@ -107,8 +124,15 @@ impl State { pixels, gui, rays, - ray_queue: Vec::new(), + ray_queue: Arc::new(Mutex::new(Vec::new())), raytracing_options: Arc::new(RaytracingOption::default()), + result_rx: rx, + render_active: Arc::new(AtomicBool::new(false)), + rendering: false, + keys_pressed: HashSet::new(), + right_mouse_down: false, + last_mouse_pos: None, + camera_dirty: false, } } @@ -192,98 +216,109 @@ impl State { } fn keyboard_input(&mut self, key: &KeyboardInput) { - if let Some(VirtualKeyCode::A) = key.virtual_keycode { - // Handle 'A' key event here - } - } - - fn mouse_input(&mut self, _button: &MouseButton) { - // Handle mouse input here - } - - fn draw(&mut self) -> Result<(), Box> { - //Draw ray_num in a block - let randomness = self.raytracing_options.ray_randomness; - let samples = self.raytracing_options.ray_samples; - let samples_f32 = samples as f32; - - let num_threads = self.raytracing_options.threads; - let pixels_per_thread = self.raytracing_options.pixels_per_thread; - - let mut handles = vec![]; - - for _ in 0..num_threads { - //Get necessary variables to render - let rays = self.rays.clone(); - let scene = self.scene.clone(); - let options = self.raytracing_options.clone(); - let bvh = self.bvh.clone(); - - //Get the workload for a thread - let mut load = vec![]; - for _ in 0..pixels_per_thread { - match self.ray_queue.pop() { - Some(index) => load.push(index), - None => break, + if let Some(keycode) = key.virtual_keycode { + match key.state { + ElementState::Pressed => { + self.keys_pressed.insert(keycode); + } + ElementState::Released => { + self.keys_pressed.remove(&keycode); } } - //The finished queue of the thread - let mut finished = vec![]; + } + } - //Create a new thread for these pixels - let handle = thread::spawn({ - move || { - for index in &load { - //Shade colour for selected index - let mut colour: Vector3 = Vector3::zeros(); - let ray = &rays[*index]; - for _ in 0..samples { - //Generate a ray in a random direction - let point = ray.a; - let dir = ray.b; - let rx = (random::() - 0.5) / randomness; - let ry = (random::() - 0.5) / randomness; - let rz = (random::() - 0.5) / randomness; - let nx = dir.x + rx; - let ny = dir.y + ry; - let nz = dir.z + rz; + fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) { + if *button == MouseButton::Right { + self.right_mouse_down = *state == ElementState::Pressed; + if !self.right_mouse_down { + self.last_mouse_pos = None; + } + } + } - let rand_ray = Ray::new(point, Vector3::new(nx, ny, nz)); + fn cursor_moved(&mut self, x: f64, y: f64) { + if self.right_mouse_down { + if let Some((last_x, last_y)) = self.last_mouse_pos { + let dx = x - last_x; + let dy = y - last_y; + self.camera.orbit( + -dx * CAMERA_ORBIT_SPEED, + -dy * CAMERA_ORBIT_SPEED, + ); + self.camera_dirty = true; + } + self.last_mouse_pos = Some((x, y)); + } + } - if let Some(ray_colour) = rand_ray.shade_ray(&scene, 0, &options, &bvh) - { - colour += ray_colour; - } - } - colour = (colour / samples_f32) * 255.0; - let rgba = [colour.x as u8, colour.y as u8, colour.z as u8, 0xff]; - finished.push(rgba); + fn process_camera_movement(&mut self) { + let speed = CAMERA_MOVE_SPEED; + + if self.keys_pressed.contains(&VirtualKeyCode::W) { + self.camera.move_forward(speed); + self.camera_dirty = true; + } + if self.keys_pressed.contains(&VirtualKeyCode::S) { + self.camera.move_forward(-speed); + self.camera_dirty = true; + } + if self.keys_pressed.contains(&VirtualKeyCode::A) { + self.camera.move_right(-speed); + self.camera_dirty = true; + } + if self.keys_pressed.contains(&VirtualKeyCode::D) { + self.camera.move_right(speed); + self.camera_dirty = true; + } + if self.keys_pressed.contains(&VirtualKeyCode::Q) { + self.camera.move_up(-speed); + self.camera_dirty = true; + } + if self.keys_pressed.contains(&VirtualKeyCode::E) { + self.camera.move_up(speed); + self.camera_dirty = true; + } + + if self.camera_dirty { + self.camera_dirty = false; + self.rays = Arc::new(Ray::cast_rays( + &self.camera.eye, + &self.camera.target, + &self.camera.up, + self.raytracing_options.buffer_fov, + self.buffer_width, + self.buffer_height, + )); + self.gui.update_camera(&self.camera); + let _ = self.clear_buffer(); + self.reset_queue(); + } + } + + fn draw(&mut self) { + if !self.rendering { + return; + } + + // Drain completed results from background workers + loop { + match self.result_rx.try_recv() { + Ok(results) => { + let frame = self.pixels.frame_mut(); + for (index, rgba) in results { + frame[index * 4..(index + 1) * 4].copy_from_slice(&rgba); } - return (load, finished); } - }); - handles.push(handle); + Err(mpsc::TryRecvError::Empty) => break, + Err(mpsc::TryRecvError::Disconnected) => { + // All worker threads have finished + self.rendering = false; + self.gui.stop_render_timer(); + break; + } + } } - - let mut all_results = vec![]; - - for handle in handles.drain(..) { - let (load, finished) = handle - .join() - .map_err(|e| format!("Thread panicked: {:?}", e))?; - let thread_results: Vec<_> = load.into_iter().zip(finished.into_iter()).collect(); - all_results.extend(thread_results); - } - - //Now we have two vectors will all the indicies and rgba values, we can upload them to the bufer - - let frame = self.pixels.frame_mut(); - for result in all_results { - let index = result.0; - let rgba = result.1; - frame[index * 4..(index + 1) * 4].copy_from_slice(&rgba); - } - Ok(()) } fn clear_buffer(&mut self) -> Result<(), Box> { @@ -295,26 +330,113 @@ impl State { } fn reset_queue(&mut self) { + // Signal any existing workers to stop + self.render_active.store(false, Ordering::Relaxed); + match self.raytracing_options.bvh_active { true => self.bvh = Arc::new(Some(BVH::build(&self.scene.nodes))), false => self.bvh = Arc::new(None), } + + // Create new shuffled queue let size = self.buffer_height as usize * self.buffer_width as usize; let mut ray_queue: Vec = (0..size).collect(); ray_queue.shuffle(&mut thread_rng()); - self.ray_queue = ray_queue; + self.ray_queue = Arc::new(Mutex::new(ray_queue)); + + // Create new channel and active flag + let (tx, rx) = mpsc::channel(); + self.result_rx = rx; + let render_active = Arc::new(AtomicBool::new(true)); + self.render_active = render_active.clone(); + self.rendering = true; + + // Spawn persistent worker threads + let num_threads = self.raytracing_options.threads; + let pixels_per_thread = self.raytracing_options.pixels_per_thread; + + for _ in 0..num_threads { + let rays = self.rays.clone(); + let scene = self.scene.clone(); + let options = self.raytracing_options.clone(); + let bvh = self.bvh.clone(); + let queue = self.ray_queue.clone(); + let tx = tx.clone(); + let active = render_active.clone(); + + thread::spawn(move || { + let randomness = options.ray_randomness; + let samples = options.ray_samples; + let samples_f32 = samples as f32; + + loop { + if !active.load(Ordering::Relaxed) { + break; + } + + // Pop a batch from the shared queue + let load: Vec = { + let mut q = queue.lock().unwrap(); + let mut batch = Vec::with_capacity(pixels_per_thread as usize); + for _ in 0..pixels_per_thread { + match q.pop() { + Some(index) => batch.push(index), + None => break, + } + } + batch + }; + + if load.is_empty() { + break; + } + + // Process the batch + let mut results = Vec::with_capacity(load.len()); + for index in &load { + let mut colour: Vector3 = Vector3::zeros(); + let ray = &rays[*index]; + for _ in 0..samples { + let point = ray.a; + let dir = ray.b; + let rx = (random::() - 0.5) / randomness; + let ry = (random::() - 0.5) / randomness; + let rz = (random::() - 0.5) / randomness; + let nx = dir.x + rx; + let ny = dir.y + ry; + let nz = dir.z + rz; + + let rand_ray = Ray::new(point, Vector3::new(nx, ny, nz)); + + if let Some(ray_colour) = + rand_ray.shade_ray(&scene, 0, &options, &bvh) + { + colour += ray_colour; + } + } + colour = (colour / samples_f32) * 255.0; + let rgba = [colour.x as u8, colour.y as u8, colour.z as u8, 0xff]; + results.push((*index, rgba)); + } + + // Send results back to main thread + if tx.send(results).is_err() { + break; + } + } + }); + } + // Drop our copy of tx so the channel disconnects when all workers finish + drop(tx); + + self.gui.start_render_timer(); } fn render(&mut self) -> Result<(), Box> { // Update state self.update()?; - // Draw rays if we have remaining rays in queue - match self.draw() { - Err(e) => { - println!("ERROR: {}", e); - } - _ => {} - } + // Collect completed rays from background workers + self.draw(); // Render Gui self.gui .prepare(&self.window) @@ -355,11 +477,17 @@ pub fn run() -> Result<(), Box> { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Resized(size) => state.resize(&size).expect("Window Resize Error"), WindowEvent::KeyboardInput { input, .. } => state.keyboard_input(&input), - WindowEvent::MouseInput { button, .. } => state.mouse_input(&button), + WindowEvent::MouseInput { button, state: elem_state, .. } => { + state.mouse_input(&button, &elem_state) + } + WindowEvent::CursorMoved { position, .. } => { + state.cursor_moved(position.x, position.y) + } _ => {} }, Event::RedrawRequested(_) => { + state.process_camera_movement(); if let Err(_e) = state.render() { *control_flow = ControlFlow::Exit; }