Compare commits

..

4 Commits

Author SHA1 Message Date
47473a3a08 Fix raytracer bugs: BVH traversal, AABB transforms, root selection, and shading
- BVH: transform AABB using all 8 corners, fix leaf node traversal to check all primitives
- Node: reset AABB from primitive before transform, compute distance in world space
- Primitives: correct quadratic root selection (pick smallest positive), fix t-guards for Circle/RectangleXY, fix Torus AABB orientation
- Ray: fix random_unit_vec to cover all octants, compute reflection outside light loop, add indirect diffuse GI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:45:54 +00:00
fa31d18c12 1. Camera movable with mouse and keyboard 2. GUI runs in separate thread 3. Improvement to GUI widgets 4. Fixes to BVH 2026-03-08 20:13:34 +00:00
d64085461a update readme 2026-02-23 14:11:55 +00:00
79493b8924 update readme 2026-02-23 14:11:29 +00:00
10 changed files with 805 additions and 351 deletions

186
README.md
View File

@@ -1,41 +1,155 @@
# Graphics Project # Graphics Project
This is my graphics project that I will be working on for A5 # Introduction
I will use rlua for interacting with lua files
## Installation ![image](img/img2.png)
## Scripting This is a project I undertook at the University of Waterloo, where I first started using rust. Because of my inexperience, the code isn't as organised as it would be if I made it today and represents my first steps in computer graphics and the rust language.
V() My unique aim was to perform ray intersections on special geometric surfaces, such as the CrossCap surface and the Steiner surface, hence those are among my _primitives_.
P()
Scene() # Installation
Scene.addNode()
Scene.addLight() Clone and run with `cargo run`, however much better performance will be granted with `cargo run --release`.
Node()
Node.translate() ![example](img/example.png)
Node.rotate()
Node.scale() # Rhai
Camera()
Light() Rhai is used as an interactive scripting lang for this project. Examples are found in `rhai/`.
Material()
MaterialRed() ## Full List of rhai commands
MaterialBlue()
MaterialGreen() ```
MaterialMagenta() /// Basic math types
MaterialTurquoise()
Sphere() V(x : float, y : float, z : float) -> Vector3
SphereUnit() // 3dimensional vector, used for directions, colors, etc.
Cube()
CubeUnit() P(x : float, y : float, z : float) -> Position3
Cone() // 3dimensional position vector, used for points in space.
ConeUnit()
Cyclinder()
//CylinderUnit() /// Scene and graph
Circle()
CircleUnit() Scene() -> Scene
Rectangle() // Create an empty scene with no nodes, lights, or camera.
RectangleUnit()
Steiner() Scene.addNode(node : Node) -> void
Torus() // Add a node (with geometry or camera) to the scene.
Scene.addLight(light : Light) -> void
// Add a light source to the scene.
Scene.setCamera(camera : Camera) -> void
// Set the active camera for this scene.
/// Nodes and transforms
Node() -> Node
// Create an empty node (no mesh / camera / light by default).
Node.translate(x : float, y : float, z : float) -> Node
// Apply translation by vector V(x, y, z) in local space, returns self for chaining.
Node.rotate(x : float, y : float, z : float) -> Node
// Rotate node by Euler angles (in radians or degrees, implementationdefined).
Node.scale(x : float, y : float, z : float) -> Node
// Nonuniform scale in local space.
Node.setMaterial(material : Material) -> Node
// Set material for this node's mesh (if any).
/// Camera
Camera(position : P, target : P, up : V) -> Camera
// Create a camera located at `position`, looking at `target`, with `up` as the up direction.
/// Lighting
Ambient(color : V) -> AmbientLight
// Ambient light contribution with RGB in [0, 1].
Light(position : P, color : V, falloff : V) -> PointLight
// Point light at `position` with RGB `color` and falloff parameters (constant, linear, quadratic).
/// Materials
Material(kd : V, ks : V, kr : V, shininess : float) -> Material
// Phongstyle material:
// kd: diffuse color
// ks: specular color
// kr: reflection / mirror color
// shininess: specular exponent.
MaterialRed() -> Material
MaterialBlue() -> Material
MaterialGreen() -> Material
MaterialMagenta() -> Material
MaterialTurquoise() -> Material
// Convenience materials with predefined colors.
/// Primitives
Sphere(pos : P, radius : float) -> Mesh
// Sphere centered at `pos` with given radius.
SphereUnit() -> Mesh
// Unit sphere at (0, 0, 0) with radius 1.
Cube(pos : P, radius : float, normal : V) -> Mesh
// Cube centered at `pos`, edge length = 2 * radius (or radius, implementationdefined),
// `normal` can define an orientation axis.
CubeUnit() -> Mesh
// Unit cube at (0, 0, 0).
Cone(radius : float, height : float) -> Mesh
// Cone aligned with +Z (for example), base radius and height.
ConeUnit() -> Mesh
// Cone with radius 1 and height 1 at the origin.
Cylinder(radius : float, height : float) -> Mesh
// Cylinder aligned with +Z, given radius and height.
CylinderUnit() -> Mesh
// Cylinder with radius 1 and height 1 at the origin.
Circle(position : P, radius : float, normal : V) -> Mesh
// Flat disk at `position` with `normal` orientation and given radius.
CircleUnit() -> Mesh
// Unit circle in the XY plane at the origin.
Rectangle(position : P, size : V, normal : V) -> Mesh
// Axisaligned rectangle centered at `position`, width/height from size.x / size.y, oriented by `normal`.
RectangleUnit() -> Mesh
// 1x1 rectangle in the XY plane centered at origin.
/// Special / parametric surfaces
Steiner() -> Mesh
// A Steiner surface with default parameters and resolution.
Torus(radiusMajor : float, radiusMinor : float) -> Mesh
// Torus with major and minor radius, centered at origin.
Roman() -> Mesh
// Roman surface with default scale and resolution.
CrossCap() -> Mesh
// Crosscap surface (Boy's surface variant / projective plane immersion).
Gnonom() -> Mesh
// Gnomonlike parametric surface (implementationdefined shape).
```

BIN
img/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

81
rhai/bvh_test.rhai Normal file
View File

@@ -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

View File

@@ -21,8 +21,8 @@ pub struct AABB {
impl AABB { impl AABB {
// New box with respective coordinates // New box with respective coordinates
pub fn new(bln: Point3<f64>, trf: Point3<f64>) -> AABB { pub fn new(bln: Point3<f64>, trf: Point3<f64>) -> AABB {
let bln = bln + Vector3::new(EPSILON, EPSILON, EPSILON); let bln = bln - Vector3::new(EPSILON, EPSILON, EPSILON);
let trf = trf - Vector3::new(EPSILON, EPSILON, EPSILON); let trf = trf + Vector3::new(EPSILON, EPSILON, EPSILON);
let centroid = bln + (trf - bln) / 2.0; let centroid = bln + (trf - bln) / 2.0;
AABB { bln, trf, centroid } AABB { bln, trf, centroid }
} }
@@ -36,64 +36,46 @@ impl AABB {
} }
//Apply a matrix transformation to a box //Apply a matrix transformation to a box
pub fn transform_mut(&mut self, mat: &Matrix4<f64>) { pub fn transform_mut(&mut self, mat: &Matrix4<f64>) {
let bln = &mut self.bln; let corners = [
let trf = &mut self.trf; Point3::new(self.bln.x, self.bln.y, self.bln.z),
let centroid = &mut self.centroid; Point3::new(self.trf.x, self.bln.y, self.bln.z),
self.bln = mat.transform_point(bln); Point3::new(self.bln.x, self.trf.y, self.bln.z),
self.trf = mat.transform_point(trf); Point3::new(self.trf.x, self.trf.y, self.bln.z),
self.centroid = mat.transform_point(centroid); Point3::new(self.bln.x, self.bln.y, self.trf.z),
Point3::new(self.trf.x, self.bln.y, self.trf.z),
Point3::new(self.bln.x, self.trf.y, self.trf.z),
Point3::new(self.trf.x, self.trf.y, self.trf.z),
];
let mut new_bln = Point3::new(f64::MAX, f64::MAX, f64::MAX);
let mut new_trf = Point3::new(f64::MIN, f64::MIN, f64::MIN);
for corner in &corners {
let t = mat.transform_point(corner);
new_bln = new_bln.inf(&t);
new_trf = new_trf.sup(&t);
}
self.bln = new_bln;
self.trf = new_trf;
self.centroid = self.bln + (self.trf - self.bln) / 2.0;
} }
// Intersect bounding box exactly // Intersect bounding box exactly
pub fn intersect_ray(&self, ray: &Ray) -> bool { pub fn intersect_ray(&self, ray: &Ray) -> bool {
let bln = &self.bln; let t1 = (self.bln - ray.a).component_div(&ray.b);
let trf = &self.trf; let t2 = (self.trf - ray.a).component_div(&ray.b);
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 tmin = t1.inf(&t2).max();
let tmax = t1.sup(&t2).max(); let tmax = t1.sup(&t2).min();
if tmax >= tmin { tmax >= tmin && tmax > 0.0
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
} }
// Intersect way with some epsilon term // Intersect with some epsilon tolerance
pub fn intersect_ray_aprox(&self, ray: &Ray) -> bool { pub fn intersect_ray_aprox(&self, ray: &Ray) -> bool {
let bln = &self.bln; let t1 = (self.bln - ray.a).component_div(&ray.b);
let trf = &self.trf; let t2 = (self.trf - ray.a).component_div(&ray.b);
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 tmin = t1.inf(&t2).max();
let tmax = t1.sup(&t2).max(); let tmax = t1.sup(&t2).min();
if tmax >= tmin { tmax >= tmin - EPSILON && tmax > 0.0
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
} }
// Get the center of this bounding box // Get the center of this bounding box
fn get_centroid(&self) -> Point3<f64> { fn get_centroid(&self) -> Point3<f64> {
@@ -126,6 +108,7 @@ impl AABB {
self.trf.y.max(other.trf.y), self.trf.y.max(other.trf.y),
self.trf.z.max(other.trf.z), 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 //Grow the AABB to contain the cover the point
pub fn grow(&self, other: &Point3<f64>) -> AABB { pub fn grow(&self, other: &Point3<f64>) -> AABB {
@@ -154,6 +137,7 @@ impl AABB {
self.trf.y.max(other.y), self.trf.y.max(other.y),
self.trf.z.max(other.z), self.trf.z.max(other.z),
); );
self.centroid = self.bln + (self.trf - self.bln) / 2.0;
} }
// Size of AABB // Size of AABB
pub fn size(&self) -> Vector3<f64> { pub fn size(&self) -> Vector3<f64> {
@@ -304,7 +288,7 @@ impl BVH {
// let mut best_pos = 0.0; // let mut best_pos = 0.0;
// let mut best_cost = 1e30; // let mut best_cost = 1e30;
// let first_prim_idx = self.bvh_nodes[index].first_prim; // 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 { // for i in 0..self.bvh_nodes[index].prim_count {
// let node = &self.nodes[first_prim_idx + i]; // let node = &self.nodes[first_prim_idx + i];
// //Get the centroid of the bounding box // //Get the centroid of the bounding box
@@ -391,20 +375,22 @@ impl BVH {
return None; return None;
} }
if bvh_node.prim_count != 0 { if bvh_node.prim_count != 0 {
// Leaf node intersection // Leaf node - check all primitives it contains
let node_idx = bvh_node.first_prim; let mut closest: Option<(&Node, Intersection)> = None;
let node = &self.nodes[node_idx]; let mut closest_dist = f64::MAX;
if !node.active { for i in 0..bvh_node.prim_count {
return None; let node = &self.nodes[bvh_node.first_prim + i];
} if !node.active {
if let Some(intersect) = node.intersect_ray(&ray) { continue;
if intersect.distance < EPSILON { }
return None; if let Some(intersect) = node.intersect_ray(&ray) {
} else { if intersect.distance >= EPSILON && intersect.distance < closest_dist {
return Some((node, intersect)); closest_dist = intersect.distance;
closest = Some((node, intersect));
}
} }
} }
return None; return closest;
} else { } else {
//Recurse down the BVH //Recurse down the BVH
//Recurse down the BVH right node //Recurse down the BVH right node
@@ -438,17 +424,14 @@ impl BVH {
let aabb = self.nodes[node.first_prim + i].get_world_aabb(); let aabb = self.nodes[node.first_prim + i].get_world_aabb();
if aabb.trf[axis] < pos { if aabb.trf[axis] < pos {
l_count += 1; l_count += 1;
l_aabb.grow_mut(&aabb.trf); l_aabb.join_mut(&aabb);
} else { } else {
r_count += 1; 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(); let cost = l_count as f64 * l_aabb.area() + r_count as f64 * r_aabb.area();
match cost > 0.0 { if cost > 0.0 { cost } else { 1e30 }
true => 0.0,
false => 1e30,
}
} }
} }

View File

@@ -51,6 +51,66 @@ impl Camera {
self.recalculate_matrix(); self.recalculate_matrix();
} }
/// Get the forward direction vector (from eye toward target)
pub fn forward(&self) -> Vector3<f64> {
(self.target - self.eye).normalize()
}
/// Get the right direction vector
pub fn right(&self) -> Vector3<f64> {
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 /// Recalculate the view and inverse view matrices based on the current eye, target, and up vectors
fn recalculate_matrix(&mut self) { fn recalculate_matrix(&mut self) {
self._view = Matrix4::look_at_lh(&self.eye, &self.target, &self.up); self._view = Matrix4::look_at_lh(&self.eye, &self.target, &self.up);

View File

@@ -11,7 +11,7 @@ use imgui::*;
use nalgebra::{Point3, Vector3}; use nalgebra::{Point3, Vector3};
use pixels::{wgpu, PixelsContext}; use pixels::{wgpu, PixelsContext};
use rhai::Engine; use rhai::Engine;
use std::time::Instant; use std::time::{Duration, Instant};
//BUFFER CONSTANTS //BUFFER CONSTANTS
const BUFFER_PROPORTION_MIN: f32 = 0.1; 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; const MAX_DIFFUSE_COEFFICIENT: f32 = 1.0;
//MATERIAL CONSTANTS //MATERIAL CONSTANTS
const MIN_D: f32 = 0.0;
const MIN_S: f32 = 0.0;
const MIN_SHINE: 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; const MAX_SHINE: f32 = 50.0;
//TRANSFORMATION CONSTANTS //TRANSFORMATION CONSTANTS
const MIN_COLOUR: f32 = 0.0;
const MIN_FALLOFF: f32 = 0.0; const MIN_FALLOFF: f32 = 0.0;
const MIN_SCALE: f64 = 0.0; const MIN_SCALE: f64 = 0.0;
//const MIN_POSITION: f64 = -10.0; //const MIN_POSITION: f64 = -10.0;
const MIN_ROTATION: f64 = -180.0; const MIN_ROTATION: f64 = -180.0;
const MIN_TRANSLATE: f64 = -10.0; const MIN_TRANSLATE: f64 = -10.0;
//-- //--
const MAX_COLOUR: f32 = 1.0;
const MAX_FALLOFF: f32 = 1.0; const MAX_FALLOFF: f32 = 1.0;
const MAX_SCALE: f64 = 3.0; const MAX_SCALE: f64 = 3.0;
//const MAX_POSITION: f64 = 10.0; //const MAX_POSITION: f64 = 10.0;
@@ -81,6 +75,9 @@ pub struct Gui {
pub event: Option<GuiEvent>, pub event: Option<GuiEvent>,
render_start: Option<Instant>,
render_elapsed: Option<Duration>,
script_filename: String, script_filename: String,
script: String, script: String,
engine: Engine, engine: Engine,
@@ -141,6 +138,9 @@ impl Gui {
last_cursor: None, last_cursor: None,
event: None, event: None,
render_start: None,
render_elapsed: None,
script_filename: String::from(INIT_FILE), script_filename: String::from(INIT_FILE),
script: String::new(), script: String::new(),
engine: init_engine(), engine: init_engine(),
@@ -171,6 +171,17 @@ impl Gui {
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. /// Prepare Dear ImGui.
pub fn prepare( pub fn prepare(
&mut self, &mut self,
@@ -216,25 +227,42 @@ impl Gui {
&mut self.raytracing_option.threads, &mut self.raytracing_option.threads,
); );
// Numbers of rays to render per pass // Numbers of rays to render per pass
ui.slider( Drag::new("Rays Per Pass")
"Rays Per Pass", .range(RAYS_MIN, RAYS_MAX)
RAYS_MIN, .speed(50.0)
RAYS_MAX, .build(ui, &mut self.raytracing_option.pixels_per_thread);
&mut self.raytracing_option.pixels_per_thread,
);
// Proportion of the window the buffer occupies // Proportion of the window the buffer occupies
ui.slider( Drag::new("% Buffer: ")
"% Buffer: ", .range(BUFFER_PROPORTION_MIN, BUFFER_PROPORTION_MAX)
BUFFER_PROPORTION_MIN, .speed(0.005)
BUFFER_PROPORTION_MAX, .display_format("%.2f")
&mut self.raytracing_option.buffer_proportion, .build(ui, &mut self.raytracing_option.buffer_proportion);
);
//Clear colour for scene //Clear colour for scene
ui.slider_config("Clear Colour", 0, 255) let mut clear_f32 = [
.build_array(&mut self.raytracing_option.clear_color); 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 //Clear colour if no intersect
ui.slider_config("Pixel Clear Colour", 0, 255) let mut pixel_clear_f32 = [
.build_array(&mut self.raytracing_option.pixel_clear); 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 //Ray depth slider
ui.slider( ui.slider(
"Ray Depth", "Ray Depth",
@@ -250,12 +278,11 @@ impl Gui {
&mut self.raytracing_option.ray_samples, &mut self.raytracing_option.ray_samples,
); );
//Ray randomness //Ray randomness
ui.slider( Drag::new("Ray Randomness")
"Ray Randomness", .range(MIN_RANDOM, MAX_RANDOM)
MIN_RANDOM, .speed(5.0)
MAX_RANDOM, .display_format("%.1f")
&mut self.raytracing_option.ray_randomness, .build(ui, &mut self.raytracing_option.ray_randomness);
);
//Number of diffuse rays //Number of diffuse rays
ui.slider( ui.slider(
"Diffuse Rays", "Diffuse Rays",
@@ -264,12 +291,11 @@ impl Gui {
&mut self.raytracing_option.diffuse_rays, &mut self.raytracing_option.diffuse_rays,
); );
//Diffuse Coefficient //Diffuse Coefficient
ui.slider( Drag::new("Diffuse Coefficient")
"Diffuse Coefficient", .range(MIN_DIFFUSE_COEFFICIENT, MAX_DIFFUSE_COEFFICIENT)
MIN_DIFFUSE_COEFFICIENT, .speed(0.005)
MAX_DIFFUSE_COEFFICIENT, .display_format("%.3f")
&mut self.raytracing_option.diffuse_coefficient, .build(ui, &mut self.raytracing_option.diffuse_coefficient);
);
// Fov of the buffer // Fov of the buffer
ui.slider( ui.slider(
"fov", "fov",
@@ -283,6 +309,15 @@ impl Gui {
ui.checkbox("Enable Reflections", &mut self.raytracing_option.reflect); ui.checkbox("Enable Reflections", &mut self.raytracing_option.reflect);
ui.checkbox("Enable Specular", &mut self.raytracing_option.specular); ui.checkbox("Enable Specular", &mut self.raytracing_option.specular);
ui.checkbox("Enable Diffuse", &mut self.raytracing_option.diffuse); 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 // Apply stored changes
if ui.button("Apply") { if ui.button("Apply") {
self.event = Some(GuiEvent::RaytracerOption(self.raytracing_option.clone())); self.event = Some(GuiEvent::RaytracerOption(self.raytracing_option.clone()));
@@ -292,12 +327,21 @@ impl Gui {
if CollapsingHeader::new("Camera").build(ui) { if CollapsingHeader::new("Camera").build(ui) {
// Eye, target and up vector inputs // Eye, target and up vector inputs
ui.text("Camera options:"); ui.text("Camera options:");
ui.slider_config("Eye", MIN_TRANSLATE, MAX_TRANSLATE) Drag::new("Eye")
.build_array(self.camera.eye.coords.as_mut_slice()); .range(MIN_TRANSLATE, MAX_TRANSLATE)
ui.slider_config("Target", MIN_TRANSLATE, MAX_TRANSLATE) .speed(0.05)
.build_array(self.camera.target.coords.as_mut_slice()); .display_format("%.2f")
ui.slider_config("Up", 0.0, 1.0) .build_array(ui, self.camera.eye.coords.as_mut_slice());
.build_array(self.camera.up.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") { if ui.button("Apply Camera") {
println!("Camera changed"); println!("Camera changed");
self.event = Some(GuiEvent::CameraUpdate(self.camera.clone())); self.event = Some(GuiEvent::CameraUpdate(self.camera.clone()));
@@ -361,12 +405,21 @@ impl Gui {
ui.checkbox(format!("##active{label}"), &mut node.active); ui.checkbox(format!("##active{label}"), &mut node.active);
ui.same_line(); ui.same_line();
if let Some(_t) = ui.tree_node(label) { if let Some(_t) = ui.tree_node(label) {
ui.slider_config("Translation", MIN_TRANSLATE, MAX_TRANSLATE) Drag::new("Translation")
.build_array(&mut node.translation); .range(MIN_TRANSLATE, MAX_TRANSLATE)
ui.slider_config("Rotation", MIN_ROTATION, MAX_ROTATION) .speed(0.05)
.build_array(&mut node.rotation); .display_format("%.2f")
ui.slider_config("Scale", MIN_SCALE, MAX_SCALE) .build_array(ui, &mut node.translation);
.build_array(&mut node.scale); 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") { if let Some(_t) = ui.tree_node("Materials") {
for (label, material) in &mut self.scene.materials { for (label, material) in &mut self.scene.materials {
if let Some(_t) = ui.tree_node(label) { if let Some(_t) = ui.tree_node(label) {
ui.slider_config("ks", MIN_D, MAX_D) let mut ks_arr: [f32; 3] = material.ks.into();
.build_array(material.ks.as_mut_slice()); if ui.color_edit3("ks", &mut ks_arr) {
ui.slider_config("kd", MIN_S, MAX_S) material.ks = Vector3::from(ks_arr);
.build_array(material.kd.as_mut_slice()); }
ui.slider("shine", MIN_SHINE, MAX_SHINE, &mut material.shininess); 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.checkbox(format!("##activelight{label}"), &mut light.active);
ui.same_line(); ui.same_line();
if let Some(_t) = ui.tree_node(label) { if let Some(_t) = ui.tree_node(label) {
ui.slider_config("Colour", MIN_COLOUR, MAX_COLOUR) let mut colour_arr: [f32; 3] = light.colour.into();
.build_array(light.colour.as_mut_slice()); if ui.color_edit3("Colour", &mut colour_arr) {
ui.slider_config("Position", MIN_TRANSLATE, MAX_TRANSLATE) light.colour = Vector3::from(colour_arr);
.build_array(light.position.coords.as_mut_slice()); }
ui.slider_config("Falloff", MIN_FALLOFF, MAX_FALLOFF) Drag::new("Position")
.build_array(light.falloff.as_mut_slice()); .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. /// Handle any outstanding events.
pub fn handle_event( pub fn handle_event(
&mut self, &mut self,

View File

@@ -99,17 +99,19 @@ impl Node {
// Compute the inverse model matrix by inverting the model matrix // Compute the inverse model matrix by inverting the model matrix
self.inv_model = self.model.try_inverse().unwrap(); self.inv_model = self.model.try_inverse().unwrap();
self.inv_transpose_model = self.inv_model.transpose().remove_row(3).remove_column(3); self.inv_transpose_model = self.inv_model.transpose().remove_row(3).remove_column(3);
// Reset AABB from primitive local space before transforming to world space
self.aabb = self.primitive.get_aabb();
self.aabb.transform_mut(&self.model); self.aabb.transform_mut(&self.model);
} }
// Intersection of a ray, will convert to model coords and check // Intersection of a ray, will convert to model coords and check
pub fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> { pub fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let ray = ray.transform(&self.inv_model); //Transform from world coordinates let local_ray = ray.transform(&self.inv_model); //Transform from world coordinates
if let Some(mut intersect) = self.primitive.intersect_ray(&ray) { if let Some(mut intersect) = self.primitive.intersect_ray(&local_ray) {
if intersect.distance < EPSILON { if intersect.distance < EPSILON {
return None; return None;
} }
intersect.transform_mut(&self.model, &self.inv_transpose_model); //Transform to world coords 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, &ray.a); // use world-space ray origin
return Some(intersect); return Some(intersect);
} }
return None; return None;

View File

@@ -47,14 +47,13 @@ impl Primitive for Sphere {
Roots::No(_) => return None, Roots::No(_) => return None,
Roots::One([x1]) => x1, Roots::One([x1]) => x1,
Roots::Two([x1, x2]) => { Roots::Two([x1, x2]) => {
// roots are returned in ascending order: x1 <= x2
if x1 <= 0.0 && x2 <= 0.0 { if x1 <= 0.0 && x2 <= 0.0 {
return None; return None;
} else if x1 <= 0.0 {
x2
} else { } else {
if x1.abs() < x2.abs() { x1
x1
} else {
x2
}
} }
} }
_ => return None, _ => return None,
@@ -124,9 +123,9 @@ impl Primitive for Circle {
let n_dot_b = ray.b.dot(&self.normal); let n_dot_b = ray.b.dot(&self.normal);
let t = (self.constant - n_dot_a) / n_dot_b; let t = (self.constant - n_dot_a) / n_dot_b;
if t > INFINITY { if t <= 0.0 || t > INFINITY {
return None; return None;
}; }
let intersect = ray.at_t(t); let intersect = ray.at_t(t);
//Distance to center of circle //Distance to center of circle
@@ -197,14 +196,13 @@ impl Primitive for Cylinder {
Roots::No(_) => return None, Roots::No(_) => return None,
Roots::One([x1]) => Some(x1), Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => { Roots::Two([x1, x2]) => {
// roots are returned in ascending order: x1 <= x2
if x1 <= 0.0 && x2 <= 0.0 { if x1 <= 0.0 && x2 <= 0.0 {
return None; return None;
} else if x1 <= 0.0 {
Some(x2)
} else { } else {
if x1.abs() < x2.abs() { Some(x1)
Some(x1)
} else {
Some(x2)
}
} }
} }
_ => return None, _ => return None,
@@ -325,14 +323,13 @@ impl Primitive for Cone {
Roots::No(_) => None, Roots::No(_) => None,
Roots::One([x1]) => Some(x1), Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => { Roots::Two([x1, x2]) => {
// roots are returned in ascending order: x1 <= x2
if x1 <= 0.0 && x2 <= 0.0 { if x1 <= 0.0 && x2 <= 0.0 {
None None
} else if x1 <= 0.0 {
Some(x2)
} else { } else {
if x1.abs() < x2.abs() { Some(x1)
Some(x1)
} else {
Some(x2)
}
} }
} }
_ => None, _ => None,
@@ -359,7 +356,15 @@ impl Primitive for Cone {
(None, None) => None, (None, None) => None,
(Some(cone_intersect), None) => Some(cone_intersect), (Some(cone_intersect), None) => Some(cone_intersect),
(None, Some(circle_intersect)) => Some(circle_intersect), (None, Some(circle_intersect)) => Some(circle_intersect),
(Some(cone_intersect), Some(_)) => Some(cone_intersect), (Some(cone_intersect), Some(circle_intersect)) => {
let cone_dist = distance(&ray.a, &cone_intersect.point);
let circle_dist = distance(&ray.a, &circle_intersect.point);
if cone_dist < circle_dist {
Some(cone_intersect)
} else {
Some(circle_intersect)
}
}
} }
} }
@@ -395,7 +400,7 @@ impl Primitive for RectangleXY {
let az = ray.a.z; let az = ray.a.z;
let bz = ray.b.z; let bz = ray.b.z;
let t = (z - az) / bz; let t = (z - az) / bz;
if t > INFINITY { if t <= 0.0 || t > INFINITY {
return None; return None;
} }
let intersect = ray.at_t(t); let intersect = ray.at_t(t);
@@ -470,21 +475,28 @@ impl Primitive for Cube {
return None; // Intersection is outside the box return None; // Intersection is outside the box
} }
//Get normal of intersection point // Determine which face was hit by finding the t-value closest to tmin
//t1 is bln t2 is trf let diffs = [
let normal = if tmin == t1.x { (t1.x - tmin).abs(),
Vector3::new(-1.0, 0.0, 0.0) (t1.y - tmin).abs(),
} else if tmin == t1.y { (t1.z - tmin).abs(),
Vector3::new(0.0, -1.0, 0.0) (t2.x - tmin).abs(),
} else if tmin == t1.z { (t2.y - tmin).abs(),
Vector3::new(0.0, 0.0, -1.0) (t2.z - tmin).abs(),
} else if tmin == t2.x { ];
Vector3::new(1.0, 0.0, 0.0) let normals = [
} else if tmin == t2.y { Vector3::new(-1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0) Vector3::new(0.0, -1.0, 0.0),
} else { Vector3::new(0.0, 0.0, -1.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 { Some(Intersection {
point: intersect, point: intersect,
@@ -645,9 +657,9 @@ impl Mesh {
let u = vertices[v1 - 1]; let u = vertices[v1 - 1];
let v = vertices[v2 - 1]; let v = vertices[v2 - 1];
let w = vertices[v3 - 1]; let w = vertices[v3 - 1];
let uv = u - v; let uv = v - u;
let uw = w - v; let uw = w - u;
let normal = uv.cross(&uw).normalize(); let normal = uw.cross(&uv).normalize();
triangles.push(Triangle { u, v, w, normal }); triangles.push(Triangle { u, v, w, normal });
} }
} }
@@ -805,9 +817,9 @@ impl Primitive for Torus {
} }
fn get_aabb(&self) -> AABB { fn get_aabb(&self) -> AABB {
//TODO! let extent = self.inner_rad + self.outer_rad;
let trf = Point3::new(1.0, 1.0, 1.0); let bln = Point3::new(-extent, -extent, -self.outer_rad);
let bln = Point3::new(-1.0, -1.0, -1.0); let trf = Point3::new(extent, extent, self.outer_rad);
AABB::new(bln, trf) AABB::new(bln, trf)
} }
} }
@@ -846,19 +858,19 @@ impl Gnonom {
impl Primitive for Gnonom { impl Primitive for Gnonom {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> { fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
match self.x_cube.intersect_ray(ray) { let mut closest: Option<Intersection> = None;
Some(intersect) => return Some(intersect), let mut closest_dist = f64::MAX;
None => (),
}; for cube in [&self.x_cube, &self.y_cube, &self.z_cube] {
match self.y_cube.intersect_ray(ray) { if let Some(intersect) = cube.intersect_ray(ray) {
Some(intersect) => return Some(intersect), let dist = distance(&ray.a, &intersect.point);
None => (), if dist < closest_dist {
}; closest_dist = dist;
match self.z_cube.intersect_ray(ray) { closest = Some(intersect);
Some(intersect) => return Some(intersect), }
None => (), }
}; }
None closest
} }
fn get_aabb(&self) -> AABB { fn get_aabb(&self) -> AABB {

View File

@@ -3,7 +3,11 @@ use nalgebra::{distance, Matrix3, Matrix4, Point3, Vector3};
use rand; use rand;
fn random_vec() -> Vector3<f64> { fn random_vec() -> Vector3<f64> {
Vector3::new(rand::random(), rand::random(), rand::random()) Vector3::new(
rand::random::<f64>() * 2.0 - 1.0,
rand::random::<f64>() * 2.0 - 1.0,
rand::random::<f64>() * 2.0 - 1.0,
)
} }
fn random_unit_vec() -> Vector3<f64> { fn random_unit_vec() -> Vector3<f64> {
random_vec().normalize() random_vec().normalize()
@@ -165,9 +169,30 @@ impl Ray {
let incidence = &ray.b; let incidence = &ray.b;
let material = &node.material; let material = &node.material;
// Compute the ambient light component and set it as base colour
let mut colour = Vector3::zeros(); 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 { for (_, light) in &scene.lights {
if !light.active { if !light.active {
continue; continue;
@@ -192,27 +217,10 @@ impl Ray {
let n_dot_l = normal.dot(&to_light).max(0.0) as f32; let n_dot_l = normal.dot(&to_light).max(0.0) as f32;
//Reflected component //Direct diffuse component (Lambertian)
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)
let mut diffuse = Vector3::zeros(); let mut diffuse = Vector3::zeros();
if options.diffuse { if options.diffuse {
diffuse += material.kd * n_dot_l; 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 //Specular component
@@ -234,10 +242,13 @@ impl Ray {
+ light.falloff[2] * light_distance * light_distance); + 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; colour += &intensity;
} }
// Add light-independent terms
colour += reflect + indirect;
colour colour
} }
@@ -246,20 +257,9 @@ impl Ray {
match bvh { match bvh {
Some(bvh) => { Some(bvh) => {
//We have a bvh so use bvh traversal //We have a bvh so use bvh traversal
for (_, node) in &scene.nodes { if let Some((_, intersect)) = bvh.traverse(self, 0) {
if !node.active { return intersect.distance < light_distance;
continue;
}
match bvh.traverse(self, 0) {
Some((_, intersect)) => {
if intersect.distance < light_distance {
return true;
}
}
None => continue,
}
} }
return false;
} }
None => { None => {
for (_, node) in &scene.nodes { for (_, node) in &scene.nodes {

View File

@@ -5,6 +5,7 @@ use crate::camera::Camera;
use crate::ray::Ray; use crate::ray::Ray;
use crate::{gui::Gui, scene::Scene}; use crate::{gui::Gui, scene::Scene};
use crate::{gui::GuiEvent, log_error}; use crate::{gui::GuiEvent, log_error};
use std::collections::HashSet;
use std::path::Path; use std::path::Path;
use std::thread; use std::thread;
@@ -13,12 +14,15 @@ use rand::seq::SliceRandom;
use rand::{random, thread_rng}; use rand::{random, thread_rng};
use std::error::Error; use std::error::Error;
use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use anyhow::Result; use anyhow::Result;
use pixels::{Pixels, SurfaceTexture}; use pixels::{Pixels, SurfaceTexture};
use winit::dpi::{LogicalSize, PhysicalSize}; 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::event_loop::{ControlFlow, EventLoop};
use winit::window::{Window, WindowBuilder}; 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 { pub struct State {
scene: Arc<Scene>, scene: Arc<Scene>,
bvh: Arc<Option<BVH>>, bvh: Arc<Option<BVH>>,
@@ -85,8 +92,17 @@ pub struct State {
gui: Gui, gui: Gui,
rays: Arc<Vec<Ray>>, rays: Arc<Vec<Ray>>,
ray_queue: Vec<usize>, ray_queue: Arc<Mutex<Vec<usize>>>,
raytracing_options: Arc<RaytracingOption>, raytracing_options: Arc<RaytracingOption>,
result_rx: mpsc::Receiver<Vec<(usize, [u8; 4])>>,
render_active: Arc<AtomicBool>,
rendering: bool,
keys_pressed: HashSet<VirtualKeyCode>,
right_mouse_down: bool,
last_mouse_pos: Option<(f64, f64)>,
camera_dirty: bool,
} }
impl State { impl State {
@@ -96,6 +112,7 @@ impl State {
let pixels = pixels; let pixels = pixels;
let camera = Camera::unit(); let camera = Camera::unit();
let rays = Arc::new(Vec::new()); let rays = Arc::new(Vec::new());
let (_tx, rx) = mpsc::channel();
Self { Self {
scene, scene,
@@ -107,8 +124,15 @@ impl State {
pixels, pixels,
gui, gui,
rays, rays,
ray_queue: Vec::new(), ray_queue: Arc::new(Mutex::new(Vec::new())),
raytracing_options: Arc::new(RaytracingOption::default()), 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) { fn keyboard_input(&mut self, key: &KeyboardInput) {
if let Some(VirtualKeyCode::A) = key.virtual_keycode { if let Some(keycode) = key.virtual_keycode {
// Handle 'A' key event here match key.state {
} ElementState::Pressed => {
} self.keys_pressed.insert(keycode);
}
fn mouse_input(&mut self, _button: &MouseButton) { ElementState::Released => {
// Handle mouse input here self.keys_pressed.remove(&keycode);
}
fn draw(&mut self) -> Result<(), Box<dyn Error>> {
//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,
} }
} }
//The finished queue of the thread }
let mut finished = vec![]; }
//Create a new thread for these pixels fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) {
let handle = thread::spawn({ if *button == MouseButton::Right {
move || { self.right_mouse_down = *state == ElementState::Pressed;
for index in &load { if !self.right_mouse_down {
//Shade colour for selected index self.last_mouse_pos = None;
let mut colour: Vector3<f32> = 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::<f64>() - 0.5) / randomness;
let ry = (random::<f64>() - 0.5) / randomness;
let rz = (random::<f64>() - 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)); 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) fn process_camera_movement(&mut self) {
{ let speed = CAMERA_MOVE_SPEED;
colour += ray_colour;
} if self.keys_pressed.contains(&VirtualKeyCode::W) {
} self.camera.move_forward(speed);
colour = (colour / samples_f32) * 255.0; self.camera_dirty = true;
let rgba = [colour.x as u8, colour.y as u8, colour.z as u8, 0xff]; }
finished.push(rgba); 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);
} }
}); Err(mpsc::TryRecvError::Empty) => break,
handles.push(handle); 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<dyn Error>> { fn clear_buffer(&mut self) -> Result<(), Box<dyn Error>> {
@@ -295,26 +330,113 @@ impl State {
} }
fn reset_queue(&mut self) { fn reset_queue(&mut self) {
// Signal any existing workers to stop
self.render_active.store(false, Ordering::Relaxed);
match self.raytracing_options.bvh_active { match self.raytracing_options.bvh_active {
true => self.bvh = Arc::new(Some(BVH::build(&self.scene.nodes))), true => self.bvh = Arc::new(Some(BVH::build(&self.scene.nodes))),
false => self.bvh = Arc::new(None), false => self.bvh = Arc::new(None),
} }
// Create new shuffled queue
let size = self.buffer_height as usize * self.buffer_width as usize; let size = self.buffer_height as usize * self.buffer_width as usize;
let mut ray_queue: Vec<usize> = (0..size).collect(); let mut ray_queue: Vec<usize> = (0..size).collect();
ray_queue.shuffle(&mut thread_rng()); 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<usize> = {
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<f32> = Vector3::zeros();
let ray = &rays[*index];
for _ in 0..samples {
let point = ray.a;
let dir = ray.b;
let rx = (random::<f64>() - 0.5) / randomness;
let ry = (random::<f64>() - 0.5) / randomness;
let rz = (random::<f64>() - 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<dyn Error>> { fn render(&mut self) -> Result<(), Box<dyn Error>> {
// Update state // Update state
self.update()?; self.update()?;
// Draw rays if we have remaining rays in queue // Collect completed rays from background workers
match self.draw() { self.draw();
Err(e) => {
println!("ERROR: {}", e);
}
_ => {}
}
// Render Gui // Render Gui
self.gui self.gui
.prepare(&self.window) .prepare(&self.window)
@@ -355,11 +477,17 @@ pub fn run() -> Result<(), Box<dyn Error>> {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Resized(size) => state.resize(&size).expect("Window Resize Error"), WindowEvent::Resized(size) => state.resize(&size).expect("Window Resize Error"),
WindowEvent::KeyboardInput { input, .. } => state.keyboard_input(&input), 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(_) => { Event::RedrawRequested(_) => {
state.process_camera_movement();
if let Err(_e) = state.render() { if let Err(_e) = state.render() {
*control_flow = ControlFlow::Exit; *control_flow = ControlFlow::Exit;
} }