use super::entities::*; use super::state::*; use crate::core::aggregates::CreateBVH; use crate::core::camera::CameraFactory; use crate::core::film::FilmFactory; use crate::core::filter::FilterFactory; use crate::core::image::{HostImage, ImageIO}; use crate::core::material::MaterialFactory; use crate::core::medium::CreateMedium; use crate::core::primitive::{CreateGeometricPrimitive, CreateSimplePrimitive}; use crate::core::sampler::SamplerFactory; use crate::core::shape::{ShapeFactory, ShapeWithContext}; use crate::core::texture::{FloatTexture, SpectrumTexture}; use crate::integrators::{CreateIntegrator, PathConfig, PathIntegrator}; use crate::lights::sampler::create_light_sampler; use crate::utils::parallel::{run_async, AsyncJob}; use crate::utils::parameters::{NamedTextures, ParameterDictionary, TextureParameterDictionary}; use crate::utils::resolve_filename; use crate::wavefront::{CpuAggregate, CpuWavefrontRenderer, CreateWavefront}; use crate::{Arena, ArenaUpload, FileLoc}; use anyhow::{anyhow, Result}; use parking_lot::Mutex; use shared::core::aggregates::{BVHAggregate, SplitMethod}; use shared::core::camera::Camera; use shared::core::camera::CameraTrait; use shared::core::color::LINEAR; use shared::core::film::Film; use shared::core::filter::Filter; use shared::core::light::{Light, LightTrait}; use shared::core::material::Material; use shared::core::medium::{Medium, MediumInterface}; use shared::core::primitive::{AnimatedPrimitive, GeometricPrimitive, Primitive, SimplePrimitive}; use shared::core::sampler::{Sampler, SamplerTrait}; use shared::core::shape::Shape; use shared::core::texture::SpectrumType; use shared::core::{LightIdx, MaterialIdx}; use shared::spectra::RGBColorSpace; use shared::textures::FloatConstantTexture; use shared::utils::soa::SoA; use shared::wavefront::*; use shared::{gvec, Ptr, WorkQueue}; use std::collections::HashMap; use std::sync::Arc; fn find_medium( media: &HashMap>, name: &str, loc: &FileLoc, ) -> Option> { if name.is_empty() { return None; } match media.get(name) { Some(m) => Some(Arc::clone(m)), None => { log::error!("{}: medium '{}' not defined", loc, name); None } } } fn resolve_medium_interface( media: &HashMap>, inside: &str, outside: &str, loc: &FileLoc, ) -> MediumInterface { MediumInterface { inside: find_medium(media, inside, loc) .as_ref() .map(|m| Ptr::from(m.as_ref())) .unwrap_or(Ptr::null()), outside: find_medium(media, outside, loc) .as_ref() .map(|m| Ptr::from(m.as_ref())) .unwrap_or(Ptr::null()), } } fn resolve_material( mat_ref: &MaterialRef, named_materials: &HashMap, materials: &[Material], loc: &FileLoc, _arena: &Arena, ) -> MaterialIdx { match mat_ref { MaterialRef::Name(name) => match named_materials.get(name) { Some(m) => *m, None => { log::error!("{}: named material '{}' not found", loc, name); MaterialIdx::NONE } }, // Anonymous materials are placed at the front of `materials`, so the // index handed out by `add_material` is the handle directly. MaterialRef::Index(idx) => { if *idx < materials.len() { MaterialIdx(*idx as u32) } else { log::error!("{}: material index {} out of bounds", loc, idx); MaterialIdx::NONE } } MaterialRef::None => MaterialIdx::NONE, } } /// Resolve alpha texture from parameters fn get_alpha_texture( params: &ParameterDictionary, loc: &FileLoc, textures: &HashMap>, ) -> Option> { let name = params.get_texture("alpha"); if !name.is_empty() { match textures.get(&name) { Some(tex) => return Some(tex.clone()), None => panic!( "{}: couldn't find float texture '{}' for \"alpha\" parameter.", loc, name ), } } let alpha = params.get_one_float("alpha", 1.0).unwrap(); if alpha < 1.0 { Some(Arc::new(FloatTexture::Constant(FloatConstantTexture::new( alpha, )))) } else { 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 Default for BasicScene { fn default() -> Self { Self::new() } } pub type AreaLightMap = HashMap<(usize, usize), LightIdx>; 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: &Arena, ) -> 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, arena) .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), &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 res = film_instance.as_ref().base().full_resolution; let sampler_result = Sampler::create(&sampler.name, &sampler.parameters, res, &sampler.loc, arena) .map_err(|e| anyhow!("Failed to create sampler: {}", e))?; *self.sampler_state.lock() = SingletonState { result: Some(Arc::new(sampler_result)), job: None, }; let medium = self.get_medium(&camera.medium, &camera.base.loc); let camera_result = Camera::create( &camera.base.name, &camera.base.parameters, &camera.camera_transform, medium, Arc::clone(&film_instance), &camera.base.loc, arena, ) .map_err(|e| anyhow!("Failed to create camera: {}", e))?; *self.camera_state.lock() = SingletonState { result: Some(Arc::new(camera_result)), job: None, }; 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_medium(&self, name: &str, medium: MediumSceneEntity) { let mut state = self.media_state.lock(); state.entities.push((name.to_string(), medium)); } 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 } 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(); if texture.render_from_object.is_animated() { log::info!( "{}: animated world-to-texture not supported, using start transform", texture.base.loc ); } if texture.base.name != "imagemap" && texture.base.name != "ptex" { state.serial_spectrum_textures.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(()); } // Avoid duplicate work if the same file is already being loaded if state.loading_texture_filenames.contains(&filename) { state.serial_spectrum_textures.push((name, texture)); return Ok(()); } state.loading_texture_filenames.insert(filename.clone()); state .async_spectrum_textures .push((name.clone(), texture.clone())); let job = run_async(move || { let render_from_texture = texture.render_from_object.start_transform; let tex_dict = TextureParameterDictionary::new(texture.base.parameters.into(), None); Arc::new( SpectrumTexture::create( &texture.base.name, render_from_texture, tex_dict, SpectrumType::Albedo, texture.base.loc, &arena, ) .expect("Could not create spectrum texture"), ) }); state.spectrum_texture_jobs.insert(name, job); Ok(()) } 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); } // Texture creation pub fn create_textures(&self, arena: &Arena) -> NamedTextures { let mut state = self.texture_state.lock(); let mut float_textures: HashMap> = HashMap::new(); let mut spectrum_textures: HashMap> = HashMap::new(); 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()); } let mut named = NamedTextures { float_textures: Arc::new(float_textures.clone()), albedo_spectrum_textures: Arc::new(spectrum_textures.clone()), illuminant_spectrum_textures: Arc::new(HashMap::new()), unbounded_spectrum_textures: Arc::new(HashMap::new()), }; for (name, entity) in state.async_spectrum_textures.drain(..) { let render_from_texture = entity.render_from_object.start_transform; let params = entity.base.parameters.clone(); let unbounded = SpectrumTexture::create( &entity.base.name, render_from_texture, TextureParameterDictionary::new(params.clone().into(), None), SpectrumType::Unbounded, entity.base.loc.clone(), arena, ) .expect("Could not create unbounded spectrum texture"); let illum = SpectrumTexture::create( &entity.base.name, render_from_texture, TextureParameterDictionary::new(params.into(), None), SpectrumType::Illuminant, entity.base.loc, arena, ) .expect("Could not create illuminant spectrum texture"); Arc::make_mut(&mut named.unbounded_spectrum_textures) .insert(name.clone(), Arc::new(unbounded)); Arc::make_mut(&mut named.illuminant_spectrum_textures).insert(name, Arc::new(illum)); } // Serial float textures may reference already-loaded textures 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 make = |st: SpectrumType, named: &NamedTextures, loc| { let tex_dict = TextureParameterDictionary::new( entity.base.parameters.clone().into(), Some(named), ); SpectrumTexture::create( &entity.base.name, render_from_texture, tex_dict, st, loc, arena, ) .expect("Could not create spectrum texture") }; let albedo = make(SpectrumType::Albedo, &named, entity.base.loc.clone()); let unbounded = make(SpectrumType::Unbounded, &named, entity.base.loc.clone()); let illum = make(SpectrumType::Illuminant, &named, entity.base.loc); Arc::make_mut(&mut named.albedo_spectrum_textures) .insert(name.clone(), Arc::new(albedo)); Arc::make_mut(&mut named.unbounded_spectrum_textures) .insert(name.clone(), Arc::new(unbounded)); Arc::make_mut(&mut named.illuminant_spectrum_textures).insert(name, Arc::new(illum)); } named } pub fn create_materials( &self, textures: &NamedTextures, arena: &Arena, ) -> Result<(HashMap, Vec, MaterialIdx)> { let mut state = self.material_state.lock(); // Finish async normal map loads 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); } } } // Named materials must be *created* first: an anonymous material may be a // mix that resolves its sub-materials by name. They are *placed* after the // anonymous ones though, so that `MaterialRef::Index(i)` — an index into // `state.materials` handed out by `add_material` — is `MaterialIdx(i)`. let mut named_created: Vec<(String, Material)> = Vec::new(); // Value map for resolving named sub-materials (e.g. mix) during creation. let mut named_values: HashMap = HashMap::new(); for (name, entity) in &state.named_materials { if named_values.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 for '{}'", entity.loc, name); 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_values, // value map, not index map entity.loc.clone(), arena, ) { Ok(mat) => { named_values.insert(name.clone(), mat); named_created.push((name.clone(), mat)); } Err(e) => { log::error!( "{}: failed to create material '{}': {}", entity.loc, name, e ); } } } // Indexed (anonymous) materials occupy [0, state.materials.len()) so that // a `MaterialRef::Index` needs no offset. let mut materials: Vec = Vec::with_capacity(state.materials.len()); for entity in &state.materials { 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_values, entity.loc.clone(), arena, ) })(); let mat = match result { Ok(mat) => mat, Err(e) => { log::error!("{}: failed to create material: {}", entity.loc, e); crate::core::material::default_diffuse_material(arena) } }; materials.push(mat); } let mut named_materials: HashMap = HashMap::new(); for (name, mat) in named_created { named_materials.insert(name, MaterialIdx(materials.len() as u32)); materials.push(mat); } let default_mtl = MaterialIdx(materials.len() as u32); materials.push(crate::core::material::default_diffuse_material(arena)); Ok((named_materials, materials, default_mtl)) } pub fn create_media(&self, arena: &Arena) -> HashMap> { let mut state = self.media_state.lock(); if !state.jobs.is_empty() { let jobs: Vec<(String, AsyncJob)> = state.jobs.drain().collect(); for (name, job) in jobs { state.map.insert(name, Arc::new(job.wait())); } } let entities: Vec<(String, MediumSceneEntity)> = state.entities.drain(..).collect(); for (name, entity) in entities { if entity.render_from_object.is_animated() { log::warn!( "{}: animated media aren't supported, using start transform.", entity.base.loc ); } match Medium::create( &entity.base.name, &entity.base.parameters, entity.render_from_object.start_transform, &entity.base.loc, arena, ) { Ok(m) => { state.map.insert(name, Arc::new(*m)); } Err(e) => log::error!("{}: failed to create medium: {}", entity.base.loc, e), } } state.map.clone() } pub fn create_lights( &self, textures: &NamedTextures, media: &HashMap>, arena: &Arena, ) -> (Vec, AreaLightMap) { let light_state = self.light_state.lock(); let shapes = self.shapes.lock(); let film_cs = self.film_colorspace.lock(); let film_cs_ref = film_cs.as_deref(); let camera = self .get_camera() .expect("Camera must be initialized before lights"); let camera_transform = camera.base().camera_transform; let mut lights: Vec = Vec::new(); for entity in &light_state.lights { let medium = self.get_medium(&entity.medium, &entity.transformed_base.base.loc); if entity.transformed_base.render_from_object.is_animated() { log::warn!( "{}: animated lights aren't supported, using start transform.", entity.transformed_base.base.loc ); } match crate::core::light::create_light( &entity.transformed_base.base.name, entity.transformed_base.render_from_object.start_transform, medium.map(|m| *m), &entity.transformed_base.base.parameters, &entity.transformed_base.base.loc, camera_transform, arena, ) { Ok(light) => lights.push(light), // bare Light, no Arc Err(e) => log::error!( "{}: failed to create light: {}", entity.transformed_base.base.loc, e ), } } let mut area_map: AreaLightMap = HashMap::new(); for (entity_idx, entity) in shapes.iter().enumerate() { let Some(al_idx) = entity.light_index else { continue; }; let al = &light_state.area_lights[al_idx]; let created_shapes = 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(s) => s, Err(_) => continue, }; let alpha_tex = get_alpha_texture( &entity.base.parameters, &entity.base.loc, &textures.float_textures, ); let cs = al.parameters.color_space.as_deref().or(film_cs_ref); for (sub_idx, shape) in created_shapes.iter().enumerate() { let default_alpha = Arc::new(FloatTexture::default()); let alpha_ref = alpha_tex.as_ref().unwrap_or(&default_alpha); match crate::core::light::create_area_light( *entity.render_from_object, None, &al.parameters, &al.loc, shape, alpha_ref, cs, arena, ) { Ok(light) => { let idx = LightIdx(lights.len() as u32); lights.push(light); area_map.insert((entity_idx, sub_idx), idx); } Err(e) => log::error!("{}: area light creation failed: {}", al.loc, e), } } } (lights, area_map) } pub fn create_aggregate( &self, textures: &NamedTextures, named_materials: &HashMap, materials: &[Material], area_map: &AreaLightMap, media: &HashMap>, arena: &Arena, ) -> Arc { let mut shapes = self.shapes.lock(); let mut animated_shapes = self.animated_shapes.lock(); let mut instance_defs = self.instance_definitions.lock(); let mut instances = self.instances.lock(); let light_state = self.light_state.lock(); let film_cs = self.film_colorspace.lock(); let film_cs_ref = film_cs.as_deref(); log::info!("Starting shapes"); let mut primitives = Self::create_primitives_for_shapes( &shapes, textures, named_materials, materials, Some(area_map), media, arena, ); shapes.clear(); shapes.shrink_to_fit(); let animated_primitives = Self::create_primitives_for_animated_shapes( &animated_shapes, textures, named_materials, materials, media, arena, ); primitives.extend(animated_primitives); animated_shapes.clear(); animated_shapes.shrink_to_fit(); log::info!("Finished shapes"); log::info!("Starting instances"); let mut resolved_defs: HashMap> = HashMap::new(); for (name, def) in instance_defs.drain() { let mut inst_prims = Self::create_primitives_for_shapes( &def.shapes, textures, named_materials, materials, None, media, arena, ); let animated_inst_prims = Self::create_primitives_for_animated_shapes( &def.animated_shapes, textures, named_materials, materials, media, arena, ); inst_prims.extend(animated_inst_prims); let aggregate = if inst_prims.len() > 1 { Some(Primitive::BVH(arena.alloc(BVHAggregate::new( inst_prims, 4, SplitMethod::SAH, )))) } else if inst_prims.len() == 1 { Some(inst_prims.into_iter().next().unwrap()) } else { None }; resolved_defs.insert(name, aggregate); } for inst in instances.drain(..) { let def = match resolved_defs.get(&inst.name) { Some(Some(prim)) => prim, Some(None) => continue, None => { log::error!("{}: object instance '{}' not defined", inst.loc, inst.name); continue; } }; let prim = match &inst.transform { InstanceTransform::Static(xform) => { Primitive::Transformed(shared::core::primitive::TransformedPrimitive { primitive: arena.alloc(*def), render_from_primitive: arena.alloc(**xform), }) } InstanceTransform::Animated(anim) => Primitive::Animated(AnimatedPrimitive { primitive: arena.alloc(*def), render_from_primitive: arena.alloc(*anim), }), }; primitives.push(prim); } log::info!("Finished instances"); log::info!("Starting top-level accelerator"); let aggregate = if !primitives.is_empty() { BVHAggregate::new(primitives, 4, SplitMethod::SAH) } else { BVHAggregate::empty() }; let agg_ptr = arena.alloc(aggregate); log::info!("Finished top-level accelerator"); Arc::new(Primitive::BVH(agg_ptr)) } // Integrator pub fn create_integrator( &self, camera: Arc, sampler: Arc, aggregate: Arc, lights: Vec, materials: Vec, arena: &Arena, ) -> PathIntegrator { let integrator_entity = self.integrator.lock().clone().unwrap(); let name = &integrator_entity.name; match name.as_str() { "path" | "volpath" => PathIntegrator::create( integrator_entity.parameters.clone(), camera, sampler, aggregate, lights, materials, PathConfig::FULL, arena, ) .expect("Integrator creation failed"), _ => panic!("Unknown integrator: {}", name), } } pub fn create_wavefront_integrator( &self, camera: Arc, sampler: Arc, aggregate: Arc, lights: Vec, materials: Vec, arena: &Arena, ) -> CpuWavefrontRenderer { let integrator_entity = self.integrator.lock().clone().unwrap(); let params = &integrator_entity.parameters; CpuWavefrontRenderer::create( params.clone(), camera, sampler, aggregate, lights, materials, arena, ) } // 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") } 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 medium: Arc = Arc::new(job.wait()); state.map.insert(name.to_string(), medium.clone()); return Some(medium); } log::error!("{}: medium '{}' is not defined.", loc, name); None } fn create_primitives_for_shapes( shapes: &[ShapeSceneEntity], textures: &NamedTextures, named_materials: &HashMap, materials: &[Material], // `None` for object-instance definitions: the map is keyed by the // top-level shape index, so it must not be consulted for the // independently-numbered shapes inside an instance definition. area_map: Option<&AreaLightMap>, media: &HashMap>, arena: &Arena, ) -> Vec { let mut primitives = Vec::new(); for (entity_idx, entity) in shapes.iter().enumerate() { if area_map.is_none() && entity.light_index.is_some() { log::error!( "{}: area lights are not supported with object instancing.", entity.base.loc ); } let created_shapes = 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(s) => s, Err(e) => { log::error!("{}: shape creation failed: {}", entity.base.loc, e); continue; } }; if created_shapes.is_empty() { continue; } let mtl: MaterialIdx = resolve_material( &entity.material, named_materials, materials, &entity.base.loc, arena, ); let alpha_tex = get_alpha_texture( &entity.base.parameters, &entity.base.loc, &textures.float_textures, ); let mi = resolve_medium_interface( media, &entity.inside_medium, &entity.outside_medium, &entity.base.loc, ); for (sub_idx, shape) in created_shapes.into_iter().enumerate() { // look up the pre-created light index instead of creating one let light_idx = area_map .and_then(|m| m.get(&(entity_idx, sub_idx)).copied()) .unwrap_or(LightIdx::NONE); let prim = if light_idx.is_none() && !mi.is_medium_transition() && alpha_tex.is_none() { Primitive::Simple(SimplePrimitive::new(shape, mtl)) // mtl is MaterialIdx now } else { let alpha_ptr = alpha_tex .as_ref() .map(|t| arena.upload(t.as_ref())) .unwrap_or(Ptr::null()); Primitive::Geometric(GeometricPrimitive::new( shape, mtl, light_idx, mi, alpha_ptr, )) }; primitives.push(prim); } } primitives } fn create_primitives_for_animated_shapes( shapes: &[AnimatedShapeSceneEntity], textures: &NamedTextures, named_materials: &HashMap, materials: &[Material], media: &HashMap>, arena: &Arena, ) -> Vec { let mut primitives = Vec::new(); for entity in shapes { let start_xform = entity.transformed_base.render_from_object.start_transform; let created_shapes = match Shape::create( &entity.transformed_base.base.name, start_xform, start_xform.inverse(), entity.reverse_orientation, entity.transformed_base.base.parameters.clone(), &textures.float_textures, entity.transformed_base.base.loc.clone(), arena, ) { Ok(s) => s, Err(e) => { log::error!( "{}: animated shape creation failed: {}", entity.transformed_base.base.loc, e ); continue; } }; if created_shapes.is_empty() { continue; } let mtl = resolve_material( &entity.material, named_materials, materials, &entity.transformed_base.base.loc, arena, ); let alpha_tex = get_alpha_texture( &entity.transformed_base.base.parameters, &entity.transformed_base.base.loc, &textures.float_textures, ); let mi = resolve_medium_interface( media, &entity.inside_medium, &entity.outside_medium, &entity.transformed_base.base.loc, ); if entity.light_index.is_some() { log::error!( "{}: animated area lights are not supported.", entity.transformed_base.base.loc ); } // Build base primitives, then wrap each in AnimatedPrimitive let mut base_prims = Vec::new(); for shape in created_shapes { let base = if !mi.is_medium_transition() && alpha_tex.is_none() { Primitive::Simple(SimplePrimitive::new(shape, mtl)) } else { let alpha_ptr = alpha_tex .as_ref() .map(|t| arena.upload(t.as_ref())) .unwrap_or(Ptr::null()); Primitive::Geometric(GeometricPrimitive::new( shape, mtl, LightIdx::default(), // no area light on animated shapes mi, alpha_ptr, )) }; base_prims.push(base); } // Collapse multiple sub-shapes into a BVH, then animate the whole thing let base = if base_prims.len() > 1 { let bvh = BVHAggregate::new(base_prims, 4, SplitMethod::SAH); Primitive::BVH(arena.alloc(bvh)) } else { base_prims.into_iter().next().unwrap() }; primitives.push(Primitive::Animated(AnimatedPrimitive { primitive: arena.alloc(base), render_from_primitive: arena.alloc(entity.transformed_base.render_from_object), })); } primitives } 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 transform", texture.base.loc ); } // Non-image textures must be created serially (they may reference other textures) 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(()); } // Already loading this file — fall back to serial to avoid duplicate work 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() { log::error!( "{}: \"string filename\" not provided for image texture.", loc ); *n_missing += 1; return false; } if !std::path::Path::new(filename).exists() { log::error!("{}: {}: file not found.", loc, filename); *n_missing += 1; return false; } true } // Material helpers 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 = HostImage::read(path, Some(LINEAR)) .unwrap_or_else(|e| panic!("{}: failed to read normal map: {}", filename_clone, e)); let rgb_desc = immeta .image .get_channel_desc(&["R", "G", "B"]) .unwrap_or_else(|_| { panic!( "{}: 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_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)) } #[allow(dead_code)] fn upload_shapes( &self, arena: &Arena, entities: &[ShapeSceneEntity], loaded: Vec, textures: &NamedTextures, named_materials: &HashMap, materials: &[Material], media: &HashMap>, shape_lights: &AreaLightMap, ) -> Vec { // TODO: GPU wavefront path — upload shapes into device-visible arena, // build SOA primitive arrays for kernel dispatch let _ = ( arena, entities, loaded, textures, named_materials, materials, media, shape_lights, ); Vec::new() } #[allow(dead_code)] fn upload_animated_shapes( &self, arena: &Arena, entities: &[AnimatedShapeSceneEntity], loaded: Vec>, textures: &NamedTextures, named_materials: &HashMap, materials: &[Material], media: &HashMap>, ) -> Vec { // TODO: GPU wavefront path — animated shape upload let _ = ( arena, entities, loaded, textures, named_materials, materials, media, ); Vec::new() } }