diff --git a/shared/src/core/geometry/primitives.rs b/shared/src/core/geometry/primitives.rs index 0b95fb5..88136e6 100644 --- a/shared/src/core/geometry/primitives.rs +++ b/shared/src/core/geometry/primitives.rs @@ -8,6 +8,7 @@ use core::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, }; use num_traits::{AsPrimitive, FloatConst, Num, Signed, Zero}; +use core::fmt; pub trait MulAdd { type Output; @@ -35,6 +36,45 @@ pub struct Point(pub [T; N]); #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Normal(pub [T; N]); +impl fmt::Display for Vector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Vector(")?; + for (i, item) in (&self.0).into_iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", item)?; + } + write!(f, ")") + } +} + +impl fmt::Display for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Point(")?; + for (i, item) in (&self.0).into_iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", item)?; + } + write!(f, ")") + } +} + +impl fmt::Display for Normal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Normal(")?; + for (i, item) in (&self.0).into_iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", item)?; + } + write!(f, ")") + } +} + #[macro_export] macro_rules! impl_tuple_core { ($Struct:ident) => { diff --git a/shared/src/core/material.rs b/shared/src/core/material.rs index 99cec02..dbbffe8 100644 --- a/shared/src/core/material.rs +++ b/shared/src/core/material.rs @@ -194,3 +194,10 @@ pub enum Material { Mix(MixMaterial), } + +// TODO: THIS IS A HACK JUST FOR TESTING +impl PartialEq for Material { + fn eq(&self, other: &Self) -> bool { + core::mem::discriminant(self) == core::mem::discriminant(other) + } +} diff --git a/src/core/render.rs b/src/core/render.rs index 137916a..05f9f19 100644 --- a/src/core/render.rs +++ b/src/core/render.rs @@ -1,13 +1,22 @@ use crate::core::scene::BasicScene; +use crate::globals::get_options; +use crate::integrators::pipeline::render; use crate::Arena; -use anyhow::Result; +use anyhow::{bail, Result}; use log::warn; use shared::core::camera::CameraTrait; +use shared::core::geometry::{Point2f, Vector2f}; +use shared::core::interaction::InteractionTrait; +use shared::core::primitive::PrimitiveTrait; +use shared::core::sampler::CameraSample; +use shared::spectra::{SampledWavelengths, LAMBDA_MAX, LAMBDA_MIN}; +use shared::Float; +use std::sync::Arc; fn render_scene(scene: &BasicScene, arena: &Arena) -> Result<()> { let media = scene.create_media(); let textures = scene.create_textures(arena); - let lights = scene.create_lights() + let (lights, _) = scene.create_lights(&textures, arena); let (named_materials, materials) = scene.create_materials(&textures, arena)?; let (aggregate, area_lights) = scene.create_aggregate(&textures, &named_materials, &materials, arena); @@ -15,6 +24,90 @@ fn render_scene(scene: &BasicScene, arena: &Arena) -> Result<()> { let film = camera.get_film(); warn!("Creating integrator"); let sampler = scene.get_sampler()?; - let integrator = scene.create_integrator(camera, sampler, aggregate, lights, arena); + let integrator = scene.create_integrator(camera.clone(), sampler.clone(), aggregate.clone(), lights, arena); + let mut have_scattering = false; + for sh in scene.shapes.lock().iter() { + if !sh.inside_medium.is_empty() || !sh.outside_medium.is_empty() { + have_scattering = true; + } + } + for sh in scene.animated_shapes.lock().iter() { + if !sh.inside_medium.is_empty() || !sh.outside_medium.is_empty() { + have_scattering = true; + } + } + + if get_options().pixel_material.is_some() { + let lambda = + SampledWavelengths::sample_uniform(0.5, LAMBDA_MIN as Float, LAMBDA_MAX as Float); + let cs = CameraSample { + p_film: Point2f::from(get_options().pixel_material.unwrap()) + Vector2f::new(0.5, 0.5), + time: 0.5, + p_lens: Point2f::new(0.5, 0.5), + filter_weight: 1., + }; + + let Some(cr) = camera.generate_ray_differential(cs, &lambda) else { + bail!("Unable to generate ray for pixel") + }; + + let mut depth = 1; + let mut ray = cr.ray; + loop { + if let Some(isect) = aggregate.intersect(&ray, Some(Float::INFINITY)) { + let intr = isect.intr; + if intr.material.is_null() { + log::warn!("Ignoring material") + } else { + let world_from_render = camera.base().camera_transform.world_from_render; + log::debug!("Intersection depth {}\n", depth); + log::debug!( + "World-space p: {}\n", + world_from_render.apply_to_point(intr.p()) + ); + log::debug!( + "World-space n: {}\n", + world_from_render.apply_to_normal(intr.n()) + ); + log::debug!( + "World-space ns: {}\n", + world_from_render.apply_to_normal(intr.shading.n) + ); + log::debug!("Distance from camera: {}\n", intr.p().distance(cr.ray.o)); + + let mut is_named = false; + for (name, mtl) in &named_materials { + if *mtl == unsafe { *intr.material.as_ref() } { + log::debug!("Named material: {}\n\n", name); + is_named = true; + break; + } + } + + // if !is_named { + // log::warn!("{}\n\n", intr.material.as_ref().to_str()); + // } + // + + depth += 1; + ray = intr.spawn_ray(ray.d); + } + } else { + if depth == 1 { + bail!("No geometry visible from pixel") + } else { + break; + } + } + } + } + + render( + &integrator, + &integrator.base, + &camera, + sampler.as_ref(), + arena, + ); Ok(()) } diff --git a/src/core/scene/builder.rs b/src/core/scene/builder.rs index e55b5fe..6871b5a 100644 --- a/src/core/scene/builder.rs +++ b/src/core/scene/builder.rs @@ -2,7 +2,6 @@ use super::entities::*; use super::BasicScene; use crate::spectra::get_colorspace_device; use crate::utils::error::FileLoc; -use crate::utils::normalize_utf8; use crate::utils::parameters::{ParameterDictionary, ParsedParameterVector}; use crate::utils::parser::{ParserError, ParserTarget}; use crate::Arena; @@ -17,9 +16,14 @@ use shared::Float; use std::collections::{HashMap, HashSet}; use std::ops::{Index, IndexMut}; use std::sync::Arc; +use unicode_normalization::UnicodeNormalization; const MAX_TRANSFORMS: usize = 2; +fn normalize_utf8(input: &str) -> String { + input.nfc().collect::() +} + #[derive(Debug, Default, Clone, Copy)] struct TransformSet { t: [Transform; MAX_TRANSFORMS], diff --git a/src/core/scene/scene.rs b/src/core/scene/scene.rs index e6b3520..3f25ecb 100644 --- a/src/core/scene/scene.rs +++ b/src/core/scene/scene.rs @@ -31,6 +31,7 @@ use shared::core::shape::Shape; use shared::core::texture::{GPUFloatTexture, SpectrumType}; use shared::lights::sampler::LightSampler; use shared::spectra::RGBColorSpace; +use shared::textures::FloatConstantTexture; use shared::{Ptr, Transform}; use std::collections::HashMap; use std::sync::Arc; @@ -508,52 +509,165 @@ impl BasicScene { Ok((named_materials, materials)) } - pub fn create_lights(&self, camera_transform: &CameraTransform, arena: &Arena) -> Vec { - let state = self.light_state.lock(); + pub fn create_lights( + &self, + textures: &NamedTextures, + arena: &Arena, + ) -> (Vec>, HashMap>>) { + let shape_entities = self.shapes.lock(); + let light_state = self.light_state.lock(); + let material_state = self.material_state.lock(); - state - .lights - .iter() - .filter_map(|entity| { - let render_from_light = entity.transformed_base.render_from_object.start_transform; + let mut all_lights: Vec> = Vec::new(); + let mut shape_index_to_area_lights: HashMap>> = HashMap::new(); - let medium = self - .get_medium(&entity.medium, &entity.transformed_base.base.loc) - .map(|m| *m); + for (i, entity) in shape_entities.iter().enumerate() { + let light_idx = match entity.light_index { + Some(idx) => idx, + None => continue, + }; - match crate::core::light::create_light( - &entity.transformed_base.base.name, - render_from_light, - medium, - &entity.transformed_base.base.parameters, - &entity.transformed_base.base.loc, - camera_transform.clone(), - arena, - ) { - Ok(light) => Some(light), - Err(e) => { - log::error!( - "{}: failed to create light: {}", - entity.transformed_base.base.loc, - e - ); - None + let material_name = match &entity.material { + MaterialRef::Name(name) => { + match material_state + .named_materials + .iter() + .find(|(n, _)| n == name) + { + Some((_, mtl_entity)) => { + match mtl_entity.parameters.get_one_string("type", "") { + Ok(t) if !t.is_empty() => t, + _ => { + log::error!( + "{}: named material '{}' missing type", + entity.base.loc, + name + ); + continue; + } + } + } + None => { + log::error!( + "{}: no named material '{}' defined.", + entity.base.loc, + name + ); + continue; + } } } - }) - .collect() + MaterialRef::Index(idx) => match material_state.materials.get(*idx) { + Some(mtl_entity) => mtl_entity.name.clone(), + None => { + log::error!("{}: material index {} out of bounds", entity.base.loc, idx); + continue; + } + }, + MaterialRef::None => String::new(), + }; + + if material_name == "interface" || material_name == "none" || material_name.is_empty() { + log::warn!( + "{}: Ignoring area light specification for shape with \"interface\" material.", + entity.base.loc + ); + continue; + } + + let shape_objects = match Shape::create( + &entity.base.name, + *entity.render_from_object, + *entity.object_from_render, + entity.reverse_orientation, + entity.base.parameters.clone(), + &textures.float_textures, + entity.base.loc.clone(), + arena, + ) { + Ok(shapes) => shapes, + Err(e) => { + log::error!("{}: failed to create shape: {}", entity.base.loc, e); + continue; + } + }; + + if shape_objects.is_empty() { + continue; + } + + let alpha_tex = Self::get_alpha_texture( + &entity.base.parameters, + &entity.base.loc, + &textures.float_textures, + ); + let default_alpha = Arc::new(FloatTexture::default()); + let alpha_ref = alpha_tex.as_ref().unwrap_or(&default_alpha); + + let inside = self.get_medium(&entity.inside_medium, &entity.base.loc); + let outside = self.get_medium(&entity.outside_medium, &entity.base.loc); + let medium_interface = MediumInterface { + inside: inside + .as_ref() + .map(|m| Ptr::from(m.as_ref())) + .unwrap_or(Ptr::null()), + outside: outside + .as_ref() + .map(|m| Ptr::from(m.as_ref())) + .unwrap_or(Ptr::null()), + }; + + let al_entity = &light_state.area_lights[light_idx]; + + let film_cs = self.film_colorspace.lock(); + let colorspace_ref = al_entity + .parameters + .color_space + .as_ref() + .or(film_cs.as_ref()); + + let mut shape_lights: Vec> = Vec::new(); + + for shape in &shape_objects { + let cs = colorspace_ref.map(|cs| cs.as_ref()); + match crate::core::light::create_area_light( + *entity.render_from_object, + None, + &al_entity.parameters, + &al_entity.loc, + shape, + alpha_ref, + cs, + arena, + ) { + Ok(light) => { + let light_arc = Arc::new(light); + all_lights.push(Arc::clone(&light_arc)); + shape_lights.push(light_arc); + } + Err(e) => { + log::error!("{}: failed to create area light: {}", al_entity.loc, e); + } + } + } + + if !shape_lights.is_empty() { + shape_index_to_area_lights.insert(i, shape_lights); + } + } + + log::debug!("Finished area lights"); + + (all_lights, shape_index_to_area_lights) } - /// Create area lights for shapes that reference one. Produces a map from - /// shape index to a vec of lights. - /// Must be called after shapes are loaded but before upload_shapes. pub fn create_area_lights( &self, loaded_shapes: &[Vec], - shape_entities: &[ShapeSceneEntity], textures: &NamedTextures, arena: &Arena, ) -> HashMap> { + let shape_entities = &self.shapes.lock(); let light_state = self.light_state.lock(); let mut shape_lights: HashMap> = HashMap::new(); @@ -681,7 +795,7 @@ impl BasicScene { sampler: Arc, aggregate: Arc, lights: Vec>, - arena: &Arena + arena: &Arena, ) -> PathIntegrator { let integrator = &self.integrator.lock().clone().unwrap(); PathIntegrator::create( @@ -691,7 +805,7 @@ impl BasicScene { aggregate, lights, PathConfig::SIMPLE, - arena + arena, ) .expect("Integrator creation has failed") } @@ -1114,12 +1228,23 @@ impl BasicScene { textures: &HashMap>, ) -> Option> { let name = params.get_texture("alpha"); - if name.is_empty() { - return None; - } - match textures.get(&name) { - Some(tex) => Some(tex.clone()), - None => panic!("{:?}: Alpha texture '{}' not found", loc, name), + if !name.is_empty() { + match textures.get(&name) { + Some(tex) => Some(tex.clone()), + None => panic!( + "{}: couldn't find float texture '{}' for \"alpha\" parameter.", + loc, name + ), + } + } else { + let alpha = params.get_one_float("alpha", 1.0).unwrap(); + if alpha < 1.0 { + Some(Arc::new(FloatTexture::Constant(FloatConstantTexture::new( + alpha, + )))) + } else { + None + } } } diff --git a/src/integrators/mod.rs b/src/integrators/mod.rs index b0f14eb..2bb690e 100644 --- a/src/integrators/mod.rs +++ b/src/integrators/mod.rs @@ -66,7 +66,7 @@ impl CreateIntegrator for PathIntegrator { ) -> Result { let _max_depth = parameters.get_one_int("maxdepth", 5)?; let _regularize = parameters.get_one_bool("regularize", false)?; - let light_sampler = create_light_sampler("bvh", &lights, arena); + let light_sampler = create_light_sampler("power", &lights, arena); let integrator = PathIntegrator::new(aggregate, lights, camera, light_sampler, config); Ok(integrator) } diff --git a/src/integrators/pipeline.rs b/src/integrators/pipeline.rs index 7cfc58c..96383c4 100644 --- a/src/integrators/pipeline.rs +++ b/src/integrators/pipeline.rs @@ -78,7 +78,7 @@ pub fn render( _base: &IntegratorBase, camera: &Camera, sampler_prototype: &Sampler, - arena: Arc, + arena: &Arena, ) where T: RayIntegratorTrait + Sync, { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 08a570d..c1f0417 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -8,7 +8,6 @@ pub mod mipmap; pub mod parallel; pub mod parameters; pub mod parser; -pub mod strings; pub mod upload; pub use error::FileLoc; @@ -16,7 +15,6 @@ pub use file::{read_float_file, resolve_filename}; pub use parameters::{ ParameterDictionary, ParsedParameter, ParsedParameterVector, TextureParameterDictionary, }; -pub use strings::*; pub use mipmap::{MIPMap, MIPMapFilterOptions}; pub use upload::{Upload, ArenaUpload}; diff --git a/src/utils/parallel.rs b/src/utils/parallel.rs index f027b2c..77e166a 100644 --- a/src/utils/parallel.rs +++ b/src/utils/parallel.rs @@ -1,47 +1,6 @@ use crossbeam_channel::{Receiver, bounded}; use rayon::prelude::*; -pub fn init_parallel(n_threads: usize) { - let threads = if n_threads == 0 { - num_cpus::get() - } else { - n_threads - }; - - if let Err(e) = rayon::ThreadPoolBuilder::new() - .num_threads(threads) - .build_global() - { - eprintln!("Warning: Rayon thread pool already initialized: {}", e); - } -} - -pub fn num_system_cores() -> usize { - num_cpus::get() -} - -pub fn max_concurrency() -> usize { - rayon::current_num_threads() -} - -pub fn parallel_for(start: i64, end: i64, func: F) -where - F: Fn(i64) + Sync + Send, -{ - (start..end).into_par_iter().for_each(|i| func(i)); -} - -pub fn parallel_for_2d(start_x: i64, end_x: i64, start_y: i64, end_y: i64, func: F) -where - F: Fn(i64, i64) + Sync + Send, -{ - (start_y..end_y).into_par_iter().for_each(|y| { - (start_x..end_x).into_par_iter().for_each(|x| { - func(x, y); - }); - }); -} - #[derive(Debug)] pub struct AsyncJob { receiver: Receiver, diff --git a/src/utils/strings.rs b/src/utils/strings.rs deleted file mode 100644 index eafbc77..0000000 --- a/src/utils/strings.rs +++ /dev/null @@ -1,5 +0,0 @@ -use unicode_normalization::UnicodeNormalization; - -pub fn normalize_utf8(input: &str) -> String { - input.nfc().collect::() -}