use super::entities::*; use super::state::*; use crate::core::camera::CameraFactory; use crate::core::film::FilmFactory; use crate::core::filter::FilterFactory; use crate::core::image::{io::ImageIO, Image}; use crate::core::material::MaterialFactory; use crate::core::primitive::{CreateGeometricPrimitive, CreateSimplePrimitive}; use crate::core::sampler::SamplerFactory; use crate::core::shape::ShapeFactory; use crate::core::texture::{FloatTexture, SpectrumTexture}; use crate::utils::parallel::{run_async, AsyncJob}; use crate::utils::parameters::{NamedTextures, ParameterDictionary, TextureParameterDictionary}; use crate::utils::{resolve_filename, Upload}; use crate::{Arena, FileLoc}; use anyhow::{anyhow, Result}; use parking_lot::Mutex; use rayon::prelude::*; use shared::core::camera::{Camera, CameraTransform}; use shared::core::color::LINEAR; use shared::core::film::Film; use shared::core::filter::Filter; use shared::core::light::Light; use shared::core::material::Material; use shared::core::medium::{Medium, MediumInterface}; use shared::core::primitive::{GeometricPrimitive, Primitive, SimplePrimitive}; use shared::core::sampler::Sampler; use shared::core::shape::Shape; use shared::core::texture::SpectrumType; use shared::spectra::RGBColorSpace; use shared::utils::Ptr; use std::collections::HashMap; use std::sync::Arc; pub struct SceneLookup<'a> { pub textures: &'a NamedTextures, pub media: &'a HashMap>, pub named_materials: &'a HashMap, pub materials: &'a Vec, pub shape_lights: &'a HashMap>, } impl<'a> SceneLookup<'a> { pub fn find_medium(&self, name: &str, loc: &FileLoc) -> Option> { if name.is_empty() { return None; } self.media.get(name).cloned().or_else(|| { log::error!("{}: medium '{}' not defined", loc, name); None }) } pub fn resolve_material(&self, mat_ref: &MaterialRef, loc: &FileLoc) -> Option { match mat_ref { MaterialRef::Name(name) => match self.named_materials.get(name) { Some(m) => Some(*m), None => { log::error!("{}: named material '{}' not found", loc, name); None } }, MaterialRef::Index(idx) => { if *idx < self.materials.len() { Some(self.materials[*idx]) } else { log::error!("{}: material index {} out of bounds", loc, idx); None } } MaterialRef::None => None, } } } pub struct BasicScene { pub integrator: Mutex>, pub accelerator: Mutex>, pub film_colorspace: Mutex>>, pub shapes: Mutex>, pub animated_shapes: Mutex>, pub instances: Mutex>, pub instance_definitions: Mutex>>, pub media_state: Mutex, pub material_state: Mutex, pub light_state: Mutex, pub texture_state: Mutex, pub camera_state: Mutex>, pub sampler_state: Mutex>, pub film_state: Mutex>, } impl BasicScene { pub fn new() -> Self { Self { integrator: Mutex::new(None), accelerator: Mutex::new(None), film_colorspace: Mutex::new(None), shapes: Mutex::new(Vec::new()), animated_shapes: Mutex::new(Vec::new()), instances: Mutex::new(Vec::new()), instance_definitions: Mutex::new(HashMap::new()), media_state: Mutex::new(MediaState::default()), material_state: Mutex::new(MaterialState::default()), light_state: Mutex::new(LightState::default()), texture_state: Mutex::new(TextureState::default()), camera_state: Mutex::new(SingletonState::default()), sampler_state: Mutex::new(SingletonState::default()), film_state: Mutex::new(SingletonState::default()), } } pub fn set_options( self: &Arc, filter: SceneEntity, film: SceneEntity, camera: CameraSceneEntity, sampler: SceneEntity, integ: SceneEntity, accel: SceneEntity, arena: Arc, ) -> Result<()> { *self.integrator.lock() = Some(integ); *self.accelerator.lock() = Some(accel); if let Some(cs) = film.parameters.color_space.as_ref() { *self.film_colorspace.lock() = Some(Arc::clone(cs)); } let filter = Filter::create(&filter.name, &filter.parameters, &filter.loc) .map_err(|e| anyhow!("Failed to create filter: {}", e))?; let shutter_close = camera.base.parameters.get_one_float("shutterclose", 1.)?; let shutter_open = camera.base.parameters.get_one_float("shutteropen", 0.)?; let exposure_time = shutter_close - shutter_open; let film_instance = Arc::new( Film::create( &film.name, &film.parameters, exposure_time, filter, Some(camera.camera_transform.clone()), &film.loc, &arena, ) .map_err(|e| anyhow!("Failed to create film: {}", e))?, ); *self.film_state.lock() = SingletonState { result: Some(Arc::clone(&film_instance)), job: None, }; let arena_sampler = Arc::clone(&arena); let sampler_film = Arc::clone(&film_instance); let sampler_job = run_async(move || { let res = sampler_film.as_ref().base().full_resolution; Sampler::create( &sampler.name, &sampler.parameters, res, &sampler.loc, &arena_sampler, ) .map_err(|e| anyhow!("Failed to create sampler: {}", e)) }); self.sampler_state.lock().job = Some(sampler_job); let arena_camera = Arc::clone(&arena); let camera_film = Arc::clone(&film_instance); let scene_ptr = Arc::clone(self); let camera_job = run_async(move || { let medium = scene_ptr.get_medium(&camera.medium, &camera.base.loc); Camera::create( &camera.base.name, &camera.base.parameters, &camera.camera_transform, medium, camera_film, &camera.base.loc, &arena_camera, ) .map_err(|e| anyhow!("Failed to create camera: {}", e)) }); self.camera_state.lock().job = Some(camera_job); Ok(()) } pub fn add_named_material(&self, name: &str, material: SceneEntity) { let mut state = self.material_state.lock(); self.start_loading_normal_maps(&mut state, &material.parameters); state.named_materials.push((name.to_string(), material)); } pub fn add_material(&self, material: SceneEntity) -> usize { let mut state = self.material_state.lock(); self.start_loading_normal_maps(&mut state, &material.parameters); state.materials.push(material); state.materials.len() - 1 } fn add_texture_generic( &self, name: String, texture: TextureSceneEntity, state: &mut TextureState, get_serial: impl FnOnce(&mut TextureState) -> &mut Vec<(String, TextureSceneEntity)>, get_jobs: impl FnOnce(&mut TextureState) -> &mut HashMap>>, create_fn: F, ) -> Result<()> where T: Send + Sync + 'static, F: FnOnce(TextureSceneEntity) -> T + Send + 'static, { if texture.render_from_object.is_animated() { log::info!( "{}: Animated world to texture not supported, using start", texture.base.loc ); } if texture.base.name != "imagemap" && texture.base.name != "ptex" { get_serial(state).push((name, texture)); return Ok(()); } let filename = resolve_filename(&texture.base.parameters.get_one_string("filename", "")?); if !self.validate_texture_file(&filename, &texture.base.loc, &mut state.n_missing_textures) { return Ok(()); } if state.loading_texture_filenames.contains(&filename) { get_serial(state).push((name, texture)); return Ok(()); } state.loading_texture_filenames.insert(filename); let job = run_async(move || Arc::new(create_fn(texture))); get_jobs(state).insert(name, job); Ok(()) } fn validate_texture_file(&self, filename: &str, loc: &FileLoc, n_missing: &mut usize) -> bool { if filename.is_empty() { eprintln!( "[{:?}] \"string filename\" not provided for image texture.", loc ); *n_missing += 1; return false; } if !std::path::Path::new(filename).exists() { eprintln!("[{:?}] {}: file not found.", loc, filename); *n_missing += 1; return false; } true } pub fn add_float_texture( &self, name: String, texture: TextureSceneEntity, arena: Arc, ) -> Result<()> { let mut state = self.texture_state.lock(); self.add_texture_generic( name, texture, &mut state, |s| &mut s.serial_float_textures, |s| &mut s.float_texture_jobs, move |tex| { let render_from_texture = tex.render_from_object.start_transform; let tex_dict = TextureParameterDictionary::new(tex.base.parameters.into(), None); FloatTexture::create( &tex.base.name, render_from_texture, tex_dict, tex.base.loc, &arena, ) .expect("Could not create Float texture") }, ) } pub fn add_spectrum_texture( &self, name: String, texture: TextureSceneEntity, arena: Arc, ) -> Result<()> { let mut state = self.texture_state.lock(); self.add_texture_generic( name, texture, &mut state, |s| &mut s.serial_spectrum_textures, |s| &mut s.spectrum_texture_jobs, move |tex| { let render_from_texture = tex.render_from_object.start_transform; let tex_dict = TextureParameterDictionary::new(tex.base.parameters.into(), None); SpectrumTexture::create( &tex.base.name, render_from_texture, tex_dict, SpectrumType::Albedo, tex.base.loc, &arena, ) .expect("Could not create spectrum texture") }, ) } pub fn add_area_light(&self, light: SceneEntity) -> usize { let mut state = self.light_state.lock(); state.area_lights.push(light); state.area_lights.len() - 1 } pub fn add_light(&self, light: LightSceneEntity) { self.light_state.lock().lights.push(light); } pub fn add_shape(&self, shape: ShapeSceneEntity) { self.shapes.lock().push(shape); } pub fn add_shapes(&self, new_shapes: Vec) { self.shapes.lock().extend(new_shapes); } pub fn add_animated_shapes(&self, new_shapes: Vec) { self.animated_shapes.lock().extend(new_shapes); } pub fn add_instance_definition(&self, instance: InstanceDefinitionSceneEntity) { let name = instance.name.clone(); self.instance_definitions .lock() .insert(name, Arc::new(instance)); } pub fn add_instance_uses(&self, uses: Vec) { self.instances.lock().extend(uses); } pub fn create_textures(&self, arena: &mut Arena) -> NamedTextures { let mut state = self.texture_state.lock(); let mut float_textures: HashMap> = HashMap::new(); let mut spectrum_textures: HashMap> = HashMap::new(); // Collect async jobs for (name, job) in state.float_texture_jobs.drain() { float_textures.insert(name, job.wait()); } for (name, job) in state.spectrum_texture_jobs.drain() { spectrum_textures.insert(name, job.wait()); } // Create serial textures (need access to loaded textures, using shared memory) let mut named = NamedTextures { float_textures: Arc::new(float_textures.clone()), albedo_spectrum_textures: Arc::new(spectrum_textures.clone()), illuminant_spectrum_textures: Arc::new(spectrum_textures.clone()), unbounded_spectrum_textures: Arc::new(spectrum_textures.clone()), }; for (name, entity) in state.serial_float_textures.drain(..) { let render_from_texture = entity.render_from_object.start_transform; let tex_dict = TextureParameterDictionary::new(entity.base.parameters.into(), Some(&named)); let tex = FloatTexture::create( &entity.base.name, render_from_texture, tex_dict, entity.base.loc, arena, ) .expect("Could not create float texture"); Arc::make_mut(&mut named.float_textures).insert(name, Arc::new(tex)); } for (name, entity) in state.serial_spectrum_textures.drain(..) { let render_from_texture = entity.render_from_object.start_transform; let tex_dict = TextureParameterDictionary::new(entity.base.parameters.into(), Some(&named)); let tex = SpectrumTexture::create( &entity.base.name, render_from_texture, tex_dict, SpectrumType::Albedo, entity.base.loc, arena, ) .expect("Could not create spectrum texture"); Arc::make_mut(&mut named.albedo_spectrum_textures).insert(name, Arc::new(tex)); } named } // Assuming that we can carry on if a material is missing. // This might be a bad idea, but testing it for now (2026/02/19) pub fn create_materials( &self, textures: &NamedTextures, arena: &mut Arena, ) -> Result<(HashMap, Vec)> { let mut state = self.material_state.lock(); let finished: Vec<_> = state.normal_map_jobs.drain().collect(); for (filename, job) in finished { match std::panic::catch_unwind(|| job.wait()) { Ok(img) => { state.normal_maps.insert(filename, img); } Err(_) => { log::error!("Failed to load normal map: {}", filename); } } } let mut named_materials: HashMap = HashMap::new(); for (name, entity) in &state.named_materials { if named_materials.contains_key(name) { log::error!( "{}: trying to redefine named material '{}'.", entity.loc, name ); continue; } let mat_type = entity.parameters.get_one_string("type", "")?; if mat_type.is_empty() { log::error!("{}: missing material type", entity.loc); continue; } let normal_map = self.get_normal_map(&state, &entity.parameters)?; let tex_dict = TextureParameterDictionary::new( Arc::new(entity.parameters.clone()), Some(textures), ); match Material::create( &mat_type, &tex_dict, normal_map, &named_materials, entity.loc.clone(), arena, ) { Ok(mat) => { named_materials.insert(name.clone(), mat); } Err(e) => { log::error!( "{}: Failed to create material '{}': {}", entity.loc, name, e ); } } } let materials: Vec = state .materials .iter() .filter_map(|entity| { let result: Result = (|| { let normal_map = self.get_normal_map(&state, &entity.parameters)?; let tex_dict = TextureParameterDictionary::new( entity.parameters.clone().into(), Some(textures), ); Material::create( &entity.name, &tex_dict, normal_map, &named_materials, entity.loc.clone(), arena, ) })(); match result { Ok(mat) => Some(mat), Err(e) => { log::error!("{}: Failed to create material: {}", entity.loc, e); None } } }) .collect(); Ok((named_materials, materials)) } pub fn create_lights( &self, camera_transform: &CameraTransform, arena: &mut Arena, ) -> Vec { let state = self.light_state.lock(); state .lights .iter() .filter_map(|entity| { let render_from_light = entity.transformed_base.render_from_object.start_transform; let medium = self .get_medium(&entity.medium, &entity.transformed_base.base.loc) .map(|m| *m); 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 } } }) .collect() } /// 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: &mut Arena, ) -> HashMap> { let light_state = self.light_state.lock(); let mut shape_lights: HashMap> = HashMap::new(); for (i, entity) in shape_entities.iter().enumerate() { let light_idx = match entity.light_index { Some(idx) => idx, None => continue, }; let shapes = match loaded_shapes.get(i) { Some(s) if !s.is_empty() => s, _ => continue, }; let al_entity = &light_state.area_lights[light_idx]; 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); // Use the film colorspace as fallback for area light emission let film_cs = self.film_colorspace.lock(); let colorspace_ref = al_entity .parameters .color_space .as_ref() .or(film_cs.as_ref()); let render_from_light = *entity.render_from_object; let lights: Vec = shapes .iter() .filter_map(|shape| { match crate::core::light::create_area_light( render_from_light, None, &al_entity.parameters, &al_entity.loc, shape, alpha_ref, colorspace_ref.map(|cs| cs.as_ref()), arena, ) { Ok(light) => Some(light), Err(e) => { log::error!("{}: failed to create area light: {}", al_entity.loc, e); None } } }) .collect(); if !lights.is_empty() { shape_lights.insert(i, lights); } } shape_lights } pub fn create_aggregate( &self, textures: &NamedTextures, named_materials: &HashMap, materials: &Vec, shape_lights: &HashMap>, arena: &mut Arena, ) -> Vec { let shapes = self.shapes.lock(); let animated_shapes = self.animated_shapes.lock(); let media = self.media_state.lock(); let lookup = SceneLookup { textures, media: &media.map, named_materials, materials, shape_lights, }; let mut primitives = Vec::new(); let loaded = self.load_shapes_parallel(&shapes, &lookup, arena); primitives.extend(self.upload_shapes(arena, &shapes, loaded, &lookup)); let loaded_anim = self.load_animated_shapes_parallel(&animated_shapes, &lookup, arena); primitives.extend(self.upload_animated_shapes( arena, &animated_shapes, loaded_anim, &lookup, )); primitives } pub fn load_shapes_parallel( &self, entities: &[ShapeSceneEntity], lookup: &SceneLookup, arena: &mut Arena, ) -> Vec> { entities .par_iter() .map(|sh| { Shape::create( &sh.base.name, *sh.render_from_object.as_ref(), *sh.object_from_render.as_ref(), sh.reverse_orientation, sh.base.parameters.clone(), &lookup.textures.float_textures, sh.base.loc.clone(), arena, ) .unwrap_or_else(|e| { eprintln!("Shape '{}' failed: {}", sh.base.name, e); Vec::new() }) }) .collect() } fn load_animated_shapes_parallel( &self, entities: &[AnimatedShapeSceneEntity], lookup: &SceneLookup, arena: &Arena, ) -> Vec> { entities .par_iter() .map(|sh| { Shape::create( &sh.transformed_base.base.name, *sh.identity.as_ref(), *sh.identity.as_ref(), sh.reverse_orientation, sh.transformed_base.base.parameters.clone(), &lookup.textures.float_textures, sh.transformed_base.base.loc.clone(), arena, ) .expect("Could not create shape") }) .collect() } fn upload_shapes( &self, arena: &Arena, entities: &[ShapeSceneEntity], loaded: Vec>, lookup: &SceneLookup, ) -> Vec { let mut primitives = Vec::new(); for (i, (entity, shapes)) in entities.iter().zip(loaded).enumerate() { if shapes.is_empty() { continue; } let alpha_tex = self.get_alpha_texture( &entity.base.parameters, &entity.base.loc, &lookup.textures.float_textures, ); let mtl = lookup .resolve_material(&entity.material, &entity.base.loc) .unwrap_or_else(|| crate::core::material::default_diffuse_material(arena)); let mi = MediumInterface { inside: lookup .find_medium(&entity.inside_medium, &entity.base.loc) .map(|m| Ptr::from(m.as_ref())) .unwrap_or(Ptr::null()), outside: lookup .find_medium(&entity.outside_medium, &entity.base.loc) .map(|m| Ptr::from(m.as_ref())) .unwrap_or(Ptr::null()), }; let shape_lights_opt = lookup.shape_lights.get(&i); for (j, shape) in shapes.into_iter().enumerate() { let mut area_light = None; if entity.light_index.is_some() { if let Some(lights) = shape_lights_opt { if j < lights.len() { area_light = Some(lights[j].clone()); } } } let shape_ptr = shape.upload(arena); let prim = if area_light.is_none() && !mi.is_medium_transition() && alpha_tex.is_none() { let p = SimplePrimitive::new(shape_ptr, Ptr::from(&mtl)); Primitive::Simple(p) } else { let p = GeometricPrimitive::new( shape_ptr, mtl.upload(arena), area_light.upload(arena), mi.clone(), alpha_tex.upload(arena), ); Primitive::Geometric(p) }; primitives.push(prim); } } primitives } fn upload_animated_shapes( &self, _arena: &mut Arena, _entities: &[AnimatedShapeSceneEntity], _loaded: Vec>, _lookup: &SceneLookup, ) -> Vec { // TODO: implement animated shape upload Vec::new() } // Getters pub fn get_camera(&self) -> Result> { self.get_singleton(&self.camera_state, "Camera") } pub fn get_sampler(&self) -> Result> { self.get_singleton(&self.sampler_state, "Sampler") } pub fn get_film(&self) -> Result> { self.get_singleton(&self.film_state, "Film") } // Helpers fn get_singleton( &self, state: &Mutex>, name: &str, ) -> Result> { let mut guard = state.lock(); if let Some(ref res) = guard.result { return Ok(res.clone()); } if let Some(job) = guard.job.take() { let val = job.wait()?; let res = Arc::new(val); guard.result = Some(res.clone()); return Ok(res); } Err(anyhow!("{} requested but not initialized!", name)) } fn start_loading_normal_maps( &self, state: &mut MaterialState, params: &ParameterDictionary, ) -> Result<()> { let filename = resolve_filename(¶ms.get_one_string("normalmap", "")?); if filename.is_empty() { return Ok(()); } if state.normal_map_jobs.contains_key(&filename) || state.normal_maps.contains_key(&filename) { return Ok(()); } let filename_clone = filename.clone(); let job = run_async(move || { let path = std::path::Path::new(&filename_clone); let immeta = Image::read(path, Some(LINEAR)).expect(&format!( "{}: normal map must contain R, G, B channels", filename_clone )); let rgb_desc = immeta .image .get_channel_desc(&["R", "G", "B"]) .expect(&format!( "{}: normal map must contain R, G, B channels", filename_clone )); Arc::new(immeta.image.select_channels(&rgb_desc)) }); state.normal_map_jobs.insert(filename, job); Ok(()) } fn get_normal_map( &self, state: &MaterialState, params: &ParameterDictionary, ) -> Result>> { let filename = resolve_filename(¶ms.get_one_string("normalmap", "")?); if filename.is_empty() { return Ok(None); } Ok(state.normal_maps.get(&filename).cloned()) } fn get_alpha_texture( &self, params: &ParameterDictionary, loc: &FileLoc, 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), } } pub fn get_medium(&self, name: &str, loc: &FileLoc) -> Option> { if name.is_empty() { return None; } let mut state = self.media_state.lock(); if let Some(medium) = state.map.get(name) { return Some(Arc::clone(medium)); } if let Some(job) = state.jobs.remove(name) { let job: AsyncJob = job; let result: Medium = job.wait(); let medium: Arc = Arc::new(result); state.map.insert(name.to_string(), medium.clone()); return Some(medium); } log::error!("{}: Medium \"{}\" is not defined.", loc, name); None } }