1305 lines
45 KiB
Rust
1305 lines
45 KiB
Rust
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<String, Arc<Medium>>,
|
|
name: &str,
|
|
loc: &FileLoc,
|
|
) -> Option<Arc<Medium>> {
|
|
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<String, Arc<Medium>>,
|
|
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<String, MaterialIdx>,
|
|
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<String, Arc<FloatTexture>>,
|
|
) -> Option<Arc<FloatTexture>> {
|
|
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<Option<SceneEntity>>,
|
|
pub accelerator: Mutex<Option<SceneEntity>>,
|
|
pub film_colorspace: Mutex<Option<Arc<RGBColorSpace>>>,
|
|
|
|
pub shapes: Mutex<Vec<ShapeSceneEntity>>,
|
|
pub animated_shapes: Mutex<Vec<AnimatedShapeSceneEntity>>,
|
|
|
|
pub instances: Mutex<Vec<InstanceSceneEntity>>,
|
|
pub instance_definitions: Mutex<HashMap<String, Arc<InstanceDefinitionSceneEntity>>>,
|
|
|
|
pub media_state: Mutex<MediaState>,
|
|
pub material_state: Mutex<MaterialState>,
|
|
pub light_state: Mutex<LightState>,
|
|
pub texture_state: Mutex<TextureState>,
|
|
|
|
pub camera_state: Mutex<SingletonState<Camera>>,
|
|
pub sampler_state: Mutex<SingletonState<Sampler>>,
|
|
pub film_state: Mutex<SingletonState<Film>>,
|
|
}
|
|
|
|
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<Self>,
|
|
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<Arena>,
|
|
) -> 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<Arena>,
|
|
) -> 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<ShapeSceneEntity>) {
|
|
self.shapes.lock().extend(new_shapes);
|
|
}
|
|
|
|
pub fn add_animated_shapes(&self, new_shapes: Vec<AnimatedShapeSceneEntity>) {
|
|
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<InstanceSceneEntity>) {
|
|
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<String, Arc<FloatTexture>> = HashMap::new();
|
|
let mut spectrum_textures: HashMap<String, Arc<SpectrumTexture>> = 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<String, MaterialIdx>, Vec<Material>, 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<String, Material> = 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<Material> = Vec::with_capacity(state.materials.len());
|
|
for entity in &state.materials {
|
|
let result: Result<Material> = (|| {
|
|
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<String, MaterialIdx> = 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<String, Arc<Medium>> {
|
|
let mut state = self.media_state.lock();
|
|
if !state.jobs.is_empty() {
|
|
let jobs: Vec<(String, AsyncJob<Medium>)> = 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<String, Arc<Medium>>,
|
|
arena: &Arena,
|
|
) -> (Vec<Light>, 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<Light> = 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<String, MaterialIdx>,
|
|
materials: &[Material],
|
|
area_map: &AreaLightMap,
|
|
media: &HashMap<String, Arc<Medium>>,
|
|
arena: &Arena,
|
|
) -> Arc<Primitive> {
|
|
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<String, Option<Primitive>> = 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<Camera>,
|
|
sampler: Arc<Sampler>,
|
|
aggregate: Arc<Primitive>,
|
|
lights: Vec<Light>,
|
|
materials: Vec<Material>,
|
|
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<Camera>,
|
|
sampler: Arc<Sampler>,
|
|
aggregate: Arc<Primitive>,
|
|
lights: Vec<Light>,
|
|
materials: Vec<Material>,
|
|
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<Arc<Camera>> {
|
|
self.get_singleton(&self.camera_state, "Camera")
|
|
}
|
|
|
|
pub fn get_sampler(&self) -> Result<Arc<Sampler>> {
|
|
self.get_singleton(&self.sampler_state, "Sampler")
|
|
}
|
|
|
|
pub fn get_film(&self) -> Result<Arc<Film>> {
|
|
self.get_singleton(&self.film_state, "Film")
|
|
}
|
|
|
|
pub fn get_medium(&self, name: &str, loc: &FileLoc) -> Option<Arc<Medium>> {
|
|
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<Medium> = 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<String, MaterialIdx>,
|
|
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<String, Arc<Medium>>,
|
|
arena: &Arena,
|
|
) -> Vec<Primitive> {
|
|
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<String, MaterialIdx>,
|
|
materials: &[Material],
|
|
media: &HashMap<String, Arc<Medium>>,
|
|
arena: &Arena,
|
|
) -> Vec<Primitive> {
|
|
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<T, F>(
|
|
&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<String, AsyncJob<Arc<T>>>,
|
|
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<Option<Arc<HostImage>>> {
|
|
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<T: Send + 'static>(
|
|
&self,
|
|
state: &Mutex<SingletonState<T>>,
|
|
name: &str,
|
|
) -> Result<Arc<T>> {
|
|
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<ShapeWithContext>,
|
|
textures: &NamedTextures,
|
|
named_materials: &HashMap<String, MaterialIdx>,
|
|
materials: &[Material],
|
|
media: &HashMap<String, Arc<Medium>>,
|
|
shape_lights: &AreaLightMap,
|
|
) -> Vec<Primitive> {
|
|
// 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<Ptr<Shape>>,
|
|
textures: &NamedTextures,
|
|
named_materials: &HashMap<String, MaterialIdx>,
|
|
materials: &[Material],
|
|
media: &HashMap<String, Arc<Medium>>,
|
|
) -> Vec<Primitive> {
|
|
// TODO: GPU wavefront path — animated shape upload
|
|
let _ = (
|
|
arena,
|
|
entities,
|
|
loaded,
|
|
textures,
|
|
named_materials,
|
|
materials,
|
|
media,
|
|
);
|
|
Vec::new()
|
|
}
|
|
}
|