diff --git a/assignment-1d/Cargo.lock b/assignment-1d/Cargo.lock index 7d06198..5b92be1 100644 --- a/assignment-1d/Cargo.lock +++ b/assignment-1d/Cargo.lock @@ -52,6 +52,7 @@ dependencies = [ "rand", "rayon", "tracing", + "tracing-appender", "tracing-subscriber", ] @@ -785,6 +786,33 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + [[package]] name = "tracing" version = "0.1.37" @@ -797,6 +825,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.23" diff --git a/assignment-1d/Cargo.toml b/assignment-1d/Cargo.toml index a75aa13..79a6fc3 100644 --- a/assignment-1d/Cargo.toml +++ b/assignment-1d/Cargo.toml @@ -32,4 +32,5 @@ ordered-float = "3.4.0" rand = "0.8.5" rayon = "1.6.1" tracing = "0.1.37" +tracing-appender = "0.2.2" tracing-subscriber = { version = "0.3.16", features = ["json"] } diff --git a/assignment-1d/src/main.rs b/assignment-1d/src/main.rs index c51dcf6..ca4c711 100644 --- a/assignment-1d/src/main.rs +++ b/assignment-1d/src/main.rs @@ -1,8 +1,10 @@ #[macro_use] +extern crate anyhow; +#[macro_use] extern crate tracing; -use std::fs::File; use std::path::PathBuf; +use std::{fs::File, str::FromStr}; use anyhow::Result; use assignment_1d::{image::Image, ray::Ray, scene::Scene}; @@ -11,6 +13,7 @@ use clap::{ArgAction, Parser}; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use tracing::metadata::LevelFilter; +use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{ fmt::Layer, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, @@ -29,6 +32,14 @@ struct Opt { #[clap(short = 'o', long = "output")] output_path: Option, + /// Log output in json + #[clap(long = "json")] + use_json: bool, + + /// Which file to send logs to (stderr by default) + #[clap(long = "log-output")] + log_output: Option, + /// Force parallel projection to be used #[clap(long = "parallel")] force_parallel: bool, @@ -40,30 +51,16 @@ struct Opt { /// Verbosity #[clap(short, long, action = ArgAction::Count)] verbosity: u8, + + /// Evaluate at a single pixel + #[clap(short, long = "render-pixel")] + render_pixel: Option, } fn main() -> Result<()> { let opt = Opt::parse(); - let level_filter = match opt.verbosity { - 0 => LevelFilter::ERROR, - 1 => LevelFilter::WARN, - 2 => LevelFilter::INFO, - 3 => LevelFilter::DEBUG, - _ => LevelFilter::TRACE, - }; - - // Set up logging - let layer = Layer::default() - .json() - .with_target(false) - .with_timer(tracing_subscriber::fmt::time::uptime()) - .with_level(true); - - tracing_subscriber::registry() - .with(layer) - .with(level_filter) - .init(); + let _guard = setup_logging(&opt); // Rename the output file if it's not provided let out_file = opt @@ -81,6 +78,37 @@ fn main() -> Result<()> { // Translate image pixels to real-world 3d coords let translate_pixel = scene.pixel_translation_function(distance); + let evaluate_at_pixel = |px, py| { + let span = trace_span!("main_loop", px = px, py = py); + let _enter = span.enter(); + + let pixel_in_space = translate_pixel(px, py); + + let ray_start = if scene.parallel_projection { + // For a parallel projection, we'll just take the view direction and + // subtract it from the target point. This means every single + // ray will be viewed from a point at infinity, rather than a single eye + // position. + let n = scene.view_dir.normalize(); + let view_dir = n * distance; + pixel_in_space - view_dir + } else { + scene.eye_pos + }; + + let ray = Ray::from_endpoints(ray_start, pixel_in_space); + + // let res= rayon::spawn(|| scene.trace_single_ray(ray, 0)); + scene.trace_single_ray(scene.eye_pos, ray, 0) + }; + + // For debugging purposes! + if let Some(RenderPixel(px, py)) = opt.render_pixel { + let pixel_color = evaluate_at_pixel(px, py)?; + println!("Pixel color: {pixel_color}"); + return Ok(()); + } + // Generate a parallel iterator for pixels // The iterator preserves order and uses row-major order let pixels_iter = (0..scene.image_height) @@ -89,29 +117,7 @@ fn main() -> Result<()> { // Loop through every single pixel of the output file let pixels = pixels_iter - .map(|(px, py)| { - let span = trace_span!("main_loop", px = px, py = py); - let _enter = span.enter(); - - let pixel_in_space = translate_pixel(px, py); - - let ray_start = if scene.parallel_projection { - // For a parallel projection, we'll just take the view direction and - // subtract it from the target point. This means every single - // ray will be viewed from a point at infinity, rather than a single eye - // position. - let n = scene.view_dir.normalize(); - let view_dir = n * distance; - pixel_in_space - view_dir - } else { - scene.eye_pos - }; - - let ray = Ray::from_endpoints(ray_start, pixel_in_space); - - // let res= rayon::spawn(|| scene.trace_single_ray(ray, 0)); - scene.trace_single_ray(scene.eye_pos, ray, 0) - }) + .map(|(px, py)| evaluate_at_pixel(px, py)) .collect::>>()?; // Construct and emit image @@ -128,3 +134,71 @@ fn main() -> Result<()> { Ok(()) } + +#[derive(Clone)] +struct RenderPixel(usize, usize); + +impl FromStr for RenderPixel { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let parts = s.split(",").collect::>(); + ensure!(parts.len() == 2, "must be a pair"); + + let x = parts[0].parse::()?; + let y = parts[1].parse::()?; + + Ok(RenderPixel(x, y)) + } +} + +/// A little bit of engineering to make it easy to write conditional builders +/// for logging setup because the tracing-subscriber crate for some reason +/// decided it would be a good idea to have all of its builders be polymorphic? +macro_rules! logsetup_if { + ($ident:ident , $cond:expr , $iftrue:expr , $iffalse:expr => { $($body:tt)* }) => { + if ($cond) { + let $ident = $iftrue; + $($body)* + } else { + let $ident = $iffalse; + $($body)* + } + }; +} + +fn setup_logging(opt: &Opt) -> Option { + let mut result = None; + + let level_filter = match opt.verbosity { + 0 => LevelFilter::ERROR, + 1 => LevelFilter::WARN, + 2 => LevelFilter::INFO, + 3 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + }; + + let layer = Layer::default(); + + logsetup_if! (layer, opt.use_json, layer.json(), layer => { + let layer = layer + .with_target(false) + .with_timer(tracing_subscriber::fmt::time::uptime()) + .with_level(true); + + logsetup_if! (layer, opt.log_output.is_some(), { + let log_output = opt.log_output.clone().unwrap(); + let file_appender = tracing_appender::rolling::never(".", log_output); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + result = Some(guard); + layer.with_writer(non_blocking) + }, layer => { + tracing_subscriber::registry() + .with(layer) + .with(level_filter) + .init(); + }); + }); + + result +} diff --git a/assignment-1d/src/ray.rs b/assignment-1d/src/ray.rs index b5d74dc..379ac8d 100644 --- a/assignment-1d/src/ray.rs +++ b/assignment-1d/src/ray.rs @@ -1,15 +1,34 @@ +use std::fmt; + use crate::{Point, Vector}; /// A normalized parametric Ray of the form (origin + direction * time) /// /// That means at any time t: f64, the point represented by origin + direction * -/// time occurs on the ray. -#[derive(Debug)] +/// time occurs on the ray. This is pretty much a (time -> point) function. pub struct Ray { + /// The point in space where the ray started pub origin: Point, + + /// The direction the ray is headed pub direction: Vector, } +impl fmt::Debug for Ray { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({:.2}, {:.2}, {:.2}) + t * ({:.2}, {:.2}, {:.2})", + self.origin.x, + self.origin.y, + self.origin.z, + self.direction.x, + self.direction.y, + self.direction.z, + ) + } +} + impl Ray { pub fn new(origin: Point, direction: Vector) -> Self { Ray { origin, direction } diff --git a/assignment-1d/src/scene/illumination.rs b/assignment-1d/src/scene/illumination.rs index 6f663b0..0b5e7c8 100644 --- a/assignment-1d/src/scene/illumination.rs +++ b/assignment-1d/src/scene/illumination.rs @@ -11,7 +11,9 @@ use rayon::prelude::{ use crate::{ image::Color, ray::Ray, - utils::{compute_refraction_lengths, dot, RefractionResult}, + utils::{ + compute_reflection_ray, compute_refraction_lengths, dot, RefractionResult, + }, Point, Vector, }; @@ -48,8 +50,7 @@ impl Scene { intersection_context: IntersectionContext, depth: usize, ) -> Result { - let span = - trace_span!("compute_pixel_color", intersection = ?intersection_context); + let span = trace_span!("compute_pixel_color", intersection = ?intersection_context, incident_ray=?incident_ray); let _enter = span.enter(); let material = match self.materials.get(object.material_idx) { @@ -83,17 +84,7 @@ impl Scene { // The vector pointing in the direction of the light let light_direction = light.direction_from(intersection_context.point); - let normal = { - let mut normal = intersection_context.normal.normalize(); - - // If we're exiting the material, the normal should face the other direction since that's - // how the reflection works - if intersection_context.exiting { - normal = -normal; - } - - normal - }; + let normal = intersection_context.normal.normalize(); // reflection_normal(); // Viewer direction is no longer towards the eye, but to the last origin point, so that // transmitted rays reflect properly @@ -147,30 +138,6 @@ impl Scene { }) .sum(); - /* - let specular_reflection: Color = { - let reflection_ray = self.compute_reflection_ray( - incident_ray.direction, - intersection_context.normal, - ); - - let fresnel_coefficient = self.compute_fresnel_coefficient( - &material, - incident_ray.direction, - intersection_context.normal, - ); - - // Jitter a bit to reduce acne - let origin = intersection_context.point; - let origin = origin + JITTER_CONST * reflection_ray; - - let ray = Ray::new(origin, reflection_ray); - let r_lambda = self.trace_single_ray(origin, ray, depth + 1)?; - - fresnel_coefficient * r_lambda - }; - */ - let (eta_i, eta_t) = match intersection_context.exiting { // true => (material.eta, 1.0), _ => (1.0, material.eta), @@ -186,7 +153,7 @@ impl Scene { )? }; - let transparency_component = if eta_t < 1.0 { + let transparency_component = if eta_t < 1.0 || material.alpha == 1.0 { ZERO_COLOR } else { self.compute_transparency( @@ -212,6 +179,14 @@ impl Scene { * (1.0 - material.alpha) * transparency_component }; + debug!( + last_time_component = ?(ambient_component + diffuse_and_specular), + ?specular_reflection_component, + ?transparency_component, + ?fresnel_coefficient, + ?color, + "color result" + ); // Apply depth cueing to the result let a_dc = { @@ -412,30 +387,15 @@ impl Scene { fr } - fn compute_reflection_ray( - &self, - incident_ray: Vector, - normal: Vector, - ) -> Vector { - let opposite_incident_ray = (-incident_ray).normalize(); - let unit_normal = normal.normalize(); - - let a = dot(unit_normal, opposite_incident_ray); - let r = 2.0 * (a * unit_normal) - opposite_incident_ray; - - r - } - fn compute_specular_reflection( &self, intersection_context: &IntersectionContext, incident_ray: &Ray, depth: usize, ) -> Result { - // Specular reflection - let reflection_ray = self.compute_reflection_ray( + let reflection_ray = compute_reflection_ray( incident_ray.direction.clone(), - intersection_context.normal, + intersection_context.reflection_normal().normalize(), ); let origin = intersection_context.point; @@ -458,14 +418,7 @@ impl Scene { let _enter = span.enter(); // Fix the normal direction to account for exiting a material - let normal = { - let mut n = intersection_context.normal.normalize(); - if intersection_context.exiting { - n = -n; - } - - n - }; + let normal = intersection_context.reflection_normal().normalize(); let i = incident_ray.direction.normalize(); @@ -484,7 +437,7 @@ impl Scene { match compute_refraction_lengths(normal, &incident_ray, eta_i, eta_t) { Some(RefractionResult { cos_theta_i, - sin_theta_i, + sin_theta_i: _, sin_theta_t, cos_theta_t, }) => { @@ -493,7 +446,7 @@ impl Scene { // new direction. // Calculate refraction direction - let a = normal.normalize() * cos_theta_t; + let a = normal * cos_theta_t; let s_direction = cos_theta_i * normal - i; let m_unit = s_direction.normalize(); let b = m_unit * sin_theta_t; @@ -550,4 +503,13 @@ pub struct IntersectionContext { impl Eq for IntersectionContext {} -impl IntersectionContext {} +impl IntersectionContext { + // If we're exiting the material, the normal should face the other direction + // since that's how the reflection works + pub fn reflection_normal(&self) -> Vector { + match self.exiting { + true => -self.normal, + false => self.normal, + } + } +} diff --git a/assignment-1d/src/scene/tracing.rs b/assignment-1d/src/scene/tracing.rs index fb733fa..d6a0020 100644 --- a/assignment-1d/src/scene/tracing.rs +++ b/assignment-1d/src/scene/tracing.rs @@ -44,6 +44,8 @@ impl Scene { let earliest_intersection = intersections.into_iter().min_by_key(|(_, t, _)| t.time); + info!("Ray {ray:?} intersected at: {earliest_intersection:?}"); + Ok(match earliest_intersection { // Take the object's material color Some((obj_idx, intersection_context, object)) => self diff --git a/assignment-1d/src/scene/triangle.rs b/assignment-1d/src/scene/triangle.rs index be994a8..9a23f63 100644 --- a/assignment-1d/src/scene/triangle.rs +++ b/assignment-1d/src/scene/triangle.rs @@ -61,6 +61,11 @@ impl Triangle { -(a * x0 + b * y0 + c * z0 + d) / denom }; + // Intersected the plane behind where the ray started + if time < 0.0 { + return Ok(None); + } + let time = NotNan::new(time)?; let point = ray.eval(*time); diff --git a/assignment-1d/src/utils.rs b/assignment-1d/src/utils.rs index 0df891c..9c67284 100644 --- a/assignment-1d/src/utils.rs +++ b/assignment-1d/src/utils.rs @@ -134,3 +134,27 @@ pub fn compute_refraction_lengths( cos_theta_t, }) } + +#[allow(non_snake_case)] +pub fn compute_reflection_ray(incident_ray: Vector, normal: Vector) -> Vector { + let I = (-incident_ray).normalize(); + let N = normal.normalize(); + + 2.0 * dot(N, I) * N - I +} + +#[cfg(test)] +mod tests { + use crate::{utils::compute_reflection_ray, Vector}; + + #[test] + fn test_reflection_ray() { + let incident_ray = Vector::new(2.0, -1.0, 2.0); + let normal = Vector::new(0.0, 1.0, 0.0); + + assert_eq!( + compute_reflection_ray(incident_ray, normal), + Vector::new(2.0, 1.0, 2.0).normalize() + ); + } +}