diff --git a/.gitignore b/.gitignore index 79766be..13d16e2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ tests/ *.spv *.json *.txt +scenes/ diff --git a/shared/src/utils/interval.rs b/shared/src/utils/interval.rs index b76733b..c8decfc 100644 --- a/shared/src/utils/interval.rs +++ b/shared/src/utils/interval.rs @@ -1,7 +1,7 @@ use crate::core::pbrt::Float; use crate::utils::math::{next_float_down, next_float_up}; -use num_traits::Zero; use core::ops::{Add, Div, Mul, Neg, Sub}; +use num_traits::Zero; #[repr(C)] #[derive(Debug, Copy, Clone, PartialEq)] diff --git a/src/cameras/perspective.rs b/src/cameras/perspective.rs index e69de29..8b13789 100644 --- a/src/cameras/perspective.rs +++ b/src/cameras/perspective.rs @@ -0,0 +1 @@ + diff --git a/src/cameras/spherical.rs b/src/cameras/spherical.rs index e69de29..8b13789 100644 --- a/src/cameras/spherical.rs +++ b/src/cameras/spherical.rs @@ -0,0 +1 @@ + diff --git a/src/core/light.rs b/src/core/light.rs index 295ce9f..c02abe9 100644 --- a/src/core/light.rs +++ b/src/core/light.rs @@ -19,123 +19,118 @@ pub fn lookup_spectrum(s: &Spectrum) -> Arc { cache.lookup(dense_spectrum).into() } -pub trait CreateLight { - fn create( - render_from_light: Transform, - medium: Medium, - parameters: &ParameterDictionary, - loc: &FileLoc, - shape: &Shape, - alpha_text: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - arena: &Arena, - ) -> Result; +// Placeholders for non-area lights that never inspect these arguments. +// TODO: refactor each light's create to only take what it actually needs, +// then delete these. +fn dummy_shape() -> Shape { + Shape::default() } -pub trait LightFactory { - fn create( - name: &str, - arena: &mut Arena, - render_from_light: Transform, - medium: Medium, - parameters: &ParameterDictionary, - loc: &FileLoc, - shape: &Shape, - alpha_tex: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - camera_transform: CameraTransform, - ) -> Result - where - Self: Sized; +fn dummy_alpha() -> FloatTexture { + FloatTexture::default() } -impl LightFactory for Light { - fn create( - name: &str, - arena: &mut Arena, - render_from_light: Transform, - medium: Medium, - parameters: &ParameterDictionary, - loc: &FileLoc, - shape: &Shape, - alpha_tex: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - camera_transform: CameraTransform, - ) -> Result - where - Self: Sized, - { - match name { - "diffuse" => DiffuseAreaLight::create( - render_from_light, - medium, - parameters, - loc, - shape, - alpha_tex, - colorspace, - arena, - ), - "point" => PointLight::create( - render_from_light, - medium, - parameters, - loc, - shape, - alpha_tex, - colorspace, - arena, - ), - "spot" => SpotLight::create( - render_from_light, - medium, - parameters, - loc, - shape, - alpha_tex, - colorspace, - arena, - ), - "goniometric" => GoniometricLight::create( - render_from_light, - medium, - parameters, - loc, - shape, - alpha_tex, - colorspace, - arena, - ), - "projection" => ProjectionLight::create( - render_from_light, - medium, - parameters, - loc, - shape, - alpha_tex, - colorspace, - arena, - ), - "distant" => DistantLight::create( - render_from_light, - medium, - parameters, - loc, - shape, - alpha_tex, - colorspace, - arena, - ), - "infinite" => crate::lights::infinite::create( - render_from_light, - medium.into(), - camera_transform, - parameters, - colorspace, - loc, - arena, - ), - _ => Err(anyhow!("{}: unknown light type: \"{}\"", loc, name)), - } +/// Create a non-area light from a scene file directive. +pub fn create_light( + name: &str, + render_from_light: Transform, + medium: Option, + parameters: &ParameterDictionary, + loc: &FileLoc, + camera_transform: CameraTransform, + arena: &mut Arena, +) -> Result { + let shape = dummy_shape(); + let alpha = dummy_alpha(); + + match name { + "point" => crate::lights::point::create( + render_from_light, + medium, + parameters, + loc, + &shape, + &alpha, + None, + arena, + ), + "spot" => crate::lights::spot::create( + render_from_light, + medium, + parameters, + loc, + &shape, + &alpha, + None, + arena, + ), + "distant" => crate::lights::distant::create( + render_from_light, + medium, + parameters, + loc, + &shape, + &alpha, + None, + arena, + ), + "goniometric" => crate::lights::goniometric::create( + render_from_light, + medium, + parameters, + loc, + &shape, + &alpha, + None, + arena, + ), + "projection" => crate::lights::projection::create( + render_from_light, + medium, + parameters, + loc, + &shape, + &alpha, + None, + arena, + ), + "infinite" => crate::lights::infinite::create( + render_from_light, + medium.into(), + camera_transform, + parameters, + None, + loc, + arena, + ), + "diffuse" => Err(anyhow!( + "{}: \"diffuse\" is an area light; use create_area_light with a shape", + loc + )), + _ => Err(anyhow!("{}: unknown light type \"{}\"", loc, name)), } } + +/// Create a diffuse area light bound to a specific shape. +/// Called once per sub-shape (e.g. once per triangle in a mesh). +pub fn create_area_light( + render_from_light: Transform, + medium: Option, + parameters: &ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_tex: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + arena: &mut Arena, +) -> Result { + crate::lights::diffuse::create( + render_from_light, + medium, + parameters, + loc, + shape, + alpha_tex, + colorspace, + arena, + ) +} diff --git a/src/core/material.rs b/src/core/material.rs index 20118b4..f0ba65d 100644 --- a/src/core/material.rs +++ b/src/core/material.rs @@ -39,7 +39,10 @@ impl MaterialFactory for Material { named_materials: &HashMap, loc: FileLoc, arena: &Arena, - ) -> Result where Self: Sized { + ) -> Result + where + Self: Sized, + { match name { "diffuse" => { DiffuseMaterial::create(parameters, normal_map, named_materials, &loc, arena) diff --git a/src/core/scene/builder.rs b/src/core/scene/builder.rs index cb7320a..2e6a6a9 100644 --- a/src/core/scene/builder.rs +++ b/src/core/scene/builder.rs @@ -641,7 +641,7 @@ impl ParserTarget for BasicSceneBuilder { arena: Arc, ) -> Result<(), ParserError> { let name = normalize_utf8(orig_name); - self.verify_world("Texture", &loc); + self.verify_world("Texture", &loc)?; let dict = ParameterDictionary::from_array( params.clone(), &self.graphics_state.texture_attributes, @@ -701,7 +701,7 @@ impl ParserTarget for BasicSceneBuilder { params: ParsedParameterVector, loc: FileLoc, ) -> Result<(), ParserError> { - self.verify_world("material", &loc); + self.verify_world("material", &loc)?; let entity = SceneEntity { name: name.to_string(), loc, @@ -733,9 +733,9 @@ impl ParserTarget for BasicSceneBuilder { let dict = ParameterDictionary::from_array( params.clone(), &self.graphics_state.medium_attributes, - self.graphics_state.color_space.clone() + self.graphics_state.color_space.clone(), )?; - + let render_from_light = AnimatedTransform::new( &self.graphics_state.ctm.t[0], self.graphics_state.transform_start_time, @@ -757,7 +757,6 @@ impl ParserTarget for BasicSceneBuilder { self.scene.add_light(entity); Ok(()) - } fn area_light_source( @@ -786,7 +785,7 @@ impl ParserTarget for BasicSceneBuilder { let dict = ParameterDictionary::from_array( params.clone(), &self.graphics_state.shape_attributes, - self.graphics_state.color_space.clone() + self.graphics_state.color_space.clone(), )?; let render_from_object = self.graphics_state.ctm[0]; @@ -797,7 +796,7 @@ impl ParserTarget for BasicSceneBuilder { let light_entity = SceneEntity { name: al.name.clone(), loc: al.loc.clone(), - parameters: al_dict + parameters: al_dict, }; Some(self.scene.add_area_light(light_entity)) } else { @@ -816,7 +815,7 @@ impl ParserTarget for BasicSceneBuilder { base: SceneEntity { name: name.to_string(), loc, - parameters: dict + parameters: dict, }, render_from_object: Arc::new(render_from_object), object_from_render: Arc::new(object_from_render), @@ -828,7 +827,11 @@ impl ParserTarget for BasicSceneBuilder { }; if self.active_instance_definition.is_some() { - self.active_instance_definition.as_mut().unwrap().shapes.push(entity) + self.active_instance_definition + .as_mut() + .unwrap() + .shapes + .push(entity) } else { self.scene.add_shape(entity); } diff --git a/src/core/scene/mod.rs b/src/core/scene/mod.rs index 9b00488..7b89973 100644 --- a/src/core/scene/mod.rs +++ b/src/core/scene/mod.rs @@ -1,9 +1,11 @@ -mod builder; -mod entities; -mod scene; -mod state; +pub mod builder; +pub mod entities; +pub mod scene; +pub mod state; pub use builder::BasicSceneBuilder; pub use entities::*; pub use scene::{BasicScene, SceneLookup}; pub use state::*; + + diff --git a/src/core/scene/scene.rs b/src/core/scene/scene.rs index 6ca9fda..fd1198f 100644 --- a/src/core/scene/scene.rs +++ b/src/core/scene/scene.rs @@ -4,7 +4,6 @@ use crate::core::camera::CameraFactory; use crate::core::film::FilmFactory; use crate::core::filter::FilterFactory; use crate::core::image::{Image, io::ImageIO}; -use crate::core::light::LightFactory; use crate::core::material::MaterialFactory; use crate::core::primitive::{CreateGeometricPrimitive, CreateSimplePrimitive}; use crate::core::sampler::SamplerFactory; @@ -17,7 +16,7 @@ use crate::{Arena, FileLoc}; use anyhow::{Result, anyhow}; use parking_lot::Mutex; use rayon::prelude::*; -use shared::core::camera::{CameraTransform, Camera}; +use shared::core::camera::{Camera, CameraTransform}; use shared::core::color::LINEAR; use shared::core::film::Film; use shared::core::filter::Filter; @@ -502,26 +501,114 @@ impl BasicScene { &self, camera_transform: &CameraTransform, arena: &mut Arena, - ) -> Result> { + ) -> Vec { let state = self.light_state.lock(); - state.lights.par_iter().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, + 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 (one per sub-shape, e.g. per triangle). + /// 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, ); - Light::create( - &entity.transformed_base.base.name, - &entity.transformed_base.base.parameters, - render_from_light, - camera_transform, - medium.map(|m| *m), - &entity.transformed_base.base.loc, - arena, - ) - }).collect() + // 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_tex + .as_ref() + .expect("Alpha texture required for area light"), + 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( diff --git a/src/core/scene/state.rs b/src/core/scene/state.rs index 68f161e..76c5617 100644 --- a/src/core/scene/state.rs +++ b/src/core/scene/state.rs @@ -1,4 +1,4 @@ -use super::{SceneEntity, TextureSceneEntity, LightSceneEntity}; +use super::{LightSceneEntity, SceneEntity, TextureSceneEntity}; use crate::core::image::Image; use crate::core::texture::{FloatTexture, SpectrumTexture}; use crate::utils::parallel::AsyncJob; diff --git a/src/core/texture.rs b/src/core/texture.rs index 2349856..40e709a 100644 --- a/src/core/texture.rs +++ b/src/core/texture.rs @@ -45,6 +45,12 @@ pub enum FloatTexture { Wrinkled(WrinkledTexture), } +impl Default for FloatTexture { + fn default() -> Self { + FloatTexture::Constant(FloatConstantTexture::new(1.0)) + } +} + impl FloatTextureTrait for Arc { fn evaluate(&self, ctx: &TextureEvalContext) -> Float { self.as_ref().evaluate(ctx) diff --git a/src/films/rgb.rs b/src/films/rgb.rs index fb8b20b..2a568e1 100644 --- a/src/films/rgb.rs +++ b/src/films/rgb.rs @@ -1,7 +1,8 @@ use super::*; -use crate::Arena; use crate::core::film::{CreateFilmBase, PixelSensor}; use crate::utils::containers::Array2D; +use std::sync::Arc; +use crate::Arena; use anyhow::Result; use shared::core::camera::CameraTransform; use shared::core::film::{Film, FilmBase, RGBFilm, RGBPixel}; @@ -69,7 +70,10 @@ impl CreateFilm for RGBFilm { loc: &FileLoc, _arena: &Arena, ) -> Result { - let colorspace = params.color_space.as_ref().unwrap(); + let colorspace = params.color_space.as_ref().cloned().unwrap_or_else(|| { + let stdcs = crate::spectra::get_colorspace_device(); + Arc::new(*stdcs.srgb) + }); let max_component_value = params.get_one_float("maxcomponentvalue", Float::INFINITY)?; let write_fp16 = params.get_one_bool("savefp16", true)?; let sensor = PixelSensor::create(params, colorspace.clone(), exposure_time, loc)?; diff --git a/src/integrators/constants.rs b/src/integrators/constants.rs index 8957c6f..2a60f3e 100644 --- a/src/integrators/constants.rs +++ b/src/integrators/constants.rs @@ -23,20 +23,52 @@ pub static UC_RHO: [Float; N_RHO_SAMPLES] = [ ]; pub static U_RHO: [Point2f; N_RHO_SAMPLES] = [ - Point2f { 0: [0.855985, 0.570367]}, - Point2f { 0: [0.381823, 0.851844]}, - Point2f { 0: [0.285328, 0.764262]}, - Point2f { 0: [0.733380, 0.114073]}, - Point2f { 0: [0.542663, 0.344465]}, - Point2f { 0: [0.127274, 0.414848]}, - Point2f { 0: [0.964700, 0.947162]}, - Point2f { 0: [0.594089, 0.643463]}, - Point2f { 0: [0.095109, 0.170369]}, - Point2f { 0: [0.825444, 0.263359]}, - Point2f { 0: [0.429467, 0.454469]}, - Point2f { 0: [0.244460, 0.816459]}, - Point2f { 0: [0.756135, 0.731258]}, - Point2f { 0: [0.516165, 0.152852]}, - Point2f { 0: [0.180888, 0.214174]}, - Point2f { 0: [0.898579, 0.503897]}, + Point2f { + 0: [0.855985, 0.570367], + }, + Point2f { + 0: [0.381823, 0.851844], + }, + Point2f { + 0: [0.285328, 0.764262], + }, + Point2f { + 0: [0.733380, 0.114073], + }, + Point2f { + 0: [0.542663, 0.344465], + }, + Point2f { + 0: [0.127274, 0.414848], + }, + Point2f { + 0: [0.964700, 0.947162], + }, + Point2f { + 0: [0.594089, 0.643463], + }, + Point2f { + 0: [0.095109, 0.170369], + }, + Point2f { + 0: [0.825444, 0.263359], + }, + Point2f { + 0: [0.429467, 0.454469], + }, + Point2f { + 0: [0.244460, 0.816459], + }, + Point2f { + 0: [0.756135, 0.731258], + }, + Point2f { + 0: [0.516165, 0.152852], + }, + Point2f { + 0: [0.180888, 0.214174], + }, + Point2f { + 0: [0.898579, 0.503897], + }, ]; diff --git a/src/integrators/path.rs b/src/integrators/path.rs index d62556c..5b5fe19 100644 --- a/src/integrators/path.rs +++ b/src/integrators/path.rs @@ -1,10 +1,10 @@ +use super::RayIntegratorTrait; use super::base::IntegratorBase; use super::constants::*; use super::state::PathState; -use super::RayIntegratorTrait; -use crate::core::interaction::InteractionGetter; use crate::Arena; -use shared::core::bsdf::{BSDFSample, BSDF}; +use crate::core::interaction::InteractionGetter; +use shared::core::bsdf::{BSDF, BSDFSample}; use shared::core::bxdf::{BxDFFlags, FArgs, TransportMode}; use shared::core::camera::Camera; use shared::core::film::VisibleSurface; diff --git a/src/lights/diffuse.rs b/src/lights/diffuse.rs index c53f339..076d479 100644 --- a/src/lights/diffuse.rs +++ b/src/lights/diffuse.rs @@ -1,14 +1,14 @@ use std::path::Path; use crate::core::image::{Image, ImageIO}; -use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::light::lookup_spectrum; use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::FloatTexture; use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename}; use anyhow::{Result, anyhow}; use shared::core::geometry::Point2i; use shared::core::light::{Light, LightBase, LightType}; -use shared::core::medium::Medium; +use shared::core::medium::{Medium, MediumInterface}; use shared::core::shape::{Shape, ShapeTrait}; use shared::core::spectrum::Spectrum; use shared::core::texture::GPUFloatTexture; @@ -18,147 +18,155 @@ use shared::spectra::RGBColorSpace; use shared::utils::{Ptr, Transform}; use shared::{Float, PI}; -impl CreateLight for DiffuseAreaLight { - fn create( - render_from_light: Transform, - medium: Medium, - params: &ParameterDictionary, - loc: &FileLoc, - shape: &Shape, - alpha: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - arena: &Arena, - ) -> Result { - let mut l = params.get_one_spectrum("l", None, SpectrumType::Illuminant); - let illum_spec = Spectrum::Dense(colorspace.unwrap().illuminant); - let mut scale = params.get_one_float("scale", 1.)?; - let two_sided = params.get_one_bool("twosided", false)?; +pub fn create( + render_from_light: Transform, + medium: Option, + params: &ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + arena: &Arena, +) -> Result { + let mut l = params.get_one_spectrum("l", None, SpectrumType::Illuminant); + let illum_spec = Spectrum::Dense(colorspace.unwrap().illuminant); + let mut scale = params.get_one_float("scale", 1.)?; + let two_sided = params.get_one_bool("twosided", false)?; - let filename = resolve_filename(¶ms.get_one_string("filename", "")?); - let (image, image_color_space) = if !filename.is_empty() { - if l.is_some() { - return Err(anyhow!("{}: both \"L\" and \"filename\" specified", loc)); - } - - let im = Image::read(Path::new(&filename), None)?; - - if im.image.has_any_infinite_pixels() { - return Err(anyhow!("{}: image has infinite pixel values", loc)); - } - if im.image.has_any_nan_pixels() { - return Err(anyhow!("{}: image has NaN pixel values", loc)); - } - - let channel_desc = im - .image - .get_channel_desc(&["R", "G", "B"]) - .map_err(|_| anyhow!("{}: image must have R, G, B channels", loc))?; - - let image = im.image.select_channels(&channel_desc); - let cs = im.metadata.get_colorspace(); - - (Some(image), cs) - } else { - if l.is_none() { - l = Some(illum_spec); - } - (None, None) - }; - - let l_for_scale = l.as_ref().unwrap_or(&illum_spec); - scale /= spectrum_to_photometric(*l_for_scale); - - let phi_v = params.get_one_float("power", -1.0)?; - if phi_v > 0.0 { - // k_e is the emissive power of the light as defined by the spectral - // distribution and texture and is used to normalize the emitted - // radiance such that the user-defined power will be the actual power - // emitted by the light. - - let mut k_e: Float = 1.0; - - if let Some(ref img) = image { - // Get the appropriate luminance vector from the image colour space - let lum_vec = image_color_space.unwrap().luminance_vector(); - - let mut sum_k_e = 0.0; - let res = img.resolution(); - - for y in 0..res.y() { - for x in 0..res.x() { - let r = img.get_channel(Point2i::new(x, y), 0); - let g = img.get_channel(Point2i::new(x, y), 1); - let b = img.get_channel(Point2i::new(x, y), 2); - - sum_k_e += r * lum_vec[0] + g * lum_vec[1] + b * lum_vec[2]; - } - } - k_e = sum_k_e / (res.x() * res.y()) as Float; - } - - let side_factor = if two_sided { 2.0 } else { 1.0 }; - k_e *= side_factor * shape.area() * PI; - - // now multiply up scale to hit the target power - scale *= phi_v / k_e; + let filename = resolve_filename(¶ms.get_one_string("filename", "")?); + let (image, image_color_space) = if !filename.is_empty() { + if l.is_some() { + return Err(anyhow!("{}: both \"L\" and \"filename\" specified", loc)); } - let alpha_ptr = alpha.upload(arena); - let is_constant_zero = match &*alpha_ptr { - GPUFloatTexture::Constant(tex) => tex.evaluate(&TextureEvalContext::default()) == 0.0, - _ => false, - }; + let im = Image::read(Path::new(&filename), None)?; - let (light_type, stored_alpha) = if is_constant_zero { - (LightType::DeltaPosition, None) - } else { - (LightType::Area, Some(alpha)) - }; + if im.image.has_any_infinite_pixels() { + return Err(anyhow!("{}: image has infinite pixel values", loc)); + } + if im.image.has_any_nan_pixels() { + return Err(anyhow!("{}: image has NaN pixel values", loc)); + } + + let channel_desc = im + .image + .get_channel_desc(&["R", "G", "B"]) + .map_err(|_| anyhow!("{}: image must have R, G, B channels", loc))?; + + let image = im.image.select_channels(&channel_desc); + let cs = im.metadata.get_colorspace(); + + (Some(image), cs) + } else { + if l.is_none() { + l = Some(illum_spec); + } + (None, None) + }; + + let l_for_scale = l.as_ref().unwrap_or(&illum_spec); + scale /= spectrum_to_photometric(*l_for_scale); + + let phi_v = params.get_one_float("power", -1.0)?; + if phi_v > 0.0 { + // k_e is the emissive power of the light as defined by the spectral + // distribution and texture and is used to normalize the emitted + // radiance such that the user-defined power will be the actual power + // emitted by the light. + + let mut k_e: Float = 1.0; - let base = LightBase::new(light_type, render_from_light, medium.into()); if let Some(ref img) = image { - let desc = img - .get_channel_desc(&["R", "G", "B"]) - .expect("Image used for DiffuseAreaLight doesn't have R, G, B channels"); - assert_eq!(3, desc.size()); - assert!(desc.is_identity()); - assert!( - image_color_space.is_some(), - "Image provided but ColorSpace is missing" - ); + // Get the appropriate luminance vector from the image colour space + let lum_vec = image_color_space.unwrap().luminance_vector(); + + let mut sum_k_e = 0.0; + let res = img.resolution(); + + for y in 0..res.y() { + for x in 0..res.x() { + let r = img.get_channel(Point2i::new(x, y), 0); + let g = img.get_channel(Point2i::new(x, y), 1); + let b = img.get_channel(Point2i::new(x, y), 2); + + sum_k_e += r * lum_vec[0] + g * lum_vec[1] + b * lum_vec[2]; + } + } + k_e = sum_k_e / (res.x() * res.y()) as Float; } - let is_triangle_or_bilinear = - matches!(*shape, Shape::Triangle(_) | Shape::BilinearPatch(_)); - if render_from_light.has_scale(None) && !is_triangle_or_bilinear { - println!( - "Scaling detected in rendering to light space transformation! \ - Proceed at your own risk; your image may have errors." - ); - } + let side_factor = if two_sided { 2.0 } else { 1.0 }; + k_e *= side_factor * shape.area() * PI; - let shape_ptr = shape.upload(arena); - let image_ptr = image - .as_ref() - .map(|img| arena.alloc(*img.device())) - .unwrap_or(Ptr::null()); - let colorspace_ptr = image_color_space - .map(|cs| cs.upload(arena)) - .unwrap_or(Ptr::null()); - let lemit_ptr = arena.alloc(lookup_spectrum(l_for_scale).device()); - - let specific = DiffuseAreaLight { - base, - area: shape.area(), - shape: shape_ptr, - alpha: alpha_ptr, - image: image_ptr, - colorspace: colorspace_ptr, - lemit: lemit_ptr, - two_sided, - scale, - }; - - Ok(Light::DiffuseArea(specific)) + // now multiply up scale to hit the target power + scale *= phi_v / k_e; } + + let alpha_ptr = alpha.upload(arena); + let is_constant_zero = match &*alpha_ptr { + GPUFloatTexture::Constant(tex) => tex.evaluate(&TextureEvalContext::default()) == 0.0, + _ => false, + }; + + let (light_type, stored_alpha) = if is_constant_zero { + (LightType::DeltaPosition, None) + } else { + (LightType::Area, Some(alpha)) + }; + + let mi = match medium { + Some(m) => { + let ptr = arena.alloc(m); + MediumInterface { + inside: ptr, + outside: ptr, + } + } + None => MediumInterface::default(), + }; + let base = LightBase::new(light_type, render_from_light, mi); + + if let Some(ref img) = image { + let desc = img + .get_channel_desc(&["R", "G", "B"]) + .expect("Image used for DiffuseAreaLight doesn't have R, G, B channels"); + assert_eq!(3, desc.size()); + assert!(desc.is_identity()); + assert!( + image_color_space.is_some(), + "Image provided but ColorSpace is missing" + ); + } + + let is_triangle_or_bilinear = matches!(*shape, Shape::Triangle(_) | Shape::BilinearPatch(_)); + if render_from_light.has_scale(None) && !is_triangle_or_bilinear { + println!( + "Scaling detected in rendering to light space transformation! \ + Proceed at your own risk; your image may have errors." + ); + } + + let shape_ptr = shape.upload(arena); + let image_ptr = image + .as_ref() + .map(|img| arena.alloc(*img.device())) + .unwrap_or(Ptr::null()); + let colorspace_ptr = image_color_space + .map(|cs| cs.upload(arena)) + .unwrap_or(Ptr::null()); + let lemit_ptr = arena.alloc(lookup_spectrum(l_for_scale).device()); + + let specific = DiffuseAreaLight { + base, + area: shape.area(), + shape: shape_ptr, + alpha: alpha_ptr, + image: image_ptr, + colorspace: colorspace_ptr, + lemit: lemit_ptr, + two_sided, + scale, + }; + + Ok(Light::DiffuseArea(specific)) } diff --git a/src/lights/distant.rs b/src/lights/distant.rs index 5dc67cd..25bf957 100644 --- a/src/lights/distant.rs +++ b/src/lights/distant.rs @@ -1,4 +1,4 @@ -use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::light::lookup_spectrum; use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::FloatTexture; use crate::utils::{Arena, FileLoc, ParameterDictionary}; @@ -36,59 +36,57 @@ impl CreateDistantLight for DistantLight { } } -impl CreateLight for DistantLight { - fn create( - render_from_light: Transform, - _medium: Medium, - parameters: &ParameterDictionary, - _loc: &FileLoc, - _shape: &Shape, - _alpha_text: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - _arena: &Arena, - ) -> Result { - let l = parameters - .get_one_spectrum( - "L", - Some(Spectrum::Dense(colorspace.unwrap().illuminant)), - SpectrumType::Illuminant, - ) - .unwrap(); - let mut scale = parameters.get_one_float("scale", 1.)?; +pub fn create( + render_from_light: Transform, + _medium: Option, + parameters: &ParameterDictionary, + _loc: &FileLoc, + _shape: &Shape, + _alpha_text: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + _arena: &Arena, +) -> Result { + let l = parameters + .get_one_spectrum( + "L", + Some(Spectrum::Dense(colorspace.unwrap().illuminant)), + SpectrumType::Illuminant, + ) + .unwrap(); + let mut scale = parameters.get_one_float("scale", 1.)?; - let from = parameters.get_one_point3f("from", Point3f::new(0., 0., 0.))?; - let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?; - let w = (from - to).normalize(); - let (v1, v2) = w.coordinate_system(); - let m: [Float; 16] = [ - v1.x(), - v2.x(), - w.x(), - 0., - v1.y(), - v2.y(), - w.y(), - 0., - v1.z(), - v2.z(), - w.z(), - 0., - 0., - 0., - 0., - 1., - ]; - let t = Transform::from_flat(&m).expect("Could not create transform for DistantLight"); - let final_render = render_from_light * t; - scale /= spectrum_to_photometric(l); - // Adjust scale to meet target illuminance value - let e_v = parameters.get_one_float("illuminance", -1.)?; - if e_v > 0. { - scale *= e_v; - } - - let specific = DistantLight::new(final_render, l, scale); - - Ok(Light::Distant(specific)) + let from = parameters.get_one_point3f("from", Point3f::new(0., 0., 0.))?; + let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?; + let w = (from - to).normalize(); + let (v1, v2) = w.coordinate_system(); + let m: [Float; 16] = [ + v1.x(), + v2.x(), + w.x(), + 0., + v1.y(), + v2.y(), + w.y(), + 0., + v1.z(), + v2.z(), + w.z(), + 0., + 0., + 0., + 0., + 1., + ]; + let t = Transform::from_flat(&m).expect("Could not create transform for DistantLight"); + let final_render = render_from_light * t; + scale /= spectrum_to_photometric(l); + // Adjust scale to meet target illuminance value + let e_v = parameters.get_one_float("illuminance", -1.)?; + if e_v > 0. { + scale *= e_v; } + + let specific = DistantLight::new(final_render, l, scale); + + Ok(Light::Distant(specific)) } diff --git a/src/lights/goniometric.rs b/src/lights/goniometric.rs index 72c3796..1da2b84 100644 --- a/src/lights/goniometric.rs +++ b/src/lights/goniometric.rs @@ -1,7 +1,7 @@ use std::path::Path; use crate::core::image::{Image, ImageIO}; -use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::light::lookup_spectrum; use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::FloatTexture; use crate::utils::sampling::PiecewiseConstant2D; @@ -9,7 +9,7 @@ use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename use anyhow::{Result, anyhow}; use shared::core::geometry::Point2i; use shared::core::light::{Light, LightBase, LightType}; -use shared::core::medium::Medium; +use shared::core::medium::{Medium, MediumInterface}; use shared::core::shape::Shape; use shared::core::spectrum::Spectrum; use shared::core::texture::SpectrumType; @@ -18,95 +18,99 @@ use shared::spectra::RGBColorSpace; use shared::utils::{Ptr, Transform}; use shared::{Float, PI}; -impl CreateLight for GoniometricLight { - fn create( - render_from_light: Transform, - medium: Medium, - params: &ParameterDictionary, - loc: &FileLoc, - _shape: &Shape, - _alpha_text: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - arena: &Arena, - ) -> Result { - let i = params - .get_one_spectrum( - "I", - Some(Spectrum::Dense(colorspace.unwrap().illuminant)), - SpectrumType::Illuminant, - ) - .expect("Could not retrieve spectrum"); - let mut scale = params.get_one_float("scale", 1.)?; - let filename = resolve_filename(¶ms.get_one_string("filename", "")?); - let image: Ptr = if filename.is_empty() { - Ptr::null() - } else { - let im = Image::read(Path::new(&filename), None) - .map_err(|e| anyhow!("could not load image '{}': {}", filename, e))?; +pub fn create( + render_from_light: Transform, + medium: Option, + params: &ParameterDictionary, + loc: &FileLoc, + _shape: &Shape, + _alpha_text: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + arena: &Arena, +) -> Result { + let i = params + .get_one_spectrum( + "I", + Some(Spectrum::Dense(colorspace.unwrap().illuminant)), + SpectrumType::Illuminant, + ) + .expect("Could not retrieve spectrum"); + let mut scale = params.get_one_float("scale", 1.)?; + let filename = resolve_filename(¶ms.get_one_string("filename", "")?); + let image: Ptr = if filename.is_empty() { + Ptr::null() + } else { + let im = Image::read(Path::new(&filename), None) + .map_err(|e| anyhow!("could not load image '{}': {}", filename, e))?; - let loaded = im.image; - let res = loaded.resolution(); + let loaded = im.image; + let res = loaded.resolution(); - if loaded.has_any_infinite_pixels() { - return Err(anyhow!( - "image '{}' has infinite pixels, not suitable for light", - filename - )); - } - - if res.x() != res.y() { - return Err(anyhow!( - "image resolution ({}, {}) is non-square; unlikely to be an equal-area map", - res.x(), - res.y() - )); - } - - Ptr::from(&convert_to_luminance_image(&loaded, &filename, loc)?) - }; - - scale /= spectrum_to_photometric(i); - let phi_v = params.get_one_float("power", -1.0)?; - - if phi_v > 0.0 { - let k_e = compute_emissive_power(&image); - scale *= phi_v / k_e; + if loaded.has_any_infinite_pixels() { + return Err(anyhow!( + "image '{}' has infinite pixels, not suitable for light", + filename + )); } - let swap_yz: [Float; 16] = [ - 1., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., - ]; - let t = Transform::from_flat(&swap_yz) - .expect("Could not create transform for GoniometricLight"); - let final_render_from_light = render_from_light * t; + if res.x() != res.y() { + return Err(anyhow!( + "image resolution ({}, {}) is non-square; unlikely to be an equal-area map", + res.x(), + res.y() + )); + } - let base = LightBase::new( - LightType::DeltaPosition, - final_render_from_light, - medium.into(), - ); + Ptr::from(&convert_to_luminance_image(&loaded, &filename, loc)?) + }; - let iemit = lookup_spectrum(&i); + scale /= spectrum_to_photometric(i); + let phi_v = params.get_one_float("power", -1.0)?; - let image_ptr = if !image.is_null() { - let distrib = PiecewiseConstant2D::from_image(&image); - let distrib_ptr = distrib.upload(arena); - let img_ptr = image.upload(arena); - (img_ptr, distrib_ptr) - } else { - (Ptr::null(), Ptr::null()) - }; - - let specific = GoniometricLight { - base, - iemit: arena.alloc(iemit.device()), - scale, - image: image_ptr.0, - distrib: image_ptr.1, - }; - - Ok(Light::Goniometric(specific)) + if phi_v > 0.0 { + let k_e = compute_emissive_power(&image); + scale *= phi_v / k_e; } + + let swap_yz: [Float; 16] = [ + 1., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., + ]; + let t = + Transform::from_flat(&swap_yz).expect("Could not create transform for GoniometricLight"); + let final_render_from_light = render_from_light * t; + + let mi = match medium { + Some(m) => { + let ptr = arena.alloc(m); + MediumInterface { + inside: ptr, + outside: ptr, + } + } + None => MediumInterface::default(), + }; + let base = LightBase::new(LightType::DeltaPosition, render_from_light, mi); + + let iemit = lookup_spectrum(&i); + + let image_ptr = if !image.is_null() { + let distrib = PiecewiseConstant2D::from_image(&image); + let distrib_ptr = distrib.upload(arena); + let img_ptr = image.upload(arena); + (img_ptr, distrib_ptr) + } else { + (Ptr::null(), Ptr::null()) + }; + + let specific = GoniometricLight { + base, + iemit: arena.alloc(iemit.device()), + scale, + image: image_ptr.0, + distrib: image_ptr.1, + }; + + Ok(Light::Goniometric(specific)) } fn convert_to_luminance_image(image: &Image, filename: &str, loc: &FileLoc) -> Result { diff --git a/src/lights/infinite.rs b/src/lights/infinite.rs index fef0422..cc62b55 100644 --- a/src/lights/infinite.rs +++ b/src/lights/infinite.rs @@ -11,7 +11,7 @@ use shared::core::camera::CameraTransform; use shared::core::geometry::{Bounds2f, Frame, Point2f, Point2i, Point3f, VectorLike, cos_theta}; use shared::core::image::{DeviceImage, WrapMode}; use shared::core::light::{Light, LightBase, LightType}; -use shared::core::medium::MediumInterface; +use shared::core::medium::{Medium, MediumInterface}; use shared::core::spectrum::Spectrum; use shared::core::texture::SpectrumType; use shared::lights::{ImageInfiniteLight, PortalInfiniteLight, UniformInfiniteLight}; @@ -124,7 +124,7 @@ impl CreateUniformInfiniteLight for UniformInfiniteLight { pub fn create( render_from_light: Transform, - _medium: MediumInterface, + _medium: Option, camera_transform: CameraTransform, parameters: &ParameterDictionary, colorspace: Option<&RGBColorSpace>, diff --git a/src/lights/point.rs b/src/lights/point.rs index 5875b9d..9fc642e 100644 --- a/src/lights/point.rs +++ b/src/lights/point.rs @@ -1,4 +1,4 @@ -use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::light::lookup_spectrum; use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::FloatTexture; use crate::utils::{Arena, FileLoc, ParameterDictionary}; @@ -42,36 +42,46 @@ impl CreatePointLight for PointLight { } } -impl CreateLight for PointLight { - fn create( - render_from_light: Transform, - medium: Medium, - parameters: &ParameterDictionary, - _loc: &FileLoc, - _shape: &Shape, - _alpha: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - _arena: &Arena, - ) -> Result { - let l = parameters - .get_one_spectrum( - "L", - Some(Spectrum::Dense(colorspace.unwrap().illuminant)), - SpectrumType::Illuminant, - ) - .unwrap(); - let mut scale = parameters.get_one_float("scale", 1.)?; - scale /= spectrum_to_photometric(l); - let phi_v = parameters.get_one_float("power", 1.)?; - if phi_v > 0. { - let k_e = 4. * PI; - scale *= phi_v / k_e; - } - - let from = parameters.get_one_point3f("from", Point3f::zero())?; - let tf = Transform::translate(from.into()); - let final_render = render_from_light * tf; - let specific = PointLight::new(final_render, medium.into(), l, scale); - Ok(Light::Point(specific)) +pub fn create( + render_from_light: Transform, + medium: Option, + parameters: &ParameterDictionary, + _loc: &FileLoc, + _shape: &Shape, + _alpha: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + arena: &Arena, +) -> Result { + let l = parameters + .get_one_spectrum( + "L", + Some(Spectrum::Dense(colorspace.unwrap().illuminant)), + SpectrumType::Illuminant, + ) + .unwrap(); + let mut scale = parameters.get_one_float("scale", 1.)?; + scale /= spectrum_to_photometric(l); + let phi_v = parameters.get_one_float("power", 1.)?; + if phi_v > 0. { + let k_e = 4. * PI; + scale *= phi_v / k_e; } + + let from = parameters.get_one_point3f("from", Point3f::zero())?; + let tf = Transform::translate(from.into()); + let final_render = render_from_light * tf; + + let mi = match medium { + Some(m) => { + let ptr = arena.alloc(m); + MediumInterface { + inside: ptr, + outside: ptr, + } + } + None => MediumInterface::default(), + }; + + let specific = PointLight::new(final_render, mi, l, scale); + Ok(Light::Point(specific)) } diff --git a/src/lights/projection.rs b/src/lights/projection.rs index f10d1b0..522983f 100644 --- a/src/lights/projection.rs +++ b/src/lights/projection.rs @@ -1,5 +1,4 @@ use crate::core::image::{Image, ImageIO}; -use crate::core::light::CreateLight; use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::FloatTexture; use crate::utils::sampling::PiecewiseConstant2D; @@ -10,7 +9,7 @@ use shared::core::geometry::{ Bounds2f, Point2f, Point2i, Point3f, Vector3f, VectorLike, cos_theta, }; use shared::core::light::{Light, LightBase, LightType}; -use shared::core::medium::Medium; +use shared::core::medium::{Medium, MediumInterface}; use shared::core::shape::Shape; use shared::core::spectrum::Spectrum; use shared::lights::ProjectionLight; @@ -19,117 +18,124 @@ use shared::utils::Transform; use shared::utils::math::{radians, square}; use std::path::Path; -impl CreateLight for ProjectionLight { - fn create( - render_from_light: Transform, - medium: Medium, - parameters: &ParameterDictionary, - loc: &FileLoc, - _shape: &Shape, - _alpha_text: &FloatTexture, - _colorspace: Option<&RGBColorSpace>, - arena: &Arena, - ) -> Result { - let mut scale = parameters.get_one_float("scale", 1.)?; - let power = parameters.get_one_float("power", -1.)?; - let fov = parameters.get_one_float("fov", 90.)?; +pub fn create( + render_from_light: Transform, + medium: Option, + parameters: &ParameterDictionary, + loc: &FileLoc, + _shape: &Shape, + _alpha_text: &FloatTexture, + _colorspace: Option<&RGBColorSpace>, + arena: &Arena, +) -> Result { + let mut scale = parameters.get_one_float("scale", 1.)?; + let power = parameters.get_one_float("power", -1.)?; + let fov = parameters.get_one_float("fov", 90.)?; - let filename = resolve_filename(¶meters.get_one_string("filename", "")?); - if filename.is_empty() { - return Err(anyhow!( - "{}: must provide filename for projection light", - loc - )); - } - - let im = Image::read(Path::new(&filename), None) - .map_err(|e| anyhow!("{}: could not load image '{}': {}", loc, filename, e))?; - - if im.image.has_any_infinite_pixels() { - return Err(anyhow!( - "{}: image '{}' has infinite pixels, not suitable for light", - loc, - filename - )); - } - - if im.image.has_any_nan_pixels() { - return Err(anyhow!( - "{}: image '{}' has NaN pixels, not suitable for light", - loc, - filename - )); - } - - let channel_desc = im - .image - .get_channel_desc(&["R", "G", "B"]) - .map_err(|_| anyhow!("{}: image '{}' must have R, G, B channels", loc, filename))?; - - let image = im.image.select_channels(&channel_desc); - let colorspace = im - .metadata - .colorspace - .ok_or_else(|| anyhow!("{}: image '{}' missing colorspace metadata", loc, filename))?; - - scale /= spectrum_to_photometric(Spectrum::Dense(colorspace.illuminant)); - if power > 0. { - let k_e = compute_emissive_power(&image, &colorspace, fov); - scale /= k_e; - } - - let flip = Transform::scale(1., -1., 1.); - let render_from_light_flip = render_from_light * flip; - - let base = LightBase::new(LightType::DeltaPosition, render_from_light, medium.into()); - - let opposite = (radians(fov) / 2.0).tan(); - let res = image.resolution(); - let aspect = res.x() as Float / res.y() as Float; - let aspect_ratio = if aspect > 1.0 { aspect } else { 1.0 / aspect }; - let a = 4.0 * square(opposite) * aspect_ratio; - let screen_bounds = if aspect > 1.0 { - Bounds2f::from_points(Point2f::new(-aspect, -1.0), Point2f::new(aspect, 1.0)) - } else { - Bounds2f::from_points( - Point2f::new(-1.0, -1.0 / aspect), - Point2f::new(1.0, 1.0 / aspect), - ) - }; - - let hither = 1e-3; - let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap(); - let light_from_screen = screen_from_light.inverse(); - - let dwda = |p: Point2f| { - let w = - Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.0))); - cos_theta(w.normalize()).powi(3) - }; - - let d = image.get_sampling_distribution(dwda, screen_bounds); - let distrib = PiecewiseConstant2D::from_slice( - d.as_slice(), - d.x_size() as usize, - d.y_size() as usize, - screen_bounds, - ); - - let specific = ProjectionLight { - base, - image: image.upload(arena), - image_color_space: colorspace.upload(arena), - distrib: distrib.upload(arena), - screen_bounds, - screen_from_light, - light_from_screen, - scale, - hither, - a, - }; - - Ok(Light::Projection(specific)) + let filename = resolve_filename(¶meters.get_one_string("filename", "")?); + if filename.is_empty() { + return Err(anyhow!( + "{}: must provide filename for projection light", + loc + )); } + + let im = Image::read(Path::new(&filename), None) + .map_err(|e| anyhow!("{}: could not load image '{}': {}", loc, filename, e))?; + + if im.image.has_any_infinite_pixels() { + return Err(anyhow!( + "{}: image '{}' has infinite pixels, not suitable for light", + loc, + filename + )); + } + + if im.image.has_any_nan_pixels() { + return Err(anyhow!( + "{}: image '{}' has NaN pixels, not suitable for light", + loc, + filename + )); + } + + let channel_desc = im + .image + .get_channel_desc(&["R", "G", "B"]) + .map_err(|_| anyhow!("{}: image '{}' must have R, G, B channels", loc, filename))?; + + let image = im.image.select_channels(&channel_desc); + let colorspace = im + .metadata + .colorspace + .ok_or_else(|| anyhow!("{}: image '{}' missing colorspace metadata", loc, filename))?; + + scale /= spectrum_to_photometric(Spectrum::Dense(colorspace.illuminant)); + if power > 0. { + let k_e = compute_emissive_power(&image, &colorspace, fov); + scale /= k_e; + } + + let flip = Transform::scale(1., -1., 1.); + let render_from_light_flip = render_from_light * flip; + + let mi = match medium { + Some(m) => { + let ptr = arena.alloc(m); + MediumInterface { + inside: ptr, + outside: ptr, + } + } + None => MediumInterface::default(), + }; + let base = LightBase::new(LightType::DeltaPosition, render_from_light, mi); + + let opposite = (radians(fov) / 2.0).tan(); + let res = image.resolution(); + let aspect = res.x() as Float / res.y() as Float; + let aspect_ratio = if aspect > 1.0 { aspect } else { 1.0 / aspect }; + let a = 4.0 * square(opposite) * aspect_ratio; + let screen_bounds = if aspect > 1.0 { + Bounds2f::from_points(Point2f::new(-aspect, -1.0), Point2f::new(aspect, 1.0)) + } else { + Bounds2f::from_points( + Point2f::new(-1.0, -1.0 / aspect), + Point2f::new(1.0, 1.0 / aspect), + ) + }; + + let hither = 1e-3; + let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap(); + let light_from_screen = screen_from_light.inverse(); + + let dwda = |p: Point2f| { + let w = Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.0))); + cos_theta(w.normalize()).powi(3) + }; + + let d = image.get_sampling_distribution(dwda, screen_bounds); + let distrib = PiecewiseConstant2D::from_slice( + d.as_slice(), + d.x_size() as usize, + d.y_size() as usize, + screen_bounds, + ); + + let specific = ProjectionLight { + base, + image: image.upload(arena), + image_color_space: colorspace.upload(arena), + distrib: distrib.upload(arena), + screen_bounds, + screen_from_light, + light_from_screen, + scale, + hither, + a, + }; + + Ok(Light::Projection(specific)) } fn compute_screen_bounds(aspect: Float) -> Bounds2f { diff --git a/src/lights/spot.rs b/src/lights/spot.rs index 20607de..f9ad29d 100644 --- a/src/lights/spot.rs +++ b/src/lights/spot.rs @@ -1,4 +1,4 @@ -use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::light::lookup_spectrum; use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::FloatTexture; use crate::utils::{Arena, FileLoc, ParameterDictionary}; @@ -53,52 +53,53 @@ impl CreateSpotLight for SpotLight { } } -impl CreateLight for SpotLight { - fn create( - render_from_light: Transform, - medium: Medium, - parameters: &ParameterDictionary, - _loc: &FileLoc, - _shape: &Shape, - _alpha_tex: &FloatTexture, - colorspace: Option<&RGBColorSpace>, - arena: &Arena, - ) -> Result { - let i = parameters - .get_one_spectrum( - "I", - Some(Spectrum::Dense(colorspace.unwrap().illuminant)), - SpectrumType::Illuminant, - ) - .expect("No spectrum"); - let mut scale = parameters.get_one_float("scale", 1.)?; - let coneangle = parameters.get_one_float("coneangle", 30.)?; - let conedelta = parameters.get_one_float("conedelta", 5.)?; - let from = parameters.get_one_point3f("from", Point3f::zero())?; - let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?; - let dir_to_z = Transform::from(Frame::from_z((to - from).normalize())); - let t = Transform::translate(from.into()) * dir_to_z.inverse(); - let final_render = render_from_light * t; - scale /= spectrum_to_photometric(i); +pub fn create( + render_from_light: Transform, + medium: Option, + parameters: &ParameterDictionary, + _loc: &FileLoc, + _shape: &Shape, + _alpha_tex: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + arena: &Arena, +) -> Result { + let i = parameters + .get_one_spectrum( + "I", + Some(Spectrum::Dense(colorspace.unwrap().illuminant)), + SpectrumType::Illuminant, + ) + .expect("No spectrum"); + let mut scale = parameters.get_one_float("scale", 1.)?; + let coneangle = parameters.get_one_float("coneangle", 30.)?; + let conedelta = parameters.get_one_float("conedelta", 5.)?; + let from = parameters.get_one_point3f("from", Point3f::zero())?; + let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?; + let dir_to_z = Transform::from(Frame::from_z((to - from).normalize())); + let t = Transform::translate(from.into()) * dir_to_z.inverse(); + let final_render = render_from_light * t; + scale /= spectrum_to_photometric(i); - let phi_v = parameters.get_one_float("power", -1.)?; - if phi_v > 0. { - let cos_falloff_end = radians(coneangle).cos(); - let cos_falloff_start = radians(coneangle - conedelta).cos(); - let k_e = - 2. * PI * ((1. - cos_falloff_start) + (cos_falloff_start - cos_falloff_end) / 2.); - scale *= phi_v / k_e; - } - - let specific = SpotLight::new( - final_render, - medium.into(), - i, - scale, - coneangle, - coneangle - conedelta, - ); - arena.alloc(specific); - Ok(Light::Spot(specific)) + let phi_v = parameters.get_one_float("power", -1.)?; + if phi_v > 0. { + let cos_falloff_end = radians(coneangle).cos(); + let cos_falloff_start = radians(coneangle - conedelta).cos(); + let k_e = 2. * PI * ((1. - cos_falloff_start) + (cos_falloff_start - cos_falloff_end) / 2.); + scale *= phi_v / k_e; } + + let mi = match medium { + Some(m) => { + let ptr = arena.alloc(m); + MediumInterface { + inside: ptr, + outside: ptr, + } + } + None => MediumInterface::default(), + }; + + let specific = SpotLight::new(final_render, mi, i, scale, coneangle, coneangle - conedelta); + arena.alloc(specific); + Ok(Light::Spot(specific)) } diff --git a/src/materials/measured.rs b/src/materials/measured.rs index e69de29..8b13789 100644 --- a/src/materials/measured.rs +++ b/src/materials/measured.rs @@ -0,0 +1 @@ + diff --git a/src/textures/bilerp.rs b/src/textures/bilerp.rs index 7df099b..6e108fb 100644 --- a/src/textures/bilerp.rs +++ b/src/textures/bilerp.rs @@ -45,11 +45,7 @@ impl CreateSpectrumTexture for SpectrumBilerpTexture { } impl SpectrumTextureTrait for SpectrumBilerpTexture { - fn evaluate( - &self, - _ctx: &TextureEvalContext, - _lambda: &SampledWavelengths, - ) -> SampledSpectrum { + fn evaluate(&self, _ctx: &TextureEvalContext, _lambda: &SampledWavelengths) -> SampledSpectrum { todo!() } } diff --git a/src/utils/io.rs b/src/utils/io.rs index e69de29..8b13789 100644 --- a/src/utils/io.rs +++ b/src/utils/io.rs @@ -0,0 +1 @@ + diff --git a/src/utils/parameters.rs b/src/utils/parameters.rs index d9b07b1..f693ce8 100644 --- a/src/utils/parameters.rs +++ b/src/utils/parameters.rs @@ -3,8 +3,7 @@ use crate::core::texture::{FloatTexture, SpectrumTexture}; use crate::spectra::data::get_named_spectrum; use crate::spectra::piecewise::PiecewiseLinearSpectrumBuffer; use crate::utils::FileLoc; -use anyhow::{Result, bail}; -use shared::Float; +use anyhow::{bail, Result}; use shared::core::color::RGB; use shared::core::geometry::{Normal3f, Point2f, Point3f, Vector2f, Vector3f}; use shared::core::spectrum::Spectrum; @@ -13,11 +12,12 @@ use shared::spectra::{ PiecewiseLinearSpectrum, RGBAlbedoSpectrum, RGBColorSpace, RGBIlluminantSpectrum, RGBUnboundedSpectrum, }; +use shared::Float; use std::collections::HashMap; use std::sync::{ - Arc, atomic::{AtomicBool, Ordering}, + Arc, }; pub fn error_exit(loc: Option<&FileLoc>, message: &str) -> String { @@ -350,30 +350,31 @@ impl ParameterDictionary { where T: PBRTParameter, { - let param = self.params[0].clone(); - if param.name == name && param.type_name == T::TYPE_NAME { - let values = T::get_values(¶m); + for param in &self.params { + if param.name == name && param.type_name == T::TYPE_NAME { + let values = T::get_values(param); - if values.is_empty() { - bail!( - "{}: No values provided for parameter \"{}\".", - ¶m.loc, - name - ); + if values.is_empty() { + bail!( + "{}: No values provided for parameter \"{}\".", + ¶m.loc, + name + ); + } + + if values.len() != T::N_PER_ITEM { + bail!( + "{}: Expected {} values for parameter \"{}\". Found {}.", + ¶m.loc, + T::N_PER_ITEM, + name, + values.len() + ); + } + + param.looked_up.store(true, Ordering::Relaxed); + return Ok(T::convert(values)); } - - if values.len() != T::N_PER_ITEM { - bail!( - "{}: Expected {} values for parameter \"{}\". Found {}.", - ¶m.loc, - T::N_PER_ITEM, - name, - values.len() - ); - } - - param.looked_up.store(true, Ordering::Relaxed); - return Ok(T::convert(values)); } Ok(default_val) diff --git a/src/utils/parser.rs b/src/utils/parser.rs index ae95c79..a5a47e2 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -385,6 +385,7 @@ impl Tokenizer { if first_char == '"' { self.advance(); let mut val = String::new(); + val.push('"'); loop { let ch = self.advance().ok_or(ParserError::UnexpectedEof)?; @@ -406,6 +407,8 @@ impl Tokenizer { val.push(ch); } } + val.push('"'); + return Ok(Some(Token { text: val, loc: start_loc,