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
This is my graphics project that I will be working on for A5
I will use rlua for interacting with lua files
# Introduction
## 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()
P()
Scene()
Scene.addNode()
Scene.addLight()
Node()
Node.translate()
Node.rotate()
Node.scale()
Camera()
Light()
Material()
MaterialRed()
MaterialBlue()
MaterialGreen()
MaterialMagenta()
MaterialTurquoise()
Sphere()
SphereUnit()
Cube()
CubeUnit()
Cone()
ConeUnit()
Cyclinder()
//CylinderUnit()
Circle()
CircleUnit()
Rectangle()
RectangleUnit()
Steiner()
Torus()
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_.
# Installation
Clone and run with `cargo run`, however much better performance will be granted with `cargo run --release`.
![example](img/example.png)
# Rhai
Rhai is used as an interactive scripting lang for this project. Examples are found in `rhai/`.
## Full List of rhai commands
```
/// Basic math types
V(x : float, y : float, z : float) -> Vector3
// 3dimensional vector, used for directions, colors, etc.
P(x : float, y : float, z : float) -> Position3
// 3dimensional position vector, used for points in space.
/// Scene and graph
Scene() -> Scene
// Create an empty scene with no nodes, lights, or camera.
Scene.addNode(node : Node) -> void
// 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 {
// New box with respective coordinates
pub fn new(bln: Point3<f64>, trf: Point3<f64>) -> 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 }
}
@@ -36,64 +36,46 @@ impl AABB {
}
//Apply a matrix transformation to a box
pub fn transform_mut(&mut self, mat: &Matrix4<f64>) {
let bln = &mut self.bln;
let trf = &mut self.trf;
let centroid = &mut self.centroid;
self.bln = mat.transform_point(bln);
self.trf = mat.transform_point(trf);
self.centroid = mat.transform_point(centroid);
let corners = [
Point3::new(self.bln.x, self.bln.y, self.bln.z),
Point3::new(self.trf.x, self.bln.y, self.bln.z),
Point3::new(self.bln.x, self.trf.y, self.bln.z),
Point3::new(self.trf.x, self.trf.y, self.bln.z),
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
pub fn intersect_ray(&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 t1 = (self.bln - ray.a).component_div(&ray.b);
let t2 = (self.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 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 t1 = (self.bln - ray.a).component_div(&ray.b);
let t2 = (self.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 > 0.0
}
// Get the center of this bounding box
fn get_centroid(&self) -> Point3<f64> {
@@ -126,6 +108,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<f64>) -> AABB {
@@ -154,6 +137,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<f64> {
@@ -304,7 +288,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 +375,22 @@ 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 - check all primitives it contains
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 && 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,17 +424,14 @@ 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,
false => 1e30,
}
if cost > 0.0 { cost } else { 1e30 }
}
}

View File

@@ -51,6 +51,66 @@ impl Camera {
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
fn recalculate_matrix(&mut self) {
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 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<GuiEvent>,
render_start: Option<Instant>,
render_elapsed: Option<Duration>,
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,

View File

@@ -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);
// Reset AABB from primitive local space before transforming to world space
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<Intersection> {
let ray = ray.transform(&self.inv_model); //Transform from world coordinates
if let Some(mut intersect) = self.primitive.intersect_ray(&ray) {
let local_ray = ray.transform(&self.inv_model); //Transform from world coordinates
if let Some(mut intersect) = self.primitive.intersect_ray(&local_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, &ray.a); // use world-space ray origin
return Some(intersect);
}
return None;

View File

@@ -47,14 +47,13 @@ impl Primitive for Sphere {
Roots::No(_) => return None,
Roots::One([x1]) => x1,
Roots::Two([x1, x2]) => {
// roots are returned in ascending order: x1 <= x2
if x1 <= 0.0 && x2 <= 0.0 {
return None;
} else if x1 <= 0.0 {
x2
} else {
if x1.abs() < x2.abs() {
x1
} else {
x2
}
x1
}
}
_ => return None,
@@ -124,9 +123,9 @@ 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 <= 0.0 || t > INFINITY {
return None;
};
}
let intersect = ray.at_t(t);
//Distance to center of circle
@@ -197,14 +196,13 @@ impl Primitive for Cylinder {
Roots::No(_) => return None,
Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => {
// roots are returned in ascending order: x1 <= x2
if x1 <= 0.0 && x2 <= 0.0 {
return None;
} else if x1 <= 0.0 {
Some(x2)
} else {
if x1.abs() < x2.abs() {
Some(x1)
} else {
Some(x2)
}
Some(x1)
}
}
_ => return None,
@@ -325,14 +323,13 @@ impl Primitive for Cone {
Roots::No(_) => None,
Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => {
// roots are returned in ascending order: x1 <= x2
if x1 <= 0.0 && x2 <= 0.0 {
None
} else if x1 <= 0.0 {
Some(x2)
} else {
if x1.abs() < x2.abs() {
Some(x1)
} else {
Some(x2)
}
Some(x1)
}
}
_ => None,
@@ -359,7 +356,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_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 bz = ray.b.z;
let t = (z - az) / bz;
if t > INFINITY {
if t <= 0.0 || t > INFINITY {
return None;
}
let intersect = ray.at_t(t);
@@ -470,21 +475,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 +657,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 +817,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, -extent, -self.outer_rad);
let trf = Point3::new(extent, extent, self.outer_rad);
AABB::new(bln, trf)
}
}
@@ -846,19 +858,19 @@ impl Gnonom {
impl Primitive for Gnonom {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
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<Intersection> = 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 {

View File

@@ -3,7 +3,11 @@ use nalgebra::{distance, Matrix3, Matrix4, Point3, Vector3};
use rand;
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> {
random_vec().normalize()
@@ -165,9 +169,30 @@ impl Ray {
let incidence = &ray.b;
let material = &node.material;
// 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 +217,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 +242,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,20 +257,9 @@ 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;
}
None => {
for (_, node) in &scene.nodes {

View File

@@ -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<Scene>,
bvh: Arc<Option<BVH>>,
@@ -85,8 +92,17 @@ pub struct State {
gui: Gui,
rays: Arc<Vec<Ray>>,
ray_queue: Vec<usize>,
ray_queue: Arc<Mutex<Vec<usize>>>,
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 {
@@ -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<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,
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<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;
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<dyn Error>> {
@@ -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<usize> = (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<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>> {
// 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<dyn Error>> {
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;
}