diff --git a/clovers-cli/Cargo.toml b/clovers-cli/Cargo.toml index c663f496..3eb35d12 100644 --- a/clovers-cli/Cargo.toml +++ b/clovers-cli/Cargo.toml @@ -23,20 +23,20 @@ clovers = { path = "../clovers", features = [ # External blue-noise-sampler = "0.1.0" -clap = { version = "4.5.6", features = ["std", "derive"] } +clap = { version = "4.5.10", features = ["std", "derive"] } human_format = "1.1.0" humantime = "2.1.0" -image = { version = "0.25.1", features = ["png"], default-features = false } +image = { version = "0.25.2", features = ["png"], default-features = false } img-parts = "0.3.0" indicatif = { version = "0.17.8", features = [ "rayon", ], default-features = false } -nalgebra = { version = "0.32.5" } +nalgebra = { version = "0.33.0" } palette = { version = "0.7.6", features = ["serializing"] } paste = { version = "1.0.15" } rand = { version = "0.8.5", features = ["small_rng"], default-features = false } rayon = "1.10.0" -serde = { version = "1.0.203", features = ["derive"], default-features = false } +serde = { version = "1.0.204", features = ["derive"], default-features = false } serde_json = { version = "1.0", features = ["alloc"], default-features = false } time = { version = "0.3.36", default-features = false } tracing = "0.1.40" @@ -45,7 +45,3 @@ tracing-subscriber = { version = "0.3.18", features = ["time"] } [dev-dependencies] divan = "0.1.14" proptest = "1" - -[[bench]] -name = "draw_cpu" -harness = false diff --git a/clovers-cli/benches/draw_cpu.rs b/clovers-cli/benches/draw_cpu.rs deleted file mode 100644 index 2f6ea93a..00000000 --- a/clovers-cli/benches/draw_cpu.rs +++ /dev/null @@ -1,38 +0,0 @@ -use clovers::scenes::{initialize, Scene, SceneFile}; -use clovers::RenderOpts; -use clovers_runtime::draw_cpu::draw; -use clovers_runtime::sampler::Sampler; -use divan::{black_box, AllocProfiler}; - -#[global_allocator] -static ALLOC: AllocProfiler = AllocProfiler::system(); - -fn main() { - divan::main(); -} - -const WIDTH: u32 = 256; -const HEIGHT: u32 = 256; -const OPTS: RenderOpts = RenderOpts { - width: WIDTH, - height: HEIGHT, - samples: 1, - max_depth: 100, - quiet: true, - normalmap: false, -}; - -#[divan::bench] -fn draw_cornell(bencher: divan::Bencher) { - bencher - .with_inputs(get_cornell) - .counter(1u32) - .bench_values(|scene| black_box(draw(OPTS, &scene, Sampler::Random))) -} - -fn get_cornell<'scene>() -> Scene<'scene> { - const INPUT: &str = include_str!("../../scenes/cornell.json"); - let scene_file: SceneFile = serde_json::from_str(INPUT).unwrap(); - let scene: Scene = initialize(scene_file, WIDTH, HEIGHT); - scene -} diff --git a/clovers-cli/src/colorize.rs b/clovers-cli/src/colorize.rs index cdeba0d1..5d214d8f 100644 --- a/clovers-cli/src/colorize.rs +++ b/clovers-cli/src/colorize.rs @@ -40,7 +40,7 @@ pub fn colorize( // Send the ray to the scene, and see if it hits anything. // distance_min is set to an epsilon to avoid "shadow acne" that can happen when set to zero let Some(hit_record) = scene - .hitables + .bvh_root .hit(ray, EPSILON_SHADOW_ACNE, Float::MAX, rng) else { // If the ray hits nothing, early return the background color. @@ -88,10 +88,8 @@ pub fn colorize( // Multiple Importance Sampling: // Create a new PDF object from the priority hitables of the scene, given the current hit_record position - let light_ptr = PDF::HitablePDF(HitablePDF::new( - &scene.priority_hitables, - hit_record.position, - )); + let light_ptr = + PDF::HitablePDF(HitablePDF::new(&scene.mis_bvh_root, hit_record.position)); // Create a mixture PDF from the above + the PDF from the scatter_record let mixture_pdf = MixturePDF::new(light_ptr, scatter_record.pdf_ptr); diff --git a/clovers-cli/src/debug_visualizations.rs b/clovers-cli/src/debug_visualizations.rs new file mode 100644 index 00000000..f9f6569e --- /dev/null +++ b/clovers-cli/src/debug_visualizations.rs @@ -0,0 +1,61 @@ +//! Alternative rendering methods for debug visualization purposes. + +use clovers::{ray::Ray, scenes::Scene, Float, EPSILON_SHADOW_ACNE}; +use palette::LinSrgb; +use rand::rngs::SmallRng; + +/// Visualizes the BVH traversal count - how many BVH nodes needed to be tested for intersection? +#[must_use] +pub fn bvh_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb { + let mut depth = 0; + scene + .bvh_root + .testcount(&mut depth, ray, EPSILON_SHADOW_ACNE, Float::MAX, rng); + + bvh_testcount_to_color(depth) +} + +#[must_use] +pub fn bvh_testcount_to_color(depth: usize) -> LinSrgb { + match depth { + // under 256, grayscale + 0..=255 => { + let depth = depth as Float / 255.0; + LinSrgb::new(depth, depth, depth) + } + // more than 256, yellow + 256..=511 => LinSrgb::new(1.0, 1.0, 0.0), + // more than 512, orange + 512..=1023 => LinSrgb::new(1.0, 0.5, 0.0), + // more than 1024, red + 1024.. => LinSrgb::new(1.0, 0.0, 0.0), + } +} + +/// Visualizes the primitive traversal count - how many primitives needed to be tested for intersection? +#[must_use] +pub fn primitive_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb { + let mut depth = 0; + scene + .bvh_root + .primitive_testcount(&mut depth, ray, EPSILON_SHADOW_ACNE, Float::MAX, rng); + + primitive_testcount_to_color(depth) +} + +#[must_use] +pub fn primitive_testcount_to_color(depth: usize) -> LinSrgb { + match depth { + // under 256, grayscale + 0..=255 => { + let depth = depth as Float / 255.0; + LinSrgb::new(depth, depth, depth) + } + // more than 256, yellow + 256..=511 => LinSrgb::new(1.0, 1.0, 0.0), + // more than 512, orange + 512..=1023 => LinSrgb::new(1.0, 0.5, 0.0), + // more than 1024, red + 1024.. => LinSrgb::new(1.0, 0.0, 0.0), + } +} diff --git a/clovers-cli/src/draw_cpu.rs b/clovers-cli/src/draw_cpu.rs index cc3d52c1..5646bd1c 100644 --- a/clovers-cli/src/draw_cpu.rs +++ b/clovers-cli/src/draw_cpu.rs @@ -2,7 +2,7 @@ use clovers::wavelength::random_wavelength; use clovers::Vec2; -use clovers::{ray::Ray, scenes::Scene, Float, RenderOpts}; +use clovers::{ray::Ray, scenes::Scene, Float}; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use palette::chromatic_adaptation::AdaptInto; use palette::convert::IntoColorUnclamped; @@ -13,23 +13,44 @@ use rand::{Rng, SeedableRng}; use rayon::prelude::*; use crate::colorize::colorize; +use crate::debug_visualizations::{bvh_testcount, primitive_testcount}; use crate::normals::normal_map; +use crate::render::{RenderMode, RenderOptions}; use crate::sampler::blue::BlueSampler; use crate::sampler::random::RandomSampler; use crate::sampler::{Randomness, Sampler, SamplerTrait}; +use crate::GlobalOptions; /// The main drawing function, returns a `Vec` as a pixelbuffer. -pub fn draw(opts: RenderOpts, scene: &Scene, sampler: Sampler) -> Vec> { - let width = opts.width as usize; - let height = opts.height as usize; - let bar = progress_bar(&opts); +pub fn draw( + global_options: &GlobalOptions, + render_options: &RenderOptions, + scene: &Scene, + _sampler: Sampler, +) -> Vec> { + let GlobalOptions { debug: _, quiet } = *global_options; + let RenderOptions { + input: _, + output: _, + width, + height, + samples, + max_depth: _, + mode, + sampler, + bvh: _, + } = *render_options; + let bar = progress_bar(height, quiet); + + let height = height as usize; + let width = width as usize; let pixelbuffer: Vec> = (0..height) .into_par_iter() .map(|row_index| { let mut sampler_rng = SmallRng::from_entropy(); let mut sampler: Box = match sampler { - Sampler::Blue => Box::new(BlueSampler::new(&opts)), + Sampler::Blue => Box::new(BlueSampler::new(samples)), Sampler::Random => Box::new(RandomSampler::new(&mut sampler_rng)), }; @@ -38,11 +59,29 @@ pub fn draw(opts: RenderOpts, scene: &Scene, sampler: Sampler) -> Vec> for index in 0..width { let index = index + row_index * width; - if opts.normalmap { - row.push(render_pixel_normalmap(scene, &opts, index, &mut rng)); - } else { - row.push(render_pixel(scene, &opts, index, &mut rng, &mut *sampler)); - } + let pixel = match mode { + RenderMode::PathTracing => { + render_pixel(scene, render_options, index, &mut rng, &mut *sampler) + } + RenderMode::NormalMap => { + render_pixel_normalmap(scene, render_options, index, &mut rng) + } + RenderMode::BvhTestCount => render_pixel_bvhtestcount( + scene, + render_options, + index, + &mut rng, + &mut *sampler, + ), + RenderMode::PrimitiveTestCount => render_pixel_primitivetestcount( + scene, + render_options, + index, + &mut rng, + &mut *sampler, + ), + }; + row.push(pixel); } bar.inc(1); row @@ -56,7 +95,7 @@ pub fn draw(opts: RenderOpts, scene: &Scene, sampler: Sampler) -> Vec> // Render a single pixel, including possible multisampling fn render_pixel( scene: &Scene, - opts: &RenderOpts, + opts: &RenderOptions, index: usize, rng: &mut SmallRng, sampler: &mut dyn SamplerTrait, @@ -95,7 +134,7 @@ fn render_pixel( // Render a single pixel in normalmap mode fn render_pixel_normalmap( scene: &Scene, - opts: &RenderOpts, + opts: &RenderOptions, index: usize, rng: &mut SmallRng, ) -> Srgb { @@ -115,7 +154,53 @@ fn render_pixel_normalmap( color } -fn index_to_params(opts: &RenderOpts, index: usize) -> (Float, Float, Float, Float) { +// Render a single pixel in bvh test count visualization mode +fn render_pixel_bvhtestcount( + scene: &Scene, + render_options: &RenderOptions, + index: usize, + rng: &mut SmallRng, + _sampler: &mut dyn SamplerTrait, +) -> Srgb { + let (x, y, width, height) = index_to_params(render_options, index); + let pixel_location = Vec2::new(x / width, y / height); + let lens_offset = Vec2::new(0.0, 0.0); + let wavelength = random_wavelength(rng); + let time = rng.gen(); + let ray: Ray = scene + .camera + .get_ray(pixel_location, lens_offset, time, wavelength); + + let color: LinSrgb = { bvh_testcount(&ray, scene, rng) }; + let color: Srgb = color.into_color_unclamped(); + let color: Srgb = color.into_format(); + color +} + +// Render a single pixel in primitive test count visualization mode +fn render_pixel_primitivetestcount( + scene: &Scene, + render_options: &RenderOptions, + index: usize, + rng: &mut SmallRng, + _sampler: &mut dyn SamplerTrait, +) -> Srgb { + let (x, y, width, height) = index_to_params(render_options, index); + let pixel_location = Vec2::new(x / width, y / height); + let lens_offset = Vec2::new(0.0, 0.0); + let wavelength = random_wavelength(rng); + let time = rng.gen(); + let ray: Ray = scene + .camera + .get_ray(pixel_location, lens_offset, time, wavelength); + + let color: LinSrgb = { primitive_testcount(&ray, scene, rng) }; + let color: Srgb = color.into_color_unclamped(); + let color: Srgb = color.into_format(); + color +} + +fn index_to_params(opts: &RenderOptions, index: usize) -> (Float, Float, Float, Float) { let x = (index % (opts.width as usize)) as Float; let y = (index / (opts.width as usize)) as Float; let width = opts.width as Float; @@ -123,9 +208,9 @@ fn index_to_params(opts: &RenderOpts, index: usize) -> (Float, Float, Float, Flo (x, y, width, height) } -fn progress_bar(opts: &RenderOpts) -> ProgressBar { - let bar = ProgressBar::new(opts.height as u64); - if opts.quiet { +fn progress_bar(height: u32, quiet: bool) -> ProgressBar { + let bar = ProgressBar::new(height as u64); + if quiet { bar.set_draw_target(ProgressDrawTarget::hidden()) } else { bar.set_style(ProgressStyle::default_bar().template( diff --git a/clovers-cli/src/json_scene.rs b/clovers-cli/src/json_scene.rs index 4e60c3e6..ae189984 100644 --- a/clovers-cli/src/json_scene.rs +++ b/clovers-cli/src/json_scene.rs @@ -1,4 +1,5 @@ -use clovers::scenes::{self, Scene, SceneFile}; +use clovers::bvh::BvhAlgorithm; +use clovers::scenes::Scene; use std::error::Error; use std::fs::File; use std::io::Read; @@ -6,8 +7,11 @@ use std::path::Path; use tracing::info; -pub(crate) fn initialize<'scene>( +use crate::scenefile::SceneFile; + +pub fn initialize<'scene>( path: &Path, + bvh_algorithm: BvhAlgorithm, width: u32, height: u32, ) -> Result, Box> { @@ -17,7 +21,7 @@ pub(crate) fn initialize<'scene>( info!("Parsing the scene file"); let scene_file: SceneFile = serde_json::from_str(&contents)?; info!("Initializing the scene"); - let scene: Scene = scenes::initialize(scene_file, width, height); - info!("Count of nodes in the BVH tree: {}", scene.hitables.count()); + let scene: Scene = SceneFile::initialize(scene_file, bvh_algorithm, width, height); + info!("Count of nodes in the BVH tree: {}", scene.bvh_root.count()); Ok(scene) } diff --git a/clovers-cli/src/lib.rs b/clovers-cli/src/lib.rs index e5a54cae..e7cf3918 100644 --- a/clovers-cli/src/lib.rs +++ b/clovers-cli/src/lib.rs @@ -1,6 +1,24 @@ //! Runtime functions of the `clovers` renderer. +use clap::Args; + pub mod colorize; +pub mod debug_visualizations; pub mod draw_cpu; +pub mod json_scene; pub mod normals; +pub mod render; pub mod sampler; +pub mod scenefile; + +// TODO: move this into a better place - but keep rustc happy with the imports +/// Global options +#[derive(Args, Debug)] +pub struct GlobalOptions { + /// Enable some debug logging + #[clap(long)] + pub debug: bool, + /// Suppress most of the text output + #[clap(short, long)] + pub quiet: bool, +} diff --git a/clovers-cli/src/main.rs b/clovers-cli/src/main.rs index 67b29e6d..074e1993 100644 --- a/clovers-cli/src/main.rs +++ b/clovers-cli/src/main.rs @@ -3,7 +3,9 @@ #![deny(clippy::all)] // External imports -use clap::{Args, Parser, Subcommand}; +use clap::{Parser, Subcommand}; +use clovers_runtime::GlobalOptions; +use render::RenderOptions; use std::error::Error; // Internal imports @@ -11,6 +13,8 @@ use clovers::*; #[doc(hidden)] mod colorize; #[doc(hidden)] +pub mod debug_visualizations; +#[doc(hidden)] mod draw_cpu; #[doc(hidden)] mod json_scene; @@ -18,12 +22,14 @@ mod json_scene; pub mod normals; #[doc(hidden)] mod render; -use render::{render, RenderParams}; +use render::render; #[doc(hidden)] mod sampler; #[doc(hidden)] mod validate; use validate::{validate, ValidateParams}; +#[doc(hidden)] +pub mod scenefile; /// clovers 🍀 path tracing renderer #[derive(Parser)] @@ -34,23 +40,12 @@ pub struct Cli { command: Commands, } -#[derive(Args, Debug)] -/// Global options -pub struct GlobalOptions { - /// Enable some debug logging - #[clap(long)] - debug: bool, - /// Suppress most of the text output - #[clap(short, long)] - quiet: bool, -} - #[derive(Subcommand, Debug)] /// Subcommands for the CLI pub enum Commands { #[command(arg_required_else_help = true)] /// Render a given scene file - Render(RenderParams), + Render(RenderOptions), #[command(arg_required_else_help = true)] /// Validate a given scene file Validate(ValidateParams), diff --git a/clovers-cli/src/normals.rs b/clovers-cli/src/normals.rs index 1348e340..aec10bf4 100644 --- a/clovers-cli/src/normals.rs +++ b/clovers-cli/src/normals.rs @@ -10,7 +10,7 @@ use rand::rngs::SmallRng; #[must_use] pub fn normal_map(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb { let Some(hit_record) = scene - .hitables + .bvh_root .hit(ray, EPSILON_SHADOW_ACNE, Float::MAX, rng) else { // If the ray hits nothing, early return black diff --git a/clovers-cli/src/render.rs b/clovers-cli/src/render.rs index d9c2f5c0..4874c4d7 100644 --- a/clovers-cli/src/render.rs +++ b/clovers-cli/src/render.rs @@ -1,5 +1,4 @@ -use clap::Args; -use clovers::RenderOpts; +use clap::{Args, ValueEnum}; use humantime::format_duration; use image::{ImageBuffer, ImageFormat, Rgb, RgbImage}; use img_parts::png::{Png, PngChunk}; @@ -11,49 +10,80 @@ use time::OffsetDateTime; use tracing::{debug, info, Level}; use tracing_subscriber::fmt::time::UtcTime; +use crate::draw_cpu; +use crate::json_scene::initialize; use crate::sampler::Sampler; -use crate::{draw_cpu, json_scene, GlobalOptions}; +use crate::GlobalOptions; #[derive(Args, Debug)] -pub struct RenderParams { +pub struct RenderOptions { /// Input filename / location #[arg()] - input: String, - /// Output filename / location. [default: ./renders/unix_timestamp.png] + pub input: String, + /// Output filename / location. Defaults to ./renders/unix_timestamp.png #[arg(short, long)] - output: Option, + pub output: Option, /// Width of the image in pixels. #[arg(short, long, default_value = "1024")] - width: u32, + pub width: u32, /// Height of the image in pixels. #[arg(short, long, default_value = "1024")] - height: u32, + pub height: u32, /// Number of samples to generate per each pixel. #[arg(short, long, default_value = "64")] - samples: u32, + pub samples: u32, /// Maximum evaluated bounce depth for each ray. #[arg(short = 'd', long, default_value = "64")] - max_depth: u32, - /// Render a normal map only. - #[arg(long)] - normalmap: bool, + pub max_depth: u32, + /// Rendering mode. + #[arg(short = 'm', long, default_value = "path-tracing")] + pub mode: RenderMode, /// Sampler to use for rendering. #[arg(long, default_value = "random")] - sampler: Sampler, + pub sampler: Sampler, + /// BVH construction algorithm. + #[arg(long, default_value = "sah")] + pub bvh: BvhAlgorithm, } -pub(crate) fn render(opts: GlobalOptions, params: RenderParams) -> Result<(), Box> { - let GlobalOptions { quiet, debug } = opts; - let RenderParams { - input, - output, +#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] +pub enum RenderMode { + /// Full path tracing, the default + PathTracing, + /// Surface normals of the first hit + NormalMap, + /// Debug view for BVH ray hit count + BvhTestCount, + /// Debug view for primitive object ray hit count + PrimitiveTestCount, +} + +#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] +pub enum BvhAlgorithm { + /// Split at the Longest Axis Midpoint of the current AABB + Lam, + /// Split based on the Surface Area Heuristic. + Sah, +} + +// CLI usage somehow not detected +#[allow(dead_code)] +pub(crate) fn render( + global_options: GlobalOptions, + render_options: RenderOptions, +) -> Result<(), Box> { + let GlobalOptions { quiet, debug } = global_options; + let RenderOptions { + ref input, + ref output, width, height, samples, max_depth, - normalmap, + mode, sampler, - } = params; + bvh, + } = render_options; if debug { tracing_subscriber::fmt() @@ -73,7 +103,7 @@ pub(crate) fn render(opts: GlobalOptions, params: RenderParams) -> Result<(), Bo println!("clovers 🍀 path tracing renderer"); println!(); println!("{width}x{height} resolution"); - if normalmap { + if mode == RenderMode::NormalMap { println!("rendering a normalmap"); } else { println!("{samples} samples per pixel"); @@ -87,21 +117,19 @@ pub(crate) fn render(opts: GlobalOptions, params: RenderParams) -> Result<(), Bo panic!("the blue sampler only supports the following sample-per-pixel counts: [1, 2, 4, 8, 16, 32, 64, 128, 256]"); } - let renderopts: RenderOpts = RenderOpts { - width, - height, - samples, - max_depth, - quiet, - normalmap, + // TODO: improve ergonomics? + let bvh_algorithm: clovers::bvh::BvhAlgorithm = match bvh { + BvhAlgorithm::Lam => clovers::bvh::BvhAlgorithm::Lam, + BvhAlgorithm::Sah => clovers::bvh::BvhAlgorithm::Sah, }; + let threads = std::thread::available_parallelism()?; info!("Reading the scene file"); let path = Path::new(&input); let scene = match path.extension() { Some(ext) => match &ext.to_str() { - Some("json") => json_scene::initialize(path, width, height), + Some("json") => initialize(path, bvh_algorithm, width, height), _ => panic!("Unknown file type"), }, None => panic!("Unknown file type"), @@ -109,7 +137,7 @@ pub(crate) fn render(opts: GlobalOptions, params: RenderParams) -> Result<(), Bo info!("Calling draw()"); let start = Instant::now(); - let pixelbuffer = draw_cpu::draw(renderopts, &scene, sampler); + let pixelbuffer = draw_cpu::draw(&global_options, &render_options, &scene, sampler); info!("Drawing a pixelbuffer finished"); info!("Converting pixelbuffer to an image"); @@ -138,11 +166,18 @@ pub(crate) fn render(opts: GlobalOptions, params: RenderParams) -> Result<(), Bo img.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)?; let mut png = Png::from_bytes(bytes.into())?; - let comment = if normalmap { - format!("Comment\0{input} rendered with the clovers raytracing engine at {width}x{height} in normalmap mode. finished render in {formatted_duration}, using {threads} threads") - } else { - format!("Comment\0{input} rendered with the clovers raytracing engine at {width}x{height}, {samples} samples per pixel, {max_depth} max ray bounce depth. finished render in {formatted_duration}, using {threads} threads") + let common = format!( + "Comment\0Rendered with the clovers path tracing engine. Scene file {input} rendered using the {mode:?} rendering mode at {width}x{height} resolution" + ); + let details = match mode { + RenderMode::PathTracing => { + format!(", {samples} samples per pixel, {max_depth} max ray bounce depth.") + } + _ => ".".to_owned(), }; + let stats = format!("Rendering finished in {formatted_duration}, using {threads} threads."); + let comment = format!("{common}{details} {stats}"); + let software = "Software\0https://github.com/walther/clovers".to_string(); for metadata in [comment, software] { @@ -152,7 +187,7 @@ pub(crate) fn render(opts: GlobalOptions, params: RenderParams) -> Result<(), Bo } let target = match output { - Some(filename) => filename, + Some(filename) => filename.to_owned(), None => { // Default to using a timestamp & `renders/` directory let timestamp = OffsetDateTime::now_utc().unix_timestamp(); diff --git a/clovers-cli/src/sampler.rs b/clovers-cli/src/sampler.rs index c818fb44..6ccb9608 100644 --- a/clovers-cli/src/sampler.rs +++ b/clovers-cli/src/sampler.rs @@ -36,11 +36,11 @@ pub struct Randomness { } /// Enum of the supported samplers. -#[derive(Clone, Debug, PartialEq, ValueEnum)] +#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] pub enum Sampler { - /// Blue noise based sampler, see [BlueSampler](blue::BlueSampler) + /// Blue noise based sampler Blue, - /// Random number generator based sampler, see [RandomSampler](random::RandomSampler) + /// Random number generator based sampler Random, } @@ -55,7 +55,7 @@ impl Display for Sampler { } /// Various sampling dimensions used by the samplers -#[derive(Clone, Copy)] +#[derive(Clone, Debug)] pub enum SamplerDimension { PixelOffsetX, PixelOffsetY, diff --git a/clovers-cli/src/sampler/blue.rs b/clovers-cli/src/sampler/blue.rs index c44e736b..396a2fc8 100644 --- a/clovers-cli/src/sampler/blue.rs +++ b/clovers-cli/src/sampler/blue.rs @@ -2,7 +2,7 @@ //! //! Utilizes library code from . -use clovers::{wavelength::sample_wavelength, Float, RenderOpts, Vec2, PI}; +use clovers::{wavelength::sample_wavelength, Float, Vec2, PI}; use super::*; @@ -10,9 +10,9 @@ pub struct BlueSampler { get: fn(i32, i32, i32, SamplerDimension) -> Float, } -impl<'scene> BlueSampler { - pub fn new(opts: &'scene RenderOpts) -> Self { - let get = match opts.samples { +impl BlueSampler { + pub fn new(samples: u32) -> Self { + let get = match samples { 1 => blue_sample_spp1, 2 => blue_sample_spp2, 4 => blue_sample_spp4, diff --git a/clovers-cli/src/scenefile.rs b/clovers-cli/src/scenefile.rs new file mode 100644 index 00000000..aaaf3d38 --- /dev/null +++ b/clovers-cli/src/scenefile.rs @@ -0,0 +1,111 @@ +use std::boxed::Box; + +use clovers::{ + bvh::{BVHNode, BvhAlgorithm}, + camera::{Camera, CameraInit}, + hitable::Hitable, + materials::SharedMaterial, + objects::{object_to_hitable, Object}, + scenes::Scene, + Float, Vec, +}; + +use palette::Srgb; +use tracing::info; + +// TODO: better naming +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +/// A serialized representation of a [Scene]. +pub struct SceneFile { + time_0: Float, + time_1: Float, + background_color: Srgb, + camera: CameraInit, + objects: Vec, + #[serde(default)] + materials: Vec, +} + +impl SceneFile { + /// Initializes a new [Scene] instance by parsing the contents of a [`SceneFile`] structure and then using those details to construct the [Scene]. + #[must_use] + pub fn initialize<'scene>( + scene_file: SceneFile, + bvh_algorithm: BvhAlgorithm, + width: u32, + height: u32, + ) -> Scene<'scene> { + let time_0 = scene_file.time_0; + let time_1 = scene_file.time_1; + let background_color = scene_file.background_color; + + #[allow(clippy::cast_precision_loss)] + let camera = Camera::new( + scene_file.camera.look_from, + scene_file.camera.look_at, + scene_file.camera.up, + scene_file.camera.vertical_fov, + width as Float / height as Float, + scene_file.camera.aperture, + scene_file.camera.focus_distance, + time_0, + time_1, + ); + let mut materials = scene_file.materials; + materials.push(SharedMaterial::default()); + let materials = Box::leak(Box::new(materials)); + + info!("Creating a flattened list from the objects"); + let mut hitables: Vec = Vec::new(); + let mut priority_hitables: Vec = Vec::new(); + + // TODO: this isn't the greatest ergonomics, but it gets the job done for now + for object in scene_file.objects { + let priority = match &object { + Object::Boxy(i) => i.priority, + Object::ConstantMedium(i) => i.priority, + Object::MovingSphere(i) => i.priority, + Object::ObjectList(i) => i.priority, + Object::Quad(i) => i.priority, + Object::RotateY(i) => i.priority, + Object::Sphere(i) => i.priority, + Object::STL(i) => i.priority, + Object::GLTF(i) => i.priority, + Object::Translate(i) => i.priority, + Object::Triangle(i) => i.priority, + }; + let hitable = object_to_hitable(object, materials); + + match hitable { + // Flatten any lists we got. Potential sources: `GLTF`, `STL`, `ObjectList` + Hitable::HitableList(l) => { + let flattened = &mut l.flatten(); + hitables.append(&mut flattened.clone()); + if priority { + priority_hitables.append(&mut flattened.clone()); + } + } + // Otherwise, push as-is + _ => { + hitables.push(hitable.clone()); + if priority { + priority_hitables.push(hitable.clone()); + } + } + }; + } + info!("All objects parsed into hitables"); + info!("Building the BVH root for hitables"); + let bvh_root = BVHNode::from_list(bvh_algorithm, hitables); + info!("Building the MIS BVH root for priority hitables"); + let mis_bvh_root = Hitable::BVHNode(BVHNode::from_list(bvh_algorithm, priority_hitables)); + info!("BVH root nodes built"); + + Scene { + camera, + bvh_root, + mis_bvh_root, + background_color, + } + } +} diff --git a/clovers-cli/src/validate.rs b/clovers-cli/src/validate.rs index 090e0a1c..dd254736 100644 --- a/clovers-cli/src/validate.rs +++ b/clovers-cli/src/validate.rs @@ -1,4 +1,5 @@ use clap::Args; +use clovers::bvh::BvhAlgorithm; use std::{error::Error, path::Path}; use crate::json_scene; @@ -18,7 +19,7 @@ pub(crate) fn validate(params: ValidateParams) -> Result<(), Box> { let path = Path::new(&input); let scene = match path.extension() { Some(ext) => match &ext.to_str() { - Some("json") => json_scene::initialize(path, 1, 1), + Some("json") => json_scene::initialize(path, BvhAlgorithm::default(), 1, 1), _ => panic!("Unknown file type"), }, None => panic!("Unknown file type"), diff --git a/clovers/Cargo.toml b/clovers/Cargo.toml index 1f82e46e..5e88c681 100644 --- a/clovers/Cargo.toml +++ b/clovers/Cargo.toml @@ -20,11 +20,11 @@ traces = ["tracing"] [dependencies] enum_dispatch = "0.3.13" gltf = { version = "1.4.1", optional = true } -nalgebra = { version = "0.32.5" } +nalgebra = { version = "0.33.0" } palette = { version = "0.7.6", features = ["serializing"] } rand = { version = "0.8.5", features = ["small_rng"], default-features = false } rand_distr = { version = "0.4.3", features = ["std_math"] } -serde = { version = "1.0.203", features = [ +serde = { version = "1.0.204", features = [ "derive", ], default-features = false, optional = true } stl_io = { version = "0.7.0", optional = true } diff --git a/clovers/benches/aabb.rs b/clovers/benches/aabb.rs index fa3f2502..e9d65c2a 100644 --- a/clovers/benches/aabb.rs +++ b/clovers/benches/aabb.rs @@ -1,9 +1,8 @@ -use std::f32::{INFINITY, NEG_INFINITY}; - use clovers::interval::Interval; use clovers::random::random_unit_vector; use clovers::ray::Ray; use clovers::wavelength::random_wavelength; +use clovers::Float; use clovers::{aabb::*, Vec3}; use divan::{black_box, AllocProfiler}; use rand::rngs::SmallRng; @@ -32,29 +31,7 @@ fn hit(bencher: divan::Bencher) { bencher .with_inputs(random_aabb_and_ray) .counter(1u32) - .bench_values(|(aabb, ray)| black_box(aabb.hit(&ray, NEG_INFINITY, INFINITY))) -} - -#[divan::bench] -fn hit_old(bencher: divan::Bencher) { - bencher - .with_inputs(random_aabb_and_ray) - .counter(1u32) - .bench_values(|(aabb, ray)| { - #[allow(deprecated)] - black_box(aabb.hit_old(&ray, NEG_INFINITY, INFINITY)) - }) -} - -#[divan::bench] -fn hit_new(bencher: divan::Bencher) { - bencher - .with_inputs(random_aabb_and_ray) - .counter(1u32) - .bench_values(|(aabb, ray)| { - #[allow(deprecated)] - black_box(aabb.hit_new(&ray, NEG_INFINITY, INFINITY)) - }) + .bench_values(|(aabb, ray)| black_box(aabb.hit(&ray, Float::NEG_INFINITY, Float::INFINITY))) } // Helper functions diff --git a/clovers/benches/interval.rs b/clovers/benches/interval.rs index f9dd93cd..63b79b36 100644 --- a/clovers/benches/interval.rs +++ b/clovers/benches/interval.rs @@ -22,7 +22,7 @@ fn new(bencher: divan::Bencher) { } #[divan::bench] -fn new_from_intervals(bencher: divan::Bencher) { +fn combine(bencher: divan::Bencher) { bencher .with_inputs(|| { let mut rng = SmallRng::from_entropy(); @@ -31,7 +31,7 @@ fn new_from_intervals(bencher: divan::Bencher) { (ab, cd) }) .counter(1u32) - .bench_values(|(ab, cd)| black_box(Interval::new_from_intervals(ab, cd))) + .bench_values(|(ab, cd)| black_box(Interval::combine(&ab, &cd))) } #[divan::bench] diff --git a/clovers/benches/triangle.rs b/clovers/benches/triangle.rs index e7b38d1b..93830907 100644 --- a/clovers/benches/triangle.rs +++ b/clovers/benches/triangle.rs @@ -1,11 +1,10 @@ -use std::f32::{INFINITY, NEG_INFINITY}; - use clovers::hitable::HitableTrait; use clovers::materials::Material; use clovers::objects::Triangle; use clovers::random::random_unit_vector; use clovers::ray::Ray; use clovers::wavelength::random_wavelength; +use clovers::Float; use clovers::Vec3; use divan::black_box; use divan::AllocProfiler; @@ -39,7 +38,7 @@ fn hit(bencher: divan::Bencher) { .bench_values(|(triangle, ray, mut rng)| { black_box( triangle - .hit(&ray, NEG_INFINITY, INFINITY, &mut rng) + .hit(&ray, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .is_some(), ) }) diff --git a/clovers/src/aabb.rs b/clovers/src/aabb.rs index 2378039a..de249efa 100644 --- a/clovers/src/aabb.rs +++ b/clovers/src/aabb.rs @@ -7,7 +7,7 @@ use crate::{interval::Interval, ray::Ray, Float, Position, Vec3, EPSILON_RECT_TH /// Axis-aligned bounding box Defined by two opposing corners, each of which are a [Vec3]. /// /// This is useful for creating bounding volume hierarchies, which is an optimization for reducing the time spent on calculating ray-object intersections. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] pub struct AABB { /// The bounding interval on the X axis @@ -39,75 +39,45 @@ impl AABB { } } + /// The inverse method for `AABB::new`: given an existing constructed `AABB`, returns the minimum coordinate and maximum coordinate on the opposing corners. + #[must_use] + pub fn bounding_positions(&self) -> (Position, Position) { + ( + Position::new(self.x.min, self.y.min, self.z.min), + Position::new(self.x.max, self.y.max, self.z.max), + ) + } + #[allow(clippy::doc_link_with_quotes)] - /// Given a [Ray], returns whether the ray hits the bounding box or not. Current default method, based on ["An Optimized AABB Hit Method"](https://raytracing.github.io/books/RayTracingTheNextWeek.html) + /// Given a [Ray], returns whether the ray hits the bounding box or not. Current method based on the "Axis-aligned bounding box class" of the [Raytracing The Next Week book](https://raytracing.github.io/books/RayTracingTheNextWeek.html). #[must_use] pub fn hit(&self, ray: &Ray, mut tmin: Float, mut tmax: Float) -> bool { - // TODO: Create an improved hit method with more robust handling of zeroes. See https://github.com/RayTracing/raytracing.github.io/issues/927 - // Both methods below are susceptible for NaNs and infinities, and have subtly different edge cases. + let ray_origin = ray.origin; + let ray_dir = ray.direction; - // "My adjusted method" - possibly more zero-resistant? - // TODO: validate for axis in 0..3 { - // If ray direction component is 0, invd becomes infinity. - // Ignore? False positive hit for aabb is probably better than false negative; the actual object can still be hit more accurately - let invd = 1.0 / ray.direction[axis]; - if !invd.is_normal() { - continue; - } - // If the value in parenthesis ends up as zero, 0*inf can be NaN - let mut t0: Float = (self.axis(axis).min - ray.origin[axis]) * invd; - let mut t1: Float = (self.axis(axis).max - ray.origin[axis]) * invd; - if !t0.is_normal() || !t1.is_normal() { - continue; - } - if invd < 0.0 { - core::mem::swap(&mut t0, &mut t1); - } - tmin = if t0 > tmin { t0 } else { tmin }; - tmax = if t1 < tmax { t1 } else { tmax }; - if tmax <= tmin { - return false; - } - } + let ax = self.axis(axis); + let adinv = 1.0 / ray_dir[axis]; - // If we have not missed on any axis, return true for the hit - true - } + let t0 = (ax.min - ray_origin[axis]) * adinv; + let t1 = (ax.max - ray_origin[axis]) * adinv; - /// Given a [Ray], returns whether the ray hits the bounding box or not. Old method from a GitHub issue. Exists mostly for testing purposes. - #[must_use] - #[deprecated] - pub fn hit_old(&self, ray: &Ray, mut tmin: Float, mut tmax: Float) -> bool { - // "Old method" - for axis in 0..3 { - let invd = 1.0 / ray.direction[axis]; - let mut t0: Float = (self.axis(axis).min - ray.origin[axis]) * invd; - let mut t1: Float = (self.axis(axis).max - ray.origin[axis]) * invd; - if invd < 0.0 { - core::mem::swap(&mut t0, &mut t1); - } - tmin = if t0 > tmin { t0 } else { tmin }; - tmax = if t1 < tmax { t1 } else { tmax }; - if tmax <= tmin { - return false; + if t0 < t1 { + if t0 > tmin { + tmin = t0; + }; + if t1 < tmax { + tmax = t1; + }; + } else { + if t1 > tmin { + tmin = t1; + }; + if t0 < tmax { + tmax = t0; + }; } - } - true - } - /// Given a [Ray], returns whether the ray hits the bounding box or not. Newer method from a GitHub issue. Exists mostly for testing purposes. - #[must_use] - #[deprecated] - pub fn hit_new(&self, ray: &Ray, mut tmin: Float, mut tmax: Float) -> bool { - // "New method" - for axis in 0..3 { - let a = (self.axis(axis).min - ray.origin[axis]) / ray.direction[axis]; - let b = (self.axis(axis).max - ray.origin[axis]) / ray.direction[axis]; - let t0: Float = a.min(b); - let t1: Float = a.max(b); - tmin = t0.max(tmin); - tmax = t1.min(tmax); if tmax <= tmin { return false; } @@ -117,11 +87,11 @@ impl AABB { /// Given two axis-aligned bounding boxes, return a new [AABB] that contains both. #[must_use] - pub fn surrounding_box(box0: &AABB, box1: &AABB) -> AABB { + pub fn combine(box0: &AABB, box1: &AABB) -> AABB { AABB { - x: Interval::new_from_intervals(box0.x, box1.x), - y: Interval::new_from_intervals(box0.y, box1.y), - z: Interval::new_from_intervals(box0.z, box1.z), + x: Interval::combine(&box0.x, &box1.x), + y: Interval::combine(&box0.y, &box1.y), + z: Interval::combine(&box0.z, &box1.z), } } @@ -130,17 +100,17 @@ impl AABB { // TODO: refactor let delta = EPSILON_RECT_THICKNESS; let new_x: Interval = if self.x.size() >= delta { - self.x + self.x.clone() } else { self.x.expand(delta) }; let new_y: Interval = if self.y.size() >= delta { - self.y + self.y.clone() } else { self.y.expand(delta) }; let new_z: Interval = if self.z.size() >= delta { - self.z + self.z.clone() } else { self.z.expand(delta) }; @@ -151,14 +121,57 @@ impl AABB { /// Returns the interval of the given axis. // TODO: this api is kind of annoying #[must_use] - pub fn axis(&self, n: usize) -> Interval { + pub fn axis(&self, n: usize) -> &Interval { match n { - 0 => self.x, - 1 => self.y, - 2 => self.z, + 0 => &self.x, + 1 => &self.y, + 2 => &self.z, _ => panic!("AABB::axis called with invalid parameter: {n:?}"), } } + + /// Distance of a `Ray` to the bounding box. + /// + /// Returns `None` if the `AABB` is not hit, whether it is passed by the ray, or is behind the ray origin considering the ray direction. + /// + /// Based on the `IntersectAABB` method described at . + #[allow(clippy::similar_names)] + #[must_use] + pub fn distance(&self, ray: &Ray) -> Option { + let (box_min, box_max) = self.bounding_positions(); + let (mut tmin, mut tmax); + let tx1 = (box_min.x - ray.origin.x) / ray.direction.x; + let tx2 = (box_max.x - ray.origin.x) / ray.direction.x; + tmin = Float::min(tx1, tx2); + tmax = Float::max(tx1, tx2); + let ty1 = (box_min.y - ray.origin.y) / ray.direction.y; + let ty2 = (box_max.y - ray.origin.y) / ray.direction.y; + tmin = Float::max(tmin, Float::min(ty1, ty2)); + tmax = Float::min(tmax, Float::max(ty1, ty2)); + let tz1 = (box_min.z - ray.origin.z) / ray.direction.z; + let tz2 = (box_max.z - ray.origin.z) / ray.direction.z; + tmin = Float::max(tmin, Float::min(tz1, tz2)); + let tmax = Float::min(tmax, Float::max(tz1, tz2)); + if tmax >= tmin /* && tmin < ray.t */ && tmax > 0.0 { + return Some(tmin); + }; + + None + } + + /// Returns the area of this [`AABB`]. + #[must_use] + pub fn area(&self) -> Float { + let (min, max) = self.bounding_positions(); + let extent: Vec3 = max - min; + 2.0 * (extent.x * extent.y + extent.y * extent.z + extent.x * extent.z) + } + + /// Returns the centroid of this [`AABB`]. + #[must_use] + pub fn centroid(&self) -> Position { + Position::new(self.x.center(), self.y.center(), self.z.center()) + } } impl Add for AABB { @@ -168,3 +181,108 @@ impl Add for AABB { AABB::new(self.x + offset.x, self.y + offset.y, self.z + offset.z) } } + +#[cfg(test)] +#[allow(clippy::float_cmp)] +mod tests { + use super::*; + + #[test] + fn area_cube() { + let aabb = AABB::new( + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + ); + let area = aabb.area(); + let expected = 6.0; + assert_eq!(area, expected); + } + + #[test] + fn area_cuboid_positive() { + let aabb = AABB::new( + Interval::new(0.0, 1.0), + Interval::new(0.0, 2.0), + Interval::new(0.0, 3.0), + ); + let area = aabb.area(); + let expected = 22.0; + assert_eq!(area, expected); + } + + #[test] + fn area_cuboid_negative() { + let aabb = AABB::new( + Interval::new(-1.0, 0.0), + Interval::new(-2.0, 0.0), + Interval::new(-3.0, 0.0), + ); + let area = aabb.area(); + let expected = 22.0; + assert_eq!(area, expected); + } + + #[test] + fn centroid() { + let aabb = AABB::new( + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + ); + let centroid = aabb.centroid(); + let expected = Position::new(0.5, 0.5, 0.5); + assert_eq!(centroid, expected); + } + + #[test] + fn default() { + let aabb = AABB::default(); + let centroid = aabb.centroid(); + let expected = Position::new(0.0, 0.0, 0.0); + assert_eq!(centroid, expected); + } + + #[test] + fn combine_zero() { + let box0 = AABB::default(); + let box1 = AABB::default(); + let combined = AABB::combine(&box0, &box1); + let expected = AABB::default(); + assert_eq!(combined, expected); + } + + #[test] + fn combine_zero_unit() { + let box0 = AABB::default(); + let box1 = AABB::new( + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + ); + let combined = AABB::combine(&box0, &box1); + let expected = box1; + assert_eq!(combined, expected); + } + + #[test] + fn combine_positive_negative() { + let box0 = AABB::new( + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + Interval::new(0.0, 1.0), + ); + let box1 = AABB::new( + Interval::new(0.0, -1.0), + Interval::new(0.0, -1.0), + Interval::new(0.0, -1.0), + ); + let combined = AABB::combine(&box0, &box1); + let expected = AABB::new( + Interval::new(-1.0, 1.0), + Interval::new(-1.0, 1.0), + Interval::new(-1.0, 1.0), + ); + assert_eq!(combined, expected); + } +} diff --git a/clovers/src/bvh.rs b/clovers/src/bvh.rs new file mode 100644 index 00000000..138718dd --- /dev/null +++ b/clovers/src/bvh.rs @@ -0,0 +1,89 @@ +//! Bounding Volume Hierarchy acceleration structures and related utilities. + +use core::fmt::Display; +use std::time::Instant; + +use build::{longest_axis_midpoint, surface_area_heuristic}; +#[cfg(feature = "tracing")] +use tracing::info; + +use crate::{aabb::AABB, hitable::Hitable, Box}; + +pub(crate) mod build; +mod hitable_trait; +mod primitive_testcount; +mod testcount; + +/// Bounding Volume Hierarchy Node. +/// +/// A node in a tree structure defining a hierarchy of objects in a scene: a node knows its bounding box, and has two children which are also `BVHNode`s. This is used for accelerating the ray-object intersection calculation in the ray tracer. See [Bounding Volume hierarchies](https://raytracing.github.io/books/RayTracingTheNextWeek.html) +#[derive(Debug, Clone)] +pub struct BVHNode<'scene> { + /// Left child of the `BVHNode` + pub left: Box>, + /// Right child of the `BVHNode` + pub right: Box>, + /// Bounding box containing both of the child nodes + pub aabb: AABB, +} + +/// The choice of algorithms used for constructing the Bounding Volume Hierarchy tree +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub enum BvhAlgorithm { + /// Splitting method based on the longest axis of the current `AABB` + #[default] + Lam, + /// Splitting method based on the Surface Area Heuristic. + /// + /// Heavily inspired by the wonderful blog series . + Sah, +} + +impl Display for BvhAlgorithm { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BvhAlgorithm::Lam => write!(f, "Longest Axis Midpoint"), + BvhAlgorithm::Sah => write!(f, "Surface Area Heuristic"), + } + } +} + +impl<'scene> BVHNode<'scene> { + /// Create a new `BVHNode` tree from a given list of [Object](crate::objects::Object)s + #[must_use] + pub fn from_list(bvh_algorithm: BvhAlgorithm, hitables: Vec) -> BVHNode { + #[cfg(feature = "tracing")] + { + info!("BVH tree build algorithm: {bvh_algorithm}"); + info!( + "BVH tree build starting for a list of {} hitables", + hitables.len() + ); + } + let start = Instant::now(); + let bvh = match bvh_algorithm { + BvhAlgorithm::Lam => longest_axis_midpoint(hitables), + BvhAlgorithm::Sah => surface_area_heuristic(hitables), + }; + let end = Instant::now(); + let duration = (end - start).as_millis(); + #[cfg(feature = "tracing")] + info!("BVH tree build done in {duration} ms"); + bvh + } + + #[must_use] + /// Returns the count of the nodes in the tree + pub fn count(&self) -> usize { + let leftsum = match &*self.left { + Hitable::BVHNode(b) => b.count(), + _ => 1, + }; + let rightsum = match &*self.right { + Hitable::BVHNode(b) => b.count(), + _ => 1, + }; + + leftsum + rightsum + } +} diff --git a/clovers/src/bvh/build.rs b/clovers/src/bvh/build.rs new file mode 100644 index 00000000..aa42e2e6 --- /dev/null +++ b/clovers/src/bvh/build.rs @@ -0,0 +1,7 @@ +mod longest_axis_midpoint; +mod surface_area_heuristic; +pub use longest_axis_midpoint::build as longest_axis_midpoint; +pub use surface_area_heuristic::build as surface_area_heuristic; + +// Internal use only +pub(crate) mod utils; diff --git a/clovers/src/bvh/build/longest_axis_midpoint.rs b/clovers/src/bvh/build/longest_axis_midpoint.rs new file mode 100644 index 00000000..b3def0c8 --- /dev/null +++ b/clovers/src/bvh/build/longest_axis_midpoint.rs @@ -0,0 +1,92 @@ +use core::cmp::Ordering; + +use crate::{ + aabb::AABB, + bvh::BVHNode, + hitable::{Empty, Hitable, HitableTrait}, + Float, +}; + +use super::utils::{get_comparator, vec_bounding_box}; + +pub fn build(mut hitables: Vec) -> BVHNode { + // Initialize two child nodes + let left: Box; + let right: Box; + + // What is the axis with the largest span? + // TODO: horribly inefficient, improve! + let bounding: AABB = vec_bounding_box(&hitables).expect("No bounding box for objects"); + let spans = [ + bounding.axis(0).size(), + bounding.axis(1).size(), + bounding.axis(2).size(), + ]; + let largest = Float::max(Float::max(spans[0], spans[1]), spans[2]); + #[allow(clippy::float_cmp)] // TODO: better code for picking the largest axis... + let axis: usize = spans.iter().position(|&x| x == largest).unwrap(); + let comparator = get_comparator(axis); + + // How many objects do we have? + let object_span = hitables.len(); + + if object_span == 1 { + // If we only have one object, add one and an empty object. + // TODO: can this hack be removed? + left = Box::new(hitables[0].clone()); + right = Box::new(Hitable::Empty(Empty {})); + let aabb = left.aabb().unwrap().clone(); // TODO: remove unwrap + return BVHNode { left, right, aabb }; + } else if object_span == 2 { + // If we are comparing two objects, perform the comparison + // Insert the child nodes in order + match comparator(&hitables[0], &hitables[1]) { + Ordering::Less => { + left = Box::new(hitables[0].clone()); + right = Box::new(hitables[1].clone()); + } + Ordering::Greater => { + left = Box::new(hitables[1].clone()); + right = Box::new(hitables[0].clone()); + } + Ordering::Equal => { + // TODO: what should happen here? + panic!("Equal objects in BVHNode from_list"); + } + } + } else if object_span == 3 { + // Three objects: create one bare object and one BVHNode with two objects + hitables.sort_by(comparator); + left = Box::new(hitables[0].clone()); + right = Box::new(Hitable::BVHNode(BVHNode { + left: Box::new(hitables[1].clone()), + right: Box::new(hitables[2].clone()), + aabb: AABB::combine( + // TODO: no unwrap? + hitables[1].aabb().unwrap(), + hitables[2].aabb().unwrap(), + ), + })); + } else { + // Otherwise, recurse + hitables.sort_by(comparator); + + // Split the vector; divide and conquer + let mid = object_span / 2; + let hitables_right = hitables.split_off(mid); + left = Box::new(Hitable::BVHNode(build(hitables))); + right = Box::new(Hitable::BVHNode(build(hitables_right))); + } + + let box_left = left.aabb(); + let box_right = right.aabb(); + + // Generate a bounding box and BVHNode if possible + if let (Some(box_left), Some(box_right)) = (box_left, box_right) { + let aabb = AABB::combine(box_left, box_right); + + BVHNode { left, right, aabb } + } else { + panic!("No bounding box in bvh_node constructor"); + } +} diff --git a/clovers/src/bvh/build/surface_area_heuristic.rs b/clovers/src/bvh/build/surface_area_heuristic.rs new file mode 100644 index 00000000..2459f4ba --- /dev/null +++ b/clovers/src/bvh/build/surface_area_heuristic.rs @@ -0,0 +1,159 @@ +//! Surface Area Heuristic for the BVH tree construction. +//! +//! Heavily inspired by the wonderful blog series . + +#[cfg(feature = "tracing")] +use tracing::warn; + +use crate::{ + aabb::AABB, + bvh::BVHNode, + hitable::{Empty, Hitable, HitableList, HitableTrait}, + Float, +}; + +use super::utils::vec_bounding_box; + +/// Heavily inspired by the wonderful blog series . +pub fn build(hitables: Vec) -> BVHNode { + // Initialize two child nodes + let left: Box; + let right: Box; + + let aabb = vec_bounding_box(&hitables).unwrap(); + let count = hitables.len(); + + // Possible leaf nodes + match count { + 0 => { + #[cfg(feature = "tracing")] + warn!("building a BVHNode from zero hitables"); + left = Box::new(Hitable::Empty(Empty {})); + right = Box::new(Hitable::Empty(Empty {})); + return BVHNode { left, right, aabb }; + } + 1 => { + left = Box::new(hitables[0].clone()); + right = Box::new(Hitable::Empty(Empty {})); + return BVHNode { left, right, aabb }; + } + 2 => { + left = Box::new(hitables[0].clone()); + right = Box::new(hitables[1].clone()); + return BVHNode { left, right, aabb }; + } + _ => (), + }; + + // If we have more than two nodes, split and recurse + let (axis, position) = find_best_split(&hitables); + let (hitables_left, hitables_right) = do_split(hitables, axis, position); + + // Avoid infinite recursion + if hitables_left.is_empty() { + #[cfg(feature = "tracing")] + warn!("hitables_left is empty"); + left = Box::new(Hitable::Empty(Empty {})); + right = Box::new(Hitable::HitableList(HitableList::new(hitables_right))); + + return BVHNode { left, right, aabb }; + }; + if hitables_right.is_empty() { + #[cfg(feature = "tracing")] + warn!("hitables_right is empty"); + left = Box::new(Hitable::HitableList(HitableList::new(hitables_left))); + right = Box::new(Hitable::Empty(Empty {})); + + return BVHNode { left, right, aabb }; + }; + + left = Box::new(Hitable::BVHNode(build(hitables_left))); + right = Box::new(Hitable::BVHNode(build(hitables_right))); + + BVHNode { left, right, aabb } +} + +fn find_best_split(hitables: &Vec) -> (usize, Float) { + // TODO: configurable? + const SPLIT_COUNT: u8 = 8; + const SPLIT_COUNT_F: Float = SPLIT_COUNT as Float; + + #[cfg(feature = "tracing")] + if hitables.len() == 1 { + warn!("best_split trying to split a single hitable"); + }; + + let mut found = false; + let mut best_axis = 0; + let mut best_pos = 0.0; + let mut best_cost = Float::INFINITY; + + for axis in 0..3 { + // find the splitting bounds based on the centroids of the hitables + // this is better than using the bounding box of the hitables + // because the bounding box can be much larger due to the size of the objects + let mut bounds_min = Float::INFINITY; + let mut bounds_max = Float::NEG_INFINITY; + for hitable in hitables { + bounds_min = Float::min(bounds_min, hitable.centroid()[axis]); + bounds_max = Float::max(bounds_max, hitable.centroid()[axis]); + } + + #[allow(clippy::float_cmp)] + if bounds_min == bounds_max { + continue; + }; + + let scale = (bounds_max - bounds_min) / SPLIT_COUNT_F; + for i in 0..SPLIT_COUNT { + let candidate_pos = bounds_min + Float::from(i) * scale; + let cost = evaluate_sah(hitables, axis, candidate_pos); + if cost < best_cost { + found = true; + best_pos = candidate_pos; + best_axis = axis; + best_cost = cost; + } + } + } + + // TODO: fix this, if possible! + #[cfg(feature = "tracing")] + if !found { + warn!("best_split did not find an improved split, returning defaults!"); + } + + (best_axis, best_pos) +} + +fn do_split(hitables: Vec, axis: usize, position: f32) -> (Vec, Vec) { + let (hitables_left, hitables_right): (Vec<_>, Vec<_>) = hitables + .into_iter() + // NOTE: match comparison in evaluate_sah + .partition(|hitable| hitable.centroid()[axis] <= position); + (hitables_left, hitables_right) +} + +fn evaluate_sah(hitables: &Vec, axis: usize, position: Float) -> Float { + // determine triangle counts and bounds for this split candidate + let mut left_box = AABB::default(); + let mut right_box = AABB::default(); + // 2 * 2^64 primitives should be enough + let mut left_count = 0u64; + let mut right_count = 0u64; + for hitable in hitables { + // NOTE: match comparison in do_split + if hitable.centroid()[axis] <= position { + left_count += 1; + left_box = AABB::combine(&left_box, hitable.aabb().unwrap()); // TODO: remove unwrap + } else { + right_count += 1; + right_box = AABB::combine(&right_box, hitable.aabb().unwrap()); // TODO: remove unwrap + } + } + #[allow(clippy::cast_precision_loss)] + let cost: Float = + left_count as Float * left_box.area() + right_count as Float * right_box.area(); + + cost +} diff --git a/clovers/src/bvh/build/utils.rs b/clovers/src/bvh/build/utils.rs new file mode 100644 index 00000000..e9f9815c --- /dev/null +++ b/clovers/src/bvh/build/utils.rs @@ -0,0 +1,74 @@ +// Internal helper functions + +use core::cmp::Ordering; + +use crate::{aabb::AABB, hitable::Hitable, hitable::HitableTrait}; + +pub(crate) fn box_compare(a: &Hitable, b: &Hitable, axis: usize) -> Ordering { + let box_a: Option<&AABB> = a.aabb(); + let box_b: Option<&AABB> = b.aabb(); + + if let (Some(box_a), Some(box_b)) = (box_a, box_b) { + if box_a.axis(axis).min < box_b.axis(axis).min { + Ordering::Less + } else { + // Default to greater, even if equal + Ordering::Greater + } + } else { + panic!("No bounding box to compare with.") + } +} + +fn box_x_compare(a: &Hitable, b: &Hitable) -> Ordering { + box_compare(a, b, 0) +} + +fn box_y_compare(a: &Hitable, b: &Hitable) -> Ordering { + box_compare(a, b, 1) +} + +fn box_z_compare(a: &Hitable, b: &Hitable) -> Ordering { + box_compare(a, b, 2) +} + +pub(crate) fn get_comparator(axis: usize) -> fn(&Hitable, &Hitable) -> Ordering { + let comparators = [box_x_compare, box_y_compare, box_z_compare]; + comparators[axis] +} + +// TODO: inefficient, O(n) *and* gets called at every iteration of BVHNode creation => quadratic behavior +#[must_use] +pub(crate) fn vec_bounding_box(vec: &Vec) -> Option { + if vec.is_empty() { + return None; + } + + // Mutable AABB that we grow from zero + let mut output_box: Option = None; + + // Go through all the objects, and expand the AABB + for object in vec { + // Check if the object has a box + let Some(bounding) = object.aabb() else { + // No box found for the object, early return. + // Having even one unbounded object in a list makes the entire list unbounded! + return None; + }; + + // Do we have an output_box already saved? + match output_box { + // If we do, expand it & recurse + Some(old_box) => { + output_box = Some(AABB::combine(&old_box, bounding)); + } + // Otherwise, set output box to be the newly-found box + None => { + output_box = Some(bounding.clone()); + } + } + } + + // Return the final combined output_box + output_box +} diff --git a/clovers/src/bvh/hitable_trait.rs b/clovers/src/bvh/hitable_trait.rs new file mode 100644 index 00000000..ec7bf013 --- /dev/null +++ b/clovers/src/bvh/hitable_trait.rs @@ -0,0 +1,137 @@ +use rand::{rngs::SmallRng, Rng}; + +use crate::{ + aabb::AABB, + hitable::{Hitable, HitableTrait}, + ray::Ray, + wavelength::Wavelength, + Direction, Displacement, Float, HitRecord, Position, +}; + +use super::BVHNode; + +impl<'scene> HitableTrait for BVHNode<'scene> { + /// The main `hit` function for a [`BVHNode`]. Given a [Ray], and an interval `distance_min` and `distance_max`, returns either `None` or `Some(HitRecord)` based on whether the ray intersects with the encased objects during that interval. + #[must_use] + fn hit( + &self, + ray: &Ray, + distance_min: Float, + distance_max: Float, + rng: &mut SmallRng, + ) -> Option { + // If we do not hit the bounding box of current node, early return None + if !self.aabb.hit(ray, distance_min, distance_max) { + return None; + } + + // Check the distance to the bounding boxes + let (left_aabb_distance, right_aabb_distance) = match (self.left.aabb(), self.right.aabb()) + { + // Early returns, if there's no bounding box + (None, None) => return None, + (Some(_l), None) => return self.left.hit(ray, distance_min, distance_max, rng), + (None, Some(_r)) => return self.right.hit(ray, distance_min, distance_max, rng), + // If we have bounding boxes, get the distances + (Some(l), Some(r)) => (l.distance(ray), r.distance(ray)), + }; + let (_closest_aabb_distance, furthest_aabb_distance) = + match (left_aabb_distance, right_aabb_distance) { + // Early return: neither child AABB can be hit with the ray + (None, None) => return None, + // Early return: only one child can be hit with the ray + (Some(_d), None) => return self.left.hit(ray, distance_min, distance_max, rng), + (None, Some(_d)) => return self.right.hit(ray, distance_min, distance_max, rng), + // Default case: both children can be hit with the ray, check the distance + (Some(l), Some(r)) => (Float::min(l, r), Float::max(l, r)), + }; + + // Check the closest first + let (closest_bvh, furthest_bvh) = if left_aabb_distance < right_aabb_distance { + (&self.left, &self.right) + } else { + (&self.right, &self.left) + }; + let closest_bvh_hit = closest_bvh.hit(ray, distance_min, distance_max, rng); + + // Is the hit closer than the closest point of the other AABB? + if let Some(ref hit_record) = closest_bvh_hit { + if hit_record.distance < furthest_aabb_distance { + return Some(hit_record.clone()); + } + } + // Otherwise, check the other child too + let furthest_bvh_hit = furthest_bvh.hit(ray, distance_min, distance_max, rng); + + // Did we hit neither of the child nodes, one of them, or both? + // Return the closest thing we hit + match (&closest_bvh_hit, &furthest_bvh_hit) { + (None, None) => None, + (None, Some(_)) => furthest_bvh_hit, + (Some(_), None) => closest_bvh_hit, + (Some(left), Some(right)) => { + if left.distance < right.distance { + return closest_bvh_hit; + } + furthest_bvh_hit + } + } + } + + /// Returns the axis-aligned bounding box [AABB] of the objects within this [`BVHNode`]. + #[must_use] + fn aabb(&self) -> Option<&AABB> { + Some(&self.aabb) + } + + /// Returns a probability density function value based on the children + #[must_use] + fn pdf_value( + &self, + origin: Position, + direction: Direction, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + match (&*self.left, &*self.right) { + (_, Hitable::Empty(_)) => self + .left + .pdf_value(origin, direction, wavelength, time, rng), + (Hitable::Empty(_), _) => self + .right + .pdf_value(origin, direction, wavelength, time, rng), + (_, _) => { + (self + .left + .pdf_value(origin, direction, wavelength, time, rng) + + self + .right + .pdf_value(origin, direction, wavelength, time, rng)) + / 2.0 + } + } + } + + // TODO: improve correctness & optimization! + /// Returns a random point on the surface of one of the children + #[must_use] + fn random(&self, origin: Position, rng: &mut SmallRng) -> Displacement { + match (&*self.left, &*self.right) { + (_, Hitable::Empty(_)) => self.left.random(origin, rng), + (Hitable::Empty(_), _) => self.right.random(origin, rng), + (_, _) => { + if rng.gen::() { + self.left.random(origin, rng) + } else { + self.right.random(origin, rng) + } + } + } + } + + // TODO: remove? + fn centroid(&self) -> Position { + self.aabb.centroid() + } +} diff --git a/clovers/src/bvh/primitive_testcount.rs b/clovers/src/bvh/primitive_testcount.rs new file mode 100644 index 00000000..1c8e06c1 --- /dev/null +++ b/clovers/src/bvh/primitive_testcount.rs @@ -0,0 +1,77 @@ +use rand::rngs::SmallRng; + +use crate::{hitable::Hitable, ray::Ray, Float}; + +use super::BVHNode; + +impl<'scene> BVHNode<'scene> { + /// Alternate hit method that maintains a test count for the primitives + pub fn primitive_testcount( + &'scene self, + count: &mut usize, + ray: &Ray, + distance_min: Float, + distance_max: Float, + rng: &mut SmallRng, + ) { + // If we do not hit the bounding box of current node, early return None + if !self.aabb.hit(ray, distance_min, distance_max) { + return; + } + + // Otherwise we have hit the bounding box of this node, recurse to child nodes + primitive_testcount_recurse_condition( + &self.left, + count, + ray, + distance_min, + distance_max, + rng, + ); + + primitive_testcount_recurse_condition( + &self.right, + count, + ray, + distance_min, + distance_max, + rng, + ); + } +} + +fn primitive_testcount_recurse_condition( + bvhnode: &Hitable, // BVHNode + count: &mut usize, + ray: &Ray, + distance_min: Float, + distance_max: Float, + rng: &mut SmallRng, +) { + match bvhnode { + // These recurse + Hitable::BVHNode(x) => { + x.primitive_testcount(count, ray, distance_min, distance_max, rng); + } + + // These are counted + Hitable::MovingSphere(_) + | Hitable::Quad(_) + | Hitable::Sphere(_) + | Hitable::ConstantMedium(_) + | Hitable::Triangle(_) + | Hitable::GLTFTriangle(_) + | Hitable::RotateY(_) + | Hitable::Translate(_) => { + // TODO: currently RotateY and Translate are counted wrong. They may contain more primitives! + *count += 1; + } + Hitable::HitableList(l) => { + *count += l.hitables.len(); + } + Hitable::Boxy(_) => { + *count += 6; + } + Hitable::Empty(_) => (), + } +} diff --git a/clovers/src/bvh/testcount.rs b/clovers/src/bvh/testcount.rs new file mode 100644 index 00000000..8fff0c7e --- /dev/null +++ b/clovers/src/bvh/testcount.rs @@ -0,0 +1,104 @@ +use rand::rngs::SmallRng; + +use crate::{ + hitable::{Hitable, HitableTrait}, + ray::Ray, + Float, HitRecord, +}; + +use super::BVHNode; + +impl<'scene> BVHNode<'scene> { + // NOTE: this must be kept in close alignment with the implementation of BVHNode::hit()! + // TODO: maybe move the statistics counting to the method itself? Measure the impact? + /// Alternate hit method that maintains a test count for the BVH traversals. + pub fn testcount( + &'scene self, + depth: &mut usize, + ray: &Ray, + distance_min: Float, + distance_max: Float, + rng: &mut SmallRng, + ) -> Option { + *depth += 1; + + // If we do not hit the bounding box of current node, early return None + if !self.aabb.hit(ray, distance_min, distance_max) { + return None; + } + + // Check the distance to the bounding boxes + let (left_aabb_distance, right_aabb_distance) = match (self.left.aabb(), self.right.aabb()) + { + // Early returns, if there's no bounding box + (None, None) => return None, + (Some(_l), None) => { + return recurse(&self.left, depth, ray, distance_min, distance_max, rng) + } + (None, Some(_r)) => { + return recurse(&self.right, depth, ray, distance_min, distance_max, rng) + } + // If we have bounding boxes, get the distances + (Some(l), Some(r)) => (l.distance(ray), r.distance(ray)), + }; + let (_closest_aabb_distance, furthest_aabb_distance) = + match (left_aabb_distance, right_aabb_distance) { + // Early return: neither child AABB can be hit with the ray + (None, None) => return None, + // Early return: only one child can be hit with the ray + (Some(_d), None) => { + return recurse(&self.left, depth, ray, distance_min, distance_max, rng) + } + (None, Some(_d)) => { + return recurse(&self.right, depth, ray, distance_min, distance_max, rng) + } + // Default case: both children can be hit with the ray, check the distance + (Some(l), Some(r)) => (Float::min(l, r), Float::max(l, r)), + }; + + // Check the closest first + let (closest_bvh, furthest_bvh) = if left_aabb_distance < right_aabb_distance { + (&self.left, &self.right) + } else { + (&self.right, &self.left) + }; + let closest_bvh_hit = recurse(closest_bvh, depth, ray, distance_min, distance_max, rng); + + // Is the hit closer than the closest point of the other AABB? + if let Some(ref hit_record) = closest_bvh_hit { + if hit_record.distance < furthest_aabb_distance { + return Some(hit_record.clone()); + } + } + // Otherwise, check the other child too + let furthest_bvh_hit = recurse(furthest_bvh, depth, ray, distance_min, distance_max, rng); + + // Did we hit neither of the child nodes, one of them, or both? + // Return the closest thing we hit + match (&closest_bvh_hit, &furthest_bvh_hit) { + (None, None) => None, + (None, Some(_)) => furthest_bvh_hit, + (Some(_), None) => closest_bvh_hit, + (Some(left), Some(right)) => { + if left.distance < right.distance { + return closest_bvh_hit; + } + furthest_bvh_hit + } + } + } +} + +fn recurse<'scene>( + bvhnode: &'scene Hitable, // BVHNode + depth: &mut usize, + ray: &Ray, + distance_min: Float, + distance_max: Float, + rng: &mut SmallRng, +) -> Option> { + match bvhnode { + Hitable::BVHNode(bvh) => bvh.testcount(depth, ray, distance_min, distance_max, rng), + hitable => hitable.hit(ray, distance_min, distance_max, rng), + } +} diff --git a/clovers/src/bvhnode.rs b/clovers/src/bvhnode.rs deleted file mode 100644 index fa01734e..00000000 --- a/clovers/src/bvhnode.rs +++ /dev/null @@ -1,296 +0,0 @@ -//! Bounding Volume Hierarchy Node. - -use core::cmp::Ordering; - -use rand::{rngs::SmallRng, Rng}; - -use crate::{ - aabb::AABB, - hitable::{Empty, HitRecord, Hitable, HitableTrait}, - ray::Ray, - wavelength::Wavelength, - Box, Direction, Displacement, Float, Position, Vec, -}; - -/// Bounding Volume Hierarchy Node. -/// -/// A node in a tree structure defining a hierarchy of objects in a scene: a node knows its bounding box, and has two children which are also `BVHNode`s. This is used for accelerating the ray-object intersection calculation in the ray tracer. See [Bounding Volume hierarchies](https://raytracing.github.io/books/RayTracingTheNextWeek.html) -#[derive(Debug, Clone)] -pub struct BVHNode<'scene> { - /// Left child of the `BVHNode` - pub left: Box>, - /// Right child of the `BVHNode` - pub right: Box>, - /// Bounding box containing both of the child nodes - pub bounding_box: AABB, -} - -impl<'scene> BVHNode<'scene> { - /// Create a new `BVHNode` tree from a given list of [Object](crate::objects::Object)s - #[must_use] - pub fn from_list(mut hitables: Vec, time_0: Float, time_1: Float) -> BVHNode { - // Initialize two child nodes - let left: Box; - let right: Box; - - let comparators = [box_x_compare, box_y_compare, box_z_compare]; - - // What is the axis with the largest span? - // TODO: horribly inefficient, improve! - let bounding: AABB = - vec_bounding_box(&hitables, time_0, time_1).expect("No bounding box for objects"); - let spans = [ - bounding.axis(0).size(), - bounding.axis(1).size(), - bounding.axis(2).size(), - ]; - let largest = f32::max(f32::max(spans[0], spans[1]), spans[2]); - #[allow(clippy::float_cmp)] // TODO: better code for picking the largest axis... - let axis: usize = spans.iter().position(|&x| x == largest).unwrap(); - let comparator = comparators[axis]; - - // How many objects do we have? - let object_span = hitables.len(); - - if object_span == 1 { - // If we only have one object, add one and an empty object. - // TODO: can this hack be removed? - left = Box::new(hitables[0].clone()); - right = Box::new(Hitable::Empty(Empty {})); - let bounding_box = left.bounding_box(time_0, time_1).unwrap().clone(); // TODO: remove unwrap - return BVHNode { - left, - right, - bounding_box, - }; - } else if object_span == 2 { - // If we are comparing two objects, perform the comparison - // Insert the child nodes in order - match comparator(&hitables[0], &hitables[1]) { - Ordering::Less => { - left = Box::new(hitables[0].clone()); - right = Box::new(hitables[1].clone()); - } - Ordering::Greater => { - left = Box::new(hitables[1].clone()); - right = Box::new(hitables[0].clone()); - } - Ordering::Equal => { - // TODO: what should happen here? - panic!("Equal objects in BVHNode from_list"); - } - } - } else if object_span == 3 { - // Three objects: create one bare object and one BVHNode with two objects - hitables.sort_by(comparator); - left = Box::new(hitables[0].clone()); - right = Box::new(Hitable::BVHNode(BVHNode { - left: Box::new(hitables[1].clone()), - right: Box::new(hitables[2].clone()), - bounding_box: AABB::surrounding_box( - // TODO: no unwrap? - hitables[1].bounding_box(time_0, time_1).unwrap(), - hitables[2].bounding_box(time_0, time_1).unwrap(), - ), - })); - } else { - // Otherwise, recurse - hitables.sort_by(comparator); - - // Split the vector; divide and conquer - let mid = object_span / 2; - let hitables_right = hitables.split_off(mid); - left = Box::new(Hitable::BVHNode(BVHNode::from_list( - hitables, time_0, time_1, - ))); - right = Box::new(Hitable::BVHNode(BVHNode::from_list( - hitables_right, - time_0, - time_1, - ))); - } - - let box_left = left.bounding_box(time_0, time_1); - let box_right = right.bounding_box(time_0, time_1); - - // Generate a bounding box and BVHNode if possible - if let (Some(box_left), Some(box_right)) = (box_left, box_right) { - let bounding_box = AABB::surrounding_box(box_left, box_right); - - BVHNode { - left, - right, - bounding_box, - } - } else { - panic!("No bounding box in bvh_node constructor"); - } - } - - #[must_use] - /// Returns the count of the nodes in the tree - pub fn count(&self) -> usize { - let leftsum = match &*self.left { - Hitable::BVHNode(b) => b.count(), - _ => 1, - }; - let rightsum = match &*self.right { - Hitable::BVHNode(b) => b.count(), - _ => 1, - }; - - leftsum + rightsum - } -} - -impl<'scene> HitableTrait for BVHNode<'scene> { - /// The main `hit` function for a [`BVHNode`]. Given a [Ray], and an interval `distance_min` and `distance_max`, returns either `None` or `Some(HitRecord)` based on whether the ray intersects with the encased objects during that interval. - #[must_use] - fn hit( - &self, - ray: &Ray, - distance_min: Float, - distance_max: Float, - rng: &mut SmallRng, - ) -> Option { - // If we do not hit the bounding box of current node, early return None - if !self.bounding_box.hit(ray, distance_min, distance_max) { - return None; - } - - // Otherwise we have hit the bounding box of this node, recurse to child nodes - let hit_left = self.left.hit(ray, distance_min, distance_max, rng); - let hit_right = self.right.hit(ray, distance_min, distance_max, rng); - - // Did we hit neither of the child nodes, one of them, or both? - // Return the closest thing we hit - match (&hit_left, &hit_right) { - (None, None) => None, // In theory, this case should not be reachable - (None, Some(_)) => hit_right, - (Some(_), None) => hit_left, - (Some(left), Some(right)) => { - if left.distance < right.distance { - return hit_left; - } - hit_right - } - } - } - - /// Returns the axis-aligned bounding box [AABB] of the objects within this [`BVHNode`]. - #[must_use] - fn bounding_box(&self, _t0: Float, _t11: Float) -> Option<&AABB> { - Some(&self.bounding_box) - } - - /// Returns a probability density function value based on the children - #[must_use] - fn pdf_value( - &self, - origin: Position, - direction: Direction, - wavelength: Wavelength, - time: Float, - rng: &mut SmallRng, - ) -> Float { - match (&*self.left, &*self.right) { - (_, Hitable::Empty(_)) => self - .left - .pdf_value(origin, direction, wavelength, time, rng), - (Hitable::Empty(_), _) => self - .right - .pdf_value(origin, direction, wavelength, time, rng), - (_, _) => { - (self - .left - .pdf_value(origin, direction, wavelength, time, rng) - + self - .right - .pdf_value(origin, direction, wavelength, time, rng)) - / 2.0 - } - } - } - - // TODO: improve correctness & optimization! - /// Returns a random point on the surface of one of the children - #[must_use] - fn random(&self, origin: Position, rng: &mut SmallRng) -> Displacement { - match (&*self.left, &*self.right) { - (_, Hitable::Empty(_)) => self.left.random(origin, rng), - (Hitable::Empty(_), _) => self.right.random(origin, rng), - (_, _) => { - if rng.gen::() { - self.left.random(origin, rng) - } else { - self.right.random(origin, rng) - } - } - } - } -} - -fn box_compare(a: &Hitable, b: &Hitable, axis: usize) -> Ordering { - // TODO: proper time support? - let box_a: Option<&AABB> = a.bounding_box(0.0, 1.0); - let box_b: Option<&AABB> = b.bounding_box(0.0, 1.0); - - if let (Some(box_a), Some(box_b)) = (box_a, box_b) { - if box_a.axis(axis).min < box_b.axis(axis).min { - Ordering::Less - } else { - // Default to greater, even if equal - Ordering::Greater - } - } else { - panic!("No bounding box to compare with.") - } -} - -fn box_x_compare(a: &Hitable, b: &Hitable) -> Ordering { - box_compare(a, b, 0) -} - -fn box_y_compare(a: &Hitable, b: &Hitable) -> Ordering { - box_compare(a, b, 1) -} - -fn box_z_compare(a: &Hitable, b: &Hitable) -> Ordering { - box_compare(a, b, 2) -} - -// TODO: inefficient, O(n) *and* gets called at every iteration of BVHNode creation => quadratic behavior -#[must_use] -fn vec_bounding_box(vec: &Vec, t0: Float, t1: Float) -> Option { - if vec.is_empty() { - return None; - } - - // Mutable AABB that we grow from zero - let mut output_box: Option = None; - - // Go through all the objects, and expand the AABB - for object in vec { - // Check if the object has a box - let Some(bounding) = object.bounding_box(t0, t1) else { - // No box found for the object, early return. - // Having even one unbounded object in a list makes the entire list unbounded! - return None; - }; - - // Do we have an output_box already saved? - match output_box { - // If we do, expand it & recurse - Some(old_box) => { - output_box = Some(AABB::surrounding_box(&old_box, bounding)); - } - // Otherwise, set output box to be the newly-found box - None => { - output_box = Some(bounding.clone()); - } - } - } - - // Return the final combined output_box - output_box -} diff --git a/clovers/src/camera.rs b/clovers/src/camera.rs index e749c150..abfe2953 100644 --- a/clovers/src/camera.rs +++ b/clovers/src/camera.rs @@ -7,7 +7,7 @@ use crate::{ray::Ray, Float, Vec3, PI}; use crate::{Direction, Position, Vec2}; use nalgebra::Unit; -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] /// The main [Camera] object used in the ray tracing. pub struct Camera { @@ -101,7 +101,7 @@ impl Camera { /// Generates a new [Ray] from the camera #[must_use] pub fn get_ray( - self, + &self, pixel_uv: Vec2, // pixel location in image uv coordinates, range 0..1 mut lens_offset: Vec2, time: Float, diff --git a/clovers/src/hitable.rs b/clovers/src/hitable.rs index b03a62d4..3e4b36d0 100644 --- a/clovers/src/hitable.rs +++ b/clovers/src/hitable.rs @@ -1,53 +1,19 @@ //! An abstraction for things that can be hit by [Rays](crate::ray::Ray). -#[cfg(feature = "stl")] -use crate::objects::STL; #[cfg(feature = "gl_tf")] -use crate::objects::{GLTFTriangle, GLTF}; +use crate::objects::GLTFTriangle; use crate::{ aabb::AABB, - bvhnode::BVHNode, - materials::MaterialTrait, + bvh::{build::utils::vec_bounding_box, BVHNode}, objects::{Boxy, ConstantMedium, MovingSphere, Quad, RotateY, Sphere, Translate, Triangle}, ray::Ray, wavelength::Wavelength, - Direction, Displacement, Float, Position, + Direction, Displacement, Float, HitRecord, Position, Vec3, }; use enum_dispatch::enum_dispatch; -use rand::rngs::SmallRng; - -/// Represents a ray-object intersection, with plenty of data about the intersection. -#[derive(Debug)] -pub struct HitRecord<'a> { - /// Distance from the ray origin to the hitpoint - pub distance: Float, - /// 3D coordinate of the hitpoint - pub position: Position, - /// Surface normal from the hitpoint - pub normal: Direction, - /// U surface coordinate of the hitpoint - pub u: Float, - /// V surface coordinate of the hitpoint - pub v: Float, - /// Reference to the material at the hitpoint - pub material: &'a dyn MaterialTrait, - /// Is the hitpoint at the front of the surface - pub front_face: bool, -} - -impl<'a> HitRecord<'a> { - /// Helper function for getting normals pointing at the correct direction. TODO: consider removal? - pub fn set_face_normal(&mut self, ray: &Ray, outward_normal: Direction) { - self.front_face = ray.direction.dot(&outward_normal) < 0.0; - if self.front_face { - self.normal = outward_normal; - } else { - self.normal = -outward_normal; - } - } -} +use rand::{rngs::SmallRng, seq::IteratorRandom}; /// Enumeration of all runtime entities that can be intersected aka "hit" by a [Ray]. #[enum_dispatch(HitableTrait)] @@ -61,15 +27,12 @@ pub enum Hitable<'scene> { Quad(Quad<'scene>), RotateY(RotateY<'scene>), Sphere(Sphere<'scene>), - #[cfg(feature = "stl")] - STL(STL<'scene>), - #[cfg(feature = "gl_tf")] - GLTF(GLTF<'scene>), Translate(Translate<'scene>), Triangle(Triangle<'scene>), Empty(Empty), #[cfg(feature = "gl_tf")] GLTFTriangle(GLTFTriangle<'scene>), + HitableList(HitableList<'scene>), } // TODO: remove horrible hack @@ -88,7 +51,7 @@ impl HitableTrait for Empty { None } - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { None } @@ -102,6 +65,10 @@ impl HitableTrait for Empty { ) -> Float { 0.0 } + + fn centroid(&self) -> Position { + Vec3::new(0.0, 0.0, 0.0) + } } #[enum_dispatch] @@ -119,7 +86,7 @@ pub trait HitableTrait { #[must_use] /// Returns the bounding box of the entity. - fn bounding_box(&self, t0: Float, t1: Float) -> Option<&AABB>; + fn aabb(&self) -> Option<&AABB>; #[must_use] /// Probability density function value method, used for multiple importance sampling. @@ -139,6 +106,10 @@ pub trait HitableTrait { "HitableTrait::random called for a Hitable that has no implementation for it!" ); } + + /// Returns the center point of the hitable + #[must_use] + fn centroid(&self) -> Position; } /// Returns a tuple of `(front_face, normal)`. Used in lieu of `set_face_normal` in the Ray Tracing for the Rest Of Your Life book. @@ -153,3 +124,94 @@ pub fn get_orientation(ray: &Ray, outward_normal: Direction) -> (bool, Direction (front_face, normal) } + +/// A list of `Hitable`s, occasionally used as the leaf of `BVHNode` when further splitting is not possible or beneficial. +/// +/// Hopefully temporary. +// TODO: remove? +#[derive(Debug, Clone)] +pub struct HitableList<'scene> { + /// Hitables in the list + pub hitables: Vec>, + aabb: AABB, +} + +impl<'scene> HitableList<'scene> { + /// Creates a new [`HitableList`]. + #[must_use] + pub fn new(hitables: Vec>) -> Self { + let aabb = vec_bounding_box(&hitables).unwrap(); + Self { hitables, aabb } + } + + /// Recursively flattens the `HitableList` into a `Vec` + #[must_use] + pub fn flatten(self) -> Vec> { + let mut flat: Vec = Vec::new(); + for hitable in &self.hitables { + match hitable { + Hitable::HitableList(l) => { + let mut flatten = l.clone().flatten(); + flat.append(&mut flatten); + } + h => flat.push(h.clone()), + } + } + flat + } +} + +// TODO: ideally, this impl should be removed entirely +impl<'scene> HitableTrait for HitableList<'scene> { + #[must_use] + fn hit( + &self, + ray: &Ray, + distance_min: Float, + distance_max: Float, + rng: &mut SmallRng, + ) -> Option { + let mut distance = Float::INFINITY; + let mut closest: Option = None; + for hitable in &self.hitables { + let hit_record = hitable.hit(ray, distance_min, distance_max, rng)?; + if hit_record.distance < distance { + distance = hit_record.distance; + closest = Some(hit_record); + } + } + + closest + } + + #[must_use] + fn aabb(&self) -> Option<&AABB> { + Some(&self.aabb) + } + + #[must_use] + fn pdf_value( + &self, + _origin: Position, + _direction: Direction, + _wavelength: Wavelength, + _time: Float, + _rng: &mut SmallRng, + ) -> Float { + // TODO: fix + 0.0 + } + + #[must_use] + fn centroid(&self) -> Position { + // TODO: ideally, this shouldn't be used at all! + // Currently, this can be called when a `HitableList` is used as an object within a `Translate` or `RotateY` + // Those should be removed too! + self.aabb.centroid() + } + + fn random(&self, origin: Position, rng: &mut SmallRng) -> Displacement { + let hitable = self.hitables.iter().choose(rng).unwrap(); + hitable.random(origin, rng) + } +} diff --git a/clovers/src/hitrecord.rs b/clovers/src/hitrecord.rs new file mode 100644 index 00000000..812a76ba --- /dev/null +++ b/clovers/src/hitrecord.rs @@ -0,0 +1,34 @@ +//! The main data structure returned for every surface intersection. + +use crate::{materials::MaterialTrait, ray::Ray, Direction, Float, Position}; + +/// Represents a ray-object intersection, with plenty of data about the intersection. +#[derive(Clone, Debug)] +pub struct HitRecord<'a> { + /// Distance from the ray origin to the hitpoint + pub distance: Float, + /// 3D coordinate of the hitpoint + pub position: Position, + /// Surface normal from the hitpoint + pub normal: Direction, + /// U surface coordinate of the hitpoint + pub u: Float, + /// V surface coordinate of the hitpoint + pub v: Float, + /// Reference to the material at the hitpoint + pub material: &'a dyn MaterialTrait, + /// Is the hitpoint at the front of the surface + pub front_face: bool, +} + +impl<'a> HitRecord<'a> { + /// Sets the face normal of this [`HitRecord`]. + pub fn set_face_normal(&mut self, ray: &Ray, outward_normal: Direction) { + self.front_face = ray.direction.dot(&outward_normal) < 0.0; + if self.front_face { + self.normal = outward_normal; + } else { + self.normal = -outward_normal; + } + } +} diff --git a/clovers/src/interval.rs b/clovers/src/interval.rs index ff8bb0a4..87ccaaa7 100644 --- a/clovers/src/interval.rs +++ b/clovers/src/interval.rs @@ -5,7 +5,7 @@ use core::ops::Add; use crate::Float; /// An interval structure. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] pub struct Interval { /// Smallest value of the interval. Must be kept in order @@ -27,7 +27,7 @@ impl Interval { /// Constructs a new interval from two intervals // TODO: explanation, clarification #[must_use] - pub fn new_from_intervals(a: Interval, b: Interval) -> Self { + pub fn combine(a: &Interval, b: &Interval) -> Self { Interval { min: a.min.min(b.min), max: a.max.max(b.max), @@ -42,9 +42,15 @@ impl Interval { /// Returns the size of the interval #[must_use] - pub fn size(self) -> Float { + pub fn size(&self) -> Float { self.max - self.min } + + /// Returns the center of this [`Interval`] + #[must_use] + pub fn center(&self) -> Float { + self.min + 0.5 * self.size() + } } impl Add for Interval { @@ -54,3 +60,41 @@ impl Add for Interval { Interval::new(self.min + offset, self.max + offset) } } + +#[cfg(test)] +#[allow(clippy::float_cmp)] +mod tests { + use super::*; + + #[test] + fn center() { + let interval = Interval::new(0.0, 1.0); + let center = interval.center(); + let expected = 0.5; + assert_eq!(center, expected); + } + + #[test] + fn center_zero_crossing() { + let interval = Interval::new(-1.0, 1.0); + let center = interval.center(); + let expected = 0.0; + assert_eq!(center, expected); + } + + #[test] + fn size() { + let interval = Interval::new(0.0, 1.0); + let size = interval.size(); + let expected = 1.0; + assert_eq!(size, expected); + } + + #[test] + fn size_zero_crossing() { + let interval = Interval::new(-1.0, 1.0); + let size = interval.size(); + let expected = 2.0; + assert_eq!(size, expected); + } +} diff --git a/clovers/src/lib.rs b/clovers/src/lib.rs index 07abb106..6c6936e6 100644 --- a/clovers/src/lib.rs +++ b/clovers/src/lib.rs @@ -30,9 +30,9 @@ //! //! - Rendering is done by creating [`Ray`](ray::Ray)s and seeing what they hit //! - A [`Ray`](ray::Ray) has an origin and a direction -//! - Every [`Object`](objects::Object) has a `hit()` method that takes a [Ray](ray::Ray) and returns an Option<[`HitRecord`](hitable::HitRecord)> +//! - Every [`Object`](objects::Object) has a `hit()` method that takes a [Ray](ray::Ray) and returns an Option<[`HitRecord`]> //! - If you get None, use that information to colorize your pixel with a default color -//! - If you get Some([`HitRecord`](hitable::HitRecord)), use its details to colorize your pixel +//! - If you get Some([`HitRecord`]), use its details to colorize your pixel //! - You most likely also want to recurse: depending on the material, maybe `scatter()` and cast a new [`Ray`](ray::Ray)? //! //! You most likely want to repeat this process multiple times for each of your pixels: generating multiple samples per pixel results in a higher quality image. @@ -77,10 +77,12 @@ use nalgebra::{ // Internals pub mod aabb; -pub mod bvhnode; +pub mod bvh; pub mod camera; pub mod colorinit; pub mod hitable; +pub mod hitrecord; +pub use hitrecord::HitRecord; pub mod interval; pub mod materials; pub mod objects; @@ -93,24 +95,6 @@ pub mod spectrum; pub mod textures; pub mod wavelength; -/// Rendering options struct -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -pub struct RenderOpts { - /// Width of the render in pixels - pub width: u32, - /// Height of the render in pixels - pub height: u32, - /// Samples per pixel to render for multisampling. Higher number implies higher quality. - pub samples: u32, - /// Maximum ray bounce depth. Higher number implies higher quality. - pub max_depth: u32, - /// Optionally, suppress CLI output - pub quiet: bool, - /// Experimental render mode: return a normal map only instead of doing a full path trace render. - pub normalmap: bool, -} - // Handy aliases for internal use /// Internal type alias: this allows the crate to easily switch between float precision without modifying a lot of files. diff --git a/clovers/src/materials.rs b/clovers/src/materials.rs index 0745f83b..4b02338a 100644 --- a/clovers/src/materials.rs +++ b/clovers/src/materials.rs @@ -4,7 +4,7 @@ use alloc::string::String; use core::fmt::Debug; use nalgebra::Unit; -use crate::{hitable::HitRecord, pdf::PDF, ray::Ray, Direction, Float, Position, Vec3}; +use crate::{pdf::PDF, ray::Ray, Direction, Float, HitRecord, Position, Vec3}; pub mod cone_light; pub mod dielectric; pub mod diffuse_light; diff --git a/clovers/src/materials/cone_light.rs b/clovers/src/materials/cone_light.rs index 0b846974..e92a4c8f 100644 --- a/clovers/src/materials/cone_light.rs +++ b/clovers/src/materials/cone_light.rs @@ -2,10 +2,9 @@ use super::{MaterialTrait, ScatterRecord}; use crate::{ - hitable::HitRecord, ray::Ray, textures::{SolidColor, Texture, TextureTrait}, - Float, Position, + Float, HitRecord, Position, }; use palette::{white_point::E, Xyz}; use rand::prelude::SmallRng; diff --git a/clovers/src/materials/dielectric.rs b/clovers/src/materials/dielectric.rs index 1abc79f0..363f9d3b 100644 --- a/clovers/src/materials/dielectric.rs +++ b/clovers/src/materials/dielectric.rs @@ -2,10 +2,9 @@ use super::{reflect, refract, schlick, MaterialTrait, MaterialType, ScatterRecord}; use crate::{ - hitable::HitRecord, pdf::{ZeroPDF, PDF}, ray::Ray, - Direction, Float, + Direction, Float, HitRecord, }; use palette::{white_point::E, Xyz}; use rand::rngs::SmallRng; diff --git a/clovers/src/materials/diffuse_light.rs b/clovers/src/materials/diffuse_light.rs index 6b503b17..3568f0f8 100644 --- a/clovers/src/materials/diffuse_light.rs +++ b/clovers/src/materials/diffuse_light.rs @@ -2,10 +2,9 @@ use super::{MaterialTrait, ScatterRecord}; use crate::{ - hitable::HitRecord, ray::Ray, textures::{SolidColor, Texture, TextureTrait}, - Float, Position, + Float, HitRecord, Position, }; use palette::{white_point::E, Xyz}; use rand::prelude::SmallRng; diff --git a/clovers/src/materials/dispersive.rs b/clovers/src/materials/dispersive.rs index c9ff3b1d..6d7ebf06 100644 --- a/clovers/src/materials/dispersive.rs +++ b/clovers/src/materials/dispersive.rs @@ -17,11 +17,10 @@ use palette::Xyz; use rand::{rngs::SmallRng, Rng}; use crate::{ - hitable::HitRecord, pdf::{ZeroPDF, PDF}, ray::Ray, wavelength::Wavelength, - Direction, Float, + Direction, Float, HitRecord, }; use super::{reflect, refract, schlick, MaterialTrait, MaterialType, ScatterRecord}; diff --git a/clovers/src/materials/gltf.rs b/clovers/src/materials/gltf.rs index 39ed6c0f..f6723ff8 100644 --- a/clovers/src/materials/gltf.rs +++ b/clovers/src/materials/gltf.rs @@ -12,11 +12,10 @@ use palette::{ use rand::rngs::SmallRng; use crate::{ - hitable::HitRecord, pdf::{ZeroPDF, PDF}, random::random_unit_vector, ray::Ray, - Direction, Float, Vec2, Vec3, Vec4, PI, + Direction, Float, HitRecord, Vec2, Vec3, Vec4, PI, }; use super::{reflect, MaterialTrait, MaterialType, ScatterRecord}; diff --git a/clovers/src/materials/isotropic.rs b/clovers/src/materials/isotropic.rs index 2f77ec06..53404d3d 100644 --- a/clovers/src/materials/isotropic.rs +++ b/clovers/src/materials/isotropic.rs @@ -2,11 +2,10 @@ use super::{MaterialTrait, MaterialType, ScatterRecord}; use crate::{ - hitable::HitRecord, pdf::{SpherePDF, PDF}, ray::Ray, textures::{Texture, TextureTrait}, - Float, PI, + Float, HitRecord, PI, }; use rand::prelude::SmallRng; diff --git a/clovers/src/materials/lambertian.rs b/clovers/src/materials/lambertian.rs index 4b590b7f..b57b67e8 100644 --- a/clovers/src/materials/lambertian.rs +++ b/clovers/src/materials/lambertian.rs @@ -2,11 +2,10 @@ use super::{MaterialTrait, MaterialType, ScatterRecord}; use crate::{ - hitable::HitRecord, pdf::{CosinePDF, PDF}, ray::Ray, textures::{Texture, TextureTrait}, - Float, PI, + Float, HitRecord, PI, }; use rand::prelude::SmallRng; diff --git a/clovers/src/materials/metal.rs b/clovers/src/materials/metal.rs index e039d67e..dc231752 100644 --- a/clovers/src/materials/metal.rs +++ b/clovers/src/materials/metal.rs @@ -2,12 +2,11 @@ use super::{reflect, MaterialTrait, MaterialType, ScatterRecord}; use crate::{ - hitable::HitRecord, pdf::{ZeroPDF, PDF}, random::random_unit_vector, ray::Ray, textures::{Texture, TextureTrait}, - Direction, Float, + Direction, Float, HitRecord, }; use nalgebra::Unit; use rand::prelude::SmallRng; diff --git a/clovers/src/objects.rs b/clovers/src/objects.rs index 3ebe5f8b..c051c856 100644 --- a/clovers/src/objects.rs +++ b/clovers/src/objects.rs @@ -1,8 +1,7 @@ //! Various literal objects and meta-object utilities for creating content in [Scenes](crate::scenes::Scene). use crate::{ - bvhnode::BVHNode, - hitable::Hitable, + hitable::{Hitable, HitableList}, materials::{Material, MaterialInit, SharedMaterial}, Box, }; @@ -108,8 +107,7 @@ pub fn object_to_hitable(obj: Object, materials: &[SharedMaterial]) -> Hitable<' .iter() .map(|object| -> Hitable { object_to_hitable(object.clone(), materials) }) .collect(); - let bvh = BVHNode::from_list(objects, 0.0, 1.0); - Hitable::BVHNode(bvh) + Hitable::HitableList(HitableList::new(objects)) } Object::Quad(x) => { let material = initialize_material(x.material, materials); @@ -125,11 +123,14 @@ pub fn object_to_hitable(obj: Object, materials: &[SharedMaterial]) -> Hitable<' Hitable::Sphere(Sphere::new(x.center, x.radius, material)) } #[cfg(feature = "stl")] - Object::STL(stl_init) => Hitable::STL(initialize_stl(stl_init, materials)), + Object::STL(stl_init) => { + let stl = initialize_stl(stl_init, materials); + Hitable::HitableList(HitableList::new(stl.hitables)) + } #[cfg(feature = "gl_tf")] Object::GLTF(x) => { - // TODO: time - Hitable::GLTF(GLTF::new(x, 0.0, 1.0)) + let gltf = GLTF::new(x); + Hitable::HitableList(HitableList::new(gltf.hitables)) } Object::Translate(x) => { let obj = *x.object; diff --git a/clovers/src/objects/boxy.rs b/clovers/src/objects/boxy.rs index 7cb892b1..f0d047c4 100644 --- a/clovers/src/objects/boxy.rs +++ b/clovers/src/objects/boxy.rs @@ -3,15 +3,15 @@ use super::Quad; use crate::{ aabb::AABB, - hitable::{HitRecord, Hitable, HitableTrait}, + hitable::{Hitable, HitableTrait}, materials::{Material, MaterialInit}, ray::Ray, wavelength::Wavelength, - Box, Direction, Float, Position, Vec3, + Box, Direction, Float, HitRecord, Position, Vec3, }; use rand::rngs::SmallRng; -/// `BoxyInit` structure describes the necessary data for constructing a [Boxy]. Used with [serde] when importing [`SceneFile`](crate::scenes::SceneFile)s. +/// `BoxyInit` structure describes the necessary data for constructing a [Boxy]. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] pub struct BoxyInit { @@ -106,7 +106,7 @@ impl<'scene> HitableTrait for Boxy<'scene> { /// Returns the axis-aligned bounding box [AABB] of the object. #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { Some(&self.aabb) } @@ -129,4 +129,9 @@ impl<'scene> HitableTrait for Boxy<'scene> { sum } + + // TODO: correctness for rotations/translations? + fn centroid(&self) -> Position { + self.aabb.centroid() + } } diff --git a/clovers/src/objects/constant_medium.rs b/clovers/src/objects/constant_medium.rs index 33f8a345..d6051945 100644 --- a/clovers/src/objects/constant_medium.rs +++ b/clovers/src/objects/constant_medium.rs @@ -2,13 +2,13 @@ use crate::{ aabb::AABB, - hitable::{HitRecord, Hitable, HitableTrait}, + hitable::{Hitable, HitableTrait}, materials::{isotropic::Isotropic, Material}, random::random_unit_vector, ray::Ray, textures::Texture, wavelength::Wavelength, - Box, Direction, Float, Position, EPSILON_CONSTANT_MEDIUM, + Box, Direction, Float, HitRecord, Position, EPSILON_CONSTANT_MEDIUM, }; use rand::rngs::SmallRng; use rand::Rng; @@ -17,7 +17,7 @@ use super::Object; #[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -/// `ConstantMediumInit` structure describes the necessary data for constructing a [`ConstantMedium`]. Used with [serde] when importing [`SceneFiles`](crate::scenes::SceneFile). +/// `ConstantMediumInit` structure describes the necessary data for constructing a [`ConstantMedium`]. pub struct ConstantMediumInit { /// Used for multiple importance sampling #[cfg_attr(feature = "serde-derive", serde(default))] @@ -128,8 +128,8 @@ impl<'scene> HitableTrait for ConstantMedium<'scene> { /// Returns the axis-aligned bounding box [AABB] of the defining `boundary` object for the fog. #[must_use] - fn bounding_box(&self, t0: Float, t1: Float) -> Option<&AABB> { - self.boundary.bounding_box(t0, t1) + fn aabb(&self) -> Option<&AABB> { + self.boundary.aabb() } /// Returns a probability density function value based on the boundary object @@ -145,4 +145,8 @@ impl<'scene> HitableTrait for ConstantMedium<'scene> { self.boundary .pdf_value(origin, direction, wavelength, time, rng) } + + fn centroid(&self) -> Position { + self.boundary.centroid() + } } diff --git a/clovers/src/objects/gltf.rs b/clovers/src/objects/gltf.rs index c69e4fe0..69f65c35 100644 --- a/clovers/src/objects/gltf.rs +++ b/clovers/src/objects/gltf.rs @@ -11,13 +11,13 @@ use tracing::debug; use crate::{ aabb::AABB, - bvhnode::BVHNode, - hitable::{get_orientation, HitRecord, Hitable, HitableTrait}, + bvh::build::utils::vec_bounding_box, + hitable::{get_orientation, Hitable, HitableTrait}, interval::Interval, materials::gltf::GLTFMaterial, ray::Ray, wavelength::Wavelength, - Direction, Float, Position, Vec3, EPSILON_RECT_THICKNESS, EPSILON_SHADOW_ACNE, + Direction, Float, HitRecord, Position, Vec3, EPSILON_RECT_THICKNESS, EPSILON_SHADOW_ACNE, }; /// GLTF initialization structure @@ -58,8 +58,8 @@ impl<'scene> From for Vec> { /// Internal GLTF object representation after initialization. #[derive(Debug, Clone)] pub struct GLTF<'scene> { - /// Bounding Volume Hierarchy tree for the object - pub bvhnode: BVHNode<'scene>, + /// Hitables of the `GLTF` object. Most likely a list of `GLTFTriangle`s. + pub hitables: Vec>, /// Axis-aligned bounding box of the object pub aabb: AABB, } @@ -67,70 +67,34 @@ pub struct GLTF<'scene> { impl<'scene> GLTF<'scene> { #[must_use] /// Create a new STL object with the given initialization parameters. - pub fn new(gltf_init: GLTFInit, time_0: Float, time_1: Float) -> Self { - let triangles: Vec = gltf_init.into(); - let bvhnode = BVHNode::from_list(triangles, time_0, time_1); - // TODO: remove unwrap - let aabb = bvhnode.bounding_box(time_0, time_1).unwrap().clone(); + pub fn new(gltf_init: GLTFInit) -> Self { + let hitables: Vec = gltf_init.into(); + let aabb = vec_bounding_box(&hitables).unwrap(); - GLTF { bvhnode, aabb } - } -} - -impl<'scene> HitableTrait for GLTF<'scene> { - /// Hit method for the GLTF object - #[must_use] - fn hit( - &self, - ray: &Ray, - distance_min: f32, - distance_max: f32, - rng: &mut SmallRng, - ) -> Option { - self.bvhnode.hit(ray, distance_min, distance_max, rng) - } - - /// Return the axis-aligned bounding box for the object - #[must_use] - fn bounding_box(&self, _t0: f32, _t1: f32) -> Option<&AABB> { - Some(&self.aabb) - } - - /// Returns a probability density function value based on the object - #[must_use] - fn pdf_value( - &self, - origin: Position, - direction: Direction, - wavelength: Wavelength, - time: Float, - rng: &mut SmallRng, - ) -> Float { - self.bvhnode - .pdf_value(origin, direction, wavelength, time, rng) + GLTF { hitables, aabb } } } fn parse_node<'scene>( node: &Node, - objects: &mut Vec>, + hitables: &mut Vec>, buffers: &Vec, materials: &'scene Vec, images: &'scene Vec, ) { // Handle direct meshes if let Some(mesh) = node.mesh() { - parse_mesh(&mesh, objects, buffers, materials, images); + parse_mesh(&mesh, hitables, buffers, materials, images); } // Handle nesting for child in node.children() { - parse_node(&child, objects, buffers, materials, images); + parse_node(&child, hitables, buffers, materials, images); } } fn parse_mesh<'scene>( mesh: &Mesh, - objects: &mut Vec>, + hitables: &mut Vec>, buffers: &[gltf::buffer::Data], materials: &'scene [gltf::Material], images: &'scene [Data], @@ -211,8 +175,7 @@ fn parse_mesh<'scene>( } } - let bvh: BVHNode = BVHNode::from_list(trianglelist, 0.0, 1.0); - objects.push(Hitable::BVHNode(bvh)); + hitables.append(&mut trianglelist); } _ => unimplemented!(), } @@ -325,7 +288,7 @@ impl<'scene> HitableTrait for GLTFTriangle<'scene> { }) } - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { Some(&self.aabb) } @@ -355,6 +318,11 @@ impl<'scene> HitableTrait for GLTFTriangle<'scene> { None => 0.0, } } + + // TODO: correctness + fn centroid(&self) -> Position { + self.q + (self.u / 4.0) + (self.v / 4.0) + } } #[must_use] diff --git a/clovers/src/objects/moving_sphere.rs b/clovers/src/objects/moving_sphere.rs index 9203867d..0b446a38 100644 --- a/clovers/src/objects/moving_sphere.rs +++ b/clovers/src/objects/moving_sphere.rs @@ -2,18 +2,18 @@ use crate::{ aabb::AABB, - hitable::{HitRecord, HitableTrait}, + hitable::HitableTrait, materials::{Material, MaterialInit}, ray::Ray, wavelength::Wavelength, - Direction, Float, Position, PI, + Direction, Float, HitRecord, Position, PI, }; use nalgebra::Unit; use rand::rngs::SmallRng; #[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -/// `SphereInit` structure describes the necessary data for constructing a [`Sphere`](super::Sphere). Used with [serde] when importing [`SceneFile`](crate::scenes::SceneFile)s. +/// `SphereInit` structure describes the necessary data for constructing a [`Sphere`](super::Sphere). pub struct MovingSphereInit { /// Used for multiple importance sampling #[cfg_attr(feature = "serde-derive", serde(default))] @@ -68,7 +68,7 @@ impl<'scene> MovingSphere<'scene> { center_1 + Position::new(radius, radius, radius), ); - let aabb = AABB::surrounding_box(&box0, &box1); + let aabb = AABB::combine(&box0, &box1); MovingSphere { center_0, @@ -162,7 +162,7 @@ impl<'scene> HitableTrait for MovingSphere<'scene> { /// Returns the axis-aligned bounding box of the [`MovingSphere`] object. This is the maximum possible bounding box of the entire span of the movement of the sphere, calculated from the two center positions and the radius. #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { Some(&self.aabb) } @@ -177,4 +177,9 @@ impl<'scene> HitableTrait for MovingSphere<'scene> { // TODO: fix 0.0 } + + fn centroid(&self) -> Position { + // TODO: proper time support + self.center(0.5) + } } diff --git a/clovers/src/objects/quad.rs b/clovers/src/objects/quad.rs index 52cde50d..8e86183d 100644 --- a/clovers/src/objects/quad.rs +++ b/clovers/src/objects/quad.rs @@ -5,8 +5,8 @@ use crate::hitable::HitableTrait; use crate::materials::MaterialInit; use crate::wavelength::Wavelength; use crate::{ - aabb::AABB, hitable::get_orientation, hitable::HitRecord, materials::Material, ray::Ray, Float, - Vec3, EPSILON_RECT_THICKNESS, + aabb::AABB, hitable::get_orientation, materials::Material, ray::Ray, Float, HitRecord, Vec3, + EPSILON_RECT_THICKNESS, }; use crate::{Direction, Displacement, Position, EPSILON_SHADOW_ACNE}; use nalgebra::Unit; @@ -133,7 +133,7 @@ impl<'scene> HitableTrait for Quad<'scene> { /// Returns the bounding box of the quad #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { Some(&self.aabb) } @@ -173,6 +173,11 @@ impl<'scene> HitableTrait for Quad<'scene> { + (rng.gen::() * self.v); point - origin } + + // TODO: correctness + fn centroid(&self) -> Position { + self.q + (self.u / 2.0) + (self.v / 2.0) + } } #[must_use] diff --git a/clovers/src/objects/rotate.rs b/clovers/src/objects/rotate.rs index 9edb75c0..1c0aa662 100644 --- a/clovers/src/objects/rotate.rs +++ b/clovers/src/objects/rotate.rs @@ -2,10 +2,10 @@ use crate::{ aabb::AABB, - hitable::{HitRecord, Hitable, HitableTrait}, + hitable::{Hitable, HitableTrait}, ray::Ray, wavelength::Wavelength, - Box, Direction, Float, Position, Vec3, + Box, Direction, Float, HitRecord, Position, Vec3, }; use nalgebra::Unit; use rand::rngs::SmallRng; @@ -14,7 +14,7 @@ use super::Object; #[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -/// `RotateInit` structure describes the necessary data for constructing a [`RotateY`]. Used with [serde] when importing [`SceneFile`](crate::scenes::SceneFile)s. +/// `RotateInit` structure describes the necessary data for constructing a [`RotateY`]. pub struct RotateInit { /// Used for multiple importance sampling #[cfg_attr(feature = "serde-derive", serde(default))] @@ -39,12 +39,10 @@ impl<'scene> RotateY<'scene> { #[must_use] pub fn new(object: Box>, angle: Float) -> Self { // TODO: add proper time support - let time_0: Float = 0.0; - let time_1: Float = 1.0; let radians: Float = angle.to_radians(); let sin_theta: Float = radians.sin(); let cos_theta: Float = radians.cos(); - let bounding_box: Option<&AABB> = object.bounding_box(time_0, time_1); + let bounding_box: Option<&AABB> = object.aabb(); // Does our object have a bounding box? let Some(bbox) = bounding_box else { @@ -163,7 +161,7 @@ impl<'scene> HitableTrait for RotateY<'scene> { /// Bounding box method for the [`RotateY`] object. Finds the axis-aligned bounding box [AABB] for the encased [Object] after adjusting for rotation. #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { self.aabb.as_ref() } @@ -178,4 +176,9 @@ impl<'scene> HitableTrait for RotateY<'scene> { // TODO: fix 0.0 } + + // TODO: correctness! + fn centroid(&self) -> Position { + self.object.centroid() + } } diff --git a/clovers/src/objects/sphere.rs b/clovers/src/objects/sphere.rs index 3d6e00ba..c6ed3e5a 100644 --- a/clovers/src/objects/sphere.rs +++ b/clovers/src/objects/sphere.rs @@ -2,19 +2,19 @@ use crate::{ aabb::AABB, - hitable::{HitRecord, HitableTrait}, + hitable::HitableTrait, materials::{Material, MaterialInit}, onb::ONB, ray::Ray, wavelength::Wavelength, - Direction, Displacement, Float, Position, Vec3, EPSILON_SHADOW_ACNE, PI, + Direction, Displacement, Float, HitRecord, Position, Vec3, EPSILON_SHADOW_ACNE, PI, }; use nalgebra::Unit; use rand::{rngs::SmallRng, Rng}; #[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -/// `SphereInit` structure describes the necessary data for constructing a [Sphere]. Used with [serde] when importing [`SceneFile`](crate::scenes::SceneFile)s. +/// `SphereInit` structure describes the necessary data for constructing a [Sphere]. pub struct SphereInit { /// Used for multiple importance sampling #[cfg_attr(feature = "serde-derive", serde(default))] @@ -127,7 +127,7 @@ impl<'scene> HitableTrait for Sphere<'scene> { /// Returns the axis-aligned bounding box [AABB] for the sphere. #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { Some(&self.aabb) } @@ -171,6 +171,10 @@ impl<'scene> HitableTrait for Sphere<'scene> { let vec = Unit::new_normalize(vec); *uvw.local(vec) } + + fn centroid(&self) -> Position { + self.center + } } /// Internal helper. diff --git a/clovers/src/objects/stl.rs b/clovers/src/objects/stl.rs index 55eded89..6e02c8fe 100644 --- a/clovers/src/objects/stl.rs +++ b/clovers/src/objects/stl.rs @@ -2,72 +2,28 @@ use alloc::string::String; use nalgebra::Rotation3; -use rand::prelude::SmallRng; use std::fs::OpenOptions; use crate::{ aabb::AABB, - bvhnode::BVHNode, - hitable::{HitRecord, Hitable, HitableTrait}, + bvh::build::utils::vec_bounding_box, + hitable::Hitable, materials::{Material, MaterialInit, SharedMaterial}, objects::Triangle, - ray::Ray, - wavelength::Wavelength, - Direction, Displacement, Float, Position, Vec3, + Float, Position, Vec3, }; /// Internal STL object representation after initialization. Contains the material for all triangles in it to avoid having n copies. #[derive(Debug, Clone)] pub struct STL<'scene> { - /// Bounding Volume Hierarchy tree for the object - pub bvhnode: BVHNode<'scene>, + /// Primitives of the `STL` object. Most likely a list of `Triangle`s. + pub hitables: Vec>, /// Material for the object pub material: &'scene Material, /// Axis-aligned bounding box of the object pub aabb: AABB, } -impl<'scene> HitableTrait for STL<'scene> { - /// Hit method for the STL object - #[must_use] - fn hit( - &self, - ray: &Ray, - distance_min: f32, - distance_max: f32, - rng: &mut SmallRng, - ) -> Option { - self.bvhnode.hit(ray, distance_min, distance_max, rng) - } - - /// Return the axis-aligned bounding box for the object - #[must_use] - fn bounding_box(&self, _t0: f32, _t1: f32) -> Option<&AABB> { - Some(&self.aabb) - } - - /// Returns a probability density function value based on the object - #[must_use] - fn pdf_value( - &self, - origin: Position, - direction: Direction, - wavelength: Wavelength, - time: Float, - rng: &mut SmallRng, - ) -> Float { - self.bvhnode - .pdf_value(origin, direction, wavelength, time, rng) - } - - // TODO: improve correctness & optimization! - /// Returns a random point on the surface of the object - #[must_use] - fn random(&self, origin: Position, rng: &mut SmallRng) -> Displacement { - self.bvhnode.random(origin, rng) - } -} - /// STL structure. This gets converted into an internal representation using [Triangles](crate::objects::Triangle) #[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] @@ -101,7 +57,7 @@ pub fn initialize_stl<'scene>( .unwrap(); let mesh = stl_io::read_stl(&mut file).unwrap(); let triangles = mesh.vertices; - let mut hitable_list = Vec::new(); + let mut hitables = Vec::new(); let material: &Material = match stl_init.material { MaterialInit::Shared(name) => &materials.iter().find(|m| m.name == name).unwrap().material, MaterialInit::Owned(m) => { @@ -135,19 +91,13 @@ pub fn initialize_stl<'scene>( let c: Vec3 = c * stl_init.scale + stl_init.center; let triangle = Triangle::from_coordinates(a, b, c, material); - hitable_list.push(Hitable::Triangle(triangle)); + hitables.push(Hitable::Triangle(triangle)); } - - // TODO: time - let time_0 = 0.0; - let time_1 = 1.0; - - let bvhnode = BVHNode::from_list(hitable_list, time_0, time_1); // TODO: remove unwrap - let aabb = bvhnode.bounding_box(time_0, time_1).unwrap().clone(); + let aabb = vec_bounding_box(&hitables).unwrap(); STL { - bvhnode, + hitables, material, aabb, } diff --git a/clovers/src/objects/translate.rs b/clovers/src/objects/translate.rs index 31bb4823..97da4fff 100644 --- a/clovers/src/objects/translate.rs +++ b/clovers/src/objects/translate.rs @@ -2,10 +2,10 @@ use crate::{ aabb::AABB, - hitable::{HitRecord, Hitable, HitableTrait}, + hitable::{Hitable, HitableTrait}, ray::Ray, wavelength::Wavelength, - Box, Direction, Float, Position, Vec3, + Box, Direction, Float, HitRecord, Position, Vec3, }; use rand::rngs::SmallRng; @@ -13,7 +13,7 @@ use super::Object; #[derive(Clone, Debug)] #[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -/// `TranslateInit` structure describes the necessary data for constructing a [Translate] object. Used with [serde] when importing [`SceneFile`](crate::scenes::SceneFile)s. +/// `TranslateInit` structure describes the necessary data for constructing a [Translate] object. pub struct TranslateInit { /// Used for multiple importance sampling #[cfg_attr(feature = "serde-derive", serde(default))] @@ -37,7 +37,7 @@ impl<'scene> Translate<'scene> { #[must_use] pub fn new(object: Box>, offset: Vec3) -> Self { // TODO: time - let aabb = object.bounding_box(0.0, 1.0).unwrap().clone() + offset; + let aabb = object.aabb().unwrap().clone() + offset; Translate { object, offset, @@ -77,7 +77,7 @@ impl<'scene> HitableTrait for Translate<'scene> { /// Bounding box method for the [Translate] object. Finds the axis-aligned bounding box [AABB] for the encased [Object] after adjusting for translation. #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { Some(&self.aabb) } @@ -95,4 +95,9 @@ impl<'scene> HitableTrait for Translate<'scene> { self.object .pdf_value(origin + self.offset, direction, wavelength, time, rng) } + + // TODO: correctness + fn centroid(&self) -> Position { + self.object.centroid() + self.offset + } } diff --git a/clovers/src/objects/triangle.rs b/clovers/src/objects/triangle.rs index 77962f06..34ec14be 100644 --- a/clovers/src/objects/triangle.rs +++ b/clovers/src/objects/triangle.rs @@ -6,8 +6,7 @@ use crate::interval::Interval; use crate::materials::MaterialInit; use crate::wavelength::Wavelength; use crate::{ - aabb::AABB, hitable::HitRecord, materials::Material, ray::Ray, Float, Vec3, - EPSILON_RECT_THICKNESS, + aabb::AABB, materials::Material, ray::Ray, Float, HitRecord, Vec3, EPSILON_RECT_THICKNESS, }; use crate::{Direction, Displacement, Position, EPSILON_SHADOW_ACNE}; use nalgebra::Unit; @@ -167,7 +166,7 @@ impl<'scene> HitableTrait for Triangle<'scene> { /// Returns the bounding box of the triangle #[must_use] - fn bounding_box(&self, _t0: Float, _t1: Float) -> Option<&AABB> { + fn aabb(&self) -> Option<&AABB> { // TODO: this is from quad and not updated! // although i guess a triangle's aabb is the same as the quad's aabb in worst case Some(&self.aabb) @@ -221,6 +220,11 @@ impl<'scene> HitableTrait for Triangle<'scene> { point - origin } + + // TODO: correctness + fn centroid(&self) -> Position { + self.q + (self.u / 4.0) + (self.v / 4.0) + } } #[must_use] @@ -231,6 +235,7 @@ fn hit_ab(a: Float, b: Float) -> bool { (0.0..=1.0).contains(&a) && (0.0..=1.0).contains(&b) && (a + b <= 1.0) } +// TODO: proptest! #[cfg(test)] mod tests { use alloc::boxed::Box; @@ -242,8 +247,15 @@ mod tests { const TIME_0: Float = 0.0; const TIME_1: Float = 1.0; - const RAY: Ray = Ray { - origin: Position::new(0.0, 0.0, -1.0), + const RAY_POS: Ray = Ray { + origin: Position::new(0.01, 0.01, -1.0), + direction: Unit::new_unchecked(Vec3::new(0.0, 0.0, 1.0)), + time: TIME_0, + wavelength: 600, + }; + + const RAY_NEG: Ray = Ray { + origin: Position::new(-0.01, -0.01, -1.0), direction: Unit::new_unchecked(Vec3::new(0.0, 0.0, 1.0)), time: TIME_0, wavelength: 600, @@ -262,9 +274,7 @@ mod tests { &material, ); - let aabb = triangle - .bounding_box(TIME_0, TIME_1) - .expect("No AABB for the triangle"); + let aabb = triangle.aabb().expect("No AABB for the triangle"); let expected_aabb = AABB::new( Interval::new(0.0, 1.0), @@ -274,15 +284,15 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); + let boxhit = aabb.hit(&RAY_POS, TIME_0, TIME_1); assert!(boxhit); let hit_record = triangle - .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + .hit(&RAY_POS, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); - assert_eq!(hit_record.position, Vec3::new(0.0, 0.0, 0.0)); + assert_eq!(hit_record.position, Vec3::new(0.01, 0.01, 0.0)); assert_eq!( hit_record.normal, Unit::new_normalize(Vec3::new(0.0, 0.0, -1.0)) @@ -303,9 +313,7 @@ mod tests { &material, ); - let aabb = triangle - .bounding_box(TIME_0, TIME_1) - .expect("No AABB for the triangle"); + let aabb = triangle.aabb().expect("No AABB for the triangle"); let expected_aabb = AABB::new( Interval::new(0.0, 1.0), @@ -315,15 +323,15 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); + let boxhit = aabb.hit(&RAY_POS, TIME_0, TIME_1); assert!(boxhit); let hit_record = triangle - .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + .hit(&RAY_POS, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); - assert_eq!(hit_record.position, Position::new(0.0, 0.0, 0.0)); + assert_eq!(hit_record.position, Position::new(0.01, 0.01, 0.0)); assert_eq!( hit_record.normal, Unit::new_normalize(Vec3::new(0.0, 0.0, -1.0)) @@ -344,9 +352,7 @@ mod tests { &material, ); - let aabb = triangle - .bounding_box(TIME_0, TIME_1) - .expect("No AABB for the triangle"); + let aabb = triangle.aabb().expect("No AABB for the triangle"); let expected_aabb = AABB::new( Interval::new(-1.0, 0.0), @@ -356,15 +362,15 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); + let boxhit = aabb.hit(&RAY_NEG, TIME_0, TIME_1); assert!(boxhit); let hit_record = triangle - .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + .hit(&RAY_NEG, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); - assert_eq!(hit_record.position, Position::new(0.0, 0.0, 0.0)); + assert_eq!(hit_record.position, Position::new(-0.01, -0.01, 0.0)); assert_eq!( hit_record.normal, Unit::new_normalize(Vec3::new(0.0, 0.0, -1.0)) @@ -385,9 +391,7 @@ mod tests { &material, ); - let aabb = triangle - .bounding_box(TIME_0, TIME_1) - .expect("No AABB for the triangle"); + let aabb = triangle.aabb().expect("No AABB for the triangle"); let expected_aabb = AABB::new( Interval::new(-1.0, 0.0), @@ -397,15 +401,15 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); + let boxhit = aabb.hit(&RAY_NEG, TIME_0, TIME_1); assert!(boxhit); let hit_record = triangle - .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + .hit(&RAY_NEG, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); - assert_eq!(hit_record.position, Position::new(0.0, 0.0, 0.0)); + assert_eq!(hit_record.position, Position::new(-0.01, -0.01, 0.0)); assert_eq!( hit_record.normal, Unit::new_normalize(Vec3::new(0.0, 0.0, -1.0)) diff --git a/clovers/src/scenes.rs b/clovers/src/scenes.rs index ecd08cff..20534508 100644 --- a/clovers/src/scenes.rs +++ b/clovers/src/scenes.rs @@ -1,131 +1,18 @@ //! A collection of objects, camera, and other things necessary to describe the environment you wish to render. -use alloc::boxed::Box; - -use crate::{ - bvhnode::BVHNode, - camera::{Camera, CameraInit}, - hitable::Hitable, - materials::SharedMaterial, - objects::{object_to_hitable, Object}, - Float, Vec, -}; +use crate::{bvh::BVHNode, camera::Camera, hitable::Hitable}; use palette::Srgb; -#[cfg(feature = "traces")] -use tracing::info; #[derive(Debug)] /// A representation of the scene that is being rendered. pub struct Scene<'scene> { /// Bounding-volume hierarchy of [Hitable] objects in the scene. This could, as currently written, be any [Hitable] - in practice, we place the root of the [`BVHNode`] tree here. - pub hitables: BVHNode<'scene>, + pub bvh_root: BVHNode<'scene>, /// The camera object used for rendering the scene. pub camera: Camera, /// The background color to use when the rays do not hit anything in the scene. pub background_color: Srgb, // TODO: make into Texture or something? - /// A [`BVHNode`] tree of prioritized objects - e.g. glass items or lights - that affect the biased sampling of the scene. Wrapped into a [Hitable] for convenience reasons (see various PDF functions). - pub priority_hitables: Hitable<'scene>, -} - -impl<'scene> Scene<'scene> { - /// Creates a new [Scene] with the given parameters. - #[must_use] - pub fn new( - time_0: Float, - time_1: Float, - camera: Camera, - hitables: Vec>, - priority_hitables: Vec>, - background_color: Srgb, - ) -> Scene<'scene> { - Scene { - hitables: BVHNode::from_list(hitables, time_0, time_1), - camera, - background_color, - priority_hitables: Hitable::BVHNode(BVHNode::from_list( - priority_hitables, - time_0, - time_1, - )), - } - } -} - -// TODO: better naming -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] -/// A serialized representation of a [Scene]. -pub struct SceneFile { - time_0: Float, - time_1: Float, - background_color: Srgb, - camera: CameraInit, - objects: Vec, - #[cfg_attr(feature = "serde-derive", serde(default))] - materials: Vec, -} - -/// Initializes a new [Scene] instance by parsing the contents of a [`SceneFile`] structure and then using those details to construct the [Scene]. -#[must_use] -pub fn initialize<'scene>(scene_file: SceneFile, width: u32, height: u32) -> Scene<'scene> { - let time_0 = scene_file.time_0; - let time_1 = scene_file.time_1; - let background_color = scene_file.background_color; - - #[allow(clippy::cast_precision_loss)] - let camera = Camera::new( - scene_file.camera.look_from, - scene_file.camera.look_at, - scene_file.camera.up, - scene_file.camera.vertical_fov, - width as Float / height as Float, - scene_file.camera.aperture, - scene_file.camera.focus_distance, - time_0, - time_1, - ); - let mut materials = scene_file.materials; - materials.push(SharedMaterial::default()); - let materials = Box::leak(Box::new(materials)); - - #[cfg(feature = "traces")] - info!("Creating a flattened list from the objects"); - let mut hitables: Vec = Vec::new(); - let mut priority_hitables: Vec = Vec::new(); - - // TODO: this isn't the greatest ergonomics, but it gets the job done for now - for object in scene_file.objects { - if match &object { - Object::Boxy(i) => i.priority, - Object::ConstantMedium(i) => i.priority, - Object::MovingSphere(i) => i.priority, - Object::ObjectList(i) => i.priority, - Object::Quad(i) => i.priority, - Object::RotateY(i) => i.priority, - Object::Sphere(i) => i.priority, - #[cfg(feature = "stl")] - Object::STL(i) => i.priority, - #[cfg(feature = "gl_tf")] - Object::GLTF(i) => i.priority, - Object::Translate(i) => i.priority, - Object::Triangle(i) => i.priority, - } { - let hitable = object_to_hitable(object, materials); - hitables.push(hitable.clone()); - priority_hitables.push(hitable); - } else { - let hitable = object_to_hitable(object, materials); - hitables.push(hitable.clone()); - } - } - - Scene::new( - time_0, - time_1, - camera, - hitables, - priority_hitables, - background_color, - ) + /// A [`BVHNode`] tree of priority objects - e.g. glass items or lights - for multiple importance sampling. Wrapped into a [Hitable] for convenience reasons (see various PDF functions). + pub mis_bvh_root: Hitable<'scene>, } diff --git a/scenes/cornell_with_smoke.json b/scenes/cornell_with_smoke.json deleted file mode 100644 index 4b3b56f7..00000000 --- a/scenes/cornell_with_smoke.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "time_0": 0, - "time_1": 1, - "camera": { - "look_from": [278, 278, -800], - "look_at": [278, 278, 0], - "up": [0, 1, 0], - "vertical_fov": 40, - "aperture": 0, - "focus_distance": 10 - }, - "background_color": [0, 0, 0], - "objects": [ - { - "kind": "Quad", - "q": [555, 0, 0], - "u": [0, 0, 555], - "v": [0, 555, 0], - "material": "green wall", - "comment": "green wall, left" - }, - { - "kind": "Quad", - "q": [0, 0, 555], - "u": [0, 0, -555], - "v": [0, 555, 0], - "material": "red wall", - "comment": "red wall, right" - }, - { - "kind": "Quad", - "q": [0, 0, 0], - "u": [555, 0, 0], - "v": [0, 0, 555], - "material": "grey wall", - "comment": "floor" - }, - { - "kind": "Quad", - "q": [0, 555, 0], - "u": [555, 0, 0], - "v": [0, 0, 555], - "material": "grey wall", - "comment": "ceiling" - }, - { - "kind": "Quad", - "q": [0, 0, 555], - "u": [555, 0, 0], - "v": [0, 555, 0], - "material": "grey wall", - "comment": "back wall" - }, - { - "kind": "Quad", - "priority": true, - "q": [113, 554, 127], - "u": [330, 0, 0], - "v": [0, 0, 305], - "material": "lamp", - "comment": "big ceiling light" - }, - { - "kind": "ConstantMedium", - "boundary": { - "kind": "Translate", - "offset": [265, 0, 295], - "object": { - "kind": "RotateY", - "angle": 15, - "object": { - "kind": "Boxy", - "corner_0": [0, 0, 0], - "corner_1": [165, 330, 165] - } - } - }, - "density": 0.01, - "texture": { - "kind": "SolidColor", - "color": [0, 0, 0] - } - }, - { - "kind": "ConstantMedium", - "boundary": { - "kind": "Translate", - "offset": [130, 0, 65], - "object": { - "kind": "RotateY", - "angle": -18, - "object": { - "kind": "Boxy", - "corner_0": [0, 0, 0], - "corner_1": [165, 165, 165] - } - } - }, - "density": 0.01, - "texture": { - "kind": "SolidColor", - "color": [1, 1, 1] - } - } - ], - "materials": [ - { - "name": "lamp", - "kind": "DiffuseLight", - "emit": { - "kind": "SolidColor", - "color": [7, 7, 7] - } - }, - { - "name": "green wall", - "kind": "Lambertian", - "albedo": { - "kind": "SolidColor", - "color": [0.12, 0.45, 0.15] - } - }, - { - "name": "red wall", - "kind": "Lambertian", - "albedo": { - "kind": "SolidColor", - "color": [0.65, 0.05, 0.05] - } - }, - { - "name": "grey wall", - "kind": "Lambertian", - "albedo": { - "kind": "SolidColor", - "color": [0.73, 0.73, 0.73] - } - } - ] -} diff --git a/scenes/dragon.json b/scenes/dragon.json new file mode 100644 index 00000000..a22fbd55 --- /dev/null +++ b/scenes/dragon.json @@ -0,0 +1,78 @@ +{ + "time_0": 0, + "time_1": 1, + "background_color": [0, 0, 0], + "camera": { + "look_from": [0, 250, -800], + "look_at": [0, 250, 0], + "up": [0, 1, 0], + "vertical_fov": 40, + "aperture": 10, + "focus_distance": 800 + }, + "objects": [ + { + "kind": "Quad", + "comment": "back wall", + "q": [-2000, 0, 800], + "u": [4000, 0, 0], + "v": [0, 1000, 0], + "material": { + "kind": "Lambertian", + "albedo": { "kind": "SolidColor", "color": [1, 1, 1] } + } + }, + { + "kind": "STL", + "comment": "dragon", + "path": "stl/dragon.stl", + "scale": 12, + "center": [0, 240, 250], + "rotation": [-90, -40, 0], + "material": { + "name": "Dense flint glass SF10", + "kind": "Dispersive", + "cauchy_a": 1.728, + "cauchy_b": 0.01342 + } + }, + { + "kind": "Quad", + "q": [-2000, 0, -200], + "u": [4000, 0, 0], + "v": [0, 0, 1000], + "comment": "floor", + "priority": false, + "material": { + "kind": "Lambertian", + "albedo": { "kind": "SolidColor", "color": [1, 1, 1] } + } + }, + { + "kind": "Quad", + "q": [-200, 800, 100], + "u": [400, 0, 0], + "v": [0, 0, 400], + "material": { + "kind": "DiffuseLight", + "emit": { "kind": "SolidColor", "color": [7, 7, 7] } + }, + "comment": "big ceiling light", + "priority": true + } + ], + "priority_objects": [ + { + "kind": "Quad", + "q": [-200, 800, 100], + "u": [400, 0, 0], + "v": [0, 0, 400], + "material": { + "kind": "DiffuseLight", + "emit": { "kind": "SolidColor", "color": [7, 7, 7] } + }, + "comment": "big ceiling light", + "priority": true + } + ] +} diff --git a/scenes/grey.json b/scenes/grey.json deleted file mode 100644 index 512d950c..00000000 --- a/scenes/grey.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "time_0": 0, - "time_1": 1, - "camera": { - "look_from": [30, 10, 0], - "look_at": [0, 0, 0], - "up": [-0.25, 1, -0.25], - "vertical_fov": 40, - "aperture": 0, - "focus_distance": 10 - }, - "background_color": [0, 0, 0], - "objects": [ - { - "kind": "Quad", - "priority": true, - "q": [-100, 80, -100], - "u": [200, 0, 0], - "v": [0, 0, 100], - "material": "lamp" - }, - { - "kind": "Sphere", - "center": [0, 0, 0], - "radius": 8, - "material": "grey" - } - ], - "materials": [ - { - "name": "lamp", - "kind": "DiffuseLight", - "emit": { - "kind": "SolidColor", - "color": [2.5, 2.5, 2.5] - } - }, - { - "name": "grey", - "kind": "Lambertian" - } - ] -}