From 0ef563d1a551767d183b243b56c0375ccc3f773a Mon Sep 17 00:00:00 2001 From: pingu Date: Sat, 10 Jan 2026 00:16:24 +0000 Subject: [PATCH] Implementing arena based memory allocation --- src/lights/diffuse.rs | 188 ++++++++++++++++++--------- src/lights/distant.rs | 108 ++++++++++++++++ src/lights/goniometric.rs | 166 ++++++++++++++++++++++-- src/lights/infinite.rs | 164 +++++++++++++----------- src/lights/mod.rs | 3 + src/lights/point.rs | 74 +++++++++++ src/lights/projection.rs | 153 +++++++++++++++++----- src/lights/spot.rs | 100 +++++++++++++++ src/utils/arena.rs | 259 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 1036 insertions(+), 179 deletions(-) create mode 100644 src/lights/distant.rs create mode 100644 src/lights/point.rs create mode 100644 src/lights/spot.rs create mode 100644 src/utils/arena.rs diff --git a/src/lights/diffuse.rs b/src/lights/diffuse.rs index 6463eb5..4d197fa 100644 --- a/src/lights/diffuse.rs +++ b/src/lights/diffuse.rs @@ -1,55 +1,34 @@ +use shared::Float; +use shared::core::ligh::{Light, LightBase, LightType}; +use shared::core::medium::MediumInterface; +use shared::core::shape::Shape; +use shared::core::spectrum::{Spectrum, SpectrumTrait}; +use shared::core::texture::SpectrumType; +use shared::lights::DiffuseAreaLight; +use shared::spectra::RGBColorSpace; +use std::sync::Arc; + +use crate::core::image::{Image, ImageIO}; +use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::spectrum::spectrum_to_photometric; use crate::core::texture::{ FloatTexture, FloatTextureTrait, TextureEvalContext, TextureEvaluator, UniversalTextureEvaluator, }; -use crate::shapes::{Shape, ShapeSample, ShapeSampleContext, ShapeTrait}; -use crate::utils::hash::hash_float; -use shared::core::color::RGB; -use shared::core::geometry::{ - Bounds3f, Normal3f, Point2f, Point2fi, Point2i, Point3f, Point3fi, Ray, Vector3f, VectorLike, -}; -use shared::core::interaction::{ - Interaction, InteractionTrait, SimpleInteraction, SurfaceInteraction, -}; -use shared::core::light::{ - LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, -}; -use shared::core::medium::MediumInterface; -use shared::core::spectrum::{Spectrum, SpectrumTrait}; -use shared::images::Image; -use shared::spectra::{ - DenselySampledSpectrum, RGBColorSpace, RGBIlluminantSpectrum, SampledSpectrum, - SampledWavelengths, -}; -use shared::{Float, PI}; -use std::sync::Arc; +use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename}; -#[derive(Clone, Debug)] -pub struct DiffuseAreaLightStorage { - shape: Arc, - alpha: Option>, - image: Option>, - image_color_space: Option>, -} - -#[derive(Clone, Debug)] -pub struct DiffuseAreaLightHost { - pub view: DiffuseAreaLight, - _storage: DiffuseAreaLightStorage, -} - -impl DiffuseAreaLightHost { - #[allow(clippy::too_many_arguments)] - pub fn new( - render_from_light: Transform, +impl CreateLight for DiffuseAreaLight { + fn new( + render_from_light: shared::utils::Transform, medium_interface: MediumInterface, le: Spectrum, scale: Float, - shape: Shape, - alpha: FloatTexture, - image: Option, - image_color_space: Option>, - two_sided: bool, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, ) -> Self { let is_constant_zero = match &alpha { FloatTexture::FloatConstant(tex) => tex.evaluate(&TextureEvalContext::default()) == 0.0, @@ -85,28 +64,117 @@ impl DiffuseAreaLightHost { if render_from_light.has_scale(None) && !is_triangle_or_bilinear { println!( "Scaling detected in rendering to light space transformation! \ - The system has numerous assumptions, implicit and explicit, \ - that this transform will have no scale factors in it. \ - Proceed at your own risk; your image may have errors." + The system has numerous assumptions, implicit and explicit, \ + that this transform will have no scale factors in it. \ + Proceed at your own risk; your image may have errors." ); } - let storage = DiffuseAreaLightStorage { - shape, - alpha: stored_alpha, - image, - image_color_space, - }; - - let view = DiffuseAreaLight { + Self { base, area: shape.area(), + image, + image_color_space, shape: Ptr::from(&*storage.shape), - }; - - Self { - view, - _storage: storage, + alpha: stored_alpha, + lemit, + two_sided, + scale, } } + + fn create( + arena: &mut Arena, + render_from_light: Transform, + medium: Medium, + params: &ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_text: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + let mut l = params.get_one_spectrum("l", None, SpectrumType::Illuminant); + let mut scale = params.get_one_float("scale", 1.); + let two_sided = params.get_one_bool("twosided", false); + let filename = resolve_filename(params.get_one_string("filename", "")); + let mut image_color_space = colorspace.clone(); + if !filename.is_empty() { + if l.is_some() { + panic!( + "{}: Both \"L\" and \"filename\" specified for DiffuseAreaLight.", + loc + ); + } + let im = Image::read(filename, None); + let image: Image = im.image; + if image.has_any_infinite_pixels() { + panic!( + "{:?}: Image '{}' has NaN pixels. Not suitable for light.", + loc, filename + ); + } + let channel_desc = image.get_channel_desc(&["R", "G", "B"]).expect(&format!( + "{:?}: Image '{}' must have R, G, B channels", + loc, filename + )); + let selected_image = image.select_channels(channel_desc); + image_color_space = im.metadata.colorspace; + } else if l.is_none() { + l = Some(colorpace.unwrap().Illuminant.clone()); + } + + let l_for_scale = l.as_ref().unwrap_or(&colorspace.illuminant); + let lemit_data = lookup_spectrum(l_for_scale); + scale /= spectrum_to_photometric(lemit_data); + + let phi_v = parameters.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_host { + // Get the appropriate luminance vector from the image colour space + let lum_vec = image_color_space.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_data.area() * PI; + + // now multiply up scale to hit the target power + scale *= phi_v / k_e; + } + + let specific = DiffuseAreaLight::new( + render_from_light, + medium.into(), + l, + scale, + shape.upload(arena), + alpha.upload(arena), + image.upload(arena), + image_color_space.upload(arena), + Some(true), + shape.area(), + ); + + Light::DiffuseArea(specific) + } } diff --git a/src/lights/distant.rs b/src/lights/distant.rs new file mode 100644 index 0000000..8e0ded7 --- /dev/null +++ b/src/lights/distant.rs @@ -0,0 +1,108 @@ +use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::spectrum::spectrum_to_photometric; +use crate::core::texture::FloatTexture; +use crate::utils::{Arena, FileLoc, ParameterDictionary}; +use shared::core::geometry::{Vector3f, VectorLike}; +use shared::core::light::{Light, LightBase}; +use shared::core::medium::{Medium, MediumInterface}; +use shared::core::shape::Shape; +use shared::lights::DistantLight; +use shared::spectra::RGBColorSpace; +use shared::utils::Transform; + +impl CreateLight for DistantLight { + fn new( + render_from_light: Transform, + medium_interface: shared::core::medium::MediumInterface, + le: shared::core::spectrum::Spectrum, + scale: shared::Float, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, + ) -> Self { + let base = LightBase::new( + LightType::DeltaDirection, + render_from_light, + MediumInterface::empty(), + ); + let lemit = lookup_spectrum(le); + Self { + base, + lemit, + scale, + scene_center: Vector3f::default(), + scene_radius: 0., + } + } + + fn create( + arena: &mut Arena, + render_from_light: Transform, + medium: Medium, + parameters: &ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_text: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + let l = parameters + .get_one_spectrum( + "L", + colorspace.unwrap().illuminant, + SpectrumType::Illuminant, + ) + .unwrap(); + let lemit = lookup_spectrum(l); + 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(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); + let final_render = render_from_light * t; + scale /= spectrum_to_photometric(l.unwrap()); + // Adjust scale to meet target illuminance value + // Like for IBLs we measure illuminance as incident on an upward-facing + // patch. + let e_v = parameters.get_one_float("illuminance", -1.); + if e_v > 0. { + sc *= e_v; + } + let specific = DistantLight::new( + final_render, + medium.into(), + le, + scale, + None, + None, + None, + None, + None, + None, + ); + + Light::Distant(specific) + } +} diff --git a/src/lights/goniometric.rs b/src/lights/goniometric.rs index 15843fe..e8107f1 100644 --- a/src/lights/goniometric.rs +++ b/src/lights/goniometric.rs @@ -1,12 +1,31 @@ -use crate::core::light::{LightBaseTrait, LightFactory}; +use crate::core::image::{Image, ImageIO, ImageMetadata}; +use crate::core::light::{CreateLight, lookup_spectrum}; +use crate::core::spectrum::spectrum_to_photometric; +use crate::core::texture::FloatTexture; +use crate::utils::sampling::PiecewiseConstant2D; +use crate::utils::{Arena, FileLoc, ParameterDictionary, resolve_filename}; +use shared::core::image::ImageBase; +use shared::core::light::{Light, LightBase, LightType}; +use shared::core::medium::{Medium, MediumInterface}; +use shared::core::spectrum::Spectrum; +use shared::core::texture::SpectrumType; +use shared::lights::GoniometricLight; +use shared::spectra::RGBColorSpace; +use shared::utils::Transform; +use shared::utils::containers::Array2D; -impl LightFactory for GoniometricLight { +impl CreateLight for GoniometricLight { fn new( render_from_light: Transform, medium_interface: MediumInterface, - scale: Float, - iemit: &Spectrum, - image: &Image, + le: Spectrum, + scale: shared::Float, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, ) -> Self { let base = LightBase::new( LightType::DeltaPosition, @@ -14,15 +33,142 @@ impl LightFactory for GoniometricLight { medium_interface, ); - let i_interned = LightBase::lookup_spectrum(&iemit); - let d = image.get_sampling_distribution_uniform(); - let distrib = PiecewiseConstant2D::new_with_data(&d); + let iemit = lookup_spectrum(le); + let d = image.unwrap().get_sampling_distribution_uniform(); + let distrib = PiecewiseConstant2D::new_with_data(d); Self { base, - iemit: i_interned, + iemit, scale, - image: Ptr::from(image), + image, distrib, } } + + fn create( + arena: &mut Arena, + render_from_light: Transform, + medium: Medium, + params: &ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_text: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + let i = params.get_one_spectrum( + "I", + colorspace.unwrap().illuminant, + SpectrumType::Illuminant, + ); + let lemit_data = lookup_spectrum(&i_spectrum_def); + let sc = params.get_one_float("scale", 1.); + let filename = resolve_filename(params.get_one_string("filename", "")); + let mut image: Option = None; + + if filename.is_empty() { + println!( + "{}: Both \"L\" and \"filename\" specified for DiffuseAreaLight.", + loc + ) + } else { + let im = Image::read(filename, None).expect("Could not load image"); + let loaded_img: Image = im.image; + let res = loaded_img.resolution(); + let metadata: ImageMetadata = im.metadata; + if loaded_img.has_any_infinite_pixels() { + panic!( + "{:?}: Image '{}' has NaN pixels. Not suitable for light.", + loc, filename + ); + } + if res.x() != res.y() { + panic!( + "{:?}: Image resolution ({}, {}) is non square. Unlikely that this is an equal area map.", + loc, + res.x(), + res.y(), + y() + ); + } + let rgb_desc = loaded_img.get_channel_desc(&["R", "G", "B"]); + let y_desc = loaded_img.get_channel_desc(&["Y"]); + if let Ok(rgb) = rgb_desc { + if y_desc.is_ok() { + panic!( + "{:?}: Image '{}' has both RGB and Y channels. Ambiguous.", + loc, filename + ); + } + + let mut y_pixels = Vec::with_capacity((res.x * res.y) as usize); + + for y in 0..res.y { + for x in 0..res.x { + let r = loaded_img.get_channel(Point2i::new(x, y), 0); + let g = loaded_img.get_channel(Point2i::new(x, y), 1); + let b = loaded_img.get_channel(Point2i::new(x, y), 2); + + y_pixels.push((r + g + b) / 3.0); + } + } + + loaded_img = Some(Image::new( + PixelFormat::F32, + res, + &["Y"], + ColorEncoding::Linear, + )); + } else if y_desc.is_ok() { + image = Some(loaded_img); + } else { + panic!( + "{:?}: Image '{}' has neither RGB nor Y channels.", + loc, filename + ); + } + } + + scale /= spectrum_to_photometric(&lemit_data); + let phi_v = params.get_one_float("power", -1.0); + + if phi_v > 0.0 { + if let Some(ref img) = image { + let mut sum_y = 0.0; + let res = img.resolution(); + + for y in 0..res.y { + for x in 0..res.x { + sum_y += img.get_channel(Point2i::new(x, y), 0); + } + } + + let k_e = 4.0 * PI * sum_y / (res.x * res.y) as Float; + 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); + let final_render_from_light = render_from_light * t; + + let d: Array2D = image.get_sampling_distribution(); + distrib = PiecewiseConstant2D::new_with_data(d); + + let specific = GoniometricLight::new( + render_from_light, + medium_interface, + le, + scale, + None, + None, + Some(image), + None, + None, + None, + ); + + Light::Goniometric(specific) + } } diff --git a/src/lights/infinite.rs b/src/lights/infinite.rs index b80d2fe..e95b00d 100644 --- a/src/lights/infinite.rs +++ b/src/lights/infinite.rs @@ -1,44 +1,27 @@ use shared::Float; use shared::core::geometry::{Bounds3f, Point2f}; -use shared::core::light::{LightBase, LightType}; +use shared::core::light::{CreateLight, Light, LightBase, LightType}; use shared::core::medium::MediumInterface; use shared::lights::{InfiniteImageLight, InfinitePortalLight, InfiniteUniformLight}; use shared::spectra::RGBColorSpace; use shared::utils::Transform; -use shared::utils::sampling::PiecewiseConstant2D; +use shared::utils::sampling::DevicePiecewiseConstant2D; use std::sync::Arc; -use crate::core::light::{LightBaseTrait, LightFactory}; +use crate::core::light::{LightBaseTrait, lookup_spectrum}; -#[derive(Debug)] -struct InfiniteImageLightStorage { - image: Image, - distrib: PiecewiseConstant2D, - compensated_distrib: PiecewiseConstant2D, - image_color_space: RGBColorSpace, -} - -#[derive(Clone, Debug)] -pub struct InfiniteImageLightHost { - pub view: InfiniteImageLight, - pub filename: String, // Kept on Host only - _storage: Arc, -} - -impl std::ops::Deref for InfiniteImageLightHost { - type Target = InfiniteImageLight; - fn deref(&self) -> &Self::Target { - &self.view - } -} - -impl InfiniteImageLightHost { - pub fn new( +impl CreateLight for InfiniteImageLight { + fn new( render_from_light: Transform, - image: Image, - image_color_space: RGBColorSpace, + medium_interface: MediumInterface, + le: shared::core::spectrum::Spectrum, scale: Float, - filename: String, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, ) -> Self { let base = LightBase::new( LightType::Infinite, @@ -53,6 +36,7 @@ impl InfiniteImageLightHost { assert_eq!(3, desc.size()); assert!(desc.is_identity()); if image.resolution().x() != image.resolution().y() { + hash(hashee, into); panic!( "{}: image resolution ({}, {}) is non-square. It's unlikely this is an equal area environment map.", filename, @@ -62,7 +46,7 @@ impl InfiniteImageLightHost { } let mut d = image.get_sampling_distribution_uniform(); let domain = Bounds2f::from_points(Point2f::new(0., 0.), Point2f::new(1., 1.)); - let distrib = PiecewiseConstant2D::new_with_bounds(&d, domain); + let distrib = DevicePiecewiseConstant2D::new_with_bounds(&d, domain); let slice = &mut d.values; // or d.as_slice_mut() let count = slice.len() as Float; let sum: Float = slice.iter().sum(); @@ -79,31 +63,31 @@ impl InfiniteImageLightHost { } } - let compensated_distrib = PiecewiseConstant2D::new_with_bounds(&d, domain); + let compensated_distrib = DevicePiecewiseConstant2D::new_with_bounds(&d, domain); - let storage = Arc::new(InfiniteImageLightStorage { - image, - distrib, - compensated_distrib, - image_color_space, - }); - - let view = InfiniteImageLight { + InfiniteImageLight { base, - image: &storage.image, + image: &image, image_color_space: &storage.image_color_space, scene_center: Point3f::default(), scene_radius: 0., scale, - distrib: &storage.distrib, - compensated_distrib: &storage.compensated_distrib, + distrib: &distrib, + compensated_distrib: &compensated_distrib, }; + } - Self { - view, - filename, - _storage: storage, - } + fn create( + arena: &mut crate::utils::Arena, + render_from_light: Transform, + medium: Medium, + parameters: &crate::utils::ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_tex: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> shared::core::light::Light { + todo!() } } @@ -121,21 +105,18 @@ pub struct InfinitePortalLightHost { _storage: Arc, } -impl std::ops::Deref for InfinitePortalLightHost { - type Target = InfinitePortalLight; - fn deref(&self) -> &Self::Target { - &self.view - } -} - -impl InfinitePortalLightHost { - pub fn new( +impl CreateLight for InfinitePortalLightHost { + fn new( render_from_light: Transform, - equal_area_image: &Image, - image_color_space: RGBColorSpace, + medium_interface: MediumInterface, + le: shared::core::spectrum::Spectrum, scale: Float, - filename: String, - points: Vec, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, ) -> Self { let base = LightBase::new( LightType::Infinite, @@ -250,34 +231,54 @@ impl InfinitePortalLightHost { image_color_space, }); - let view = InfinitePortalLight { + InfinitePortalLight { base, - image: &storage.image, + image, image_color_space: &storage.image_color_space, scale, scene_center: Point3f::default(), scene_radius: 0., portal, portal_frame, - distribution: &storage.distribution, - }; - - Self { - view, - filename, - _storage: storage, + distribution, } } + + fn create( + arena: &mut crate::utils::Arena, + render_from_light: Transform, + medium: Medium, + parameters: &crate::utils::ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_tex: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + todo!() + } } -impl LightFactory for InfiniteUniformLight { - fn new(render_from_light: Transform, le: Spectrum, scale: Float) -> Self { +impl CreateLight for InfiniteUniformLight { + fn new( + render_from_light: Transform, + medium_interface: MediumInterface, + le: shared::core::spectrum::Spectrum, + scale: Float, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, + cos_fallof_start: Option, + total_width: Option, + ) -> Self { let base = LightBase::new( LightType::Infinite, - render_from_light, - MediumInterface::default(), + &render_from_light, + &MediumInterface::default(), ); - let lemit = LightBase::lookup_spectrum(&le); + let lemit = lookup_spectrum(le); Self { base, lemit, @@ -286,4 +287,17 @@ impl LightFactory for InfiniteUniformLight { scene_radius: 0., } } + + fn create( + arena: &mut crate::utils::Arena, + render_from_light: Transform, + medium: Medium, + parameters: &crate::utils::ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_tex: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + todo!() + } } diff --git a/src/lights/mod.rs b/src/lights/mod.rs index 8c3347a..cbcaadc 100644 --- a/src/lights/mod.rs +++ b/src/lights/mod.rs @@ -1,5 +1,8 @@ pub mod diffuse; +pub mod distant; pub mod goniometric; pub mod infinite; +pub mod point; pub mod projection; pub mod sampler; +pub mod spot; diff --git a/src/lights/point.rs b/src/lights/point.rs new file mode 100644 index 0000000..18a6779 --- /dev/null +++ b/src/lights/point.rs @@ -0,0 +1,74 @@ +use crate::core::light::{CreateLight, LightBaseTrait, lookup_spectrum}; +use crate::core::spectrum::spectrum_to_photometric; +use crate::core::texture::FloatTexture; +use crate::utils::{Arena, FileLoc, ParameterDictionary}; +use shared::PI; +use shared::core::geometry::VectorLike; +use shared::core::light::{Light, LightBase, LightType}; +use shared::core::medium::Medium; +use shared::core::shape::Shape; +use shared::lights::PointLight; +use shared::spectra::RGBColorSpace; +use shared::utils::Transform; + +impl CreateLight for PointLight { + fn new( + render_from_light: Transform, + medium_interface: shared::core::medium::MediumInterface, + le: shared::core::spectrum::Spectrum, + scale: shared::Float, + _shape: Option>, + _alpha: Option>, + _image: Option>, + _image_color_space: Option>, + _two_sided: Option, + _fov: Option, + ) -> Self { + let base = LightBase::new( + LightType::DeltaPosition, + render_from_light, + medium_interface, + ); + let i = lookup_spectrum(le); + + Self { base, scale, i } + } + + fn create( + arena: &mut Arena, + render_from_light: Transform, + medium: Medium, + parameters: &ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_text: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + let l = parameters + .get_one_spectrum( + "L", + colorspace.unwrap().illuminant, + SpectrumType::Illuminant, + ) + .unwrap(); + let lemit = lookup_spectrum(l); + let mut scale = parameters.get_one_float("scale", 1); + scale /= spectrum_to_photometric(l.unwrap()); + let phi_v = parameters.get_one_float("power", 1.); + if phi_v > 0. { + let k_e = 4. * PI; + sc *= 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 base = LightBase::new(LightType::DeltaPosition, render_from_light, medium.into()); + let specific = PointLight { + base, + scale, + i: arena.alloc(lemit), + }; + Light::Point(specific) + } +} diff --git a/src/lights/projection.rs b/src/lights/projection.rs index b592990..3a487fc 100644 --- a/src/lights/projection.rs +++ b/src/lights/projection.rs @@ -1,26 +1,30 @@ -use crate::core::image::ImageBuffer; -use shared::core::light::LightBase; +use crate::core::image::{Image, ImageIO, ImageMetadata}; +use crate::core::light::CreateLight; +use crate::core::spectrum::spectrum_to_photometric; +use crate::spectra::colorspace::new; +use crate::utils::{Ptr, Upload, resolve_filename}; +use shared::core::geometry::{Bounds2f, VectorLike}; +use shared::core::image::ImageAccess; +use shared::core::light::{Light, LightBase}; +use shared::core::medium::MediumInterface; +use shared::core::spectrum::Spectrum; use shared::lights::ProjectionLight; +use shared::spectra::RGBColorSpace; +use shared::utils::math::{radians, square}; +use shared::utils::{Ptr, Transform}; -pub struct ProjectionLightStorage { - image: *const Image, - distrib: *const PiecewiseConstant2D, - image_color_space: *const RGBColorSpace, -} - -pub struct ProjectionLightHost { - pub view: ProjectionLight, - _storage: ProjectionLightStorage, -} - -impl ProjectionLightHost { - pub fn new( +impl CreateLight for ProjectionLight { + fn new( render_from_light: Transform, medium_interface: MediumInterface, - image: ImageBuffer, - image_color_space: RGBColorSpace, - scale: Float, - fov: Float, + le: Spectrum, + scale: shared::Float, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, ) -> Self { let base = LightBase::new( LightType::DeltaPosition, @@ -38,9 +42,9 @@ impl ProjectionLightHost { }; let hither = 1e-3; - let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap(); + let screen_from_light = Transform::perspective(fov.unwrap(), hither, 1e30).unwrap(); let light_from_screen = screen_from_light.inverse(); - let opposite = (radians(fov) / 2.).tan(); + let opposite = (radians(fov.unwrap()) / 2.).tan(); let aspect_ratio = if aspect > 1. { aspect } else { 1. / aspect }; let a = 4. * square(opposite) * aspect_ratio; let dwda = |p: Point2f| { @@ -52,28 +56,109 @@ impl ProjectionLightHost { let d = image.get_sampling_distribution(dwda, screen_bounds); let distrib = PiecewiseConstant2D::new_with_bounds(&d, screen_bounds); - let storage = ProjectionLightStorage { + Self { + base, image, image_color_space, distrib, - }; - - let view = ProjectionLight { - base, - image: storage.image, - image_color_space: storage.image_color_space, screen_bounds, screen_from_light, light_from_screen, scale, hither, a, - distrib: storage.distrib, - }; - - Self { - view, - _storage: storage, } } + + fn create( + arena: &mut crate::utils::Arena, + render_from_light: shared::utils::Transform, + medium: Medium, + parameters: &crate::utils::ParameterDictionary, + loc: &FileLoc, + _shape: &Shape, + _alpha_text: &FloatTexture, + _colorspace: Option<&shared::spectra::RGBColorSpace>, + ) -> Light { + 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(parameters.get_one_string("filename", "")); + if filename.is_empty() { + panic!( + "{}: Must provide filename for projection light source.", + loc + ); + } + let im = Image::read(filename, None).unwrap(); + let image: Image = im.image; + let metadata: ImageMetadata = im.metadata; + if image.has_any_infinite_pixels() { + panic!( + "{:?}: Image '{}' has NaN pixels. Not suitable for light.", + loc, filename + ); + } + let colorspace: RGBColorSpace = metadata.colorspace.unwrap(); + let channel_desc = image + .get_channel_desc(&["R", "G", "B"]) + .unwrap_or_else(|_| { + panic!( + "{:?}: Image '{}' must have R, G and B channels.", + loc, filename + ) + }); + let image = image.select_channels(channel_desc); + scale /= spectrum_to_photometric(colorspace.illuminant); + if power > 0. { + let hither = 1e-3; + let aspect = image.resolution().x() as Float / image.resolution().y() as Float; + let screen_bounds = if aspect > 1. { + Bounds2f::from_poins(Point2f::new(-aspect, -1.), Point2f::new(aspect, 1.)) + } else { + Bounds2f::from_points( + Point2f::new(-1., -1. / aspect), + Point2f::new(1., 1. / aspect), + ) + }; + let screen_from_light = Transform::perspective(fov, hither, 1e30); + let light_from_screen = screen_from_light.inverse(); + let opposite = (radians(fov) / 2.).tan(); + let aspect_factor = if aspect > 1. { aspect } else { 1. / aspect }; + let a = 4. * square(aspect_factor); + let mut sum = 0; + let luminance = colorspace.luminance_vector(); + let res = image.resolution(); + for y in 0..res.y() { + for x in 0..res.x() { + let lerp_factor = Point2f::new((x + 0.5) / res.x(), (y + 0.5) / res.y()); + let ps = screen_bounds.lerp(lerp_factor); + let w_point = + light_from_screen.apply_to_point(Point3f::new(ps.x(), ps.y(), 0.)); + let w = Vector3f::from(w_point).normalize(); + let dwda = w.z().powi(3); + for c in 0..3 { + sum += image.get_channel(Point2f::new(x, y), c) * luminance[c] * dwda; + } + } + } + scale *= power / (a * sum / (res.x() + res.y())); + } + + let flip = Transform::scale(1., -1., 1.); + let render_from_light_flip = render_from_light * flip; + let specific = Self::new( + render_from_light, + medium_interface, + le, + scale, + None, + None, + image.upload(arena), + None, + None, + None, + ); + Light::Projection(specific) + } } diff --git a/src/lights/spot.rs b/src/lights/spot.rs new file mode 100644 index 0000000..b694922 --- /dev/null +++ b/src/lights/spot.rs @@ -0,0 +1,100 @@ +use crate::core::image::{Image, ImageIO, ImageMetadata}; +use crate::core::light::CreateLight; +use crate::core::spectrum::spectrum_to_photometric; +use crate::spectra::colorspace::new; +use crate::utils::{Ptr, Upload, resolve_filename}; +use shared::PI; +use shared::core::geometry::{Bounds2f, Frame, VectorLike}; +use shared::core::image::ImageAccess; +use shared::core::light::{Light, LightBase, LightType}; +use shared::core::medium::MediumInterface; +use shared::core::spectrum::Spectrum; +use shared::core::texture::SpectrumType; +use shared::lights::{ProjectionLight, SpotLight}; +use shared::spectra::RGBColorSpace; +use shared::utils::math::{radians, square}; +use shared::utils::{Ptr, Transform}; + +impl CreateLight for SpotLight { + fn new( + render_from_light: Transform, + medium_interface: MediumInterface, + le: Spectrum, + scale: shared::Float, + shape: Option>, + alpha: Option>, + image: Option>, + image_color_space: Option>, + two_sided: Option, + fov: Option, + cos_falloff_start: Option, + total_width: Option, + ) -> Self { + let base = LightBase::new( + LightType::DeltaPosition, + render_from_light, + MediumInterface::empty(), + ); + + let iemit = lookup_spectrum(le); + Self { + base, + iemit, + scale, + cos_falloff_end: radians(total_width.unwrap().cos()), + cos_falloff_start: radians(cos_falloff_start.unwrap().cos()), + } + } + + fn create( + arena: &mut crate::utils::Arena, + render_from_light: Transform, + medium: Medium, + parameters: &crate::utils::ParameterDictionary, + loc: &FileLoc, + shape: &Shape, + alpha_tex: &FloatTexture, + colorspace: Option<&RGBColorSpace>, + ) -> Light { + let i = parameters + .get_one_spectrum( + "I", + colorspace.unwrap().illuminant, + SpectrumType::Illuminant, + ) + .expect("No spectrum"); + let mut scale = parameters.get_one_float("scale", 1.); + let coneangle = parameters.get_one_float("coneangle", def); + let conedelta = parameters.get_one_float("conedelta", def); + 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(), + le, + scale, + None, + None, + None, + None, + None, + None, + Some(coneangle), + Some(coneangle - conedelta), + ); + } +} diff --git a/src/utils/arena.rs b/src/utils/arena.rs new file mode 100644 index 0000000..bd838e3 --- /dev/null +++ b/src/utils/arena.rs @@ -0,0 +1,259 @@ +use crate::core::image::Image; +use crate::core::texture::FloatTexture; +use crate::utils::sampling::PiecewiseConstant2D; +use core::alloc::Layout; +use shared::Float; +use shared::core::color::RGBToSpectrumTable; +use shared::core::image::DeviceImage; +use shared::core::shape::Shape; +use shared::core::texture::GPUFloatTexture; +use shared::spectra::{RGBColorSpace, StandardColorSpaces}; +use shared::textures::*; +use shared::utils::Ptr; +use shared::utils::sampling::DevicePiecewiseConstant2D; + +pub struct Arena { + buffer: Vec, +} + +impl Arena { + pub fn new() -> Self { + Self { + buffer: Vec::with_capacity(64 * 1024 * 1024), + } + } + + pub fn alloc(&mut self, value: T) -> Ptr { + let layout = Layout::new::(); + let offset = self.alloc_raw(layout); + + unsafe { + let ptr = self.buffer.as_mut_ptr().add(offset) as *mut T; + std::ptr::write(ptr, value); + } + + Ptr { + offset: offset as i32, + _marker: std::marker::PhantomData, + } + } + + pub fn alloc_slice(&mut self, values: &[T]) -> Ptr { + let layout = Layout::array::(values.len()).unwrap(); + let offset = self.alloc_raw(layout); + + unsafe { + let ptr = self.buffer.as_mut_ptr().add(offset) as *mut T; + std::ptr::copy_nonoverlapping(values.as_ptr(), ptr, values.len()); + } + + Ptr { + offset: offset as i32, + _marker: std::marker::PhantomData, + } + } + + fn alloc_raw(&mut self, layout: Layout) -> usize { + let len = self.buffer.len(); + let align_offset = (len + layout.align() - 1) & !(layout.align() - 1); + let new_len = align_offset + layout.size(); + + if new_len > self.buffer.capacity() { + // Growth strategy: Double capacity to reduce frequency of resizing + let new_cap = std::cmp::max(self.buffer.capacity() * 2, new_len); + self.buffer.reserve(new_cap - self.buffer.len()); + } + + self.buffer.resize(new_len, 0); + align_offset + } + + pub fn raw_data(&self) -> &[u8] { + &self.buffer + } +} + +pub trait Upload { + type Target: Copy; + + fn upload(&self, arena: &mut Arena) -> Ptr; +} + +impl Upload for Shape { + type Target = Shape; + fn upload(&self, arena: &mut Arena) -> Ptr { + arena.alloc(self.clone()) + } +} + +impl Upload for Image { + type Target = DeviceImage; + fn upload(&self, arena: &mut Arena) -> Ptr { + let pixels_ptr = arena.alloc_slice(&self.storage_as_slice()); + let device_img = DeviceImage { + base: self.base, + pixels: pixels_ptr, + }; + arena.alloc(device_img) + } +} + +impl Upload for FloatTexture { + type Target = GPUFloatTexture; + fn upload(&self, arena: &mut Arena) -> Ptr { + let gpu_variant = match self { + FloatTexture::Constant(tex) => GPUFloatTexture::Constant(tex.clone()), + FloatTexture::Checkerboard(tex) => GPUFloatTexture::Checkerboard(tex.clone()), + FloatTexture::Dots(tex) => GPUFloatTexture::Dots(tex.clone()), + FloatTexture::FBm(tex) => GPUFloatTexture::FBm(tex.clone()), + FloatTexture::Windy(tex) => GPUFloatTexture::Windy(tex.clone()), + FloatTexture::Wrinkled(tex) => GPUFloatTexture::Wrinkled(tex.clone()), + FloatTexture::Constant(val) => GPUFloatTexture::Constant(*val), + FloatTexture::Scaled(tex) => { + let child_ptr = tex.texture.upload(arena); + + let gpu_scaled = GPUFloatScaledTexture { + tex: child_ptr, + scale: tex.scale.upload(arena), + }; + GPUFloatTexture::Scaled(gpu_scaled) + } + + FloatTexture::Mix(tex) => { + let tex1_ptr = tex.tex1.upload(arena); + let tex2_ptr = tex.tex2.upload(arena); + let amount_ptr = tex.amount.upload(arena); + + let gpu_mix = GPUFloatMixTexture { + tex1: tex1_ptr, + tex2: tex2_ptr, + amount: amount_ptr, + }; + GPUFloatTexture::Mix(gpu_mix) + } + + FloatTexture::DirectionMix(tex) => { + let tex1_ptr = tex.tex1.upload(arena); + let tex2_ptr = tex.tex2.upload(arena); + let gpu_dmix = shared::textures::GPUFloatDirectionMixTexture { + tex1: tex1_ptr, + tex2: tex2_ptr, + dir: tex.dir, + }; + GPUFloatTexture::DirectionMix(gpu_dmix) + } + FloatTexture::Image(tex) => { + let image_ptr = tex.image.upload(arena); + + let gpu_image_tex = GPUFloatImageTexture { + mapping: tex.mapping, + tex_obj: image_ptr.offset as u64, + scale: tex.scale, + invert: tex.invert, + mapping: tex.mapping, + }; + GPUFloatTexture::Image(gpu_image_tex) + } + + FloatTexture::Ptex(tex) => { + todo!("Implement Ptex buffer upload") + } + + FloatTexture::Bilerp(tex) => GPUFloatTexture::Bilerp(tex.clone()), + }; + + arena.alloc(gpu_variant) + } +} + +impl Upload for RGBToSpectrumTable { + type Target = RGBToSpectrumTable; + + fn upload(&self, arena: &mut Arena) -> Ptr { + let z_ptr = arena.alloc_slice(&self.z_nodes); + let c_ptr = arena.alloc_slice(&self.coeffs); + + let shared_table = RGBToSpectrumTable { + z_nodes: z_ptr, + coeffs: c_ptr, + }; + + arena.alloc(shared_table) + } +} + +impl Upload for RGBColorSpace { + type Target = RGBColorSpace; + + fn upload(&self, arena: &mut Arena) -> Ptr { + let table_ptr = self.rgb_to_spectrum_table.upload(arena); + + let shared_space = RGBColorSpace { + r: self.r, + g: self.g, + b: self.b, + w: self.w, + illuminant: self.illuminant.clone(), + rgb_to_spectrum_table: table_ptr, + xyz_from_rgb: self.xyz_from_rgb, + rgb_from_xyz: self.rgb_from_xyz, + }; + + arena.alloc(shared_space) + } +} + +impl Upload for StandardColorSpaces { + type Target = StandardColorSpaces; + + fn upload(&self, arena: &mut Arena) -> Ptr { + let srgb_ptr = self.srgb.upload(arena); + let dci_ptr = self.dci_p3.upload(arena); + let rec_ptr = self.rec2020.upload(arena); + let aces_ptr = self.aces2065_1.upload(arena); + + let registry = StandardColorSpaces { + srgb: srgb_ptr, + dci_p3: dci_ptr, + rec2020: rec_ptr, + aces2065_1: aces_ptr, + }; + + arena.alloc(registry) + } +} + +impl Upload for PiecewiseConstant2D { + type Target = DevicePiecewiseConstant2D; + + fn upload(&self, arena: &mut Arena) -> Ptr { + let marginal_shared = self.p_marginal.to_shared(arena); + + let conditionals_shared: Vec = self + .p_conditionals + .iter() + .map(|c| c.to_shared(arena)) + .collect(); + + let conditionals_ptr = arena.alloc_slice(&conditionals_shared); + + let shared_2d = DevicePiecewiseConstant2D { + domain: self.domain, + p_marginal: marginal_shared, + n_conditionals: self.p_conditionals.len(), + p_conditional_v: conditionals_ptr, + }; + + arena.alloc(shared_2d) + } +} + +impl Upload for Option { + type Target = T::Target; + fn upload(&self, arena: &mut Arena) -> Ptr { + match self { + Some(val) => val.upload(arena), + None => Ptr::null(), + } + } +}