diff --git a/.gitignore b/.gitignore index 097ebfb..b050cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -/target +target/ *.lock *.log *.bak flip.rs .vscode rust-analyzer.json +data/ diff --git a/Cargo.toml b/Cargo.toml index 4558311..e3f9352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ cust = { git = "https://github.com/Rust-GPU/Rust-CUDA", branch = "main", default ptex = "0.3.0" ptex-sys = "0.3.0" slice = "0.0.4" +crossbeam-channel = "0.5.15" +num_cpus = "1.17.0" +ply-rs = "0.1.3" [build-dependencies] spirv-builder = { git = "https://github.com/rust-gpu/rust-gpu", branch = "main", optional = true } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 10c9a27..dc25d87 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -15,6 +15,8 @@ num-traits = "0.2.19" once_cell = "1.21.3" smallvec = "1.15.1" cuda_std = { git = "https://github.com/Rust-GPU/Rust-CUDA", branch = "main", default-features = false, optional = true } +half = "2.7.1" +rand = "0.9.2" [features] use_f64 = [] diff --git a/shared/src/cameras/orthographic.rs b/shared/src/cameras/orthographic.rs index dbecd56..c404766 100644 --- a/shared/src/cameras/orthographic.rs +++ b/shared/src/cameras/orthographic.rs @@ -6,7 +6,6 @@ use crate::core::geometry::{ use crate::core::medium::Medium; use crate::core::pbrt::Float; use crate::core::sampler::CameraSample; -use crate::images::ImageMetadata; use crate::spectra::{SampledSpectrum, SampledWavelengths}; use crate::utils::Transform; use crate::utils::sampling::sample_uniform_disk_concentric; @@ -80,11 +79,6 @@ impl OrthographicCamera { } impl CameraTrait for OrthographicCamera { - #[cfg(not(target_os = "cuda"))] - fn init_metadata(&self, metadata: &mut ImageMetadata) { - self.base.init_metadata(metadata) - } - fn base(&self) -> &CameraBase { &self.base } diff --git a/shared/src/cameras/perspective.rs b/shared/src/cameras/perspective.rs index f602013..b50b505 100644 --- a/shared/src/cameras/perspective.rs +++ b/shared/src/cameras/perspective.rs @@ -10,7 +10,8 @@ use crate::spectra::{SampledSpectrum, SampledWavelengths}; use crate::utils::sampling::sample_uniform_disk_concentric; use crate::utils::transform::Transform; -#[derive(Debug)] +#[repr(C)] +#[derive(Debug, Copy)] pub struct PerspectiveCamera { pub base: CameraBase, pub screen_from_camera: Transform, @@ -78,11 +79,6 @@ impl PerspectiveCamera { } impl PerspectiveCamera { - #[cfg(not(target_os = "cuda"))] - fn init_metadata(&self, metadata: &mut crate::image::ImageMetadata) { - self.base.init_metadata(metadata) - } - fn base(&self) -> &CameraBase { &self.base } diff --git a/shared/src/cameras/realistic.rs b/shared/src/cameras/realistic.rs index 739cd9d..b4ba93e 100644 --- a/shared/src/cameras/realistic.rs +++ b/shared/src/cameras/realistic.rs @@ -1,15 +1,15 @@ use crate::PI; use crate::core::camera::{CameraBase, CameraRay, CameraTrait, CameraTransform}; +use crate::core::color::SRGB; use crate::core::film::Film; use crate::core::geometry::{ Bounds2f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector2f, Vector2i, Vector3f, VectorLike, }; +use crate::core::image::{Image, PixelFormat}; use crate::core::medium::Medium; use crate::core::pbrt::Float; use crate::core::sampler::CameraSample; use crate::core::scattering::refract; -use crate::images::{Image, PixelFormat}; -use crate::spectra::color::SRGB; use crate::spectra::{SampledSpectrum, SampledWavelengths}; use crate::utils::math::{lerp, quadratic, square}; @@ -385,32 +385,15 @@ impl RealisticCamera { } impl CameraTrait for RealisticCamera { - fn init_metadata(&self, metadata: &mut crate::image::ImageMetadata) { - self.base.init_metadata(metadata) - } - fn base(&self) -> &CameraBase { &self.base } - fn get_film(&self) -> &Film { - #[cfg(not(target_os = "cuda"))] - { - if self.base.film.is_null() { - panic!( - "FilmBase error: PixelSensor pointer is null. This should have been checked during construction." - ); - } - } - unsafe { &*self.base.film } - } - fn generate_ray( &self, sample: CameraSample, _lambda: &SampledWavelengths, ) -> Option { - // Find point on film, _pFilm_, corresponding to _sample.pFilm_ let film = self.get_film(); let s = Point2f::new( sample.p_film.x() / film.full_resolution().x() as Float, diff --git a/shared/src/cameras/spherical.rs b/shared/src/cameras/spherical.rs index 1efca11..c46b34a 100644 --- a/shared/src/cameras/spherical.rs +++ b/shared/src/cameras/spherical.rs @@ -1,4 +1,4 @@ -use crate::core::camera::{CameraBase, CameraRay, CameraTransform}; +use crate::core::camera::{CameraBase, CameraRay, CameraTrait, CameraTransform}; use crate::core::film::Film; use crate::core::geometry::{Bounds2f, Point2f, Point3f, Ray, Vector3f, spherical_direction}; use crate::core::medium::Medium; @@ -6,7 +6,6 @@ use crate::core::pbrt::{Float, PI}; use crate::core::sampler::CameraSample; use crate::spectra::{SampledSpectrum, SampledWavelengths}; use crate::utils::math::{equal_area_square_to_sphere, wrap_equal_area_square}; -use std::sync::Arc; #[repr(C)] #[derive(Debug, Copy, Clone, PartialEq)] @@ -15,36 +14,18 @@ pub enum Mapping { EqualArea, } +#[repr(C)] #[derive(Debug, Copy, Clone)] pub struct SphericalCamera { pub mapping: Mapping, pub base: CameraBase, } -#[cfg(not(target_os = "cuda"))] -impl SphericalCamera { - pub fn init_metadata(&self, metadata: &mut crate::image::ImageMetadata) { - self.base.init_metadata(metadata) - } -} - impl CameraTrait for SphericalCamera { fn base(&self) -> &CameraBase { &self.base } - fn get_film(&self) -> &Film { - #[cfg(not(target_os = "cuda"))] - { - if self.base.film.is_null() { - panic!( - "FilmBase error: PixelSensor pointer is null. This should have been checked during construction." - ); - } - } - unsafe { &*self.base.film } - } - fn generate_ray( &self, sample: CameraSample, @@ -58,7 +39,6 @@ impl CameraTrait for SphericalCamera { ); let dir: Vector3f; if self.mapping == Mapping::EquiRectangular { - // Compute ray direction using equirectangular mapping let theta = PI * uv[1]; let phi = 2. * PI * uv[0]; dir = spherical_direction(theta.sin(), theta.cos(), phi); @@ -67,7 +47,7 @@ impl CameraTrait for SphericalCamera { uv = wrap_equal_area_square(&mut uv); dir = equal_area_square_to_sphere(uv); } - std::mem::swap(&mut dir.y(), &mut dir.z()); + core::mem::swap(&mut dir.y(), &mut dir.z()); let ray = Ray::new( Point3f::new(0., 0., 0.), diff --git a/shared/src/core/bssrdf.rs b/shared/src/core/bssrdf.rs index 12c2ca5..3627ba1 100644 --- a/shared/src/core/bssrdf.rs +++ b/shared/src/core/bssrdf.rs @@ -1,11 +1,13 @@ -use crate::core::bxdf::BSDF; +use crate::core::bxdf::{BSDF, NormalizedFresnelBxDF}; use crate::core::geometry::{Frame, Normal3f, Point2f, Point3f, Point3fi, Vector3f}; -use crate::core::interaction::{InteractionData, Shadinggeom, SurfaceInteraction}; +use crate::core::interaction::{InteractionBase, ShadingGeom, SurfaceInteraction}; use crate::core::pbrt::{Float, PI}; -use crate::shapes::Shape; +use crate::core::shape::Shape; use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum}; +use crate::utils::RelPtr; use crate::utils::math::{catmull_rom_weights, square}; use crate::utils::sampling::sample_catmull_rom_2d; +use crate::utils::{Ptr, Slice}; use enum_dispatch::enum_dispatch; use std::sync::Arc; @@ -63,20 +65,12 @@ impl From for SubsurfaceInteraction { impl From<&SubsurfaceInteraction> for SurfaceInteraction { fn from(ssi: &SubsurfaceInteraction) -> SurfaceInteraction { SurfaceInteraction { - common: InteractionData { - pi: ssi.pi, - n: ssi.n, - wo: Vector3f::zero(), - time: 0., - medium_interface: None, - medium: None, - }, - uv: Point2f::zero(), + common: InteractionBase::new_minimal(ssi.pi, ssi.n), dpdu: ssi.dpdu, dpdv: ssi.dpdv, dndu: Normal3f::zero(), dndv: Normal3f::zero(), - shading: Shadinggeom { + shading: ShadingGeom { n: ssi.ns, dpdu: ssi.dpdus, dpdv: ssi.dpdvs, @@ -84,44 +78,30 @@ impl From<&SubsurfaceInteraction> for SurfaceInteraction { dndv: Normal3f::zero(), }, face_index: 0, - area_light: None, - material: None, + area_light: Ptr::null(), + material: Ptr::null(), dpdx: Vector3f::zero(), dpdy: Vector3f::zero(), dudx: 0., dvdx: 0., dudy: 0., dvdy: 0., - shape: Shape::default().into(), + shape: Ptr::from(&Shape::default()), } } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] pub struct BSSRDFTable { - rho_samples: Vec, - radius_samples: Vec, - profile: Vec, - rho_eff: Vec, - profile_cdf: Vec, + pub rho_samples: Slice, + pub radius_samples: *const Float, + pub profile: *const Float, + pub rho_eff: *const Float, + pub profile_cdf: *const Float, } impl BSSRDFTable { - pub fn new(n_rho_samples: usize, n_radius_samples: usize) -> Self { - let rho_samples: Vec = Vec::with_capacity(n_rho_samples); - let radius_samples: Vec = Vec::with_capacity(n_radius_samples); - let profile: Vec = Vec::with_capacity(n_radius_samples * n_rho_samples); - let rho_eff: Vec = Vec::with_capacity(n_rho_samples); - let profile_cdf: Vec = Vec::with_capacity(n_radius_samples * n_rho_samples); - Self { - rho_samples, - radius_samples, - profile, - rho_eff, - profile_cdf, - } - } - pub fn eval_profile(&self, rho_index: usize, radius_index: usize) -> Float { assert!(rho_index < self.rho_samples.len()); assert!(radius_index < self.radius_samples.len()); @@ -129,33 +109,40 @@ impl BSSRDFTable { } } -#[derive(Clone, Default, Debug)] +#[repr(C)] +#[derive(Copy, Clone, Default, Debug)] pub struct BSSRDFProbeSegment { pub p0: Point3f, pub p1: Point3f, } #[enum_dispatch] -pub trait BSSRDFTrait: Send + Sync + std::fmt::Debug { +pub trait BSSRDFTrait { fn sample_sp(&self, u1: Float, u2: Point2f) -> Option; - fn probe_intersection_to_sample(&self, si: &SubsurfaceInteraction) -> BSSRDFSample; + fn probe_intersection_to_sample( + &self, + si: &SubsurfaceInteraction, + bxdf: NormalizedFresnelBxDF, + ) -> BSSRDFSample; } +#[repr(C)] #[enum_dispatch(BSSRDFTrait)] -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub enum BSSRDF { Tabulated(TabulatedBSSRDF), } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] pub struct TabulatedBSSRDF { po: Point3f, wo: Vector3f, ns: Normal3f, eta: Float, - sigma_t: SampledSpectrum, - rho: SampledSpectrum, - table: Arc, + sigma_t: RelPtr, + rho: RelPtr, + table: *const BSSRDFTable, } impl TabulatedBSSRDF { @@ -166,7 +153,7 @@ impl TabulatedBSSRDF { eta: Float, sigma_a: &SampledSpectrum, sigma_s: &SampledSpectrum, - table: Arc, + table: *const BSSRDFTable, ) -> Self { let sigma_t = *sigma_a + *sigma_s; let rho = SampledSpectrum::safe_div(sigma_s, &sigma_t); @@ -321,7 +308,11 @@ impl BSSRDFTrait for TabulatedBSSRDF { }) } - fn probe_intersection_to_sample(&self, _si: &SubsurfaceInteraction) -> BSSRDFSample { + fn probe_intersection_to_sample( + &self, + _si: &SubsurfaceInteraction, + _bxdf: NormalizedFresnelBxDF, + ) -> BSSRDFSample { todo!() } } diff --git a/shared/src/core/bxdf.rs b/shared/src/core/bxdf.rs index f1325e7..566fc11 100644 --- a/shared/src/core/bxdf.rs +++ b/shared/src/core/bxdf.rs @@ -6,6 +6,7 @@ use std::sync::{Arc, RwLock}; use enum_dispatch::enum_dispatch; +use crate::core::color::RGB; use crate::core::geometry::{ Frame, Normal3f, Point2f, Vector3f, VectorLike, abs_cos_theta, cos_theta, same_hemisphere, spherical_direction, spherical_theta, @@ -18,9 +19,9 @@ use crate::core::scattering::{ refract, }; use crate::spectra::{ - N_SPECTRUM_SAMPLES, RGB, RGBColorSpace, RGBUnboundedSpectrum, SampledSpectrum, - SampledWavelengths, + N_SPECTRUM_SAMPLES, RGBColorSpace, RGBUnboundedSpectrum, SampledSpectrum, SampledWavelengths, }; +use crate::utils::RelPtr; use crate::utils::hash::hash_buffer; use crate::utils::math::{ clamp, fast_exp, i0, lerp, log_i0, radians, safe_acos, safe_asin, safe_sqrt, sample_discrete, @@ -170,7 +171,8 @@ impl BSDFSample { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct DiffuseBxDF { pub r: SampledSpectrum, } @@ -181,10 +183,12 @@ impl DiffuseBxDF { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct DiffuseTransmissionBxDF; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct DielectricBxDF { pub eta: Float, pub mf_distrib: TrowbridgeReitzDistribution, @@ -196,7 +200,8 @@ impl DielectricBxDF { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct ThinDielectricBxDF { pub eta: Float, } @@ -208,17 +213,18 @@ impl ThinDielectricBxDF { } static P_MAX: usize = 3; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct HairBxDF { - h: Float, - eta: Float, - sigma_a: SampledSpectrum, - beta_m: Float, - beta_n: Float, - v: [Float; P_MAX + 1], - s: Float, - sin_2k_alpha: [Float; P_MAX], - cos_2k_alpha: [Float; P_MAX], + pub h: Float, + pub eta: Float, + pub sigma_a: SampledSpectrum, + pub beta_m: Float, + pub beta_n: Float, + pub v: [Float; P_MAX + 1], + pub s: Float, + pub sin_2k_alpha: [Float; P_MAX], + pub cos_2k_alpha: [Float; P_MAX], } impl HairBxDF { @@ -320,7 +326,7 @@ impl HairBxDF { std::array::from_fn(|i| ap[i].average() / sum_y) } - fn sigma_a_from_concentration(ce: Float, cp: Float) -> RGBUnboundedSpectrum { + pub fn sigma_a_from_concentration(ce: Float, cp: Float) -> RGBUnboundedSpectrum { let eumelanin_sigma_a = RGB::new(0.419, 0.697, 1.37); let pheomelanin_sigma_a = RGB::new(0.187, 0.4, 1.05); let sigma_a = ce * eumelanin_sigma_a + cp * pheomelanin_sigma_a; @@ -328,23 +334,28 @@ impl HairBxDF { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct MeasuredBxDFData { - wavelengths: Vec, - spectra: PiecewiseLinear2D<3>, - ndf: PiecewiseLinear2D<0>, - vndf: PiecewiseLinear2D<2>, - sigma: PiecewiseLinear2D<0>, - isotropic: bool, - luminance: PiecewiseLinear2D<2>, + pub wavelengths: *const Float, + pub spectra: PiecewiseLinear2D<3>, + pub ndf: PiecewiseLinear2D<0>, + pub vndf: PiecewiseLinear2D<2>, + pub sigma: PiecewiseLinear2D<0>, + pub isotropic: bool, + pub luminance: PiecewiseLinear2D<2>, } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct MeasuredBxDF { - brdf: MeasuredBxDFData, - lambda: SampledWavelengths, + pub brdf: MeasuredBxDFData, + pub lambda: SampledWavelengths, } +unsafe impl Send for MeasuredBxDF {} +unsafe impl Sync for MeasuredBxDF {} + impl MeasuredBxDF { pub fn new(brdf: MeasuredBxDFData, lambda: &SampledWavelengths) -> Self { Self { @@ -367,13 +378,17 @@ impl MeasuredBxDF { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct ConductorBxDF { - mf_distrib: TrowbridgeReitzDistribution, - eta: SampledSpectrum, - k: SampledSpectrum, + pub mf_distrib: TrowbridgeReitzDistribution, + pub eta: SampledSpectrum, + pub k: SampledSpectrum, } +unsafe impl Send for ConductorBxDF {} +unsafe impl Sync for ConductorBxDF {} + impl ConductorBxDF { pub fn new( mf_distrib: &TrowbridgeReitzDistribution, @@ -388,8 +403,11 @@ impl ConductorBxDF { } } -#[derive(Debug, Clone)] -pub struct NormalizedFresnelBxDF; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct NormalizedFresnelBxDF { + pub eta: Float, +} #[derive(Debug, Copy, Clone)] pub struct FArgs { @@ -407,7 +425,7 @@ impl Default for FArgs { } #[enum_dispatch] -pub trait BxDFTrait: Any + Send + Sync + std::fmt::Debug { +pub trait BxDFTrait: Any { fn flags(&self) -> BxDFFlags; fn f(&self, wo: Vector3f, wi: Vector3f, mode: TransportMode) -> SampledSpectrum; @@ -488,12 +506,13 @@ pub enum BxDF { Hair(HairBxDF), CoatedDiffuse(CoatedDiffuseBxDF), CoatedConductor(CoatedConductorBxDF), - // DiffuseTransmission(DiffuseTransmissionBxDF), + NormalizedFresnel(NormalizedFresnelBxDF), } -#[derive(Debug, Default)] +#[repr(C)] +#[derive(Copy, Debug, Default)] pub struct BSDF { - bxdf: Option, + bxdf: RelPtr, shading_frame: Frame, } diff --git a/shared/src/core/camera.rs b/shared/src/core/camera.rs index dd11d54..e2494a0 100644 --- a/shared/src/core/camera.rs +++ b/shared/src/core/camera.rs @@ -8,7 +8,6 @@ use crate::core::medium::Medium; use crate::core::options::RenderingCoordinateSystem; use crate::core::pbrt::Float; use crate::core::sampler::CameraSample; -use crate::images::ImageMetadata; use crate::spectra::{SampledSpectrum, SampledWavelengths}; use crate::utils::math::lerp; use crate::utils::transform::{AnimatedTransform, Transform}; @@ -117,16 +116,6 @@ pub struct CameraBase { pub min_dir_differential_y: Vector3f, } -#[cfg(not(target_os = "cuda"))] -impl CameraBase { - pub fn init_metadata(&self, metadata: &mut ImageMetadata) { - let camera_from_world: Transform = - self.camera_transform.camera_from_world(self.shutter_open); - - metadata.camera_from_world = Some(camera_from_world.get_matrix()); - } -} - #[enum_dispatch(CameraTrait)] #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -139,19 +128,19 @@ pub enum Camera { #[enum_dispatch] pub trait CameraTrait { - #[cfg(not(target_os = "cuda"))] - fn init_metadata(&self, metadata: &mut ImageMetadata); - fn base(&self) -> &CameraBase; - fn generate_ray(&self, sample: CameraSample, lambda: &SampledWavelengths) -> Option; - #[cfg(not(target_os = "cuda"))] - fn get_film(&self) -> Result<&Film, String> { - if self.film.is_null() { - return Err("Camera error: Film pointer is null.".to_string()); + fn get_film(&self) -> &Film { + #[cfg(not(target_os = "cuda"))] + { + if self.base().film.is_null() { + panic!( + "FilmBase error: PixelSensor pointer is null. This should have been checked during construction." + ); + } } - Ok(unsafe { &*self.film }) + unsafe { &*self.base().film } } fn sample_time(&self, u: Float) -> Float { diff --git a/shared/src/core/color.rs b/shared/src/core/color.rs index 61c610c..bb78711 100644 --- a/shared/src/core/color.rs +++ b/shared/src/core/color.rs @@ -282,6 +282,18 @@ impl RGB { self.r.max(self.g).max(self.b) } + pub fn min_component_value(&self) -> Float { + self.r.min(self.g).min(self.b) + } + + pub fn min_component_index(&self) -> usize { + if self.r < self.g { + if self.r < self.b { 0 } else { 2 } + } else { + if self.g < self.b { 1 } else { 2 } + } + } + pub fn max_component_index(&self) -> usize { if self.r > self.g { if self.r > self.b { 0 } else { 2 } @@ -290,8 +302,16 @@ impl RGB { } } - pub fn clamp_zero(rgb: Self) -> Self { - RGB::new(rgb.r.max(0.), rgb.b.max(0.), rgb.g.max(0.)) + pub fn clamp(&self, min: Float, max: Float) -> Self { + RGB::new( + clamp(self.r, min, max), + clamp(self.g, min, max), + clamp(self.b, min, max), + ) + } + + pub fn clamp_zero(&self) -> Self { + RGB::new(self.r.max(0.), self.b.max(0.), self.g.max(0.)) } } @@ -549,10 +569,23 @@ impl RGBSigmoidPolynomial { } #[enum_dispatch] -pub trait ColorEncodingTrait: 'static + Send + Sync + fmt::Debug + fmt::Display { - fn from_linear_slice(&self, vin: &[Float], vout: &mut [u8]); - fn to_linear_slice(&self, vin: &[u8], vout: &mut [Float]); +pub trait ColorEncodingTrait: 'static + Send + Sync { + fn from_linear(&self, vin: &[Float], vout: &mut [u8]); + fn to_linear(&self, vin: &[u8], vout: &mut [Float]); fn to_float_linear(&self, v: Float) -> Float; + + fn from_linear_scalar(&self, v: Float) -> u8 { + let mut out = [0u8; 1]; + self.from_linear(&[v], &mut out); + out[0] + } + + fn to_linear_scalar(&self, v: u8) -> Float { + let mut out = [0.0; 1]; + self.to_linear(&[v], &mut out); + out[0] + } + fn type_id(&self) -> TypeId { TypeId::of::() } @@ -576,19 +609,29 @@ impl fmt::Display for ColorEncoding { #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct LinearEncoding; impl ColorEncodingTrait for LinearEncoding { - fn from_linear_slice(&self, vin: &[Float], vout: &mut [u8]) { + fn from_linear(&self, vin: &[Float], vout: &mut [u8]) { for (i, &v) in vin.iter().enumerate() { vout[i] = (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8; } } - fn to_linear_slice(&self, vin: &[u8], vout: &mut [Float]) { + + fn to_linear(&self, vin: &[u8], vout: &mut [Float]) { for (i, &v) in vin.iter().enumerate() { vout[i] = v as Float / 255.0; } } + fn to_float_linear(&self, v: Float) -> Float { v } + + fn from_linear_scalar(&self, v: Float) -> u8 { + (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8 + } + + fn to_linear_scalar(&self, v: u8) -> Float { + v as Float / 255.0 + } } impl fmt::Display for LinearEncoding { @@ -601,7 +644,7 @@ impl fmt::Display for LinearEncoding { #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct SRGBEncoding; impl ColorEncodingTrait for SRGBEncoding { - fn from_linear_slice(&self, vin: &[Float], vout: &mut [u8]) { + fn from_linear(&self, vin: &[Float], vout: &mut [u8]) { for (i, &v_linear) in vin.iter().enumerate() { let v = v_linear.clamp(0.0, 1.0); let v_encoded = if v <= 0.0031308 { @@ -613,12 +656,33 @@ impl ColorEncodingTrait for SRGBEncoding { } } - fn to_linear_slice(&self, vin: &[u8], vout: &mut [Float]) { + fn to_linear(&self, vin: &[u8], vout: &mut [Float]) { for (i, &v) in vin.iter().enumerate() { vout[i] = SRGB_TO_LINEAR_LUT[v as usize]; } } + fn from_linear_scalar(&self, v: Float) -> u8 { + let v_clamped = v.clamp(0.0, 1.0); + let v_encoded = if v_clamped <= 0.0031308 { + v_clamped * 12.92 + } else { + 1.055 * v_clamped.powf(1.0 / 2.4) - 0.055 + }; + (v_encoded * 255.0 + 0.5) as u8 + } + + fn to_linear_scalar(&self, v: u8) -> Float { + // Normalize 0-255 to 0.0-1.0 first + let v_float = v as Float / 255.0; + // Apply sRGB -> Linear math + if v_float <= 0.04045 { + v_float / 12.92 + } else { + ((v_float + 0.055) / 1.055).powf(2.4) + } + } + fn to_float_linear(&self, v: Float) -> Float { let v = v.clamp(0.0, 1.0); if v <= 0.04045 { @@ -1015,3 +1079,28 @@ impl RGBToSpectrumTable { } } } + +const LMS_FROM_XYZ: SquareMatrix3f = SquareMatrix::new([ + [0.8951, 0.2664, -0.1614], + [-0.7502, 1.7135, 0.0367], + [0.0389, -0.0685, 1.0296], +]); + +const XYZ_FROM_LMS: SquareMatrix3f = SquareMatrix::new([ + [0.986993, -0.147054, 0.159963], + [0.432305, 0.51836, 0.0492912], + [-0.00852866, 0.0400428, 0.968487], +]); + +pub fn white_balance(src_white: Point2f, target_white: Point2f) -> SquareMatrix3f { + let src_xyz = XYZ::from_xyy(src_white, None); + let dst_xyz = XYZ::from_xyy(target_white, None); + let src_lms = LMS_FROM_XYZ * src_xyz; + let dst_lms = LMS_FROM_XYZ * dst_xyz; + let lms_correct = SquareMatrix3f::diag(&[ + dst_lms[0] / src_lms[0], + dst_lms[1] / src_lms[1], + dst_lms[2] / src_lms[2], + ]); + XYZ_FROM_LMS * lms_correct * LMS_FROM_XYZ +} diff --git a/shared/src/core/film.rs b/shared/src/core/film.rs index 0dc92b0..3e22233 100644 --- a/shared/src/core/film.rs +++ b/shared/src/core/film.rs @@ -5,14 +5,13 @@ use crate::core::geometry::{ Bounds2f, Bounds2fi, Bounds2i, Normal3f, Point2f, Point2i, Point3f, Tuple, Vector2f, Vector2fi, Vector2i, Vector3f, }; +use crate::core::image::{Image, PixelFormat}; use crate::core::interaction::SurfaceInteraction; use crate::core::pbrt::Float; use crate::core::spectrum::{Spectrum, SpectrumTrait, StandardSpectra}; -use crate::images::{Image, ImageChannelDesc, ImageChannelValues, ImageMetadata, PixelFormat}; use crate::spectra::{ ConstantSpectrum, DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, N_SPECTRUM_SAMPLES, PiecewiseLinearSpectrum, RGBColorSpace, SampledSpectrum, SampledWavelengths, colorspace, - get_named_spectrum, }; use crate::utils::AtomicFloat; use crate::utils::containers::Array2D; @@ -33,7 +32,7 @@ pub struct RGBFilm { } #[repr(C)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct RGBPixel { rgb_sum: [AtomicFloat; 3], weight_sum: AtomicFloat, @@ -154,7 +153,7 @@ impl RGBFilm { } pub fn get_pixel_rgb(&self, p: Point2i, splat_scale: Option) -> RGB { - let pixel = unsafe { &self.pixels.get(p.x(), p.y())[p] }; + let pixel = unsafe { &self.pixels.get(p) }; let mut rgb = RGB::new( pixel.rgb_sum[0].load() as Float, pixel.rgb_sum[1].load() as Float, @@ -190,7 +189,9 @@ impl RGBFilm { } } -#[derive(Debug, Clone, Copy, Default)] +#[repr(C)] +#[derive(Debug, Default)] +#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] struct GBufferPixel { pub rgb_sum: [AtomicFloat; 3], pub weight_sum: AtomicFloat, @@ -206,7 +207,9 @@ struct GBufferPixel { pub rgb_variance: VarianceEstimator, } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Debug, Default)] +#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] pub struct GBufferFilm { pub base: FilmBase, output_from_render: AnimatedTransform, @@ -312,7 +315,7 @@ impl GBufferFilm { } pub fn get_pixel_rgb(&self, p: Point2i, splat_scale: Option) -> RGB { - let pixel = unsafe { &self.pixels.get(p.x(), p.y()) }; + let pixel = unsafe { &self.pixels.get(p) }; let mut rgb = RGB::new( pixel.rgb_sum[0].load() as Float, pixel.rgb_sum[1].load() as Float, @@ -343,7 +346,8 @@ impl GBufferFilm { } #[repr(C)] -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default)] +#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] pub struct SpectralPixel { pub rgb_sum: [AtomicFloat; 3], pub rgb_weigh_sum: AtomicFloat, @@ -351,15 +355,9 @@ pub struct SpectralPixel { pub bucket_offset: usize, } -pub struct SpectralPixelView<'a> { - pub metadata: &'a SpectralPixel, - pub bucket_sums: &'a [f64], - pub weight_sums: &'a [f64], - pub bucket_splats: &'a [AtomicFloat], -} - #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Default)] +#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] pub struct SpectralFilm { pub base: FilmBase, pub colorspace: RGBColorSpace, @@ -371,59 +369,9 @@ pub struct SpectralFilm { pub filter_integral: Float, pub pixels: Array2D, pub output_rgbf_from_sensor_rgb: SquareMatrix, - pub bucket_sums: Vec, - pub weight_sums: Vec, - pub bucket_splats: Vec, -} - -#[cfg(not(target_os = "cuda"))] -impl SpectralFilm { - pub fn new( - base: &FilmBase, - lambda_min: Float, - lambda_max: Float, - n_buckets: usize, - colorspace: &RGBColorSpace, - max_component_value: Float, - write_fp16: bool, - ) -> Self { - assert!(!base.pixel_bounds.is_empty()); - let sensor_ptr = base.sensor; - if sensor_ptr.is_null() { - panic!("Film must have a sensor"); - } - let sensor = unsafe { &*sensor_ptr }; - let output_rgbf_from_sensor_rgb = colorspace.rgb_from_xyz * sensor.xyz_from_sensor_rgb; - let n_pixels = base.pixel_bounds.area() as usize; - let total_bucket_count = n_pixels * n_buckets; - let bucket_sums = vec![0.0; total_bucket_count]; - let weight_sums = vec![0.0; total_bucket_count]; - let filter_integral = base.filter.integral(); - let bucket_splats: Vec = (0..total_bucket_count) - .map(|_| AtomicFloat::new(0.0)) - .collect(); - - let mut pixels = Array2D::::new(base.pixel_bounds); - for i in 0..n_pixels { - pixels.get_linear_mut(i).bucket_offset = i * n_buckets; - } - - Self { - base: base.clone(), - lambda_min, - lambda_max, - n_buckets, - pixels, - bucket_sums, - weight_sums, - bucket_splats, - colorspace: colorspace.clone(), - max_component_value, - write_fp16, - filter_integral, - output_rgbf_from_sensor_rgb, - } - } + pub bucket_sums: *mut f64, + pub weight_sums: *mut f64, + pub bucket_splats: *mut AtomicFloat, } impl SpectralFilm { @@ -438,19 +386,6 @@ impl SpectralFilm { fn uses_visible_surface(&self) -> bool { true } - - pub fn get_pixel_view(&self, p: Point2i) -> SpectralPixelView { - let metadata = unsafe { &self.pixels.get(p.x(), p.y()) }; - let start = metadata.bucket_offset; - let end = start + self.n_buckets; - - SpectralPixelView { - metadata, - bucket_sums: &self.bucket_sums[start..end], - weight_sums: &self.weight_sums[start..end], - bucket_splats: &self.bucket_splats[start..end], - } - } } #[repr(C)] @@ -471,10 +406,11 @@ impl PixelSensor { g: Spectrum, b: Spectrum, output_colorspace: RGBColorSpace, - sensor_illum: Option>, + sensor_illum: *const Spectrum, imaging_ratio: Float, + spectra: *const StandardSpectra, swatches: &[Spectrum; 24], - ) -> Result> { + ) -> Self { // As seen in usages of this constructos, sensor_illum can be null // Going with the colorspace's own illuminant, but this might not be the right choice // TODO: Test this @@ -505,15 +441,15 @@ impl PixelSensor { let mut xyz_output = [[0.; 3]; Self::N_SWATCH_REFLECTANCES]; let sensor_white_g = illum.inner_product(&Spectrum::DenselySampled(g_bar.clone())); - let sensor_white_y = illum.inner_product(cie_y()); + let sensor_white_y = illum.inner_product(spectra.y); for i in 0..Self::N_SWATCH_REFLECTANCES { let s = swatches[i].clone(); let xyz = Self::project_reflectance::( &s, &output_colorspace.illuminant, - cie_x(), - cie_y(), - cie_z(), + spectra.x, + spectra.y, + spectra.z, ) * (sensor_white_y / sensor_white_g); for c in 0..3 { xyz_output[i][c] = xyz[c]; @@ -535,14 +471,15 @@ impl PixelSensor { output_colorspace: &RGBColorSpace, sensor_illum: Option>, imaging_ratio: Float, + spectra: *const StandardSpectra, ) -> Self { - let r_bar = DenselySampledSpectrum::from_spectrum(cie_x()); - let g_bar = DenselySampledSpectrum::from_spectrum(cie_y()); - let b_bar = DenselySampledSpectrum::from_spectrum(cie_z()); + let r_bar = DenselySampledSpectrum::from_spectrum(spectra.x); + let g_bar = DenselySampledSpectrum::from_spectrum(spectra.y); + let b_bar = DenselySampledSpectrum::from_spectrum(spectra.z); let xyz_from_sensor_rgb: SquareMatrix; if let Some(illum) = sensor_illum { - let source_white = illum.to_xyz().xy(); + let source_white = illum.to_xyz(spectra).xy(); let target_white = output_colorspace.w; xyz_from_sensor_rgb = white_balance(source_white, target_white); } else { @@ -633,7 +570,7 @@ impl VisibleSurface { } #[repr(C)] -#[derive(Default, Copy, Clone)] +#[derive(Default, Debug, Copy, Clone)] pub struct FilmBase { pub full_resolution: Point2i, pub pixel_bounds: Bounds2i, @@ -643,7 +580,8 @@ pub struct FilmBase { } #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug)] +#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] pub enum Film { RGB(RGBFilm), GBuffer(GBufferFilm), diff --git a/shared/src/core/filter.rs b/shared/src/core/filter.rs index 46ffb08..9e95669 100644 --- a/shared/src/core/filter.rs +++ b/shared/src/core/filter.rs @@ -11,7 +11,8 @@ pub struct FilterSample { pub weight: Float, } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct FilterSampler { pub domain: Bounds2f, pub distrib: PiecewiseConstant2D, @@ -53,7 +54,7 @@ impl FilterSampler { return FilterSample { p, weight: 0.0 }; } - let weight = *self.f.get_linear(pi.x() as usize + self.f.x_size()) / pdf; + let weight = *self.f.get_linear(pi.x() as u32 + self.f.x_size()) / pdf; FilterSample { p, weight } } } @@ -67,7 +68,7 @@ pub trait FilterTrait { #[repr(C)] #[enum_dispatch(FilterTrait)] -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Filter { Box(BoxFilter), Gaussian(GaussianFilter), diff --git a/shared/src/core/geometry/mod.rs b/shared/src/core/geometry/mod.rs index 8d8efb1..133c0d1 100644 --- a/shared/src/core/geometry/mod.rs +++ b/shared/src/core/geometry/mod.rs @@ -7,8 +7,9 @@ pub mod traits; pub use self::bounds::{Bounds, Bounds2f, Bounds2fi, Bounds2i, Bounds3f, Bounds3fi, Bounds3i}; pub use self::cone::DirectionCone; pub use self::primitives::{ - Frame, Normal, Normal3f, Point, Point2f, Point2fi, Point2i, Point3, Point3f, Point3fi, Point3i, - Vector, Vector2, Vector2f, Vector2fi, Vector2i, Vector3, Vector3f, Vector3fi, Vector3i, + Frame, MulAdd, Normal, Normal3f, Point, Point2f, Point2fi, Point2i, Point3, Point3f, Point3fi, + Point3i, Vector, Vector2, Vector2f, Vector2fi, Vector2i, Vector3, Vector3f, Vector3fi, + Vector3i, }; pub use self::ray::{Ray, RayDifferential}; pub use self::traits::{Lerp, Sqrt, Tuple, VectorLike}; diff --git a/shared/src/core/geometry/primitives.rs b/shared/src/core/geometry/primitives.rs index ba1fa3c..938ad1b 100644 --- a/shared/src/core/geometry/primitives.rs +++ b/shared/src/core/geometry/primitives.rs @@ -9,6 +9,19 @@ use std::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, }; +pub trait MulAdd { + type Output; + fn mul_add(self, multiplier: M, addend: A) -> Self::Output; +} + +impl MulAdd for Float { + type Output = Float; + #[inline(always)] + fn mul_add(self, multiplier: Float, addend: Float) -> Self::Output { + self.mul_add(multiplier, addend) + } +} + // N-dimensional displacement #[repr(C)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -260,6 +273,27 @@ macro_rules! impl_op_assign { }; } +#[macro_export] +macro_rules! impl_mul_add { + ($Struct:ident) => { + impl MulAdd> for $Struct + where + T: MulAdd + Copy, + { + type Output = $Struct; + + #[inline(always)] + fn mul_add(self, multiplier: T, addend: $Struct) -> Self::Output { + let mut result = self.0; + for i in 0..N { + result[i] = self.0[i].mul_add(multiplier, addend.0[i]); + } + Self(result) + } + } + }; +} + #[macro_export] macro_rules! impl_float_vector_ops { ($Struct:ident) => { @@ -381,6 +415,10 @@ impl_accessors!(Vector); impl_accessors!(Point); impl_accessors!(Normal); +impl_mul_add!(Vector); +impl_mul_add!(Point); +impl_mul_add!(Normal); + // Convert from tuple of Floats, for parsing issues impl_tuple_conversions!(Vector); impl_tuple_conversions!(Point); @@ -778,7 +816,8 @@ impl Normal3 where T: Num + PartialOrd + Copy + Neg + Sqrt, { - pub fn face_forward(self, v: Vector3) -> Self { + pub fn face_forward(self, v: impl Into>) -> Self { + let v: Vector3 = v.into(); if Vector3::::from(self).dot(v) < T::zero() { -self } else { @@ -787,8 +826,8 @@ where } } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct OctahedralVector { x: u16, y: u16, @@ -850,6 +889,7 @@ impl From for Vector3f { } } +#[repr(C)] #[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct Frame { pub x: Vector3f, diff --git a/shared/src/core/geometry/ray.rs b/shared/src/core/geometry/ray.rs index ebeb451..1daf9c0 100644 --- a/shared/src/core/geometry/ray.rs +++ b/shared/src/core/geometry/ray.rs @@ -2,16 +2,18 @@ use super::{Normal3f, Point3f, Point3fi, Vector3f, VectorLike}; use crate::core::medium::Medium; use crate::core::pbrt::Float; use crate::utils::math::{next_float_down, next_float_up}; +use crate::utils::ptr::Ptr; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct Ray { pub o: Point3f, pub d: Vector3f, - pub medium: *const Medium, pub time: Float, + pub medium: Ptr, // We do this instead of creating a trait for Rayable or some gnarly thing like that - pub differential: *const RayDifferential, + pub has_differentials: bool, + pub differential: RayDifferential, } impl Default for Ray { @@ -19,20 +21,21 @@ impl Default for Ray { Self { o: Point3f::new(0.0, 0.0, 0.0), d: Vector3f::new(0.0, 0.0, 0.0), - medium: None, + medium: Ptr::null(), time: 0.0, - differential: None, + has_differentials: false, + differential: RayDifferential::default(), } } } impl Ray { - pub fn new(o: Point3f, d: Vector3f, time: Option, medium: *const Medium) -> Self { + pub fn new(o: Point3f, d: Vector3f, time: Option, medium: &Medium) -> Self { Self { o, d, time: time.unwrap_or_else(|| Self::default().time), - medium, + medium: Ptr::from(medium), ..Self::default() } } @@ -68,8 +71,9 @@ impl Ray { o: origin, d, time, - medium: None, - differential: None, + medium: Ptr::null(), + has_differentials: false, + differential: RayDifferential::default(), } } @@ -95,13 +99,15 @@ impl Ray { o: pf, d, time, - medium: None, - differential: None, + medium: Ptr::null(), + has_differentials: false, + differential: RayDifferential::default(), } } pub fn scale_differentials(&mut self, s: Float) { - if let Some(differential) = &mut self.differential { + if self.has_differentials { + let differential = &mut self.differential; differential.rx_origin = self.o + (differential.rx_origin - self.o) * s; differential.ry_origin = self.o + (differential.ry_origin - self.o) * s; differential.rx_direction = self.d + (differential.rx_direction - self.d) * s; diff --git a/shared/src/core/image.rs b/shared/src/core/image.rs new file mode 100644 index 0000000..cb15801 --- /dev/null +++ b/shared/src/core/image.rs @@ -0,0 +1,178 @@ +use crate::core::color::{ColorEncoding, ColorEncodingTrait, LINEAR}; +use crate::core::geometry::{Bounds2f, Point2f, Point2fi, Point2i}; +use crate::core::pbrt::Float; +use crate::utils::containers::Array2D; +use crate::utils::math::{f16_to_f32, lerp, square}; +use core::hash; +use half::f16; +use smallvec::{SmallVec, smallvec}; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum WrapMode { + Black, + Clamp, + Repeat, + OctahedralSphere, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WrapMode2D { + pub uv: [WrapMode; 2], +} + +impl From for WrapMode2D { + fn from(w: WrapMode) -> Self { + Self { uv: [w, w] } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PixelFormat { + U8, + F16, + F32, +} + +impl PixelFormat { + pub fn is_8bit(&self) -> bool { + matches!(self, PixelFormat::U8) + } + + pub fn is_16bit(&self) -> bool { + matches!(self, PixelFormat::F16) + } + + pub fn is_32bit(&self) -> bool { + matches!(self, PixelFormat::F32) + } + + pub fn texel_bytes(&self) -> usize { + match self { + PixelFormat::U8 => 1, + PixelFormat::F16 => 2, + PixelFormat::F32 => 4, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub enum Pixels { + U8(*const u8), + F16(*const u16), + F32(*const f32), +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct Image { + pub format: PixelFormat, + pub pixels: Pixels, + pub encoding: ColorEncoding, + pub resolution: Point2i, + pub n_channels: i32, +} + +impl Image { + pub fn resolution(&self) -> Point2i { + self.resolution + } + + pub fn is_valid(&self) -> bool { + self.resolution.x() > 0. && self.resolution.y() > 0. + } + + pub fn format(&self) -> PixelFormat { + self.format + } + + pub fn n_channels(&self) -> i32 { + self.n_channels + } + + pub fn pixel_offset(&self, p: Point2i) -> u32 { + let width = self.resolution.x() as u32; + let idx = p.y() as u32 * width + p.x() as u32; + idx * (self.n_channels as u32) + } + + pub fn get_channel_with_wrap(&self, p: Point2i, c: i32, wrap_mode: WrapMode2D) -> Float { + if !self.remap_pixel_coords(&mut p, wrap_mode) { + return 0.; + } + + let offset = self.pixel_offset(p) + c; + unsafe { + match self.pixels { + Pixels::U8(ptr) => { + let raw_u8 = *ptr.add(offset); + self.encoding.to_linear_scalar(raw_u8) + } + Pixels::F16(ptr) => { + let half_bits = *ptr.add(offset); + f16_to_f32(f16::from_bits(half_bits)) + } + Pixels::F32(ptr) => *ptr.add(offset), + } + } + } + + pub fn get_channel(&self, p: Point2i, c: i32) -> Float { + self.get_channel_with_wrap(p, c, WrapMode::Clamp.into()) + } + + pub fn remap_pixel_coords(&self, p: &mut Point2i, wrap_mode: WrapMode2D) -> bool { + for i in 0..2 { + if p[i] >= 0 && p[i] < self.resolution[i] { + continue; + } + match wrap_mode.uv[i] { + WrapMode::Black => return false, + WrapMode::Clamp => p[i] = p[i].clamp(0, self.resolution[i] - 1), + WrapMode::Repeat => p[i] = p[i].rem_euclid(self.resolution[i]), + WrapMode::OctahedralSphere => { + p[i] = p[i].clamp(0, self.resolution[i] - 1); + } + } + } + true + } + + pub fn bilerp_channel(&self, p: Point2f, c: i32) -> Float { + self.bilerp_channel_with_wrap(p, c, WrapMode::Clamp.into()) + } + + pub fn bilerp_channel_with_wrap(&self, p: Point2f, c: i32, wrap_mode: WrapMode2D) -> Float { + let x = p.x() * self.resolution.x() as Float - 0.5; + let y = p.y() * self.resolution.y() as Float - 0.5; + let xi = x.floor() as i32; + let yi = y.floor() as i32; + let dx = x - xi as Float; + let dy = y - yi as Float; + let v00 = self.get_channel_with_wrap(Point2i::new(xi, yi), c, wrap_mode); + let v10 = self.get_channel_with_wrap(Point2i::new(xi + 1, yi), c, wrap_mode); + let v01 = self.get_channel_with_wrap(Point2i::new(xi, yi + 1), c, wrap_mode); + let v11 = self.get_channel_with_wrap(Point2i::new(xi + 1, yi + 1), c, wrap_mode); + lerp(dy, lerp(dx, v00, v10), lerp(dx, v01, v11)) + } + + pub fn lookup_nearest_channel_with_wrap( + &self, + p: Point2f, + c: i32, + wrap_mode: WrapMode2D, + ) -> Float { + let pi = Point2i::new( + p.x() as i32 * self.resolution.x(), + p.y() as i32 * self.resolution.y(), + ); + + self.get_channel_with_wrap(pi, c, wrap_mode) + } + + pub fn lookup_nearest_channel(&self, p: Point2f, c: i32) -> Float { + self.lookup_nearest_channel_with_wrap(p, c, WrapMode::Clamp.into()) + } +} diff --git a/shared/src/core/interaction.rs b/shared/src/core/interaction.rs index 6a7a87b..3200216 100644 --- a/shared/src/core/interaction.rs +++ b/shared/src/core/interaction.rs @@ -1,41 +1,92 @@ +use crate::Float; use crate::core::bssrdf::BSSRDF; use crate::core::bxdf::{BSDF, BxDF, BxDFFlags, DiffuseBxDF}; -use crate::core::camera::Camera; +use crate::core::camera::{Camera, CameraTrait}; use crate::core::geometry::{ Normal3f, Point2f, Point3f, Point3fi, Ray, RayDifferential, Vector3f, VectorLike, }; -use crate::core::light::Light; +use crate::core::image::Image; +use crate::core::light::{Light, LightTrait}; use crate::core::material::{ Material, MaterialEvalContext, MaterialTrait, NormalBumpEvalContext, bump_map, normal_map, }; use crate::core::medium::{Medium, MediumInterface, PhaseFunction}; use crate::core::options::get_options; -use crate::core::pbrt::Float; use crate::core::sampler::{Sampler, SamplerTrait}; +use crate::core::shape::Shape; use crate::core::texture::{GPUFloatTexture, UniversalTextureEvaluator}; -use crate::images::Image; -use crate::shapes::Shape; use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; use crate::utils::math::{clamp, difference_of_products, square}; - use enum_dispatch::enum_dispatch; use std::any::Any; +use std::default; #[repr(C)] #[derive(Default, Copy, Clone, Debug)] -pub struct InteractionData { +pub struct InteractionBase { pub pi: Point3fi, pub n: Normal3f, pub time: Float, pub wo: Vector3f, + pub uv: Point2f, pub medium_interface: MediumInterface, - pub medium: *const Medium, + pub medium: Ptr, +} + +impl InteractionBase { + pub fn new_surface_geom( + pi: Point3fi, + n: Normal3f, + uv: Point2f, + wo: Vector3f, + time: Float, + ) -> Self { + Self { + pi, + n, + uv, + wo: wo.normalize(), + time, + medium_interface: MediumInterface::default(), + medium: Ptr::null(), + } + } + + pub fn new_medium(p: Point3f, wo: Vector3f, time: Float, medium: Ptr) -> Self { + Self { + pi: Point3fi::new_from_point(p), + n: Normal3f::zero(), + uv: Point2f::default(), + wo: wo.normalize(), + time, + medium_interface: MediumInterface::default(), + medium, + } + } + + pub fn new_minimal(pi: Point3fi, n: Normal3f) -> Self { + Self { + pi, + n, + ..Default::default() + } + } + + pub fn new_boundary(p: Point3f, time: Float, medium_interface: MediumInterface) -> Self { + Self { + pi: Point3fi::new_from_point(p), + time, + medium_interface, + ..Default::default() + } + } } #[enum_dispatch] -pub trait InteractionTrait: Send + Sync + std::fmt::Debug { - fn get_common(&self) -> &InteractionData; - fn get_common_mut(&mut self) -> &mut InteractionData; +pub trait InteractionTrait { + fn get_common(&self) -> &InteractionBase; + fn get_common_mut(&mut self) -> &mut InteractionBase; fn p(&self) -> Point3f { self.get_common().pi.into() @@ -43,6 +94,7 @@ pub trait InteractionTrait: Send + Sync + std::fmt::Debug { fn pi(&self) -> Point3fi { self.get_common().pi } + fn time(&self) -> Float { self.get_common().time } @@ -62,13 +114,13 @@ pub trait InteractionTrait: Send + Sync + std::fmt::Debug { false } - fn get_medium(&self, w: Vector3f) -> *const Medium { + fn get_medium(&self, w: Vector3f) -> Ptr { let data = self.get_common(); - if let Some(mi) = &data.medium_interface { + if !data.medium_interface.inside.is_null() || !data.medium_interface.outside.is_null() { if w.dot(data.n.into()) > 0.0 { - mi.outside + data.medium_interface.outside } else { - mi.inside + data.medium_interface.inside } } else { data.medium @@ -90,11 +142,10 @@ pub trait InteractionTrait: Send + Sync + std::fmt::Debug { ray } - fn spawn_ray_to_interaction(&self, other: InteractionData) -> Ray { + fn spawn_ray_to_interaction(&self, other: InteractionBase) -> Ray { let data = self.get_common(); - let mut ray = - Ray::spawn_to_interaction(&data.pi, &data.n, data.time, &other_data.pi, &other_data.n); + let mut ray = Ray::spawn_to_interaction(&data.pi, &data.n, data.time, &other.pi, &other.n); ray.medium = self.get_medium(ray.d); ray } @@ -118,57 +169,40 @@ pub enum Interaction { } impl Interaction { - pub fn set_medium_interface(&mut self, mi: Option) { + pub fn set_medium_interface(&mut self, mi: MediumInterface) { match self { Interaction::Surface(si) => si.common.medium_interface = mi, - Interaction::Simple(si) => si.common.medium_interface = mi, Interaction::Medium(_) => {} // Medium interactions don't usually sit on boundaries + Interaction::Simple(si) => si.common.medium_interface = mi, } } } #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone, Copy)] pub struct SimpleInteraction { - pub common: InteractionData, + pub common: InteractionBase, } impl SimpleInteraction { - pub fn new(pi: Point3fi, time: Float, medium_interface: Option) -> Self { - Self { - common: InteractionData { - pi, - time, - medium_interface, - n: Normal3f::default(), - wo: Vector3f::default(), - medium: None, - }, - } - } - - pub fn new_interface(p: Point3f, medium_interface: Option) -> Self { - Self { - common: InteractionData { - pi: Point3fi::new_from_point(p), - n: Normal3f::zero(), - wo: Vector3f::zero(), - time: 0.0, - medium: None, - medium_interface, - }, - } + pub fn new(common: InteractionBase) -> Self { + Self { common } } } impl InteractionTrait for SimpleInteraction { - fn get_common(&self) -> &InteractionData { + fn get_common(&self) -> &InteractionBase { &self.common } - - fn get_common_mut(&mut self) -> &mut InteractionData { + fn get_common_mut(&mut self) -> &mut InteractionBase { &mut self.common } + fn is_surface_interaction(&self) -> bool { + false + } + fn is_medium_interaction(&self) -> bool { + false + } } #[repr(C)] @@ -184,17 +218,16 @@ pub struct ShadingGeom { #[repr(C)] #[derive(Debug, Default, Clone, Copy)] pub struct SurfaceInteraction { - pub common: InteractionData, - pub uv: Point2f, + pub common: InteractionBase, pub dpdu: Vector3f, pub dpdv: Vector3f, pub dndu: Normal3f, pub dndv: Normal3f, pub shading: ShadingGeom, pub face_index: u32, - pub area_light: *const Light, - pub material: *const Material, - pub shape: *const Shape, + pub area_light: Ptr, + pub material: Ptr, + pub shape: Ptr, pub dpdx: Vector3f, pub dpdy: Vector3f, pub dudx: Float, @@ -208,15 +241,17 @@ unsafe impl Sync for SurfaceInteraction {} impl SurfaceInteraction { pub fn le(&self, w: Vector3f, lambda: &SampledWavelengths) -> SampledSpectrum { - if let Some(area_light) = &self.area_light { - area_light.l(self.p(), self.n(), self.uv, w, lambda) + if !self.area_light.is_null() { + self.area_light + .l(self.p(), self.n(), self.common.uv, w, lambda) } else { SampledSpectrum::new(0.) } } pub fn compute_differentials(&mut self, r: &Ray, camera: &Camera, samples_per_pixel: i32) { - let computed = if let Some(diff) = &r.differential { + let computed = if !r.differential.is_null() { + let diff = unsafe { &*r.differential }; let dot_rx = self.common.n.dot(diff.rx_direction.into()); let dot_ry = self.common.n.dot(diff.ry_direction.into()); @@ -303,7 +338,8 @@ impl SurfaceInteraction { let new_ray = Ray::spawn(&self.pi(), &self.n(), ray.time, ray.d); ray.o = new_ray.o; // Skipping other variables, since they should not change when passing through surface - if let Some(diff) = &mut ray.differential { + if !ray.differential.is_null() { + let diff = unsafe { &mut *ray.differential }; diff.rx_origin += diff.rx_direction * t; diff.ry_origin += diff.ry_direction * t; } @@ -320,8 +356,8 @@ impl SurfaceInteraction { self.compute_differentials(r, camera, sampler.samples_per_pixel() as i32); let material = { - let root_mat = self.material.as_deref()?; - let mut active_mat: &Material = root_mat; + let root_mat = self.material; + let mut active_mat: &Material = *root_mat; let tex_eval = UniversalTextureEvaluator; while let Material::Mix(mix) = active_mat { // We need a context to evaluate the 'amount' texture @@ -376,12 +412,12 @@ impl SurfaceInteraction { fn compute_bump_geom( &mut self, tex_eval: &UniversalTextureEvaluator, - displacement: *const GPUFloatTexture, - normal_image: *const Image, + displacement: Ptr, + normal_image: Ptr, ) { let ctx = NormalBumpEvalContext::from(&*self); - let (dpdu, dpdv) = if let Some(disp) = displacement { - bump_map(tex_eval, &disp, &ctx) + let (dpdu, dpdv) = if !displacement.is_null() { + bump_map(tex_eval, &displacement, &ctx) } else if let Some(map) = normal_image { normal_map(map.as_ref(), &ctx) } else { @@ -492,15 +528,15 @@ impl SurfaceInteraction { } impl InteractionTrait for SurfaceInteraction { - fn get_common(&self) -> &InteractionData { + fn get_common(&self) -> &InteractionBase { &self.common } - fn get_common_mut(&mut self) -> &mut InteractionData { + fn get_common_mut(&mut self) -> &mut InteractionBase { &mut self.common } - fn get_medium(&self, w: Vector3f) -> *const Medium { + fn get_medium(&self, w: Vector3f) -> Ptr { self.common.medium_interface.as_ref().and_then(|interface| { if self.n().dot(w.into()) > 0.0 { interface.outside @@ -536,14 +572,7 @@ impl SurfaceInteraction { } Self { - common: InteractionData { - pi, - n, - time, - wo, - medium_interface: None, - medium: None, - }, + common: InteractionBase::new_surface_geom(pi, n, uv, wo, time), uv, dpdu, dpdv, @@ -556,16 +585,16 @@ impl SurfaceInteraction { dndu, dndv, }, - material: None, + material: Ptr::null(), face_index: 0, - area_light: None, + area_light: Ptr::null(), dpdx: Vector3f::zero(), dpdy: Vector3f::zero(), dudx: 0.0, dudy: 0.0, dvdx: 0.0, dvdy: 0.0, - shape: core::ptr::null(), + shape: Ptr::null(), } } @@ -579,7 +608,7 @@ impl SurfaceInteraction { dndv: Normal3f, time: Float, flip: bool, - face_index: usize, + face_index: u32, ) -> Self { let mut si = Self::new(pi, uv, wo, dpdu, dpdv, dndu, dndv, time, flip); si.face_index = face_index; @@ -597,7 +626,7 @@ impl SurfaceInteraction { ) { self.shading.n = ns; if orientation { - self.common.n = self.n().face_forward(self.shading.n.into()); + self.common.n = self.n().face_forward(self.shading.n); } self.shading.dpdu = dpdus; self.shading.dpdv = dpdvs; @@ -607,14 +636,7 @@ impl SurfaceInteraction { pub fn new_simple(pi: Point3fi, n: Normal3f, uv: Point2f) -> Self { Self { - common: InteractionData { - pi, - n, - time: 0., - wo: Vector3f::zero(), - medium_interface: None, - medium: None, - }, + common: InteractionBase::new_surface_geom(pi, n, uv, Vector3f::zero(), 0.), uv, ..Default::default() } @@ -622,7 +644,7 @@ impl SurfaceInteraction { pub fn new_minimal(pi: Point3fi, uv: Point2f) -> Self { Self { - common: InteractionData { + common: InteractionBase { pi, ..Default::default() }, @@ -643,7 +665,7 @@ impl SurfaceInteraction { self.area_light = area; if prim_medium_interface.is_medium_transition() { - self.common.medium_interface = *prim_medium_interface; + self.common.medium_interface = prim_medium_interface; } else { self.common.medium = ray_medium; } @@ -653,10 +675,8 @@ impl SurfaceInteraction { #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct MediumInteraction { - pub common: InteractionData, - pub medium: *const Medium, + pub common: InteractionBase, pub phase: PhaseFunction, - pub medium_interface: MediumInterface, } impl MediumInteraction { @@ -664,21 +684,12 @@ impl MediumInteraction { p: Point3f, wo: Vector3f, time: Float, - medium: *const Medium, + medium: Ptr, phase: PhaseFunction, ) -> Self { Self { - common: InteractionData { - pi: Point3fi::new_from_point(p), - n: Normal3f::default(), - time, - wo: wo.normalize(), - medium_interface: None, - medium, - }, - medium, + common: InteractionBase::new_medium(p, wo, time, medium), phase, - medium_interface: MediumInterface::empty(), } } } @@ -688,11 +699,11 @@ impl InteractionTrait for MediumInteraction { true } - fn get_common(&self) -> &InteractionData { + fn get_common(&self) -> &InteractionBase { &self.common } - fn get_common_mut(&mut self) -> &mut InteractionData { + fn get_common_mut(&mut self) -> &mut InteractionBase { &mut self.common } } diff --git a/shared/src/core/light.rs b/shared/src/core/light.rs index d5782b9..d7124a5 100644 --- a/shared/src/core/light.rs +++ b/shared/src/core/light.rs @@ -3,13 +3,13 @@ use crate::core::geometry::{ Bounds2f, Bounds3f, DirectionCone, Normal3f, Point2f, Point2i, Point3f, Point3fi, Ray, Vector3f, VectorLike, cos_theta, }; +use crate::core::image::Image; use crate::core::interaction::{ - Interaction, InteractionData, InteractionTrait, MediumInteraction, SimpleInteraction, + Interaction, InteractionBase, InteractionTrait, MediumInteraction, SimpleInteraction, SurfaceInteraction, }; use crate::core::medium::MediumInterface; use crate::core::spectrum::{Spectrum, SpectrumTrait}; -use crate::images::Image; use crate::lights::*; use crate::spectra::{ DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, RGBColorSpace, RGBIlluminantSpectrum, @@ -17,6 +17,7 @@ use crate::spectra::{ }; use crate::utils::Transform; use crate::utils::math::{equal_area_sphere_to_square, radians, safe_sqrt, smooth_step, square}; +use crate::utils::ptr::Ptr; use crate::utils::sampling::PiecewiseConstant2D; use crate::{Float, PI}; use bitflags::bitflags; @@ -49,9 +50,9 @@ impl LightType { pub struct LightLeSample { pub l: SampledSpectrum, pub ray: Ray, - pub intr: *const InteractionData, pub pdf_pos: Float, pub pdf_dir: Float, + pub intr: Interaction, } #[repr(C)] @@ -60,12 +61,12 @@ pub struct LightLiSample { pub l: SampledSpectrum, pub wi: Vector3f, pub pdf: Float, - pub p_light: InteractionData, + pub p_light: Interaction, } #[cfg(not(target_os = "cuda"))] impl LightLiSample { - pub fn new(l: SampledSpectrum, wi: Vector3f, pdf: Float, p_light: InteractionData) -> Self { + pub fn new(l: SampledSpectrum, wi: Vector3f, pdf: Float, p_light: Interaction) -> Self { Self { l, wi, @@ -140,32 +141,23 @@ impl From<&Interaction> for LightSampleContext { #[derive(Debug, Clone, Copy)] pub struct LightBase { pub render_from_light: Transform, - pub light_type: u32, + pub light_type: LightType, pub medium_interface: MediumInterface, } -#[cfg(not(target_os = "cuda"))] impl LightBase { pub fn new( light_type: LightType, - render_from_light: &Transform, - medium_interface: &MediumInterface, + render_from_light: Transform, + medium_interface: MediumInterface, ) -> Self { Self { light_type, - render_from_light: *render_from_light, - medium_interface: medium_interface.clone(), + render_from_light, + medium_interface, } } - pub fn lookup_spectrum(s: &Spectrum) -> DenselySampledSpectrum { - let cache = SPECTRUM_CACHE.get_or_init(InternCache::new); - let dense_spectrum = DenselySampledSpectrum::from_spectrum(s); - cache.lookup(dense_spectrum).as_ref() - } -} - -impl LightBase { fn l( &self, _p: Point3f, diff --git a/shared/src/core/material.rs b/shared/src/core/material.rs index cab3fcb..c5dde7e 100644 --- a/shared/src/core/material.rs +++ b/shared/src/core/material.rs @@ -1,23 +1,23 @@ +use crate::materials::*; use enum_dispatch::enum_dispatch; use std::ops::Deref; -use std::sync::Arc; +use crate::Float; use crate::core::bssrdf::BSSRDF; use crate::core::bxdf::{ BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, }; use crate::core::geometry::{Frame, Normal3f, Point2f, Point3f, Vector2f, Vector3f, VectorLike}; -use crate::core::interaction::InteractionTrait; -use crate::core::interaction::{Interaction, ShadingGeom, SurfaceInteraction}; -use crate::core::pbrt::Float; +use crate::core::image::{Image, WrapMode, WrapMode2D}; +use crate::core::interaction::{Interaction, InteractionTrait, ShadingGeom, SurfaceInteraction}; use crate::core::scattering::TrowbridgeReitzDistribution; use crate::core::spectrum::{Spectrum, SpectrumTrait}; use crate::core::texture::{ GPUFloatTexture, GPUSpectrumTexture, TextureEvalContext, TextureEvaluator, }; -use crate::images::{Image, WrapMode, WrapMode2D}; +use crate::materials::*; use crate::spectra::{SampledSpectrum, SampledWavelengths}; -use crate::utils::Ptr; +use crate::utils::RelPtr; use crate::utils::hash::hash_float; use crate::utils::math::clamp; @@ -155,7 +155,7 @@ pub fn bump_map( } #[enum_dispatch] -pub trait MaterialTrait: Send + Sync + std::fmt::Debug { +pub trait MaterialTrait { fn get_bsdf( &self, tex_eval: &T, @@ -172,8 +172,8 @@ pub trait MaterialTrait: Send + Sync + std::fmt::Debug { fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool; fn get_normal_map(&self) -> *const Image; - fn get_displacement(&self) -> Option; - fn has_surface_scattering(&self) -> bool; + fn get_displacement(&self) -> RelPtr; + fn has_subsurface_scattering(&self) -> bool; } #[derive(Clone, Copy, Debug)] @@ -191,729 +191,3 @@ pub enum Material { ThinDielectric(ThinDielectricMaterial), Mix(MixMaterial), } - -#[derive(Clone, Debug)] -pub struct CoatedDiffuseMaterial { - pub displacement: GPUFloatTexture, - pub normal_map: *const Image, - pub reflectance: GPUSpectrumTexture, - pub albedo: GPUSpectrumTexture, - pub u_roughness: GPUFloatTexture, - pub v_roughness: GPUFloatTexture, - pub thickness: GPUFloatTexture, - pub g: GPUFloatTexture, - pub eta: Spectrum, - pub remap_roughness: bool, - pub max_depth: usize, - pub n_samples: usize, -} - -impl CoatedDiffuseMaterial { - #[allow(clippy::too_many_arguments)] - #[cfg(not(target_os = "cuda"))] - pub fn new( - reflectance: GPUSpectrumTexture, - u_roughness: GPUFloatTexture, - v_roughness: GPUFloatTexture, - thickness: GPUFloatTexture, - albedo: GPUSpectrumTexture, - g: GPUFloatTexture, - eta: Spectrum, - displacement: GPUFloatTexture, - normal_map: *const Image, - remap_roughness: bool, - max_depth: usize, - n_samples: usize, - ) -> Self { - Self { - displacement, - normal_map, - reflectance, - albedo, - u_roughness, - v_roughness, - thickness, - g, - eta, - remap_roughness, - max_depth, - n_samples, - } - } -} - -impl MaterialTrait for CoatedDiffuseMaterial { - fn get_bsdf( - &self, - tex_eval: &T, - ctx: &MaterialEvalContext, - lambda: &SampledWavelengths, - ) -> BSDF { - let r = SampledSpectrum::clamp( - &tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda), - 0., - 1., - ); - - let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx); - let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx); - - if self.remap_roughness { - u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough); - v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough); - } - - let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); - - let thick = tex_eval.evaluate_float(&self.thickness, ctx); - let mut sampled_eta = self.eta.evaluate(lambda[0]); - if self.eta.is_constant() { - let mut lambda = *lambda; - lambda.terminate_secondary_inplace(); - } - - if sampled_eta == 0. { - sampled_eta = 1. - } - - let a = SampledSpectrum::clamp( - &tex_eval.evaluate_spectrum(&self.albedo, ctx, lambda), - 0., - 1., - ); - - let gg = clamp(tex_eval.evaluate_float(&self.g, ctx), -1., 1.); - let bxdf = BxDF::CoatedDiffuse(CoatedDiffuseBxDF::new( - DielectricBxDF::new(sampled_eta, distrib), - DiffuseBxDF::new(r), - thick, - a, - gg, - self.max_depth, - self.n_samples, - )); - - BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) - } - - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - None - } - - fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { - tex_eval.can_evaluate( - &[ - &self.u_roughness, - &self.v_roughness, - &self.thickness, - &self.g, - ], - &[&self.reflectance, &self.albedo], - ) - } - - fn get_normal_map(&self) -> *const Image { - self.normal_map - } - - fn get_displacement(&self) -> Option { - Some(self.displacement.clone()) - } - - fn has_surface_scattering(&self) -> bool { - false - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct CoatedConductorMaterial { - displacement: FloatTexture, - normal_map: *const Image, - interface_uroughness: FloatTexture, - interface_vroughness: FloatTexture, - thickness: FloatTexture, - interface_eta: Spectrum, - g: FloatTexture, - albedo: SpectrumTexture, - conductor_uroughness: FloatTexture, - conductor_vroughness: FloatTexture, - conductor_eta: Option, - k: Option, - reflectance: SpectrumTexture, - remap_roughness: bool, - max_depth: usize, - n_samples: usize, -} - -impl CoatedConductorMaterial { - #[allow(clippy::too_many_arguments)] - #[cfg(not(target_os = "cuda"))] - pub fn new( - displacement: FloatTexture, - normal_map: Option>, - interface_uroughness: FloatTexture, - interface_vroughness: FloatTexture, - thickness: FloatTexture, - interface_eta: Spectrum, - g: FloatTexture, - albedo: SpectrumTexture, - conductor_uroughness: FloatTexture, - conductor_vroughness: FloatTexture, - conductor_eta: Option, - k: Option, - reflectance: SpectrumTexture, - remap_roughness: bool, - max_depth: usize, - n_samples: usize, - ) -> Self { - Self { - displacement, - normal_map, - interface_uroughness, - interface_vroughness, - thickness, - interface_eta, - g, - albedo, - conductor_uroughness, - conductor_vroughness, - conductor_eta, - k, - reflectance, - remap_roughness, - max_depth, - n_samples, - } - } -} - -impl MaterialTrait for CoatedConductorMaterial { - fn get_bsdf( - &self, - tex_eval: &T, - ctx: &MaterialEvalContext, - lambda: &SampledWavelengths, - ) -> BSDF { - let mut iurough = tex_eval.evaluate_float(&self.interface_uroughness, ctx); - let mut ivrough = tex_eval.evaluate_float(&self.interface_vroughness, ctx); - - if self.remap_roughness { - iurough = TrowbridgeReitzDistribution::roughness_to_alpha(iurough); - ivrough = TrowbridgeReitzDistribution::roughness_to_alpha(ivrough); - } - let interface_distrib = TrowbridgeReitzDistribution::new(iurough, ivrough); - let thick = tex_eval.evaluate_float(&self.thickness, ctx); - - let mut ieta = self.interface_eta.evaluate(lambda[0]); - if self.interface_eta.is_constant() { - let mut lambda = *lambda; - lambda.terminate_secondary_inplace(); - } - - if ieta == 0. { - ieta = 1.; - } - - let (mut ce, mut ck) = if let Some(eta_tex) = &self.conductor_eta { - let k_tex = self - .k - .as_ref() - .expect("CoatedConductor: 'k' must be provided if 'conductor_eta' is present"); - let ce = tex_eval.evaluate_spectrum(eta_tex, ctx, lambda); - let ck = tex_eval.evaluate_spectrum(k_tex, ctx, lambda); - (ce, ck) - } else { - let r = SampledSpectrum::clamp( - &tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda), - 0., - 0.9999, - ); - let ce = SampledSpectrum::new(1.0); - let one_minus_r = SampledSpectrum::new(1.) - r; - let ck = 2. * r.sqrt() / SampledSpectrum::clamp_zero(&one_minus_r).sqrt(); - (ce, ck) - }; - - ce /= ieta; - ck /= ieta; - - let mut curough = tex_eval.evaluate_float(&self.conductor_uroughness, ctx); - let mut cvrough = tex_eval.evaluate_float(&self.conductor_vroughness, ctx); - - if self.remap_roughness { - curough = TrowbridgeReitzDistribution::roughness_to_alpha(curough); - cvrough = TrowbridgeReitzDistribution::roughness_to_alpha(cvrough); - } - - let conductor_distrib = TrowbridgeReitzDistribution::new(curough, cvrough); - let a = SampledSpectrum::clamp( - &tex_eval.evaluate_spectrum(&self.albedo, ctx, lambda), - 0., - 1., - ); - - let gg = clamp(tex_eval.evaluate_float(&self.g, ctx), -1., 1.); - let bxdf = BxDF::CoatedConductor(CoatedConductorBxDF::new( - DielectricBxDF::new(ieta, interface_distrib), - ConductorBxDF::new(&conductor_distrib, ce, ck), - thick, - a, - gg, - self.max_depth, - self.n_samples, - )); - BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) - } - - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - None - } - - fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { - let float_textures = [ - &self.interface_uroughness, - &self.interface_vroughness, - &self.thickness, - &self.g, - &self.conductor_uroughness, - &self.conductor_vroughness, - ]; - - let mut spectrum_textures = Vec::with_capacity(4); - - spectrum_textures.push(&self.albedo); - - if let Some(eta) = &self.conductor_eta { - spectrum_textures.push(eta); - } - if let Some(k) = &self.k { - spectrum_textures.push(k); - } - - if self.conductor_eta.is_none() { - spectrum_textures.push(&self.reflectance); - } - - tex_eval.can_evaluate(&float_textures, &spectrum_textures) - } - - fn get_normal_map(&self) -> *const Image { - self.normal_map - } - - fn get_displacement(&self) -> Option { - Some(self.displacement.clone()) - } - - fn has_surface_scattering(&self) -> bool { - false - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct ConductorMaterial; -impl MaterialTrait for ConductorMaterial { - fn get_bsdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - fn get_normal_map(&self) -> *const Image { - todo!() - } - fn get_displacement(&self) -> Option { - todo!() - } - fn has_surface_scattering(&self) -> bool { - todo!() - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct DielectricMaterial { - normal_map: *const Image, - displacement: FloatTexture, - u_roughness: FloatTexture, - v_roughness: FloatTexture, - remap_roughness: bool, - eta: Spectrum, -} - -impl MaterialTrait for DielectricMaterial { - fn get_bsdf( - &self, - tex_eval: &T, - ctx: &MaterialEvalContext, - lambda: &SampledWavelengths, - ) -> BSDF { - let mut sampled_eta = self.eta.evaluate(lambda[0]); - if !self.eta.is_constant() { - lambda.terminate_secondary(); - } - - if sampled_eta == 0.0 { - sampled_eta = 1.0; - } - - let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx); - let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx); - - if self.remap_roughness { - u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough); - v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough); - } - - let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); - let bxdf = BxDF::Dielectric(DielectricBxDF::new(sampled_eta, distrib)); - - BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) - } - - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - None - } - - fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { - tex_eval.can_evaluate(&[&self.u_roughness, &self.v_roughness], &[]) - } - - fn get_normal_map(&self) -> Option> { - self.normal_map.clone() - } - - fn get_displacement(&self) -> Option { - Some(self.displacement.clone()) - } - - fn has_surface_scattering(&self) -> bool { - false - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct DiffuseMaterial { - normal_map: *const Image, - displacement: FloatTexture, - reflectance: SpectrumTexture, -} - -impl MaterialTrait for DiffuseMaterial { - fn get_bsdf( - &self, - tex_eval: &T, - ctx: &MaterialEvalContext, - lambda: &SampledWavelengths, - ) -> BSDF { - let r = tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda); - let bxdf = BxDF::Diffuse(DiffuseBxDF::new(r)); - BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) - } - - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - - fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { - tex_eval.can_evaluate(&[], &[&self.reflectance]) - } - - fn get_normal_map(&self) -> Option> { - self.normal_map.clone() - } - - fn get_displacement(&self) -> Option { - Some(self.displacement.clone()) - } - - fn has_surface_scattering(&self) -> bool { - false - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct DiffuseTransmissionMaterial; - -impl MaterialTrait for DiffuseTransmissionMaterial { - fn get_bsdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - - fn get_normal_map(&self) -> Option> { - todo!() - } - - fn get_displacement(&self) -> Option { - todo!() - } - - fn has_surface_scattering(&self) -> bool { - todo!() - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct HairMaterial; - -impl MaterialTrait for HairMaterial { - fn get_bsdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - fn get_normal_map(&self) -> Option> { - todo!() - } - fn get_displacement(&self) -> Option { - todo!() - } - fn has_surface_scattering(&self) -> bool { - todo!() - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct MeasuredMaterial; -impl MaterialTrait for MeasuredMaterial { - fn get_bsdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - fn get_normal_map(&self) -> Option> { - todo!() - } - fn get_displacement(&self) -> Option { - todo!() - } - fn has_surface_scattering(&self) -> bool { - todo!() - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct SubsurfaceMaterial; -impl MaterialTrait for SubsurfaceMaterial { - fn get_bsdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - fn get_normal_map(&self) -> Option> { - todo!() - } - fn get_displacement(&self) -> Option { - todo!() - } - fn has_surface_scattering(&self) -> bool { - todo!() - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct ThinDielectricMaterial; -impl MaterialTrait for ThinDielectricMaterial { - fn get_bsdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() - } - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - fn get_normal_map(&self) -> Option> { - todo!() - } - fn get_displacement(&self) -> Option { - todo!() - } - fn has_surface_scattering(&self) -> bool { - todo!() - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug)] -pub struct MixMaterial { - pub amount: FloatTexture, - pub materials: [Ptr; 2], -} - -impl MixMaterial { - pub fn choose_material( - &self, - tex_eval: &T, - ctx: &MaterialEvalContext, - ) -> Option<&Material> { - let amt = tex_eval.evaluate_float(&self.amount, ctx); - - let index = if amt <= 0.0 { - 0 - } else if amt >= 1.0 { - 1 - } else { - let u = hash_float(&(ctx.p, ctx.wo)); - if amt < u { 0 } else { 1 } - }; - - self.materials[index].get() - } -} - -impl MaterialTrait for MixMaterial { - fn get_bsdf( - &self, - tex_eval: &T, - ctx: &MaterialEvalContext, - lambda: &SampledWavelengths, - ) -> BSDF { - if let Some(mat) = self.choose_material(tex_eval, ctx) { - mat.get_bsdf(tex_eval, ctx, lambda) - } else { - BSDF::empty() - } - } - - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - None - } - - fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { - tex_eval.can_evaluate(&[&self.amount], &[]) - } - - fn get_normal_map(&self) -> Option> { - None - } - - fn get_displacement(&self) -> Option { - panic!( - "MixMaterial::get_displacement() shouldn't be called. \ - Displacement is not supported on Mix materials directly." - ); - } - - fn has_surface_scattering(&self) -> bool { - false - } -} diff --git a/shared/src/core/medium.rs b/shared/src/core/medium.rs index 2fd60e3..ae65585 100644 --- a/shared/src/core/medium.rs +++ b/shared/src/core/medium.rs @@ -5,14 +5,16 @@ use crate::core::geometry::{ Bounds3f, Frame, Point2f, Point3f, Point3i, Ray, Vector3f, VectorLike, spherical_direction, }; use crate::core::pbrt::{Float, INV_4_PI, PI}; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; use crate::spectra::{ BlackbodySpectrum, DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, SampledWavelengths, }; use crate::utils::containers::SampledGrid; use crate::utils::math::{clamp, square}; +use crate::utils::ptr::Ptr; use crate::utils::rng::Rng; -use crate::utils::transform::TransformGeneric; +use crate::utils::transform::Transform; #[repr(C)] #[derive(Debug, Clone, Copy)] @@ -37,7 +39,7 @@ pub trait PhaseFunctionTrait { #[repr(C)] #[enum_dispatch(PhaseFunctionTrait)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum PhaseFunction { HenyeyGreenstein(HGPhaseFunction), } @@ -88,7 +90,7 @@ impl PhaseFunctionTrait for HGPhaseFunction { } #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct MajorantGrid { pub bounds: Bounds3f, pub res: Point3i, @@ -433,7 +435,8 @@ pub trait MediumTrait: Send + Sync + std::fmt::Debug { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] #[enum_dispatch(MediumTrait)] pub enum Medium { Homogeneous(HomogeneousMedium), @@ -443,7 +446,8 @@ pub enum Medium { NanoVDB(NanoVDBMedium), } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct HomogeneousMedium { sigma_a_spec: DenselySampledSpectrum, sigma_s_spec: DenselySampledSpectrum, @@ -514,7 +518,7 @@ impl MediumTrait for HomogeneousMedium { #[derive(Debug, Clone, Copy)] pub struct GridMedium { bounds: Bounds3f, - render_from_medium: TransformGeneric, + render_from_medium: Transform, sigma_a_spec: DenselySampledSpectrum, sigma_s_spec: DenselySampledSpectrum, density_grid: SampledGrid, @@ -676,7 +680,7 @@ impl RGBGridMedium { #[cfg(not(target_os = "cuda"))] pub fn new( bounds: &Bounds3f, - render_from_medium: &TransformGeneric, + render_from_medium: &Transform, g: Float, sigma_a_grid: SampledGrid, sigma_s_grid: SampledGrid, @@ -825,8 +829,8 @@ impl MediumTrait for NanoVDBMedium { #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct MediumInterface { - pub inside: *const Medium, - pub outside: *const Medium, + pub inside: Ptr, + pub outside: Ptr, } unsafe impl Send for MediumInterface {} @@ -835,25 +839,25 @@ unsafe impl Sync for MediumInterface {} impl Default for MediumInterface { fn default() -> Self { Self { - inside: core::ptr::null(), - outside: core::ptr::null(), + inside: Ptr::null(), + outside: Ptr::null(), } } } impl MediumInterface { - pub fn new(inside: *const Medium, outside: *const Medium) -> Self { - Self { inside, outside } - } - - pub fn empty() -> Self { + pub fn new(inside: &Medium, outside: &Medium) -> Self { Self { - inside: core::ptr::null(), - outside: core::ptr::null(), + inside: Ptr::from(inside), + outside: Ptr::from(outside), } } + pub fn empty() -> Self { + Self::default() + } + pub fn is_medium_transition(&self) -> bool { - self.inside != self.outside + self.inside.0 != self.outside.0 } } diff --git a/shared/src/core/mod.rs b/shared/src/core/mod.rs index de4ab79..eb6f5cd 100644 --- a/shared/src/core/mod.rs +++ b/shared/src/core/mod.rs @@ -6,6 +6,7 @@ pub mod color; pub mod film; pub mod filter; pub mod geometry; +pub mod image; pub mod interaction; pub mod light; pub mod material; @@ -15,5 +16,6 @@ pub mod pbrt; pub mod primitive; pub mod sampler; pub mod scattering; +pub mod shape; pub mod spectrum; pub mod texture; diff --git a/shared/src/core/pbrt.rs b/shared/src/core/pbrt.rs index f61a98e..f4fe9f0 100644 --- a/shared/src/core/pbrt.rs +++ b/shared/src/core/pbrt.rs @@ -1,8 +1,8 @@ use crate::core::geometry::Lerp; +use core::sync::atomic::{AtomicU64, Ordering as SyncOrdering}; use num_traits::{Num, PrimInt}; use std::hash::Hash; use std::ops::{Add, Mul}; -use std::sync::atomic::{AtomicU64, Ordering as SyncOrdering}; use std::sync::{Arc, Mutex}; pub type Float = f32; @@ -144,6 +144,7 @@ pub static RARE_EVENT_CONDITION_MET: AtomicU64 = AtomicU64::new(0); #[macro_export] macro_rules! check_rare { ($frequency_threshold:expr, $condition:expr) => { + use core::sync::atomic::{AtomicU64, Ordering as SyncOrdering}; const CHECK_INTERVAL: u64 = 4096; let total_calls = RARE_EVENT_TOTAL_CALLS.fetch_add(1, SyncOrdering::Relaxed); diff --git a/shared/src/core/primitive.rs b/shared/src/core/primitive.rs index 45f090b..462b0db 100644 --- a/shared/src/core/primitive.rs +++ b/shared/src/core/primitive.rs @@ -5,10 +5,11 @@ use crate::core::light::Light; use crate::core::material::Material; use crate::core::medium::{Medium, MediumInterface}; use crate::core::pbrt::Float; +use crate::core::shape::{Shape, ShapeIntersection, ShapeTrait}; use crate::core::texture::{GPUFloatTexture, TextureEvalContext}; -use crate::shapes::{Shape, ShapeIntersection, ShapeTrait}; +use crate::utils::RelPtr; use crate::utils::hash::hash_float; -use crate::utils::transform::{AnimatedTransform, TransformGeneric}; +use crate::utils::transform::{AnimatedTransform, Transform}; use enum_dispatch::enum_dispatch; use std::sync::Arc; @@ -87,14 +88,14 @@ impl PrimitiveTrait for GeometricPrimitive { #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct SimplePrimitive { - shape: Arc, - material: Arc, + shape: RelPtr, + material: RelPtr, } #[derive(Debug, Clone)] pub struct TransformedPrimitive { - primitive: Arc, - render_from_primitive: TransformGeneric, + pub primitive: RelPtr, + pub render_from_primitive: Transform, } impl PrimitiveTrait for TransformedPrimitive { @@ -125,9 +126,10 @@ impl PrimitiveTrait for TransformedPrimitive { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct AnimatedPrimitive { - primitive: Arc, + primitive: RelPtr, render_from_primitive: AnimatedTransform, } @@ -158,11 +160,12 @@ impl PrimitiveTrait for AnimatedPrimitive { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct BVHAggregatePrimitive { - max_prims_in_node: usize, - primitives: Vec>, - nodes: Vec, + max_prims_in_node: u32, + primitives: *const RelPtr, + nodes: *const LinearBVHNode, } impl PrimitiveTrait for BVHAggregatePrimitive { diff --git a/shared/src/core/sampler.rs b/shared/src/core/sampler.rs index 6b441c7..2c4c3f9 100644 --- a/shared/src/core/sampler.rs +++ b/shared/src/core/sampler.rs @@ -1,14 +1,8 @@ -use std::ops::RangeFull; - -use enum_dispatch::enum_dispatch; -use rand::seq::index::sample; - use crate::core::filter::FilterTrait; use crate::core::geometry::{Bounds2f, Point2f, Point2i, Vector2f}; use crate::core::options::{PBRTOptions, get_options}; use crate::core::pbrt::{Float, ONE_MINUS_EPSILON, PI, PI_OVER_2, PI_OVER_4, find_interval}; use crate::utils::containers::Array2D; -use crate::utils::error::FileLoc; use crate::utils::math::{ BinaryPermuteScrambler, DigitPermutation, FastOwenScrambler, NoRandomizer, OwenScrambler, PRIME_TABLE_SIZE, Scrambler, clamp, compute_radical_inverse_permutations, encode_morton_2, @@ -16,12 +10,14 @@ use crate::utils::math::{ radical_inverse, round_up_pow2, scrambled_radical_inverse, sobol_interval_to_index, sobol_sample, }; -use crate::utils::parameters::ParameterDictionary; use crate::utils::rng::Rng; use crate::utils::sobol::N_SOBOL_DIMENSIONS; use crate::utils::{hash::*, sobol}; +use enum_dispatch::enum_dispatch; +use rand::seq::index::sample; -#[derive(Debug, Clone, Copy)] +#[repr(C)] +#[derive(Debug, Default, Clone, Copy)] pub struct CameraSample { pub p_film: Point2f, pub p_lens: Point2f, @@ -29,17 +25,6 @@ pub struct CameraSample { pub filter_weight: Float, } -impl Default for CameraSample { - fn default() -> Self { - Self { - p_film: Point2f::default(), - p_lens: Point2f::default(), - time: 0.0, - filter_weight: 1.0, - } - } -} - pub fn get_camera_sample(sampler: &mut S, p_pixel: Point2i, filter: &F) -> CameraSample where S: SamplerTrait, @@ -54,43 +39,29 @@ where } } -#[derive(Default, Debug, Clone)] +#[repr(C)] +#[derive(Default, Debug, Clone, Copy)] pub struct IndependentSampler { - samples_per_pixel: usize, - seed: u64, - rng: Rng, + pub samples_per_pixel: u32, + pub seed: u64, + pub rng: Rng, } impl IndependentSampler { - pub fn new(samples_per_pixel: usize, seed: u64) -> Self { + pub fn new(samples_per_pixel: u32, seed: u64) -> Self { Self { samples_per_pixel, seed, rng: Rng::default(), } } - - pub fn create( - params: &ParameterDictionary, - _full_res: Point2i, - _loc: &FileLoc, - ) -> Result { - let options = get_options(); - let nsamp = options - .quick_render - .then_some(1) - .or(options.pixel_samples) - .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); - let seed = params.get_one_int("seed", options.seed); - Ok(Self::new(nsamp as usize, seed as u64)) - } } impl SamplerTrait for IndependentSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { self.samples_per_pixel } - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option) { + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option) { let hash_input = [p.x() as u64, p.y() as u64, self.seed]; let sequence_index = hash_buffer(&hash_input, 0); self.rng.set_sequence(sequence_index); @@ -111,7 +82,8 @@ impl SamplerTrait for IndependentSampler { const MAX_HALTON_RESOLUTION: i32 = 128; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[repr(C)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)] pub enum RandomizeStrategy { #[default] None, @@ -120,21 +92,22 @@ pub enum RandomizeStrategy { Owen, } -#[derive(Default, Debug, Clone)] +#[repr(C)] +#[derive(Default, Debug, Clone, Copy)] pub struct HaltonSampler { - samples_per_pixel: usize, + samples_per_pixel: u32, randomize: RandomizeStrategy, - digit_permutations: Vec, base_scales: [u64; 2], base_exponents: [u64; 2], mult_inverse: [u64; 2], halton_index: u64, - dim: usize, + dim: u32, + digit_permutations: *const DigitPermutation, } impl HaltonSampler { pub fn new( - samples_per_pixel: usize, + samples_per_pixel: u32, full_res: Point2i, randomize: RandomizeStrategy, seed: u64, @@ -180,7 +153,7 @@ impl HaltonSampler { } } - fn sample_dimension(&self, dimension: usize) -> Float { + fn sample_dimension(&self, dimension: u32) -> Float { if self.randomize == RandomizeStrategy::None { radical_inverse(dimension, self.halton_index) } else if self.randomize == RandomizeStrategy::PermuteDigits { @@ -214,45 +187,14 @@ impl HaltonSampler { (yp, xp - d * yp) } - - pub fn create( - params: &ParameterDictionary, - full_res: Point2i, - loc: &FileLoc, - ) -> Result { - let options = get_options(); - let nsamp = options - .quick_render - .then_some(1) - .or(options.pixel_samples) - .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); - let seed = params.get_one_int("seed", options.seed); - let s = match params - .get_one_string("randomization", "permutedigits") - .as_str() - { - "none" => RandomizeStrategy::None, - "permutedigits" => RandomizeStrategy::PermuteDigits, - "fastowen" => RandomizeStrategy::FastOwen, - "owen" => RandomizeStrategy::Owen, - _ => { - return Err(format!( - "{}: Unknown randomization strategy for Halton", - loc - )); - } - }; - - Ok(HaltonSampler::new(nsamp as usize, full_res, s, seed as u64)) - } } impl SamplerTrait for HaltonSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { self.samples_per_pixel } - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option) { + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option) { self.halton_index = 0; let sample_stride = self.base_scales[0] * self.base_scales[1]; @@ -287,14 +229,14 @@ impl SamplerTrait for HaltonSampler { } fn get1d(&mut self) -> Float { - if self.dim > PRIME_TABLE_SIZE { + if self.dim > PRIME_TABLE_SIZE as u32 { self.dim = 2; } self.sample_dimension(self.dim) } fn get2d(&mut self) -> Point2f { - if self.dim > PRIME_TABLE_SIZE { + if self.dim > PRIME_TABLE_SIZE as u32 { self.dim = 2; } let dim = self.dim; @@ -310,22 +252,23 @@ impl SamplerTrait for HaltonSampler { } } -#[derive(Default, Debug, Clone)] +#[repr(C)] +#[derive(Default, Debug, Clone, Copy)] pub struct StratifiedSampler { - x_pixel_samples: usize, - y_pixel_samples: usize, + x_pixel_samples: u32, + y_pixel_samples: u32, jitter: bool, seed: u64, rng: Rng, pixel: Point2i, - sample_index: usize, - dim: usize, + sample_index: u32, + dim: u32, } impl StratifiedSampler { pub fn new( - x_pixel_samples: usize, - y_pixel_samples: usize, + x_pixel_samples: u32, + y_pixel_samples: u32, seed: Option, jitter: bool, ) -> Self { @@ -340,43 +283,14 @@ impl StratifiedSampler { dim: 0, } } - - pub fn create( - params: &ParameterDictionary, - _full_res: Point2i, - _loc: &FileLoc, - ) -> Result { - let options = get_options(); - let jitter = params.get_one_bool("jitter", true); - let (x_samples, y_samples) = if options.quick_render { - (1, 1) - } else if let Some(n) = options.pixel_samples { - let div = (n as f64).sqrt() as i32; - let y = (1..=div).rev().find(|d| n % d == 0).unwrap(); - - (n / y, y) - } else { - ( - params.get_one_int("xsamples", 4), - params.get_one_int("ysamples", 4), - ) - }; - let seed = params.get_one_int("seed", options.seed); - Ok(Self::new( - x_samples as usize, - y_samples as usize, - Some(seed as u64), - jitter, - )) - } } impl SamplerTrait for StratifiedSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { self.x_pixel_samples * self.y_pixel_samples } - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option) { + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option) { self.pixel = p; self.sample_index = sample_index; let hash_input = [p.x() as u64, p.y() as u64, self.seed]; @@ -446,18 +360,19 @@ impl SamplerTrait for StratifiedSampler { } } -#[derive(Default, Debug, Clone)] +#[repr(C)] +#[derive(Default, Debug, Clone, Copy)] pub struct PaddedSobolSampler { - samples_per_pixel: usize, + samples_per_pixel: u32, seed: u64, randomize: RandomizeStrategy, pixel: Point2i, - sample_index: usize, - dim: usize, + sample_index: u32, + dim: u32, } impl PaddedSobolSampler { - pub fn new(samples_per_pixel: usize, randomize: RandomizeStrategy, seed: Option) -> Self { + pub fn new(samples_per_pixel: u32, randomize: RandomizeStrategy, seed: Option) -> Self { Self { samples_per_pixel, seed: seed.unwrap_or(0), @@ -468,7 +383,7 @@ impl PaddedSobolSampler { } } - fn sample_dimension(&self, dimension: usize, a: u32, hash: u32) -> Float { + fn sample_dimension(&self, dimension: u32, a: u32, hash: u32) -> Float { if self.randomize == RandomizeStrategy::None { return sobol_sample(a as u64, dimension, NoRandomizer); } @@ -483,41 +398,13 @@ impl PaddedSobolSampler { RandomizeStrategy::None => unreachable!(), } } - - pub fn create( - params: &ParameterDictionary, - _full_res: Point2i, - loc: &FileLoc, - ) -> Result { - let options = get_options(); - let nsamp = options - .quick_render - .then_some(1) - .or(options.pixel_samples) - .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); - let seed = params.get_one_int("seed", options.seed); - let s = match params.get_one_string("randomization", "fastowen").as_str() { - "none" => RandomizeStrategy::None, - "permutedigits" => RandomizeStrategy::PermuteDigits, - "fastowen" => RandomizeStrategy::FastOwen, - "owen" => RandomizeStrategy::Owen, - _ => { - return Err(format!( - "{}: Unknown randomization strategy for ZSobol", - loc - )); - } - }; - - Ok(Self::new(nsamp as usize, s, Some(seed as u64))) - } } impl SamplerTrait for PaddedSobolSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { self.samples_per_pixel } - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option) { + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option) { self.pixel = p; self.sample_index = sample_index; self.dim = dim.unwrap_or(0); @@ -565,18 +452,18 @@ impl SamplerTrait for PaddedSobolSampler { #[derive(Default, Debug, Clone)] pub struct SobolSampler { - samples_per_pixel: usize, + samples_per_pixel: u32, scale: i32, seed: u64, randomize: RandomizeStrategy, pixel: Point2i, - dim: usize, + dim: u32, sobol_index: u64, } impl SobolSampler { pub fn new( - samples_per_pixel: usize, + samples_per_pixel: u32, full_resolution: Point2i, randomize: RandomizeStrategy, seed: Option, @@ -593,7 +480,7 @@ impl SobolSampler { } } - fn sample_dimension(&self, dimension: usize) -> Float { + fn sample_dimension(&self, dimension: u32) -> Float { if self.randomize == RandomizeStrategy::None { return sobol_sample(self.sobol_index, dimension, NoRandomizer); } @@ -614,41 +501,13 @@ impl SobolSampler { RandomizeStrategy::None => unreachable!(), } } - - pub fn create( - params: &ParameterDictionary, - full_res: Point2i, - loc: &FileLoc, - ) -> Result { - let options = get_options(); - let nsamp = options - .quick_render - .then_some(1) - .or(options.pixel_samples) - .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); - let seed = params.get_one_int("seed", options.seed); - let s = match params.get_one_string("randomization", "fastowen").as_str() { - "none" => RandomizeStrategy::None, - "permutedigits" => RandomizeStrategy::PermuteDigits, - "fastowen" => RandomizeStrategy::FastOwen, - "owen" => RandomizeStrategy::Owen, - _ => { - return Err(format!( - "{}: Unknown randomization strategy for ZSobol", - loc - )); - } - }; - - Ok(Self::new(nsamp as usize, full_res, s, Some(seed as u64))) - } } impl SamplerTrait for SobolSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { self.samples_per_pixel } - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option) { + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option) { self.pixel = p; self.dim = 2.max(dim.unwrap_or(0)); self.sobol_index = @@ -656,7 +515,7 @@ impl SamplerTrait for SobolSampler { } fn get1d(&mut self) -> Float { - if self.dim >= N_SOBOL_DIMENSIONS { + if self.dim >= N_SOBOL_DIMENSIONS as u32 { self.dim = 2; } @@ -666,7 +525,7 @@ impl SamplerTrait for SobolSampler { } fn get2d(&mut self) -> Point2f { - if self.dim >= N_SOBOL_DIMENSIONS { + if self.dim >= N_SOBOL_DIMENSIONS as u32 { self.dim = 2; } let u = Point2f::new( @@ -696,14 +555,15 @@ impl SamplerTrait for SobolSampler { } } -#[derive(Default, Debug, Clone)] +#[repr(C)] +#[derive(Default, Copy, Debug, Clone)] pub struct ZSobolSampler { randomize: RandomizeStrategy, seed: u64, log2_samples_per_pixel: u32, n_base4_digits: u32, morton_index: u64, - dim: usize, + dim: u32, } impl ZSobolSampler { @@ -769,7 +629,7 @@ impl ZSobolSampler { let mix_input = higher_digits ^ (0x55555555 * self.dim as u64); let p = (mix_bits(mix_input) >> 24) % 24; - digit = PERMUTATIONS[p as usize][digit as usize] as u64; + digit = PERMUTATIONS[p as u32][digit as u32] as u64; sample_index |= digit << digit_shift; } @@ -781,46 +641,13 @@ impl ZSobolSampler { sample_index } - - pub fn create( - params: &ParameterDictionary, - full_res: Point2i, - loc: &FileLoc, - ) -> Result { - let options = get_options(); - let nsamp = options - .quick_render - .then_some(1) - .or(options.pixel_samples) - .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); - let seed = params.get_one_int("seed", options.seed); - let s = match params.get_one_string("randomization", "fastowen").as_str() { - "none" => RandomizeStrategy::None, - "permutedigits" => RandomizeStrategy::PermuteDigits, - "fastowen" => RandomizeStrategy::FastOwen, - "owen" => RandomizeStrategy::Owen, - _ => { - return Err(format!( - "{}: Unknown randomization strategy for ZSobol", - loc - )); - } - }; - - Ok(ZSobolSampler::new( - nsamp as u32, - full_res, - s, - Some(seed as u64), - )) - } } impl SamplerTrait for ZSobolSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { todo!() } - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option) { + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option) { self.dim = dim.unwrap_or(0); self.morton_index = (encode_morton_2(p.x() as u32, p.y() as u32) << self.log2_samples_per_pixel) @@ -886,10 +713,10 @@ impl SamplerTrait for ZSobolSampler { #[derive(Default, Debug, Clone)] pub struct MLTSampler; impl SamplerTrait for MLTSampler { - fn samples_per_pixel(&self) -> usize { + fn samples_per_pixel(&self) -> u32 { todo!() } - fn start_pixel_sample(&mut self, _p: Point2i, _sample_index: usize, _dim: Option) { + fn start_pixel_sample(&mut self, _p: Point2i, _sample_index: u32, _dim: Option) { todo!() } fn get1d(&mut self) -> Float { @@ -905,8 +732,8 @@ impl SamplerTrait for MLTSampler { #[enum_dispatch] pub trait SamplerTrait { - fn samples_per_pixel(&self) -> usize; - fn start_pixel_sample(&mut self, p: Point2i, sample_index: usize, dim: Option); + fn samples_per_pixel(&self) -> u32; + fn start_pixel_sample(&mut self, p: Point2i, sample_index: u32, dim: Option); fn get1d(&mut self) -> Float; fn get2d(&mut self) -> Point2f; fn get_pixel2d(&mut self) -> Point2f; @@ -923,40 +750,3 @@ pub enum Sampler { ZSobol(ZSobolSampler), MLT(MLTSampler), } - -impl Sampler { - pub fn create( - name: &str, - params: &ParameterDictionary, - full_res: Point2i, - loc: &FileLoc, - ) -> Result { - match name { - "zsobol" => { - let sampler = ZSobolSampler::create(params, full_res, loc)?; - Ok(Sampler::ZSobol(sampler)) - } - "paddedsobol" => { - let sampler = PaddedSobolSampler::create(params, full_res, loc)?; - Ok(Sampler::PaddedSobol(sampler)) - } - "halton" => { - let sampler = HaltonSampler::create(params, full_res, loc)?; - Ok(Sampler::Halton(sampler)) - } - "sobol" => { - let sampler = SobolSampler::create(params, full_res, loc)?; - Ok(Sampler::Sobol(sampler)) - } - "Independent" => { - let sampler = IndependentSampler::create(params, full_res, loc)?; - Ok(Sampler::Independent(sampler)) - } - "stratified" => { - let sampler = StratifiedSampler::create(params, full_res, loc)?; - Ok(Sampler::Stratified(sampler)) - } - _ => Err(format!("Film type '{}' unknown at {}", name, loc)), - } - } -} diff --git a/shared/src/core/scattering.rs b/shared/src/core/scattering.rs index 2cc49fc..c7760a4 100644 --- a/shared/src/core/scattering.rs +++ b/shared/src/core/scattering.rs @@ -9,6 +9,7 @@ use crate::utils::sampling::sample_uniform_disk_polar; use num::complex::Complex; +#[repr(C)] #[derive(Debug, Default, Clone, Copy)] pub struct TrowbridgeReitzDistribution { alpha_x: Float, diff --git a/shared/src/core/shape.rs b/shared/src/core/shape.rs new file mode 100644 index 0000000..cc20cae --- /dev/null +++ b/shared/src/core/shape.rs @@ -0,0 +1,160 @@ +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, + Vector3fi, VectorLike, +}; +use crate::core::interaction::{ + Interaction, InteractionTrait, MediumInteraction, SurfaceInteraction, +}; +use crate::core::light::Light; +use crate::core::material::Material; +use crate::core::medium::{Medium, MediumInterface}; +use crate::core::pbrt::{Float, PI}; +use crate::shapes::*; +use crate::utils::Transform; +use crate::utils::math::{next_float_down, next_float_up}; +use enum_dispatch::enum_dispatch; + +// Define Intersection objects. This only varies for +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ShapeIntersection { + pub intr: SurfaceInteraction, + pub t_hit: Float, +} + +impl ShapeIntersection { + pub fn new(intr: SurfaceInteraction, t_hit: Float) -> Self { + Self { intr, t_hit } + } + + pub fn t_hit(&self) -> Float { + self.t_hit + } + + pub fn set_t_hit(&mut self, new_t: Float) { + self.t_hit = new_t; + } + + pub fn set_intersection_properties( + &mut self, + mtl: *const Material, + area: *const Light, + prim_medium_interface: *const MediumInterface, + ray_medium: *const Medium, + ) { + let ray_medium = unsafe { *prim_medium_interface }; + self.intr + .set_intersection_properties(mtl, area, prim_medium_interface, ray_medium); + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct QuadricIntersection { + pub t_hit: Float, + pub p_obj: Point3f, + pub phi: Float, +} + +impl QuadricIntersection { + pub fn new(t_hit: Float, p_obj: Point3f, phi: Float) -> Self { + Self { t_hit, p_obj, phi } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ShapeSample { + pub intr: Interaction, + pub pdf: Float, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ShapeSampleContext { + pub pi: Point3fi, + pub n: Normal3f, + pub ns: Normal3f, + pub time: Float, +} + +impl ShapeSampleContext { + pub fn new(pi: Point3fi, n: Normal3f, ns: Normal3f, time: Float) -> Self { + Self { pi, n, ns, time } + } + + pub fn new_from_interaction(si: &SurfaceInteraction) -> Self { + Self { + pi: si.pi(), + n: si.n(), + ns: si.shading.n, + time: si.time(), + } + } + + pub fn p(&self) -> Point3f { + Point3f::from(self.pi) + } + + pub fn offset_ray_origin(&self, w: Vector3f) -> Point3f { + let d = self.n.abs().dot(self.pi.error().into()); + let mut offset = d * Vector3f::from(self.n); + if w.dot(self.n.into()) < 0.0 { + offset = -offset; + } + + let mut po = Point3f::from(self.pi) + offset; + for i in 0..3 { + if offset[i] > 0.0 { + po[i] = next_float_up(po[i]); + } else { + po[i] = next_float_down(po[i]); + } + } + po + } + + pub fn offset_ray_origin_from_point(&self, pt: Point3f) -> Point3f { + self.offset_ray_origin(pt - self.p()) + } + + pub fn spawn_ray(&self, w: Vector3f) -> Ray { + Ray::new( + self.offset_ray_origin(w), + w, + Some(self.time), + core::ptr::null(), + ) + } +} + +#[enum_dispatch] +pub trait ShapeTrait { + fn bounds(&self) -> Bounds3f; + fn normal_bounds(&self) -> DirectionCone; + fn area(&self) -> Float; + fn sample(&self, u: Point2f) -> Option; + fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option; + fn intersect(&self, ray: &Ray, t_max: Option) -> Option; + fn intersect_p(&self, ray: &Ray, t_max: Option) -> bool; + fn pdf(&self, interaction: &Interaction) -> Float; + fn pdf_from_context(&self, ctx: &ShapeSampleContext, wi: Vector3f) -> Float; +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +#[enum_dispatch(ShapeTrait)] +pub enum Shape { + Sphere(SphereShape), + Cylinder(CylinderShape), + Disk(DiskShape), + Triangle(TriangleShape), + BilinearPatch(BilinearPatchShape), + Curve(CurveShape), +} + +impl Default for Shape { + fn default() -> Self { + Shape::Sphere(SphereShape::default()) + } +} diff --git a/shared/src/core/texture.rs b/shared/src/core/texture.rs index 8c36767..65613c3 100644 --- a/shared/src/core/texture.rs +++ b/shared/src/core/texture.rs @@ -2,13 +2,14 @@ use crate::core::color::ColorEncoding; use crate::core::geometry::{ Normal3f, Point2f, Point3f, Vector2f, Vector3f, VectorLike, spherical_phi, spherical_theta, }; +use crate::core::image::WrapMode; use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; -use crate::images::WrapMode; use crate::spectra::{ RGBAlbedoSpectrum, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, SampledWavelengths, }; use crate::textures::*; +use crate::utils::RelPtr; use crate::utils::Transform; use crate::utils::math::square; use crate::{Float, INV_2_PI, INV_PI, PI}; @@ -258,7 +259,7 @@ impl PointTransformMapping { } #[repr(C)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Default, Debug)] pub struct TextureEvalContext { pub p: Point3f, pub dpdx: Vector3f, @@ -269,7 +270,7 @@ pub struct TextureEvalContext { pub dudy: Float, pub dvdx: Float, pub dvdy: Float, - pub face_index: usize, + pub face_index: u32, } impl TextureEvalContext { @@ -284,7 +285,7 @@ impl TextureEvalContext { dudy: Float, dvdx: Float, dvdy: Float, - face_index: usize, + face_index: u32, ) -> Self { Self { p, @@ -308,7 +309,7 @@ impl From<&SurfaceInteraction> for TextureEvalContext { dpdx: si.dpdx, dpdy: si.dpdy, n: si.common.n, - uv: si.uv, + uv: si.common.uv, dudx: si.dudx, dudy: si.dudy, dvdx: si.dvdx, @@ -363,7 +364,7 @@ impl GPUFloatTexture { GPUFloatTexture::Dots(t) => t.evaluate(ctx), GPUFloatTexture::FBm(t) => t.evaluate(ctx), GPUFloatTexture::Windy(t) => t.evaluate(ctx), - GPUFloatTexture::Wrinkle(t) => t.evaluate(ctx), + GPUFloatTexture::Wrinkled(t) => t.evaluate(ctx), GPUFloatTexture::Ptex(t) => t.evaluate(ctx), GPUFloatTexture::Image(t) => t.evaluate(ctx), GPUFloatTexture::Mix(t) => t.evaluate(ctx), @@ -396,7 +397,11 @@ pub enum GPUSpectrumTexture { } impl GPUSpectrumTexture { - pub fn evaluate(&self, ctx: &TextureEvalContext, lambda: &SampledWavelengths) -> Float { + pub fn evaluate( + &self, + ctx: &TextureEvalContext, + lambda: &SampledWavelengths, + ) -> SampledSpectrum { match self { GPUSpectrumTexture::Constant(t) => t.evaluate(ctx, lambda), GPUSpectrumTexture::Bilerp(t) => t.evaluate(ctx, lambda), @@ -421,7 +426,11 @@ pub trait TextureEvaluator: Send + Sync { lambda: &SampledWavelengths, ) -> SampledSpectrum; - fn can_evaluate(&self, _ftex: &[&GPUFloatTexture], _stex: &[&GPUSpectrumTexture]) -> bool; + fn can_evaluate( + &self, + _ftex: &[RelPtr], + _stex: &[RelPtr], + ) -> bool; } #[repr(C)] @@ -444,8 +453,8 @@ impl TextureEvaluator for UniversalTextureEvaluator { fn can_evaluate( &self, - _float_textures: &[&GPUFloatTexture], - _spectrum_textures: &[&GPUSpectrumTexture], + _float_textures: &[RelPtr], + _spectrum_textures: &[RelPtr], ) -> bool { true } diff --git a/shared/src/data.rs b/shared/src/data.rs index 8d8f0cf..4bd4b90 100644 --- a/shared/src/data.rs +++ b/shared/src/data.rs @@ -2,55 +2,39 @@ use crate::Float; use bytemuck::cast_slice; use once_cell::sync::Lazy; -static SRGB_SCALE_BYTES: &[u8] = include_bytes!("../../data/srgb_scale.dat"); -static SRGB_COEFFS_BYTES: &[u8] = include_bytes!("../../data/srgb_coeffs.dat"); +#[repr(C, align(16))] +struct AlignedData(pub [u8; N]); -pub static SRGB_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(SRGB_SCALE_BYTES)); +macro_rules! load_static_table { + ($name:ident, $path:literal) => { + pub static $name: &[Float] = { + static RAW_DATA: AlignedData<{ include_bytes!($path).len() }> = + AlignedData(*include_bytes!($path)); -pub static SRGB_COEFFS: Lazy<&[Float]> = - Lazy::new(|| match bytemuck::try_cast_slice(SRGB_COEFFS_BYTES) { - Ok(s) => s, - Err(_) => { - let v: Vec = bytemuck::pod_collect_to_vec(SRGB_COEFFS_BYTES); - Box::leak(v.into_boxed_slice()) - } - }); + unsafe { + let bytes = &RAW_DATA.0; -static DCI_P3_SCALE_BYTES: &[u8] = include_bytes!("../../data/dcip3_scale.dat"); -static DCI_P3_COEFFS_BYTES: &[u8] = include_bytes!("../../data/dcip3_coeffs.dat"); -pub static DCI_P3_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(DCI_P3_SCALE_BYTES)); -pub static DCI_P3_COEFFS: Lazy<&[Float]> = - Lazy::new(|| match bytemuck::try_cast_slice(DCI_P3_COEFFS_BYTES) { - Ok(s) => s, - Err(_) => { - let v: Vec = bytemuck::pod_collect_to_vec(DCI_P3_COEFFS_BYTES); - Box::leak(v.into_boxed_slice()) - } - }); + let stride = core::mem::size_of::(); + let len = bytes.len() / stride; + debug_assert!( + bytes.len() % stride == 0, + "Data file size is not a multiple of Float size" + ); -static ACES_SCALE_BYTES: &[u8] = include_bytes!("../../data/aces_scale.dat"); -static ACES_COEFFS_BYTES: &[u8] = include_bytes!("../../data/aces_coeffs.dat"); + core::slice::from_raw_parts(bytes.as_ptr() as *const Float, len) + } + }; + }; +} -pub static ACES_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(ACES_SCALE_BYTES)); +load_static_table!(SRGB_SCALE, "../../data/srgb_scale.dat"); +load_static_table!(SRGB_COEFFS, "../../data/srgb_coeffs.dat"); -pub static ACES_COEFFS: Lazy<&[Float]> = - Lazy::new(|| match bytemuck::try_cast_slice(ACES_COEFFS_BYTES) { - Ok(s) => s, - Err(_) => { - let v: Vec = bytemuck::pod_collect_to_vec(ACES_COEFFS_BYTES); - Box::leak(v.into_boxed_slice()) - } - }); +load_static_table!(DCI_P3_SCALE, "../../data/dcip3_scale.dat"); +load_static_table!(DCI_P3_COEFFS, "../../data/dcip3_coeffs.dat"); -static REC2020_SCALE_BYTES: &[u8] = include_bytes!("../../data/rec2020_scale.dat"); -static REC2020_COEFFS_BYTES: &[u8] = include_bytes!("../../data/rec2020_coeffs.dat"); +load_static_table!(ACES_SCALE, "../../data/aces_scale.dat"); +load_static_table!(ACES_COEFFS, "../../data/aces_coeffs.dat"); -pub static REC2020_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(REC2020_SCALE_BYTES)); -pub static REC2020_COEFFS: Lazy<&[Float]> = - Lazy::new(|| match bytemuck::try_cast_slice(REC2020_COEFFS_BYTES) { - Ok(s) => s, - Err(_) => { - let v: Vec = bytemuck::pod_collect_to_vec(REC2020_COEFFS_BYTES); - Box::leak(v.into_boxed_slice()) - } - }); +load_static_table!(REC2020_SCALE, "../../data/rec2020_scale.dat"); +load_static_table!(REC2020_COEFFS, "../../data/rec2020_coeffs.dat"); diff --git a/shared/src/filters/boxf.rs b/shared/src/filters/boxf.rs index d7e087d..e01b553 100644 --- a/shared/src/filters/boxf.rs +++ b/shared/src/filters/boxf.rs @@ -1,6 +1,7 @@ use crate::Float; use crate::core::filter::FilterSample; use crate::core::geometry::{Point2f, Vector2f}; +use crate::utils::math::lerp; #[derive(Clone, Debug)] pub struct BoxFilter { diff --git a/shared/src/filters/gaussian.rs b/shared/src/filters/gaussian.rs index 408cda5..27bf802 100644 --- a/shared/src/filters/gaussian.rs +++ b/shared/src/filters/gaussian.rs @@ -1,4 +1,10 @@ -#[derive(Clone, Debug)] +use crate::Float; +use crate::core::filter::{FilterSample, FilterSampler}; +use crate::core::geometry::{Point2f, Vector2f}; +use crate::utils::math::{gaussian, gaussian_integral}; + +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct GaussianFilter { pub radius: Vector2f, pub sigma: Float, diff --git a/shared/src/filters/lanczos.rs b/shared/src/filters/lanczos.rs index ddbace9..0eaa5ed 100644 --- a/shared/src/filters/lanczos.rs +++ b/shared/src/filters/lanczos.rs @@ -1,4 +1,11 @@ -#[derive(Clone, Debug)] +use crate::Float; +use crate::core::filter::{FilterSample, FilterSampler}; +use crate::core::geometry::{Point2f, Vector2f}; +use crate::utils::math::{lerp, windowed_sinc}; +use rand::Rng; + +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct LanczosSincFilter { pub radius: Vector2f, pub tau: Float, diff --git a/shared/src/filters/mitchell.rs b/shared/src/filters/mitchell.rs index 1565985..634d50c 100644 --- a/shared/src/filters/mitchell.rs +++ b/shared/src/filters/mitchell.rs @@ -1,5 +1,5 @@ use crate::Float; -use crate::core::filter::FilterSampler; +use crate::core::filter::{FilterSample, FilterSampler}; use crate::core::geometry::{Point2f, Vector2f}; #[derive(Clone, Debug)] diff --git a/shared/src/filters/triangle.rs b/shared/src/filters/triangle.rs index 357f81a..29dab26 100644 --- a/shared/src/filters/triangle.rs +++ b/shared/src/filters/triangle.rs @@ -1,4 +1,10 @@ -#[derive(Clone, Debug)] +use crate::Float; +use crate::core::filter::FilterSample; +use crate::core::geometry::{Point2f, Vector2f}; +use crate::utils::math::sample_tent; + +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct TriangleFilter { pub radius: Vector2f, } diff --git a/shared/src/images/metadata.rs b/shared/src/images/metadata.rs deleted file mode 100644 index 6dcd4ee..0000000 --- a/shared/src/images/metadata.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::core::geometry::{Bounds2i, Point2i}; -use crate::core::pbrt::Float; -use crate::spectra::colorspace::RGBColorSpace; -use crate::utils::math::SquareMatrix; -use smallvec::SmallVec; -use std::collections::HashMap; - -use std::ops::{Deref, DerefMut}; - -#[derive(Clone, Debug, Default)] -pub struct ImageChannelValues(pub SmallVec<[Float; 4]>); - -impl ImageChannelValues { - pub fn average(&self) -> Float { - if self.0.is_empty() { - return 0.0; - } - let sum: Float = self.0.iter().sum(); - sum / (self.0.len() as Float) - } - - pub fn max_value(&self) -> Float { - self.0.iter().fold(Float::MIN, |a, &b| a.max(b)) - } -} - -impl From<&[Float]> for ImageChannelValues { - fn from(slice: &[Float]) -> Self { - Self(SmallVec::from_slice(slice)) - } -} - -impl From> for ImageChannelValues { - fn from(vec: Vec) -> Self { - Self(SmallVec::from_vec(vec)) - } -} - -impl From<[Float; N]> for ImageChannelValues { - fn from(arr: [Float; N]) -> Self { - Self(SmallVec::from_slice(&arr)) - } -} - -impl Deref for ImageChannelValues { - type Target = SmallVec<[Float; 4]>; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for ImageChannelValues { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum WrapMode { - Black, - Clamp, - Repeat, - OctahedralSphere, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct WrapMode2D { - pub uv: [WrapMode; 2], -} - -impl From for WrapMode2D { - fn from(w: WrapMode) -> Self { - Self { uv: [w, w] } - } -} - -#[derive(Debug, Clone, Default)] -pub struct ImageChannelDesc { - pub offset: Vec, -} - -impl ImageChannelDesc { - pub fn new(offset: &[usize]) -> Self { - Self { - offset: offset.into(), - } - } - - pub fn size(&self) -> usize { - self.offset.len() - } - - pub fn is_empty(&self) -> bool { - self.offset.is_empty() - } - pub fn is_identity(&self) -> bool { - for i in 0..self.size() { - if self.offset[i] != i { - return false; - } - } - true - } -} - -#[derive(Debug, Default)] -pub struct ImageMetadata { - pub render_time_seconds: Option, - pub camera_from_world: Option>, - pub ndc_from_world: Option>, - pub pixel_bounds: Option, - pub full_resolution: Option, - pub samples_per_pixel: Option, - pub mse: Option, - pub colorspace: Option, - pub strings: HashMap, - pub string_vectors: HashMap>, -} diff --git a/shared/src/images/mod.rs b/shared/src/images/mod.rs deleted file mode 100644 index 079a1fd..0000000 --- a/shared/src/images/mod.rs +++ /dev/null @@ -1,443 +0,0 @@ -pub mod metadata; -pub mod ops; -pub mod pixel; - -use crate::core::geometry::{Bounds2f, Point2f, Point2fi, Point2i}; -use crate::core::pbrt::Float; -use crate::spectra::color::{ColorEncoding, ColorEncodingTrait, LINEAR}; -use crate::utils::containers::Array2D; -use crate::utils::math::{lerp, square}; -use core::hash; -use half::f16; -use pixel::PixelStorage; -use rayon::prelude::*; -use smallvec::{SmallVec, smallvec}; -use std::ops::{Deref, DerefMut}; - -pub use metadata::{ImageChannelDesc, ImageChannelValues, ImageMetadata, WrapMode, WrapMode2D}; - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum PixelFormat { - U8, - F16, - F32, -} - -impl PixelFormat { - pub fn is_8bit(&self) -> bool { - matches!(self, PixelFormat::U8) - } - - pub fn is_16bit(&self) -> bool { - matches!(self, PixelFormat::F16) - } - - pub fn is_32bit(&self) -> bool { - matches!(self, PixelFormat::F32) - } - - pub fn texel_bytes(&self) -> usize { - match self { - PixelFormat::U8 => 1, - PixelFormat::F16 => 2, - PixelFormat::F32 => 4, - } - } -} - -impl std::fmt::Display for PixelFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PixelFormat::U8 => write!(f, "U256"), - PixelFormat::F16 => write!(f, "Half"), - PixelFormat::F32 => write!(f, "Float"), - } - } -} - -#[derive(Debug, Clone)] -pub enum PixelData { - U8(Vec), - F16(Vec), - F32(Vec), -} - -#[derive(Debug, Clone)] -pub struct Image { - pub format: PixelFormat, - pub resolution: Point2i, - pub channel_names: Vec, - pub encoding: ColorEncoding, - pub pixels: PixelData, -} - -#[derive(Debug)] -pub struct ImageAndMetadata { - pub image: Image, - pub metadata: ImageMetadata, -} - -impl Image { - fn from_vector( - format: PixelFormat, - resolution: Point2i, - channel_names: Vec, - encoding: ColorEncoding, - ) -> Self { - let size = (resolution.x() * resolution.y()) as usize * channel_names.len(); - - let pixels = match format { - PixelFormat::U8 => PixelData::U8(vec![0; size]), - PixelFormat::F16 => PixelData::F16(vec![f16::ZERO; size]), - PixelFormat::F32 => PixelData::F32(vec![0.0; size]), - }; - - Self { - format, - resolution, - channel_names, - encoding, - pixels, - } - } - - pub fn new( - format: PixelFormat, - resolution: Point2i, - channel_names: &[&str], - encoding: ColorEncoding, - ) -> Self { - let owned_names = channel_names.iter().map(|s| s.to_string()).collect(); - Self::from_vector(format, resolution, owned_names, encoding) - } - - pub fn format(&self) -> PixelFormat { - self.format - } - pub fn resolution(&self) -> Point2i { - self.resolution - } - pub fn n_channels(&self) -> usize { - self.channel_names.len() - } - pub fn channel_names(&self) -> Vec<&str> { - self.channel_names.iter().map(|s| s.as_str()).collect() - } - pub fn channel_names_from_desc(&self, desc: &ImageChannelDesc) -> Vec<&str> { - desc.offset - .iter() - .map(|&i| self.channel_names[i].as_str()) - .collect() - } - pub fn encoding(&self) -> ColorEncoding { - self.encoding - } - - pub fn pixel_offset(&self, p: Point2i) -> usize { - (p.y() as usize * self.resolution.x() as usize + p.x() as usize) * self.n_channels() - } - - pub fn get_channel(&self, p: Point2i, c: usize) -> Float { - self.get_channel_with_wrap(p, c, WrapMode::Clamp.into()) - } - - pub fn get_channel_with_wrap(&self, p: Point2i, c: usize, wrap: WrapMode2D) -> Float { - let mut pp = p; - if !self.remap_pixel_coords(&mut pp, wrap) { - return 0.0; - } - - let idx = self.pixel_offset(pp) + c; - match &self.pixels { - PixelData::U8(d) => u8::to_linear(d[idx], self.encoding), - PixelData::F16(d) => f16::to_linear(d[idx], self.encoding), - PixelData::F32(d) => f32::to_linear(d[idx], self.encoding), - } - } - - pub fn get_channels(&self, p: Point2i, wrap: WrapMode2D) -> ImageChannelValues { - let mut pp = p; - - if !self.remap_pixel_coords(&mut pp, wrap) { - return ImageChannelValues(smallvec![0.0; self.n_channels()]); - } - - let start_idx = self.pixel_offset(pp); - let n_channels = self.n_channels(); - - let mut values: SmallVec<[Float; 4]> = SmallVec::with_capacity(n_channels); - - match &self.pixels { - PixelData::U8(data) => { - let slice = &data[start_idx..start_idx + n_channels]; - for &v in slice { - values.push(u8::to_linear(v, self.encoding)); - } - } - PixelData::F16(data) => { - let slice = &data[start_idx..start_idx + n_channels]; - for &v in slice { - values.push(f16::to_linear(v, self.encoding)); - } - } - PixelData::F32(data) => { - let slice = &data[start_idx..start_idx + n_channels]; - for &v in slice { - values.push(f32::to_linear(v, self.encoding)); - } - } - } - - ImageChannelValues(values) - } - - pub fn get_channels_desc( - &self, - p: Point2i, - desc: &ImageChannelDesc, - wrap: WrapMode2D, - ) -> ImageChannelValues { - let mut pp = p; - if !self.remap_pixel_coords(&mut pp, wrap) { - return ImageChannelValues(smallvec![0.0; desc.offset.len()]); - } - - let pixel_offset = self.pixel_offset(pp); - - let mut values: SmallVec<[Float; 4]> = SmallVec::with_capacity(desc.offset.len()); - - match &self.pixels { - PixelData::U8(data) => { - for &channel_idx in &desc.offset { - let val = data[pixel_offset + channel_idx]; - values.push(u8::to_linear(val, self.encoding)); - } - } - PixelData::F16(data) => { - for &channel_idx in &desc.offset { - let val = data[pixel_offset + channel_idx]; - values.push(f16::to_linear(val, self.encoding)); - } - } - PixelData::F32(data) => { - for &channel_idx in &desc.offset { - let val = data[pixel_offset + channel_idx]; - values.push(f32::to_linear(val, self.encoding)); - } - } - } - - ImageChannelValues(values) - } - - pub fn get_channels_default(&self, p: Point2i) -> ImageChannelValues { - self.get_channels(p, WrapMode::Clamp.into()) - } - - pub fn all_channels_desc(&self) -> ImageChannelDesc { - ImageChannelDesc { - offset: (0..self.n_channels()).collect(), - } - } - - pub fn get_channel_desc( - &self, - requested_channels: &[&str], - ) -> Result { - let mut offset = Vec::with_capacity(requested_channels.len()); - - for &req in requested_channels.iter() { - match self.channel_names.iter().position(|n| n == req) { - Some(idx) => { - offset.push(idx); - } - None => { - return Err(format!( - "Image is missing requested channel '{}'. Available channels: {:?}", - req, self.channel_names - )); - } - } - } - - Ok(ImageChannelDesc { offset }) - } - - pub fn set_channel(&mut self, p: Point2i, c: usize, value: Float) { - let val_no_nan = if value.is_nan() { 0.0 } else { value }; - let offset = self.pixel_offset(p) + c; - match &mut self.pixels { - PixelData::U8(data) => { - let linear = [val_no_nan]; - self.encoding - .from_linear_slice(&linear, &mut data[offset..offset + 1]); - } - PixelData::F16(data) => data[offset] = f16::from_f32(val_no_nan), - PixelData::F32(data) => data[offset] = val_no_nan, - } - } - - pub fn set_channels( - &mut self, - p: Point2i, - desc: &ImageChannelDesc, - values: &ImageChannelValues, - ) { - assert_eq!(desc.size(), values.len()); - for i in 0..desc.size() { - self.set_channel(p, desc.offset[i], values[i]); - } - } - - pub fn set_channels_all(&mut self, p: Point2i, values: &ImageChannelValues) { - self.set_channels(p, &self.all_channels_desc(), values) - } - - fn remap_pixel_coords(&self, p: &mut Point2i, wrap_mode: WrapMode2D) -> bool { - for i in 0..2 { - if p[i] >= 0 && p[i] < self.resolution[i] { - continue; - } - match wrap_mode.uv[i] { - WrapMode::Black => return false, - WrapMode::Clamp => p[i] = p[i].clamp(0, self.resolution[i] - 1), - WrapMode::Repeat => p[i] = p[i].rem_euclid(self.resolution[i]), - WrapMode::OctahedralSphere => { - p[i] = p[i].clamp(0, self.resolution[i] - 1); - } - } - } - true - } - - pub fn bilerp_channel(&self, p: Point2f, c: usize) -> Float { - self.bilerp_channel_with_wrap(p, c, WrapMode::Clamp.into()) - } - - pub fn bilerp_channel_with_wrap(&self, p: Point2f, c: usize, wrap_mode: WrapMode2D) -> Float { - let x = p.x() * self.resolution.x() as Float - 0.5; - let y = p.y() * self.resolution.y() as Float - 0.5; - let xi = x.floor() as i32; - let yi = y.floor() as i32; - let dx = x - xi as Float; - let dy = y - yi as Float; - let v00 = self.get_channel_with_wrap(Point2i::new(xi, yi), c, wrap_mode); - let v10 = self.get_channel_with_wrap(Point2i::new(xi + 1, yi), c, wrap_mode); - let v01 = self.get_channel_with_wrap(Point2i::new(xi, yi + 1), c, wrap_mode); - let v11 = self.get_channel_with_wrap(Point2i::new(xi + 1, yi + 1), c, wrap_mode); - lerp(dy, lerp(dx, v00, v10), lerp(dx, v01, v11)) - } - - pub fn lookup_nearest_channel_with_wrap( - &self, - p: Point2f, - c: usize, - wrap_mode: WrapMode2D, - ) -> Float { - let pi = Point2i::new( - p.x() as i32 * self.resolution.x(), - p.y() as i32 * self.resolution.y(), - ); - - self.get_channel_with_wrap(pi, c, wrap_mode) - } - - pub fn lookup_nearest_channel(&self, p: Point2f, c: usize) -> Float { - self.lookup_nearest_channel_with_wrap(p, c, WrapMode::Clamp.into()) - } - - pub fn get_sampling_distribution(&self, dxd_a: F, domain: Bounds2f) -> Array2D - where - F: Fn(Point2f) -> Float + Sync + Send, - { - let width = self.resolution.x(); - let height = self.resolution.y(); - - let mut dist = Array2D::new_with_dims(width as usize, height as usize); - - dist.values - .par_chunks_mut(width as usize) - .enumerate() - .for_each(|(y, row)| { - let y = y as i32; - - for (x, out_val) in row.iter_mut().enumerate() { - let x = x as i32; - - let value = self.get_channels_default(Point2i::new(x, y)).average(); - - let u = (x as Float + 0.5) / width as Float; - let v = (y as Float + 0.5) / height as Float; - let p = domain.lerp(Point2f::new(u, v)); - *out_val = value * dxd_a(p); - } - }); - - dist - } - - pub fn get_sampling_distribution_uniform(&self) -> Array2D { - let default_domain = Bounds2f::from_points(Point2f::new(0.0, 0.0), Point2f::new(1.0, 1.0)); - - self.get_sampling_distribution(|_| 1.0, default_domain) - } - - pub fn mse( - &self, - desc: ImageChannelDesc, - ref_img: &Image, - generate_mse_image: bool, - ) -> (ImageChannelValues, Option) { - let mut sum_se: Vec = vec![0.; desc.size()]; - let names_ref = self.channel_names_from_desc(&desc); - let ref_desc = ref_img - .get_channel_desc(&self.channel_names_from_desc(&desc)) - .expect("Channels not found in image"); - assert_eq!(self.resolution(), ref_img.resolution()); - - let width = self.resolution.x() as usize; - let height = self.resolution.y() as usize; - let n_channels = desc.offset.len(); - let mut mse_pixels = if generate_mse_image { - vec![0.0f32; width * height * n_channels] - } else { - Vec::new() - }; - - for y in 0..self.resolution().y() { - for x in 0..self.resolution().x() { - let v = self.get_channels_desc(Point2i::new(x, y), &desc, WrapMode::Clamp.into()); - let v_ref = - self.get_channels_desc(Point2i::new(x, y), &ref_desc, WrapMode::Clamp.into()); - for c in 0..desc.size() { - let se = square(v[c] as f64 - v_ref[c] as f64); - if se.is_infinite() { - continue; - } - sum_se[c] += se; - if generate_mse_image { - let idx = (y as usize * width + x as usize) * n_channels + c; - mse_pixels[idx] = se as f32; - } - } - } - } - - let pixel_count = (self.resolution.x() * self.resolution.y()) as f64; - let mse_values: SmallVec<[Float; 4]> = - sum_se.iter().map(|&s| (s / pixel_count) as Float).collect(); - - let mse_image = if generate_mse_image { - Some(Image::new( - PixelFormat::F32, - self.resolution, - &names_ref, - LINEAR, - )) - } else { - None - }; - - (ImageChannelValues(mse_values), mse_image) - } -} diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 9252dc8..789a249 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -2,16 +2,15 @@ #![feature(float_erf)] #![feature(f16)] -mod cameras; -mod core; -mod data; -mod filters; -mod images; -mod integrators; -mod lights; -mod shapes; -mod spectra; -mod textures; -mod utils; +pub mod cameras; +pub mod core; +pub mod data; +pub mod filters; +pub mod lights; +pub mod materials; +pub mod shapes; +pub mod spectra; +pub mod textures; +pub mod utils; pub use core::pbrt::*; diff --git a/shared/src/lights/diffuse.rs b/shared/src/lights/diffuse.rs index ff54d80..fa28cc9 100644 --- a/shared/src/lights/diffuse.rs +++ b/shared/src/lights/diffuse.rs @@ -1,106 +1,48 @@ use crate::PI; use crate::core::color::{RGB, XYZ}; use crate::core::geometry::*; -use crate::core::interaction::{Interaction, MediumInteraction, SurfaceInteraction}; +use crate::core::image::Image; +use crate::core::interaction::{ + Interaction, InteractionTrait, MediumInteraction, SurfaceInteraction, +}; use crate::core::light::{ LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, }; use crate::core::medium::MediumInterface; use crate::core::pbrt::Float; +use crate::core::shape::{Shape, ShapeSampleContext, ShapeTrait}; use crate::core::spectrum::{Spectrum, SpectrumTrait}; -use crate::core::texture::{GPUFloatTexture, TextureEvalContext, UniversalTextureEvaluator}; -use crate::images::Image; -use crate::shapes::{Shape, ShapeSampleContext}; +use crate::core::texture::{ + GPUFloatTexture, TextureEvalContext, TextureEvaluator, UniversalTextureEvaluator, +}; use crate::spectra::*; -use crate::utils::Transform; use crate::utils::hash::hash_float; +use crate::utils::{Ptr, Transform}; -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct DiffuseAreaLight { pub base: LightBase, - pub shape: *const Shape, - pub alpha: *const GPUFloatTexture, + pub shape: Ptr, + pub alpha: Ptr, + pub image_color_space: Ptr, + pub lemit: Ptr, + pub image: Ptr, pub area: Float, pub two_sided: bool, - pub lemit: DenselySampledSpectrum, pub scale: Float, - pub image: *const Image, - pub image_color_space: RGBColorSpace, } +unsafe impl Send for DiffuseAreaLight {} +unsafe impl Sync for DiffuseAreaLight {} + #[cfg(not(target_os = "cuda"))] impl DiffuseAreaLight { - #[allow(clippy::too_many_arguments)] - pub fn new( - render_from_light: Transform, - medium_interface: MediumInterface, - le: Spectrum, - scale: Float, - shape: Shape, - alpha: *const GPUFloatTexture, - image: *const Image, - image_color_space: *const RGBColorSpace, - two_sided: bool, - ) -> Self { - let is_constant_zero = match &alpha { - 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 base = LightBase::new(light_type, &render_from_light, &medium_interface); - - let lemit = LightBase::lookup_spectrum(&le); - if let Some(im) = &image { - let desc = im - .get_channel_desc(&["R", "G", "B"]) - .expect("Image used for DiffuseAreaLight doesn't have R, G, B channels"); - - assert_eq!(3, desc.size(), "Image channel description size mismatch"); - assert!( - desc.is_identity(), - "Image channel description is not 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! \ - 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." - ); - } - - Self { - base, - area: shape.area(), - shape, - alpha: stored_alpha, - two_sided, - lemit, - scale, - image, - image_color_space, - } - } - fn l_base(&self, n: Normal3f, wo: Vector3f, lambda: &SampledWavelengths) -> SampledSpectrum { - if !self.two_sided && n.dot(wo) <= 0.0 { + if !self.two_sided && n.dot(wo.into()) <= 0.0 { return SampledSpectrum::new(0.0); } - let spec = DenselySampledSpectrum::from_array(&self.lemit_coeffs); - spec.sample(lambda) * self.scale + self.lemit.sample(lambda) * self.scale } fn alpha_masked(&self, intr: &Interaction) -> bool { @@ -133,9 +75,9 @@ impl LightTrait for DiffuseAreaLight { ) -> Option { let shape_ctx = ShapeSampleContext::new(ctx.pi, ctx.n, ctx.ns, 0.0); let ss = self.shape.sample_from_context(&shape_ctx, u)?; - let mut intr: SurfaceInteraction = ss.intr.as_ref().clone(); + let mut intr: SurfaceInteraction = ss.intr; - intr.common.medium_interface = Some(self.base.medium_interface.clone()); + intr.common.medium_interface = self.base.medium_interface; let p = intr.p(); let n = intr.n(); let uv = intr.uv; @@ -234,7 +176,7 @@ impl LightTrait for DiffuseAreaLight { #[cfg(not(target_os = "cuda"))] fn bounds(&self) -> Option { let mut phi = 0.; - if let Some(image) = &self.image { + if !self.image.is_null() { for y in 0..image.resolution.y() { for x in 0..image.resolution.x() { for c in 0..3 { diff --git a/shared/src/lights/distant.rs b/shared/src/lights/distant.rs index 62cb906..82b9b86 100644 --- a/shared/src/lights/distant.rs +++ b/shared/src/lights/distant.rs @@ -1,14 +1,17 @@ -use crate::core::geometry::{Bounds3f, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector3f}; -use crate::core::interaction::{Interaction, InteractionData}; +use crate::core::geometry::{ + Bounds3f, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector3f, VectorLike, +}; +use crate::core::interaction::{Interaction, InteractionBase, SimpleInteraction}; use crate::core::light::{LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait}; use crate::spectra::{DenselySampledSpectrum, SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; use crate::{Float, PI}; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct DistantLight { pub base: LightBase, - pub lemit_coeffs: [Float; 32], + pub lemit: Ptr, pub scale: Float, pub scene_center: Point3f, pub scene_radius: Float, @@ -26,8 +29,7 @@ impl DistantLight { .apply_to_vector(Vector3f::new(0., 0., 1.)) .normalize(); let p_outside = ctx_p + wi * 2. * self.scene_radius; - let spectrum = DenselySampledSpectrum::from_array(&self.lemit_coeffs); - let li = self.scale * spectrum.sample(lambda); + let li = self.scale * self.lemit.sample(lambda); (li, wi, 1.0, p_outside) } } @@ -59,10 +61,10 @@ impl LightTrait for DistantLight { let intr = SimpleInteraction::new( Point3fi::new_from_point(p_outside), 0.0, - Some(self.base.medium_interface.clone()), + self.base.medium_interface, ); - Some(LightLiSample::new(li, wi, 1., Interaction::Simple(intr))) + Some(LightLiSample::new(li, wi, 1., intr)) } fn pdf_li( diff --git a/shared/src/lights/goniometric.rs b/shared/src/lights/goniometric.rs index ef49c99..824a864 100644 --- a/shared/src/lights/goniometric.rs +++ b/shared/src/lights/goniometric.rs @@ -1,21 +1,27 @@ -use crate::Float; -use crate::core::geometry::{Bounds3f, Normal3f, Point2f, Point3f, Vector3f}; -use crate::core::light::{LightBase, LightBounds, LightLiSample, LightTrait}; +use crate::core::geometry::{Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector3f}; +use crate::core::image::Image; +use crate::core::light::{ + LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, +}; use crate::core::medium::MediumInterface; -use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::core::spectrum::Spectrum; +use crate::spectra::{DenselySampledSpectrum, SampledSpectrum, SampledWavelengths}; use crate::utils::Transform; +use crate::utils::math::equal_area_sphere_to_square; use crate::utils::sampling::PiecewiseConstant2D; +use crate::{Float, PI}; #[derive(Debug, Clone)] pub struct GoniometricLight { pub base: LightBase, iemit: DenselySampledSpectrum, scale: Float, - image: Image, - distrib: PiecewiseConstant2D, + image: *const Image, + distrib: *const PiecewiseConstant2D, } impl GoniometricLight { + #[cfg(not(target_os = "cuda"))] pub fn new( render_from_light: &Transform, medium_interface: &MediumInterface, diff --git a/shared/src/lights/infinite.rs b/shared/src/lights/infinite.rs index dc223e5..773ddc3 100644 --- a/shared/src/lights/infinite.rs +++ b/shared/src/lights/infinite.rs @@ -1,9 +1,8 @@ use crate::{ core::geometry::Frame, - core::medium::Medium, - spectra::{RGB, RGBColorSpace, RGBIlluminantSpectrum}, + spectra::{RGBColorSpace, RGBIlluminantSpectrum}, utils::{ - math::{clamp, equal_area_square_to_sphere}, + math::{clamp, equal_area_sphere_to_square, equal_area_square_to_sphere, square}, sampling::{ AliasTable, PiecewiseConstant2D, WindowedPiecewiseConstant2D, sample_uniform_sphere, uniform_sphere_pdf, @@ -11,7 +10,21 @@ use crate::{ }, }; -use crate::images::{PixelFormat, WrapMode}; +use crate::core::color::RGB; +use crate::core::geometry::{ + Bounds2f, Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector2f, Vector3f, +}; +use crate::core::image::{Image, PixelFormat, WrapMode}; +use crate::core::interaction::{Interaction, SimpleInteraction}; +use crate::core::light::{ + LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, +}; +use crate::core::medium::{Medium, MediumInterface}; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::Transform; +use crate::{Float, PI}; +use std::sync::Arc; #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -25,7 +38,7 @@ pub struct InfiniteUniformLight { #[cfg(not(target_os = "cuda"))] impl InfiniteUniformLight { - pub fn new(render_from_light: TransformGeneric, le: Spectrum, scale: Float) -> Self { + pub fn new(render_from_light: Transform, le: Spectrum, scale: Float) -> Self { let base = LightBase::new( LightType::Infinite, &render_from_light, @@ -109,78 +122,31 @@ impl LightTrait for InfiniteUniformLight { } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] pub struct InfiniteImageLight { - base: LightBase, - image: Image, - image_color_space: u32, - scale: Float, - scene_radius: Float, - scene_center: Point3f, - distrib: PiecewiseConstant2D, - compensated_distrib: PiecewiseConstant2D, + pub base: LightBase, + pub image: *const Image, + pub image_color_space: *const RGBColorSpace, + pub distrib: *const PiecewiseConstant2D, + pub compensated_distrib: *const PiecewiseConstant2D, + pub scale: Float, + pub scene_radius: Float, + pub scene_center: Point3f, } -#[cfg(not(target_os = "cuda"))] +unsafe impl Send for InfiniteImageLight {} +unsafe impl Sync for InfiniteImageLight {} + impl InfiniteImageLight { - pub fn new( - render_from_light: TransformGeneric, - image: Image, - image_color_space: Arc, - scale: Float, - filename: String, - ) -> Self { - let base = LightBase::new( - LightType::Infinite, - &render_from_light, - &MediumInterface::default(), - ); + #[inline(always)] + fn color_space(&self) -> &RGBColorSpace { + unsafe { &*self.image_color_space } + } - let desc = image - .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()); - if image.resolution().x() != image.resolution().y() { - panic!( - "{}: image resolution ({}, {}) is non-square. It's unlikely this is an equal area environment map.", - filename, - image.resolution.x(), - image.resolution.y() - ); - } - 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 slice = &mut d.values; // or d.as_slice_mut() - let count = slice.len() as Float; - let sum: Float = slice.iter().sum(); - let average = sum / count; - - for v in slice.iter_mut() { - *v = (*v - average).max(0.0); - } - - let all_zero = slice.iter().all(|&v| v == 0.0); - if all_zero { - for v in slice.iter_mut() { - *v = 1.0; - } - } - - let compensated_distrib = PiecewiseConstant2D::new_with_bounds(&d, domain); - - Self { - base, - image, - image_color_space, - scene_center: Point3f::default(), - scene_radius: 0., - scale, - distrib, - compensated_distrib, - } + #[inline(always)] + fn image(&self) -> &Image { + unsafe { &*self.image } } fn image_le(&self, uv: Point2f, lambda: &SampledWavelengths) -> SampledSpectrum { @@ -192,8 +158,7 @@ impl InfiniteImageLight { WrapMode::OctahedralSphere.into(), ); } - let spec = - RGBIlluminantSpectrum::new(self.image_color_space.as_ref(), RGB::clamp_zero(rgb)); + let spec = RGBIlluminantSpectrum::new(self.color_space(), RGB::clamp_zero(rgb)); self.scale * spec.sample(lambda) } } @@ -312,7 +277,6 @@ pub struct InfinitePortalLight { pub image: Image, pub image_color_space: RGBColorSpace, pub scale: Float, - pub filename: String, pub portal: [Point3f; 4], pub portal_frame: Frame, pub distribution: WindowedPiecewiseConstant2D, @@ -320,137 +284,7 @@ pub struct InfinitePortalLight { pub scene_radius: Float, } -#[cfg(not(target_os = "cuda"))] impl InfinitePortalLight { - pub fn new( - render_from_light: TransformGeneric, - equal_area_image: &Image, - image_color_space: Arc, - scale: Float, - filename: String, - points: Vec, - ) -> Self { - let base = LightBase::new( - LightType::Infinite, - &render_from_light, - &MediumInterface::default(), - ); - - let desc = equal_area_image - .get_channel_desc(&["R", "G", "B"]) - .unwrap_or_else(|_| { - panic!( - "{}: image used for PortalImageInfiniteLight doesn't have R, G, B channels.", - filename - ) - }); - - assert_eq!(3, desc.offset.len()); - let src_res = equal_area_image.resolution; - if src_res.x() != src_res.y() { - panic!( - "{}: image resolution ({}, {}) is non-square. It's unlikely this is an equal area environment map.", - filename, - src_res.x(), - src_res.y() - ); - } - - if points.len() != 4 { - panic!( - "Expected 4 vertices for infinite light portal but given {}", - points.len() - ); - } - - let portal: [Point3f; 4] = [points[0], points[1], points[2], points[3]]; - - let p01 = (portal[1] - portal[0]).normalize(); - let p12 = (portal[2] - portal[1]).normalize(); - let p32 = (portal[2] - portal[3]).normalize(); - let p03 = (portal[3] - portal[0]).normalize(); - - if (p01.dot(p32) - 1.0).abs() > 0.001 || (p12.dot(p03) - 1.0).abs() > 0.001 { - panic!("Infinite light portal isn't a planar quadrilateral (opposite edges)"); - } - - if p01.dot(p12).abs() > 0.001 - || p12.dot(p32).abs() > 0.001 - || p32.dot(p03).abs() > 0.001 - || p03.dot(p01).abs() > 0.001 - { - panic!("Infinite light portal isn't a planar quadrilateral (perpendicular edges)"); - } - - let portal_frame = Frame::from_xy(p03, p01); - - let width = src_res.x(); - let height = src_res.y(); - - let mut new_pixels = vec![0.0 as Float; (width * height * 3) as usize]; - - new_pixels - .par_chunks_mut((width * 3) as usize) - .enumerate() - .for_each(|(y, row_pixels)| { - let y = y as i32; - - for x in 0..width { - let uv = Point2f::new( - (x as Float + 0.5) / width as Float, - (y as Float + 0.5) / height as Float, - ); - - let (w_world, _) = Self::render_from_image(portal_frame, uv); - let w_local = render_from_light.apply_inverse_vector(w_world).normalize(); - let uv_equi = equal_area_sphere_to_square(w_local); - - let pixel_idx = (x * 3) as usize; - - for c in 0..3 { - let val = equal_area_image.bilerp_channel_with_wrap( - uv_equi, - c, - WrapMode::OctahedralSphere.into(), - ); - row_pixels[pixel_idx + c] = val; - } - } - }); - - let image = Image::new( - PixelFormat::F32, - src_res, - &["R", "G", "B"], - equal_area_image.encoding, - ); - - let duv_dw_closure = |p: Point2f| -> Float { - let (_, jacobian) = Self::render_from_image(portal_frame, p); - jacobian - }; - - let d = image.get_sampling_distribution( - duv_dw_closure, - Bounds2f::from_points(Point2f::new(0., 0.), Point2f::new(1., 1.)), - ); - - let distribution = WindowedPiecewiseConstant2D::new(d); - - Self { - base, - image, - image_color_space, - scale, - scene_center: Point3f::default(), - scene_radius: 0., - filename, - portal, - portal_frame, - distribution, - } - } - pub fn image_lookup(&self, uv: Point2f, lambda: &SampledWavelengths) -> SampledSpectrum { let mut rgb = RGB::default(); for c in 0..3 { @@ -571,8 +405,8 @@ impl LightTrait for InfinitePortalLight { } #[cfg(not(target_os = "cuda"))] - fn preprocess(&mut self, _scene_bounds: &Bounds3f) { - todo!() + fn preprocess(&mut self, scene_bounds: &Bounds3f) { + (self.scene_center, self.scene_radius) = scene_bounds.bounding_sphere(); } #[cfg(not(target_os = "cuda"))] diff --git a/shared/src/lights/mod.rs b/shared/src/lights/mod.rs index 5b2c3e6..4006aaf 100644 --- a/shared/src/lights/mod.rs +++ b/shared/src/lights/mod.rs @@ -13,3 +13,4 @@ pub use goniometric::GoniometricLight; pub use infinite::{InfiniteImageLight, InfinitePortalLight, InfiniteUniformLight}; pub use point::PointLight; pub use projection::ProjectionLight; +pub use spot::SpotLight; diff --git a/shared/src/lights/point.rs b/shared/src/lights/point.rs index e789e39..2c908d1 100644 --- a/shared/src/lights/point.rs +++ b/shared/src/lights/point.rs @@ -1,17 +1,20 @@ -use crate::Float; -use crate::core::light::LightBaseData; -use crate::spectra::SampledSpectrum; -use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::core::geometry::{Bounds3f, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector3f}; +use crate::core::interaction::{Interaction, SimpleInteraction}; +use crate::core::light::{ + Light, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, +}; +use crate::spectra::{DenselySampledSpectrum, SampledSpectrum, SampledWavelengths}; +use crate::{Float, PI}; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct PointLight { - pub base: LightBasea, - pub i: + pub base: LightBase, pub scale: Float, + pub i: *const DenselySampledSpectrum, } -impl LightTrait for PointLight { +impl PointLight { fn sample_li_base( &self, ctx_p: Point3f, @@ -27,7 +30,9 @@ impl LightTrait for PointLight { let li = self.scale * spectrum.sample(lambda) / p.distance_squared(ctx_p); (li, wi, 1.0, pi) } +} +impl LightTrait for PointLight { fn base(&self) -> &LightBase { &self.base } diff --git a/shared/src/lights/projection.rs b/shared/src/lights/projection.rs index 4fdbd60..1e2ec2e 100644 --- a/shared/src/lights/projection.rs +++ b/shared/src/lights/projection.rs @@ -1,5 +1,17 @@ +use crate::Float; +use crate::core::color::RGB; +use crate::core::geometry::{ + Bounds2f, Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector3f, VectorLike, cos_theta, +}; +use crate::core::image::Image; +use crate::core::light::{ + LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, +}; +use crate::core::medium::MediumInterface; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::math::{radians, square}; use crate::{ - spectra::{RGB, RGBColorSpace}, + spectra::{RGBColorSpace, RGBIlluminantSpectrum}, utils::{Transform, sampling::PiecewiseConstant2D}, }; @@ -12,65 +24,14 @@ pub struct ProjectionLight { pub screen_bounds: Bounds2f, pub screen_from_light: Transform, pub light_from_screen: Transform, - pub image_id: u32, pub a: Float, - pub distrib: PiecewiseConstant2D, + pub image: *const Image, + pub distrib: *const PiecewiseConstant2D, pub image_color_space: *const RGBColorSpace, } impl ProjectionLight { - pub fn new( - render_from_light: Transform, - medium_interface: MediumInterface, - image_id: u32, - image_color_space: RGBColorSpace, - scale: Float, - fov: Float, - ) -> Self { - let base = LightBase::new( - LightType::DeltaPosition, - &render_from_light, - &medium_interface, - ); - let image = Image::new(); - let aspect = image.resolution().x() as Float / image.resolution().y() as Float; - let screen_bounds = if aspect > 1. { - Bounds2f::from_points(Point2f::new(-aspect, -1.), Point2f::new(aspect, 1.)) - } else { - Bounds2f::from_points( - Point2f::new(-1., 1. / aspect), - Point2f::new(1., 1. / aspect), - ) - }; - - let hither = 1e-3; - let screen_from_light = TransformGeneric::perspective(fov, hither, 1e30).unwrap(); - let light_from_screen = screen_from_light.inverse(); - let opposite = (radians(fov) / 2.).tan(); - let aspect_ratio = if aspect > 1. { aspect } else { 1. / aspect }; - let a = 4. * square(opposite) * aspect_ratio; - let dwda = |p: Point2f| { - let w = - Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.))); - cos_theta(w.normalize()).powi(3) - }; - - let d = image.get_sampling_distribution(dwda, screen_bounds); - let distrib = PiecewiseConstant2D::new_with_bounds(&d, screen_bounds); - - Self { - base, - image_id, - image_color_space, - screen_bounds, - screen_from_light, - light_from_screen, - scale, - hither, - a, - distrib, - } - } + #[cfg(not(target_os = "cuda"))] pub fn i(&self, w: Vector3f, lambda: SampledWavelengths) -> SampledSpectrum { if w.z() < self.hither { @@ -149,10 +110,12 @@ impl LightTrait for ProjectionLight { rgb[c] = self.image.get_channel(Point2i::new(x, y), c); } - let s = RGBIlluminantSpectrum::new( - self.image_color_space.as_ref(), - RGB::clamp_zero(rgb), - ); + let s = unsafe { + RGBIlluminantSpectrum::new( + self.image_color_space.as_ref(), + RGB::clamp_zero(rgb), + ); + }; sum += s.sample(&lambda) * dwda; } } diff --git a/shared/src/lights/sampler.rs b/shared/src/lights/sampler.rs index c88560c..9348280 100644 --- a/shared/src/lights/sampler.rs +++ b/shared/src/lights/sampler.rs @@ -1,13 +1,12 @@ use crate::core::geometry::primitives::OctahedralVector; use crate::core::geometry::{Bounds3f, Normal3f, Point3f, Vector3f, VectorLike}; use crate::core::geometry::{DirectionCone, Normal}; -use crate::utils::math::{clamp, lerp, sample_discrete}; -use std::collections::HashMap; -use std::sync::Arc; - +use crate::core::light::Light; use crate::core::light::{LightBounds, LightSampleContext}; use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::math::{clamp, lerp, sample_discrete}; use crate::utils::math::{safe_sqrt, square}; +use crate::utils::ptr::{Ptr, Slice}; use crate::utils::sampling::AliasTable; use crate::{Float, ONE_MINUS_EPSILON, PI}; use enum_dispatch::enum_dispatch; @@ -155,22 +154,22 @@ impl CompactLightBounds { #[derive(Debug, Clone)] pub struct SampledLight { - pub light: Arc, + pub light: Ptr, pub p: Float, } impl SampledLight { - pub fn new(light: Arc, p: Float) -> Self { + pub fn new(light: Light, p: Float) -> Self { Self { light, p } } } #[enum_dispatch] -pub trait LightSamplerTrait: Send + Sync + std::fmt::Debug { +pub trait LightSamplerTrait { fn sample_with_context(&self, ctx: &LightSampleContext, u: Float) -> Option; - fn pmf_with_context(&self, ctx: &LightSampleContext, light: &Arc) -> Float; + fn pmf_with_context(&self, ctx: &LightSampleContext, light: &Light) -> Float; fn sample(&self, u: Float) -> Option; - fn pmf(&self, light: &Arc) -> Float; + fn pmf(&self, light: &Light) -> Float; } #[derive(Clone, Debug)] @@ -183,14 +182,18 @@ pub enum LightSampler { #[derive(Clone, Debug)] pub struct UniformLightSampler { - lights: Vec>, + lights: *const Light, + lights_len: u32, } impl UniformLightSampler { - pub fn new(lights: &[Arc]) -> Self { - Self { - lights: lights.to_vec(), - } + pub fn new(lights: *const Light, lights_len: u32) -> Self { + Self { lights, lights_len } + } + + #[inline(always)] + fn light(&self, idx: usize) -> Light { + unsafe { *self.lights.add(idx) } } } @@ -198,77 +201,52 @@ impl LightSamplerTrait for UniformLightSampler { fn sample_with_context(&self, _ctx: &LightSampleContext, u: Float) -> Option { self.sample(u) } - fn pmf_with_context(&self, _ctx: &LightSampleContext, light: &Arc) -> Float { + fn pmf_with_context(&self, _ctx: &LightSampleContext, light: &Light) -> Float { self.pmf(light) } fn sample(&self, u: Float) -> Option { - if self.lights.is_empty() { + if self.lights_len == 0 { return None; } - let light_index = (u as usize * self.lights.len()).min(self.lights.len() - 1); + let light_index = (u as u32 * self.lights_len).min(self.lights_len - 1) as usize; Some(SampledLight { - light: self.lights[light_index].clone(), - p: 1. / self.lights.len() as Float, + light: self.light(light_index), + p: 1. / self.lights_len as Float, }) } - fn pmf(&self, _light: &Arc) -> Float { - if self.lights.is_empty() { + fn pmf(&self, _light: &Light) -> Float { + if self.lights_len == 0 { return 0.; } - 1. / self.lights.len() as Float + 1. / self.lights_len as Float } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct Alias { + pub q: Float, + pub alias: u32, +} + +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct PowerLightSampler { - lights: Vec>, - light_to_index: HashMap, - alias_table: AliasTable, + pub lights: Slice, + pub lights_len: u32, + pub alias_table: AliasTable, } -impl PowerLightSampler { - pub fn new(lights: &[Arc]) -> Self { - if lights.is_empty() { - return Self { - lights: Vec::new(), - light_to_index: HashMap::new(), - alias_table: AliasTable::new(&[]), - }; - } - - let mut lights_vec = Vec::with_capacity(lights.len()); - let mut light_to_index = HashMap::with_capacity(lights.len()); - let mut light_power = Vec::with_capacity(lights.len()); - - let lambda = SampledWavelengths::sample_visible(0.5); - - for (i, light) in lights.iter().enumerate() { - lights_vec.push(light.clone()); - - let ptr = Arc::as_ptr(light) as usize; - light_to_index.insert(ptr, i); - - let phi = SampledSpectrum::safe_div(&light.phi(lambda), &lambda.pdf()); - light_power.push(phi.average()); - } - - let alias_table = AliasTable::new(&light_power); - - Self { - lights: lights_vec, - light_to_index, - alias_table, - } - } -} +unsafe impl Send for PowerLightSampler {} +unsafe impl Sync for PowerLightSampler {} impl LightSamplerTrait for PowerLightSampler { fn sample_with_context(&self, _ctx: &LightSampleContext, u: Float) -> Option { self.sample(u) } - fn pmf_with_context(&self, _ctx: &LightSampleContext, light: &Arc) -> Float { + fn pmf_with_context(&self, _ctx: &LightSampleContext, light: &Light) -> Float { self.pmf(light) } @@ -279,24 +257,29 @@ impl LightSamplerTrait for PowerLightSampler { let (light_index, pmf, _) = self.alias_table.sample(u); + let light_ref = &self.lights[light_index as usize]; Some(SampledLight { - light: self.lights[light_index].clone(), + light: Ptr::from(light_ref), p: pmf, }) } - fn pmf(&self, light: &Arc) -> Float { - if self.alias_table.size() == 0 { - return 0.; + fn pmf(&self, light: &Light) -> Float { + if self.lights_len == 0 { + return 0.0; } - let ptr = Arc::as_ptr(light) as usize; + let light_ptr = light as *const Light; + let start = self.lights.as_ptr(); - if let Some(&index) = self.light_to_index.get(&ptr) { - self.alias_table.pmf(index) - } else { - 0.0 + let end = unsafe { start.add(self.lights.len as usize) }; + + if light_ptr >= start && light_ptr < end { + let index = unsafe { light_ptr.offset_from(start) }; + return self.alias_table.pmf(index as u32); } + + 0. } } @@ -304,7 +287,6 @@ impl LightSamplerTrait for PowerLightSampler { #[repr(C, align(32))] pub struct LightBVHNode { pub light_bounds: CompactLightBounds, - // Bit 31 (MSB) : isLeaf (1 bit) // Bits 0..31 : childOrLightIndex (31 bits) packed_data: u32, @@ -373,14 +355,40 @@ impl LightBVHNode { #[derive(Clone, Debug)] pub struct BVHLightSampler { - lights: Vec>, - infinite_lights: Vec>, - all_light_bounds: Bounds3f, - nodes: Vec, - light_to_bit_trail: HashMap, + pub nodes: *const LightBVHNode, + pub lights: *const Light, + pub infinite_lights: *const Light, + pub bit_trails: *const u64, + pub nodes_len: u32, + pub lights_len: u32, + pub infinite_lights_len: u32, + pub all_light_bounds: Bounds3f, } +unsafe impl Send for BVHLightSampler {} +unsafe impl Sync for BVHLightSampler {} + impl BVHLightSampler { + #[inline(always)] + fn node(&self, idx: usize) -> &LightBVHNode { + unsafe { &*self.nodes.add(idx) } + } + + #[inline(always)] + fn light(&self, idx: usize) -> Light { + unsafe { *self.lights.add(idx) } + } + + #[inline(always)] + fn infinite_light(&self, idx: usize) -> Light { + unsafe { *self.infinite_lights.add(idx) } + } + + #[inline(always)] + fn bit_trail(&self, idx: usize) -> u64 { + unsafe { *self.bit_trails.add(idx) } + } + fn evaluate_cost(&self, b: &LightBounds, bounds: &Bounds3f, dim: usize) -> Float { let theta_o = b.cos_theta_o.acos(); let theta_e = b.cos_theta_e.acos(); @@ -397,9 +405,9 @@ impl BVHLightSampler { impl LightSamplerTrait for BVHLightSampler { fn sample_with_context(&self, ctx: &LightSampleContext, mut u: Float) -> Option { - let empty_nodes = if self.nodes.is_empty() { 0. } else { 1. }; - let inf_size = self.infinite_lights.len() as Float; - let light_size = self.lights.len() as Float; + let empty_nodes = if self.nodes_len == 0 { 0. } else { 1. }; + let inf_size = self.infinite_lights_len as Float; + let light_size = self.lights_len as Float; let p_inf = inf_size / (inf_size + empty_nodes); @@ -407,115 +415,128 @@ impl LightSamplerTrait for BVHLightSampler { u /= p_inf; let ind = (u * light_size).min(light_size - 1.) as usize; let pmf = p_inf / inf_size; - Some(SampledLight::new(self.infinite_lights[ind].clone(), pmf)) - } else { - if self.nodes.is_empty() { - return None; - } - let p = ctx.p(); - let n = ctx.ns; - u = ((u - p_inf) / (1. - p_inf)).min(ONE_MINUS_EPSILON); - let mut node_ind = 0; - let mut pmf = 1. - p_inf; + return Some(SampledLight::new(self.infinite_light(ind), pmf)); + } - loop { - let node = self.nodes[node_ind]; - if !node.is_leaf() { - let children: [LightBVHNode; 2] = [ - self.nodes[node_ind + 1], - self.nodes[node.child_or_light_index() as usize], - ]; - let ci: [Float; 2] = [ - children[0] - .light_bounds - .importance(p, n, &self.all_light_bounds), - children[1] - .light_bounds - .importance(p, n, &self.all_light_bounds), - ]; + if self.nodes_len == 0 { + return None; + } + let p = ctx.p(); + let n = ctx.ns; + u = ((u - p_inf) / (1. - p_inf)).min(ONE_MINUS_EPSILON); + let mut node_ind = 0; + let mut pmf = 1. - p_inf; - if ci[0] == 0. && ci[1] == 0. { - return None; - } + loop { + let node = self.node(node_ind); + if !node.is_leaf() { + let child0_idx = node_ind + 1; + let child1_idx = node.child_or_light_index() as usize; + let child0 = self.node(child0_idx); + let child1 = self.node(child1_idx); - let mut node_pmf: Float = 0.; - let child = sample_discrete(&ci, u, Some(&mut node_pmf), Some(&mut u)); - pmf *= node_pmf; - node_ind = if child == 0 { - node_ind + 1 - } else { - node.child_or_light_index() as usize - }; - } else { - if node_ind > 0 - || node.light_bounds.importance(p, n, &self.all_light_bounds) > 0. - { - return Some(SampledLight::new( - self.lights[node.child_or_light_index() as usize].clone(), - pmf, - )); - } + let ci: [Float; 2] = [ + child0.light_bounds.importance(p, n, &self.all_light_bounds), + child1.light_bounds.importance(p, n, &self.all_light_bounds), + ]; + + if ci[0] == 0. && ci[1] == 0. { return None; } + + let mut node_pmf: Float = 0.; + let child = sample_discrete(&ci, u, Some(&mut node_pmf), Some(&mut u)); + pmf *= node_pmf; + node_ind = if child == 0 { child0_idx } else { child1_idx }; + } else { + if node_ind > 0 || node.light_bounds.importance(p, n, &self.all_light_bounds) > 0. { + let light_idx = node.child_or_light_index() as usize; + return Some(SampledLight::new(self.light(light_idx), pmf)); + } + return None; } } } - fn pmf_with_context(&self, ctx: &LightSampleContext, light: &Arc) -> Float { - let ptr = Arc::as_ptr(light) as usize; - let empty_nodes = if self.nodes.is_empty() { 0. } else { 1. }; - if self.light_to_bit_trail.contains_key(&ptr) { - return 1. / (self.infinite_lights.len() as Float + empty_nodes); + fn pmf_with_context(&self, ctx: &LightSampleContext, light: &Light) -> Float { + let light_ptr = light as *const Light; + let empty_nodes = if self.nodes_len == 0 { 0. } else { 1. }; + let n_infinite = self.infinite_lights_len as Float; + + let inf_start = self.infinite_lights; + let inf_end = unsafe { self.infinite_lights.add(self.infinite_lights_len as usize) }; + if light_ptr >= inf_start && light_ptr < inf_end { + return 1.0 / (n_infinite + empty_nodes); } - let mut bit_trail = self.light_to_bit_trail[&ptr]; + let finite_start = self.lights; + let finite_end = unsafe { self.lights.add(self.lights_len as usize) }; + + if light_ptr < finite_start || light_ptr >= finite_end { + return 0.0; + } + + let light_index = unsafe { light_ptr.offset_from(finite_start) as usize }; + + let mut bit_trail = self.bit_trail(light_index); + + let p_inf = n_infinite / (n_infinite + empty_nodes); + let mut pmf = 1.0 - p_inf; + let mut node_ind = 0; let p = ctx.p(); let n = ctx.ns; - let p_inf = self.infinite_lights.len() as Float - / (self.infinite_lights.len() as Float + empty_nodes); - let mut pmf = 1. - p_inf; - let mut node_ind = 0; loop { - let node = self.nodes[node_ind]; + let node = self.node(node_ind); if node.is_leaf() { return pmf; } - let child0 = self.nodes[node_ind + 1]; - let child1 = self.nodes[node.child_or_light_index() as usize]; + let child0 = self.node(node_ind + 1); + let child1 = self.node(node.child_or_light_index() as usize); let ci = [ child0.light_bounds.importance(p, n, &self.all_light_bounds), child1.light_bounds.importance(p, n, &self.all_light_bounds), ]; - pmf *= ci[bit_trail & 1] / (ci[0] + ci[1]); - node_ind = if (bit_trail & 1) != 0 { + + let sum_importance = ci[0] + ci[1]; + if sum_importance == 0.0 { + return 0.0; + } + + let which_child = (bit_trail & 1) as usize; + + // Update probability: prob of picking the correct child + pmf *= ci[which_child] / sum_importance; + + // Advance + node_ind = if which_child == 1 { node.child_or_light_index() as usize } else { node_ind + 1 }; + bit_trail >>= 1; } } fn sample(&self, u: Float) -> Option { - if self.lights.is_empty() { + if self.lights_len == 0 { return None; } - let light_ind = - (u * self.lights.len() as Float).min(self.lights.len() as Float - 1.) as usize; + let light_ind = (u * self.lights_len as Float).min(self.lights_len as Float - 1.) as usize; Some(SampledLight::new( - self.lights[light_ind].clone(), - 1. / self.lights.len() as Float, + self.light(light_ind), + 1. / self.lights_len as Float, )) } - fn pmf(&self, _light: &Arc) -> Float { - if self.lights.is_empty() { + fn pmf(&self, _light: &Light) -> Float { + if self.lights_len == 0 { return 0.; } - 1. / self.lights.len() as Float + 1. / self.lights_len as Float } } diff --git a/shared/src/lights/spot.rs b/shared/src/lights/spot.rs index 0581383..7ba3fef 100644 --- a/shared/src/lights/spot.rs +++ b/shared/src/lights/spot.rs @@ -1,16 +1,24 @@ -use crate::core::light::{LightBase, LightLiSample, LightSampleContext, LightTrait}; +use crate::core::geometry::{ + Bounds3f, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector3f, VectorLike, +}; +use crate::core::interaction::{Interaction, InteractionTrait, SimpleInteraction}; +use crate::core::light::{LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait}; +use crate::core::spectrum::SpectrumTrait; +use crate::spectra::{DenselySampledSpectrum, SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; +use crate::{Float, PI}; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct SpotLight { pub base: LightBase, - pub iemit_coeffs: [Float; 32], + pub iemit: Ptr, pub scale: Float, pub cos_falloff_start: Float, pub cos_falloff_end: Float, } -impl SpotLightData { +impl SpotLight { pub fn i(&self, w: Vector3f, lambda: &SampledWavelengths) -> SampledSpectrum { let cos_theta = w.z(); // assuming normalized in light space let falloff = crate::utils::math::smooth_step( @@ -18,8 +26,7 @@ impl SpotLightData { self.cos_falloff_end, self.cos_falloff_start, ); - let spectrum = DenselySampledSpectrum::from_array(&self.iemit_coeffs); - falloff * self.scale * spectrum.sample(lambda) + falloff * self.scale * self.iemit.sample(lambda) } } @@ -42,9 +49,9 @@ impl LightTrait for SpotLight { let p: Point3f = pi.into(); let wi = (p - ctx.p()).normalize(); let w_light = self.base.render_from_light.apply_inverse_vector(-wi); - let li = self.i(w_light, *lambda) / p.distance_squared(ctx.p()); + let li = self.i(w_light, lambda) / p.distance_squared(ctx.p()); - let intr = SimpleInteraction::new(pi, 0.0, Some(self.base.medium_interface.clone())); + let intr = SimpleInteraction::new(pi, 0.0, Ptr::from(&self.base.medium_interface)); Some(LightLiSample::new(li, wi, 1., Interaction::Simple(intr))) } @@ -78,7 +85,7 @@ impl LightTrait for SpotLight { * self.iemit.sample(&lambda) * 2. * PI - * ((1. - self.cos_fallof_start) + (self.cos_fallof_start - self.cos_fallof_end) / 2.) + * ((1. - self.cos_falloff_start) + (self.cos_falloff_start - self.cos_falloff_end) / 2.) } #[cfg(not(target_os = "cuda"))] @@ -98,12 +105,12 @@ impl LightTrait for SpotLight { .apply_to_vector(Vector3f::new(0., 0., 1.)) .normalize(); let phi = self.scale * self.iemit.max_value() * 4. * PI; - let cos_theta_e = (self.cos_fallof_end.acos() - self.cos_fallof_start.acos()).cos(); + let cos_theta_e = (self.cos_falloff_end.acos() - self.cos_falloff_start.acos()).cos(); Some(LightBounds::new( &Bounds3f::from_points(p, p), w, phi, - self.cos_fallof_start, + self.cos_falloff_start, cos_theta_e, false, )) diff --git a/shared/src/materials/coated.rs b/shared/src/materials/coated.rs new file mode 100644 index 0000000..4a12a8a --- /dev/null +++ b/shared/src/materials/coated.rs @@ -0,0 +1,337 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct CoatedDiffuseMaterial { + pub normal_map: *const Image, + pub displacement: RelPtr, + pub reflectance: RelPtr, + pub albedo: RelPtr, + pub u_roughness: RelPtr, + pub v_roughness: RelPtr, + pub thickness: RelPtr, + pub g: RelPtr, + pub eta: RelPtr, + pub remap_roughness: bool, + pub max_depth: usize, + pub n_samples: usize, +} + +impl CoatedDiffuseMaterial { + #[allow(clippy::too_many_arguments)] + #[cfg(not(target_os = "cuda"))] + pub fn new( + reflectance: GPUSpectrumTexture, + u_roughness: GPUFloatTexture, + v_roughness: GPUFloatTexture, + thickness: GPUFloatTexture, + albedo: GPUSpectrumTexture, + g: GPUFloatTexture, + eta: Spectrum, + displacement: GPUFloatTexture, + normal_map: *const Image, + remap_roughness: bool, + max_depth: usize, + n_samples: usize, + ) -> Self { + Self { + displacement, + normal_map, + reflectance, + albedo, + u_roughness, + v_roughness, + thickness, + g, + eta, + remap_roughness, + max_depth, + n_samples, + } + } +} + +impl MaterialTrait for CoatedDiffuseMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + let r = SampledSpectrum::clamp( + &tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda), + 0., + 1., + ); + + let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx); + let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx); + + if self.remap_roughness { + u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough); + v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough); + } + + let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); + + let thick = tex_eval.evaluate_float(&self.thickness, ctx); + let mut sampled_eta = self.eta.evaluate(lambda[0]); + if self.eta.is_constant() { + let mut lambda = *lambda; + lambda.terminate_secondary_inplace(); + } + + if sampled_eta == 0. { + sampled_eta = 1. + } + + let a = SampledSpectrum::clamp( + &tex_eval.evaluate_spectrum(&self.albedo, ctx, lambda), + 0., + 1., + ); + + let gg = clamp(tex_eval.evaluate_float(&self.g, ctx), -1., 1.); + let bxdf = BxDF::CoatedDiffuse(CoatedDiffuseBxDF::new( + DielectricBxDF::new(sampled_eta, distrib), + DiffuseBxDF::new(r), + thick, + a, + gg, + self.max_depth, + self.n_samples, + )); + + BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate( + &[self.u_roughness, self.v_roughness, self.thickness, self.g], + &[self.reflectance, self.albedo], + ) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct CoatedConductorMaterial { + normal_map: *const Image, + displacement: RelPtr, + interface_uroughness: RelPtr, + interface_vroughness: RelPtr, + thickness: RelPtr, + interface_eta: RelPtr, + g: RelPtr, + albedo: RelPtr, + conductor_uroughness: RelPtr, + conductor_vroughness: RelPtr, + conductor_eta: RelPtr, + k: RelPtr, + reflectance: RelPtr, + remap_roughness: bool, + max_depth: u32, + n_samples: u32, +} + +impl CoatedConductorMaterial { + #[allow(clippy::too_many_arguments)] + #[cfg(not(target_os = "cuda"))] + pub fn new( + displacement: GPUFloatTexture, + normal_map: *const Image, + interface_uroughness: GPUFloatTexture, + interface_vroughness: GPUFloatTexture, + thickness: GPUFloatTexture, + interface_eta: Spectrum, + g: GPUFloatTexture, + albedo: GPUSpectrumTexture, + conductor_uroughness: GPUFloatTexture, + conductor_vroughness: GPUFloatTexture, + conductor_eta: Option, + k: Option, + reflectance: GPUSpectrumTexture, + remap_roughness: bool, + max_depth: usize, + n_samples: usize, + ) -> Self { + Self { + displacement, + normal_map, + interface_uroughness, + interface_vroughness, + thickness, + interface_eta, + g, + albedo, + conductor_uroughness, + conductor_vroughness, + conductor_eta, + k, + reflectance, + remap_roughness, + max_depth, + n_samples, + } + } +} + +impl MaterialTrait for CoatedConductorMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + let mut iurough = tex_eval.evaluate_float(&self.interface_uroughness, ctx); + let mut ivrough = tex_eval.evaluate_float(&self.interface_vroughness, ctx); + + if self.remap_roughness { + iurough = TrowbridgeReitzDistribution::roughness_to_alpha(iurough); + ivrough = TrowbridgeReitzDistribution::roughness_to_alpha(ivrough); + } + let interface_distrib = TrowbridgeReitzDistribution::new(iurough, ivrough); + let thick = tex_eval.evaluate_float(&self.thickness, ctx); + + let mut ieta = self.interface_eta.evaluate(lambda[0]); + if self.interface_eta.is_constant() { + let mut lambda = *lambda; + lambda.terminate_secondary_inplace(); + } + + if ieta == 0. { + ieta = 1.; + } + + let (mut ce, mut ck) = if let Some(eta_tex) = &self.conductor_eta { + let k_tex = self + .k + .as_ref() + .expect("CoatedConductor: 'k' must be provided if 'conductor_eta' is present"); + let ce = tex_eval.evaluate_spectrum(eta_tex, ctx, lambda); + let ck = tex_eval.evaluate_spectrum(k_tex, ctx, lambda); + (ce, ck) + } else { + let r = SampledSpectrum::clamp( + &tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda), + 0., + 0.9999, + ); + let ce = SampledSpectrum::new(1.0); + let one_minus_r = SampledSpectrum::new(1.) - r; + let ck = 2. * r.sqrt() / SampledSpectrum::clamp_zero(&one_minus_r).sqrt(); + (ce, ck) + }; + + ce /= ieta; + ck /= ieta; + + let mut curough = tex_eval.evaluate_float(&self.conductor_uroughness, ctx); + let mut cvrough = tex_eval.evaluate_float(&self.conductor_vroughness, ctx); + + if self.remap_roughness { + curough = TrowbridgeReitzDistribution::roughness_to_alpha(curough); + cvrough = TrowbridgeReitzDistribution::roughness_to_alpha(cvrough); + } + + let conductor_distrib = TrowbridgeReitzDistribution::new(curough, cvrough); + let a = SampledSpectrum::clamp( + &tex_eval.evaluate_spectrum(&self.albedo, ctx, lambda), + 0., + 1., + ); + + let gg = clamp(tex_eval.evaluate_float(&self.g, ctx), -1., 1.); + let bxdf = BxDF::CoatedConductor(CoatedConductorBxDF::new( + DielectricBxDF::new(ieta, interface_distrib), + ConductorBxDF::new(&conductor_distrib, ce, ck), + thick, + a, + gg, + self.max_depth, + self.n_samples, + )); + BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + let float_textures = [ + self.interface_uroughness, + self.interface_vroughness, + self.thickness, + self.g, + self.conductor_uroughness, + self.conductor_vroughness, + ]; + + let mut spectrum_textures = Vec::with_capacity(4); + + spectrum_textures.push(&self.albedo); + + if let Some(eta) = &self.conductor_eta { + spectrum_textures.push(eta); + } + if let Some(k) = &self.k { + spectrum_textures.push(k); + } + + if self.conductor_eta.is_none() { + spectrum_textures.push(self.reflectance); + } + + tex_eval.can_evaluate(&float_textures, &spectrum_textures) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} diff --git a/shared/src/materials/complex.rs b/shared/src/materials/complex.rs new file mode 100644 index 0000000..a538bcc --- /dev/null +++ b/shared/src/materials/complex.rs @@ -0,0 +1,181 @@ +use crate::Float; +use crate::core::bssrdf::{BSSRDF, BSSRDFTable}; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, MeasuredBxDF, MeasuredBxDFData, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::textures::GPUSpectrumMixTexture; +use crate::utils::RelPtr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct HairMaterial { + pub sigma_a: RelPtr, + pub color: RelPtr, + pub eumelanin: RelPtr, + pub pheomelanin: RelPtr, + pub eta: RelPtr, + pub beta_m: RelPtr, + pub beta_n: RelPtr, + pub alpha: RelPtr, +} + +impl HairMaterial { + #[cfg(not(target_os = "cuda"))] + pub fn new( + sigma_a: RelPtr, + color: RelPtr, + eumelanin: RelPtr, + pheomelanin: RelPtr, + eta: RelPtr, + beta_m: RelPtr, + beta_n: RelPtr, + alpha: RelPtr, + ) -> Self { + Self { + sigma_a, + color, + eumelanin, + pheomelanin, + eta, + beta_m, + beta_n, + alpha, + } + } +} + +impl MaterialTrait for HairMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + + fn get_normal_map(&self) -> *const Image { + todo!() + } + + fn get_displacement(&self) -> RelPtr { + RelPtr::null() + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct MeasuredMaterial { + pub displacement: RelPtr, + pub normal_map: *const Image, + pub brdf: *const MeasuredBxDFData, +} + +impl MaterialTrait for MeasuredMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + MeasuredBxDF::new(self.brdf, lambda) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + true + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SubsurfaceMaterial { + pub displacement: RelPtr, + pub normal_map: *const Image, + pub sigma_a: RelPtr, + pub sigma_s: RelPtr, + pub reflectance: RelPtr, + pub mfp: RelPtr, + pub eta: Float, + pub scale: Float, + pub u_roughness: RelPtr, + pub v_roughness: RelPtr, + pub remap_roughness: bool, + pub table: BSSRDFTable, +} + +impl MaterialTrait for SubsurfaceMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + fn get_normal_map(&self) -> *const Image { + todo!() + } + fn get_displacement(&self) -> RelPtr { + todo!() + } + fn has_subsurface_scattering(&self) -> bool { + true + } +} diff --git a/shared/src/materials/conductor.rs b/shared/src/materials/conductor.rs new file mode 100644 index 0000000..4346aec --- /dev/null +++ b/shared/src/materials/conductor.rs @@ -0,0 +1,63 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct ConductorMaterial { + pub displacement: RelPtr, + pub eta: RelPtr, + pub k: RelPtr, + pub reflectance: RelPtr, + pub u_roughness: RelPtr, + pub v_roughness: RelPtr, + pub remap_roughness: bool, + pub normal_map: *const Image, +} + +impl MaterialTrait for ConductorMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate( + &[self.u_roughness, self.v_roughness], + &[self.eta, self.k, self.reflectance], + ) + } + + fn get_normal_map(&self) -> *const Image { + todo!() + } + + fn get_displacement(&self) -> RelPtr { + todo!() + } + + fn has_subsurface_scattering(&self) -> bool { + todo!() + } +} diff --git a/shared/src/materials/dielectric.rs b/shared/src/materials/dielectric.rs new file mode 100644 index 0000000..11e9058 --- /dev/null +++ b/shared/src/materials/dielectric.rs @@ -0,0 +1,122 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DielectricMaterial { + normal_map: *const Image, + displacement: GPUFloatTexture, + u_roughness: GPUFloatTexture, + v_roughness: GPUFloatTexture, + remap_roughness: bool, + eta: Spectrum, +} + +impl MaterialTrait for DielectricMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + let mut sampled_eta = self.eta.evaluate(lambda[0]); + if !self.eta.is_constant() { + lambda.terminate_secondary(); + } + + if sampled_eta == 0.0 { + sampled_eta = 1.0; + } + + let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx); + let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx); + + if self.remap_roughness { + u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough); + v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough); + } + + let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); + let bxdf = BxDF::Dielectric(DielectricBxDF::new(sampled_eta, distrib)); + + BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[self.u_roughness, self.v_roughness], &[]) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct ThinDielectricMaterial { + pub displacement: RelPtr, + pub normal_map: *const Image, + pub eta: RelPtr, +} +impl MaterialTrait for ThinDielectricMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + true + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} diff --git a/shared/src/materials/diffuse.rs b/shared/src/materials/diffuse.rs new file mode 100644 index 0000000..91cd9a8 --- /dev/null +++ b/shared/src/materials/diffuse.rs @@ -0,0 +1,105 @@ +use crate::Float; +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DiffuseMaterial { + pub normal_map: *const Image, + pub displacement: RelPtr, + pub reflectance: RelPtr, +} + +impl MaterialTrait for DiffuseMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + let r = tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda); + let bxdf = BxDF::Diffuse(DiffuseBxDF::new(r)); + BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[], &[self.reflectance]) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DiffuseTransmissionMaterial { + pub displacement: RelPtr, + pub image: *const Image, + pub reflectance: RelPtr, + pub transmittance: RelPtr, + pub scale: Float, +} + +impl MaterialTrait for DiffuseTransmissionMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[], &[self.reflectance, self.transmittance]) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> RelPtr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} diff --git a/shared/src/materials/mix.rs b/shared/src/materials/mix.rs new file mode 100644 index 0000000..24c573c --- /dev/null +++ b/shared/src/materials/mix.rs @@ -0,0 +1,85 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; +use crate::utils::hash::hash_float; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct MixMaterial { + pub amount: RelPtr, + pub materials: [RelPtr; 2], +} + +impl MixMaterial { + pub fn choose_material( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + ) -> Option<&Material> { + let amt = tex_eval.evaluate_float(&self.amount, ctx); + + let index = if amt <= 0.0 { + 0 + } else if amt >= 1.0 { + 1 + } else { + let u = hash_float(&(ctx.p, ctx.wo)); + if amt < u { 0 } else { 1 } + }; + + self.materials[index].get() + } +} + +impl MaterialTrait for MixMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + if let Some(mat) = self.choose_material(tex_eval, ctx) { + mat.get_bsdf(tex_eval, ctx, lambda) + } else { + BSDF::empty() + } + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[self.amount], &[]) + } + + fn get_normal_map(&self) -> *const Image { + core::ptr::null() + } + + fn get_displacement(&self) -> RelPtr { + panic!( + "MixMaterial::get_displacement() shouldn't be called. \ + Displacement is not supported on Mix materials directly." + ); + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} diff --git a/shared/src/materials/mod.rs b/shared/src/materials/mod.rs new file mode 100644 index 0000000..0468537 --- /dev/null +++ b/shared/src/materials/mod.rs @@ -0,0 +1,13 @@ +pub mod coated; +pub mod complex; +pub mod conductor; +pub mod dielectric; +pub mod diffuse; +pub mod mix; + +pub use coated::*; +pub use complex::*; +pub use conductor::*; +pub use dielectric::*; +pub use diffuse::*; +pub use mix::*; diff --git a/shared/src/shapes/bilinear.rs b/shared/src/shapes/bilinear.rs index 50bee67..e26e478 100644 --- a/shared/src/shapes/bilinear.rs +++ b/shared/src/shapes/bilinear.rs @@ -1,58 +1,130 @@ -use super::{ - BilinearIntersection, BilinearPatchShape, Bounds3f, DirectionCone, Interaction, Normal3f, - Point2f, Point3f, Point3fi, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, - ShapeTrait, SurfaceInteraction, Transform, Vector3f, +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal, Normal3f, Point2f, Point3f, Point3fi, Ray, Tuple, Vector3f, + VectorLike, spherical_quad_area, }; -use crate::core::geometry::{Tuple, VectorLike, spherical_quad_area}; -use crate::core::interaction::InteractionTrait; +use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; use crate::core::pbrt::{Float, gamma}; +use crate::core::shape::{Shape, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait}; +use crate::utils::Transform; use crate::utils::math::{SquareMatrix, clamp, difference_of_products, lerp, quadratic}; use crate::utils::mesh::BilinearPatchMesh; use crate::utils::sampling::{ bilinear_pdf, invert_spherical_rectangle_sample, sample_bilinear, sample_spherical_rectangle, }; -use std::sync::Arc; -use std::sync::OnceLock; - -struct PatchData<'a> { - mesh: &'a BilinearPatchMesh, - p00: Point3f, - p10: Point3f, - p01: Point3f, - p11: Point3f, - n: Option<[Normal3f; 4]>, - uv: Option<[Point2f; 4]>, -} +#[repr(C)] +#[derive(Debug, Copy, Clone)] struct IntersectionData { - t: Float, - u: Float, - v: Float, + pub t: Float, + pub u: Float, + pub v: Float, } +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] struct TextureDerivative { - duds: Float, - dvds: Float, - dudt: Float, - dvdt: Float, + pub duds: Float, + pub dvds: Float, + pub dudt: Float, + pub dvdt: Float, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct BilinearIntersection { + pub uv: Point2f, + pub t: Float, +} + +impl BilinearIntersection { + pub fn new(uv: Point2f, t: Float) -> Self { + Self { uv, t } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct BilinearPatchShape { + pub mesh: BilinearPatchMesh, + pub blp_index: u32, + pub area: Float, + pub rectangle: bool, } -static BILINEAR_MESHES: OnceLock>> = OnceLock::new(); impl BilinearPatchShape { pub const MIN_SPHERICAL_SAMPLE_AREA: Float = 1e-4; + fn mesh(&self) -> BilinearPatchMesh { + self.mesh + } - pub fn new(_mesh: BilinearPatchMesh, mesh_index: usize, blp_index: usize) -> Self { + #[inline(always)] + fn get_vertex_indices(&self) -> [usize; 4] { + unsafe { + let base_ptr = self.mesh.vertex_indices.add((self.blp_index as usize) * 4); + [ + *base_ptr.add(0) as usize, + *base_ptr.add(1) as usize, + *base_ptr.add(2) as usize, + *base_ptr.add(3) as usize, + ] + } + } + + #[inline(always)] + fn get_points(&self) -> [Point3f; 4] { + let [v0, v1, v2, v3] = self.get_vertex_indices(); + unsafe { + [ + *self.mesh.p.0.add(v0), + *self.mesh.p.0.add(v1), + *self.mesh.p.0.add(v2), + *self.mesh.p.0.add(v3), + ] + } + } + + #[inline(always)] + fn get_uvs(&self) -> Option<[Point2f; 4]> { + if self.mesh.uv.is_null() { + return None; + } + let [v0, v1, v2, v3] = self.get_vertex_indices(); + unsafe { + Some([ + *self.mesh.uv.0.add(v0), + *self.mesh.uv.0.add(v1), + *self.mesh.uv.0.add(v2), + *self.mesh.uv.0.add(v3), + ]) + } + } + + #[inline(always)] + fn get_shading_normals(&self) -> Option<[Normal3f; 4]> { + if self.mesh.n.is_null() { + return None; + } + let [v0, v1, v2, v3] = self.get_vertex_indices(); + unsafe { + Some([ + *self.mesh.n.0.add(v0), + *self.mesh.n.0.add(v1), + *self.mesh.n.0.add(v2), + *self.mesh.n.0.add(v3), + ]) + } + } + + #[cfg(not(target_os = "cuda"))] + pub fn new(mesh: BilinearPatchMesh, blp_index: u32) -> Self { let mut bp = BilinearPatchShape { - mesh_index, + mesh, blp_index, area: 0., rectangle: false, }; - let (p00, p10, p01, p11) = { - let data = bp.get_data(); - (data.p00, data.p10, data.p01, data.p11) - }; + let [p00, p10, p01, p11] = bp.get_points(); bp.rectangle = bp.is_rectangle(p00, p10, p01, p11); @@ -84,40 +156,6 @@ impl BilinearPatchShape { bp } - fn mesh(&self) -> &Arc { - let meshes = BILINEAR_MESHES - .get() - .expect("Mesh has not been initialized"); - &meshes[self.mesh_index] - } - - fn get_data(&self) -> PatchData<'_> { - let mesh = self.mesh(); - let start_index = 4 * self.blp_index; - let v = &mesh.vertex_indices[start_index..start_index + 4]; - let p00: Point3f = mesh.p[v[0]]; - let p10: Point3f = mesh.p[v[1]]; - let p01: Point3f = mesh.p[v[2]]; - let p11: Point3f = mesh.p[v[3]]; - let n = mesh - .n - .as_ref() - .map(|normals| [normals[v[0]], normals[v[1]], normals[v[2]], normals[v[3]]]); - let uv = mesh - .uv - .as_ref() - .map(|uvs| [uvs[v[0]], uvs[v[1]], uvs[v[2]], uvs[v[3]]]); - PatchData { - mesh, - p00, - p10, - p01, - p11, - n, - uv, - } - } - fn is_rectangle(&self, p00: Point3f, p10: Point3f, p01: Point3f, p11: Point3f) -> bool { if p00 == p01 || p01 == p11 || p11 == p10 || p10 == p00 { return false; @@ -149,25 +187,26 @@ impl BilinearPatchShape { &self, ray: &Ray, t_max: Float, - data: &PatchData, + corners: &[Point3f; 4], ) -> Option { - let a = (data.p10 - data.p00).cross(data.p01 - data.p11).dot(ray.d); - let c = (data.p00 - ray.o).cross(ray.d).dot(data.p01 - data.p00); - let b = (data.p10 - ray.o).cross(ray.d).dot(data.p11 - data.p10) - (a + c); + let &[p00, p01, p10, p11] = corners; + let a = (p10 - p00).cross(p01 - p11).dot(ray.d); + let c = (p00 - ray.o).cross(ray.d).dot(p01 - p00); + let b = (p10 - ray.o).cross(ray.d).dot(p11 - p10) - (a + c); let (u1, u2) = quadratic(a, b, c)?; let eps = gamma(10) * (ray.o.abs().max_component_value() + ray.d.abs().max_component_value() - + data.p00.abs().max_component_value() - + data.p10.abs().max_component_value() - + data.p01.abs().max_component_value() - + data.p11.abs().max_component_value()); + + p00.abs().max_component_value() + + p10.abs().max_component_value() + + p01.abs().max_component_value() + + p11.abs().max_component_value()); - let hit1 = self.check_candidate(u1, ray, data); + let hit1 = self.check_candidate(u1, ray, corners); let hit2 = if u1 != u2 { - self.check_candidate(u2, ray, data) + self.check_candidate(u2, ray, corners) } else { None }; @@ -183,12 +222,18 @@ impl BilinearPatchShape { }) } - fn check_candidate(&self, u: Float, ray: &Ray, data: &PatchData) -> Option { + fn check_candidate( + &self, + u: Float, + ray: &Ray, + corners: &[Point3f; 4], + ) -> Option { if !(0.0..=1.0).contains(&u) { return None; } - let uo: Point3f = lerp(u, data.p00, data.p10); - let ud: Point3f = Point3f::from(lerp(u, data.p01, data.p11) - uo); + let &[p00, p01, p10, p11] = corners; + let uo: Point3f = lerp(u, p00, p10); + let ud: Point3f = Point3f::from(lerp(u, p01, p11) - uo); let deltao = uo - ray.o; let perp = ray.d.cross(ud.into()); let p2 = perp.norm_squared(); @@ -222,26 +267,25 @@ impl BilinearPatchShape { fn interaction_from_intersection( &self, - data: &PatchData, uv: Point2f, time: Float, wo: Vector3f, ) -> SurfaceInteraction { // Base geom and derivatives - let p = lerp( - uv[0], - lerp(uv[1], data.p00, data.p01), - lerp(uv[1], data.p10, data.p11), - ); - let mut dpdu = lerp(uv[1], data.p10, data.p11) - lerp(uv[1], data.p00, data.p01); - let mut dpdv = lerp(uv[0], data.p01, data.p11) - lerp(uv[0], data.p00, data.p10); + let corners = self.get_points(); + let [p00, p01, p10, p11] = corners; + let p = lerp(uv[0], lerp(uv[1], p00, p01), lerp(uv[1], p10, p11)); + let mut dpdu = lerp(uv[1], p10, p11) - lerp(uv[1], p00, p01); + let mut dpdv = lerp(uv[0], p01, p11) - lerp(uv[0], p00, p10); // If textured, apply coordinates - let (st, derivatives) = self.apply_texture_coordinates(data, uv, &mut dpdu, &mut dpdv); + let patch_uvs = self.get_uvs(); + let (st, derivatives) = + self.apply_texture_coordinates(uv, patch_uvs.try_into().unwrap(), &mut dpdu, &mut dpdv); // Compute second moments let n = Normal3f::from(dpdu.cross(dpdv).normalize()); - let (mut dndu, mut dndv) = self.calculate_surface_curvature(data, &dpdu, &dpdv, n); + let (mut dndu, mut dndv) = self.calculate_surface_curvature(&corners, &dpdu, &dpdv, n); if let Some(ref deriv) = derivatives { let dnds = Normal3f::from(dndu * deriv.duds + dndv * deriv.dvds); let dndt = Normal3f::from(dndu * deriv.dudt + dndv * deriv.dvdt); @@ -249,31 +293,31 @@ impl BilinearPatchShape { dndv = dndt; } - let p_abs_sum = data.p00.abs() - + Vector3f::from(data.p01.abs()) - + Vector3f::from(data.p10.abs()) - + Vector3f::from(data.p11.abs()); + let p_abs_sum = p00.abs() + + Vector3f::from(p01.abs()) + + Vector3f::from(p10.abs()) + + Vector3f::from(p11.abs()); let p_error = gamma(6) * Vector3f::from(p_abs_sum); - let flip_normal = data.mesh.reverse_orientation ^ data.mesh.transform_swaps_handedness; + let flip_normal = self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness; let pi = Point3fi::new_with_error(p, p_error); let mut isect = SurfaceInteraction::new(pi, st, wo, dpdu, dpdv, dndu, dndv, time, flip_normal); - if data.n.is_some() { - self.apply_shading_normals(&mut isect, data, uv, derivatives); - } + + self.apply_shading_normals(&mut isect, self.get_shading_normals(), uv, derivatives); isect } + #[inline(always)] fn apply_texture_coordinates( &self, - data: &PatchData, uv: Point2f, + patch_uvs: Option<[Point2f; 4]>, dpdu: &mut Vector3f, dpdv: &mut Vector3f, ) -> (Point2f, Option) { - let Some(uvs) = data.uv else { - return (uv, None); + let Some(uvs) = patch_uvs else { + return (uv, TextureDerivative::default()); }; let uv00 = uvs[0]; let uv01 = uvs[1]; @@ -297,6 +341,7 @@ impl BilinearPatchShape { if dpdu.cross(*dpdv).dot(dpds.cross(dpdt)) < 0. { dpdt = -dpdt; } + *dpdu = dpds; *dpdv = dpdt; @@ -309,32 +354,33 @@ impl BilinearPatchShape { (st, Some(factors)) } + #[inline(always)] fn calculate_base_derivatives( &self, - data: &PatchData, + corners: &[Point3f; 4], uv: Point2f, ) -> (Point3f, Vector3f, Vector3f) { - let p = lerp( - uv[0], - lerp(uv[1], data.p00, data.p01), - lerp(uv[1], data.p10, data.p11), - ); - let dpdu = lerp(uv[1], data.p10, data.p11) - lerp(uv[1], data.p00, data.p01); - let dpdv = lerp(uv[0], data.p01, data.p11) - lerp(uv[0], data.p00, data.p10); + let (p00, p10, p01, p11) = (corners[0], corners[1], corners[2], corners[3]); + + let p = lerp(uv[0], lerp(uv[1], p00, p01), lerp(uv[1], p10, p11)); + let dpdu = lerp(uv[1], p10, p11) - lerp(uv[1], p00, p01); + let dpdv = lerp(uv[0], p01, p11) - lerp(uv[0], p00, p10); (p, dpdu, dpdv) } + #[inline(always)] fn calculate_surface_curvature( &self, - data: &PatchData, + corners: &[Point3f; 4], dpdu: &Vector3f, dpdv: &Vector3f, n: Normal3f, ) -> (Normal3f, Normal3f) { + let (p00, p10, p01, p11) = (corners[0], corners[1], corners[2], corners[3]); let e = dpdu.dot(*dpdu); let f = dpdu.dot(*dpdv); let g = dpdv.dot(*dpdv); - let d2pduv = (data.p00 - data.p01) + (data.p11 - data.p10); + let d2pduv = (p00 - p01) + (p11 - p10); let d2pduu = Vector3f::zero(); let d2pdvv = Vector3f::zero(); @@ -357,11 +403,13 @@ impl BilinearPatchShape { fn apply_shading_normals( &self, isect: &mut SurfaceInteraction, - data: &PatchData, + shading_normals: Option<[Normal3f; 4]>, uv: Point2f, derivatives: Option, ) { - let Some(normals) = data.n else { return }; + let Some(normals) = shading_normals else { + return; + }; let n00 = normals[1]; let n10 = normals[1]; let n01 = normals[2]; @@ -390,10 +438,7 @@ impl BilinearPatchShape { fn sample_area_and_pdf(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option { let mut ss = self.sample(u)?; - - let mut intr_clone = (*ss.intr).clone(); - intr_clone.common.time = ctx.time; - ss.intr = Arc::new(intr_clone); + ss.intr.get_common_mut().time = ctx.time; let mut wi = ss.intr.p() - ctx.p(); let dist_sq = wi.norm_squared(); @@ -412,16 +457,18 @@ impl BilinearPatchShape { if ss.pdf.is_infinite() { None } else { Some(ss) } } - fn sample_parametric_coords(&self, data: &PatchData, u: Point2f) -> (Point2f, Float) { - if let Some(image_distrib) = &data.mesh.image_distribution { + fn sample_parametric_coords(&self, corners: &[Point3f; 4], u: Point2f) -> (Point2f, Float) { + let (p00, p10, p01, p11) = (corners[0], corners[1], corners[2], corners[3]); + if !self.mesh.image_distribution.is_null() { + let image_distrib = self.mesh.image_distribution; let (uv, pdf, _) = image_distrib.sample(u); (uv, pdf) } else if !self.rectangle { let w = [ - (data.p10 - data.p00).cross(data.p01 - data.p00).norm(), - (data.p10 - data.p00).cross(data.p11 - data.p10).norm(), - (data.p01 - data.p00).cross(data.p11 - data.p01).norm(), - (data.p11 - data.p10).cross(data.p11 - data.p01).norm(), + (p10 - p00).cross(p01 - p00).norm(), + (p10 - p00).cross(p11 - p10).norm(), + (p01 - p00).cross(p11 - p01).norm(), + (p11 - p10).cross(p11 - p01).norm(), ]; let uv = sample_bilinear(u, &w); let pdf = bilinear_pdf(uv, &w); @@ -433,11 +480,12 @@ impl BilinearPatchShape { fn sample_solid_angle( &self, - data: &PatchData, + corners: &[Point3f; 4], ctx: &ShapeSampleContext, u: Point2f, corner_dirs: &[Vector3f; 4], ) -> Option { + let (p00, p10, p01, p11) = (corners[0], corners[1], corners[2], corners[3]); let mut pdf = 1.; if ctx.ns != Normal3f::zero() { let w = [ @@ -450,19 +498,21 @@ impl BilinearPatchShape { pdf *= bilinear_pdf(u, &w); } - let eu = data.p10 - data.p00; - let ev = data.p01 - data.p00; - let (p, quad_pdf) = sample_spherical_rectangle(ctx.p(), data.p00, eu, ev, u); + let eu = p10 - p00; + let ev = p01 - p00; + let (p, quad_pdf) = sample_spherical_rectangle(ctx.p(), p00, eu, ev, u); pdf *= quad_pdf?; // Compute (u, v) and surface normal for sampled points on rectangle let uv = Point2f::new( - (p - data.p00).dot(eu) / data.p10.distance_squared(data.p00), - (p - data.p00).dot(ev) / data.p01.distance_squared(data.p00), + (p - p00).dot(eu) / p10.distance_squared(p00), + (p - p00).dot(ev) / p01.distance_squared(p00), ); - let n = self.compute_sampled_normal(data, &eu, &ev, uv); - let st = data.uv.map_or(uv, |uvs| { + let patch_uvs = self.get_uvs(); + let patch_normals = self.get_shading_normals(); + let n = self.compute_sampled_normal(patch_normals, &eu, &ev, uv); + let st = patch_uvs.map_or(uv, |uvs| { lerp( uv[0], lerp(uv[1], uvs[0], uvs[1]), @@ -474,29 +524,29 @@ impl BilinearPatchShape { let mut intr = SurfaceInteraction::new_simple(pi, n, st); intr.common.time = ctx.time; Some(ShapeSample { - intr: Arc::new(intr), + intr: Interaction::Surface(intr), pdf, }) } fn compute_sampled_normal( &self, - data: &PatchData, + patch_normals: Option<[Normal3f; 4]>, dpdu: &Vector3f, dpdv: &Vector3f, uv: Point2f, ) -> Normal3f { let mut n = Normal3f::from(dpdu.cross(*dpdv).normalize()); - if let Some(normals) = data.n { + if let Some(normals) = patch_normals { // Apply interpolated shading normal to orient the geometric normal let ns = lerp( uv[0], lerp(uv[1], normals[0], normals[2]), lerp(uv[1], normals[1], normals[3]), ); - n = n.face_forward(ns.into()); - } else if data.mesh.reverse_orientation ^ data.mesh.transform_swaps_handedness { + n = n.face_forward(ns); + } else if self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness { n = -n; } n @@ -511,36 +561,33 @@ impl ShapeTrait for BilinearPatchShape { #[inline] fn normal_bounds(&self) -> DirectionCone { - let data = self.get_data(); - if data.p00 == data.p10 - || data.p10 == data.p11 - || data.p11 == data.p01 - || data.p01 == data.p00 - { - let dpdu = lerp(0.5, data.p10, data.p11) - lerp(0.5, data.p00, data.p01); - let dpdv = lerp(0.5, data.p01, data.p11) - lerp(0.5, data.p00, data.p10); + let [p00, p01, p10, p11] = self.get_points(); + let normals = self.get_shading_normals(); + if p00 == p10 || p10 == p11 || p11 == p01 || p01 == p00 { + let dpdu = lerp(0.5, p10, p11) - lerp(0.5, p00, p01); + let dpdv = lerp(0.5, p01, p11) - lerp(0.5, p00, p10); let mut n = Normal3f::from(dpdu.cross(dpdv).normalize()); - if let Some(normals) = data.n { + if let Some(normals) = normals { let interp_n = (normals[0] + normals[1] + normals[2] + normals[3]) / 4.; - n = n.face_forward(interp_n.into()); - } else if data.mesh.reverse_orientation ^ data.mesh.transform_swaps_handedness { + n = n.face_forward(interp_n); + } else if self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness { n *= -1.; } return DirectionCone::new_from_vector(Vector3f::from(n)); } // Compute bilinear patch normals n10, n01, and n11 - let mut n00 = Normal3f::from((data.p10 - data.p00).cross(data.p01 - data.p00).normalize()); - let mut n10 = Normal3f::from((data.p11 - data.p10).cross(data.p00 - data.p10).normalize()); - let mut n01 = Normal3f::from((data.p00 - data.p01).cross(data.p11 - data.p01).normalize()); - let mut n11 = Normal3f::from((data.p01 - data.p11).cross(data.p10 - data.p11).normalize()); + let mut n00 = Normal3f::from((p10 - p00).cross(p01 - p00).normalize()); + let mut n10 = Normal3f::from((p11 - p10).cross(p00 - p10).normalize()); + let mut n01 = Normal3f::from((p00 - p01).cross(p11 - p01).normalize()); + let mut n11 = Normal3f::from((p01 - p11).cross(p10 - p11).normalize()); - if let Some(normals) = data.n { - n00 = n00.face_forward(normals[0].into()); - n10 = n10.face_forward(normals[1].into()); - n01 = n01.face_forward(normals[2].into()); - n11 = n11.face_forward(normals[3].into()); - } else if data.mesh.reverse_orientation ^ data.mesh.transform_swaps_handedness { + if let Some(normals) = normals { + n00 = n00.face_forward(normals[0]); + n10 = n10.face_forward(normals[1]); + n01 = n01.face_forward(normals[2]); + n11 = n11.face_forward(normals[3]); + } else if self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness { n00 = -n00; n10 = -n10; n01 = -n01; @@ -559,16 +606,17 @@ impl ShapeTrait for BilinearPatchShape { #[inline] fn bounds(&self) -> Bounds3f { - let data = self.get_data(); - Bounds3f::from_points(data.p00, data.p01).union(Bounds3f::from_points(data.p10, data.p11)) + let [p00, p01, p10, p11] = self.get_points(); + Bounds3f::from_points(p00, p01).union(Bounds3f::from_points(p10, p11)) } #[inline] fn intersect(&self, ray: &Ray, t_max: Option) -> Option { let t_max_val = t_max?; - let data = self.get_data(); - if let Some(bilinear_hit) = self.intersect_bilinear_patch(ray, t_max_val, &data) { - let intr = self.interaction_from_intersection(&data, bilinear_hit.uv, ray.time, -ray.d); + if let Some(bilinear_hit) = + self.intersect_bilinear_patch(ray, t_max_val, &self.get_points()) + { + let intr = self.interaction_from_intersection(bilinear_hit.uv, ray.time, -ray.d); Some(ShapeIntersection { intr, @@ -582,25 +630,28 @@ impl ShapeTrait for BilinearPatchShape { #[inline] fn intersect_p(&self, ray: &Ray, t_max: Option) -> bool { let t_max_val = t_max.unwrap_or(Float::INFINITY); - let data = self.get_data(); - self.intersect_bilinear_patch(ray, t_max_val, &data) + let corners = self.get_points(); + self.intersect_bilinear_patch(ray, t_max_val, &corners) .is_some() } #[inline] fn sample(&self, u: Point2f) -> Option { - let data = self.get_data(); + let corners = self.get_points(); + let [p00, p01, p10, p11] = corners; // Sample bilinear patch parametric coordinate (u, v) - let (uv, pdf) = self.sample_parametric_coords(&data, u); + let (uv, pdf) = self.sample_parametric_coords(&corners, u); // Compute bilinear patch geometric quantities at sampled (u, v) - let (p, dpdu, dpdv) = self.calculate_base_derivatives(&data, uv); + let (p, dpdu, dpdv) = self.calculate_base_derivatives(&corners, uv); if dpdu.norm_squared() == 0. || dpdv.norm_squared() == 0. { return None; } // Compute surface normal for sampled bilinear patch (u, v) - let n = self.compute_sampled_normal(&data, &dpdu, &dpdv, uv); - let st = data.uv.map_or(uv, |patch_uvs| { + let patch_normals = self.get_shading_normals(); + let patch_uvs = self.get_uvs(); + let n = self.compute_sampled_normal(patch_normals, &dpdu, &dpdv, uv); + let st = patch_uvs.map_or(uv, |patch_uvs| { lerp( uv[0], lerp(uv[1], patch_uvs[0], patch_uvs[1]), @@ -608,33 +659,34 @@ impl ShapeTrait for BilinearPatchShape { ) }); - let p_abs_sum = data.p00.abs() - + Vector3f::from(data.p01.abs()) - + Vector3f::from(data.p10.abs()) - + Vector3f::from(data.p11.abs()); + let p_abs_sum = p00.abs() + + Vector3f::from(p01.abs()) + + Vector3f::from(p10.abs()) + + Vector3f::from(p11.abs()); let p_error = gamma(6) * Vector3f::from(p_abs_sum); let pi = Point3fi::new_with_error(p, p_error); Some(ShapeSample { - intr: Arc::new(SurfaceInteraction::new_simple(pi, n, st)), + intr: Interaction::Surface(SurfaceInteraction::new_simple(pi, n, st)), pdf: pdf / dpdu.cross(dpdv).norm(), }) } #[inline] fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option { - let data = self.get_data(); - let v00 = (data.p00 - ctx.p()).normalize(); - let v10 = (data.p10 - ctx.p()).normalize(); - let v01 = (data.p01 - ctx.p()).normalize(); - let v11 = (data.p11 - ctx.p()).normalize(); + let corners = self.get_points(); + let [p00, p01, p10, p11] = corners; + let v00 = (p00 - ctx.p()).normalize(); + let v10 = (p10 - ctx.p()).normalize(); + let v01 = (p01 - ctx.p()).normalize(); + let v11 = (p11 - ctx.p()).normalize(); let use_area_sampling = self.rectangle - || data.mesh.image_distribution.is_some() + || !self.mesh.image_distribution.is_null() || spherical_quad_area(v00, v10, v11, v01) <= Self::MIN_SPHERICAL_SAMPLE_AREA; if use_area_sampling { self.sample_area_and_pdf(ctx, u) } else { - self.sample_solid_angle(&data, ctx, u, &[v00, v10, v01, v11]) + self.sample_solid_angle(&corners, ctx, u, &[v00, v10, v01, v11]) } } @@ -644,28 +696,30 @@ impl ShapeTrait for BilinearPatchShape { return 0.0; }; - let data = self.get_data(); - let uv = if let Some(uvs) = &data.mesh.uv { - Point2f::invert_bilinear(si.uv, uvs) + let corners = self.get_points(); + let [p00, p01, p10, p11] = corners; + let patch_uvs = self.get_uvs(); + let uv = if let Some(uvs) = patch_uvs { + Point2f::invert_bilinear(si.common.uv, &uvs) } else { - si.uv + si.common.uv }; - let param_pdf = if let Some(image_distrib) = &data.mesh.image_distribution { - image_distrib.pdf(uv) + let param_pdf = if !self.mesh.image_distribution.is_null() { + self.mesh.image_distribution.pdf(uv) } else if self.rectangle { let w = [ - (data.p10 - data.p00).cross(data.p01 - data.p00).norm(), - (data.p10 - data.p00).cross(data.p11 - data.p10).norm(), - (data.p01 - data.p00).cross(data.p11 - data.p01).norm(), - (data.p11 - data.p10).cross(data.p11 - data.p01).norm(), + (p10 - p00).cross(p01 - p00).norm(), + (p10 - p00).cross(p11 - p10).norm(), + (p01 - p00).cross(p11 - p01).norm(), + (p11 - p10).cross(p11 - p01).norm(), ]; bilinear_pdf(uv, &w) } else { 1. }; - let (_, dpdu, dpdv) = self.calculate_base_derivatives(&data, uv); + let (_, dpdu, dpdv) = self.calculate_base_derivatives(&corners, uv); let cross = dpdu.cross(dpdv).norm(); if cross == 0. { 0. } else { param_pdf / cross } } @@ -677,15 +731,16 @@ impl ShapeTrait for BilinearPatchShape { return 0.; }; - let data = self.get_data(); + let corners = self.get_points(); + let [p00, p01, p10, p11] = corners; - let v00 = (data.p00 - ctx.p()).normalize(); - let v10 = (data.p10 - ctx.p()).normalize(); - let v01 = (data.p01 - ctx.p()).normalize(); - let v11 = (data.p11 - ctx.p()).normalize(); + let v00 = (p00 - ctx.p()).normalize(); + let v10 = (p10 - ctx.p()).normalize(); + let v01 = (p01 - ctx.p()).normalize(); + let v11 = (p11 - ctx.p()).normalize(); let use_area_sampling = !self.rectangle - || data.mesh.image_distribution.is_some() + || !self.mesh.image_distribution.is_null() || spherical_quad_area(v00, v10, v01, v11) <= Self::MIN_SPHERICAL_SAMPLE_AREA; if use_area_sampling { @@ -709,9 +764,9 @@ impl ShapeTrait for BilinearPatchShape { ]; let u = invert_spherical_rectangle_sample( ctx.p(), - data.p00, - data.p10 - data.p00, - data.p01 - data.p00, + p00, + p10 - p00, + p01 - p00, isect.intr.p(), ); pdf *= bilinear_pdf(u, &w); diff --git a/shared/src/shapes/curves.rs b/shared/src/shapes/curves.rs index fe5cc33..d16c933 100644 --- a/shared/src/shapes/curves.rs +++ b/shared/src/shapes/curves.rs @@ -1,21 +1,95 @@ -use crate::core::interaction::InteractionTrait; +use crate::Float; +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, + VectorLike, +}; +use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; +use crate::core::shape::{ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait}; use crate::utils::math::{clamp, lerp, square}; use crate::utils::splines::{ bound_cubic_bezier, cubic_bezier_control_points, evaluate_cubic_bezier, subdivide_cubic_bezier, }; -use crate::utils::transform::look_at; +use crate::utils::transform::{Transform, look_at}; -use super::{ - Bounds3f, CurveCommon, CurveShape, CurveType, DirectionCone, Float, Interaction, Normal3f, - Point2f, Point3f, Point3fi, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, - ShapeTrait, SurfaceInteraction, Transform, Vector2f, Vector3f, VectorLike, -}; -use std::sync::Arc; +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CurveType { + Flat, + Cylinder, + Ribbon, +} +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CurveCommon { + pub curve_type: CurveType, + pub cp_obj: [Point3f; 4], + pub width: [Float; 2], + pub n: [Normal3f; 2], + pub normal_angle: Float, + pub inv_sin_normal_angle: Float, + pub render_from_object: Transform, + pub object_from_render: Transform, + pub reverse_orientation: bool, + pub transform_swap_handedness: bool, +} + +impl CurveCommon { + #[allow(clippy::too_many_arguments)] + pub fn new( + c: &[Point3f], + w0: Float, + w1: Float, + curve_type: CurveType, + norm: &[Normal3f], + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + ) -> Self { + let transform_swap_handedness = render_from_object.swaps_handedness(); + let width = [w0, w1]; + assert_eq!(c.len(), 4); + let cp_obj: [Point3f; 4] = c[..4].try_into().unwrap(); + + let mut n = [Normal3f::default(); 2]; + let mut normal_angle: Float = 0.; + let mut inv_sin_normal_angle: Float = 0.; + if norm.len() == 2 { + n[0] = norm[0].normalize(); + n[1] = norm[1].normalize(); + normal_angle = n[0].angle_between(n[1]); + inv_sin_normal_angle = 1. / normal_angle.sin(); + } + + Self { + curve_type, + cp_obj, + width, + n, + normal_angle, + inv_sin_normal_angle, + render_from_object, + object_from_render, + reverse_orientation, + transform_swap_handedness, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CurveShape { + pub common: CurveCommon, + pub u_min: Float, + pub u_max: Float, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] struct IntersectionContext { - ray: Ray, - object_from_ray: Arc, - common: CurveCommon, + pub ray: Ray, + pub object_from_ray: Transform, + pub common: CurveCommon, } impl CurveShape { @@ -32,7 +106,6 @@ impl CurveShape { .common .object_from_render .apply_to_ray(r, &mut Some(t_max)); - // Get object-space control points for curve segment, cpObj let cp_obj = cubic_bezier_control_points(&self.common.cp_obj, self.u_min, self.u_max); // Project curve control points to plane perpendicular to ray let mut dx = ray.d.cross(cp_obj[3] - cp_obj[0]); @@ -43,7 +116,6 @@ impl CurveShape { let ray_from_object = look_at(ray.o, ray.o + ray.d, dx).expect("Inversion error"); let cp = [0; 4].map(|i| ray_from_object.apply_to_point(cp_obj[i])); - // Test ray against bound of projected control points let max_width = lerp(self.u_min, self.common.width[0], self.common.width[1]).max(lerp( self.u_max, self.common.width[0], @@ -77,7 +149,7 @@ impl CurveShape { let context = IntersectionContext { ray, - object_from_ray: Arc::new(ray_from_object.inverse()), + object_from_ray: ray_from_object.inverse(), common: self.common.clone(), }; @@ -300,18 +372,18 @@ impl ShapeTrait for CurveShape { } fn pdf(&self, _interaction: &Interaction) -> Float { - todo!() + unimplemented!() } fn pdf_from_context(&self, _ctx: &ShapeSampleContext, _wi: Vector3f) -> Float { - todo!() + unimplemented!() } fn sample(&self, _u: Point2f) -> Option { - todo!() + unimplemented!() } fn sample_from_context(&self, _ctx: &ShapeSampleContext, _u: Point2f) -> Option { - todo!() + unimplemented!() } } diff --git a/shared/src/shapes/cylinder.rs b/shared/src/shapes/cylinder.rs index 8050db2..1560866 100644 --- a/shared/src/shapes/cylinder.rs +++ b/shared/src/shapes/cylinder.rs @@ -1,20 +1,39 @@ -use super::{ - Bounds3f, CylinderShape, DirectionCone, Float, Interaction, Normal3f, PI, Point2f, Point3f, - Point3fi, QuadricIntersection, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, - ShapeTrait, SurfaceInteraction, Transform, Vector3f, Vector3fi, +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, + Vector3fi, VectorLike, }; -use crate::core::geometry::{Sqrt, Tuple, VectorLike}; -use crate::core::interaction::InteractionTrait; -use crate::core::pbrt::gamma; +use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; +use crate::core::shape::{ + QuadricIntersection, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, +}; +use crate::utils::splines::{ + bound_cubic_bezier, cubic_bezier_control_points, evaluate_cubic_bezier, subdivide_cubic_bezier, +}; +use crate::utils::transform::{Transform, look_at}; +use crate::{Float, PI, gamma}; + +use crate::core::geometry::{Sqrt, Tuple}; use crate::utils::interval::Interval; -use crate::utils::math::{difference_of_products, lerp, square}; +use crate::utils::math::{clamp, difference_of_products, lerp, square}; use std::mem; -use std::sync::Arc; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CylinderShape { + pub radius: Float, + pub z_min: Float, + pub z_max: Float, + pub phi_max: Float, + pub render_from_object: Transform, + pub object_from_render: Transform, + pub reverse_orientation: bool, + pub transform_swap_handedness: bool, +} impl CylinderShape { pub fn new( - render_from_object: Arc, - object_from_render: Arc, + render_from_object: Transform, + object_from_render: Transform, reverse_orientation: bool, radius: Float, z_min: Float, @@ -26,7 +45,7 @@ impl CylinderShape { z_min, z_max, phi_max, - render_from_object: render_from_object.clone(), + render_from_object, object_from_render, reverse_orientation, transform_swap_handedness: render_from_object.swaps_handedness(), @@ -247,14 +266,14 @@ impl ShapeTrait for CylinderShape { (p_obj.z() - self.z_min) / (self.z_max - self.z_min), ); Some(ShapeSample { - intr: Arc::new(SurfaceInteraction::new_simple(pi, n, uv)), + intr: SurfaceInteraction::new_simple(pi, n, uv), pdf: 1. / self.area(), }) } fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option { let mut ss = self.sample(u)?; - let intr = Arc::make_mut(&mut ss.intr); + let intr = &mut ss.intr; intr.get_common_mut().time = ctx.time; let mut wi = ss.intr.p() - ctx.p(); if wi.norm_squared() == 0. { diff --git a/shared/src/shapes/disk.rs b/shared/src/shapes/disk.rs index 5e0ac80..2b25e89 100644 --- a/shared/src/shapes/disk.rs +++ b/shared/src/shapes/disk.rs @@ -1,22 +1,38 @@ -use super::{ - Bounds3f, DirectionCone, DiskShape, Float, Interaction, Normal3f, PI, Point2f, Point3f, - Point3fi, QuadricIntersection, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, - ShapeTrait, SurfaceInteraction, Transform, Vector3f, +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, + Vector3fi, VectorLike, }; -use crate::core::geometry::VectorLike; -use crate::core::interaction::InteractionTrait; +use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; +use crate::core::shape::{ + QuadricIntersection, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, +}; +use crate::utils::Transform; use crate::utils::math::square; use crate::utils::sampling::sample_uniform_disk_concentric; +use crate::{Float, PI}; use std::sync::Arc; +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct DiskShape { + pub radius: Float, + pub inner_radius: Float, + pub height: Float, + pub phi_max: Float, + pub render_from_object: Transform, + pub object_from_render: Transform, + pub reverse_orientation: bool, + pub transform_swap_handedness: bool, +} + impl DiskShape { pub fn new( radius: Float, inner_radius: Float, height: Float, phi_max: Float, - render_from_object: Arc, - object_from_render: Arc, + render_from_object: Transform, + object_from_render: Transform, reverse_orientation: bool, ) -> Self { Self { @@ -157,8 +173,9 @@ impl ShapeTrait for DiskShape { phi / self.phi_max, (self.radius - radius_sample) / (self.radius - self.inner_radius), ); + Some(ShapeSample { - intr: Arc::new(SurfaceInteraction::new_simple(pi, n, uv)), + intr: Interaction::Surface(SurfaceInteraction::new_simple(pi, n, uv)), pdf: 1. / self.area(), }) } @@ -173,17 +190,18 @@ impl ShapeTrait for DiskShape { fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option { let mut ss = self.sample(u)?; - let intr = Arc::make_mut(&mut ss.intr); - intr.get_common_mut().time = ctx.time; + ss.intr.get_common_mut().time = ctx.time; let mut wi = ss.intr.p() - ctx.p(); if wi.norm_squared() == 0. { return None; } wi = wi.normalize(); + ss.pdf = Vector3f::from(ss.intr.n()).dot(-wi).abs() / ctx.p().distance_squared(ss.intr.p()); if ss.pdf.is_infinite() { return None; } + Some(ss) } diff --git a/shared/src/shapes/mod.rs b/shared/src/shapes/mod.rs index 0034259..16e369f 100644 --- a/shared/src/shapes/mod.rs +++ b/shared/src/shapes/mod.rs @@ -5,315 +5,9 @@ pub mod disk; pub mod sphere; pub mod triangle; -use crate::core::geometry::{ - Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, - Vector3fi, VectorLike, -}; -use crate::core::interaction::{ - Interaction, InteractionTrait, MediumInteraction, SurfaceInteraction, -}; -use crate::core::material::Material; -use crate::core::medium::{Medium, MediumInterface}; -use crate::core::pbrt::{Float, PI}; -use crate::lights::Light; -use crate::utils::math::{next_float_down, next_float_up}; -use crate::utils::transform::Transform; -use enum_dispatch::enum_dispatch; -use std::sync::{Arc, Mutex}; - -#[derive(Debug, Clone)] -pub struct SphereShape { - radius: Float, - z_min: Float, - z_max: Float, - theta_z_min: Float, - theta_z_max: Float, - phi_max: Float, - render_from_object: Arc, - object_from_render: Arc, - reverse_orientation: bool, - transform_swap_handedness: bool, -} - -impl Default for SphereShape { - fn default() -> Self { - Self::new( - Transform::default().into(), - Transform::default().into(), - false, - 1.0, - -1.0, - 1.0, - 360.0, - ) - } -} - -#[derive(Debug, Clone)] -pub struct CylinderShape { - radius: Float, - z_min: Float, - z_max: Float, - phi_max: Float, - render_from_object: Arc, - object_from_render: Arc, - reverse_orientation: bool, - transform_swap_handedness: bool, -} - -#[derive(Debug, Clone)] -pub struct DiskShape { - radius: Float, - inner_radius: Float, - height: Float, - phi_max: Float, - render_from_object: Arc, - object_from_render: Arc, - reverse_orientation: bool, - transform_swap_handedness: bool, -} - -#[derive(Debug, Clone)] -pub struct TriangleShape { - pub mesh_ind: usize, - pub tri_index: usize, -} - -#[derive(Debug, Clone)] -pub struct BilinearPatchShape { - mesh_index: usize, - blp_index: usize, - area: Float, - rectangle: bool, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum CurveType { - Flat, - Cylinder, - Ribbon, -} - -#[derive(Debug, Clone)] -pub struct CurveCommon { - curve_type: CurveType, - cp_obj: [Point3f; 4], - width: [Float; 2], - n: [Normal3f; 2], - normal_angle: Float, - inv_sin_normal_angle: Float, - render_from_object: Arc, - object_from_render: Arc, - reverse_orientation: bool, - transform_swap_handedness: bool, -} - -impl CurveCommon { - #[allow(clippy::too_many_arguments)] - pub fn new( - c: &[Point3f], - w0: Float, - w1: Float, - curve_type: CurveType, - norm: &[Vector3f], - render_from_object: Arc, - object_from_render: Arc, - reverse_orientation: bool, - ) -> Self { - let transform_swap_handedness = render_from_object.swaps_handedness(); - let width = [w0, w1]; - assert_eq!(c.len(), 4); - let cp_obj: [Point3f; 4] = c[..4].try_into().unwrap(); - - let mut n = [Normal3f::default(); 2]; - let mut normal_angle: Float = 0.; - let mut inv_sin_normal_angle: Float = 0.; - if norm.len() == 2 { - n[0] = norm[0].normalize().into(); - n[1] = norm[1].normalize().into(); - normal_angle = n[0].angle_between(n[1]); - inv_sin_normal_angle = 1. / normal_angle.sin(); - } - - Self { - curve_type, - cp_obj, - width, - n, - normal_angle, - inv_sin_normal_angle, - render_from_object, - object_from_render, - reverse_orientation, - transform_swap_handedness, - } - } -} - -#[derive(Debug, Clone)] -pub struct CurveShape { - common: CurveCommon, - u_min: Float, - u_max: Float, -} - -// Define Intersection objects. This only varies for -#[derive(Debug, Clone)] -pub struct ShapeIntersection { - pub intr: SurfaceInteraction, - pub t_hit: Float, -} - -impl ShapeIntersection { - pub fn new(intr: SurfaceInteraction, t_hit: Float) -> Self { - Self { intr, t_hit } - } - - pub fn t_hit(&self) -> Float { - self.t_hit - } - - pub fn set_t_hit(&mut self, new_t: Float) { - self.t_hit = new_t; - } - - pub fn set_intersection_properties( - &mut self, - mtl: Arc, - area: Arc, - prim_medium_interface: Option, - ray_medium: Option>, - ) { - let ray_medium = ray_medium.expect("Ray medium must be defined for intersection"); - self.intr - .set_intersection_properties(mtl, area, prim_medium_interface, ray_medium); - } -} - -#[derive(Debug, Clone)] -pub struct QuadricIntersection { - t_hit: Float, - p_obj: Point3f, - phi: Float, -} - -impl QuadricIntersection { - pub fn new(t_hit: Float, p_obj: Point3f, phi: Float) -> Self { - Self { t_hit, p_obj, phi } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TriangleIntersection { - b0: Float, - b1: Float, - b2: Float, - t: Float, -} - -impl TriangleIntersection { - pub fn new(b0: Float, b1: Float, b2: Float, t: Float) -> Self { - Self { b0, b1, b2, t } - } -} - -#[derive(Debug, Clone)] -pub struct BilinearIntersection { - uv: Point2f, - t: Float, -} - -impl BilinearIntersection { - pub fn new(uv: Point2f, t: Float) -> Self { - Self { uv, t } - } -} - -#[derive(Clone)] -pub struct ShapeSample { - pub intr: Arc, - pub pdf: Float, -} - -#[derive(Clone, Debug)] -pub struct ShapeSampleContext { - pub pi: Point3fi, - pub n: Normal3f, - pub ns: Normal3f, - pub time: Float, -} - -impl ShapeSampleContext { - pub fn new(pi: Point3fi, n: Normal3f, ns: Normal3f, time: Float) -> Self { - Self { pi, n, ns, time } - } - - pub fn new_from_interaction(si: &SurfaceInteraction) -> Self { - Self { - pi: si.pi(), - n: si.n(), - ns: si.shading.n, - time: si.time(), - } - } - - pub fn p(&self) -> Point3f { - Point3f::from(self.pi) - } - - pub fn offset_ray_origin(&self, w: Vector3f) -> Point3f { - let d = self.n.abs().dot(self.pi.error().into()); - let mut offset = d * Vector3f::from(self.n); - if w.dot(self.n.into()) < 0.0 { - offset = -offset; - } - - let mut po = Point3f::from(self.pi) + offset; - for i in 0..3 { - if offset[i] > 0.0 { - po[i] = next_float_up(po[i]); - } else { - po[i] = next_float_down(po[i]); - } - } - po - } - - pub fn offset_ray_origin_from_point(&self, pt: Point3f) -> Point3f { - self.offset_ray_origin(pt - self.p()) - } - - pub fn spawn_ray(&self, w: Vector3f) -> Ray { - Ray::new(self.offset_ray_origin(w), w, Some(self.time), None) - } -} - -#[enum_dispatch] -pub trait ShapeTrait { - fn bounds(&self) -> Bounds3f; - fn normal_bounds(&self) -> DirectionCone; - fn intersect(&self, ray: &Ray, t_max: Option) -> Option; - fn intersect_p(&self, ray: &Ray, t_max: Option) -> bool; - fn area(&self) -> Float; - fn pdf(&self, interaction: &Interaction) -> Float; - fn pdf_from_context(&self, ctx: &ShapeSampleContext, wi: Vector3f) -> Float; - fn sample(&self, u: Point2f) -> Option; - fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option; -} - -#[derive(Debug, Clone)] -#[enum_dispatch(ShapeTrait)] -pub enum Shape { - Sphere(SphereShape), - Cylinder(CylinderShape), - Disk(DiskShape), - Triangle(TriangleShape), - BilinearPatch(BilinearPatchShape), - Curve(CurveShape), -} - -impl Default for Shape { - fn default() -> Self { - Shape::Sphere(SphereShape::default()) - } -} +pub use bilinear::*; +pub use curves::*; +pub use cylinder::*; +pub use disk::*; +pub use sphere::*; +pub use triangle::*; diff --git a/shared/src/shapes/sphere.rs b/shared/src/shapes/sphere.rs index 03b7689..5bffb78 100644 --- a/shared/src/shapes/sphere.rs +++ b/shared/src/shapes/sphere.rs @@ -1,22 +1,55 @@ -use super::{ - Bounds3f, DirectionCone, Float, Interaction, Normal3f, PI, Point2f, Point3f, Point3fi, - QuadricIntersection, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, - SphereShape, SurfaceInteraction, Transform, Vector3f, Vector3fi, +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, + Vector3fi, VectorLike, }; -use crate::core::geometry::{Frame, Sqrt, VectorLike, spherical_direction}; -use crate::core::interaction::InteractionTrait; +use crate::core::geometry::{Frame, Sqrt, spherical_direction}; +use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; use crate::core::pbrt::gamma; +use crate::core::shape::{ + QuadricIntersection, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, +}; +use crate::utils::Transform; use crate::utils::interval::Interval; use crate::utils::math::{clamp, difference_of_products, radians, safe_acos, safe_sqrt, square}; use crate::utils::sampling::sample_uniform_sphere; +use crate::{Float, PI}; use std::mem; use std::sync::Arc; +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct SphereShape { + pub radius: Float, + pub z_min: Float, + pub z_max: Float, + pub theta_z_min: Float, + pub theta_z_max: Float, + pub phi_max: Float, + pub render_from_object: Transform, + pub object_from_render: Transform, + pub reverse_orientation: bool, + pub transform_swap_handedness: bool, +} + +impl Default for SphereShape { + fn default() -> Self { + Self::new( + Transform::default().into(), + Transform::default().into(), + false, + 1.0, + -1.0, + 1.0, + 360.0, + ) + } +} + impl SphereShape { pub fn new( - render_from_object: Arc, - object_from_render: Arc, + render_from_object: Transform, + object_from_render: Transform, reverse_orientation: bool, radius: Float, z_min: Float, @@ -287,7 +320,7 @@ impl ShapeTrait for SphereShape { )); let si = SurfaceInteraction::new_simple(pi, n, uv); Some(ShapeSample { - intr: Arc::new(si), + intr: Interaction::Surface(si), pdf: 1. / self.area(), }) } @@ -297,8 +330,7 @@ impl ShapeTrait for SphereShape { let p_origin = ctx.offset_ray_origin_from_point(p_center); if p_origin.distance_squared(p_center) <= square(self.radius) { let mut ss = self.sample(u)?; - let intr = Arc::make_mut(&mut ss.intr); - intr.get_common_mut().time = ctx.time; + ss.intr.get_common_mut().time = ctx.time; let mut wi = ss.intr.p() - ctx.p(); if wi.norm_squared() == 0. { return None; @@ -349,9 +381,9 @@ impl ShapeTrait for SphereShape { (theta - self.theta_z_min) / (self.theta_z_max - self.theta_z_min), ); let pi = Point3fi::new_with_error(p_obj, p_error); - let si = SurfaceInteraction::new_simple(pi, n, uv); + let intr = SurfaceInteraction::new_simple(pi, n, uv); Some(ShapeSample { - intr: Arc::new(si), + intr: Interaction::Surface(intr), pdf: 1. / (2. * PI * one_minus_cos_theta_max), }) } diff --git a/shared/src/shapes/triangle.rs b/shared/src/shapes/triangle.rs index 71738dd..593293a 100644 --- a/shared/src/shapes/triangle.rs +++ b/shared/src/shapes/triangle.rs @@ -1,80 +1,130 @@ -use super::{ - Bounds3f, DirectionCone, Float, Interaction, Normal3f, Point2f, Point3f, Point3fi, Ray, - ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, SurfaceInteraction, - TriangleIntersection, TriangleShape, Vector2f, Vector3f, +use crate::Float; +use crate::core::geometry::{ + Bounds3f, DirectionCone, Normal, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3, + Vector3f, }; use crate::core::geometry::{Sqrt, Tuple, VectorLike, spherical_triangle_area}; -use crate::core::interaction::InteractionTrait; +use crate::core::interaction::{ + Interaction, InteractionBase, InteractionTrait, SimpleInteraction, SurfaceInteraction, +}; use crate::core::pbrt::gamma; +use crate::core::shape::{ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait}; use crate::utils::math::{difference_of_products, square}; use crate::utils::mesh::TriangleMesh; use crate::utils::sampling::{ bilinear_pdf, invert_spherical_triangle_sample, sample_bilinear, sample_spherical_triangle, sample_uniform_triangle, }; -use std::mem; -use std::sync::{Arc, OnceLock}; -pub static TRIANGLE_MESHES: OnceLock>> = OnceLock::new(); +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct TriangleIntersection { + b0: Float, + b1: Float, + b2: Float, + t: Float, +} -#[derive(Clone, Copy)] -struct TriangleData { - vertices: [Point3f; 3], - uvs: [Point2f; 3], - normals: Option<[Normal3f; 3]>, - area: Float, - normal: Normal3f, - reverse_orientation: bool, - transform_swaps_handedness: bool, +impl TriangleIntersection { + pub fn new(b0: Float, b1: Float, b2: Float, t: Float) -> Self { + Self { b0, b1, b2, t } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct TriangleShape { + pub mesh: TriangleMesh, + pub tri_index: u32, } impl TriangleShape { pub const MIN_SPHERICAL_SAMPLE_AREA: Float = 3e-4; pub const MAX_SPHERICAL_SAMPLE_AREA: Float = 6.22; - fn mesh(&self) -> &Arc { - let meshes = TRIANGLE_MESHES - .get() - .expect("Mesh has not been initialized"); - &meshes[self.mesh_ind] - } - - fn get_data(&self) -> TriangleData { - let mesh = self.mesh(); - let start = 3 * self.tri_index; - let indices = &mesh.vertex_indices[start..start + 3]; - let vertices = [mesh.p[indices[0]], mesh.p[indices[1]], mesh.p[indices[2]]]; - let uvs = mesh.uv.as_ref().map_or( + #[inline(always)] + fn get_vertex_indices(&self) -> [usize; 3] { + unsafe { + let base_ptr = self + .mesh + .vertex_indices + .0 + .add((self.tri_index as usize) * 3); [ - Point2f::zero(), - Point2f::new(1.0, 0.0), - Point2f::new(1.0, 1.0), - ], - |uv| [uv[indices[0]], uv[indices[1]], uv[indices[2]]], - ); - let normals = mesh - .n - .as_ref() - .map(|n| [n[indices[0]], n[indices[1]], n[indices[2]]]); - let dp1 = vertices[1] - vertices[0]; - let dp2 = vertices[2] - vertices[0]; - let normal = Normal3f::from(dp1.cross(dp2).normalize()); - let area = 0.5 * dp1.cross(dp2).norm(); - - TriangleData { - vertices, - uvs, - normals, - area, - normal, - reverse_orientation: mesh.reverse_orientation, - transform_swaps_handedness: mesh.transform_swaps_handedness, + *base_ptr.add(0) as usize, + *base_ptr.add(1) as usize, + *base_ptr.add(2) as usize, + ] } } - fn solid_angle(&self, p: Point3f) -> Float { - let data = self.get_data(); - let [p0, p1, p2] = data.vertices; + #[inline(always)] + fn get_points(&self) -> [Point3f; 3] { + let [v0, v1, v2] = self.get_vertex_indices(); + unsafe { + [ + *self.mesh.p.0.add(v0), + *self.mesh.p.0.add(v1), + *self.mesh.p.0.add(v2), + ] + } + } + + #[inline(always)] + fn get_uvs(&self) -> Option<[Point2f; 3]> { + if self.mesh.uv.is_null() { + return None; + } + let [v0, v1, v2] = self.get_vertex_indices(); + unsafe { + Some([ + *self.mesh.uv.0.add(v0), + *self.mesh.uv.0.add(v1), + *self.mesh.uv.0.add(v2), + ]) + } + } + + #[inline(always)] + fn get_tangents(&self) -> Option<[Vector3f; 3]> { + if self.mesh.s.is_null() { + return None; + } + let [v0, v1, v2] = self.get_vertex_indices(); + unsafe { + Some([ + *self.mesh.s.0.add(v0), + *self.mesh.s.0.add(v1), + *self.mesh.s.0.add(v2), + ]) + } + } + + #[inline(always)] + fn get_shading_normals(&self) -> Option<[Normal3f; 3]> { + if self.mesh.n.is_null() { + return None; + } + let [v0, v1, v2] = self.get_vertex_indices(); + unsafe { + Some([ + *self.mesh.n.0.add(v0), + *self.mesh.n.0.add(v1), + *self.mesh.n.0.add(v2), + ]) + } + } + + pub fn new(mesh: TriangleMesh, tri_index: u32) -> Self { + Self { mesh, tri_index } + } + + pub fn get_mesh(&self) -> TriangleMesh { + self.mesh + } + + pub fn solid_angle(&self, p: Point3f) -> Float { + let [p0, p1, p2] = self.get_points(); spherical_triangle_area( (p0 - p).normalize(), (p1 - p).normalize(), @@ -82,100 +132,15 @@ impl TriangleShape { ) } - fn intersect_triangle(&self, ray: &Ray, t_max: Float) -> Option { - let data = self.get_data(); - let [p0, p1, p2] = data.vertices; - if (p2 - p0).cross(p1 - p0).norm_squared() == 0. { - return None; - } - let mut p0t = p0 - Vector3f::from(ray.o); - let mut p1t = p1 - Vector3f::from(ray.o); - let mut p2t = p2 - Vector3f::from(ray.o); - - let kz = ray.d.abs().max_component_index(); - let kx = if kz == 3 { 0 } else { kz + 1 }; - let ky = if kz == 3 { 0 } else { kx + 1 }; - let d = ray.d.permute([kx, ky, kz]); - p0t = p0t.permute([kx, ky, kz]); - p1t = p1t.permute([kx, ky, kz]); - p2t = p2t.permute([kx, ky, kz]); - // Apply shear transformation to translated vertex positions - let sx = -d.x() / d.z(); - let sy = -d.y() / d.z(); - let sz = 1. / d.z(); - p0t[0] += sx * p0t.z(); - p0t[1] += sy * p0t.z(); - p1t[0] += sx * p1t.z(); - p1t[1] += sy * p1t.z(); - p2t[0] += sx * p2t.z(); - p2t[0] += sy * p2t.z(); - - // Compute edge function coefficients e0, e1, and e2 - let mut e0 = difference_of_products(p1t.x(), p2t.y(), p1t.y(), p2t.x()); - let mut e1 = difference_of_products(p2t.x(), p0t.y(), p2t.y(), p0t.x()); - let mut e2 = difference_of_products(p0t.x(), p1t.y(), p0t.y(), p1t.x()); - - // if mem::size_of::() == mem::size_of::() && (e0 == 0.0 || e1 == 0.0 || e2 == 0.0) - if e0 == 0.0 || e1 == 0.0 || e2 == 0.0 { - let [p0t64, p1t64, p2t64] = [p0t.cast::(), p1t.cast::(), p2t.cast::()]; - - e0 = (p2t64.y() * p1t64.x() - p2t64.x() * p1t64.y()) as Float; - e1 = (p0t64.y() * p2t64.x() - p0t64.x() * p2t64.y()) as Float; - e2 = (p1t64.y() * p0t64.x() - p1t64.x() * p0t64.y()) as Float; - } - - if (e0 < 0. || e1 < 0. || e2 < 0.) && (e0 > 0. || e1 > 0. || e2 > 0.) { - return None; - } - let det = e0 + e1 + e2; - if det == 0. { - return None; - } - - // Compute scaled hit distance to triangle and test against ray - p0t[2] *= sz; - p1t[2] *= sz; - p2t[2] *= sz; - - let t_scaled = e0 * p0t.z() + e1 * p1t.z() + e2 * p2t.z(); - if det < 0. && (t_scaled >= 0. || t_scaled < t_max * det) - || (det > 0. && (t_scaled <= 0. || t_scaled > t_max * det)) - { - return None; - } - - // Compute barycentric coordinates and value for triangle intersection - let inv_det = 1. / det; - let b0 = e0 * inv_det; - let b1 = e1 * inv_det; - let b2 = e2 * inv_det; - let t = t_scaled * inv_det; - - // Ensure that computed triangle is conservatively greater than zero - let max_z_t = Vector3f::new(p0t.z(), p1t.z(), p2t.z()) - .abs() - .max_component_value(); - let delta_z = gamma(3) * max_z_t; - - let max_x_t = Vector3f::new(p0t.x(), p1t.x(), p2t.x()) - .abs() - .max_component_value(); - let max_y_t = Vector3f::new(p0t.y(), p1t.y(), p2t.y()) - .abs() - .max_component_value(); - let delta_x = gamma(5) * (max_x_t + max_z_t); - let delta_y = gamma(5) * (max_y_t + max_z_t); - - let delta_e = 2. * (gamma(2) * max_x_t * max_y_t + delta_y * max_x_t + delta_x * max_y_t); - let max_e = Vector3f::new(e0, e1, e2).abs().max_component_value(); - let delta_t = - 3. * (gamma(3) * max_e * max_z_t + delta_e * max_z_t + delta_z * max_e) * inv_det.abs(); - - if t <= delta_t { - return None; - } - - Some(TriangleIntersection::new(b0, b1, b2, t)) + fn intersect_triangle( + &self, + _ray: &Ray, + _t_max: Float, + _p0: Point3f, + _p1: Point3f, + _p2: Point3f, + ) -> Option { + todo!() } fn interaction_from_intersection( @@ -184,233 +149,255 @@ impl TriangleShape { time: Float, wo: Vector3f, ) -> SurfaceInteraction { - let data = self.get_data(); - let [p0, p1, p2] = data.vertices; - let [uv0, uv1, uv2] = data.uvs; - // Compute triangle partial derivatives - let (dpdu, dpdv, degenerate_uv, det) = self.compute_partials(data); - // Interpolate (u, v) parametric coordinates and hit point - let p_hit_vec = - ti.b0 * Vector3f::from(p0) + ti.b1 * Vector3f::from(p1) + ti.b2 * Vector3f::from(p2); - let p_hit = Point3f::from(p_hit_vec); - let uv_hit_vec = - ti.b0 * Vector2f::from(uv0) + ti.b1 * Vector2f::from(uv1) + ti.b2 * Vector2f::from(uv2); - let uv_hit = Point2f::from(uv_hit_vec); + let [p0, p1, p2] = self.get_points(); + let uv = self.get_uvs().unwrap_or([ + Point2f::new(0.0, 0.0), + Point2f::new(1.0, 0.0), + Point2f::new(1.0, 1.0), + ]); + let duv02 = uv[0] - uv[2]; + let duv12 = uv[1] - uv[2]; + let dp02 = p0 - p2; + let dp12 = p1 - p2; + let determinant = difference_of_products(duv02[0], duv12[1], duv02[1], duv12[0]); + let degenerate = determinant.abs() < 1e-9; + let (mut dpdu, mut dpdv) = if !degenerate { + let invdet = 1. / determinant; + let ret0 = difference_of_products(duv12[1], dp02, duv02[1], dp12) * invdet; + let ret1 = difference_of_products(duv02[0], dp12, duv12[0], dp02) * invdet; + (ret0, ret1) + } else { + (Vector3f::zero(), Vector3f::zero()) + }; - // Return SurfaceInteraction for triangle hit> - let flip_normal = data.reverse_orientation ^ data.transform_swaps_handedness; - let p_abs_sum = (ti.b0 * Vector3f::from(p0)).abs() - + (ti.b1 * Vector3f::from(p1)).abs() - + (ti.b2 * Vector3f::from(p2)).abs(); + if degenerate || dpdu.cross(dpdv).norm_squared() == 0. { + let mut ng = (p2 - p0).cross(p1 - p0); + if ng.norm_squared() == 0. { + let v1 = p2 - p0; + let v2 = p1 - p0; + ng = v1.cast::().cross(v2.cast::()).cast::(); + assert!(ng.norm_squared() != 0.); + } + (dpdu, dpdv) = ng.normalize().coordinate_system(); + } + + let p0_vec = Vector3f::from(p0); + let p1_vec = Vector3f::from(p1); + let p2_vec = Vector3f::from(p2); + let p_hit = Point3f::from(ti.b0 * p0_vec + ti.b1 * p1_vec + ti.b2 * p2_vec); + let uv_hit = Point2f::from( + ti.b0 * Vector2f::from(uv[0]) + + ti.b1 * Vector2f::from(uv[1]) + + ti.b2 * Vector2f::from(uv[2]), + ); + + let p_abs_sum = (ti.b0 * p0_vec).abs() + (ti.b1 * p1_vec).abs() + (ti.b2 * p2_vec).abs(); let p_error = gamma(7) * p_abs_sum; + let mut ng = Normal3f::from(dp02.cross(dp12).normalize()); + + let flip_normal = self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness; + if flip_normal { + ng = -ng; + } + let mut isect = SurfaceInteraction::new( Point3fi::new_with_error(p_hit, p_error), uv_hit, wo, dpdu, dpdv, - Normal3f::default(), - Normal3f::default(), + Normal3f::zero(), + Normal3f::zero(), time, flip_normal, ); - isect.face_index = self - .mesh() - .face_indices - .as_ref() - .map_or(0, |fi| fi[self.tri_index]); - isect.common.n = data.normal; - isect.shading.n = isect.n(); - if flip_normal { - isect.common.n = -isect.n(); - isect.shading.n = -isect.shading.n; - } + isect.face_index = if !self.mesh.face_indices.is_null() { + unsafe { *self.mesh.face_indices.0.add(self.tri_index as usize) } + } else { + 0 + }; - if data.normals.is_some() || self.mesh().s.is_some() { - self.apply_shading_normals(&mut isect, ti, data, degenerate_uv, det); - } + isect.common.n = ng; + isect.shading.n = ng; + if !self.mesh.p.is_null() || !self.mesh.s.is_null() { + self.compute_shading_geometry(&mut isect, &ti, uv, dpdu, determinant, degenerate); + } isect } - fn compute_partials(&self, data: TriangleData) -> (Vector3f, Vector3f, bool, Float) { - let [p0, p1, p2] = data.vertices; - let [uv0, uv1, uv2] = data.uvs; - let duv02 = uv0 - uv2; - let duv12 = uv1 - uv2; - let dp02 = p0 - p2; - let dp12 = p1 - p2; - let det = difference_of_products(duv02[0], duv12[1], duv02[1], duv12[0]); - let degenerate_uv = det.abs() < 1e-9; - let (dpdu, dpdv) = if !degenerate_uv { - let inv_det = 1. / det; - ( - (dp02 * duv12[1] - dp12 * duv02[1]) * inv_det, - (dp12 * duv02[0] - dp02 * duv12[0]) * inv_det, - ) - } else { - let dp20 = p2 - p0; - let dp10 = p1 - p0; - let mut ng = dp20.cross(dp10); - if ng.norm_squared() == 0. { - ng = (dp20.cast::().cross(dp10.cast::())).cast(); - } - let n = ng.normalize(); - n.coordinate_system() - }; - (dpdu, dpdv, degenerate_uv, det) - } - - fn apply_shading_normals( + fn compute_shading_geometry( &self, isect: &mut SurfaceInteraction, - ti: TriangleIntersection, - data: TriangleData, + ti: &TriangleIntersection, + uv: [Point2f; 3], + dpdu_geom: Vector3f, + determinant: Float, degenerate_uv: bool, - det: Float, ) { - let Some([n0, n1, n2]) = data.normals else { - return; - }; - let [uv0, uv1, uv2] = data.uvs; - let duv02 = uv0 - uv2; - let duv12 = uv1 - uv2; - - let ns = ti.b0 * n0 + ti.b1 * n1 + ti.b2 * n2; - let ns = if ns.norm_squared() > 0. { - ns.normalize() + // Interpolate vertex normals if they exist + let ns = if let Some(normals) = self.get_shading_normals() { + let n = ti.b0 * normals[0] + ti.b1 * normals[1] + ti.b2 * normals[2]; + if n.norm_squared() > 0.0 { + n.normalize() + } else { + isect.n() + } } else { isect.n() }; - let mut ss = self.mesh().s.as_ref().map_or(isect.dpdu, |s| { - let indices = &self.mesh().vertex_indices[3 * self.tri_index..3 * self.tri_index + 3]; - let interp_s = ti.b0 * s[indices[0]] + ti.b1 * s[indices[1]] + ti.b2 * s[indices[2]]; - - if interp_s.norm_squared() > 0. { - interp_s + // Interpolate tangents if they exist + let mut ss = if let Some(tangents) = self.get_tangents() { + let s = ti.b0 * tangents[0] + ti.b1 * tangents[1] + ti.b2 * tangents[2]; + if s.norm_squared() > 0.0 { + s.normalize() } else { - isect.dpdu - } - }); - - let mut ts = Vector3f::from(ns).cross(ss); - if ts.norm_squared() > 0. { - ss = ts.cross(Vector3f::from(ns)); - } else { - (ss, ts) = Vector3f::from(ns).coordinate_system(); - } - let (dndu, dndv) = if degenerate_uv { - let dn = (n2 - n0).cross(n1 - n0); - if dn.norm_squared() == 0. { - (Normal3f::zero(), Normal3f::zero()) - } else { - dn.coordinate_system() + dpdu_geom } } else { - let inv_det = 1. / det; - let dn02 = n0 - n2; - let dn12 = n1 - n2; - ( - (dn02 * duv12[1] - dn12 * duv02[1]) * inv_det, - (dn12 * duv02[0] - dn02 * duv12[0]) * inv_det, - ) + dpdu_geom }; - isect.shading.n = ns; - isect.shading.dpdu = ss; - isect.shading.dpdv = ts; - isect.dndu = dndu; - isect.dndv = dndv; + // Ensure shading tangent (ss) is perpendicular to shading normal (ns) + let mut ts = ns.cross(ss.into()); + if ts.norm_squared() > 0.0 { + ss = ts.cross(ns.into()).into(); + } else { + let (s, t) = ns.coordinate_system(); + ss = s.into(); + ts = t.into(); + } + + // How does the normal change as we move across UVs? + let (dndu, dndv) = if let Some(normals) = self.get_shading_normals() { + if degenerate_uv { + let dn = (normals[2] - normals[0]).cross(normals[1] - normals[0]); + if dn.norm_squared() == 0.0 { + (Normal3f::zero(), Normal3f::zero()) + } else { + let (dnu, dnv) = dn.coordinate_system(); + (Normal3f::from(dnu), Normal3f::from(dnv)) + } + } else { + let dn1 = normals[0] - normals[2]; + let dn2 = normals[1] - normals[2]; + let duv02 = uv[0] - uv[2]; + let duv12 = uv[1] - uv[2]; + + let inv_det = 1.0 / determinant; + ( + difference_of_products(duv12[1], dn1, duv02[1], dn2) * inv_det, + difference_of_products(duv02[0], dn2, duv12[0], dn1) * inv_det, + ) + } + } else { + (Normal3f::zero(), Normal3f::zero()) + }; + + isect.set_shading_geom(ns, ss, ts.into(), dndu, dndv, true); } } impl ShapeTrait for TriangleShape { fn bounds(&self) -> Bounds3f { - let [p0, p1, p2] = self.get_data().vertices; + let [p0, p1, p2] = self.get_points(); Bounds3f::from_points(p0, p1).union_point(p2) } fn normal_bounds(&self) -> DirectionCone { - let data = self.get_data(); - let mut n = data.normal; - if let Some([n0, n1, n2]) = data.normals { - n = n.face_forward((n0 + n1 + n2).into()); - } else if data.reverse_orientation ^ data.transform_swaps_handedness { + let [p0, p1, p2] = self.get_points(); + let mut n: Normal3f = (p1 - p0).cross(p2 - p0).normalize().into(); + + if let Some(normals) = self.get_shading_normals() { + let [n0, n1, n2] = normals; + let ns = n0 + n1 + n2; + n = n.face_forward(ns); + } else if self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness { n = -n; } - DirectionCone::new_from_vector(Vector3f::from(n)) + + DirectionCone::new_from_vector(n.into()) } fn area(&self) -> Float { - self.get_data().area + let [p0, p1, p2] = self.get_points(); + 0.5 * (p1 - p0).cross(p2 - p0).norm() } - fn pdf(&self, _interaction: &Interaction) -> Float { - 1. / self.area() + fn sample(&self, u: Point2f) -> Option { + let [p0, p1, p2] = self.get_points(); + let b = sample_uniform_triangle(u); + let p = p0 + b[1] * (p1 - p0) + b[2] * (p2 - p0); + + let mut n: Normal3f = (p1 - p0).cross(p2 - p0).normalize().into(); + + if let Some(normals) = self.get_shading_normals() { + let [n0, n1, n2] = normals; + let ns = b[0] * n0 + b[1] * n1 + b[2] * n2; // b[2] is (1 - b0 - b1) + n = n.face_forward(ns); + } else if self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness { + n = -n; + } + + let uv_sample = if let Some(uvs) = self.get_uvs() { + let [uv0, uv1, uv2] = uvs; + uv0 + b[1] * (uv1 - uv0) + b[2] * (uv2 - uv0) + } else { + let v = b[0] * Vector2f::new(0.0, 0.0) + + b[1] * Vector2f::new(1.0, 0.0) + + b[2] * Vector2f::new(1.0, 1.0); + Point2f::from(v) + }; + + let p0_v = Vector3f::from(p0); + let p1_v = Vector3f::from(p1); + let p2_v = Vector3f::from(p2); + let p_abs_sum = (b[0] * p0_v).abs() + (b[1] * p1_v).abs() + (b[2] * p2_v).abs(); + + let p_error = Vector3f::from(p_abs_sum) * gamma(6); + + let intr_base = InteractionBase::new_surface_geom( + Point3fi::new_with_error(p, p_error), + n, + uv_sample, + Vector3f::default(), + 0., + ); + + Some(ShapeSample { + intr: Interaction::Simple(SimpleInteraction::new(intr_base)), + pdf: 1.0 / self.area(), + }) } - fn pdf_from_context(&self, ctx: &ShapeSampleContext, wi: Vector3f) -> Float { + fn sample_from_context(&self, ctx: &ShapeSampleContext, mut u: Point2f) -> Option { + let [p0, p1, p2] = self.get_points(); + + let (b, tri_pdf) = sample_spherical_triangle(&[p0, p1, p2], ctx.p(), u)?; + if tri_pdf == 0. { + return None; + } + let solid_angle = self.solid_angle(ctx.p()); - if (Self::MIN_SPHERICAL_SAMPLE_AREA..Self::MAX_SPHERICAL_SAMPLE_AREA).contains(&solid_angle) + if solid_angle < Self::MIN_SPHERICAL_SAMPLE_AREA + || solid_angle > Self::MAX_SPHERICAL_SAMPLE_AREA { - let ray = ctx.spawn_ray(wi); - return self.intersect(&ray, None).map_or(0., |isect| { - let absdot = Vector3f::from(isect.intr.n()).dot(-wi).abs(); - let d2 = ctx.p().distance_squared(isect.intr.p()); - let pdf = 1. / self.area() * (d2 / absdot); - if pdf.is_infinite() { 0. } else { pdf } - }); + let mut ss = self.sample(u)?; + ss.intr.get_common_mut().time = ctx.time; + let mut wi: Normal3f = (ss.intr.p() - ctx.p()).into(); + if wi.norm_squared() == 0. { + return None; + } + wi = wi.normalize(); + ss.pdf /= ss.intr.n().abs_dot(-wi) / ctx.p().distance_squared(ss.intr.p()); + if ss.pdf.is_infinite() { + return None; + } + return Some(ss); } - let mut pdf = 1. / solid_angle; - if ctx.ns != Normal3f::zero() { - let [p0, p1, p2] = self.get_data().vertices; - let u = invert_spherical_triangle_sample(&[p0, p1, p2], ctx.p(), wi) - .unwrap_or(Point2f::zero()); - - let rp = ctx.p(); - let wi: [Vector3f; 3] = [ - (p0 - rp).normalize(), - (p1 - rp).normalize(), - (p2 - rp).normalize(), - ]; - let w: [Float; 4] = [ - 0.01_f32.max(ctx.ns.dot(wi[1].into()).abs()), - 0.01_f32.max(ctx.ns.dot(wi[1].into()).abs()), - 0.01_f32.max(ctx.ns.dot(wi[0].into()).abs()), - 0.01_f32.max(ctx.ns.dot(wi[2].into()).abs()), - ]; - pdf *= bilinear_pdf(u, &w); - } - pdf - } - - fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option { - let data = self.get_data(); - let [p0, p1, p2] = data.vertices; - let solid_angle = self.solid_angle(ctx.p()); - if (Self::MIN_SPHERICAL_SAMPLE_AREA..Self::MAX_SPHERICAL_SAMPLE_AREA).contains(&solid_angle) - { - // Sample shape by area and compute incident direction wi - return self.sample(u).and_then(|mut ss| { - let mut intr_clone = (*ss.intr).clone(); - intr_clone.common.time = ctx.time; - ss.intr = Arc::new(intr_clone); - - let wi = (ss.intr.p() - ctx.p()).normalize(); - if wi.norm_squared() == 0. { - return None; - } - let absdot = Vector3f::from(ss.intr.n()).abs_dot(-wi); - let d2 = ctx.p().distance_squared(ss.intr.p()); - ss.pdf /= absdot / d2; - if ss.pdf.is_infinite() { None } else { Some(ss) } - }); - } - - // Sample spherical triangle from reference point let mut pdf = 1.; if ctx.ns != Normal3f::zero() { let rp = ctx.p(); @@ -420,93 +407,112 @@ impl ShapeTrait for TriangleShape { (p2 - rp).normalize(), ]; let w: [Float; 4] = [ - 0.01_f32.max(ctx.ns.dot(wi[1].into()).abs()), - 0.01_f32.max(ctx.ns.dot(wi[1].into()).abs()), - 0.01_f32.max(ctx.ns.dot(wi[0].into()).abs()), - 0.01_f32.max(ctx.ns.dot(wi[2].into()).abs()), + ctx.ns.abs_dot(wi[1].into()).max(0.01), + ctx.ns.abs_dot(wi[1].into()).max(0.01), + ctx.ns.abs_dot(wi[0].into()).max(0.01), + ctx.ns.abs_dot(wi[2].into()).max(0.01), ]; - - let u = sample_bilinear(u, &w); + u = sample_bilinear(u, &w); pdf = bilinear_pdf(u, &w); } - let (b, tri_pdf) = sample_spherical_triangle(&[p0, p1, p2], ctx.p(), u)?; - if tri_pdf == 0. { - return None; - } - pdf *= tri_pdf; - let b2 = 1. - b[0] - b[1]; + let p0_v = Vector3f::from(p0); + let p1_v = Vector3f::from(p1); + let p2_v = Vector3f::from(p2); + let p_abs_sum = (b[0] * p0_v).abs() + (b[1] * p1_v).abs() + (b[2] * p2_v).abs(); + let mut n: Normal3f = (p1 - p0).cross(p2 - p0).normalize().into(); - let p_abs_sum = b[0] * Vector3f::from(p0) - + b[1] * Vector3f::from(p1) - + (1. - b[0] - b[1]) * Vector3f::from(p2); - let p_error = gamma(6) * p_abs_sum; - // Return ShapeSample for solid angle sampled point on triangle - let p_vec = - b[0] * Vector3f::from(p0) + b[1] * Vector3f::from(p1) + b[2] * Vector3f::from(p2); - let p = Point3f::from(p_vec); - let mut n = Normal3f::from((p1 - p0).cross(p2 - p0).normalize()); - if let Some([n0, n1, n2]) = data.normals { - let ns = b[0] * n0 + b[1] * n1 + b2 * n2; - n = n.face_forward(ns.into()); - } else if data.reverse_orientation ^ data.transform_swaps_handedness { + if let Some(normals) = self.get_shading_normals() { + let [n0, n1, n2] = normals; + let ns = b[0] * n0 + b[1] * n1 + (1. - b[0] - b[1]) * n2; + n = n.face_forward(ns); + } else if self.mesh.reverse_orientation ^ self.mesh.transform_swaps_handedness { n = -n; } - let [uv0, uv1, uv2] = data.uvs; - let uv_sample_vec = - b[0] * Vector2f::from(uv0) + b[1] * Vector2f::from(uv1) + b[2] * Vector2f::from(uv2); - let uv_sample = Point2f::from(uv_sample_vec); - let pi = Point3fi::new_with_error(p, p_error); - let mut si = SurfaceInteraction::new_simple(pi, n, uv_sample); - si.common.time = ctx.time; + let uv_sample = if let Some(uvs) = self.get_uvs() { + let [uv0, uv1, uv2] = uvs; + uv0 + b[1] * (uv1 - uv0) + b[2] * (uv2 - uv0) + } else { + let v = b[0] * Vector2f::new(0.0, 0.0) + + b[1] * Vector2f::new(1.0, 0.0) + + b[2] * Vector2f::new(1.0, 1.0); + Point2f::from(v) + }; + + let p = p0 + b[1] * (p1 - p0) + b[2] * (p2 - p0); + let p_error = Vector3f::from(p_abs_sum) * gamma(6); + let intr_base = InteractionBase::new_surface_geom( + Point3fi::new_with_error(p, p_error), + n, + uv_sample, + Vector3f::default(), + 0., + ); + Some(ShapeSample { - intr: Arc::new(si), + intr: Interaction::Simple(SimpleInteraction::new(intr_base)), pdf, }) } - fn sample(&self, u: Point2f) -> Option { - let data = self.get_data(); - let [p0, p1, p2] = data.vertices; - let [uv0, uv1, uv2] = data.uvs; - let b = sample_uniform_triangle(u); - let p_vec = - b[0] * Vector3f::from(p0) + b[1] * Vector3f::from(p1) + b[2] * Vector3f::from(p2); - let b2 = 1. - b[0] - b[1]; - let p = Point3f::from(p_vec); - let mut n = data.normal; - if let Some([n0, n1, n2]) = data.normals { - let interp_n = b[0] * n0 + b[1] * n1 + b2 * n2; - n = n.face_forward(interp_n.into()); - } else if data.reverse_orientation ^ data.transform_swaps_handedness { - n = -n; - } - - let uv_sample_vec = - b[0] * Vector2f::from(uv0) + b[1] * Vector2f::from(uv1) + b[2] * Vector2f::from(uv2); - let uv_sample = Point2f::from(uv_sample_vec); - let p_abs_sum = (b[0] * Vector3f::from(p0)).abs() - + (b[1] * Vector3f::from(p1)).abs() - + ((1. - b[0] - b[1]) * Vector3f::from(p2)).abs(); - let p_error = gamma(6) * p_abs_sum; - let pi = Point3fi::new_with_error(p, p_error); - Some(ShapeSample { - intr: Arc::new(SurfaceInteraction::new_simple(pi, n, uv_sample)), - pdf: 1. / self.area(), - }) - } - fn intersect(&self, ray: &Ray, t_max: Option) -> Option { - self.intersect_triangle(ray, t_max.unwrap_or(Float::INFINITY)) - .map(|ti| { - let intr = self.interaction_from_intersection(ti, ray.time, -ray.d); - ShapeIntersection { intr, t_hit: ti.t } - }) + let [p0, p1, p2] = self.get_points(); + let tri_isect = self.intersect_triangle(ray, t_max.unwrap_or(0.), p0, p1, p2)?; + let intr = self.interaction_from_intersection(tri_isect, ray.time, -ray.d); + Some(ShapeIntersection::new(intr, tri_isect.t)) } fn intersect_p(&self, ray: &Ray, t_max: Option) -> bool { - self.intersect_triangle(ray, t_max.unwrap_or(Float::INFINITY)) - .is_some() + let [p0, p1, p2] = self.get_points(); + let tri_isect = self.intersect_triangle(ray, t_max.unwrap_or(0.), p0, p1, p2); + tri_isect.is_some() + } + + fn pdf(&self, _interaction: &Interaction) -> Float { + 1. / self.area() + } + + fn pdf_from_context(&self, ctx: &ShapeSampleContext, wi: Vector3f) -> Float { + let solid_angle = self.solid_angle(ctx.p()); + + if solid_angle < Self::MIN_SPHERICAL_SAMPLE_AREA + || solid_angle > Self::MAX_SPHERICAL_SAMPLE_AREA + { + let ray = ctx.spawn_ray(wi); + let Some(isect) = self.intersect(&ray, None) else { + return 0.; + }; + + let pdf = (1. / self.area()) + / (isect.intr.n().abs_dot(-Normal3f::from(wi)) + / ctx.p().distance_squared(isect.intr.p())); + + if pdf.is_infinite() { + return 0.; + } + return pdf; + } + + let mut pdf = 1. / solid_angle; + if ctx.ns != Normal3f::zero() { + let [p0, p1, p2] = self.get_points(); + let u = invert_spherical_triangle_sample(&[p0, p1, p2], ctx.p(), wi) + .expect("Could not calculate inverse sample"); + let rp = ctx.p(); + let wi = [ + (p0 - rp).normalize(), + (p1 - rp).normalize(), + (p2 - rp).normalize(), + ]; + let w: [Float; 4] = [ + ctx.ns.abs_dot(wi[1].into()).max(0.01), + ctx.ns.abs_dot(wi[1].into()).max(0.01), + ctx.ns.abs_dot(wi[0].into()).max(0.01), + ctx.ns.abs_dot(wi[2].into()).max(0.01), + ]; + pdf *= bilinear_pdf(u, &w); + } + pdf } } diff --git a/shared/src/spectra/cie.rs b/shared/src/spectra/cie.rs index 179ecf6..8eaf3d9 100644 --- a/shared/src/spectra/cie.rs +++ b/shared/src/spectra/cie.rs @@ -1462,7 +1462,7 @@ pub const CIE_D65: [Float; 95] = [ N!(115.392), N!(115.923), N!(112.367), - N(108.811), + N!(108.811), N!(109.082), N!(109.354), N!(108.578), diff --git a/shared/src/spectra/colorspace.rs b/shared/src/spectra/colorspace.rs index 1419a42..85a5e53 100644 --- a/shared/src/spectra/colorspace.rs +++ b/shared/src/spectra/colorspace.rs @@ -3,31 +3,28 @@ use crate::core::geometry::Point2f; use crate::core::pbrt::Float; use crate::spectra::{DenselySampledSpectrum, SampledSpectrum}; use crate::utils::math::SquareMatrix3f; - -use once_cell::sync::Lazy; +use crate::utils::ptr::Ptr; use std::cmp::{Eq, PartialEq}; -use std::error::Error; -use std::sync::Arc; #[repr(C)] #[derive(Copy, Clone)] pub struct StandardColorSpaces { - pub srgb: *const RGBColorSpace, - pub dci_p3: *const RGBColorSpace, - pub rec2020: *const RGBColorSpace, - pub aces2065_1: *const RGBColorSpace, + pub srgb: Ptr, + pub dci_p3: Ptr, + pub rec2020: Ptr, + pub aces2065_1: Ptr, } #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct RGBColorSpace { pub r: Point2f, pub g: Point2f, pub b: Point2f, pub w: Point2f, pub illuminant: DenselySampledSpectrum, - pub rgb_to_spectrum_table: *const RGBToSpectrumTable, + pub rgb_to_spectrum_table: Ptr, pub xyz_from_rgb: SquareMatrix3f, pub rgb_from_xyz: SquareMatrix3f, } diff --git a/shared/src/spectra/mod.rs b/shared/src/spectra/mod.rs index 0d9b468..3064da3 100644 --- a/shared/src/spectra/mod.rs +++ b/shared/src/spectra/mod.rs @@ -6,7 +6,7 @@ pub mod simple; use crate::core::pbrt::Float; -pub use colorspace::RGBColorSpace; +pub use colorspace::{RGBColorSpace, StandardColorSpaces}; pub use rgb::*; pub use sampled::{CIE_Y_INTEGRAL, LAMBDA_MAX, LAMBDA_MIN}; pub use sampled::{N_SPECTRUM_SAMPLES, SampledSpectrum, SampledWavelengths}; diff --git a/shared/src/spectra/rgb.rs b/shared/src/spectra/rgb.rs index 20801c9..1044b10 100644 --- a/shared/src/spectra/rgb.rs +++ b/shared/src/spectra/rgb.rs @@ -4,6 +4,7 @@ use super::{ }; use crate::core::color::{RGB, RGBSigmoidPolynomial, XYZ}; use crate::core::spectrum::SpectrumTrait; +use crate::utils::Ptr; use crate::Float; @@ -68,13 +69,12 @@ impl SpectrumTrait for UnboundedRGBSpectrum { pub struct RGBIlluminantSpectrum { pub scale: Float, pub rsp: RGBSigmoidPolynomial, - pub illuminant: DenselySampledSpectrum, + pub illuminant: Ptr, } impl RGBIlluminantSpectrum { pub fn new(cs: &RGBColorSpace, rgb: RGB) -> Self { - let illuminant = &cs.illuminant; - let densely_sampled = DenselySampledSpectrum::from_spectrum(illuminant); + let illuminant = cs.illuminant; let m = rgb.max_component_value(); let scale = 2. * m; let rsp = cs.to_rgb_coeffs(if scale == 1. { @@ -85,33 +85,31 @@ impl RGBIlluminantSpectrum { Self { scale, rsp, - illuminant, + illuminant: Ptr::from(&illuminant), } } } impl SpectrumTrait for RGBIlluminantSpectrum { fn evaluate(&self, lambda: Float) -> Float { - match &self.illuminant { - Some(illuminant) => { - self.scale * self.rsp.evaluate(lambda) * illuminant.evaluate(lambda) - } - None => 0.0, + if self.illuminant.is_null() { + return 0.; } + self.scale * self.rsp.evaluate(lambda) * self.illuminant.evaluate(lambda) } fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { - if self.illuminant.is_none() { + if self.illuminant.is_null() { return SampledSpectrum::new(0.); } SampledSpectrum::from_fn(|i| self.scale * self.rsp.evaluate(lambda[i])) } fn max_value(&self) -> Float { - match &self.illuminant { - Some(illuminant) => self.scale * self.rsp.max_value() * illuminant.max_value(), - None => 0.0, + if self.illuminant.is_null() { + return 0.; } + self.scale * self.rsp.max_value() * self.illuminant.max_value() } } diff --git a/shared/src/textures/bilerp.rs b/shared/src/textures/bilerp.rs index ec4a618..57fd128 100644 --- a/shared/src/textures/bilerp.rs +++ b/shared/src/textures/bilerp.rs @@ -1,14 +1,18 @@ +use crate::Float; +use crate::core::spectrum::Spectrum; +use crate::core::spectrum::SpectrumTrait; use crate::core::texture::{TextureEvalContext, TextureMapping2D}; -use crate::spectra::{SampledSpectrum, SampledWavelengths, SpectrumTrait}; -use crate::utils::Transform; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::{Ptr, Transform}; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct FloatBilerpTexture { - mapping: TextureMapping2D, - v00: Float, - v01: Float, - v10: Float, - v11: Float, + pub mapping: TextureMapping2D, + pub v00: Float, + pub v01: Float, + pub v10: Float, + pub v11: Float, } #[inline(always)] @@ -40,22 +44,23 @@ impl FloatBilerpTexture { } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] pub struct SpectrumBilerpTexture { pub mapping: TextureMapping2D, - pub v00: Spectrum, - pub v01: Spectrum, - pub v10: Spectrum, - pub v11: Spectrum, + pub v00: Ptr, + pub v01: Ptr, + pub v10: Ptr, + pub v11: Ptr, } impl SpectrumBilerpTexture { pub fn new( mapping: TextureMapping2D, - v00: Spectrum, - v01: Spectrum, - v10: Spectrum, - v11: Spectrum, + v00: Ptr, + v01: Ptr, + v10: Ptr, + v11: Ptr, ) -> Self { Self { mapping, @@ -69,16 +74,16 @@ impl SpectrumBilerpTexture { pub fn evaluate( &self, ctx: &TextureEvalContext, - _lambda: &SampledWavelengths, + lambda: &SampledWavelengths, ) -> SampledSpectrum { let c = self.mapping.map(ctx); bilerp( - [c.st[0], c.st[1], c.st[2]], + [c.st[0], c.st[1]], [ - v00.sample(lambda), - v01.sample(lambda), - v10.sample(lambda), - v11.sample(lambda), + self.v00.sample(lambda), + self.v01.sample(lambda), + self.v10.sample(lambda), + self.v11.sample(lambda), ], ) } diff --git a/shared/src/textures/checkerboard.rs b/shared/src/textures/checkerboard.rs index 0d99efc..e9b1943 100644 --- a/shared/src/textures/checkerboard.rs +++ b/shared/src/textures/checkerboard.rs @@ -1,29 +1,105 @@ -use crate::core::texture::TextureEvalContext; +use crate::Float; +use crate::core::texture::{ + GPUFloatTexture, GPUSpectrumTexture, TextureEvalContext, TextureMapping2D, TextureMapping3D, + TextureMapping3DTrait, +}; use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::{Ptr, RelPtr, math::square}; -// TODO: I have to implement somethign like a TaggedPointer, and change the whole codebase. -// Fantastic -#[derive(Debug, Clone)] +fn checkerboard( + ctx: &TextureEvalContext, + map2d: Ptr, + map3d: Ptr, +) -> Float { + let d = |x: Float| -> Float { + let y = x / 2. - (x / 2.).floor() - 0.5; + return x / 2. + y * (1. - 2. * y.abs()); + }; + + let bf = |x: Float, r: Float| -> Float { + if (x.floor() - r) == (x + r).floor() { + return 1. - 2. * (x.floor() as i32 & 1) as Float; + } + (d(x + r) - 2. * d(x) + d(x - r)) / square(r) + }; + + if !map2d.is_null() { + assert!(map3d.is_null()); + let c = map2d.map(&ctx); + let ds = 1.5 * c.dsdx.abs().max(c.dsdy.abs()); + let dt = 1.5 * c.dtdx.abs().max(c.dtdy.abs()); + // Integrate product of 2D checkerboard function and triangle filter + 0.5 - bf(c.st[0], ds) * bf(c.st[1], dt) / 2. + } else { + assert!(!map3d.is_null()); + let c = map3d.map(&ctx); + let dx = 1.5 * c.dpdx.x().abs().max(c.dpdy.x().abs()); + let dy = 1.5 * c.dpdx.y().abs().max(c.dpdy.y().abs()); + let dz = 1.5 * c.dpdx.z().abs().max(c.dpdy.z().abs()); + 0.5 - bf(c.p.x(), dx) * bf(c.p.y(), dy) * bf(c.p.z(), dz) + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct FloatCheckerboardTexture { - pub map_2d: TextureMapping2D, - pub map_3d: TextureMapping3D, - pub tex: [FloatTexture; 2], + pub map2d: Ptr, + pub map3d: Ptr, + pub tex: [RelPtr; 2], } impl FloatCheckerboardTexture { - pub fn evaluate(&self, _ctx: &TextureEvalContext) -> Float { - todo!() + pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { + let w = checkerboard(&ctx, self.map2d, self.map3d); + + let mut t0 = 0.0; + let mut t1 = 0.0; + + if w != 1.0 { + if let Some(tex) = self.tex[0].get() { + t0 = tex.evaluate(ctx); + } + } + + if w != 0.0 { + if let Some(tex) = self.tex[1].get() { + t1 = tex.evaluate(ctx); + } + } + + (1.0 - w) * t0 + w * t1 } } -#[derive(Clone, Debug)] -pub struct SpectrumCheckerboardTexture; +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SpectrumCheckerboardTexture { + pub map2d: Ptr, + pub map3d: Ptr, + pub tex: [RelPtr; 2], +} + impl SpectrumCheckerboardTexture { pub fn evaluate( &self, - _ctx: &TextureEvalContext, - _lambda: &SampledWavelengths, + ctx: &TextureEvalContext, + lambda: &SampledWavelengths, ) -> SampledSpectrum { - todo!() + let w = checkerboard(ctx, self.map2d, self.map3d); + let mut t0 = SampledSpectrum::new(0.); + let mut t1 = SampledSpectrum::new(0.); + if w != 1.0 { + if let Some(tex) = self.tex[0].get() { + t0 = tex.evaluate(ctx, lambda); + } + } + + if w != 0.0 { + if let Some(tex) = self.tex[1].get() { + t1 = tex.evaluate(ctx, lambda); + } + } + + t0 * (1.0 - w) + t1 * w } } diff --git a/shared/src/textures/constant.rs b/shared/src/textures/constant.rs index 3c99827..042ab67 100644 --- a/shared/src/textures/constant.rs +++ b/shared/src/textures/constant.rs @@ -1,8 +1,10 @@ use crate::Float; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; use crate::core::texture::TextureEvalContext; -use crate::spectra::{SampledSpectrum, SampledWavelengths, Spectrum}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct FloatConstantTexture { pub value: Float, } @@ -17,7 +19,8 @@ impl FloatConstantTexture { } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] pub struct SpectrumConstantTexture { pub value: Spectrum, } diff --git a/shared/src/textures/dots.rs b/shared/src/textures/dots.rs index 226382a..d879d7b 100644 --- a/shared/src/textures/dots.rs +++ b/shared/src/textures/dots.rs @@ -1,19 +1,75 @@ -#[derive(Debug, Clone)] -pub struct FloatDotsTexture; +use crate::Float; +use crate::core::geometry::{Point2f, VectorLike}; +use crate::core::texture::{ + GPUFloatTexture, GPUSpectrumTexture, TextureEvalContext, TextureMapping2D, +}; +use crate::spectra::sampled::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; +use crate::utils::math::square; +use crate::utils::noise::noise_2d; + +fn inside_polka_dot(st: Point2f) -> bool { + let s_cell = (st[0] + 0.5).floor(); + let t_cell = (st[1] + 0.5).floor(); + if noise_2d(s_cell + 0.5, t_cell + 0.5) > 0. { + let radius = 0.35; + let max_shift = 0.5 + radius; + let s_center = s_cell + max_shift * noise_2d(s_cell + 1.5, t_cell + 2.8); + let t_center = t_cell + max_shift * noise_2d(s_cell + 4.5, t_cell + 9.8); + let dst = st - Point2f::new(s_center, t_center); + if dst.norm_squared() < square(radius) { + return true; + } + } + return false; +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FloatDotsTexture { + pub mapping: TextureMapping2D, + pub outside_dot: RelPtr, + pub inside_dot: RelPtr, +} + impl FloatDotsTexture { - pub fn evaluate(&self, _ctx: &TextureEvalContext) -> Float { - todo!() + pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { + let c = self.mapping.map(ctx); + if inside_polka_dot(c.st) { + if let Some(tex) = self.inside_dot.get() { + tex.evaluate(ctx) + } + } else { + if let Some(tex) = self.outside_dot.get() { + tex.evaluate(ctx) + } + } } } -#[derive(Clone, Debug)] -pub struct SpectrumDotsTexture; +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SpectrumDotsTexture { + pub mapping: TextureMapping2D, + pub outside_dot: RelPtr, + pub inside_dot: RelPtr, +} + impl SpectrumDotsTexture { pub fn evaluate( &self, - _ctx: &TextureEvalContext, - _lambda: &SampledWavelengths, + ctx: &TextureEvalContext, + lambda: &SampledWavelengths, ) -> SampledSpectrum { - todo!() + let c = self.mapping.map(ctx); + if inside_polka_dot(c.st) { + if let Some(tex) = self.inside_dot.get() { + tex.evaluate(ctx, &lambda) + } + } else { + if let Some(tex) = self.outside_dot.get() { + tex.evaluate(ctx, &lambda) + } + } } } diff --git a/shared/src/textures/fbm.rs b/shared/src/textures/fbm.rs index bfc8870..06b289f 100644 --- a/shared/src/textures/fbm.rs +++ b/shared/src/textures/fbm.rs @@ -1,10 +1,12 @@ +use crate::Float; use crate::core::texture::{TextureEvalContext, TextureMapping3D}; +use crate::utils::noise::fbm; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct FBmTexture { pub mapping: TextureMapping3D, pub omega: Float, - pub octaves: usize, + pub octaves: u32, } impl FBmTexture { diff --git a/shared/src/textures/image.rs b/shared/src/textures/image.rs index 38c1a05..e9c912d 100644 --- a/shared/src/textures/image.rs +++ b/shared/src/textures/image.rs @@ -1,12 +1,17 @@ use crate::Float; +use crate::core::color::{RGB, XYZ}; +use crate::core::spectrum::SpectrumTrait; use crate::core::texture::{SpectrumType, TextureEvalContext, TextureMapping2D}; -use crate::spectra::RGBColorSpace; +use crate::spectra::{ + RGBAlbedoSpectrum, RGBColorSpace, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, + SampledWavelengths, +}; /* GPU heavy code, dont know if this will ever work the way Im doing things. * Leaving it here isolated, for careful handling */ #[repr(C)] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Copy)] pub struct GPUSpectrumImageTexture { pub mapping: TextureMapping2D, pub tex_obj: u64, @@ -32,8 +37,8 @@ impl GPUSpectrumImageTexture { { use cuda_std::intrinsics; let c = self.mapping.map(ctx); - let u = c.st.x; - let v = 1.0 - c.st.y; + let u = c.st.x(); + let v = 1.0 - c.st.y(); let d_p_dx = [c.dsdx, c.dtdx]; let d_p_dy = [c.dsdy, c.dtdy]; @@ -53,22 +58,20 @@ impl GPUSpectrumImageTexture { rgb = (RGB::new(1.0, 1.0, 1.0) - rgb).clamp_zero(); } - let color_space = unsafe { &*self.color_space }; - match self.spectrum_type { SpectrumType::Unbounded => { - RGBUnboundedSpectrum::new(color_space, rgb).sample(lambda) + RGBUnboundedSpectrum::new(&self.color_space, rgb).sample(lambda) } SpectrumType::Albedo => { - RGBAlbedoSpectrum::new(color_space, rgb.clamp(0.0, 1.0)).sample(lambda) + RGBAlbedoSpectrum::new(&self.color_space, rgb.clamp(0.0, 1.0)).sample(lambda) } - _ => RGBIlluminantSpectrum::new(color_space, rgb).sample(lambda), + _ => RGBIlluminantSpectrum::new(&self.color_space, rgb).sample(lambda), } } } } -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub struct GPUFloatImageTexture { pub mapping: TextureMapping2D, pub tex_obj: u64, @@ -86,13 +89,13 @@ impl GPUFloatImageTexture { { use cuda_std::intrinsics; let c = self.mapping.map(ctx); - let u = c.st.x; - let v = 1.0 - c.st.y; + let u = c.st.x(); + let v = 1.0 - c.st.y(); let d_p_dx = [c.dsdx, c.dtdx]; let d_p_dy = [c.dsdy, c.dtdy]; let val: Float = unsafe { intrinsics::tex2d_grad(self.tex_obj, u, v, d_p_dx, d_p_dy) }; - if invert { + if self.invert { return (1. - v).max(0.); } else { return v; diff --git a/shared/src/textures/marble.rs b/shared/src/textures/marble.rs index 40a17ed..177f032 100644 --- a/shared/src/textures/marble.rs +++ b/shared/src/textures/marble.rs @@ -1,27 +1,40 @@ +use crate::Float; +use crate::core::color::RGB; +use crate::core::geometry::{Point3f, Vector3f}; +use crate::core::spectrum::SpectrumTrait; use crate::core::texture::{TextureEvalContext, TextureMapping3D}; -use crate::spectra::{RGBAlbedoSpectrum, SampledSpectrum, SampledWavelengths}; +use crate::spectra::{RGBAlbedoSpectrum, RGBColorSpace, SampledSpectrum, SampledWavelengths}; +use crate::utils::math::clamp; use crate::utils::noise::fbm; use crate::utils::splines::evaluate_cubic_bezier; -use crate::Float; -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Debug, Copy)] pub struct MarbleTexture { pub mapping: TextureMapping3D, - pub octaves: usize, + pub octaves: u32, pub omega: Float, pub scale: Float, pub variation: Float, + // TODO: DO not forget to pass StandardColorSpace here!! + pub colorspace: *const RGBColorSpace, } + +unsafe impl Send for MarbleTexture {} +unsafe impl Sync for MarbleTexture {} + impl MarbleTexture { pub fn evaluate( &self, ctx: &TextureEvalContext, - _lambda: &SampledWavelengths, + lambda: &SampledWavelengths, ) -> SampledSpectrum { let mut c = self.mapping.map(ctx); - c.p *= self.scale; - let marble = c.p.y() + self.variation * fbm(c.p, self.scale, c.dpdy, omega, self.octaves); - const COLORS: [RGB; 9] = [ + c.p = Point3f::from(Vector3f::from(c.p) * self.scale); + let marble = + c.p.y() + self.variation * fbm(c.p, self.scale, c.dpdy, self.omega, self.octaves); + let t = 0.5 + 0.5 * marble.sin(); + let colors: [RGB; 9] = [ RGB::new(0.58, 0.58, 0.6), RGB::new(0.58, 0.58, 0.6), RGB::new(0.58, 0.58, 0.6), @@ -34,23 +47,13 @@ impl MarbleTexture { ]; const N_SEG: i32 = 6; // (9 - 3) - let t_clamped = t.clamp(0.0, 1.0); + let t_clamped = clamp(t, 0.0, 1.0); let first = ((t_clamped * N_SEG as Float).floor() as i32).clamp(0, N_SEG - 1); let t_segment = t_clamped * N_SEG as Float - first as Float; let first_idx = first as usize; - let rgb = evaluate_cubic_bezier(&COLORS[first_idx..first_idx + 4], t_segment) * 1.5; + let rgb = evaluate_cubic_bezier(&colors[first_idx..first_idx + 4], t_segment) * 1.5; - let color_space = { - #[cfg(target_os = "cuda")] - { - unsafe { &*RGBColorSpace_sRGB } - } - - #[cfg(not(target_os = "cuda"))] - { - RGBColorSpace::srgb() - } - }; + let color_space = unsafe { &*self.colorspace }; RGBAlbedoSpectrum::new(color_space, rgb).sample(lambda) } } diff --git a/shared/src/textures/mix.rs b/shared/src/textures/mix.rs index 4e7835a..d09957f 100644 --- a/shared/src/textures/mix.rs +++ b/shared/src/textures/mix.rs @@ -1,33 +1,133 @@ -use crate::core::geometry::Vector3f; -use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture}; -use crate::utils::Ptr; +use crate::Float; +use crate::core::geometry::{Vector3f, VectorLike}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvalContext}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct GPUFloatMixTexture { - pub tex1: Ptr, - pub tex2: Ptr, + pub tex1: RelPtr, + pub tex2: RelPtr, + pub amount: RelPtr, +} + +impl GPUFloatMixTexture { + pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { + let amt = self.amount.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0); + let t1 = if amt != 1.0 { + self.tex1.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0) + } else { + 0.0 + }; + + let t2 = if amt != 0.0 { + self.tex2.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0) + } else { + 0.0 + }; + + (1.0 - amt) * t1 + amt * t2 + } } #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct GPUFloatDirectionMixTexture { - pub tex1: Ptr, - pub tex2: Ptr, + pub tex1: RelPtr, + pub tex2: RelPtr, pub dir: Vector3f, } +impl GPUFloatDirectionMixTexture { + pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { + let amt = self.dir.abs_dot(ctx.n.into()); + let t1 = if amt != 1.0 { + self.tex1.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0) + } else { + 0.0 + }; + + let t2 = if amt != 0.0 { + self.tex2.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0) + } else { + 0.0 + }; + + (1.0 - amt) * t1 + amt * t2 + } +} + #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct GPUSpectrumMixTexture { - pub tex1: Ptr, - pub tex2: Ptr, + pub tex1: RelPtr, + pub tex2: RelPtr, + pub amount: RelPtr, +} + +impl GPUSpectrumMixTexture { + pub fn evaluate( + &self, + ctx: &TextureEvalContext, + lambda: &SampledWavelengths, + ) -> SampledSpectrum { + let amt = self.amount.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0); + let t1 = if amt != 1.0 { + self.tex1 + .get() + .map(|t| t.evaluate(&ctx, &lambda)) + .unwrap_or(SampledSpectrum::new(0.)) + } else { + SampledSpectrum::new(0.) + }; + + let t2 = if amt != 0.0 { + self.tex2 + .get() + .map(|t| t.evaluate(&ctx, &lambda)) + .unwrap_or(SampledSpectrum::new(0.)) + } else { + SampledSpectrum::new(0.) + }; + + (1.0 - amt) * t1 + amt * t2 + } } #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct GPUSpectrumDirectionMixTexture { - pub tex1: Ptr, - pub tex2: Ptr, + pub tex1: RelPtr, + pub tex2: RelPtr, pub dir: Vector3f, } + +impl GPUSpectrumDirectionMixTexture { + pub fn evaluate( + &self, + ctx: &TextureEvalContext, + lambda: &SampledWavelengths, + ) -> SampledSpectrum { + let amt = self.dir.abs_dot(ctx.n.into()); + let t1 = if amt != 1.0 { + self.tex1 + .get() + .map(|t| t.evaluate(&ctx, &lambda)) + .unwrap_or(SampledSpectrum::new(0.)) + } else { + SampledSpectrum::new(0.) + }; + + let t2 = if amt != 0.0 { + self.tex2 + .get() + .map(|t| t.evaluate(&ctx, &lambda)) + .unwrap_or(SampledSpectrum::new(0.)) + } else { + SampledSpectrum::new(0.) + }; + + (1.0 - amt) * t1 + amt * t2 + } +} diff --git a/shared/src/textures/ptex.rs b/shared/src/textures/ptex.rs index 059e305..0a4645d 100644 --- a/shared/src/textures/ptex.rs +++ b/shared/src/textures/ptex.rs @@ -1,29 +1,32 @@ use crate::Float; +use crate::core::color::RGB; +use crate::core::spectrum::{SpectrumTrait, StandardSpectra}; use crate::core::texture::{SpectrumType, TextureEvalContext}; use crate::spectra::{ RGBAlbedoSpectrum, RGBColorSpace, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, - SampledWavelengths, + SampledWavelengths, StandardColorSpaces, }; +use crate::utils::ptr::{Ptr, Slice}; -/* GPU heavy code, have to see how to best approach this -*/ - -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct GPUFloatPtexTexture { - pub face_values: Vec, + pub face_values: *const Float, } impl GPUFloatPtexTexture { pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { - self.face_values[ctx.face_index] + unsafe { *self.face_values.add(ctx.face_index as usize) } } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy)] pub struct GPUSpectrumPtexTexture { - pub face_values: *const RGB, - pub n_faces: usize, + pub face_values: Slice, + pub n_faces: u32, pub spectrum_type: SpectrumType, + pub colorspaces: StandardColorSpaces, } impl GPUSpectrumPtexTexture { @@ -32,24 +35,19 @@ impl GPUSpectrumPtexTexture { ctx: &TextureEvalContext, lambda: &SampledWavelengths, ) -> SampledSpectrum { - let index = ctx.face_index.clamp(0, self.n_faces.saturating_sub(1)); - let rgb = unsafe { *self.face_values.add(index) }; - let s_rgb = { - #[cfg(feature = "cuda")] - unsafe { - &*RGBColorSpace_sRGB - } - #[cfg(not(feature = "cuda"))] - RGBColorSpace::srgb() - }; + let index = ctx + .face_index + .clamp(0, self.n_faces.saturating_sub(1) as usize); + let rgb = self.face_values[index]; + let s_rgb = self.colorspaces.srgb; match self.spectrum_type { - SpectrumType::Unbounded => RGBUnboundedSpectrum::new(s_rgb, rgb).sample(lambda), + SpectrumType::Unbounded => RGBUnboundedSpectrum::new(&s_rgb, rgb).sample(lambda), SpectrumType::Albedo => { let clamped_rgb = rgb.clamp(0.0, 1.0); - RGBAlbedoSpectrum::new(s_rgb, clamped_rgb).sample(lambda) + RGBAlbedoSpectrum::new(&s_rgb, clamped_rgb).sample(lambda) } - SpectrumType::Illuminant => RGBIlluminantSpectrum::new(s_rgb, rgb).sample(lambda), + SpectrumType::Illuminant => RGBIlluminantSpectrum::new(&s_rgb, rgb).sample(lambda), } } } diff --git a/shared/src/textures/scaled.rs b/shared/src/textures/scaled.rs index b531f02..d46ecc7 100644 --- a/shared/src/textures/scaled.rs +++ b/shared/src/textures/scaled.rs @@ -1,16 +1,44 @@ -use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture}; -use crate::utils::Ptr; +use crate::Float; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvalContext}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::RelPtr; #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct GPUFloatScaledTexture { - tex: Ptr, - scale: Ptr, + pub tex: RelPtr, + pub scale: RelPtr, +} + +impl GPUFloatScaledTexture { + pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { + let sc = self.scale.get().map(|t| t.evaluate(&ctx)).unwrap(); + if sc == 0. { + return 0.; + } + self.tex.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.0) * sc + } } #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct GPUSpectrumScaledTexture { - tex: Ptr, - scale: Ptr, + pub tex: RelPtr, + pub scale: RelPtr, +} + +impl GPUSpectrumScaledTexture { + pub fn evaluate( + &self, + ctx: &TextureEvalContext, + lambda: &SampledWavelengths, + ) -> SampledSpectrum { + let sc = self.scale.get().map(|t| t.evaluate(&ctx)).unwrap_or(0.); + + self.tex + .get() + .map(|t| t.evaluate(&ctx, &lambda)) + .unwrap_or(SampledSpectrum::new(0.)) + * sc + } } diff --git a/shared/src/textures/windy.rs b/shared/src/textures/windy.rs index fcb3bc7..246fee0 100644 --- a/shared/src/textures/windy.rs +++ b/shared/src/textures/windy.rs @@ -1,7 +1,10 @@ +use crate::Float; +use crate::core::geometry::{Point3f, Vector3f}; use crate::core::texture::{TextureEvalContext, TextureMapping3D}; use crate::utils::noise::fbm; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct WindyTexture { pub mapping: TextureMapping3D, } @@ -9,7 +12,13 @@ pub struct WindyTexture { impl WindyTexture { pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { let c = self.mapping.map(ctx); - let wind_strength = fbm(0.1 * c.p, 0.1 * c.dpdx, 0.1 * c.dpdy, 0.5, 3); + let wind_strength = fbm( + Point3f::from(0.1 * Vector3f::from(c.p)), + 0.1 * c.dpdx, + 0.1 * c.dpdy, + 0.5, + 3, + ); let wave_height = fbm(c.p, c.dpdx, c.dpdy, 0.5, 6); wind_strength.abs() * wave_height } diff --git a/shared/src/textures/wrinkled.rs b/shared/src/textures/wrinkled.rs index 8be20d4..97b642b 100644 --- a/shared/src/textures/wrinkled.rs +++ b/shared/src/textures/wrinkled.rs @@ -1,16 +1,18 @@ +use crate::Float; use crate::core::texture::{TextureEvalContext, TextureMapping3D}; use crate::utils::noise::turbulence; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct WrinkledTexture { pub mapping: TextureMapping3D, - pub octaves: usize, + pub octaves: u32, pub omega: Float, } impl WrinkledTexture { pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { let c = self.mapping.map(ctx); - turbulence(c.p, c.dpdx, c.dpdy, omega, octaves) + turbulence(c.p, c.dpdx, c.dpdy, self.omega, self.octaves) } } diff --git a/shared/src/utils/containers.rs b/shared/src/utils/containers.rs index be1815c..4125629 100644 --- a/shared/src/utils/containers.rs +++ b/shared/src/utils/containers.rs @@ -10,53 +10,65 @@ use crate::core::geometry::{ Bounds2i, Bounds3f, Bounds3i, Point2i, Point3f, Point3i, Vector2i, Vector3f, Vector3i, }; -// pub trait Interpolatable: -// Copy + Default + Add + Sub + Mul -// { -// } -// -// impl Interpolatable for T where -// T: Copy + Default + Add + Sub + Mul -// { -// } +pub trait Interpolatable: + Copy + Default + Add + Sub + Mul +{ +} + +impl Interpolatable for T where + T: Copy + Default + Add + Sub + Mul +{ +} #[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct Array2D { pub values: *mut T, pub extent: Bounds2i, + pub x_stride: i32, } +unsafe impl Send for Array2D {} +unsafe impl Sync for Array2D {} + impl Array2D { #[inline] - pub fn x_size(&self) -> usize { - (self.extent.p_max.x() - self.extent.p_min.x()) as usize + pub fn x_size(&self) -> u32 { + (self.extent.p_max.x() - self.extent.p_min.x()) as u32 } #[inline] - pub fn y_size(&self) -> usize { - (self.extent.p_max.y() - self.extent.p_min.y()) as usize + pub fn y_size(&self) -> u32 { + (self.extent.p_max.y() - self.extent.p_min.y()) as u32 } #[inline] - pub fn size(&self) -> usize { - self.extent.area() as usize + pub fn size(&self) -> u32 { + self.extent.area() as u32 + } + + #[inline(always)] + fn offset(&self, p: Point2i) -> isize { + let ox = p.x() - self.extent.p_min.x(); + let oy = p.y() - self.extent.p_min.y(); + (ox + oy * self.x_stride) as isize } #[inline] - pub fn index(&self, x: i32, y: i32) -> usize { - let nx = x - self.extent.p_min.x; - let ny = y - self.extent.p_min.y; - (nx + self.x_size() * ny) as usize + pub fn index(&self, x: i32, y: i32) -> u32 { + let nx = x - self.extent.p_min.x(); + let ny = y - self.extent.p_min.y(); + nx as u32 + self.x_size() * ny as u32 } - #[inline] - pub unsafe fn get(&self, x: i32, y: i32) -> &T { - unsafe { &*self.values.add(self.index(x, y)) } + #[inline(always)] + pub fn get(&self, p: Point2i) -> &T { + unsafe { &*self.values.offset(self.offset(p)) } } - #[inline] - pub unsafe fn get_mut(&mut self, x: i32, y: i32) -> &mut T { - unsafe { &mut *self.values.add(self.index(x, y)) } + #[inline(always)] + pub fn get_mut(&mut self, p: Point2i) -> &mut T { + unsafe { &mut *self.values.offset(self.offset(p)) } } #[inline] @@ -66,121 +78,50 @@ impl Array2D { #[inline] pub fn get_linear_mut(&mut self, index: usize) -> &mut T { - // SAFETY: Caller must ensure index < size() unsafe { &mut *self.values.add(index) } } pub fn as_slice(&self) -> &[T] { - unsafe { core::slice::from_raw_parts(self.values, self.size()) } + unsafe { core::slice::from_raw_parts(self.values, self.size() as usize) } } pub fn as_mut_slice(&mut self) -> &mut [T] { - unsafe { core::slice::from_raw_parts_mut(self.values, self.size()) } + unsafe { core::slice::from_raw_parts_mut(self.values, self.size() as usize) } } } -#[cfg(not(target_os = "cuda"))] -impl Clone for Array2D { - fn clone(&self) -> Self { - let n = self.area(); - let mut v = Vec::with_capacity(n); - unsafe { - for i in 0..n { - v.push((*self.values.add(i)).clone()); - } - } - let values = v.as_mut_ptr(); - std::mem::forget(v); - Self { - extent: self.extent, - values, - } - } -} - -#[cfg(target_os = "cuda")] -impl Clone for Array2D { - fn clone(&self) -> Self { - *self - } -} - -#[cfg(target_os = "cuda")] -impl Copy for Array2D {} - -#[cfg(not(target_os = "cuda"))] -impl Array2D { - pub fn new(extent: Bounds2i) -> Self { - let n = extent.area() as usize; - let mut v = vec![T::default(); n]; - let values = v.as_mut_ptr(); - std::mem::forget(v); - Self { extent, values } - } - - pub fn new_with_dims(nx: usize, ny: usize) -> Self { - let extent = Bounds2i::new(Point2i::new(0, 0), Point2i::new(nx, ny)); - let n = extent.area() as usize; - - let mut v = vec![T::default(); n]; - let values = v.as_mut_ptr(); - - std::mem::forget(v); - - Self { extent, values } - } - - pub fn new_from_bounds(extent: Bounds2i, default_val: T) -> Self { - let n = extent.area() as usize; - let mut v = vec![def; n]; - let values = v.as_mut_ptr(); - std::mem::forget(v); - Self { extent, values } - } - - pub fn new_filled(width: usize, height: usize, value: T) -> Self { - let extent = Bounds2i::from_points( - Point2i::new(0, 0), - Point2i::new(width as i32, height as i32), - ); - Self::new_from_bounds(extent, value) - } -} - -#[cfg(not(feature = "cuda"))] impl Index for Array2D { type Output = T; - - fn index(&self, mut p: Point2i) -> &Self::Output { - unsafe { self.get(pos.0, pos.1) } + #[inline(always)] + fn index(&self, p: Point2i) -> &Self::Output { + self.get(p) } } -#[cfg(not(feature = "cuda"))] impl IndexMut for Array2D { - fn index_mut(&mut self, mut p: Point2i) -> &mut Self::Output { - unsafe { self.get_mut(pos.0, pos.1) } + fn index_mut(&mut self, p: Point2i) -> &mut Self::Output { + self.get_mut(p) } } -#[cfg(not(feature = "cuda"))] impl Index<(i32, i32)> for Array2D { type Output = T; - fn index(&self, pos: (i32, i32)) -> &Self::Output { - unsafe { self.get(pos.0, pos.1) } + fn index(&self, (x, y): (i32, i32)) -> &Self::Output { + &self[Point2i::new(x, y)] } } -#[cfg(not(feature = "cuda"))] impl IndexMut<(i32, i32)> for Array2D { - fn index_mut(&mut self, pos: (i32, i32)) -> &mut Self::Output { - unsafe { self.get_mut(pos.0, pos.1) } + fn index_mut(&mut self, (x, y): (i32, i32)) -> &mut Self::Output { + &mut self[Point2i::new(x, y)] } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct SampledGrid { pub values: *const T, + pub values_len: u32, pub nx: i32, pub ny: i32, pub nz: i32, @@ -195,6 +136,7 @@ impl SampledGrid { assert_eq!(slice.len(), (nx * ny * nz) as usize); Self { values: slice.as_ptr(), + values_len: (nx * ny * nz) as u32, nx, ny, nz, @@ -204,6 +146,7 @@ impl SampledGrid { pub fn empty() -> Self { Self { values: core::ptr::null(), + values_len: 0, nx: 0, ny: 0, nz: 0, @@ -214,8 +157,8 @@ impl SampledGrid { !self.values.is_null() && self.nx > 0 && self.ny > 0 && self.nz > 0 } - pub fn bytes_allocated(&self) -> usize { - self.values.len() * std::mem::size_of::() + pub fn bytes_allocated(&self) -> u32 { + self.values_len * std::mem::size_of::() as u32 } pub fn x_size(&self) -> i32 { @@ -239,7 +182,7 @@ impl SampledGrid { return U::default(); } - let sample_bounds = Bounds3i::new( + let sample_bounds = Bounds3i::from_points( Point3i::new(0, 0, 0), Point3i::new(self.nx, self.ny, self.nz), ); diff --git a/shared/src/utils/error.rs b/shared/src/utils/error.rs index 2ec443a..e3ef18b 100644 --- a/shared/src/utils/error.rs +++ b/shared/src/utils/error.rs @@ -1,9 +1,5 @@ -use image_rs::{ImageError as IError, error}; use std::fmt; use std::sync::Arc; -use thiserror::Error; - -use crate::images::PixelFormat; #[derive(Error, Debug)] pub enum LlsError { diff --git a/shared/src/utils/math.rs b/shared/src/utils/math.rs index cca9f9e..ce013af 100644 --- a/shared/src/utils/math.rs +++ b/shared/src/utils/math.rs @@ -1,10 +1,11 @@ use super::error::{InversionError, LlsError}; -use crate::core::geometry::{Lerp, Point, Point2f, Point2i, Vector, Vector3f, VectorLike}; +use crate::core::color::{RGB, XYZ}; +use crate::core::geometry::{Lerp, MulAdd, Point, Point2f, Point2i, Vector, Vector3f, VectorLike}; use crate::core::pbrt::{Float, FloatBitOps, FloatBits, ONE_MINUS_EPSILON, PI, PI_OVER_4}; -use crate::spectra::color::{RGB, XYZ}; use crate::utils::hash::{hash_buffer, mix_bits}; use crate::utils::sobol::{SOBOL_MATRICES_32, VDC_SOBOL_MATRICES, VDC_SOBOL_MATRICES_INV}; +use half::f16; use num_traits::{Float as NumFloat, Num, One, Signed, Zero}; use std::error::Error; use std::fmt::{self, Display}; @@ -72,15 +73,15 @@ pub fn evaluate_polynomial(t: Float, coeffs: &[Float]) -> Float { result } -#[inline] -pub fn difference_of_products(a: T, b: T, c: T, d: T) -> T +pub fn difference_of_products(a: Float, b: T, c: Float, d: T) -> T where - T: Mul + Add + Neg + Copy, + T: Copy + Neg + Mul + Add, + T: MulAdd, { - let cd = c * d; - let difference_of_products = fma(a, b, -cd); - let error = fma(-c, d, cd); - difference_of_products + error + let cd = d * c; + let diff = b.mul_add(a, -cd); + let error = d.mul_add(-c, cd); + diff + error } #[inline] @@ -147,8 +148,7 @@ pub fn fast_exp(x: Float) -> Float { let fxp = xp.floor(); let f = xp - fxp; let i = fxp as i32; - let two_to_f = evaluate_polynomial(f, &[1., 0.695556856, 0.226173572, 0.0781455737]) - .expect("Could not evaluate polynomial"); + let two_to_f = evaluate_polynomial(f, &[1., 0.695556856, 0.226173572, 0.0781455737]); let exponent = exponent(two_to_f) + i; if exponent < -126 { return 0.; @@ -713,8 +713,8 @@ const PRIMES: [i32; PRIME_TABLE_SIZE] = [ ]; #[inline] -pub fn radical_inverse(base_index: usize, mut a: u64) -> Float { - let base = PRIMES[base_index] as u64; +pub fn radical_inverse(base_index: u32, mut a: u64) -> Float { + let base = PRIMES[base_index as usize] as u64; let limit = (u64::MAX / base).saturating_sub(base); @@ -748,16 +748,17 @@ pub fn inverse_radical_inverse(mut inverse: u64, base: u64, n_digits: u64) -> u6 } // Digit scrambling +#[repr(C)] #[derive(Default, Debug, Clone)] pub struct DigitPermutation { - base: usize, - n_digits: usize, + base: u32, + n_digits: u32, permutations: Vec, } impl DigitPermutation { - pub fn new(base: usize, seed: u64) -> Self { - let mut n_digits = 0; + pub fn new(base: u32, seed: u64) -> Self { + let mut n_digits: u32 = 0; let inv_base = 1. / base as Float; let mut inv_base_m = 1.; @@ -766,14 +767,14 @@ impl DigitPermutation { inv_base_m *= inv_base; } - let mut permutations = vec![0u16; n_digits * base]; + let mut permutations = vec![0u16; n_digits as usize * base as usize]; for digit_index in 0..n_digits { let hash_input = [base as u64, digit_index as u64, seed]; let dseed = hash_buffer(&hash_input, 0); for digit_value in 0..base { - let index = digit_index * base + digit_value; + let index = (digit_index * base + digit_value) as usize; permutations[index] = permutation_element(digit_value as u32, base as u32, dseed as u32) as u16; @@ -1032,7 +1033,7 @@ impl u32> Scrambler for F { const N_SOBOL_DIMENSIONS: usize = 1024; const SOBOL_MATRIX_SIZE: usize = 52; #[inline] -pub fn sobol_sample(mut a: u64, dimension: usize, randomizer: S) -> Float { +pub fn sobol_sample(mut a: u64, dimension: u32, randomizer: S) -> Float { debug_assert!( dimension < N_SOBOL_DIMENSIONS, "Sobol dimension out of bounds" @@ -1571,3 +1572,53 @@ mod tests { assert_eq!(m.determinant(), 1.0); } } + +#[inline(always)] +pub fn f16_to_f32(bits: u16) -> f32 { + #[cfg(target_os = "cuda")] + { + // Use hardware intrinsic on CUDA + // Cast bits to cuda_std::f16, then cast to f32 + let half_val = unsafe { core::mem::transmute::(bits) }; + half_val.to_f32() + } + + #[cfg(target_arch = "spirv")] + { + // Use shared logic or spirv-std intrinsics if available. + // Sadly, f16 support in rust-gpu is still maturing. + // A manual bit-conversion function is often safest here. + f16_to_f32_software(bits) + } + + #[cfg(not(any(target_os = "cuda", target_arch = "spirv")))] + { + f16::from_bits(bits).to_f32() + } +} + +fn f16_to_f32_software(h: u16) -> f32 { + let sign = ((h >> 15) & 1) as u32; + let exp = ((h >> 10) & 0x1F) as u32; + let mant = (h & 0x3FF) as u32; + + let out_bits = if exp == 0 { + if mant == 0 { + sign << 31 + } else { + let mut m = mant; + let mut e = 0; + while (m & 0x400) == 0 { + m <<= 1; + e += 1; + } + (sign << 31) | ((112 - e) << 23) | ((m & 0x3FF) << 13) + } + } else if exp == 0x1F { + (sign << 31) | (0xFF << 23) | (mant << 13) + } else { + (sign << 31) | ((exp + 112) << 23) | (mant << 13) + }; + + f32::from_bits(out_bits) +} diff --git a/shared/src/utils/mesh.rs b/shared/src/utils/mesh.rs index 122b126..da87a1e 100644 --- a/shared/src/utils/mesh.rs +++ b/shared/src/utils/mesh.rs @@ -1,162 +1,34 @@ +use crate::Float; use crate::core::geometry::{Normal3f, Point2f, Point3f, Vector3f}; -use crate::core::pbrt::Float; +use crate::utils::Transform; +use crate::utils::ptr::Ptr; use crate::utils::sampling::PiecewiseConstant2D; -use crate::utils::transform::TransformGeneric; -use std::sync::Arc; -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct TriangleMesh { - pub n_triangles: usize, - pub n_vertices: usize, - pub vertex_indices: Arc>, - pub p: Arc>, - pub n: Option>>, - pub s: Option>>, - pub uv: Option>>, - pub face_indices: Option>>, + pub n_triangles: u32, + pub n_vertices: u32, + pub vertex_indices: Ptr, + pub p: Ptr, + pub n: Ptr, + pub s: Ptr, + pub uv: Ptr, + pub face_indices: Ptr, pub reverse_orientation: bool, pub transform_swaps_handedness: bool, } -impl TriangleMesh { - #[allow(clippy::too_many_arguments)] - pub fn new( - render_from_object: &TransformGeneric, - reverse_orientation: bool, - indices: Vec, - mut p: Vec, - mut s: Vec, - mut n: Vec, - uv: Vec, - face_indices: Vec, - ) -> Self { - let n_triangles = indices.len() / 3; - let n_vertices = p.len(); - for pt in p.iter_mut() { - *pt = render_from_object.apply_to_point(*pt); - } - - let transform_swaps_handedness = render_from_object.swaps_handedness(); - - let uv = if !uv.is_empty() { - assert_eq!(n_vertices, uv.len()); - Some(uv) - } else { - None - }; - - let n = if !n.is_empty() { - assert_eq!(n_vertices, n.len()); - for nn in n.iter_mut() { - *nn = render_from_object.apply_to_normal(*nn); - if reverse_orientation { - *nn = -*nn; - } - } - Some(n) - } else { - None - }; - - let s = if !s.is_empty() { - assert_eq!(n_vertices, s.len()); - for ss in s.iter_mut() { - *ss = render_from_object.apply_to_vector(*ss); - } - Some(s) - } else { - None - }; - - let face_indices = if !face_indices.is_empty() { - assert_eq!(n_triangles, face_indices.len()); - Some(face_indices) - } else { - None - }; - - assert!(p.len() <= i32::MAX as usize); - assert!(indices.len() <= i32::MAX as usize); - - Self { - n_triangles, - n_vertices, - vertex_indices: Arc::new(indices), - p: Arc::new(p), - n: n.map(Arc::new), - s: s.map(Arc::new), - uv: uv.map(Arc::new), - face_indices: face_indices.map(Arc::new), - reverse_orientation, - transform_swaps_handedness, - } - } -} - -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct BilinearPatchMesh { pub reverse_orientation: bool, pub transform_swaps_handedness: bool, - pub n_patches: usize, - pub n_vertices: usize, - pub vertex_indices: Arc>, - pub p: Arc>, - pub n: Option>>, - pub uv: Option>>, - pub image_distribution: Option, -} - -impl BilinearPatchMesh { - pub fn new( - render_from_object: &TransformGeneric, - reverse_orientation: bool, - indices: Vec, - mut p: Vec, - mut n: Vec, - uv: Vec, - image_distribution: PiecewiseConstant2D, - ) -> Self { - let n_patches = indices.len() / 3; - let n_vertices = p.len(); - for pt in p.iter_mut() { - *pt = render_from_object.apply_to_point(*pt); - } - - let transform_swaps_handedness = render_from_object.swaps_handedness(); - - let uv = if !uv.is_empty() { - assert_eq!(n_vertices, uv.len()); - Some(uv) - } else { - None - }; - - let n = if !n.is_empty() { - assert_eq!(n_vertices, n.len()); - for nn in n.iter_mut() { - *nn = render_from_object.apply_to_normal(*nn); - if reverse_orientation { - *nn = -*nn; - } - } - Some(n) - } else { - None - }; - - assert!(p.len() <= i32::MAX as usize); - assert!(indices.len() <= i32::MAX as usize); - - Self { - n_patches, - n_vertices, - vertex_indices: Arc::new(indices), - p: Arc::new(p), - n: n.map(Arc::new), - uv: uv.map(Arc::new), - reverse_orientation, - transform_swaps_handedness, - image_distribution: Some(image_distribution), - } - } + pub n_patches: u32, + pub n_vertices: u32, + pub vertex_indices: Ptr, + pub p: Ptr, + pub n: Ptr, + pub uv: Ptr, + pub image_distribution: Ptr, } diff --git a/shared/src/utils/mod.rs b/shared/src/utils/mod.rs index 3609ec9..ac06f62 100644 --- a/shared/src/utils/mod.rs +++ b/shared/src/utils/mod.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicU64, Ordering}; +use core::sync::atomic::{AtomicU32, AtomicU64, Ordering}; pub mod containers; pub mod error; @@ -15,7 +15,7 @@ pub mod sobol; pub mod splines; pub mod transform; -pub use ptr::Ptr; +pub use ptr::{Ptr, RelPtr}; pub use transform::{AnimatedTransform, Transform, TransformGeneric}; #[inline] @@ -33,55 +33,77 @@ where i } -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] pub struct AtomicFloat { - value: f64, + bits: AtomicU32, } impl AtomicFloat { - pub fn new(value: f64) -> Self { - Self { value } + pub fn new(val: f32) -> Self { + Self { + bits: AtomicU32::new(val.to_bits()), + } } - pub fn load(&self) -> f64 { - #[cfg(not(target_os = "cuda"))] - { - use core::sync::atomic::{AtomicU64, Ordering}; - let ptr = &self.value as *const f64 as *const AtomicU64; - f64::from_bits(unsafe { (*ptr).load(Ordering::Relaxed) }) - } - - #[cfg(target_os = "cuda")] - self.value + pub fn get(&self) -> f32 { + f32::from_bits(self.bits.load(Ordering::Relaxed)) } - pub fn add(&self, v: f64) { - let ptr = &self.value as *const f64 as *mut f64; + pub fn set(&self, val: f32) { + self.bits.store(val.to_bits(), Ordering::Relaxed); + } - #[cfg(target_os = "cuda")] - unsafe { - cuda_std::intrinsics::atomic_add(ptr, v); - } + /// Atomically adds `val` to the current value. + /// Uses a Compare-And-Swap (CAS) loop. + pub fn add(&self, val: f32) { + let mut current_bits = self.bits.load(Ordering::Relaxed); + loop { + let current_val = f32::from_bits(current_bits); + let new_val = current_val + val; + let new_bits = new_val.to_bits(); - #[cfg(not(target_os = "cuda"))] - unsafe { - use core::sync::atomic::{AtomicU64, Ordering}; - let atomic_ptr = ptr as *const AtomicU64; - let atomic = &*atomic_ptr; - let mut current_bits = atomic.load(Ordering::Relaxed); - loop { - let current_val = f64::from_bits(current_bits); - let new_val = current_val + v; - match atomic.compare_exchange_weak( - current_bits, - new_val.to_bits(), - Ordering::Relaxed, - Ordering::Relaxed, - ) { - Ok(_) => break, - Err(x) => current_bits = x, - } + match self.bits.compare_exchange_weak( + current_bits, + new_bits, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => current_bits = x, + } + } + } +} + +pub struct AtomicDouble { + bits: AtomicU64, +} + +impl AtomicDouble { + pub fn new(val: f64) -> Self { + Self { + bits: AtomicU64::new(val.to_bits()), + } + } + + pub fn get(&self) -> f64 { + f64::from_bits(self.bits.load(Ordering::Relaxed)) + } + + pub fn add(&self, val: f64) { + let mut current_bits = self.bits.load(Ordering::Relaxed); + loop { + let current_val = f64::from_bits(current_bits); + let new_val = current_val + val; + let new_bits = new_val.to_bits(); + + match self.bits.compare_exchange_weak( + current_bits, + new_bits, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => current_bits = x, } } } diff --git a/shared/src/utils/noise.rs b/shared/src/utils/noise.rs index f5bfdf2..06dddbe 100644 --- a/shared/src/utils/noise.rs +++ b/shared/src/utils/noise.rs @@ -45,7 +45,7 @@ fn noise_weight(t: Float) -> Float { fn grad(x: i32, y: i32, z: i32, dx: Float, dy: Float, dz: Float) -> Float { let hash = NOISE_PERM[NOISE_PERM[NOISE_PERM[x as usize] as usize + y as usize] as usize + z as usize]; - let h = h & 15; + let h = hash & 15; let u = if h < 8 || h == 12 || h == 13 { dx } else { dy }; let v = if h < 4 || h == 12 || h == 13 { dy } else { dz }; @@ -64,10 +64,20 @@ fn grad(x: i32, y: i32, z: i32, dx: Float, dy: Float, dz: Float) -> Float { } #[inline(always)] -pub fn noise_from_point(mut p: Point3f) -> Float { +pub fn noise_from_point(p: Point3f) -> Float { noise(p.x(), p.y(), p.z()) } +#[inline(always)] +pub fn noise_2d(x: Float, y: Float) -> Float { + noise(x, y, 0.5) +} + +#[inline(always)] +pub fn noise_1d(x: Float) -> Float { + noise(x, 0.5, 0.5) +} + #[inline(always)] pub fn noise(mut x: Float, mut y: Float, mut z: Float) -> Float { let max_coord = (1i32 << 30) as Float; @@ -98,13 +108,13 @@ pub fn noise(mut x: Float, mut y: Float, mut z: Float) -> Float { // Fetch gradients let w000 = grad(ix, iy, iz, dx, dy, dz); - let w100 = grad(ix + 1, iy, iz, dx - 1, dy, dz); - let w010 = grad(ix, iy + 1, iz, dx, dy - 1, dz); - let w110 = grad(ix + 1, iy + 1, iz, dx - 1, dy - 1, dz); - let w001 = grad(ix, iy, iz + 1, dx, dy, dz - 1); - let w101 = grad(ix + 1, iy, iz + 1, dx - 1, dy, dz - 1); - let w011 = grad(ix, iy + 1, iz + 1, dx, dy - 1, dz - 1); - let w111 = grad(ix + 1, iy + 1, iz + 1, dx - 1, dy - 1, dz - 1); + let w100 = grad(ix + 1, iy, iz, dx - 1., dy, dz); + let w010 = grad(ix, iy + 1, iz, dx, dy - 1., dz); + let w110 = grad(ix + 1, iy + 1, iz, dx - 1., dy - 1., dz); + let w001 = grad(ix, iy, iz + 1, dx, dy, dz - 1.); + let w101 = grad(ix + 1, iy, iz + 1, dx - 1., dy, dz - 1.); + let w011 = grad(ix, iy + 1, iz + 1, dx, dy - 1., dz - 1.); + let w111 = grad(ix + 1, iy + 1, iz + 1, dx - 1., dy - 1., dz - 1.); let wx = noise_weight(dx); let wy = noise_weight(dy); @@ -121,23 +131,25 @@ pub fn noise(mut x: Float, mut y: Float, mut z: Float) -> Float { lerp(wz, y0, y1) } -pub fn fbm(p: Point3f, dpdx: Vector3f, dpdy: Vector3f, omega: Float, max_octaves: usize) -> Float { +pub fn fbm(p: Point3f, dpdx: Vector3f, dpdy: Vector3f, omega: Float, max_octaves: u32) -> Float { // Compute number of octaves for antialiased FBm let len2 = dpdx.norm_squared().max(dpdy.norm_squared()); - let n = clamp(-1. - len.log2() / 2., 0., max_octaves); - let n_int = n.floor(); + let n = clamp(-1. - len2.log2() / 2., 0., max_octaves as Float); + let n_int = n.floor() as usize; let mut sum = 0.; let mut lambda = 1.; - let mut o = 1; - for i in 0..n_int { - sum += o * Noise(lambda * p); + let mut o = 1.; + for _ in 0..n_int { + sum += o * noise_from_point(Point3f::from(lambda * Vector3f::from(p))); lambda *= 1.99; o *= omega; } - let n_partial = n - n_int; - sum += o * smooth_step(n_partial, 0.3, 0.7) * noise_from_point(lambda * p); + let n_partial = n - n_int as Float; + sum += o + * smooth_step(n_partial, 0.3, 0.7) + * noise_from_point(Point3f::from(lambda * Vector3f::from(p))); return sum; } @@ -147,30 +159,30 @@ pub fn turbulence( dpdx: Vector3f, dpdy: Vector3f, omega: Float, - max_octaves: usize, + max_octaves: u32, ) -> Float { // Compute number of octaves for antialiased FBm let len2 = dpdx.norm_squared().max(dpdy.norm_squared()); - let n = clamp(-1. - len2.log2() / 2., 0, maxOctaves); - let n_int = n.floor(); + let n = clamp(-1. - len2.log2() / 2., 0., max_octaves as Float); + let n_int = n.floor() as usize; // Compute sum of octaves of noise for turbulence let mut sum = 0.; let mut lambda = 1.; let mut o = 1.; - for i in 0..n_int { - sum += o * noise_from_point(lambda * p).abs(); + for _ in 0..n_int { + sum += o * noise_from_point(Point3f::from(lambda * Vector3f::from(p))); lambda *= 1.99; o *= omega; } - let n_partial = n - n_int; + let n_partial = n - n_int as Float; sum += o * lerp( smooth_step(n_partial, 0.3, 0.7), 0.2, - noise_from_point(lambda * p).abs(), + noise_from_point(Point3f::from(lambda * Vector3f::from(p))).abs(), ); - for i in n_int..max_octaves { + for _ in n_int..max_octaves as usize { sum += o * 0.2; o *= omega; } diff --git a/shared/src/utils/ptr.rs b/shared/src/utils/ptr.rs index b8a51ef..3271b6f 100644 --- a/shared/src/utils/ptr.rs +++ b/shared/src/utils/ptr.rs @@ -1,21 +1,22 @@ use core::marker::PhantomData; +use core::ops::Index; #[repr(C)] #[derive(Debug)] -pub struct Ptr { - offset: isize, +pub struct RelPtr { + offset: i32, _phantom: PhantomData, } -impl Clone for Ptr { +impl Clone for RelPtr { fn clone(&self) -> Self { *self } } -impl Copy for Ptr {} +impl Copy for RelPtr {} -impl Ptr { +impl RelPtr { pub fn null() -> Self { Self { offset: 0, @@ -23,8 +24,8 @@ impl Ptr { } } - pub fn is_null(&self) -> Self { - self.offset = 0; + pub fn is_null(&self) -> bool { + self.offset == 0 } pub fn get(&self) -> Option<&T> { @@ -33,17 +34,115 @@ impl Ptr { } else { unsafe { let base = self as *const _ as *const u8; - let target = base.offset(self.offset) as *const T; + let target = base.offset(self.offset as isize) as *const T; + target.as_ref() } } } #[cfg(not(target_os = "cuda"))] pub fn new(me: *const Self, target: *const T) -> Self { + if target.is_null() { + return Self::null(); + } + let diff = unsafe { (target as *const u8).offset_from(me as *const u8) }; + + if diff > i32::MAX as isize || diff < i32::MIN as isize { + panic!("RelPtr offset out of i32 range! Are objects too far apart?"); + } + Self { - offset: diff, + offset: diff as i32, _phantom: PhantomData, } } } + +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct Ptr(pub *const T); + +impl Default for Ptr { + fn default() -> Self { + Self(core::ptr::null()) + } +} + +impl Ptr { + pub fn null() -> Self { + Self::default() + } + + #[inline(always)] + pub fn is_null(&self) -> bool { + self.0.is_null() + } + + #[inline(always)] + pub fn as_ref(&self) -> Option<&T> { + unsafe { self.0.as_ref() } + } +} + +unsafe impl Send for Ptr {} +unsafe impl Sync for Ptr {} + +impl std::ops::Deref for Ptr { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { &*self.0 } + } +} + +impl From<&T> for Ptr { + fn from(r: &T) -> Self { + Self(r as *const T) + } +} + +impl From<&mut T> for Ptr { + fn from(r: &mut T) -> Self { + Self(r as *const T) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct Slice { + pub ptr: Ptr, + pub len: u32, + pub _marker: PhantomData, +} + +unsafe impl Send for Slice {} +unsafe impl Sync for Slice {} + +impl Slice { + pub fn new(ptr: Ptr, len: u32) -> Self { + Self { + ptr, + len, + _marker: PhantomData, + } + } + + pub fn as_ptr(&self) -> *const T { + self.ptr.0 + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +impl Index for Slice { + type Output = T; + + #[inline(always)] + fn index(&self, index: usize) -> &Self::Output { + unsafe { &*self.ptr.0.add(index) } + } +} diff --git a/shared/src/utils/rng.rs b/shared/src/utils/rng.rs index 4e5e558..d574336 100644 --- a/shared/src/utils/rng.rs +++ b/shared/src/utils/rng.rs @@ -1,7 +1,8 @@ -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct Rng { - state: u64, - inc: u64, + pub state: u64, + pub inc: u64, } impl Default for Rng { diff --git a/shared/src/utils/sampling.rs b/shared/src/utils/sampling.rs index 3141ff2..e9ba4ac 100644 --- a/shared/src/utils/sampling.rs +++ b/shared/src/utils/sampling.rs @@ -2,18 +2,18 @@ use crate::check_rare; use crate::core::geometry::{ Bounds2f, Frame, Point2f, Point2i, Point3f, Vector2f, Vector2i, Vector3f, VectorLike, }; -use crate::core::pbrt::{ - Float, INV_2_PI, INV_4_PI, INV_PI, ONE_MINUS_EPSILON, PI, PI_OVER_2, PI_OVER_4, find_interval, -}; use crate::core::pbrt::{RARE_EVENT_CONDITION_MET, RARE_EVENT_TOTAL_CALLS}; use crate::utils::containers::Array2D; use crate::utils::math::{ catmull_rom_weights, clamp, difference_of_products, evaluate_polynomial, lerp, logistic, newton_bisection, safe_sqrt, square, sum_of_products, }; +use crate::utils::ptr::Ptr; use crate::utils::rng::Rng; +use crate::{ + Float, INV_2_PI, INV_4_PI, INV_PI, ONE_MINUS_EPSILON, PI, PI_OVER_2, PI_OVER_4, find_interval, +}; use num_traits::Num; -use std::sync::atomic::{AtomicU64, Ordering as SyncOrdering}; pub fn linear_pdf(x: T, a: T, b: T) -> T where @@ -642,23 +642,14 @@ pub fn cosine_hemisphere_pdf(cos_theta: Float) -> Float { cos_theta * INV_PI } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] pub struct VarianceEstimator { mean: Float, s: Float, n: i64, } -impl Default for VarianceEstimator { - fn default() -> Self { - Self { - mean: 0.0, - s: 0.0, - n: 0, - } - } -} - impl VarianceEstimator { pub fn add(&mut self, x: Float) { self.n += 1; @@ -717,8 +708,8 @@ pub struct PiecewiseConstant1D { pub func_integral: Float, } -#[cfg(not(target_os = "cuda"))] impl PiecewiseConstant1D { + #[cfg(not(target_os = "cuda"))] pub fn new_with_bounds(f: &[Float], min: Float, max: Float) -> Self { let n = f.len(); let mut func_vec = f.to_vec(); @@ -728,10 +719,11 @@ impl PiecewiseConstant1D { for i in 1..=n { cdf_vec[i] = cdf_vec[i - 1] + func_vec[i - 1] / n as Float; } - let func_int = cdf_vec[n]; - if func_int > 0.0 { + + let func_integral = cdf_vec[n]; + if func_integral > 0.0 { for i in 1..=n { - cdf_vec[i] /= func_int; + cdf_vec[i] /= func_integral; } } else { for i in 1..=n { @@ -754,24 +746,11 @@ impl PiecewiseConstant1D { } } + #[cfg(not(target_os = "cuda"))] pub fn new(f: &[Float]) -> Self { Self::new_with_bounds(f, 0., 1.) } -} -#[cfg(not(target_os = "cuda"))] -impl Drop for PiecewiseConstant1D { - fn drop(&mut self) { - if !self.func.is_null() { - unsafe { - let _ = Vec::from_raw_parts(self.func, self.n, self.n); - let _ = Vec::from_raw_parts(self.cdf, self.n + 1, self.n + 1); - } - } - } -} - -impl PiecewiseConstant1D { pub fn integral(&self) -> Float { self.func_integral } @@ -801,16 +780,16 @@ impl PiecewiseConstant1D { #[derive(Debug, Copy, Clone)] pub struct PiecewiseConstant2D { pub domain: Bounds2f, - pub p_conditional_v: *mut PiecewiseConstant1D, + pub p_conditional_v: Ptr, pub p_marginal: PiecewiseConstant1D, pub n_conditionals: usize, } -#[cfg(not(target_os = "cuda"))] impl PiecewiseConstant2D { - pub fn new(data: &Array2D, nu: usize, nv: usize, domain: Bounds2f) -> Self { - let nu = data.x_size() as usize; - let nv = data.y_size() as usize; + #[cfg(not(target_os = "cuda"))] + pub fn new(data: &Array2D, x_size: u32, y_size: u32, domain: Bounds2f) -> Self { + let nu = x_size as usize; + let nv = y_size as usize; let mut conditionals = Vec::with_capacity(nv); for v in 0..nv { let row = unsafe { core::slice::from_raw_parts(data.values.add(v * nu), nu) }; @@ -839,10 +818,12 @@ impl PiecewiseConstant2D { } } + #[cfg(not(target_os = "cuda"))] pub fn new_with_bounds(data: &Array2D, domain: Bounds2f) -> Self { Self::new(data, data.x_size(), data.y_size(), domain) } + #[cfg(not(target_os = "cuda"))] pub fn new_with_data(data: &Array2D) -> Self { let nx = data.x_size(); let ny = data.y_size(); @@ -850,24 +831,7 @@ impl PiecewiseConstant2D { Self::new(data, nx, ny, domain) } -} -#[cfg(not(target_os = "cuda"))] -impl Drop for PiecewiseConstant2D { - fn drop(&mut self) { - if !self.p_conditional_v.is_null() { - unsafe { - let _ = Vec::from_raw_parts( - self.p_conditional_v, - self.n_conditionals, - self.n_conditionals, - ); - } - } - } -} - -impl PiecewiseConstant2D { pub fn resolution(&self) -> Point2i { Point2i::new( self.p_conditional_v[0].size() as i32, @@ -904,9 +868,10 @@ impl PiecewiseConstant2D { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct SummedAreaTable { - sum: Array2D, + pub sum: Array2D, } impl SummedAreaTable { @@ -983,7 +948,8 @@ impl SummedAreaTable { } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct WindowedPiecewiseConstant2D { sat: SummedAreaTable, func: Array2D, @@ -1095,96 +1061,54 @@ impl WindowedPiecewiseConstant2D { } } +#[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Bin { - q: Float, - p: Float, - alias: usize, + pub q: Float, + pub p: Float, + pub alias: u32, } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Copy, Debug, Clone)] pub struct AliasTable { - bins: Vec, + pub bins: *const Bin, + pub size: u32, } +unsafe impl Send for AliasTable {} +unsafe impl Sync for AliasTable {} + impl AliasTable { - pub fn new(weights: &[Float]) -> Self { - let n = weights.len(); - if n == 0 { - return Self { bins: Vec::new() }; - } - let sum: f64 = weights.iter().map(|&w| w as f64).sum(); - assert!(sum > 0.0, "Sum of weights must be positive"); - let mut bins = Vec::with_capacity(n); - for &w in weights { - bins.push(Bin { - p: (w as f64 / sum) as Float, - q: 0.0, - alias: 0, - }); - } - - struct Outcome { - p_hat: f64, - index: usize, - } - - let mut under = Vec::with_capacity(n); - let mut over = Vec::with_capacity(n); - - for (i, bin) in bins.iter().enumerate() { - let p_hat = (bin.p as f64) * (n as f64); - if p_hat < 1.0 { - under.push(Outcome { p_hat, index: i }); - } else { - over.push(Outcome { p_hat, index: i }); - } - } - - while !under.is_empty() && !over.is_empty() { - let un = under.pop().unwrap(); - let ov = over.pop().unwrap(); - - bins[un.index].q = un.p_hat as Float; - bins[un.index].alias = ov.index; - - let p_excess = un.p_hat + ov.p_hat - 1.0; - - if p_excess < 1.0 { - under.push(Outcome { - p_hat: p_excess, - index: ov.index, - }); - } else { - over.push(Outcome { - p_hat: p_excess, - index: ov.index, - }); - } - } - - while let Some(ov) = over.pop() { - bins[ov.index].q = 1.0; - bins[ov.index].alias = ov.index; - } - - while let Some(un) = under.pop() { - bins[un.index].q = 1.0; - bins[un.index].alias = un.index; - } - - Self { bins } + #[inline(always)] + fn bin(&self, idx: u32) -> &Bin { + unsafe { &*self.bins.add(idx as usize) } } - pub fn sample(&self, u: Float) -> (usize, Float, Float) { - let n = self.bins.len(); + pub fn size(&self) -> u32 { + self.size as u32 + } - let val = u * (n as Float); - let offset = std::cmp::min(val as usize, n - 1); + pub fn pmf(&self, index: u32) -> Float { + if index >= self.size() { + return 0.0; + } + self.bin(index).p + } + + pub fn sample(&self, u: Float) -> (u32, Float, Float) { + if self.size == 0 { + return (0, 0.0, 0.0); + } + + let n = self.size as Float; + + let val = u * n; + let offset = (val.min(n - 1.0)) as u32; let up = (val - (offset as Float)).min(ONE_MINUS_EPSILON); - let bin = &self.bins[offset]; + let bin = self.bin(offset); if up < bin.q { debug_assert!(bin.p > 0.0); @@ -1197,7 +1121,7 @@ impl AliasTable { } else { let alias_idx = bin.alias; - let alias_p = self.bins[alias_idx].p; + let alias_p = self.bin(alias_idx).p; debug_assert!(alias_p > 0.0); let u_remapped = ((up - bin.q) / (1.0 - bin.q)).min(ONE_MINUS_EPSILON); @@ -1205,140 +1129,22 @@ impl AliasTable { (alias_idx, alias_p, u_remapped) } } - - pub fn size(&self) -> usize { - self.bins.len() - } - - pub fn pmf(&self, index: usize) -> Float { - self.bins[index].p - } } -#[derive(Debug, Clone)] +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct PiecewiseLinear2D { - size: Vector2i, - inv_patch_size: Vector2f, - param_size: [u32; N], - param_strides: [u32; N], - param_values: [Vec; N], - data: Vec, - marginal_cdf: Vec, - conditional_cdf: Vec, + pub size: Vector2i, + pub inv_patch_size: Vector2f, + pub param_size: [u32; N], + pub param_strides: [u32; N], + pub param_values: [Ptr; N], + pub data: Ptr, + pub marginal_cdf: Ptr, + pub conditional_cdf: Ptr, } impl PiecewiseLinear2D { - pub fn new( - data: &[Float], - x_size: i32, - y_size: i32, - param_res: [i32; N], - param_values: [&[Float]; N], - normalize: bool, - build_cdf: bool, - ) -> Self { - if build_cdf && !normalize { - panic!("PiecewiseLinear2D::new: build_cdf implies normalize=true"); - } - - let size = Vector2i::new(x_size, y_size); - let inv_patch_size = Vector2f::new(1. / (x_size - 1) as Float, 1. / (y_size - 1) as Float); - - let mut param_size = [0u32; N]; - let mut param_strides = [0u32; N]; - let param_values = std::array::from_fn(|i| param_values[i].to_vec()); - - let mut slices: u32 = 1; - for i in (0..N).rev() { - if param_res[i] < 1 { - panic!("PiecewiseLinear2D::new: parameter resolution must be >= 1!"); - } - param_size[i] = param_res[i] as u32; - param_strides[i] = if param_res[i] > 1 { slices } else { 0 }; - slices *= param_size[i]; - } - - let n_values = (x_size * y_size) as usize; - let mut new_data = vec![0.0; slices as usize * n_values]; - let mut marginal_cdf = if build_cdf { - vec![0.0; slices as usize * y_size as usize] - } else { - Vec::new() - }; - let mut conditional_cdf = if build_cdf { - vec![0.0; slices as usize * n_values] - } else { - Vec::new() - }; - - let mut data_offset = 0; - for slice in 0..slices as usize { - let slice_offset = slice * n_values; - let current_data = &data[data_offset..data_offset + n_values]; - let mut sum = 0.; - - // Construct conditional CDF - if normalize { - for y in 0..(y_size - 1) { - for x in 0..(x_size - 1) { - let i = (y * x_size + x) as usize; - let v00 = current_data[i] as f64; - let v10 = current_data[i + 1] as f64; - let v01 = current_data[i + x_size as usize] as f64; - let v11 = current_data[i + 1 + x_size as usize] as f64; - sum += 0.25 * (v00 + v10 + v01 + v11); - } - } - } - - let normalization = if normalize && sum > 0.0 { - 1.0 / sum as Float - } else { - 1.0 - }; - for k in 0..n_values { - new_data[slice_offset + k] = current_data[k] * normalization; - } - - if build_cdf { - let marginal_slice_offset = slice * y_size as usize; - // Construct marginal CDF - for y in 0..y_size as usize { - let mut cdf_sum = 0.0; - let i_base = y * x_size as usize; - conditional_cdf[slice_offset + i_base] = 0.0; - for x in 0..(x_size - 1) as usize { - let i = i_base + x; - cdf_sum += - 0.5 * (new_data[slice_offset + i] + new_data[slice_offset + i + 1]); - conditional_cdf[slice_offset + i + 1] = cdf_sum; - } - } - // Construct marginal CDF - marginal_cdf[marginal_slice_offset] = 0.0; - let mut marginal_sum = 0.0; - for y in 0..(y_size - 1) as usize { - let cdf1 = conditional_cdf[slice_offset + (y + 1) * x_size as usize - 1]; - let cdf2 = conditional_cdf[slice_offset + (y + 2) * x_size as usize - 1]; - marginal_sum += 0.5 * (cdf1 + cdf2); - marginal_cdf[marginal_slice_offset + y + 1] = marginal_sum; - } - } - data_offset += n_values; - } - - Self { - size, - inv_patch_size, - param_size, - param_strides, - param_values, - data: new_data, - marginal_cdf, - conditional_cdf, - } - } - pub fn sample(&self, mut sample: Point2f, params: [Float; N]) -> PLSample { sample = Point2f::new( sample.x().clamp(0.0, ONE_MINUS_EPSILON), @@ -1353,7 +1159,7 @@ impl PiecewiseLinear2D { let conditional_offset = slice_offset * conditional_size; let fetch_marginal = |idx: u32| { self.lookup( - &self.marginal_cdf, + self.marginal_cdf, marginal_offset + idx, marginal_size, ¶m_weights, @@ -1363,13 +1169,13 @@ impl PiecewiseLinear2D { let marginal_cdf_row = fetch_marginal(row); sample[1] -= marginal_cdf_row; let r0 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_offset + (row + 1) * self.size.x() as u32 - 1, conditional_size, ¶m_weights, ); let r1 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_offset + (row + 2) * self.size.x() as u32 - 1, conditional_size, ¶m_weights, @@ -1386,13 +1192,13 @@ impl PiecewiseLinear2D { let conditional_row_offset = conditional_offset + row * self.size.x() as u32; let fetch_conditional = |idx: u32| { let v0 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_row_offset + idx, conditional_size, ¶m_weights, ); let v1 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_row_offset + idx + self.size.x() as u32, conditional_size, ¶m_weights, @@ -1404,16 +1210,16 @@ impl PiecewiseLinear2D { }); sample[0] -= fetch_conditional(col); let offset = conditional_row_offset + col; - let v00 = self.lookup(&self.data, offset, slice_size, ¶m_weights); - let v10 = self.lookup(&self.data, offset + 1, slice_size, ¶m_weights); + let v00 = self.lookup(self.data, offset, slice_size, ¶m_weights); + let v10 = self.lookup(self.data, offset + 1, slice_size, ¶m_weights); let v01 = self.lookup( - &self.data, + self.data, offset + self.size.x() as u32, slice_size, ¶m_weights, ); let v11 = self.lookup( - &self.data, + self.data, offset + self.size.x() as u32 + 1, slice_size, ¶m_weights, @@ -1450,16 +1256,16 @@ impl PiecewiseLinear2D { let frac = Point2f::new(p.x() - col as Float, p.y() - row as Float); let slice_size = (self.size.x() * self.size.y()) as u32; let offset = slice_offset * slice_size + (row * self.size.x() + col) as u32; - let v00 = self.lookup(&self.data, offset, slice_size, ¶m_weights); - let v10 = self.lookup(&self.data, offset + 1, slice_size, ¶m_weights); + let v00 = self.lookup(self.data, offset, slice_size, ¶m_weights); + let v10 = self.lookup(self.data, offset + 1, slice_size, ¶m_weights); let v01 = self.lookup( - &self.data, + self.data, offset + self.size.x() as u32, slice_size, ¶m_weights, ); let v11 = self.lookup( - &self.data, + self.data, offset + self.size.x() as u32 + 1, slice_size, ¶m_weights, @@ -1473,26 +1279,26 @@ impl PiecewiseLinear2D { u[0] = w1.x() * (c0 + 0.5 * w1.x() * (c1 - c0)); let conditional_row_offset = slice_offset * slice_size + (row * self.size.x()) as u32; let v0 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_row_offset + col as u32, slice_size, ¶m_weights, ); let v1 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_row_offset + col as u32 + self.size.x() as u32, slice_size, ¶m_weights, ); u[0] += (1.0 - u.y()) * v0 + u.y() * v1; let r0 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_row_offset + self.size.x() as u32 - 1, slice_size, ¶m_weights, ); let r1 = self.lookup( - &self.conditional_cdf, + self.conditional_cdf, conditional_row_offset + self.size.x() as u32 * 2 - 1, slice_size, ¶m_weights, @@ -1501,7 +1307,7 @@ impl PiecewiseLinear2D { u[1] = w1.y() * (r0 + 0.5 * w1.y() * (r1 - r0)); let marginal_offset = slice_offset * self.size.y() as u32 + row as u32; u[1] += self.lookup( - &self.marginal_cdf, + self.marginal_cdf, marginal_offset, self.size.y() as u32, ¶m_weights, @@ -1525,16 +1331,16 @@ impl PiecewiseLinear2D { let w0 = Point2f::new(1.0 - w1.x(), 1.0 - w1.y()); let slice_size = (self.size.x() * self.size.y()) as u32; let offset = slice_offset * slice_size + (row * self.size.x() + col) as u32; - let v00 = self.lookup(&self.data, offset, slice_size, ¶m_weights); - let v10 = self.lookup(&self.data, offset + 1, slice_size, ¶m_weights); + let v00 = self.lookup(self.data, offset, slice_size, ¶m_weights); + let v10 = self.lookup(self.data, offset + 1, slice_size, ¶m_weights); let v01 = self.lookup( - &self.data, + self.data, offset + self.size.x() as u32, slice_size, ¶m_weights, ); let v11 = self.lookup( - &self.data, + self.data, offset + self.size.x() as u32 + 1, slice_size, ¶m_weights, @@ -1544,29 +1350,41 @@ impl PiecewiseLinear2D { pdf * self.inv_patch_size.x() * self.inv_patch_size.y() } + #[inline(always)] + fn get_param_value(&self, dim: usize, idx: usize) -> Float { + // Safety: Bounds checking against param_size ensures this is valid + unsafe { *self.param_values[dim].0.add(idx) } + } + fn get_slice_info(&self, params: [Float; N]) -> (u32, [(Float, Float); N]) { let mut param_weight = [(0.0, 0.0); N]; let mut slice_offset = 0u32; for dim in 0..N { - if self.param_size[dim] == 1 { - param_weight[2 * dim] = (1.0, 0.0); + let size = self.param_size[dim]; + + if size == 1 { + param_weight[dim] = (1.0, 0.0); continue; } - let param_index = find_interval(self.param_size[dim], |idx| { - self.param_values[dim][idx as usize] <= params[dim] - }); + let param_index = find_interval(size as usize, |idx| { + self.get_param_value(dim, idx) <= params[dim] + }) as u32; + + let p0 = self.get_param_value(dim, param_index as usize); + + let next_index = (param_index + 1).min(size - 1); + let p1 = self.get_param_value(dim, next_index as usize); - let p0 = self.param_values[dim][param_index as usize]; - let p1 = self.param_values[dim] - [(param_index as usize + 1).min(self.param_values[dim].len() - 1)]; let w1 = if p1 != p0 { ((params[dim] - p0) / (p1 - p0)).clamp(0.0, 1.0) } else { 0.0 }; - param_weight[dim] = (1. - w1, w1); + + param_weight[dim] = (1.0 - w1, w1); + slice_offset += self.param_strides[dim] * param_index; } @@ -1575,7 +1393,7 @@ impl PiecewiseLinear2D { fn lookup( &self, - data: &[Float], + data: Ptr, i0: u32, size: u32, param_weight: &[(Float, Float); N], @@ -1596,32 +1414,35 @@ impl PiecewiseLinear2D { current_mask >>= 1; } - result += weight * data[(i0 + offset) as usize]; + let idx = (i0 + offset) as usize; + let val = unsafe { *data.0.add(idx) }; + result += weight * val; } result } } -#[derive(Clone, Debug)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] pub struct WeightedReservoirSampler { rng: Rng, weight_sum: Float, reservoir_weight: Float, - reservoir: Option, + reservoir: T, } -impl Default for WeightedReservoirSampler { +impl Default for WeightedReservoirSampler { fn default() -> Self { Self { rng: Rng::default(), weight_sum: 0.0, reservoir_weight: 0.0, - reservoir: None, + reservoir: T::default(), } } } -impl WeightedReservoirSampler { +impl WeightedReservoirSampler { pub fn new(seed: u64) -> Self { let mut rng = Rng::default(); rng.set_sequence(seed); @@ -1629,7 +1450,7 @@ impl WeightedReservoirSampler { rng, weight_sum: 0.0, reservoir_weight: 0.0, - reservoir: None, + reservoir: T::default(), } } @@ -1642,7 +1463,7 @@ impl WeightedReservoirSampler { let p = weight / self.weight_sum; if self.rng.uniform::() < p { - self.reservoir = Some(sample); + self.reservoir = sample; self.reservoir_weight = weight; return true; } @@ -1659,7 +1480,7 @@ impl WeightedReservoirSampler { let p = weight / self.weight_sum; if self.rng.uniform::() < p { - self.reservoir = Some(func()); + self.reservoir = func(); self.reservoir_weight = weight; return true; } @@ -1682,7 +1503,11 @@ impl WeightedReservoirSampler { } pub fn get_sample(&self) -> Option<&T> { - self.reservoir.as_ref() + if self.has_sample() { + Some(&self.reservoir) + } else { + None + } } pub fn sample_probability(&self) -> Float { @@ -1700,19 +1525,12 @@ impl WeightedReservoirSampler { pub fn reset(&mut self) { self.reservoir_weight = 0.0; self.weight_sum = 0.0; - self.reservoir = None; + self.reservoir = T::default(); } - pub fn merge(&mut self, other: &WeightedReservoirSampler) - where - T: Clone, - { - // debug_assert!(self.weight_sum + other.weight_sum < 1e80); - - if let Some(other_sample) = &other.reservoir - && self.add(other_sample.clone(), other.weight_sum) - { - self.reservoir_weight = other.reservoir_weight; + pub fn merge(&mut self, other: &Self) { + if other.has_sample() { + self.add(other.reservoir, other.weight_sum); } } } diff --git a/shared/src/utils/splines.rs b/shared/src/utils/splines.rs index 2a25218..a7b9b9b 100644 --- a/shared/src/utils/splines.rs +++ b/shared/src/utils/splines.rs @@ -59,6 +59,46 @@ pub fn subdivide_cubic_bezier(cp: &[Point3f]) -> [Point3f; 7] { ] } +pub fn elevate_quadratic_bezier_to_cubic(cp: &[Point3f]) -> [Point3f; 4] { + [ + cp[0], + lerp(2. / 3., cp[0], cp[1]), + lerp(1. / 3., cp[1], cp[2]), + cp[2], + ] +} + +pub fn cubic_bspline_to_bezier(cp: &[Point3f]) -> [Point3f; 4] { + // Blossom from p012, p123, p234, and p345 to the Bezier control points + // p222, p223, p233, and p333. + // https://people.eecs.berkeley.edu/~sequin/CS284/IMGS/cubicbsplinepoints.gif + let p012 = cp[0]; + let p123 = cp[1]; + let p234 = cp[2]; + let p345 = cp[3]; + + let p122 = lerp(2. / 3., p012, p123); + let p223 = lerp(1. / 3., p123, p234); + let p233 = lerp(2. / 3., p123, p234); + let p334 = lerp(1. / 3., p234, p345); + + let p222 = lerp(0.5, p122, p223); + let p333 = lerp(0.5, p233, p334); + + [p222, p223, p233, p333] +} + +pub fn quadratic_bspline_to_bezier(cp: &[Point3f]) -> [Point3f; 3] { + // We can compute equivalent Bezier control points via some blossoming. + // We have three control points and a uniform knot vector; we will label + // the points p01, p12, and p23. We want the Bezier control points of + // the equivalent curve, which are p11, p12, and p22. We already have + // p12. + let p11 = lerp(0.5, cp[0], cp[1]); + let p22 = lerp(0.5, cp[1], cp[2]); + [p11, cp[1], p22] +} + pub fn evaluate_cubic_bezier(cp: &[Point3f], u: Float) -> (Point3f, Vector3f) { let cp1 = [ lerp(u, cp[0], cp[1]), diff --git a/shared/src/utils/transform.rs b/shared/src/utils/transform.rs index 80bdfdf..e3479b0 100644 --- a/shared/src/utils/transform.rs +++ b/shared/src/utils/transform.rs @@ -1,6 +1,5 @@ use num_traits::Float as NumFloat; use std::error::Error; -use std::fmt::{self, Display}; use std::iter::{Product, Sum}; use std::ops::{Add, Div, Index, IndexMut, Mul}; use std::sync::Arc; @@ -13,7 +12,7 @@ use crate::core::geometry::{ VectorLike, }; use crate::core::interaction::{ - Interaction, InteractionData, InteractionTrait, MediumInteraction, SurfaceInteraction, + Interaction, InteractionBase, InteractionTrait, MediumInteraction, SurfaceInteraction, }; use crate::core::pbrt::{Float, gamma}; use crate::utils::error::InversionError; diff --git a/shared/src/core/aggregates.rs b/src/core/aggregates.rs similarity index 99% rename from shared/src/core/aggregates.rs rename to src/core/aggregates.rs index 0f8642f..4178ba5 100644 --- a/shared/src/core/aggregates.rs +++ b/src/core/aggregates.rs @@ -1,7 +1,7 @@ use crate::core::geometry::{Bounds3f, Point3f, Ray, Vector3f}; use crate::core::pbrt::{Float, find_interval}; use crate::core::primitive::PrimitiveTrait; -use crate::shapes::ShapeIntersection; +use crate::core::shape::ShapeIntersection; use crate::utils::math::encode_morton_3; use crate::utils::math::next_float_down; use crate::utils::partition_slice; diff --git a/src/core/bssrdf.rs b/src/core/bssrdf.rs new file mode 100644 index 0000000..1e56225 --- /dev/null +++ b/src/core/bssrdf.rs @@ -0,0 +1,34 @@ +pub struct BSSRDFTableData { + pub rho_samples: Vec, + pub radius_samples: Vec, + pub profile: Vec, + pub rho_eff: Vec, + pub profile_cdf: Vec, +} + +impl BSSRDFTableData { + pub fn new(n_rho_samples: usize, n_radius_samples: usize) -> Self { + let rho_samples: Vec = Vec::with_capacity(n_rho_samples); + let radius_samples: Vec = Vec::with_capacity(n_radius_samples); + let profile: Vec = Vec::with_capacity(n_radius_samples * n_rho_samples); + let rho_eff: Vec = Vec::with_capacity(n_rho_samples); + let profile_cdf: Vec = Vec::with_capacity(n_radius_samples * n_rho_samples); + Self { + rho_samples, + radius_samples, + profile, + rho_eff, + profile_cdf, + } + } + + pub fn view(&self, rho_ptr: *const f32, radius_ptr: *const f32) -> BSSRDFTableView { + BSSRDFTable { + rho_samples: rho_ptr, + n_rho: self.rho_samples.len() as u32, + radius_samples: radius_ptr, + n_radius: self.radius_samples.len() as u32, + // ... + } + } +} diff --git a/src/core/camera.rs b/src/core/camera.rs index e48b660..23b196f 100644 --- a/src/core/camera.rs +++ b/src/core/camera.rs @@ -1,7 +1,8 @@ +use crate::core::image::ImageMetadata; use crate::utils::{FileLoc, ParameterDictionary}; use shared::Float; use shared::cameras::*; -use shared::core::camera::{Camera, CameraBase, CameraTransform}; +use shared::core::camera::{Camera, CameraBase, CameraTrait, CameraTransform}; use shared::core::film::Film; use shared::core::medium::Medium; use shared::core::options::get_options; @@ -62,6 +63,23 @@ pub trait CameraBaseFactory { impl CameraBaseFactory for CameraBase {} +pub trait InitMetadata { + fn init_metadata(&self, metadata: &mut ImageMetadata); +} + +impl InitMetadata for CameraBase { + fn init_metadata(&self, metadata: &mut ImageMetadata) { + let camera_from_world = self.camera_transform.camera_from_world(self.shutter_open); + metadata.camera_from_world = Some(camera_from_world.get_matrix()); + } +} + +impl InitMetadata for Camera { + fn init_metadata(&self, metadata: &mut ImageMetadata) { + self.base().init_metadata(metadata); + } +} + pub trait CameraFactory { fn create( name: &str, diff --git a/src/core/color.rs b/src/core/color.rs index 908c0cb..8ac3bda 100644 --- a/src/core/color.rs +++ b/src/core/color.rs @@ -32,16 +32,14 @@ impl RGBToSpectrumTableData { view, } } + + pub fn load(base_dir: &Path, name: &str) -> io::Result { + let z_path = base_dir.join(format!("{}_znodes.dat", name)); + let c_path = base_dir.join(format!("{}_coeffs.dat", name)); + + let z_nodes = read_float_file(&z_path)?; + let coeffs = read_float_file(&c_path)?; + + Ok(Self::new(z_nodes, coeffs)) + } } - -const LMS_FROM_XYZ: SquareMatrix3f = SquareMatrix::new([ - [0.8951, 0.2664, -0.1614], - [-0.7502, 1.7135, 0.0367], - [0.0389, -0.0685, 1.0296], -]); - -const XYZ_FROM_LMS: SquareMatrix3f = SquareMatrix::new([ - [0.986993, -0.147054, 0.159963], - [0.432305, 0.51836, 0.0492912], - [-0.00852866, 0.0400428, 0.968487], -]); diff --git a/src/core/film.rs b/src/core/film.rs index 746b591..c923473 100644 --- a/src/core/film.rs +++ b/src/core/film.rs @@ -75,6 +75,95 @@ pub trait PixelSensorHost { } } +struct SpectralFilmStorage { + pixels: Array2D, + bucket_sums: Vec, + weight_sums: Vec, + bucket_splats: Vec, +} + +pub struct SpectralFilmHost { + pub view: SpectralFilm, + _storage: Box, +} + +impl SpectralFilmHost { + pub fn new( + base: &FilmBase, + lambda_min: Float, + lambda_max: Float, + n_buckets: usize, + colorspace: &RGBColorSpace, + max_component_value: Float, + write_fp16: bool, + ) -> Self { + let n_pixels = base.pixel_bounds.area() as usize; + let total_buckets = n_pixels * n_buckets; + + let bucket_sums = vec![0.0; total_buckets]; + let weight_sums = vec![0.0; total_buckets]; + + let mut bucket_splats = Vec::with_capacity(total_buckets); + for _ in 0..total_buckets { + bucket_splats.push(AtomicFloat::new(0.0)); + } + + let mut pixels = Array2D::::new(base.pixel_bounds); + + let p_sums_base = bucket_sums.as_ptr() as *mut f64; + let p_weights_base = weight_sums.as_ptr() as *mut f64; + let p_splats_base = bucket_splats.as_ptr() as *mut AtomicFloat; + + for i in 0..n_pixels { + let pixel = pixels.get_linear_mut(i); + + pixel.bucket_offset = i * n_buckets; + + unsafe { + let offset = i * n_buckets; + + pixel.bucket_sums = p_sums_base.add(offset); + pixel.weight_sums = p_weights_base.add(offset); + pixel.bucket_splats = p_splats_base.add(offset); + } + } + + let storage = Box::new(SpectralFilmStorage { + pixels, + bucket_sums, + weight_sums, + bucket_splats, + }); + + let view = SpectralFilm { + base: base.clone(), + colorspace: colorspace.clone(), + lambda_min, + lambda_max, + n_buckets: n_buckets as u32, + max_component_value, + write_fp16, + filter_integral: base.filter.integral(), + output_rgbf_from_sensor_rgb: SquareMatrix::identity(), // Logic omitted + + pixels: Array2DView { + data: storage.pixels.as_mut_ptr(), + extent: base.pixel_bounds, + x_stride: base.pixel_bounds.max.x - base.pixel_bounds.min.x, + }, + + bucket_sums: storage.bucket_sums.as_ptr() as *mut f64, + weight_sums: storage.weight_sums.as_ptr() as *mut f64, + bucket_splats: storage.bucket_splats.as_ptr() as *mut AtomicFloat, + }; + + Self { + view, + _storage: storage, + } + } +} + pub trait FilmBaseHost { fn create( params: &ParameterDictionary, diff --git a/src/core/image/io.rs b/src/core/image/io.rs new file mode 100644 index 0000000..30cde8b --- /dev/null +++ b/src/core/image/io.rs @@ -0,0 +1,368 @@ +use super::ImageBuffer; +use crate::utils::error::ImageError; +use anyhow::{Context, Result, bail}; +use exr::prelude::{read_first_rgba_layer_from_file, write_rgba_file}; +use image_rs::ImageReader; +use image_rs::{DynamicImage, ImageBuffer, Rgb, Rgba}; +use shared::Float; +use shared::core::color::{ColorEncoding, LINEAR, SRGB}; +use shared::core::image::Image; +use std::fs::File; +use std::io::{BufRead, BufReader, BufWriter, Read, Write}; +use std::path::Path; + +pub trait ImageIO { + fn read(path: &Path, encoding: Option) -> Result; + fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<()>; + fn write_png(&self, path: &Path) -> Result<()>; + fn write_exr(&self, path: &Path, metadata: &ImageMetadata) -> Result<()>; + fn write_qoi(&self, path: &Path) -> Result<()>; + fn write_pfm(&self, path: &Path) -> Result<()>; + fn to_u8_buffer(&self) -> Vec; +} + +impl ImageIO for ImageBuffer { + fn read(path: &Path, encoding: Option) -> Result { + let ext = path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_lowercase(); + + match ext.as_str() { + "exr" => read_exr(path), + "pfm" => read_pfm(path), + _ => read_generic(path, encoding), + } + } + + fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + let path = Path::new(filename); + let ext = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + let res = match ext.to_lowercase().as_str() { + "exr" => self.write_exr(path, metadata), + "png" => self.write_png(path), + "pfm" => self.write_pfm(path), + "qoi" => self.write_qoi(path), + _ => Err(anyhow::anyhow!("Unsupported write format: {}", ext)), + }; + res.map_err(|e| ImageError::Io(std::io::Error::other(e))) + } + + fn write_png(&self, path: &Path) -> Result<()> { + let w = self.resolution.x() as u32; + let h = self.resolution.y() as u32; + + // Convert whatever we have to u8 [0..255] + let data = self.to_u8_buffer(); + let channels = self.n_channels(); + + match channels { + 1 => { + // Luma + image_rs::save_buffer_with_format( + path, + &data, + w, + h, + image_rs::ColorType::L8, + image_rs::ImageFormat::Png, + )?; + } + 3 => { + // RGB + image_rs::save_buffer_with_format( + path, + &data, + w, + h, + image_rs::ColorType::Rgb8, + image_rs::ImageFormat::Png, + )?; + } + 4 => { + // RGBA + image_rs::save_buffer_with_format( + path, + &data, + w, + h, + image_rs::ColorType::Rgba8, + image_rs::ImageFormat::Png, + )?; + } + _ => bail!("PNG writer only supports 1, 3, or 4 channels"), + } + Ok(()) + } + + fn write_qoi(&self, path: &Path) -> Result<()> { + let w = self.resolution.x() as u32; + let h = self.resolution.y() as u32; + let data = self.to_u8_buffer(); + + let color_type = match self.n_channels() { + 3 => image_rs::ColorType::Rgb8, + 4 => image_rs::ColorType::Rgba8, + _ => bail!("QOI only supports 3 or 4 channels"), + }; + + image_rs::save_buffer_with_format( + path, + &data, + w, + h, + color_type, + image_rs::ImageFormat::Qoi, + )?; + Ok(()) + } + + fn write_exr(&self, path: &Path, _metadata: &ImageMetadata) -> Result<()> { + // EXR requires F32 + let w = self.resolution.x() as usize; + let h = self.resolution.y() as usize; + let c = self.n_channels(); + + write_rgba_file(path, w, h, |x, y| { + // Helper to get float value regardless of internal storage + let get = |ch| { + self.get_channel_with_wrap( + Point2i::new(x as i32, y as i32), + ch, + WrapMode::Clamp.into(), + ) + }; + + if c == 1 { + let v = get(0); + (v, v, v, 1.0) + } else if c == 3 { + (get(0), get(1), get(2), 1.0) + } else { + (get(0), get(1), get(2), get(3)) + } + }) + .context("Failed to write EXR")?; + + Ok(()) + } + + fn write_pfm(&self, path: &Path) -> Result<()> { + let file = File::create(path)?; + let mut writer = BufWriter::new(file); + + if self.n_channels() != 3 { + bail!("PFM writing currently only supports 3 channels (RGB)"); + } + + // Header + writeln!(writer, "PF")?; + writeln!(writer, "{} {}", self.resolution.x(), self.resolution.y())?; + let scale = if cfg!(target_endian = "little") { + -1.0 + } else { + 1.0 + }; + writeln!(writer, "{}", scale)?; + + // PBRT stores top-to-bottom. + for y in (0..self.resolution.y()).rev() { + for x in 0..self.resolution.x() { + for c in 0..3 { + let val = + self.get_channel_with_wrap(Point2i::new(x, y), c, WrapMode::Clamp.into()); + writer.write_all(&val.to_le_bytes())?; + } + } + } + + Ok(()) + } + + fn to_u8_buffer(&self) -> Vec { + match &self.pixels { + PixelData::U8(data) => data.clone(), + PixelData::F16(data) => data + .iter() + .map(|v| (v.to_f32().clamp(0.0, 1.0) * 255.0 + 0.5) as u8) + .collect(), + PixelData::F32(data) => data + .iter() + .map(|v| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8) + .collect(), + } + } +} + +fn read_generic(path: &Path, encoding: Option) -> Result { + let dyn_img = ImageReader::open(path) + .with_context(|| format!("Failed to open image: {:?}", path))? + .decode()?; + + let w = dyn_img.width() as i32; + let h = dyn_img.height() as i32; + let res = Point2i::new(w, h); + + // Check if it was loaded as high precision or standard + let image = match dyn_img { + DynamicImage::ImageRgb32F(buf) => Image { + format: PixelFormat::F32, + resolution: res, + channel_names: vec!["R".into(), "G".into(), "B".into()], + encoding: LINEAR, + pixels: PixelData::F32(buf.into_raw()), + }, + DynamicImage::ImageRgba32F(buf) => Image { + format: PixelFormat::F32, + resolution: res, + channel_names: vec!["R".into(), "G".into(), "B".into(), "A".into()], + encoding: LINEAR, + pixels: PixelData::F32(buf.into_raw()), + }, + _ => { + // Default to RGB8 for everything else + if dyn_img.color().has_alpha() { + let buf = dyn_img.to_rgba8(); + Image { + format: PixelFormat::U8, + resolution: res, + channel_names: vec!["R".into(), "G".into(), "B".into(), "A".into()], + encoding: encoding.unwrap_or(SRGB), + pixels: PixelData::U8(buf.into_raw()), + } + } else { + let buf = dyn_img.to_rgb8(); + Image { + format: PixelFormat::U8, + resolution: res, + channel_names: vec!["R".into(), "G".into(), "B".into()], + encoding: encoding.unwrap_or(SRGB), + pixels: PixelData::U8(buf.into_raw()), + } + } + } + }; + + let metadata = ImageMetadata::default(); + Ok(ImageAndMetadata { image, metadata }) +} + +fn read_exr(path: &Path) -> Result { + let image = read_first_rgba_layer_from_file( + path, + |resolution, _| { + let size = resolution.width() * resolution.height() * 4; + vec![0.0 as Float; size] + }, + |buffer, position, pixel| { + let width = position.width(); + let idx = (position.y() * width + position.x()) * 4; + // Map exr pixel struct to our buffer + buffer[idx] = pixel.0; + buffer[idx + 1] = pixel.1; + buffer[idx + 2] = pixel.2; + buffer[idx + 3] = pixel.3; + }, + ) + .with_context(|| format!("Failed to read EXR: {:?}", path))?; + + let w = image.layer_data.size.width() as i32; + let h = image.layer_data.size.height() as i32; + + let image = Image { + format: PixelFormat::F32, + resolution: Point2i::new(w, h), + channel_names: vec!["R".into(), "G".into(), "B".into(), "A".into()], + encoding: LINEAR, + pixels: PixelData::F32(image.layer_data.channel_data.pixels), + }; + + let metadata = ImageMetadata::default(); + Ok(ImageAndMetadata { image, metadata }) +} + +fn read_pfm(path: &Path) -> Result { + let file = File::open(path)?; + let mut reader = BufReader::new(file); + + // PFM Headers are: "PF\nwidth height\nscale\n" (or Pf for grayscale) + let mut header_word = String::new(); + reader.read_line(&mut header_word)?; + let header_word = header_word.trim(); + + let channels = match header_word { + "PF" => 3, + "Pf" => 1, + _ => bail!("Invalid PFM header: {}", header_word), + }; + + let mut dims_line = String::new(); + reader.read_line(&mut dims_line)?; + let dims: Vec = dims_line + .split_whitespace() + .map(|s| s.parse().unwrap_or(0)) + .collect(); + + if dims.len() < 2 { + bail!("Invalid PFM dimensions"); + } + let w = dims[0]; + let h = dims[1]; + + let mut scale_line = String::new(); + reader.read_line(&mut scale_line)?; + let scale: f32 = scale_line.trim().parse().context("Invalid PFM scale")?; + + let file_is_little_endian = scale < 0.0; + let abs_scale = scale.abs(); + + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer)?; + + let expected_bytes = (w * h * channels) as usize * 4; + if buffer.len() < expected_bytes { + bail!("PFM file too short"); + } + + let mut pixels = vec![0.0 as Float; (w * h * channels) as usize]; + + // PFM is Bottom-to-Top + for y in 0..h { + // Flippety-do + let src_y = h - 1 - y; + for x in 0..w { + for c in 0..channels { + let src_idx = ((src_y * w + x) * channels + c) as usize * 4; + let dst_idx = ((y * w + x) * channels + c) as usize; + + let bytes: [u8; 4] = buffer[src_idx..src_idx + 4].try_into()?; + + let val = if file_is_little_endian { + f32::from_le_bytes(bytes) + } else { + f32::from_be_bytes(bytes) + }; + + pixels[dst_idx] = val * abs_scale; + } + } + } + + let names = if channels == 1 { + vec!["Y".into()] + } else { + vec!["R".into(), "G".into(), "B".into()] + }; + + let image = Image { + format: PixelFormat::F32, + resolution: Point2i::new(w, h), + channel_names: names, + encoding: LINEAR, + pixels: PixelData::F32(pixels), + }; + + let metadata = ImageMetadata::default(); + Ok(ImageAndMetadata { image, metadata }) +} diff --git a/src/core/image/metadata.rs b/src/core/image/metadata.rs new file mode 100644 index 0000000..e8aef14 --- /dev/null +++ b/src/core/image/metadata.rs @@ -0,0 +1,50 @@ +use crate::core::geometry::{Bounds2i, Point2i}; +use crate::core::pbrt::Float; +use crate::spectra::colorspace::RGBColorSpace; +use crate::utils::math::SquareMatrix; +use smallvec::SmallVec; +use std::collections::HashMap; + +use std::ops::{Deref, DerefMut}; +#[derive(Debug, Clone, Default)] +pub struct ImageChannelDesc { + pub offset: Vec, +} + +impl ImageChannelDesc { + pub fn new(offset: &[usize]) -> Self { + Self { + offset: offset.into(), + } + } + + pub fn size(&self) -> usize { + self.offset.len() + } + + pub fn is_empty(&self) -> bool { + self.offset.is_empty() + } + pub fn is_identity(&self) -> bool { + for i in 0..self.size() { + if self.offset[i] != i { + return false; + } + } + true + } +} + +#[derive(Debug, Default)] +pub struct ImageMetadata { + pub render_time_seconds: Option, + pub camera_from_world: Option>, + pub ndc_from_world: Option>, + pub pixel_bounds: Option, + pub full_resolution: Option, + pub samples_per_pixel: Option, + pub mse: Option, + pub colorspace: Option, + pub strings: HashMap, + pub string_vectors: HashMap>, +} diff --git a/src/core/image/mod.rs b/src/core/image/mod.rs new file mode 100644 index 0000000..72fbef3 --- /dev/null +++ b/src/core/image/mod.rs @@ -0,0 +1,636 @@ +use shared::core::geometry::Point2i; +use shared::core::image::{Image, PixelFormat}; +use shared::utils::math::f16_to_f32; +use std::ops::Deref; + +pub mod io; +pub mod metadata; +pub mod ops; +pub mod pixel; + +pub use metadata::*; + +impl std::fmt::Display for PixelFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PixelFormat::U8 => write!(f, "U256"), + PixelFormat::F16 => write!(f, "Half"), + PixelFormat::F32 => write!(f, "Float"), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ImageChannelValues(pub SmallVec<[Float; 4]>); + +impl ImageChannelValues { + pub fn average(&self) -> Float { + if self.0.is_empty() { + return 0.0; + } + let sum: Float = self.0.iter().sum(); + sum / (self.0.len() as Float) + } + + pub fn max_value(&self) -> Float { + self.0.iter().fold(Float::MIN, |a, &b| a.max(b)) + } +} + +impl From<&[Float]> for ImageChannelValues { + fn from(slice: &[Float]) -> Self { + Self(SmallVec::from_slice(slice)) + } +} + +impl From> for ImageChannelValues { + fn from(vec: Vec) -> Self { + Self(SmallVec::from_vec(vec)) + } +} + +impl From<[Float; N]> for ImageChannelValues { + fn from(arr: [Float; N]) -> Self { + Self(SmallVec::from_slice(&arr)) + } +} + +impl Deref for ImageChannelValues { + type Target = SmallVec<[Float; 4]>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ImageChannelValues { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone)] +pub enum PixelStorage { + U8(Vec), + F16(Vec), + F32(Vec), +} + +pub struct ImageBuffer { + pub view: Image, + pub channel_names: Vec, + _storage: PixelStorage, +} + +impl Deref for ImageBuffer { + type Target = Image; + fn deref(&self) -> &Self::Target { + &self.view + } +} + +#[derive(Debug, Clone)] +pub struct ImageAndMetadata { + pub image: ImageBuffer, + pub metadata: ImageMetadata, +} + +impl ImageBuffer { + fn resolution(&self) { + self.view.resolution() + } + + pub fn new_empty() -> Self { + Self { + channel_names: Vec::new(), + _storage: PixelStorage::U8(Vec::new()), + view: Image { + format: PixelFormat::U256, + resolution: Point2i::new(0, 0), + n_channels: 0, + pixels: Pixels::U8(std::ptr::null()), + encoding: ColorEncoding::default(), + }, + } + } + + fn from_vector( + format: PixelFormat, + resolution: Point2i, + channel_names: Vec, + encoding: ColorEncoding, + ) -> Self { + let n_channels = channel_names.len() as i32; + let (format, pixels_view) = match &storage { + PixelStorage::U8(vec) => (PixelFormat::U8, ImagePixels::U8(vec.as_ptr())), + PixelStorage::F16(vec) => (PixelFormat::F16, ImagePixels::F16(vec.as_ptr())), + PixelStorage::F32(vec) => (PixelFormat::F32, ImagePixels::F32(vec.as_ptr())), + }; + + let view = Image { + format, + resolution, + n_channels, + encoding, + pixels: pixels_view, + }; + + Self { + view, + _storage: storage, + channel_names, + } + } + + pub fn from_u8( + data: Vec, + resolution: Point2i, + channel_names: Vec, + encoding: ColorEncoding, + ) -> Self { + let n_channels = channel_names.len() as i32; + let expected_len = (resolution.x * resolution.y) as usize * n_channels as usize; + if data.len() != expected_len { + panic!( + "ImageBuffer::from_u8: Data length {} does not match resolution {:?} * channels {}", + data.len(), + resolution, + n_channels + ); + } + + let storage = PixelStorage::U8(data); + + let ptr = match &storage { + PixelStorage::U8(v) => v.as_ptr(), + _ => unreachable!(), + }; + + let view = Image { + format: PixelFormat::U256, + resolution, + n_channels, + pixels: Pixels::U8(ptr), + encoding: encoding.clone(), + }; + + Self { + view, + channel_names, + _storage: storage, + } + } + + pub fn from_u8( + data: Vec, + resolution: Point2i, + channel_names: Vec, + encoding: ColorEncoding, + ) -> Self { + let n_channels = channel_names.len() as i32; + let expected_len = (resolution.x * resolution.y) as usize * n_channels as usize; + + if data.len() != expected_len { + panic!( + "ImageBuffer::from_u8: Data length {} does not match resolution {:?} * channels {}", + data.len(), + resolution, + n_channels + ); + } + + let storage = PixelStorage::U8(data); + + let ptr = match &storage { + PixelStorage::U8(v) => v.as_ptr(), + _ => unreachable!(), + }; + + let view = Image { + format: PixelFormat::U256, + resolution, + n_channels, + pixels: Pixels::U8(ptr), + encoding: encoding.clone(), + }; + + Self { + view, + channel_names, + _storage: storage, + } + } + + pub fn from_f16(data: Vec, resolution: Point2i, channel_names: Vec) -> Self { + let n_channels = channel_names.len() as i32; + let expected_len = (resolution.x * resolution.y) as usize * n_channels as usize; + + if data.len() != expected_len { + panic!("ImageBuffer::from_f16: Data length mismatch"); + } + + let storage = PixelStorage::F16(data); + + let ptr = match &storage { + PixelStorage::F16(v) => v.as_ptr() as *const u16, + _ => unreachable!(), + }; + + let view = Image { + format: PixelFormat::Half, + resolution, + n_channels, + pixels: Pixels::F16(ptr), + encoding: ColorEncoding::default(), + }; + + Self { + view, + channel_names, + _storage: storage, + } + } + + pub fn from_f32(data: Vec, resolution: Point2i, channel_names: Vec) -> Self { + let n_channels = channel_names.len() as i32; + let expected_len = (resolution.x * resolution.y) as usize * n_channels as usize; + + if data.len() != expected_len { + panic!("ImageBuffer::from_f32: Data length mismatch"); + } + + let storage = PixelStorage::F32(data); + + let ptr = match &storage { + PixelStorage::F32(v) => v.as_ptr(), + _ => unreachable!(), + }; + + let view = Image { + format: PixelFormat::Float, + resolution, + n_channels, + pixels: Pixels::F32(ptr), + encoding: ColorEncoding::default(), + }; + + Self { + view, + channel_names, + _storage: storage, + } + } + + pub fn new( + format: PixelFormat, + resolution: Point2i, + channel_names: &[&str], + encoding: *const ColorEncoding, + ) -> Self { + let n_channels = channel_names.len(); + let pixel_count = (resolution.x * resolution.y) as usize * n_channels; + let owned_names: Vec = channel_names.iter().map(|s| s.to_string()).collect(); + + let storage = match format { + PixelFormat::U8 => PixelStorage::U8(vec![0; pixel_count]), + PixelFormat::F16 => PixelStorage::F16(vec![0; pixel_count]), + PixelFormat::F32 => PixelStorage::F32(vec![0.0; pixel_count]), + }; + + Self::from_vector(storage, resolution, owned_names, encoding) + } + + pub fn channel_names(&self) -> Vec<&str> { + self.channel_names.iter().map(|s| s.as_str()).collect() + } + + pub fn encoding(&self) -> ColorEncoding { + self.view.encoding + } + + pub fn set_channel(&self, p: Point2i, c: i32, mut value: Float) { + if value.is_nan() { + value = 0.0; + } + + if !self.view.resolution.inside_exclusive(p) { + return; + } + + let offset = self.view.pixel_offset(p) + c as usize; + match &mut self._storage { + PixelStorage::U8(data) => { + if !self.view.encoding.is_null() { + data[offset] = self.view.encoding.from_linear_scalar(value); + } else { + let val = (value * 255.0 + 0.5).clamp(0.0, 255.0); + data[offset] = val as u8; + } + } + PixelStorage::F16(data) => { + data[offset] = f16_to_f32(value); + } + PixelStorage::F32(data) => { + data[offset] = value; + } + } + } + + pub fn get_channels_with_wrap(&self, p: Point2i, wrap_mode: WrapMode2D) -> ImageChannelValues { + let mut pp = p; + if !self.view.remap_pixel_coords(&mut pp, wrap_mode) { + return ImageChannelValues(smallvec![0.0; desc.offset.len()]); + } + + let pixel_offset = self.view.pixel_offset(pp); + let mut values: SmallVec<[Float; 4]> = SmallVec::with_capacity(desc.offset.len()); + match &self.pixels { + PixelData::U8(data) => { + for i in 0..self.view.n_channels() { + let raw_val = data[pixel_offset + i]; + let linear = self.view.encoding.to_linear_scalar(raw_val); + values.push(linear); + } + } + PixelData::F16(data) => { + for i in 0..self.view.n_channels() { + let raw_val = data[pixel_offset]; + values.push(f16_to_f32(raw_val)); + } + } + PixelData::F32(data) => { + for i in 0..self.view.n_channels() { + let val = data[pixel_offset + i]; + values.push(val); + } + } + } + + ImageChannelValues(values) + } + + pub fn get_channels(&self, p: Point2i) -> ImageChannelValues { + self.get_channels_with_wrap(p, WrapMode::Clamp.into()) + } + + pub fn get_channels_with_desc( + &self, + p: Point2i, + desc: &ImageChannelDesc, + wrap: WrapMode2D, + ) -> ImageChannelValues { + let mut pp = p; + if !self.view.remap_pixel_coords(&mut pp, wrap) { + return ImageChannelValues(smallvec![0.0; desc.offset.len()]); + } + + let pixel_offset = self.view.pixel_offset(pp); + + let mut values: SmallVec<[Float; 4]> = SmallVec::with_capacity(desc.offset.len()); + + match &self.pixels { + PixelData::U8(data) => { + for &channel_idx in &desc.offset { + let raw_val = data[pixel_offset + channel_idx as usize]; + let linear = self.view.encoding.to_linear_scalar(raw_val); + values.push(linear); + } + } + PixelData::F16(data) => { + for &channel_idx in &desc.offset { + let raw_val = data[pixel_offset + channel_idx as usize]; + values.push(f16_to_f32(raw_val)); + } + } + PixelData::F32(data) => { + for &channel_idx in &desc.offset { + let val = data[pixel_offset + channel_idx as usize]; + values.push(val); + } + } + } + + ImageChannelValues(values) + } + + pub fn channel_names_from_desc(&self, desc: &ImageChannelDesc) -> Vec<&str> { + desc.offset + .iter() + .map(|&i| self.channel_names[i].as_str()) + .collect() + } + + pub fn get_channel_desc( + &self, + requested_channels: &[&str], + ) -> Result { + let mut offset = Vec::with_capacity(requested_channels.len()); + + for &req in requested_channels.iter() { + match self.channel_names.iter().position(|n| n == req) { + Some(idx) => { + offset.push(idx); + } + None => { + return Err(format!( + "Image is missing requested channel '{}'. Available channels: {:?}", + req, self.channel_names + )); + } + } + } + + Ok(ImageChannelDesc { offset }) + } + + pub fn all_channels_desc(&self) -> ImageChannelDesc { + ImageChannelDesc { + offset: (0..self.n_channels()).collect(), + } + } + + pub fn select_channels(&self, desc: &ImageChannelDesc) -> Self { + let desc_channel_names: Vec = desc + .offset + .iter() + .map(|&i| self.channel_names[i as usize]) + .collect(); + + let new_storage = match &self._storage { + PixelStorage::U8(src_data) => { + // Allocate destination buffer + let mut dst_data = vec![0u8; pixel_count * dst_n_channels]; + + // Iterate over every pixel (Flat loop is faster than nested x,y) + for i in 0..pixel_count { + let src_pixel_start = i * src_n_channels; + let dst_pixel_start = i * dst_n_channels; + + // Copy specific channels based on desc + for (out_idx, &in_channel_offset) in desc.offset.iter().enumerate() { + let val = src_data[src_pixel_start + in_channel_offset as usize]; + dst_data[dst_pixel_start + out_idx] = val; + } + } + PixelStorage::U8(dst_data) + } + PixelStorage::F16(src_data) => { + let mut dst_data = vec![half::f16::ZERO; pixel_count * dst_n_channels]; + + for i in 0..pixel_count { + let src_pixel_start = i * src_n_channels; + let dst_pixel_start = i * dst_n_channels; + + for (out_idx, &in_channel_offset) in desc.offset.iter().enumerate() { + let val = src_data[src_pixel_start + in_channel_offset as usize]; + dst_data[dst_pixel_start + out_idx] = val; + } + } + PixelStorage::F16(dst_data) + } + PixelStorage::F32(src_data) => { + let mut dst_data = vec![0.0; pixel_count * dst_n_channels]; + + for i in 0..pixel_count { + let src_pixel_start = i * src_n_channels; + let dst_pixel_start = i * dst_n_channels; + + for (out_idx, &in_channel_offset) in desc.offset.iter().enumerate() { + let val = src_data[src_pixel_start + in_channel_offset as usize]; + dst_data[dst_pixel_start + out_idx] = val; + } + } + PixelStorage::F32(dst_data) + } + }; + + let image = Self::new( + self.format, + self.resolution, + desc_channel_names, + self.encoding(), + ); + } + + pub fn set_channels( + &mut self, + p: Point2i, + desc: &ImageChannelDesc, + mut values: &ImageChannelValues, + ) { + assert_eq!(desc.size(), values.len()); + for i in 0..desc.size() { + self.view.set_channel(p, desc.offset[i], values[i]); + } + } + + pub fn set_channels_all(&mut self, p: Point2i, values: &ImageChannelValues) { + self.set_channels(p, &self.all_channels_desc(), values) + } + + pub fn get_sampling_distribution(&self, dxd_a: F, domain: Bounds2f) -> Array2D + where + F: Fn(Point2f) -> Float + Sync + Send, + { + let width = self.resolution().x(); + let height = self.resolution().y(); + + let mut dist = Array2D::new_with_dims(width as usize, height as usize); + + dist.values + .par_chunks_mut(width as usize) + .enumerate() + .for_each(|(y, row)| { + let y = y as i32; + + for (x, out_val) in row.iter_mut().enumerate() { + let x = x as i32; + + let value = self.get_channels_default(Point2i::new(x, y)).average(); + + let u = (x as Float + 0.5) / width as Float; + let v = (y as Float + 0.5) / height as Float; + let p = domain.lerp(Point2f::new(u, v)); + *out_val = value * dxd_a(p); + } + }); + + dist + } + + pub fn get_sampling_distribution_uniform(&self) -> Array2D { + let default_domain = Bounds2f::from_points(Point2f::new(0.0, 0.0), Point2f::new(1.0, 1.0)); + + self.get_sampling_distribution(|_| 1.0, default_domain) + } + + pub fn mse( + &self, + desc: ImageChannelDesc, + ref_img: &Image, + generate_mse_image: bool, + ) -> (ImageChannelValues, Option) { + let mut sum_se: Vec = vec![0.; desc.size()]; + let names_ref = self.channel_names_from_desc(&desc); + let ref_desc = ref_img + .get_channel_desc(&self.channel_names_from_desc(&desc)) + .expect("Channels not found in image"); + assert_eq!(self.resolution(), ref_img.resolution()); + + let width = self.resolution.x() as usize; + let height = self.resolution.y() as usize; + let n_channels = desc.offset.len(); + let mut mse_pixels = if generate_mse_image { + vec![0.0f32; width * height * n_channels] + } else { + Vec::new() + }; + + for y in 0..self.resolution().y() { + for x in 0..self.resolution().x() { + let v = + self.get_channel_with_desc(Point2i::new(x, y), &desc, WrapMode::Clamp.into()); + let v_ref = self.get_channel_with_desc( + Point2i::new(x, y), + &ref_desc, + WrapMode::Clamp.into(), + ); + for c in 0..desc.size() { + let se = square(v[c] as f64 - v_ref[c] as f64); + if se.is_infinite() { + continue; + } + sum_se[c] += se; + if generate_mse_image { + let idx = (y as usize * width + x as usize) * n_channels + c; + mse_pixels[idx] = se as f32; + } + } + } + } + + let pixel_count = (self.resolution().x() * self.resolution.y()) as f64; + let mse_values: SmallVec<[Float; 4]> = + sum_se.iter().map(|&s| (s / pixel_count) as Float).collect(); + + let mse_image = if generate_mse_image { + Some(Image::new( + PixelFormat::F32, + self.resolution, + &names_ref, + LINEAR, + )) + } else { + None + }; + + (ImageChannelValues(mse_values), mse_image) + } + + pub fn update_view_pointers(&mut self) { + self.view.pixels = match &self._storage { + PixelStorage::U8(vec) => Pixels::U8(vec.as_ptr()), + PixelStorage::F16(vec) => Pixels::F16(vec.as_ptr() as *const u16), + PixelStorage::F32(vec) => Pixels::F32(vec.as_ptr()), + }; + } +} diff --git a/shared/src/images/ops.rs b/src/core/image/ops.rs similarity index 98% rename from shared/src/images/ops.rs rename to src/core/image/ops.rs index ce231ef..f4b1770 100644 --- a/shared/src/images/ops.rs +++ b/src/core/image/ops.rs @@ -1,10 +1,10 @@ // use rayon::prelude::*; -use crate::core::geometry::{Bounds2i, Point2i}; -use crate::core::pbrt::Float; -use crate::image::pixel::PixelStorage; -use crate::image::{Image, PixelData, PixelFormat, WrapMode, WrapMode2D}; -use crate::utils::math::windowed_sinc; +use super::ImageBuffer; use rayon::prelude::*; +use shared::Float; +use shared::core::geometry::{Bounds2i, Point2i}; +use shared::core::image::{PixelFormat, WrapMode, WrapMode2D}; +use shared::utils::math::windowed_sinc; use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, Copy)] @@ -13,7 +13,7 @@ pub struct ResampleWeight { pub weight: [Float; 4], } -impl Image { +impl ImageBuffer { pub fn flip_y(&mut self) { let res = self.resolution; let nc = self.n_channels(); diff --git a/shared/src/images/pixel.rs b/src/core/image/pixel.rs similarity index 100% rename from shared/src/images/pixel.rs rename to src/core/image/pixel.rs diff --git a/src/core/light.rs b/src/core/light.rs new file mode 100644 index 0000000..9a60ac0 --- /dev/null +++ b/src/core/light.rs @@ -0,0 +1,16 @@ +use shared::core::light::LIghtBase; +use shared::core::spectrum::Spectrum; +use shared::spectra::DenselySampledSpectrum; + +use crate::core::spectrum::SPECTRUM_CACHE; +use crate::utils::containers::InternCache; + +pub trait LightBaseTrait { + pub fn lookup_spectrum(s: &Spectrum) -> DenselySampledSpectrum { + let cache = SPECTRUM_CACHE.get_or_init(InternCache::new); + let dense_spectrum = DenselySampledSpectrum::from_spectrum(s); + cache.lookup(dense_spectrum).as_ref() + } +} + +impl LightBaseTrait for LightBase {} diff --git a/src/core/material.rs b/src/core/material.rs new file mode 100644 index 0000000..65f0ac2 --- /dev/null +++ b/src/core/material.rs @@ -0,0 +1,68 @@ +use crate::core::image::ImageBuffer; +use crate::utils::error::FileLoc; +use crate::utils::parameters::ParameterDictionary; +use shared::core::material::Material; +use shared::materials::*; +use std::collections::HashMap; + +pub trait CreateMaterial: Sized { + fn create( + parameters: &TextureParameterDictionary, + normal_map: Option>, + named_materials: &HashMap, + loc: &FileLoc, + ) -> Self; +} + +macro_rules! make_material_factory { + ( + $name:ident, $params:ident, $nmap:ident, $mats:ident, $loc:ident; + $($key:literal => $variant:ident($concrete:ty)),+ $(,)? + ) => { + match $name { + $( + $key => { + let mat = <$concrete>::create($params, $nmap, $mats, $loc); + Ok(Material::$variant(mat)) + } + )+ + _ => Err(format!("Material type '{}' unknown at {}", $name, $loc)), + } + }; +} + +pub trait MaterialFactory { + fn create( + name: &str, + params: &TextureParameterDictionary, + normal_map: Arc, + named_materials: HashMap, + loc: &FileLoc, + ) -> Result; +} + +impl MaterialFactory for Material { + fn create( + name: &str, + params: &TextureParameterDictionary, + normal_map: Option>, + named_materials: &HashMap, + loc: &FileLoc, + ) -> Result { + make_material_factory!( + name, params, normal_map, named_materials, loc; + + "diffuse" => Diffuse(DiffuseMaterial), + "coateddiffuse" => CoatedDiffuse(CoatedDiffuseMaterial), + "coatedconductor" => Conductor(CoatedConductorMaterial), + "diffusetransmission" => DiffuseTransmission(DiffuseTransmissionMaterial), + "dielectric" => Dielectric(DielectricMaterial), + "thindielectric" => ThinDielectric(ThinDielectricMaterial), + "hair" => Hair(HairMaterial), + "conductor" => Conductor(ConductorMaterial), + "measured" => Measured(MeasuredMaterial), + "subsurface" => Subsurface(SubsurfaceMaterial), + "mix" => Mix(MixMaterial) + ) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index ba96e37..7e673a4 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,7 +1,15 @@ +pub mod aggregates; +pub mod bssrdf; pub mod camera; pub mod color; pub mod film; pub mod filter; +pub mod image; +pub mod light; +pub mod material; +pub mod sampler; +pub mod sampler; pub mod scene; +pub mod shape; pub mod spectrum; pub mod texture; diff --git a/src/core/sampler.rs b/src/core/sampler.rs new file mode 100644 index 0000000..1c07da3 --- /dev/null +++ b/src/core/sampler.rs @@ -0,0 +1,47 @@ +use shared::core::sampler::Sampler; + +pub trait SamplerFactory { + fn create( + name: &str, + params: &ParameterDictionary, + full_res: Point2i, + loc: &FileLoc, + ) -> Result; +} + +impl SamplerFactory for Sampler { + fn create( + name: &str, + params: &ParameterDictionary, + full_res: Point2i, + loc: &FileLoc, + ) -> Result { + match name { + "zsobol" => { + let sampler = ZSobolSampler::create(params, full_res, loc)?; + Ok(Sampler::ZSobol(sampler)) + } + "paddedsobol" => { + let sampler = PaddedSobolSampler::create(params, full_res, loc)?; + Ok(Sampler::PaddedSobol(sampler)) + } + "halton" => { + let sampler = HaltonSampler::create(params, full_res, loc)?; + Ok(Sampler::Halton(sampler)) + } + "sobol" => { + let sampler = SobolSampler::create(params, full_res, loc)?; + Ok(Sampler::Sobol(sampler)) + } + "Independent" => { + let sampler = IndependentSampler::create(params, full_res, loc)?; + Ok(Sampler::Independent(sampler)) + } + "stratified" => { + let sampler = StratifiedSampler::create(params, full_res, loc)?; + Ok(Sampler::Stratified(sampler)) + } + _ => Err(format!("Film type '{}' unknown at {}", name, loc)), + } + } +} diff --git a/src/core/scene.rs b/src/core/scene.rs index cb10807..9e62180 100644 --- a/src/core/scene.rs +++ b/src/core/scene.rs @@ -1,30 +1,36 @@ use crate::core::filter::FilterFactory; -use crate::utils::parameters::{ParameterDictionary, ParsedParameterVector}; +use crate::core::image::{ImageBuffer, io::ImageIO}; +use crate::core::texture::SpectrumTexture; +use crate::utils::parallel::{AsyncJob, run_async}; +use crate::utils::parameters::{ + NamedTextures, ParameterDictionary, ParsedParameterVector, TextureParameterDictionary, +}; +use crate::utils::parser::ParserTarget; use crate::utils::{normalize_utf8, resolve_filename}; use parking_lot::Mutex; use shared::Float; use shared::core::camera::{Camera, CameraTransform}; +use shared::core::color::LINEAR; use shared::core::film::{Film, FilmTrait}; use shared::core::filter::Filter; use shared::core::geometry::{Point3f, Vector3f}; use shared::core::lights::Light; +use shared::core::material::Material; use shared::core::medium::Medium; use shared::core::options::RenderingCoordinateSystem; use shared::core::sampler::Sampler; +use shared::core::spectrum::{Spectrum, SpectrumType}; use shared::core::texture::{FloatTexture, SpectrumTexture}; use shared::images::Image; use shared::spectra::RGBColorSpace; use shared::utils::error::FileLoc; use shared::utils::math::SquareMatrix; -use shared::utils::parser::ParserTarget; -use shared::utils::transform::look_at; -use shared::utils::transform::{AnimatedTransform, TransformGeneric}; +use shared::utils::transform::{AnimatedTransform, Transform, look_at}; use std::char::MAX; use std::collections::{HashMap, HashSet}; use std::ops::{Index as IndexTrait, IndexMut as IndexMutTrait}; use std::sync::Arc; use std::sync::atomic::{AtomicI32, Ordering}; -use std::thread::{self, JoinHandle}; #[derive(Clone, Debug)] pub enum MaterialRef { @@ -34,7 +40,7 @@ pub enum MaterialRef { } #[derive(Clone, Default, Debug)] -pub struct SceneEntityBase { +pub struct SceneEntity { pub name: String, pub loc: FileLoc, pub parameters: ParameterDictionary, @@ -42,7 +48,7 @@ pub struct SceneEntityBase { #[derive(Clone, Debug)] pub struct TransformedSceneEntity { - pub base: SceneEntityBase, + pub base: SceneEntity, pub render_from_object: AnimatedTransform, } @@ -51,16 +57,16 @@ pub type TextureSceneEntity = TransformedSceneEntity; #[derive(Clone, Debug)] pub struct CameraSceneEntity { - pub base: SceneEntityBase, + pub base: SceneEntity, pub camera_transform: CameraTransform, pub medium: String, } #[derive(Clone, Debug)] pub struct ShapeSceneEntity { - pub base: SceneEntityBase, - pub render_from_object: Arc>, - pub object_from_render: Arc>, + pub base: SceneEntity, + pub render_from_object: Arc, + pub object_from_render: Arc, pub reverse_orientation: bool, pub material: MaterialRef, pub light_index: Option, @@ -73,7 +79,7 @@ pub struct ShapeSceneEntity { pub struct AnimatedShapeSceneEntity { pub transformed_base: TransformedSceneEntity, - pub identity: Arc>, + pub identity: Arc, pub reverse_orientation: bool, pub material: MaterialRef, pub light_index: Option, @@ -108,71 +114,73 @@ pub struct InstanceSceneEntity { pub transform: InstanceTransform, } -pub struct TextureData { +#[derive(Default)] +pub struct TextureState { pub serial_float_textures: Vec<(String, TextureSceneEntity)>, pub serial_spectrum_textures: Vec<(String, TextureSceneEntity)>, pub async_spectrum_textures: Vec<(String, TextureSceneEntity)>, pub loading_texture_filenames: HashSet, - pub float_texture_jobs: HashMap>>, - pub spectrum_texture_jobs: HashMap>>, + pub float_texture_jobs: HashMap>>, + pub spectrum_texture_jobs: HashMap>>, pub n_missing_textures: i32, } -pub struct NamedTextures { - pub float_textures: HashMap>, - pub albedo_spectrum_textures: HashMap>, - pub unbounded_spectrum_textures: HashMap>, - pub illuminant_spectrum_textures: HashMap>, -} - -pub struct LightData { - pub light_jobs: Vec>, - pub area_lights: Vec, -} - -pub struct MaterialData { - pub named_materials: Vec<(String, SceneEntityBase)>, - pub materials: Vec, - pub normal_map_jobs: HashMap>>, +#[derive(Default)] +pub struct MaterialState { + pub named_materials: Vec<(String, SceneEntity)>, + pub materials: Vec, + pub normal_map_jobs: HashMap>>, pub normal_maps: HashMap>, } -pub struct BasicScene { - pub integrator: Mutex>, - pub accelerator: Mutex>, - pub film_colorspace: Mutex>>, +#[derive(Default)] +pub struct LightState { + pub light_jobs: Vec>, + pub area_lights: Vec, +} + +#[derive(Default)] +pub struct MediaState { + pub jobs: HashMap>, + pub map: HashMap>, +} + +#[derive(Default)] +pub struct SingletonState { + pub result: Option>, + pub job: Option>, +} + +pub struct BasicScene { + pub integrator: Option, + pub accelerator: Option, + pub film_colorspace: Option>, - // Collections pub shapes: Mutex>, pub animated_shapes: Mutex>, + pub instances: Mutex>, pub instance_definitions: Mutex>>, - // Simple mutexes in C++ - pub medium_jobs: Mutex>>, - pub media_map: Mutex>, - pub material_data: Mutex, - pub light_data: Mutex, - pub texture_data: Mutex, + pub media_state: Mutex, + pub material_state: Mutex, + pub light_state: Mutex, + pub texture_state: Mutex, - // Top level - pub camera: Mutex>>, - pub film: Mutex>>, - pub sampler: Mutex>>, - - pub camera_job: Mutex>>, - pub sampler_job: Mutex>>, + pub camera_state: Mutex>, + pub sampler_state: Mutex>, + pub film_state: Mutex>, } impl BasicScene { fn set_options( self: &Arc, - filter: SceneEntityBase, - film: SceneEntityBase, + filter: SceneEntity, + film: SceneEntity, camera: CameraSceneEntity, - sampler: SceneEntityBase, - integ: SceneEntityBase, - accel: SceneEntityBase, + sampler: SceneEntity, + integ: SceneEntity, + accel: SceneEntity, ) { *self.integrator.lock() = Some(integ); *self.accelerator.lock() = Some(accel); @@ -223,43 +231,20 @@ impl BasicScene { *self.camera_job.lock() = Some(camera_job); } - fn get_medium(&self, name: &str, loc: &FileLoc) -> Option { - if name.is_empty() || name == "none" { - return None; - } - - loop { - { - let media_map = self.media_map.lock(); - if let Some(m) = media_map.get(name) { - return Some(m.clone()); - } - - let mut jobs = self.medium_jobs.lock(); - if let Some(handle) = jobs.remove(name) { - drop(jobs); - drop(media_map); - - log::info!("Waiting for medium '{}' to finish loading...", name); - let m = handle.join().expect("Medium loading thread panicked"); - - self.media_map.lock().insert(name.to_string(), m.clone()); - return Some(m); - } - - if !self.medium_jobs.lock().contains_key(name) - && !self.media_map.lock().contains_key(name) - { - log::error!("[{:?}] {}: medium is not defined.", loc, name); - return None; - } - } - - thread::yield_now(); - } + 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)); } - fn add_float_texture(&self, name: &str, texture: &TextureSceneEntity) { + pub fn add_material(&self, name: &str, 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: &str, texture: &TextureSceneEntity) { if texture.render_from_object.is_animated() { log::info!( "{}: Animated world to texture not supported, using start", @@ -272,6 +257,7 @@ impl BasicScene { texture_state .serial_float_textures .push((name.to_string(), texture.clone())); + return; } let filename = resolve_filename(&texture.base.parameters.get_one_string("filename", "")); @@ -283,7 +269,7 @@ impl BasicScene { texture_state.n_missing_textures += 1; return; } - if !std::fs::exists(&filename).expect("Not sure what to do here, just moving on") { + if !std::path::Path::new(&filename).exists() { log::error!("[{:?}] {}: file not found.", texture.base.loc, filename); texture_state.n_missing_textures += 1; return; @@ -293,7 +279,462 @@ impl BasicScene { texture_state .serial_float_textures .push((name.to_string(), texture.clone())); + return; } + + texture_state + .loading_texture_filenames + .insert(filename.clone()); + + let texture_clone = texture.clone(); + let name_clone = name.to_string(); + + let job = run_async(move || { + let render_from_texture = texture_clone.render_from_object.start_transform(); + let tex_dict = + TextureParameterDictionary::new(&texture_clone.base.parameters.into(), None); + + FloatTexture::create( + &texture_clone.name, + render_from_texture, + tex_dict, + &texture_clone.base.loc, + ) + }); + + texture_state + .float_texture_jobs + .insert(name.to_string(), job); + } + + pub fn add_spectrum_texture(&self, name: &str, texture: &TextureSceneEntity) { + if texture.render_from_object.is_animated() { + log::info!( + "{}: Animated world to texture not supported, using start", + texture.base.loc + ); + } + + let mut texture_state = self.texture_data.lock(); + if name != "imagemap" && name != "ptex" { + texture_state + .serial_spectrum_textures + .push((name.to_string(), texture.clone())); + return; + } + + let filename = resolve_filename(&texture.base.parameters.get_one_string("filename", "")); + if filename.is_empty() { + log::error!( + "[{:?}] \"string filename\" not provided for image texture.", + texture.base.loc + ); + texture_state.n_missing_textures += 1; + return; + } + + if !std::path::Path::new(&filename).exists() { + log::error!("[{:?}] {}: file not found.", texture.base.loc, filename); + texture_state.n_missing_textures += 1; + return; + } + + if texture_state.loading_texture_filenames.contains(&filename) { + texture_state + .serial_float_textures + .push((name.to_string(), texture.clone())); + return; + } + + texture_state + .loading_texture_filenames + .insert(filename.clone()); + + let texture_clone = texture.clone(); + let name_clone = name.to_string(); + + let job = run_async(move || { + let render_from_texture = texture_clone.render_from_object.start_transform(); + let tex_dict = + TextureParameterDictionary::new(&texture_clone.base.parameters.into(), None); + + SpectrumTexture::create( + &texture_clone.name, + render_from_texture, + tex_dict, + SpectrumType::Albedo, + &texture_clone.base.loc, + ) + }); + + texture_state + .float_texture_jobs + .insert(name.to_string(), job); + } + + pub fn add_light(&self, light: LightSceneEntity) { + if light.transformed_base.render_from_object.is_animated() { + log::info!( + "{}: Animated world to texture not supported, using start", + light.transformed_base.base.loc + ); + } + + let medium = self + .get_medium(light.medium, &light.transformed_base.base.loc) + .unwrap(); + let camera_transform = self.get_camera().camera_transform; + + let light_clone = light.clone(); + let mut light_state = self.light_data.lock().unwrap(); + let job = run_async(move || { + let render_from_light = light_clone + .transformed_base + .render_from_object + .start_transform; + + Light::create( + &light_clone.transformed_base.base.name, + &light_clone.transformed_base.base.parameters, + render_from_light, + camera_transform, + medium, + &light_cmd.transformed_base.base.loc, + ) + }); + light_state.light_jobs.insert(name.to_string(), job); + } + + pub fn add_area_light(&self, light: SceneEntity) -> usize { + let mut state = self.light_state.lock(); + state.area_lights.push(light.clone()); + state.area_lights.len() - 1 + } + + pub fn add_shapes(&self, new_shapes: &mut Vec) { + let mut shapes = self.shapes.lock().unwrap(); + shapes.append(new_shapes); + } + + pub fn add_animated_shapes(&self, new_shapes: &mut Vec) -> usize { + let mut shapes = self.animated_shapes.lock().unwrap(); + shapes.append(new_shapes); + } + + pub fn add_instance_definition(&self, instance: InstanceDefinitionSceneEntity) { + let def = Arc::new(instance); + let name = def.name.clone(); + let mut instances = self.instance_definitions.lock().unwrap(); + instances.insert(name, def); + } + + pub fn add_instance_uses(&self, new_defs: &mut Vec) { + let mut defs = self.instances.lock().unwrap(); + defs.append(new_defs); + } + + pub fn create_materials( + &self, + textures: &NamedTextures, + ) -> (HashMap, Vec) { + let mut state = self.material_state.lock().unwrap(); + log::info!( + "Starting to consume {} normal map futures", + state.normal_map_jobs.len() + ); + + let jobs: Vec<_> = state.normal_maps_jobs.drain().collect(); + for (filename, job) in jobs { + if state.normal_map_jobs.contains_key(&filename) { + panic!("Trying to redefine '{}' twice in jobs and maps", filename); + } + + let image = job.wait(); + state.normal_map_jobs.insert(filename, image); + } + + let mut named_materials_out: HashMap = HashMap::new(); + + for (name, entity) in &state.named_materials { + if named_materials_out.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!( + "{}: \"string type\" not provided in named material's parameters.", + entity.loc + ); + continue; + } + + let nm_filename = resolve_filename(&entity.parameters.get_one_string("normalmap", "")); + let mut normal_map_img = None; + + if !nm_filename.is_empty() { + if let Some(img) = state.normal_maps.get(&nm_filename) { + normal_map_img = Some(img.clone()); + } else { + panic!( + "Normal map '{}' was expected but not found in map.", + nm_filename + ); + } + } + + let tex_dict = TextureParameterDictionary::new(&entity.parameters, textures); + + let mat = Material::create( + &mat_type, + &tex_dict, + normal_map_img, + &named_materials_out, + &entity.loc, + ); + + named_materials_out.insert(name.clone(), mat); + } + + let mut materials_out: Vec = Vec::with_capacity(state.materials.len()); + for entity in &state.materials { + let nm_filename = resolve_filename(&entity.parameters.get_one_string("normalmap", "")); + let mut normal_map_img = None; + + if !nm_filename.is_empty() { + if let Some(img) = state.normal_maps.get(&nm_filename) { + normal_map_img = Some(img.clone()); + } else { + panic!( + "Normal map '{}' was expected but not found in map.", + nm_filename + ); + } + } + + let tex_dict = TextureParameterDictionary::new(&entity.parameters, textures); + + let mat = Material::create( + &mat_type, + &tex_dict, + normal_map_img, + &named_materials_out, + &entity.loc, + ); + } + (named_materials_out, materials_out) + } + + pub fn get_camera(&self) -> Arc { + self.get_singleton(&self.camera_state, "Camera") + } + + pub fn get_sampler(&self) -> Arc { + self.get_singleton(&self.sampler_state, "Sampler") + } + + pub fn get_film(&self) -> Arc { + self.get_singleton(&self.film_state, "Film") + } + + pub fn done(&self) { + let shapes = self.shapes.lock().unwrap(); + let animated_shapes = self.animated_shapes.lock().unwrap(); + let instances = self.instances.lock().unwrap(); + let instance_defs = self.instance_definitions.lock().unwrap(); + + let tex_state = self.texture_state.lock().unwrap(); + let mat_state = self.material_state.lock().unwrap(); + let med_state = self.media_state.lock().unwrap(); + let light_state = self.light_state.lock().unwrap(); + + log::info!( + "Scene stats: {} shapes, {} animated shapes, {} instance definitions, \ + {} instance uses, {} float textures, {} spectrum textures, \ + {} named materials, {} materials", + shapes.len(), + animated_shapes.len(), + instance_defs.len(), + instances.len(), + // Count serial + async jobs for total textures + tex_state.serial_float_textures.len() + tex_state.float_texture_jobs.len(), + tex_state.serial_spectrum_textures.len() + + tex_state.async_spectrum_textures.len() + + tex_state.spectrum_texture_jobs.len(), + mat_state.named_materials.len(), + mat_state.materials.len() + ); + + { + let mut unused_float_tex: HashSet<&str> = HashSet::new(); + let mut unused_spec_tex: HashSet<&str> = HashSet::new(); + + for (name, _) in &tex_state.serial_float_textures { + unused_float_tex.insert(name); + } + for name in tex_state.float_texture_jobs.keys() { + unused_float_tex.insert(name); + } + + for (name, _) in &tex_state.serial_spectrum_textures { + unused_spec_tex.insert(name); + } + for (name, _) in &tex_state.async_spectrum_textures { + unused_spec_tex.insert(name); + } + for name in tex_state.spectrum_texture_jobs.keys() { + unused_spec_tex.insert(name); + } + + // Define the Checker Closure + let mut check_params = |params: &ParameterDictionary| { + for param in ¶ms.params { + if param.type_name == "texture" { + // Textures are usually stored in `strings` + for tex_name in ¶m.strings { + if unused_float_tex.contains(tex_name.as_str()) { + unused_float_tex.remove(tex_name.as_str()); + } else if unused_spec_tex.contains(tex_name.as_str()) { + unused_spec_tex.remove(tex_name.as_str()); + } + } + } + } + }; + + // Materials + for (_, mat) in &mat_state.named_materials { + check_params(&mat.parameters); + } + for mat in &mat_state.materials { + check_params(&mat.parameters); + } + + for (_, tex) in &tex_state.serial_float_textures { + check_params(&tex.parameters); + } + for (_, tex) in &tex_state.serial_spectrum_textures { + check_params(&tex.parameters); + } + + // Shapes + for shape in shapes.iter() { + check_params(&shape.parameters); + } + for anim_shape in animated_shapes.iter() { + check_params(&anim_shape.parameters); + } + + // Instance Definitions + for def in instance_defs.values() { + for shape in &def.shapes { + check_params(&shape.parameters); + } + for anim_shape in &def.animated_shapes { + check_params(&anim_shape.parameters); + } + } + + for name in unused_float_tex { + log::warn!("{}: float texture unused in scene", name); + } + for name in unused_spec_tex { + log::warn!("{}: spectrum texture unused in scene", name); + } + } + } + + // PRIVATE METHODS + + fn get_singleton( + &self, + mutex: &Mutex>, + name: &str, + ) -> Arc { + let mut state = mutex.lock().unwrap(); + + if let Some(ref res) = state.result { + return res.clone(); + } + + if let Some(job) = state.job.take() { + let res = Arc::new(job.wait()); + state.result = Some(res.clone()); + log::debug!("Retrieved {} from future", name); + return res; + } + + panic!("{} requested but not initialized!", name); + } + + fn start_loading_normal_maps( + &self, + state: &mut MaterialState, + parameters: &ParameterDictionary, + ) { + let filename = resolve_filename(parameters.get_one_string("filename", "")); + if filename.is_empty() { + log::error!("Could not load normal maps"); + return; + } + + if state.normal_map_jobs.contains_key(&filename) + || state.normal_maps.contains_key(&filename) + { + return; + } + + let filename_clone = filename.clone(); + + let job = crate::parallel::run_async(move || { + let path = std::path::Path::new(&filename_clone); + + let immeta = ImageBuffer::read(path, Some(ColorEncoding::Linear)) + .unwrap_or_else(|e| panic!("{}: unable to read normal map: {}", filename_clone, e)); + + let image = &immeta.image; + + let rgb_desc = image.get_channel_desc(&["R", "G", "B"]).unwrap_or_else(|| { + panic!( + "{}: normal map must contain R, G, and B channels", + filename_clone + ) + }); + + let normal_map = image.select_channels(&rgb_desc); + + Arc::new(normal_map) + }); + + state.normal_map_jobs.insert(filename, job); + } + + fn get_medium(&self, name: &str, loc: &FileLoc) -> Option> { + if name.is_empty() { + return None; + } + + let mut state = self.media_state.lock().unwrap(); + + if let Some(medium) = state.map.get(name) { + return Some(medium.clone()); + } + + if let Some(job) = state.jobs.remove(name) { + let 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 } } @@ -344,27 +785,26 @@ impl IndexMutTrait for TransformSet { #[derive(Default, Debug, Clone)] struct GraphicsState { - current_inside_medium: String, - current_outside_medium: String, - current_material_name: String, - area_light_name: String, - area_light_params: ParsedParameterVector, - area_light_loc: FileLoc, - shape_attributes: ParsedParameterVector, - light_attributes: ParsedParameterVector, - material_attributes: ParsedParameterVector, - medium_attributes: ParsedParameterVector, - texture_attributes: ParsedParameterVector, + pub current_inside_medium: String, + pub current_outside_medium: String, + pub current_material_name: String, + pub area_light_name: String, + pub area_light_params: ParsedParameterVector, + pub area_light_loc: FileLoc, + pub shape_attributes: ParsedParameterVector, + pub light_attributes: ParsedParameterVector, + pub material_attributes: ParsedParameterVector, + pub medium_attributes: ParsedParameterVector, + pub texture_attributes: ParsedParameterVector, - reverse_orientation: bool, - // None implies sRGB usually - color_space: Option>, + pub reverse_orientation: bool, + pub color_space: Option>, - ctm: TransformSet, + pub ctm: TransformSet, // 1=Start, 2=End, 3=All - active_transform_bits: u32, - transform_start_time: Float, - transform_end_time: Float, + pub active_transform_bits: u32, + pub transform_start_time: Float, + pub transform_end_time: Float, } #[derive(PartialEq, Eq)] @@ -381,21 +821,19 @@ pub struct BasicSceneBuilder { push_stack: Vec<(char, FileLoc)>, render_from_world: TransformGeneric, named_coordinate_systems: HashMap, - // Object Instancing State - // If this is Some, we are inside an "ObjectBegin" block active_instance_definition: Option, - shapes_buffer: Vec, - instance_uses_buffer: Vec, + shapes: Mutex>, + animated_shapes: Mutex>, named_material_names: HashSet, medium_names: HashSet, current_camera: Option, - current_film: Option, - current_integrator: Option, - current_sampler: Option, - current_filter: Option, - current_accelerator: Option, + current_film: Option, + current_integrator: Option, + current_sampler: Option, + current_filter: Option, + current_accelerator: Option, } impl BasicSceneBuilder { @@ -413,13 +851,13 @@ impl BasicSceneBuilder { render_from_world: TransformGeneric::identity(), named_coordinate_systems: HashMap::new(), active_instance_definition: None, - shapes_buffer: Vec::new(), - instance_uses_buffer: Vec::new(), + shapes: Vec::new(), + instances: Vec::new(), named_material_names: HashSet::new(), medium_names: HashSet::new(), current_camera: Some(CameraSceneEntity { - base: SceneEntityBase { + base: SceneEntity { name: "perspective".into(), ..Default::default() }, @@ -429,23 +867,23 @@ impl BasicSceneBuilder { ), medium: String::new(), }), - current_sampler: Some(SceneEntityBase { + current_sampler: Some(SceneEntity { name: "zsobol".into(), ..Default::default() }), - current_filter: Some(SceneEntityBase { + current_filter: Some(SceneEntity { name: "gaussian".into(), ..Default::default() }), - current_integrator: Some(SceneEntityBase { + current_integrator: Some(SceneEntity { name: "volpath".into(), ..Default::default() }), - current_accelerator: Some(SceneEntityBase { + current_accelerator: Some(SceneEntity { name: "bvh".into(), ..Default::default() }), - current_film: Some(SceneEntityBase { + current_film: Some(SceneEntity { name: "rgb".into(), ..Default::default() }), @@ -605,7 +1043,7 @@ impl ParserTarget for BasicSceneBuilder { ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); self.current_camera = Some(CameraSceneEntity { - base: SceneEntityBase { + base: SceneEntity { name: name.to_string(), loc, parameters, @@ -641,7 +1079,7 @@ impl ParserTarget for BasicSceneBuilder { let parameters = ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); self.verify_options("PixelFilter", &loc); - self.current_filter = Some(SceneEntityBase { + self.current_filter = Some(SceneEntity { name: name.to_string(), loc, parameters, @@ -652,7 +1090,7 @@ impl ParserTarget for BasicSceneBuilder { let parameters = ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); self.verify_options("Film", &loc); - self.current_filter = Some(SceneEntityBase { + self.current_filter = Some(SceneEntity { name: type_name.to_string(), loc, parameters, @@ -663,7 +1101,7 @@ impl ParserTarget for BasicSceneBuilder { let parameters = ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); self.verify_options("PixelFilter", &loc); - self.current_filter = Some(SceneEntityBase { + self.current_filter = Some(SceneEntity { name: name.to_string(), loc, parameters, @@ -674,7 +1112,7 @@ impl ParserTarget for BasicSceneBuilder { let parameters = ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); self.verify_options("PixelFilter", &loc); - self.current_filter = Some(SceneEntityBase { + self.current_filter = Some(SceneEntity { name: name.to_string(), loc, parameters, @@ -693,7 +1131,7 @@ impl ParserTarget for BasicSceneBuilder { let parameters = ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); - let entity = SceneEntityBase { + let entity = SceneEntity { name: name.to_string(), loc, parameters, @@ -717,12 +1155,13 @@ impl ParserTarget for BasicSceneBuilder { let parameters = ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); self.verify_options("Sampler", &loc); - self.current_sampler = Some(SceneEntityBase { + self.current_sampler = Some(SceneEntity { name: name.to_string(), loc, parameters, }) } + fn world_begin(&mut self, loc: FileLoc) { self.verify_options("WorldBegin", &loc); self.current_block = BlockState::WorldBlock; @@ -820,13 +1259,60 @@ impl ParserTarget for BasicSceneBuilder { fn texture( &mut self, - _name: &str, - _type_name: &str, - _tex_name: &str, - _params: &ParsedParameterVector, + orig_name: &str, + type_name: &str, + tex_name: &str, + params: &ParsedParameterVector, loc: FileLoc, ) { + let name = normalize_utf8(orig_name); self.verify_world("Texture", &loc); + let dict = ParameterDictionary::from_array( + params.clone(), + &self.graphics_state.texture_attributes, + self.graphics_state.color_space.clone(), + ); + + if type_name != "float" && type_name != "spectrum" { + self.error_exit_deferred( + &loc, + &format!( + "{}: texture type unknown. Must be \"float\" or \"spectrum\".", + tex_type + ), + ); + return; + } + + { + let names = if type_name == "float" { + &mut self.float_texture_names + } else { + &mut self.spectrum_texture_names + }; + + if names.contains(name) { + self.error_exit_deferred(&loc, &format!("Redefining texture \"{}\".", name)); + return; + } + names.insert(name.to_string()); + } + + let base = SceneEntity { + name: tex_name, + parameters: dict, + loc, + }; + let entity = TextureSceneEntity { + base, + render_from_object: self.graphics_state.render_from_object.clone(), + }; + + if type_name == "float" { + self.scene.add_float_texture(name.to_string(), entity); + } else { + self.scene.add_spectrum_texture(name.to_string(), entity); + } } fn material(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) { diff --git a/src/core/shape.rs b/src/core/shape.rs new file mode 100644 index 0000000..4b0a3b1 --- /dev/null +++ b/src/core/shape.rs @@ -0,0 +1,104 @@ +use crate::core::texture::FloatTexture; +use crate::shapes::TriQuadMesh; +use crate::utils::{FileLoc, ParameterDictionary, resolve_filename}; +use shared::core::options::get_options; +use shared::core::shape::*; +use shared::shapes::*; +use shared::spectra::*; +use shared::utils::Transform; +use shared::utils::mesh::{BilinearPatchMesh, TriangleMesh}; +use std::sync::{Arc, Mutex}; + +pub static ALL_TRIANGLE_MESHES: Mutex>> = Mutex::new(Vec::new()); +pub static ALL_TRIANGLE_MESHES: Mutex>> = Mutex::new(Vec::new()); + +pub trait CreateShape { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Vec; +} + +pub trait ShapeFactory { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Vec; +} + +impl ShapeFactory for Shape { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Vec { + match name { + "sphere" => SphereShape::create( + name, + render_from_object, + object_from_render, + reverse_orientation, + parameters, + float_textures, + loc, + ), + "cylinder" => CylinderShape::create( + name, + render_from_object, + object_from_render, + reverse_orientation, + parameters, + float_textures, + loc, + ), + "disk" => DiskShape::create( + name, + render_from_object, + object_from_render, + reverse_orientation, + parameters, + float_textures, + loc, + ), + "bilinearmesh" => BilinearPatchShape::create( + name, + render_from_object, + object_from_render, + reverse_orientation, + parameters, + float_textures, + loc, + ), + "trianglemesh" => TriangleShape::create( + name, + render_from_object, + object_from_render, + reverse_orientation, + parameters, + float_textures, + loc, + ), + "plymesh" => { + let filename = resolve_filename(parameters.get_one_string("filename", "")); + let ply_mesh = TriQuadMesh::read_ply(filename); + let mut edge_length = parameters.get_one_float("edgelength", 1.); + edge_length *= get_options().displacement_edge_scale; + let displacement_tex_name = parameters.get_texture("displacement"); + } + } + } +} diff --git a/src/core/spectrum.rs b/src/core/spectrum.rs index d24e749..4fc51e1 100644 --- a/src/core/spectrum.rs +++ b/src/core/spectrum.rs @@ -1,6 +1,6 @@ use crate::utils::containers::InternCache; -static SPECTRUM_CACHE: Lazy>> = +pub static SPECTRUM_CACHE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); fn get_spectrum_cache() -> &'static InternCache { diff --git a/src/core/texture.rs b/src/core/texture.rs index b4695fa..19e37cb 100644 --- a/src/core/texture.rs +++ b/src/core/texture.rs @@ -23,33 +23,6 @@ pub enum FloatTexture { Wrinkled(WrinkledTexture), } -impl FloatConstantTexture { - pub fn create( - name: &str, - render_from_texture: &Transform, - params: &TextureParameterDictionary, - loc: &FileLoc, - ) -> Result { - Self::new(params.get_one_float("value", 1.0)) - } -} - -impl FloatBilerpTexture { - pub fn create( - name: &str, - render_from_texture: &Transform, - params: &TextureParameterDictionary, - loc: &FileLoc, - ) -> Result { - let mapping = TextureMapping2D::create(params, render_from_texture, loc); - let v00 = params.get_one_float("v00", 0.); - let v01 = params.get_one_float("v01", 1.); - let v10 = params.get_one_float("v10", 0.); - let v11 = params.get_one_float("v11", 1.); - Self::new(mapping, v00, v01, v10, v11) - } -} - impl FloatTexture { pub fn create( name: &str, @@ -165,84 +138,11 @@ impl TextureMapping2D { } } -static TEXTURE_CACHE: OnceLock>>> = OnceLock::new(); +pub static TEXTURE_CACHE: OnceLock>>> = OnceLock::new(); -fn get_texture_cache() -> &'static Mutex>> { +pub fn get_texture_cache() -> &'static Mutex>> { TEXTURE_CACHE.get_or_init(|| Mutex::new(HashMap::new())) } - -#[derive(Clone, Debug)] -pub struct ImageTextureBase { - pub mapping: TextureMapping2D, - pub filename: String, - pub scale: Float, - pub invert: bool, - pub mipmap: Arc, -} - -impl ImageTextureBase { - pub fn new( - mapping: TextureMapping2D, - filename: String, - filter_options: MIPMapFilterOptions, - wrap_mode: WrapMode, - scale: Float, - invert: bool, - encoding: ColorEncoding, - ) -> Self { - let tex_info = TexInfo { - filename: filename.clone(), - filter_options, - wrap_mode, - encoding, - }; - - let cache_mutex = get_texture_cache(); - - { - let cache = cache_mutex.lock().unwrap(); - if let Some(mipmap) = cache.get(&tex_info) { - return Self { - mapping, - filename, - scale, - invert, - mipmap: mipmap.clone(), - }; - } - } - - let path = Path::new(&filename); - let mipmap_raw = MIPMap::create_from_file(path, filter_options, wrap_mode, encoding) - .expect("Failed to create MIPMap from file"); - - let mipmap_arc = Arc::new(mipmap_raw); - - { - let mut cache = cache_mutex.lock().unwrap(); - - let stored_mipmap = cache.entry(tex_info).or_insert(mipmap_arc); - - Self { - mapping, - filename, - scale, - invert, - mipmap: stored_mipmap.clone(), - } - } - } - - pub fn clear_cache() { - let mut cache = get_texture_cache().lock().unwrap(); - cache.clear(); - } - - pub fn multiply_scale(&mut self, s: Float) { - self.scale *= s; - } -} - #[derive(Debug, Hash, PartialEq, Eq, Clone)] struct TexInfo { filename: String, diff --git a/src/globals.rs b/src/globals.rs new file mode 100644 index 0000000..52764e9 --- /dev/null +++ b/src/globals.rs @@ -0,0 +1,74 @@ +use crate::Float; +use crate::core::color::RGBToSpectrumTableData; +use bytemuck::cast_slice; +use once_cell::sync::Lazy; + +static SRGB_SCALE_BYTES: &[u8] = include_bytes!("../../data/srgb_scale.dat"); +static SRGB_COEFFS_BYTES: &[u8] = include_bytes!("../../data/srgb_coeffs.dat"); + +pub static SRGB_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(SRGB_SCALE_BYTES)); + +pub static SRGB_COEFFS: Lazy<&[Float]> = + Lazy::new(|| match bytemuck::try_cast_slice(SRGB_COEFFS_BYTES) { + Ok(s) => s, + Err(_) => { + let v: Vec = bytemuck::pod_collect_to_vec(SRGB_COEFFS_BYTES); + Box::leak(v.into_boxed_slice()) + } + }); + +static DCI_P3_SCALE_BYTES: &[u8] = include_bytes!("../../data/dcip3_scale.dat"); +static DCI_P3_COEFFS_BYTES: &[u8] = include_bytes!("../../data/dcip3_coeffs.dat"); +pub static DCI_P3_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(DCI_P3_SCALE_BYTES)); +pub static DCI_P3_COEFFS: Lazy<&[Float]> = + Lazy::new(|| match bytemuck::try_cast_slice(DCI_P3_COEFFS_BYTES) { + Ok(s) => s, + Err(_) => { + let v: Vec = bytemuck::pod_collect_to_vec(DCI_P3_COEFFS_BYTES); + Box::leak(v.into_boxed_slice()) + } + }); + +static ACES_SCALE_BYTES: &[u8] = include_bytes!("../../data/aces_scale.dat"); +static ACES_COEFFS_BYTES: &[u8] = include_bytes!("../../data/aces_coeffs.dat"); + +pub static ACES_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(ACES_SCALE_BYTES)); + +pub static ACES_COEFFS: Lazy<&[Float]> = + Lazy::new(|| match bytemuck::try_cast_slice(ACES_COEFFS_BYTES) { + Ok(s) => s, + Err(_) => { + let v: Vec = bytemuck::pod_collect_to_vec(ACES_COEFFS_BYTES); + Box::leak(v.into_boxed_slice()) + } + }); + +static REC2020_SCALE_BYTES: &[u8] = include_bytes!("../../data/rec2020_scale.dat"); +static REC2020_COEFFS_BYTES: &[u8] = include_bytes!("../../data/rec2020_coeffs.dat"); + +pub static REC2020_SCALE: Lazy<&[Float]> = Lazy::new(|| cast_slice(REC2020_SCALE_BYTES)); +pub static REC2020_COEFFS: Lazy<&[Float]> = + Lazy::new(|| match bytemuck::try_cast_slice(REC2020_COEFFS_BYTES) { + Ok(s) => s, + Err(_) => { + let v: Vec = bytemuck::pod_collect_to_vec(REC2020_COEFFS_BYTES); + Box::leak(v.into_boxed_slice()) + } + }); + +pub static SRGB_TABLE: Lazy = + Lazy::new(|| RGBToSpectrumTableData::new(SRGB_SCALE, SRGB_COEFFS)); + +pub static DCI_P3_TABLE: Lazy = + Lazy::new(|| RGBToSpectrumTableData::new(DCI_P3_SCALE, DCI_P3_COEFFS)); + +pub static REC2020_TABLE: Lazy = + Lazy::new(|| RGBToSpectrumTableData::new(REC2020_SCALE, REC2020_COEFFS)); + +pub static ACES_TABLE: Lazy = + Lazy::new(|| RGBToSpectrumTableData::new(ACES_SCALE, ACES_COEFFS)); + +// pub static ACES_TABLE: Lazy = Lazy::new(|| { +// RGBToSpectrumTableData::load(Path::new("data/"), "aces2065_1") +// .expect("Failed to load ACES table") +// }); diff --git a/shared/src/integrators/mod.rs b/src/integrators/mod.rs similarity index 95% rename from shared/src/integrators/mod.rs rename to src/integrators/mod.rs index ffe9087..0c9797f 100644 --- a/shared/src/integrators/mod.rs +++ b/src/integrators/mod.rs @@ -2,32 +2,31 @@ mod pipeline; use pipeline::*; -use crate::core::bssrdf::{BSSRDFTrait, SubsurfaceInteraction}; -use crate::core::bxdf::{BSDF, BxDFFlags, BxDFReflTransFlags, BxDFTrait, FArgs, TransportMode}; -use crate::core::camera::Camera; -use crate::core::film::VisibleSurface; -use crate::core::geometry::{Bounds2i, Point2f, Point2i, Point3fi, Ray, Vector3f, VectorLike}; -use crate::core::interaction::{ - self, Interaction, InteractionTrait, MediumInteraction, SimpleInteraction, SurfaceInteraction, +use shared::core::bssrdf::{BSSRDFTrait, SubsurfaceInteraction}; +use shared::core::bxdf::{BSDF, BxDFFlags, BxDFReflTransFlags, BxDFTrait, FArgs, TransportMode}; +use shared::core::camera::Camera; +use shared::core::film::VisibleSurface; +use shared::core::geometry::{Bounds2i, Point2f, Point2i, Point3fi, Ray, Vector3f, VectorLike}; +use shared::core::interaction::{ + Interaction, InteractionTrait, MediumInteraction, SimpleInteraction, SurfaceInteraction, }; -use crate::core::medium::{MediumTrait, PhaseFunctionTrait}; -use crate::core::options::get_options; -use crate::core::pbrt::{Float, SHADOW_EPSILON}; -use crate::core::primitive::{Primitive, PrimitiveTrait}; -use crate::core::sampler::{CameraSample, Sampler, SamplerTrait}; -use crate::lights::sampler::LightSamplerTrait; -use crate::lights::sampler::{LightSampler, UniformLightSampler}; -use crate::shapes::ShapeIntersection; -use crate::spectra::{SampledSpectrum, SampledWavelengths}; -use crate::utils::hash::{hash_buffer, mix_bits}; -use crate::utils::math::{float_to_bits, sample_discrete, square}; -use crate::utils::rng::Rng; -use crate::utils::sampling::{ +use shared::core::light::{Light, LightSampleContext}; +use shared::core::medium::{MediumTrait, PhaseFunctionTrait}; +use shared::core::options::get_options; +use shared::core::pbrt::{Float, SHADOW_EPSILON}; +use shared::core::primitive::{Primitive, PrimitiveTrait}; +use shared::core::sampler::{CameraSample, Sampler, SamplerTrait}; +use shared::lights::sampler::LightSamplerTrait; +use shared::lights::sampler::{LightSampler, UniformLightSampler}; +use shared::shapes::ShapeIntersection; +use shared::spectra::{SampledSpectrum, SampledWavelengths}; +use shared::utils::hash::{hash_buffer, mix_bits}; +use shared::utils::math::{float_to_bits, sample_discrete, square}; +use shared::utils::rng::Rng; +use shared::utils::sampling::{ WeightedReservoirSampler, power_heuristic, sample_uniform_hemisphere, sample_uniform_sphere, uniform_hemisphere_pdf, uniform_sphere_pdf, }; -use bumpalo::Bump; -use rayon::prelude::*; use std::sync::Arc; @@ -70,21 +69,14 @@ pub trait IntegratorTrait { fn render(&self); } -pub trait RayIntegratorTrait: Send + Sync + std::fmt::Debug { - fn evaluate_pixel_sample( - &self, - p_pixel: Point2i, - sample_ind: usize, - sampler: &mut Sampler, - scratch: &Bump, - ); +pub trait RayIntegratorTrait { + fn evaluate_pixel_sample(&self, p_pixel: Point2i, sample_ind: usize, sampler: &mut Sampler); fn li( &self, ray: Ray, lambda: &SampledWavelengths, sampler: &mut Sampler, - scratch: &Bump, visible_surface: bool, ) -> (SampledSpectrum, Option); } @@ -106,6 +98,9 @@ pub struct SimplePathIntegrator { max_depth: usize, } +unsafe impl Send for SimplePathIntegrator {} +unsafe impl Sync for SimplePathIntegrator {} + impl SimplePathIntegrator { fn sample_direct_lighting( &self, @@ -206,21 +201,8 @@ impl SimplePathIntegrator { } impl RayIntegratorTrait for SimplePathIntegrator { - fn evaluate_pixel_sample( - &self, - p_pixel: Point2i, - sample_ind: usize, - sampler: &mut Sampler, - scratch: &Bump, - ) { - pipeline::evaluate_pixel_sample( - self, - self.camera.as_ref(), - sampler, - p_pixel, - sample_ind, - scratch, - ); + fn evaluate_pixel_sample(&self, p_pixel: Point2i, sample_ind: usize, sampler: &mut Sampler) { + pipeline::evaluate_pixel_sample(self, self.camera.as_ref(), sampler, p_pixel, sample_ind); } fn li( @@ -228,7 +210,6 @@ impl RayIntegratorTrait for SimplePathIntegrator { mut ray: Ray, lambda: &SampledWavelengths, sampler: &mut Sampler, - _scratch: &Bump, _visible_surface: bool, ) -> (SampledSpectrum, Option) { let mut l = SampledSpectrum::new(0.0); @@ -296,6 +277,9 @@ pub struct PathIntegrator { max_depth: usize, } +unsafe impl Send for PathIntegrator {} +unsafe impl Sync for PathIntegrator {} + impl PathIntegrator { fn sample_ld( &self, diff --git a/shared/src/integrators/pipeline.rs b/src/integrators/pipeline.rs similarity index 89% rename from shared/src/integrators/pipeline.rs rename to src/integrators/pipeline.rs index 174ceaf..4b6490d 100644 --- a/shared/src/integrators/pipeline.rs +++ b/src/integrators/pipeline.rs @@ -1,5 +1,5 @@ +use crate::core::image::Image; use crate::core::{options::PBRTOptions, sampler::get_camera_sample}; -use crate::image::{Image, ImageMetadata}; use indicatif::{ProgressBar, ProgressStyle}; use std::io::Write; use std::path::Path; @@ -72,19 +72,11 @@ pub fn render( let options = get_options(); if let Some((p_pixel, sample_index)) = options.debug_start { let s_index = sample_index as usize; - let scratch = Bump::new(); let mut tile_sampler = sampler_prototype.clone(); tile_sampler.start_pixel_sample(p_pixel, s_index, None); - evaluate_pixel_sample( - integrator, - camera, - &mut tile_sampler, - p_pixel, - s_index, - &scratch, - ); + evaluate_pixel_sample(integrator, camera, &mut tile_sampler, p_pixel, s_index); return; } @@ -145,23 +137,12 @@ pub fn render( let tiles = generate_tiles(pixel_bounds); while wave_start < spp { tiles.par_iter().for_each(|tile_bounds| { - let mut arena = Bump::with_capacity(65 * 1024); let mut sampler = sampler_prototype.clone(); for p_pixel in tile_bounds { for sample_index in wave_start..wave_end { sampler.start_pixel_sample(*p_pixel, sample_index, None); - - evaluate_pixel_sample( - integrator, - camera, - &mut sampler, - *p_pixel, - sample_index, - &arena, - ); - - arena.reset(); + evaluate_pixel_sample(integrator, camera, &mut sampler, *p_pixel, sample_index); } } @@ -221,7 +202,6 @@ pub fn evaluate_pixel_sample( sampler: &mut Sampler, pixel: Point2i, _sample_index: usize, - scratch: &Bump, ) { let mut lu = sampler.get1d(); if get_options().disable_wavelength_jitter { @@ -240,13 +220,8 @@ pub fn evaluate_pixel_sample( } let initialize_visible_surface = film.uses_visible_surface(); - let (mut l, visible_surface) = integrator.li( - camera_ray.ray, - &lambda, - sampler, - scratch, - initialize_visible_surface, - ); + let (mut l, visible_surface) = + integrator.li(camera_ray.ray, &lambda, sampler, initialize_visible_surface); l *= camera_ray.weight; if l.has_nans() || l.y(&lambda).is_infinite() { diff --git a/src/lib.rs b/src/lib.rs index 4f9bee8..e2db169 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,10 @@ pub mod core; +pub mod globals; +pub mod integrators; pub mod lights; +pub mod materials; +pub mod samplers; +pub mod shapes; pub mod spectra; pub mod textures; pub mod utils; diff --git a/src/lights/diffuse.rs b/src/lights/diffuse.rs index ebc2cf2..6463eb5 100644 --- a/src/lights/diffuse.rs +++ b/src/lights/diffuse.rs @@ -4,37 +4,41 @@ use crate::core::texture::{ }; 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, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, - LightType, RGB, RGBColorSpace, RGBIlluminantSpectrum, SampledSpectrum, SampledWavelengths, - Spectrum, SpectrumTrait, + DenselySampledSpectrum, RGBColorSpace, RGBIlluminantSpectrum, SampledSpectrum, + SampledWavelengths, }; use shared::{Float, PI}; - use std::sync::Arc; #[derive(Clone, Debug)] -pub struct DiffuseAreaLight { - base: LightBase, - shape: Shape, - alpha: Option, - area: Float, - two_sided: bool, - lemit: Arc, - scale: Float, - image: Option, +pub struct DiffuseAreaLightStorage { + shape: Arc, + alpha: Option>, + image: Option>, image_color_space: Option>, } -impl DiffuseAreaLight { +#[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, @@ -87,167 +91,22 @@ impl DiffuseAreaLight { ); } - Self { - base, - area: shape.area(), + let storage = DiffuseAreaLightStorage { shape, alpha: stored_alpha, - two_sided, - lemit, - scale, image, image_color_space, - } - } - - fn alpha_masked(&self, intr: &Interaction) -> bool { - let Some(alpha_tex) = &self.alpha else { - return false; }; - let ctx = TextureEvalContext::from(intr); - let a = UniversalTextureEvaluator.evaluate_float(alpha_tex, &ctx); - if a >= 1.0 { - return false; + + let view = DiffuseAreaLight { + base, + area: shape.area(), + shape: Ptr::from(&*storage.shape), + }; + + Self { + view, + _storage: storage, } - if a <= 0.0 { - return true; - } - hash_float(&intr.p()) > a - } -} - -impl LightTrait for DiffuseAreaLight { - fn base(&self) -> &LightBase { - &self.base - } - - fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { - let mut l = SampledSpectrum::new(0.); - if let Some(image) = &self.image { - for y in 0..image.resolution().y() { - for x in 0..image.resolution().x() { - let mut rgb = RGB::default(); - for c in 0..3 { - rgb[c] = image.get_channel(Point2i::new(x, y), c); - } - l += RGBIlluminantSpectrum::new( - self.image_color_space.as_ref().unwrap(), - RGB::clamp_zero(rgb), - ) - .sample(&lambda); - } - } - l *= self.scale / (image.resolution().x() * image.resolution().y()) as Float; - } else { - l = self.lemit.sample(&lambda) * self.scale; - } - let two_side = if self.two_sided { 2. } else { 1. }; - PI * two_side * self.area * l - } - - fn sample_li( - &self, - ctx: &LightSampleContext, - u: Point2f, - lambda: &SampledWavelengths, - _allow_incomplete_pdf: bool, - ) -> Option { - let shape_ctx = ShapeSampleContext::new(ctx.pi, ctx.n, ctx.ns, 0.0); - let ss = self.shape.sample_from_context(&shape_ctx, u)?; - let mut intr: SurfaceInteraction = ss.intr.as_ref().clone(); - - intr.common.medium_interface = Some(self.base.medium_interface.clone()); - let p = intr.p(); - let n = intr.n(); - let uv = intr.uv; - - let generic_intr = Interaction::Surface(intr); - if self.alpha_masked(&generic_intr) { - return None; - } - - let wi = (p - ctx.p()).normalize(); - let le = self.l(p, n, uv, -wi, lambda); - - if le.is_black() { - return None; - } - - Some(LightLiSample::new(le, wi, ss.pdf, generic_intr)) - } - - fn pdf_li(&self, ctx: &LightSampleContext, wi: Vector3f, _allow_incomplete_pdf: bool) -> Float { - let shape_ctx = ShapeSampleContext::new(ctx.pi, ctx.n, ctx.ns, 0.); - self.shape.pdf_from_context(&shape_ctx, wi) - } - - fn l( - &self, - p: Point3f, - n: Normal3f, - mut uv: Point2f, - w: Vector3f, - lambda: &SampledWavelengths, - ) -> SampledSpectrum { - if self.two_sided && n.dot(w.into()) < 0. { - return SampledSpectrum::new(0.); - } - let intr = Interaction::Surface(SurfaceInteraction::new_minimal( - Point3fi::new_from_point(p), - uv, - )); - - if self.alpha_masked(&intr) { - return SampledSpectrum::new(0.); - } - if let Some(image) = &self.image { - let mut rgb = RGB::default(); - uv[1] = 1. - uv[1]; - for c in 0..3 { - rgb[c] = image.bilerp_channel(uv, c); - } - - let spec = RGBIlluminantSpectrum::new( - self.image_color_space.as_ref().unwrap(), - RGB::clamp_zero(rgb), - ); - - self.scale * spec.sample(lambda) - } else { - self.scale * self.lemit.sample(lambda) - } - } - - fn le(&self, _ray: &Ray, _lambda: &SampledWavelengths) -> SampledSpectrum { - todo!() - } - - fn preprocess(&mut self, _scene_bounds: &Bounds3f) { - unimplemented!() - } - - fn bounds(&self) -> Option { - let mut phi = 0.; - if let Some(image) = &self.image { - for y in 0..image.resolution.y() { - for x in 0..image.resolution.x() { - for c in 0..3 { - phi += image.get_channel(Point2i::new(x, y), c); - } - } - } - } else { - phi = self.lemit.max_value(); - } - - let nb = self.shape.normal_bounds(); - Some(LightBounds::new( - &self.shape.bounds(), - nb.w, - phi, - nb.cos_theta, - (PI / 2.).cos(), - self.two_sided, - )) } } diff --git a/src/lights/infinite.rs b/src/lights/infinite.rs new file mode 100644 index 0000000..0d13edd --- /dev/null +++ b/src/lights/infinite.rs @@ -0,0 +1,269 @@ +use shared::Float; +use shared::core::geometry::{Bounds3f, Point2f}; +use shared::core::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 std::sync::Arc; + +#[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( + render_from_light: Transform, + image: Image, + image_color_space: RGBColorSpace, + scale: Float, + filename: String, + ) -> Self { + let base = LightBase::new( + LightType::Infinite, + &render_from_light, + &MediumInterface::default(), + ); + + let desc = image + .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()); + if image.resolution().x() != image.resolution().y() { + panic!( + "{}: image resolution ({}, {}) is non-square. It's unlikely this is an equal area environment map.", + filename, + image.resolution.x(), + image.resolution.y() + ); + } + 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 slice = &mut d.values; // or d.as_slice_mut() + let count = slice.len() as Float; + let sum: Float = slice.iter().sum(); + let average = sum / count; + + for v in slice.iter_mut() { + *v = (*v - average).max(0.0); + } + + let all_zero = slice.iter().all(|&v| v == 0.0); + if all_zero { + for v in slice.iter_mut() { + *v = 1.0; + } + } + + let compensated_distrib = PiecewiseConstant2D::new_with_bounds(&d, domain); + + let storage = Arc::new(InfiniteImageLightStorage { + image, + distrib, + compensated_distrib, + image_color_space, + }); + + let view = InfiniteImageLight { + base, + image: &storage.image, + image_color_space: &storage.image_color_space, + scene_center: Point3f::default(), + scene_radius: 0., + scale, + distrib: &storage.distrib, + compensated_distrib: &storage.compensated_distrib, + }; + + Self { + view, + filename, + _storage: storage, + } + } +} + +#[derive(Debug)] +struct InfinitePortalLightStorage { + image: Image, + distribution: WindowedPiecewiseConstant2D, + image_color_space: RGBColorSpace, +} + +#[derive(Clone, Debug)] +pub struct InfinitePortalLightHost { + pub view: InfinitePortalLight, + pub filename: String, + _storage: Arc, +} + +impl std::ops::Deref for InfinitePortalLightHost { + type Target = InfinitePortalLight; + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl InfinitePortalLightHost { + pub fn new( + render_from_light: Transform, + equal_area_image: &Image, + image_color_space: RGBColorSpace, + scale: Float, + filename: String, + points: Vec, + ) -> Self { + let base = LightBase::new( + LightType::Infinite, + &render_from_light, + &MediumInterface::default(), + ); + + let desc = equal_area_image + .get_channel_desc(&["R", "G", "B"]) + .unwrap_or_else(|_| { + panic!( + "{}: image used for PortalImageInfiniteLight doesn't have R, G, B channels.", + filename + ) + }); + + assert_eq!(3, desc.offset.len()); + let src_res = equal_area_image.resolution; + if src_res.x() != src_res.y() { + panic!( + "{}: image resolution ({}, {}) is non-square. It's unlikely this is an equal area environment map.", + filename, + src_res.x(), + src_res.y() + ); + } + + if points.len() != 4 { + panic!( + "Expected 4 vertices for infinite light portal but given {}", + points.len() + ); + } + + let portal: [Point3f; 4] = [points[0], points[1], points[2], points[3]]; + + let p01 = (portal[1] - portal[0]).normalize(); + let p12 = (portal[2] - portal[1]).normalize(); + let p32 = (portal[2] - portal[3]).normalize(); + let p03 = (portal[3] - portal[0]).normalize(); + + if (p01.dot(p32) - 1.0).abs() > 0.001 || (p12.dot(p03) - 1.0).abs() > 0.001 { + panic!("Infinite light portal isn't a planar quadrilateral (opposite edges)"); + } + + if p01.dot(p12).abs() > 0.001 + || p12.dot(p32).abs() > 0.001 + || p32.dot(p03).abs() > 0.001 + || p03.dot(p01).abs() > 0.001 + { + panic!("Infinite light portal isn't a planar quadrilateral (perpendicular edges)"); + } + + let portal_frame = Frame::from_xy(p03, p01); + + let width = src_res.x(); + let height = src_res.y(); + + let mut new_pixels = vec![0.0 as Float; (width * height * 3) as usize]; + + new_pixels + .par_chunks_mut((width * 3) as usize) + .enumerate() + .for_each(|(y, row_pixels)| { + let y = y as i32; + + for x in 0..width { + let uv = Point2f::new( + (x as Float + 0.5) / width as Float, + (y as Float + 0.5) / height as Float, + ); + + let (w_world, _) = Self::render_from_image(portal_frame, uv); + let w_local = render_from_light.apply_inverse_vector(w_world).normalize(); + let uv_equi = equal_area_sphere_to_square(w_local); + + let pixel_idx = (x * 3) as usize; + + for c in 0..3 { + let val = equal_area_image.bilerp_channel_with_wrap( + uv_equi, + c, + WrapMode::OctahedralSphere.into(), + ); + row_pixels[pixel_idx + c] = val; + } + } + }); + + let image = Image::new( + PixelFormat::F32, + src_res, + &["R", "G", "B"], + equal_area_image.encoding, + ); + + let duv_dw_closure = |p: Point2f| -> Float { + let (_, jacobian) = Self::render_from_image(portal_frame, p); + jacobian + }; + + let d = image.get_sampling_distribution( + duv_dw_closure, + Bounds2f::from_points(Point2f::new(0., 0.), Point2f::new(1., 1.)), + ); + + let distribution = WindowedPiecewiseConstant2D::new(d); + + let storage = Arc::new(InfinitePortalLightStorage { + image, + distribution, + image_color_space, + }); + + let view = InfinitePortalLight { + base, + image: &storage.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, + } + } +} diff --git a/src/lights/mod.rs b/src/lights/mod.rs index ac6f588..35b7f5a 100644 --- a/src/lights/mod.rs +++ b/src/lights/mod.rs @@ -1 +1,4 @@ pub mod diffuse; +pub mod infinite; +pub mod projection; +pub mod sampler; diff --git a/src/lights/projection.rs b/src/lights/projection.rs new file mode 100644 index 0000000..b592990 --- /dev/null +++ b/src/lights/projection.rs @@ -0,0 +1,79 @@ +use crate::core::image::ImageBuffer; +use shared::core::light::LightBase; +use shared::lights::ProjectionLight; + +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( + render_from_light: Transform, + medium_interface: MediumInterface, + image: ImageBuffer, + image_color_space: RGBColorSpace, + scale: Float, + fov: Float, + ) -> Self { + let base = LightBase::new( + LightType::DeltaPosition, + render_from_light, + medium_interface, + ); + let aspect = image.resolution().x() as Float / image.resolution().y() as Float; + let screen_bounds = if aspect > 1. { + Bounds2f::from_points(Point2f::new(-aspect, -1.), Point2f::new(aspect, 1.)) + } else { + Bounds2f::from_points( + Point2f::new(-1., 1. / aspect), + Point2f::new(1., 1. / aspect), + ) + }; + + let hither = 1e-3; + let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap(); + let light_from_screen = screen_from_light.inverse(); + let opposite = (radians(fov) / 2.).tan(); + let aspect_ratio = if aspect > 1. { aspect } else { 1. / aspect }; + let a = 4. * square(opposite) * aspect_ratio; + let dwda = |p: Point2f| { + let w = + Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.))); + cos_theta(w.normalize()).powi(3) + }; + + let d = image.get_sampling_distribution(dwda, screen_bounds); + let distrib = PiecewiseConstant2D::new_with_bounds(&d, screen_bounds); + + let storage = ProjectionLightStorage { + 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, + } + } +} diff --git a/src/lights/sampler.rs b/src/lights/sampler.rs new file mode 100644 index 0000000..8f892b3 --- /dev/null +++ b/src/lights/sampler.rs @@ -0,0 +1,44 @@ +use crate::utils::sampling::AliasTableHost; +use std::collections::HashMap; + +pub struct PowerSamplerHost { + pub lights: Vec, + pub light_to_index: HashMap, + pub alias_table: AliasTableHost, +} + +impl PowerSamplerHost { + pub fn new(lights: &[Arc]) -> Self { + if lights.is_empty() { + return Self { + lights: Vec::new(), + light_to_index: HashMap::new(), + alias_table: AliasTableHost::new(&[]), + }; + } + + let mut lights_vec = Vec::with_capacity(lights.len()); + let mut light_to_index = HashMap::with_capacity(lights.len()); + let mut light_power = Vec::with_capacity(lights.len()); + + let lambda = SampledWavelengths::sample_visible(0.5); + + for (i, light) in lights.iter().enumerate() { + lights_vec.push(light.clone()); + + let ptr = Arc::as_ptr(light) as usize; + light_to_index.insert(ptr, i); + + let phi = SampledSpectrum::safe_div(&light.phi(lambda), &lambda.pdf()); + light_power.push(phi.average()); + } + + let alias_table = AliasTable::new(&light_power); + + Self { + lights: lights_vec, + light_to_index, + alias_table, + } + } +} diff --git a/src/materials/coated.rs b/src/materials/coated.rs new file mode 100644 index 0000000..ee3a345 --- /dev/null +++ b/src/materials/coated.rs @@ -0,0 +1,128 @@ +use crate::core::image::ImageBuffer; +use crate::core::material::CreateMaterial; +use crate::utils::parameters::ParameterDictionary; +use shared::core::spectrum::Spectrum; +use shared::core::texture::SpectrumType; +use shared::materials::coated::*; +use shared::spectra::ConstantSpectrum; +use shared::textures::SpectrumConstantTexture; + +impl CreateMaterial for CoatedDiffuseMaterial { + fn create( + parameters: &TextureParameterDictionary, + normal_map: Option>, + named_materials: &HashMap, + loc: &FileLoc, + ) -> Self { + let reflectance = parameters + .get_spectrum_texture("reflectance", None, SpectrumType::Albedo) + .unwrap_or(SpectrumConstantTexture::new(Spectrum::Constant( + ConstantSpectrum::new(0.5), + ))); + + let u_roughness = parameters + .get_float_texture_or_null("uroughness") + .unwrap_or_else(|| parameters.get_float_texture("roughness", 0.5)); + let v_roughness = parameters + .get_float_texture_or_null("vroughness") + .unwap_or_else(|| parameters.get_float("roughness", 0.5)); + + let thickness = parameters.get_float_texture("thickness", 0.01); + let eta = parameters + .get_float_array("eta") + .first() + .map(|&v| ConstantSpectrum::new(v)) + .or_else(|| parameters.get_one_spectrum("eta", None, SpectrumType::Unbounded)) + .unwrap_or_else(|| ConstantSpectrum::new(1.5)); + + let max_depth = parameters.get_one_int("maxdepth", 10); + let n_samples = parameters.get_one_int("nsamples", 1); + let g = parameters.get_float_texture("g", 0.); + let albedo = parameters + .get_spectrum_texture("albedo", None, SpectrumType::Albedo) + .unwrap_or_else(|| { + SpectrumConstantTexture::new(Spectrum::Constant(ConstantSpectrum::new(0.))) + }); + let displacement = parameters.get_float_texture("displacement"); + let remap_roughness = parameters.get_one_bool("remaproughness", true); + } +} + +impl CreateMaterial for CoatedConductorMaterial { + fn create( + parameters: &TextureParameterDictionary, + normal_map: Option>, + named_materials: &HashMap, + loc: &FileLoc, + ) -> Result { + let interface_u_roughness = parameters + .get_float_texture_or_null("interface.uroughness") + .unwrap_or_else(|| parameters.get_float_texture("interface.roughness", 0.)); + let interface_v_roughness = parameters + .GetFloatTextureOrNull("interface.vroughness") + .unwrap_or_else(|| parameters.get_float_texture("interface.vroughness", 0.)); + let thickness = parameters.get_float_texture("interface.thickness", 0.01); + let interface_eta = parameters + .get_float_array("interface.eta") + .first() + .map(|&v| ConstantSpectrum::new(v)) + .or_else(|| parameters.get_one_spectrum("interface.eta", None, SpectrumType::Unbounded)) + .unwrap_or_else(|| ConstantSpectrum::new(1.5)); + let conductor_u_roughness = parameters + .get_float_texture_or_null("conductor.uroughness") + .unwrap_or_else(|| parameters.get_float_texture("conductor.roughness", 0.)); + let conductor_v_roughness = parameters + .GetFloatTextureOrNull("conductor.vroughness") + .unwrap_or_else(|| parameters.get_float_texture("conductor.vroughness", 0.)); + let reflectance = + parameters.get_spectrum_texture_or_null("reflectance", SpectrumType::Albedo); + let conductor_eta = + parameters.get_spectrum_texture_or_null("conductor.eta", SpectrumType::Unbounded); + let k = parameters.get_spectrum_texture_or_null("conductor.k", SpectrumType::Unbounded); + let (conductor_eta, k) = match (reflectance, conductor_eta, k) { + (Some(_), Some(_), _) | (Some(_), _, Some(_)) => { + return Err(error_exit( + loc, + "For the coated conductor material, both \"reflectance\" \ + and \"eta\" and \"k\" can't be provided.", + )); + } + (None, eta, k) => ( + eta.unwrap_or_else(|| { + SpectrumConstantTexture::new(get_named_spectrum("metal-Cu-eta")) + }), + k.unwrap_or_else(|| SpectrumConstantTexture::new(get_named_spectrum("metal-Cu-k"))), + ), + (Some(_), None, None) => (conductor_eta, k), + }; + + let max_depth = parameters.get_one_int("maxdepth", 10); + let n_samples = parameters.get_one_int("nsamples", 1); + let g = parameters.get_float_texture("g", 0.); + let albedo = parameters + .get_spectrum_texture("albedo", None, SpectrumType::Albedo) + .unwrap_or_else(|| { + SpectrumConstantTexture::new(Spectrum::Constant(ConstantSpectrum::new(0.))) + }); + let displacement = parameters.get_float_texture_or_null("displacement"); + let remap_roughness = parameters.get_one_bool("remaproughness", true); + Self::new( + displacement, + normal_map, + interface_u_roughness, + interface_v_roughness, + thickness, + interface_eta, + g, + albedo, + conductor_u_roughness, + conductor_v_roughness, + conductor_eta, + k, + reflectance, + remap_roughness, + max_depth, + n_samples, + ) + } +} diff --git a/src/materials/complex.rs b/src/materials/complex.rs new file mode 100644 index 0000000..089aed5 --- /dev/null +++ b/src/materials/complex.rs @@ -0,0 +1,141 @@ +use crate::core::material::CreateMaterial; +use shared::core::bxdf::HairBxDF; +use shared::core::spectrum::Spectrum; +use shared::core::texture::{GPUFloatTexture, GPUSpectrumTexture}; +use shared::materials::complex::*; + +impl CreateMaterial for HairMaterial { + fn create( + parameters: &TextureParameterDictionary, + normal_map: Option>, + named_materials: &HashMap, + loc: &FileLoc, + ) -> Result { + let sigma_a = parameters.get_spectrum_texture_or_null("sigma_a", SpectrumType::Unbounded); + let reflectance = parameters + .get_spectrum_texture_or_null("reflectance", SpectrumType::Albedo) + .or_else(|| parameters.get_spectrum_texture_or_null("color", SpectrumType::Albedo)); + let eumelanin = parameters.get_float_texture_or_null("eumelanin"); + let pheomelanin = parameters.get_float_texture_or_null("pheomelanin"); + let sigma_a = match ( + sigma_a, + reflectance, + eumelanin.is_some() || pheomelanin.is_some(), + ) { + (Some(s), Some(_), _) => { + warn( + loc, + r#"Ignoring "reflectance" parameter since "sigma_a" was provided."#, + ); + Some(s) + } + (Some(s), _, true) => { + warn( + loc, + r#"Ignoring "eumelanin"/"pheomelanin" parameter since "sigma_a" was provided."#, + ); + Some(s) + } + (Some(s), None, false) => Some(s), + + (None, Some(r), true) => { + warn( + loc, + r#"Ignoring "eumelanin"/"pheomelanin" parameter since "reflectance" was provided."#, + ); + Some(r) + } + (None, Some(r), false) => Some(r), + + (None, None, true) => None, // eumelanin/pheomelanin will be used + + (None, None, false) => Some(SpectrumConstantTexture::new(Spectrum::RGBUnbounded( + RGBUnboundedSpectrum::new(HairBxDF::sigma_a_from_concentration(1.3, 0.0)), + ))), + }; + let eta = parameters.get_flot_texture("eta", 1.55); + let beta_m = parameters.get_float_texture("beta_m", 0.3); + let beta_n = parameters.get_float_texture("beta_n", 0.3); + let alpha = parameters.get_float_texture("alpha", 2.); + HairMaterial::new( + sigma_a, + reflectance, + eumelanin, + pheomelanin, + eta, + beta_m, + beta_n, + alpha, + ) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct MeasuredMaterial; +impl MaterialTrait for MeasuredMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + fn get_normal_map(&self) -> *const Image { + todo!() + } + fn get_displacement(&self) -> Ptr { + todo!() + } + fn has_subsurface_scattering(&self) -> bool { + todo!() + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SubsurfaceMaterial; +impl MaterialTrait for SubsurfaceMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + fn get_normal_map(&self) -> *const Image { + todo!() + } + fn get_displacement(&self) -> Ptr { + todo!() + } + fn has_subsurface_scattering(&self) -> bool { + todo!() + } +} diff --git a/src/materials/conductor.rs b/src/materials/conductor.rs new file mode 100644 index 0000000..9de9429 --- /dev/null +++ b/src/materials/conductor.rs @@ -0,0 +1,57 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct ConductorMaterial { + pub displacement: Ptr, + pub eta: Ptr, + pub k: Ptr, + pub reflectance: Ptr, + pub u_roughness: Ptr, + pub v_roughness: Ptr, + pub remap_roughness: bool, + pub normal_map: *const Image, +} + +impl MaterialTrait for ConductorMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + fn get_normal_map(&self) -> *const Image { + todo!() + } + fn get_displacement(&self) -> Ptr { + todo!() + } + fn has_subsurface_scattering(&self) -> bool { + todo!() + } +} diff --git a/src/materials/dielectric.rs b/src/materials/dielectric.rs new file mode 100644 index 0000000..cce0ded --- /dev/null +++ b/src/materials/dielectric.rs @@ -0,0 +1,114 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DielectricMaterial { + normal_map: *const Image, + displacement: Ptr, + u_roughness: Ptr, + v_roughness: Ptr, + remap_roughness: bool, + eta: Ptr, +} + +impl MaterialTrait for DielectricMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + let mut sampled_eta = self.eta.evaluate(lambda[0]); + if !self.eta.is_constant() { + lambda.terminate_secondary(); + } + + if sampled_eta == 0.0 { + sampled_eta = 1.0; + } + + let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx); + let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx); + + if self.remap_roughness { + u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough); + v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough); + } + + let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); + let bxdf = BxDF::Dielectric(DielectricBxDF::new(sampled_eta, distrib)); + + BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[&self.u_roughness, &self.v_roughness], &[]) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> Ptr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct ThinDielectricMaterial; +impl MaterialTrait for ThinDielectricMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + fn get_normal_map(&self) -> *const Image { + todo!() + } + fn get_displacement(&self) -> Ptr { + todo!() + } + fn has_subsurface_scattering(&self) -> bool { + todo!() + } +} diff --git a/src/materials/diffuse.rs b/src/materials/diffuse.rs new file mode 100644 index 0000000..e588d30 --- /dev/null +++ b/src/materials/diffuse.rs @@ -0,0 +1,98 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DiffuseMaterial { + pub normal_map: *const Image, + pub displacement: Ptr, + pub reflectance: Ptr, +} + +impl MaterialTrait for DiffuseMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + let r = tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda); + let bxdf = BxDF::Diffuse(DiffuseBxDF::new(r)); + BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[], &[self.reflectance]) + } + + fn get_normal_map(&self) -> *const Image { + self.normal_map + } + + fn get_displacement(&self) -> Ptr { + self.displacement + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DiffuseTransmissionMaterial; + +impl MaterialTrait for DiffuseTransmissionMaterial { + fn get_bsdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> BSDF { + todo!() + } + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + todo!() + } + + fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { + todo!() + } + + fn get_normal_map(&self) -> Option> { + todo!() + } + + fn get_displacement(&self) -> Option { + todo!() + } + + fn has_subsurface_scattering(&self) -> bool { + todo!() + } +} diff --git a/src/materials/mix.rs b/src/materials/mix.rs new file mode 100644 index 0000000..d17f215 --- /dev/null +++ b/src/materials/mix.rs @@ -0,0 +1,85 @@ +use crate::core::bssrdf::BSSRDF; +use crate::core::bxdf::{ + BSDF, BxDF, CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF, + HairBxDF, +}; +use crate::core::image::Image; +use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::spectrum::{Spectrum, SpectrumTrait}; +use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator}; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::Ptr; +use crate::utils::hash::hash_float; +use crate::utils::math::clamp; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct MixMaterial { + pub amount: Ptr, + pub materials: [Ptr; 2], +} + +impl MixMaterial { + pub fn choose_material( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + ) -> Option<&Material> { + let amt = tex_eval.evaluate_float(&self.amount, ctx); + + let index = if amt <= 0.0 { + 0 + } else if amt >= 1.0 { + 1 + } else { + let u = hash_float(&(ctx.p, ctx.wo)); + if amt < u { 0 } else { 1 } + }; + + self.materials[index].get() + } +} + +impl MaterialTrait for MixMaterial { + fn get_bsdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> BSDF { + if let Some(mat) = self.choose_material(tex_eval, ctx) { + mat.get_bsdf(tex_eval, ctx, lambda) + } else { + BSDF::empty() + } + } + + fn get_bssrdf( + &self, + _tex_eval: &T, + _ctx: &MaterialEvalContext, + _lambda: &SampledWavelengths, + ) -> Option { + None + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + tex_eval.can_evaluate(&[&self.amount], &[]) + } + + fn get_normal_map(&self) -> *const Image { + core::ptr::null() + } + + fn get_displacement(&self) -> Ptr { + panic!( + "MixMaterial::get_displacement() shouldn't be called. \ + Displacement is not supported on Mix materials directly." + ); + } + + fn has_subsurface_scattering(&self) -> bool { + false + } +} diff --git a/src/materials/mod.rs b/src/materials/mod.rs new file mode 100644 index 0000000..af7d13e --- /dev/null +++ b/src/materials/mod.rs @@ -0,0 +1,6 @@ +pub mod coated; +pub mod complex; +pub mod conductor; +pub mod dielectric; +pub mod diffuse; +pub mod mix; diff --git a/src/samplers/halton.rs b/src/samplers/halton.rs new file mode 100644 index 0000000..4180708 --- /dev/null +++ b/src/samplers/halton.rs @@ -0,0 +1,37 @@ +use crate::core::sampler::SamplerFactory; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::geometry::Point2i; +use shared::core::sampler::{HaltonSampler, RandomizeStrategy}; + +impl SamplerFactory for HaltonSampler { + fn create( + params: &ParameterDictionary, + full_res: Point2i, + loc: &FileLoc, + ) -> Result { + let options = get_options(); + let nsamp = options + .quick_render + .then_some(1) + .or(options.pixel_samples) + .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); + let seed = params.get_one_int("seed", options.seed); + let s = match params + .get_one_string("randomization", "permutedigits") + .as_str() + { + "none" => RandomizeStrategy::None, + "permutedigits" => RandomizeStrategy::PermuteDigits, + "fastowen" => RandomizeStrategy::FastOwen, + "owen" => RandomizeStrategy::Owen, + _ => { + return Err(format!( + "{}: Unknown randomization strategy for Halton", + loc + )); + } + }; + + Ok(HaltonSampler::new(nsamp as u32, full_res, s, seed as u64)) + } +} diff --git a/src/samplers/independent.rs b/src/samplers/independent.rs new file mode 100644 index 0000000..3b23cc5 --- /dev/null +++ b/src/samplers/independent.rs @@ -0,0 +1,21 @@ +use crate::core::sampler::SamplerFactory; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::geometry::Point2i; +use shared::core::sampler::IndependentSampler; + +impl SamplerFactory for IndependentSampler { + fn create( + params: &ParameterDictionary, + _full_res: Point2i, + _loc: &FileLoc, + ) -> Result { + let options = get_options(); + let nsamp = options + .quick_render + .then_some(1) + .or(options.pixel_samples) + .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); + let seed = params.get_one_int("seed", options.seed); + Ok(Self::new(nsamp as usize, seed as u64)) + } +} diff --git a/src/samplers/mod.rs b/src/samplers/mod.rs new file mode 100644 index 0000000..7a7b1f7 --- /dev/null +++ b/src/samplers/mod.rs @@ -0,0 +1,4 @@ +pub mod halton; +pub mod independent; +pub mod sobol; +pub mod stratified; diff --git a/src/samplers/sobol.rs b/src/samplers/sobol.rs new file mode 100644 index 0000000..0ebea81 --- /dev/null +++ b/src/samplers/sobol.rs @@ -0,0 +1,96 @@ +use crate::core::sampler::SamplerFactory; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::geometry::Point2i; +use shared::core::sampler::{PaddedSobolSampler, RandomizeStrategy, SobolSampler, ZSobolSampler}; + +impl SamplerFactory for SobolSampler { + fn create( + params: &ParameterDictionary, + full_res: Point2i, + loc: &FileLoc, + ) -> Result { + let options = get_options(); + let nsamp = options + .quick_render + .then_some(1) + .or(options.pixel_samples) + .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); + let seed = params.get_one_int("seed", options.seed); + let s = match params.get_one_string("randomization", "fastowen").as_str() { + "none" => RandomizeStrategy::None, + "permutedigits" => RandomizeStrategy::PermuteDigits, + "fastowen" => RandomizeStrategy::FastOwen, + "owen" => RandomizeStrategy::Owen, + _ => { + return Err(format!("{}: Unknown randomization strategy for Sobol", loc)); + } + }; + + Ok(Self::new(nsamp as usize, full_res, s, Some(seed as u64))) + } +} + +impl SamplerFactory for PaddedSobolSampler { + fn create( + params: &ParameterDictionary, + _full_res: Point2i, + loc: &FileLoc, + ) -> Result { + let options = get_options(); + let nsamp = options + .quick_render + .then_some(1) + .or(options.pixel_samples) + .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); + let seed = params.get_one_int("seed", options.seed); + let s = match params.get_one_string("randomization", "fastowen").as_str() { + "none" => RandomizeStrategy::None, + "permutedigits" => RandomizeStrategy::PermuteDigits, + "fastowen" => RandomizeStrategy::FastOwen, + "owen" => RandomizeStrategy::Owen, + _ => { + return Err(format!( + "{}: Unknown randomization strategy for ZSobol", + loc + )); + } + }; + + Ok(Self::new(nsamp as u32, s, Some(seed as u64))) + } +} + +impl SamplerFactory for ZSobolSampler { + fn create( + params: &ParameterDictionary, + full_res: Point2i, + loc: &FileLoc, + ) -> Result { + let options = get_options(); + let nsamp = options + .quick_render + .then_some(1) + .or(options.pixel_samples) + .unwrap_or_else(|| params.get_one_int("pixelsamples", 16)); + let seed = params.get_one_int("seed", options.seed); + let s = match params.get_one_string("randomization", "fastowen").as_str() { + "none" => RandomizeStrategy::None, + "permutedigits" => RandomizeStrategy::PermuteDigits, + "fastowen" => RandomizeStrategy::FastOwen, + "owen" => RandomizeStrategy::Owen, + _ => { + return Err(format!( + "{}: Unknown randomization strategy for ZSobol", + loc + )); + } + }; + + Ok(ZSobolSampler::new( + nsamp as u32, + full_res, + s, + Some(seed as u64), + )) + } +} diff --git a/src/samplers/stratified.rs b/src/samplers/stratified.rs new file mode 100644 index 0000000..4f992b3 --- /dev/null +++ b/src/samplers/stratified.rs @@ -0,0 +1,35 @@ +use crate::core::sampler::SamplerFactory; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::geometry::{FileLoc, ParameterDictionary}; +use shared::core::sampler::StratifiedSampler; + +impl SamplerFactory for StratifiedSampler { + fn create( + params: &ParameterDictionary, + _full_res: Point2i, + _loc: &FileLoc, + ) -> Result { + let options = get_options(); + let jitter = params.get_one_bool("jitter", true); + let (x_samples, y_samples) = if options.quick_render { + (1, 1) + } else if let Some(n) = options.pixel_samples { + let div = (n as f64).sqrt() as i32; + let y = (1..=div).rev().find(|d| n % d == 0).unwrap(); + + (n / y, y) + } else { + ( + params.get_one_int("xsamples", 4), + params.get_one_int("ysamples", 4), + ) + }; + let seed = params.get_one_int("seed", options.seed); + Ok(Self::new( + x_samples as u32, + y_samples as u32, + Some(seed as u64), + jitter, + )) + } +} diff --git a/src/shapes/bilinear.rs b/src/shapes/bilinear.rs new file mode 100644 index 0000000..3cb71fb --- /dev/null +++ b/src/shapes/bilinear.rs @@ -0,0 +1,133 @@ +use crate::core::shape::{ALL_BILINEAR_MESHES, CreateShape}; +use crate::core::texture::FloatTexture; +use crate::shapes::mesh::BilinearPatchMeshHost; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::shapes::BilinearPatchShape; +use shared::utils::Transform; + +impl CreateShape for BilinearPatchShape { + fn create( + render_from_object: &Transform, + _object_from_render: &Transform, + reverse_orientation: bool, + parameters: &ParameterDictionary, + _float_textures: &HashMap, + _loc: &FileLoc, + ) -> Result { + let mut vertex_indices = parameters.get_int_array("indices"); + let mut p = parameters.get_point3f_array("P"); + let mut uv = parameters.get_point2f_array("uv"); + let mut n = parameters.get_normal3f_array("N"); + let mut face_indices = parameters.get_int_array("faceIndices"); + + if vertex_indices.is_empty() { + if p.len() == 4 { + vertex_indices = vec![0, 1, 2, 3]; + } else { + return Err( + "Vertex indices \"indices\" must be provided with bilinear patch mesh shape." + .into(), + ); + } + } else if vertex_indices.len() % 4 != 0 { + let excess = vertex_indices.len() % 4; + warn!( + "Number of vertex indices {} not a multiple of 4. Discarding {} excess.", + vertex_indices.len(), + excess + ); + let new_len = vertex_indices.len() - excess; + vertex_indices.truncate(new_len); + } + + if p.is_empty() { + return Err( + "Vertex positions \"P\" must be provided with bilinear patch mesh shape.".into(), + ); + } + + if !uv.is_empty() && uv.len() != p.len() { + warn!("Number of \"uv\"s for bilinear patch mesh must match \"P\"s. Discarding uvs."); + uv.clear(); + } + + if !n.is_empty() && n.len() != p.len() { + warn!("Number of \"N\"s for bilinear patch mesh must match \"P\"s. Discarding \"N\"s."); + n.clear(); + } + + for (i, &idx) in vertex_indices.iter().enumerate() { + if idx < 0 || idx as usize >= p.len() { + return Err(format!( + "Bilinear patch mesh has out-of-bounds vertex index {} ({} \"P\" values were given). Discarding this mesh.", + idx, + p.len() + )); + } + } + + let n_patches = vertex_indices.len() / 4; + if !face_indices.is_empty() && face_indices.len() != n_patches { + warn!( + "Number of face indices {} does not match number of bilinear patches {}. Discarding face indices.", + face_indices.len(), + n_patches + ); + face_indices.clear(); + } + + let filename = parameters.get_one_string("emissionfilename", ""); + let mut image_dist = None; + + if !filename.is_empty() { + if !uv.is_empty() { + warn!( + "\"emissionfilename\" is currently ignored for bilinear patches if \"uv\" coordinates have been provided--sorry!" + ); + } else { + match Image::read(&filename) { + Ok(mut im) => { + im.flip_y(); + + let domain = Bounds2f::new(Point2f::new(0.0, 0.0), Point2f::new(1.0, 1.0)); + let distribution = im.get_sampling_distribution(); // Assuming this returns Array2D + + image_dist = + Some(PiecewiseConstant2D::new_with_bounds(distribution, domain)); + } + Err(e) => { + warn!("Failed to load emission image \"{}\": {}", filename, e); + } + } + } + } + + let host = BilinearPatchMeshHost::new( + render_from_object, + reverse_orientation, + vertex_indices, + p, + n, + uv, + None, + ); + + let host_arc = Arc::new(host); + let mut global_store = ALL_BILINEAR_MESHES.lock().unwrap(); + let mesh_index = global_store.len() as u32; + global_store.push(host_arc.clone()); + drop(global_store); + let n_patches = host_arc.view.n_patches; + let mesh_ptr = &host_arc.view as *const _; + let mut shapes = Vec::with_capacity(n_patches as usize); + for i in 0..n_patches { + shapes.push(Shape::Bilinear(BilinearPatchShape { + mesh: mesh_ptr, + blp_index: i, + area: 0.0, + rectangle: false, + })); + } + Ok(shapes) + } +} diff --git a/src/shapes/curves.rs b/src/shapes/curves.rs new file mode 100644 index 0000000..bee1949 --- /dev/null +++ b/src/shapes/curves.rs @@ -0,0 +1,199 @@ +use crate::core::shape::CreateShape; +use crate::core::texture::FloatTexture; +use crate::utils::{FileLoc, ParameterDictionary}; +use rayon::iter::split; +use shared::core::geometry::Normal3f; +use shared::shapes::{CurveCommon, CurveShape, CurveType, CylinderShape}; +use shared::utils::Transform; +use shared::utils::splines::{ + cubic_bspline_to_bezier, elevate_quadratic_bezier_to_cubic, quadratic_bspline_to_bezier, +}; +use std::collections::HashMap; + +pub fn create_curve( + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + seg_cp_bezier: &[Point3f], + w0: Float, + w1: Float, + curve_type: CurveType, + seg_normals: &[Normal3f], + split_depth: usize, +) -> Vec { + let curve_common = CurveCommon::new( + seg_cp_bezier, + w0, + w1, + curve_type, + seg_normals, + render_from_object, + object_from_render, + reverse_orientation, + ); + let n_segments = 1 << split_depth; + let mut segments: Vec = Vec::with_capacity(n_segments); + + for i in 0..n_segments { + let u_min = i as Float / n_segments as Float; + let u_max = (i + 1) as Float / n_segments as Float; + + let curve = Curve { + common: common.clone(), + u_min, + u_max, + }; + + segments.push(Shape::Curve(curve)); + } + segments +} + +impl CreateShape for CurveShape { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Result, String> { + let width = parameters.get_one_float("width", 1.0); + let width0 = parameters.get_one_float("width0", width); + let width1 = parameters.get_one_float("width1", width); + + let degree = parameters.get_one_int("degree", 3); + if degree != 2 && degree != 3 { + return Err(format!( + "Invalid degree {}: only degree 2 and 3 curves are supported.", + degree + )); + } + + let basis = parameters.get_one_string("basis", "bezier"); + if basis != "bezier" && basis != "bspline" { + return Err(format!( + "Invalid basis \"{}\": only \"bezier\" and \"bspline\" are supported.", + basis + )); + } + + let cp = parameters.get_point3f_array("P").unwrap_or_default(); + let n_segments; + + if basis == "bezier" { + if cp.len() <= degree as usize + || ((cp.len() - 1 - degree as usize) % degree as usize) != 0 + { + return Err(format!( + "Invalid number of control points {}: for the degree {} Bezier basis {} + n * {} are required.", + cp.len(), + degree, + degree + 1, + degree + )); + } + n_segments = (cp.len() - 1) / degree as usize; + } else { + if cp.len() < (degree + 1) as usize { + return Err(format!( + "Invalid number of control points {}: for the degree {} b-spline basis, must have >= {}.", + cp.len(), + degree, + degree + 1 + )); + } + n_segments = cp.len() - degree as usize; + } + + let curve_type_str = parameters.get_one_string("type", "flat"); + let curve_type = match curve_type_str.as_str() { + "flat" => CurveType::Flat, + "ribbon" => CurveType::Ribbon, + "cylinder" => CurveType::Cylinder, + _ => { + return Err(format!("Unknown curve type \"{}\".", curve_type_str)); + } + }; + + let mut n = parameters.get_normal3f_array("N").unwrap_or_default(); + if !n.is_empty() { + if curve_type != CurveType::Ribbon { + warn!("Curve normals are only used with \"ribbon\" type curves. Discarding."); + n.clear(); + } else if n.len() != n_segments + 1 { + return Err(format!( + "Invalid number of normals {}: must provide {} normals for ribbon curves with {} segments.", + n.len(), + n_segments + 1, + n_segments + )); + } + } else if curve_type == CurveType::Ribbon { + return Err( + "Must provide normals \"N\" at curve endpoints with ribbon curves.".to_string(), + ); + } + + let use_gpu = false; // Replace with actual config check + let split_depth = if use_gpu { + 0 + } else { + parameters.get_one_int("splitdepth", 3) + }; + + let mut curves: Vec> = Vec::new(); + let mut cp_offset = 0; + + for seg in 0..n_segments { + let seg_cp_bezier: [Point3f; 4]; + + if basis == "bezier" { + if degree == 2 { + let slice = &cp[cp_offset..cp_offset + 3]; + seg_cp_bezier = elevate_quadratic_bezier_to_cubic(slice); + cp_offset += 2; // Advance by degree + } else { + let slice = &cp[cp_offset..cp_offset + 4]; + seg_cp_bezier = [slice[0], slice[1], slice[2], slice[3]]; + cp_offset += 3; // Advance by degree + } + } else { + if degree == 2 { + let slice = &cp[cp_offset..cp_offset + 3]; + let bez_cp = quadratic_bspline_to_bezier(slice); + seg_cp_bezier = elevate_quadratic_bezier_to_cubic(&bez_cp); + } else { + let slice = &cp[cp_offset..cp_offset + 4]; + seg_cp_bezier = cubic_bspline_to_bezier(slice); + } + cp_offset += 1; // Advance by 1 for B-Splines + } + + let w0 = lerp(seg as Float / n_segments as Float, width0, width1); + let w1 = lerp((seg + 1) as Float / n_segments as Float, width0, width1); + + let seg_normals = if !n.is_empty() { + Some(&n[seg..seg + 2]) + } else { + None + }; + + let new_curves = create_curve( + render_from_object, + object_from_render, + reverse_orientation, + &seg_cp_bezier, + w0, + w1, + curve_type, + seg_normals, + split_depth, + ); + + curves.extend(new_curves); + } + Ok(curves) + } +} diff --git a/src/shapes/cylinder.rs b/src/shapes/cylinder.rs new file mode 100644 index 0000000..92df50b --- /dev/null +++ b/src/shapes/cylinder.rs @@ -0,0 +1,35 @@ +use crate::core::shape::CreateShape; +use crate::core::texture::FloatTexture; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::shape::Shape; +use shared::shapes::CylinderShape; +use shared::utils::Transform; +use std::collections::HashMap; + +impl CreateShape for CylinderShape { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Result, String> { + let radius = parameters.get_one_float("radius", 1.); + let z_min = parameters.get_one_float("zmin", -1.); + let z_max = parameters.get_one_float("zmax", 1.); + let phi_max = parameters.get_one_float("phimax", 360.); + let shape = CylinderShape::new( + render_from_object, + object_from_render, + reverse_orientation, + radius, + z_min, + z_max, + phi_max, + ); + + Ok(vec![Shape::Cylinder(shape)]) + } +} diff --git a/src/shapes/disk.rs b/src/shapes/disk.rs new file mode 100644 index 0000000..41369b1 --- /dev/null +++ b/src/shapes/disk.rs @@ -0,0 +1,35 @@ +use crate::core::shape::CreateShape; +use crate::core::texture::FloatTexture; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::shape::Shape; +use shared::shapes::DiskShape; +use shared::utils::Transform; +use std::collections::HashMap; + +impl CreateShape for DiskShape { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Result, String> { + let height = parameters.get_one_float("height", 0.); + let radius = parameters.get_one_float("radius", 1.); + let inner_radius = parameters.get_one_float("innerradius", 0.); + let phi_max = parameters.get_one_float("phimax", 360.); + let shape = DiskShape::new( + radius, + inner_radius, + height, + phi_max, + render_from_object, + object_from_render, + reverse_orientation, + ); + + Ok(vec![Shape::Disk(shape)]) + } +} diff --git a/src/shapes/mesh.rs b/src/shapes/mesh.rs new file mode 100644 index 0000000..6abd8fc --- /dev/null +++ b/src/shapes/mesh.rs @@ -0,0 +1,430 @@ +use crate::utils::FileLoc; +use ply_rs::parser::Parser; +use ply_rs::ply::{DefaultElement, Property}; +use shared::utils::Transform; +use shared::utils::mesh::{BilinearPatchMesh, TriangleMesh}; +use shared::utils::sampling::PiecewiseConstant2D; +use std::collections::HashMap; +use std::fs::File; +use std::path::Path; + +#[derive(Debug, Clone, Copy, Default)] +pub struct TriQuadMesh { + pub p: Vec, + pub n: Vec, + pub uv: Vec, + pub face_indices: Vec, + pub tri_indices: Vec, + pub quad_indices: Vec, +} + +fn get_float(elem: &DefaultElement, key: &str) -> Result { + match elem.get(key) { + Some(Property::Float(v)) => Ok(*v), + Some(Property::Double(v)) => Ok(*v as f32), + Some(_) => bail!("Property {} is not a float", key), + None => bail!("Property {} not found", key), + } +} + +fn get_int(elem: &DefaultElement, key: &str) -> Result { + match elem.get(key) { + Some(Property::Int(v)) => Ok(*v), + Some(Property::UInt(v)) => Ok(*v as i32), + Some(Property::Short(v)) => Ok(*v as i32), + Some(Property::UShort(v)) => Ok(*v as i32), + Some(Property::Char(v)) => Ok(*v as i32), + Some(Property::UChar(v)) => Ok(*v as i32), + _ => bail!("Property {} not found or not integer", key), + } +} + +fn get_float_any(elem: &DefaultElement, keys: &[&str]) -> Option { + for k in keys { + if let Ok(val) = get_float(elem, k) { + return Some(val); + } + } + None +} + +fn get_list_uint(elem: &DefaultElement, key: &str) -> Result> { + match elem.get(key) { + Some(Property::List_Int(vec)) => Ok(vec.iter().map(|&x| x as u32).collect()), + Some(Property::List_UInt(vec)) => Ok(vec.clone()), + Some(Property::List_UChar(vec)) => Ok(vec.iter().map(|&x| x as u32).collect()), + Some(Property::List_Char(vec)) => Ok(vec.iter().map(|&x| x as u32).collect()), + _ => bail!("Property {} is not a list", key), + } +} + +impl TriQuadMesh { + pub fn read_ply>(filename: P) -> Result { + let filename_display = filename.as_ref().display().to_string(); + let mut f = File::open(&filename) + .with_context(|| format!("Couldn't open PLY file \"{}\"", filename_display))?; + let p = Parser::::new(); + let ply = p + .read_ply(&mut f) + .with_context(|| format!("Unable to read/parse PLY file \"{}\"", filename_display))?; + + let mut mesh = TriQuadMesh::default(); + if let Some(vertices) = ply.payload.get("vertex") { + let first = &vertices[0]; + let has_uv = (first.contains_key("u") && first.contains_key("v")) + || (first.contains_key("s") && first.contains_key("t")) + || (first.contains_key("texture_u") && first.contains_key("texture_v")) + || (first.contains_key("texture_s") && first.contains_key("texture_t")); + let has_normal = + first.contains_key("nx") && first.contains_key("ny") && first.contains_key("nz"); + for v_elem in vertices { + // Read Position (Required) + let x = get_float(v_elem, "x")?; + let y = get_float(v_elem, "y")?; + let z = get_float(v_elem, "z")?; + mesh.p.push([x, y, z]); + + // Read Normal (Optional) + if has_normal { + let nx = get_float(v_elem, "nx").unwrap_or(0.0); + let ny = get_float(v_elem, "ny").unwrap_or(0.0); + let nz = get_float(v_elem, "nz").unwrap_or(0.0); + mesh.n.push([nx, ny, nz]); + } + + // Read UVs (Optional, handling variable naming convention) + if has_uv { + let u = + get_float_any(v_elem, &["u", "s", "texture_u", "texture_s"]).unwrap_or(0.0); + let v = + get_float_any(v_elem, &["v", "t", "texture_v", "texture_t"]).unwrap_or(0.0); + mesh.uv.push([u, v]); + } + } + } else { + bail!( + "{}: PLY file is invalid! No vertex elements found!", + filename_display + ); + } + + if let Some(faces) = ply.payload.get("face") { + mesh.tri_indices.reserve(faces.len() * 3); + mesh.quad_indices.reserve(faces.len() * 4); + + for f_elem in faces { + if let Ok(fi) = get_int(f_elem, "face_indices") { + mesh.faceIndices.push(fi); + } + + if let Ok(indices) = get_list_uint(f_elem, "vertex_indices") { + match indices.len() { + 3 => { + mesh.tri_indices.extend_from_slice(&indices); + } + 4 => { + mesh.quad_indices.extend_from_slice(&indices); + } + _ => {} + } + } else { + bail!("{}: vertex indices not found in PLY file", filename_display); + } + } + } else { + bail!( + "{}: PLY file is invalid! No face elements found!", + filename_display + ); + } + + let vertex_count = mesh.p.len() as u32; + + for &idx in &mesh.tri_indices { + if idx >= vertex_count { + bail!( + "plymesh: Vertex index {} is out of bounds! Valid range is [0..{})", + idx, + vertex_count + ); + } + } + for &idx in &mesh.quad_indices { + if idx >= vertex_count { + bail!( + "plymesh: Vertex index {} is out of bounds! Valid range is [0..{})", + idx, + vertex_count + ); + } + } + + Ok(mesh) + } + + pub fn convert_to_only_triangles(&mut self) { + if self.quad_indices.is_empty() { + return; + } + + for i in (0..self.quad_indices.len()).step_by(4) { + // First triangle: 0, 1, 3 + self.tri_indices.push(self.quad_indices[i]); + self.tri_indices.push(self.quad_indices[i + 1]); + self.tri_indices.push(self.quad_indices[i + 3]); + + // Second triangle: 0, 3, 2 + self.tri_indices.push(self.quad_indices[i]); + self.tri_indices.push(self.quad_indices[i + 3]); + self.tri_indices.push(self.quad_indices[i + 2]); + } + + self.quad_indices.clear(); + } + + pub fn compute_normals(&mut self) { + self.n.resize(self.p.len(), Normal3f::zero()); + for i in (0..self.tri_indices.len()).step_by(3) { + let v = vec![ + self.tri_indices[i], + self.tri_indices[i + 1], + self.tri_indices[i + 2], + ]; + let v10 = self.p[v[1]] - self.p[v[0]]; + let v21 = self.p[v[2]] - self.p[v[1]]; + + let mut vn = v10.cross(v21); + if vn.norm_squared() > 0. { + vn = vn.normalize(); + self.n[v[0]] += vn; + self.n[v[1]] += vn; + self.n[v[2]] += vn; + } + } + + assert!(self.quad_indices == 0.); + for i in 0..self.n.len() { + if n[i].normalize() > 0. { + self.n[i] = self.n[i].normalize() + } + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TriangleMeshStorage { + vertex_indices: Vec, + p: Vec, + n: Vec, + s: Vec, + uv: Vec, + face_indices: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct TriangleMeshHost { + pub view: TriangleMesh, + _storage: Arc, +} + +impl Deref for TriangleMeshHost { + type Target = TriangleMesh; + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl TriangleMeshHost { + pub fn new( + render_from_object: Transform, + reverse_orientation: bool, + vertex_indices: Vec, + mut p: Vec, + mut s: Vec, + mut n: Vec, + uv: Vec, + face_indices: Vec, + ) -> Self { + let n_triangles = indices.len() / 3; + let n_vertices = p.len(); + for pt in p.iter_mut() { + *pt = render_from_object.apply_to_point(*pt); + } + + let transform_swaps_handedness = render_from_object.swaps_handedness(); + + let uv = if !uv.is_empty() { + assert_eq!(n_vertices, uv.len()); + Some(uv) + } else { + None + }; + + let n = if !n.is_empty() { + assert_eq!(n_vertices, n.len()); + for nn in n.iter_mut() { + *nn = render_from_object.apply_to_normal(*nn); + if reverse_orientation { + *nn = -*nn; + } + } + Some(n) + } else { + None + }; + + let s = if !s.is_empty() { + assert_eq!(n_vertices, s.len()); + for ss in s.iter_mut() { + *ss = render_from_object.apply_to_vector(*ss); + } + Some(s) + } else { + None + }; + + let face_indices = if !face_indices.is_empty() { + assert_eq!(n_triangles, face_indices.len()); + Some(face_indices) + } else { + None + }; + + let storage = Box::new(TriangleMeshStorage { + p, + vertex_indices, + n, + s, + uv, + face_indices, + }); + + assert!(p.len() <= i32::MAX as usize); + assert!(indices.len() <= i32::MAX as usize); + + let p_ptr = storage.p.as_ptr(); + let idx_ptr = storage.vertex_indices.as_ptr(); + + let n_ptr = if storage.n.is_empty() { + ptr::null() + } else { + storage.n.as_ptr() + }; + + let uv_ptr = if storage.uv.is_empty() { + ptr::null() + } else { + storage.uv.as_ptr() + }; + + let s_ptr = if storage.s.is_empty() { + ptr::null() + } else { + storage.s.as_ptr() + }; + + let mesh = TriangleMeshHost::new( + render_from_object, + reverse_orientation, + vertex_indices, + p, + s, + n, + uv, + face_indices, + ); + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BilinearMeshStorage { + vertex_indices: Vec, + p: Vec, + n: Vec, + uv: Vec, + image_distribution: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct BilinearPatchMeshHost { + pub view: BilinearPatchMesh, + _storage: Box, +} + +impl Deref for TriangleMeshHost { + type Target = TriangleMesh; + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl BilinearPatchMeshHost { + pub fn new( + render_from_object: Transform, + reverse_orientation: bool, + vertex_indices: Vec, + mut p: Vec, + mut n: Vec, + uv: Vec, + image_distribution: Option, + ) -> Self { + let n_patches = indices.len() / 3; + let n_vertices = p.len(); + for pt in p.iter_mut() { + *pt = render_from_object.apply_to_point(*pt); + } + + if !n.is_empty() { + assert_eq!(n_vertices, n.len(), "Normal count mismatch"); + for nn in n.iter_mut() { + *nn = render_from_object.apply_to_normal(*nn); + if reverse_orientation { + *nn = -*nn; + } + } + } + + if !uv.is_empty() { + assert_eq!(n_vertices, uv.len(), "UV count mismatch"); + } + + let storage = Box::new(BilinearMeshStorage { + vertex_indices, + p, + n, + uv, + image_distribution, + }); + + let transform_swaps_handedness = render_from_object.swaps_handedness(); + + let p_ptr = storage.p.as_ptr(); + let idx_ptr = storage.vertex_indices.as_ptr(); + + let n_ptr = if storage.n.is_empty() { + ptr::null() + } else { + storage.n.as_ptr() + }; + + let uv_ptr = if storage.uv.is_empty() { + ptr::null() + } else { + storage.uv.as_ptr() + }; + + let view = BilinearPatchMesh { + n_patches: n_patches as u32, + n_vertices: n_vertices as u32, + vertex_indices: idx_ptr, + p: p_ptr, + n: n_ptr, + uv: uv_ptr, + reverse_orientation, + transform_swaps_handedness, + image_distribution: dist_ptr, + }; + + Self { view, _storage } + } +} diff --git a/src/shapes/mod.rs b/src/shapes/mod.rs new file mode 100644 index 0000000..1f14aac --- /dev/null +++ b/src/shapes/mod.rs @@ -0,0 +1,16 @@ +pub mod bilinear; +pub mod curves; +pub mod cylinder; +pub mod disk; +pub mod mesh; +pub mod mesh; +pub mod sphere; +pub mod triangle; + +pub use bilinear::*; +pub use mesh::*; + +use std::sync::{Arc, Mutex}; + +pub static ALL_TRIANGLE_MESHES: Mutex>> = Mutex::new(Vec::new()); +pub static ALL_TRIANGLE_MESHES: Mutex>> = Mutex::new(Vec::new()); diff --git a/src/shapes/sphere.rs b/src/shapes/sphere.rs new file mode 100644 index 0000000..4cc1667 --- /dev/null +++ b/src/shapes/sphere.rs @@ -0,0 +1,33 @@ +use crate::core::shape::CreateShape; +use crate::core::texture::FloatTexture; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::core::shape::Shape; +use shared::shapes::SphereShape; +use shared::utils::Transform; + +impl CreateShape for SphereShape { + fn create( + name: &str, + render_from_object: Transform, + object_from_render: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + float_textures: HashMap, + loc: FileLoc, + ) -> Result, String> { + let radius = parameters.get_one_float("radius", 1.); + let zmin = parameters.get_one_float("zmin", -radius); + let zmax = parameters.get_one_float("zmax", radius); + let phimax = parameters.get_one_float("phimax", 360.); + let shape = SphereShape::new( + renderFromObject, + objectFromRender, + reverseOrientation, + radius, + zmin, + zmax, + phimax, + ); + Ok(vec![Shape::Sphere(SphereShape)]) + } +} diff --git a/src/shapes/triangle.rs b/src/shapes/triangle.rs new file mode 100644 index 0000000..9d9ddaa --- /dev/null +++ b/src/shapes/triangle.rs @@ -0,0 +1,109 @@ +use crate::core::shape::{ALL_TRIANGLE_MESHES, CreateShape}; +use crate::core::texture::FloatTexture; +use crate::shapes::mesh::TriangleMeshHost; +use crate::utils::{FileLoc, ParameterDictionary}; +use shared::shapes::TriangleShape; +use shared::utils::Transform; + +impl CreateShape for TriangleShape { + fn create( + name: &str, + render_from_object: Transform, + reverse_orientation: bool, + parameters: ParameterDictionary, + loc: FileLoc, + ) -> Result { + let mut vertex_indices = parameters.get_int_array("indices"); + let mut p = parameters.get_point3f_array("P"); + let mut uvs = parameters.get_point2f_array("uv"); + let mut s = parameters.get_vector3f_array("S"); + let mut n = parameters.get_normal3f_array("N"); + + if vertex_indices.is_empty() { + if p.len() == 3 { + } else { + return Err( + "Vertex indices \"indices\" must be provided with triangle mesh.".to_string(), + ); + } + } else if vertex_indices.len() % 3 != 0 { + let excess = vertex_indices.len() % 3; + warn!( + "Number of vertex indices {} not a multiple of 3. Discarding {} excess.", + vi.len(), + excess + ); + let new_len = vertex_indices.len() - excess; + vertex_indices.truncate(new_len); + } + + if p.is_empty() { + return Err("Vertex positions \"P\" must be provided with triangle mesh.".to_string()); + } + + if !uvs.is_empty() && uvs.len() != p.len() { + warn!("Number of \"uv\"s for triangle mesh must match \"P\"s. Discarding uvs."); + uvs.clear(); + } + + if !s.is_empty() && s.len() != p.len() { + warn!("Number of \"S\"s for triangle mesh must match \"P\"s. Discarding \"S\"s."); + s.clear(); + } + + if !n.is_empty() && n.len() != p.len() { + warn!("Number of \"N\"s for triangle mesh must match \"P\"s. Discarding \"N\"s."); + n.clear(); + } + + for (i, &index) in vertex_indices.iter().enumerate() { + // Check for negative indices (if keeping i32) or out of bounds + if index < 0 || index as usize >= p.len() { + return Err(format!( + "TriangleMesh has out-of-bounds vertex index {} ({} \"P\" values were given). Discarding this mesh.", + index, + p.len() + )); + } + } + + let mut face_indices = parameters.get_int_array("faceIndices").unwrap_or_default(); + let n_triangles = vertex_indices.len() / 3; + + if !face_indices.is_empty() && face_indices.len() != n_triangles { + warn!( + "Number of face indices {} does not match number of triangles {}. Discarding face indices.", + face_indices.len(), + num_triangles + ); + face_indices.clear(); + } + + let host = TriangleMeshHost::new( + render_from_object, + reverse_orientation, + vertex_indices, + p, + s, + n, + uvs, + face_indices, + ); + + let host_arc = Arc::new(host); + let mut global_store = ALL_TRIANGLE_MESHES.lock().unwrap(); + let mesh_index = global_store.len() as u32; + global_store.push(host_arc.clone()); + drop(global_store); + let n_patches = host_arc.view.n_patches; + let mesh_ptr = &host_arc.view as *const _; + let mut shapes = Vec::with_capacity(n_patches as usize); + for i in 0..n_patches { + shapes.push(Shape::Triangle(TriangleShape { + mesh: mesh_ptr, + tri_index: i, + })); + } + Ok(shapes) + } +} diff --git a/src/textures/image.rs b/src/textures/image.rs index ab9f474..d106289 100644 --- a/src/textures/image.rs +++ b/src/textures/image.rs @@ -1,6 +1,78 @@ -use crate::core::texture::ImageTextureBase; +use crate::core::texture::get_texture_cache; use crate::utils::{FileLoc, TextureParameterDictionary}; +#[derive(Clone, Debug)] +pub struct ImageTextureBase { + pub mapping: TextureMapping2D, + pub filename: String, + pub scale: Float, + pub invert: bool, + pub mipmap: Arc, +} + +impl ImageTextureBase { + pub fn new( + mapping: TextureMapping2D, + filename: String, + filter_options: MIPMapFilterOptions, + wrap_mode: WrapMode, + scale: Float, + invert: bool, + encoding: ColorEncoding, + ) -> Self { + let tex_info = TexInfo { + filename: filename.clone(), + filter_options, + wrap_mode, + encoding, + }; + + let cache_mutex = get_texture_cache(); + + { + let cache = cache_mutex.lock().unwrap(); + if let Some(mipmap) = cache.get(&tex_info) { + return Self { + mapping, + filename, + scale, + invert, + mipmap: mipmap.clone(), + }; + } + } + + let path = Path::new(&filename); + let mipmap_raw = MIPMap::create_from_file(path, filter_options, wrap_mode, encoding) + .expect("Failed to create MIPMap from file"); + + let mipmap_arc = Arc::new(mipmap_raw); + + { + let mut cache = cache_mutex.lock().unwrap(); + + let stored_mipmap = cache.entry(tex_info).or_insert(mipmap_arc); + + Self { + mapping, + filename, + scale, + invert, + mipmap: stored_mipmap.clone(), + } + } + } + + pub fn clear_cache() { + let mut cache = get_texture_cache().lock().unwrap(); + cache.clear(); + } + + pub fn multiply_scale(&mut self, s: Float) { + self.scale *= s; + } +} + #[derive(Clone, Debug)] pub struct SpectrumImageTexture { pub base: ImageTextureBase, diff --git a/src/utils/containers.rs b/src/utils/containers.rs index 75d7bc4..f085c25 100644 --- a/src/utils/containers.rs +++ b/src/utils/containers.rs @@ -1,3 +1,7 @@ +use crate::core::geometry::{Bounds2i, Point2i}; +use crate::shared::utils::containers::Array2D; +use std::ops::{Deref, DerefMut}; + pub struct InternCache { cache: Mutex>>, } @@ -24,3 +28,56 @@ where new_item } } + +pub struct Array2DBuffer { + pub view: Array2D, + _storage: Vec, +} + +impl Deref for Array2DBuffer { + type Target = Array2D; + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl DerefMut for Array2DBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.view + } +} + +impl Array2DBuffer { + fn from_vec(extent: Bounds2i, mut storage: Vec) -> Self { + let width = extent.p_max.x - extent.p_min.x; + let view = Array2D { + extent, + values: storage.as_mut_ptr(), + stride: width, + }; + Self { + view, + _storage: storage, + } + } +} + +impl Array2DBuffer { + pub fn new(extent: Bounds2i) -> Self { + let n = extent.area() as usize; + let storage = vec![T::default(); n]; + Self::from_vec(extent, storage) + } + + pub fn new_dims(nx: i32, ny: i32) -> Self { + let extent = Bounds2i::new(Point2i::new(0, 0), Point2i::new(nx, ny)); + Self::new(extent) + } + + pub fn new_filled(nx: i32, ny: i32, val: T) -> Self { + let extent = Bounds2i::new(Point2i::new(0, 0), Point2i::new(nx, ny)); + let n = (nx * ny) as usize; + let storage = vec![val; n]; + Self::from_vec(extent, storage) + } +} diff --git a/src/utils/error.rs b/src/utils/error.rs index 1afd2d8..57327e0 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,3 +1,7 @@ +use image_rs::{ImageError as IError, error}; +use shared::core::image::PixelFormat; +use std::sync::Arc; + #[derive(Clone, Debug)] pub struct FileLoc { pub filename: Arc, diff --git a/src/utils/io.rs b/src/utils/io.rs index eb26203..e69de29 100644 --- a/src/utils/io.rs +++ b/src/utils/io.rs @@ -1,369 +0,0 @@ -use crate::utils::error::ImageError; -use anyhow::{Context, Result, bail}; -use exr::prelude::{read_first_rgba_layer_from_file, write_rgba_file}; -use image_rs::ImageReader; -use image_rs::{DynamicImage, ImageBuffer, Rgb, Rgba}; -use shared::Float; -use shared::images::{ - Image, ImageAndMetadata, ImageMetadata, PixelData, PixelFormat, Point2i, WrapMode, -}; -use shared::spectra::color::{ColorEncoding, LINEAR, SRGB}; -use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Read, Write}; -use std::path::Path; - -pub trait ImageIO { - fn read(path: &Path, encoding: Option) -> Result; - fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<()>; - fn write_png(&self, path: &Path) -> Result<()>; - fn write_exr(&self, path: &Path, metadata: &ImageMetadata) -> Result<()>; - fn write_qoi(&self, path: &Path) -> Result<()>; - fn write_pfm(&self, path: &Path) -> Result<()>; - fn to_u8_buffer(&self) -> Vec; -} - -impl ImageIO for Image { - fn read(path: &Path, encoding: Option) -> Result { - let ext = path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_lowercase(); - - match ext.as_str() { - "exr" => read_exr(path), - "pfm" => read_pfm(path), - _ => read_generic(path, encoding), - } - } - - fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { - let path = Path::new(filename); - let ext = path.extension().and_then(|s| s.to_str()).unwrap_or(""); - let res = match ext.to_lowercase().as_str() { - "exr" => self.write_exr(path, metadata), - "png" => self.write_png(path), - "pfm" => self.write_pfm(path), - "qoi" => self.write_qoi(path), - _ => Err(anyhow::anyhow!("Unsupported write format: {}", ext)), - }; - res.map_err(|e| ImageError::Io(std::io::Error::other(e))) - } - - fn write_png(&self, path: &Path) -> Result<()> { - let w = self.resolution.x() as u32; - let h = self.resolution.y() as u32; - - // Convert whatever we have to u8 [0..255] - let data = self.to_u8_buffer(); - let channels = self.n_channels(); - - match channels { - 1 => { - // Luma - image_rs::save_buffer_with_format( - path, - &data, - w, - h, - image_rs::ColorType::L8, - image_rs::ImageFormat::Png, - )?; - } - 3 => { - // RGB - image_rs::save_buffer_with_format( - path, - &data, - w, - h, - image_rs::ColorType::Rgb8, - image_rs::ImageFormat::Png, - )?; - } - 4 => { - // RGBA - image_rs::save_buffer_with_format( - path, - &data, - w, - h, - image_rs::ColorType::Rgba8, - image_rs::ImageFormat::Png, - )?; - } - _ => bail!("PNG writer only supports 1, 3, or 4 channels"), - } - Ok(()) - } - - fn write_qoi(&self, path: &Path) -> Result<()> { - let w = self.resolution.x() as u32; - let h = self.resolution.y() as u32; - let data = self.to_u8_buffer(); - - let color_type = match self.n_channels() { - 3 => image_rs::ColorType::Rgb8, - 4 => image_rs::ColorType::Rgba8, - _ => bail!("QOI only supports 3 or 4 channels"), - }; - - image_rs::save_buffer_with_format( - path, - &data, - w, - h, - color_type, - image_rs::ImageFormat::Qoi, - )?; - Ok(()) - } - - fn write_exr(&self, path: &Path, _metadata: &ImageMetadata) -> Result<()> { - // EXR requires F32 - let w = self.resolution.x() as usize; - let h = self.resolution.y() as usize; - let c = self.n_channels(); - - write_rgba_file(path, w, h, |x, y| { - // Helper to get float value regardless of internal storage - let get = |ch| { - self.get_channel_with_wrap( - Point2i::new(x as i32, y as i32), - ch, - WrapMode::Clamp.into(), - ) - }; - - if c == 1 { - let v = get(0); - (v, v, v, 1.0) - } else if c == 3 { - (get(0), get(1), get(2), 1.0) - } else { - (get(0), get(1), get(2), get(3)) - } - }) - .context("Failed to write EXR")?; - - Ok(()) - } - - fn write_pfm(&self, path: &Path) -> Result<()> { - let file = File::create(path)?; - let mut writer = BufWriter::new(file); - - if self.n_channels() != 3 { - bail!("PFM writing currently only supports 3 channels (RGB)"); - } - - // Header - writeln!(writer, "PF")?; - writeln!(writer, "{} {}", self.resolution.x(), self.resolution.y())?; - let scale = if cfg!(target_endian = "little") { - -1.0 - } else { - 1.0 - }; - writeln!(writer, "{}", scale)?; - - // PBRT stores top-to-bottom. - for y in (0..self.resolution.y()).rev() { - for x in 0..self.resolution.x() { - for c in 0..3 { - let val = - self.get_channel_with_wrap(Point2i::new(x, y), c, WrapMode::Clamp.into()); - writer.write_all(&val.to_le_bytes())?; - } - } - } - - Ok(()) - } - - fn to_u8_buffer(&self) -> Vec { - match &self.pixels { - PixelData::U8(data) => data.clone(), - PixelData::F16(data) => data - .iter() - .map(|v| (v.to_f32().clamp(0.0, 1.0) * 255.0 + 0.5) as u8) - .collect(), - PixelData::F32(data) => data - .iter() - .map(|v| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8) - .collect(), - } - } -} - -fn read_generic(path: &Path, encoding: Option) -> Result { - let dyn_img = ImageReader::open(path) - .with_context(|| format!("Failed to open image: {:?}", path))? - .decode()?; - - let w = dyn_img.width() as i32; - let h = dyn_img.height() as i32; - let res = Point2i::new(w, h); - - // Check if it was loaded as high precision or standard - let image = match dyn_img { - DynamicImage::ImageRgb32F(buf) => Image { - format: PixelFormat::F32, - resolution: res, - channel_names: vec!["R".into(), "G".into(), "B".into()], - encoding: LINEAR, - pixels: PixelData::F32(buf.into_raw()), - }, - DynamicImage::ImageRgba32F(buf) => Image { - format: PixelFormat::F32, - resolution: res, - channel_names: vec!["R".into(), "G".into(), "B".into(), "A".into()], - encoding: LINEAR, - pixels: PixelData::F32(buf.into_raw()), - }, - _ => { - // Default to RGB8 for everything else - if dyn_img.color().has_alpha() { - let buf = dyn_img.to_rgba8(); - Image { - format: PixelFormat::U8, - resolution: res, - channel_names: vec!["R".into(), "G".into(), "B".into(), "A".into()], - encoding: encoding.unwrap_or(SRGB), - pixels: PixelData::U8(buf.into_raw()), - } - } else { - let buf = dyn_img.to_rgb8(); - Image { - format: PixelFormat::U8, - resolution: res, - channel_names: vec!["R".into(), "G".into(), "B".into()], - encoding: encoding.unwrap_or(SRGB), - pixels: PixelData::U8(buf.into_raw()), - } - } - } - }; - - let metadata = ImageMetadata::default(); - Ok(ImageAndMetadata { image, metadata }) -} - -fn read_exr(path: &Path) -> Result { - let image = read_first_rgba_layer_from_file( - path, - |resolution, _| { - let size = resolution.width() * resolution.height() * 4; - vec![0.0 as Float; size] - }, - |buffer, position, pixel| { - let width = position.width(); - let idx = (position.y() * width + position.x()) * 4; - // Map exr pixel struct to our buffer - buffer[idx] = pixel.0; - buffer[idx + 1] = pixel.1; - buffer[idx + 2] = pixel.2; - buffer[idx + 3] = pixel.3; - }, - ) - .with_context(|| format!("Failed to read EXR: {:?}", path))?; - - let w = image.layer_data.size.width() as i32; - let h = image.layer_data.size.height() as i32; - - let image = Image { - format: PixelFormat::F32, - resolution: Point2i::new(w, h), - channel_names: vec!["R".into(), "G".into(), "B".into(), "A".into()], - encoding: LINEAR, - pixels: PixelData::F32(image.layer_data.channel_data.pixels), - }; - - let metadata = ImageMetadata::default(); - Ok(ImageAndMetadata { image, metadata }) -} - -fn read_pfm(path: &Path) -> Result { - let file = File::open(path)?; - let mut reader = BufReader::new(file); - - // PFM Headers are: "PF\nwidth height\nscale\n" (or Pf for grayscale) - let mut header_word = String::new(); - reader.read_line(&mut header_word)?; - let header_word = header_word.trim(); - - let channels = match header_word { - "PF" => 3, - "Pf" => 1, - _ => bail!("Invalid PFM header: {}", header_word), - }; - - let mut dims_line = String::new(); - reader.read_line(&mut dims_line)?; - let dims: Vec = dims_line - .split_whitespace() - .map(|s| s.parse().unwrap_or(0)) - .collect(); - - if dims.len() < 2 { - bail!("Invalid PFM dimensions"); - } - let w = dims[0]; - let h = dims[1]; - - let mut scale_line = String::new(); - reader.read_line(&mut scale_line)?; - let scale: f32 = scale_line.trim().parse().context("Invalid PFM scale")?; - - let file_is_little_endian = scale < 0.0; - let abs_scale = scale.abs(); - - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer)?; - - let expected_bytes = (w * h * channels) as usize * 4; - if buffer.len() < expected_bytes { - bail!("PFM file too short"); - } - - let mut pixels = vec![0.0 as Float; (w * h * channels) as usize]; - - // PFM is Bottom-to-Top - for y in 0..h { - // Flippety-do - let src_y = h - 1 - y; - for x in 0..w { - for c in 0..channels { - let src_idx = ((src_y * w + x) * channels + c) as usize * 4; - let dst_idx = ((y * w + x) * channels + c) as usize; - - let bytes: [u8; 4] = buffer[src_idx..src_idx + 4].try_into()?; - - let val = if file_is_little_endian { - f32::from_le_bytes(bytes) - } else { - f32::from_be_bytes(bytes) - }; - - pixels[dst_idx] = val * abs_scale; - } - } - } - - let names = if channels == 1 { - vec!["Y".into()] - } else { - vec!["R".into(), "G".into(), "B".into()] - }; - - let image = Image { - format: PixelFormat::F32, - resolution: Point2i::new(w, h), - channel_names: names, - encoding: LINEAR, - pixels: PixelData::F32(pixels), - }; - - let metadata = ImageMetadata::default(); - Ok(ImageAndMetadata { image, metadata }) -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index dd88323..fdd88d9 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,8 +3,10 @@ pub mod error; pub mod file; pub mod io; pub mod mipmap; +pub mod parallel; pub mod parameters; pub mod parser; +pub mod sampling; pub use error::FileLoc; pub use file::{read_float_file, resolve_filename}; diff --git a/src/utils/parallel.rs b/src/utils/parallel.rs new file mode 100644 index 0000000..04081a8 --- /dev/null +++ b/src/utils/parallel.rs @@ -0,0 +1,74 @@ +use crossbeam_channel::{Receiver, bounded}; +use rayon::prelude::*; + +pub fn init_parallel(n_threads: usize) { + let threads = if n_threads == 0 { + num_cpus::get() + } else { + n_threads + }; + + if let Err(e) = rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build_global() + { + eprintln!("Warning: Rayon thread pool already initialized: {}", e); + } +} + +pub fn num_system_cores() -> usize { + num_cpus::get() +} + +pub fn max_concurrency() -> usize { + rayon::current_num_threads() +} + +pub fn parallel_for(start: i64, end: i64, func: F) +where + F: Fn(i64) + Sync + Send, +{ + (start..end).into_par_iter().for_each(|i| func(i)); +} + +pub fn parallel_for_2d(start_x: i64, end_x: i64, start_y: i64, end_y: i64, func: F) +where + F: Fn(i64, i64) + Sync + Send, +{ + (start_y..end_y).into_par_iter().for_each(|y| { + (start_x..end_x).into_par_iter().for_each(|x| { + func(x, y); + }); + }); +} + +pub struct AsyncJob { + receiver: Receiver, +} + +impl AsyncJob { + pub fn wait(self) -> T { + self.receiver + .recv() + .expect("AsyncJob worker thread panicked or disconnected") + } + + pub fn is_ready(&self) -> bool { + !self.receiver.is_empty() + } +} + +pub fn run_async(func: F) -> AsyncJob +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + let (tx, rx) = bounded(1); + + rayon::spawn(move || { + let result = func(); + let _ = tx.send(result); + }); + + AsyncJob { receiver: rx } +} diff --git a/src/utils/parameters.rs b/src/utils/parameters.rs index cf1c856..58304b3 100644 --- a/src/utils/parameters.rs +++ b/src/utils/parameters.rs @@ -1,7 +1,10 @@ +use crate::core::spectrum::SPECTRUM_CACHE; use crate::utils::error::FileLoc; +use image_rs::imageops::FilterType::Triangle; use shared::Float; use shared::core::geometry::{Normal3f, Point2f, Point3f, Vector2f, Vector3f}; use shared::core::options::get_options; +use shared::core::spectrum::Spectrum; use shared::core::texture::{ FloatConstantTexture, FloatTexture, SpectrumConstantTexture, SpectrumTexture, SpectrumType, }; @@ -18,9 +21,6 @@ use std::sync::{ atomic::{AtomicBool, Ordering}, }; -static CACHED_SPECTRA: Lazy>> = - Lazy::new(|| Mutex::new(HashMap::new())); - #[derive(Debug)] pub struct ParsedParameter { pub type_name: String, @@ -107,6 +107,113 @@ impl ParsedParameter { } } +pub type ParsedParameterVector = Vec; + +pub trait PBRTParameter: Sized { + type Raw: Clone; + const TYPE_NAME: &'static str; + const N_PER_ITEM: usize; + fn convert(v: &[Self::Raw]) -> Self; + fn get_values(param: &ParsedParameter) -> &[Self::Raw]; +} + +impl PBRTParameter for bool { + type Raw = bool; + const TYPE_NAME: &'static str = "bool"; + const N_PER_ITEM: usize = 1; + fn convert(v: &[Self::Raw]) -> Self { + v[0] + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.bools + } +} + +impl PBRTParameter for Float { + type Raw = Float; + const TYPE_NAME: &'static str = "float"; + const N_PER_ITEM: usize = 1; + fn convert(v: &[Self::Raw]) -> Self { + v[0] + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.floats + } +} + +impl PBRTParameter for i32 { + type Raw = i32; + const TYPE_NAME: &'static str = "integer"; + const N_PER_ITEM: usize = 1; + fn convert(v: &[Self::Raw]) -> Self { + v[0] + } + + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.ints + } +} + +impl PBRTParameter for Point2f { + type Raw = Point2f; + const TYPE_NAME: &'static str = "point2"; + const N_PER_ITEM: usize = 2; + fn convert(v: &[Self::Raw]) -> Self { + Poin23f::new(v[0], v[1]) + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + params.floats + } +} + +impl PBRTParameter for Point3f { + type Raw = Point3f; + const TYPE_NAME: &'static str = "point3"; + const N_PER_ITEM: usize = 3; + fn convert(v: &[Self::Raw]) -> Self { + Point3f::new(v[0], v[1], v[2]) + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.floats + } +} + +impl PBRTParameter for Vector2f { + type Raw = Vector2f; + const TYPE_NAME: &'static str = "vector2"; + const N_PER_ITEM: usize = 3; + fn convert(v: &[Self::Raw]) -> Self { + Vector2f::new(v[0], v[1]) + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.floats + } +} + +impl PBRTParameter for Normal3f { + type Raw = Normal3f; + const TYPE_NAME: &'static str = "normal"; + const N_PER_ITEM: usize = 3; + fn convert(v: &[Self::Raw]) -> Self { + Vector3f::new(v[0], v[1], v[2]) + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.floats + } +} + +impl PBRTParameter for String { + type Raw = String; + const TYPE_NAME: &'static str = "string"; + const N_PER_ITEM: usize = 1; + fn convert(v: &[Self::Raw]) -> Self { + v[0] + } + fn get_values(param: &ParsedParameter) -> &[Self::Raw] { + param.strings + } +} + #[derive(Default)] pub struct NamedTextures { pub float_textures: HashMap>, @@ -117,200 +224,238 @@ pub struct NamedTextures { #[derive(Debug, Default, Clone)] pub struct ParameterDictionary { - pub params: Vec, + pub params: ParsedParameterVector, pub color_space: Option>, + pub n_owned_params: usize, } impl ParameterDictionary { - pub fn new(params: Vec, color_space: Option>) -> Self { - Self { + pub fn new(mut params: ParsedParameterVector, color_space: Option>) -> Self { + params.reverse(); + let dict = Self { params, color_space, - } + n_owned_params: params.len(), + }; + dict.check_parameter_types(); + dict } - fn lookup_single( - &self, - name: &str, - default_val: T, - extractor: impl Fn(&ParsedParameter) -> Option<&Vec>, - ) -> T { - for param in &self.params { - if param.name == name { - if let Some(vec) = extractor(param) { - if vec.len() == 1 { - param.looked_up.store(true, Ordering::Relaxed); - return vec[0].clone(); + pub fn from_array( + mut params0: ParsedParameterVector, + params1: &[ParsedParameter], + color_space: Option>, + ) -> Self { + let n_owned = params.len(); + p0.extend(params1.iter().rev().cloned()); + + let dict = Self { + params: p0, + color_space, + n_owned_params, + }; + dict.check_parameter_types(); + dict + } + + fn check_parameter_types(&self) { + for p in &self.params { + match p.type_name.as_str() { + bool::TYPE_NAME => { + if p.bools.is_empty() { + error_exit( + &p.loc, + &format!( + "\"{}\": non-Boolean values provided for Boolean-valued parameter", + p.name + ), + ); } } + + Float::TYPE_NAME + | i32::TYPE_NAME + | Point2f::TYPE_NAME + | Vector2f::TYPE_NAME + | Point3f::TYPE_NAME + | Vector3f::TYPE_NAME + | Normal3f::TYPE_NAME + | "rgb" + | "blackbody" => { + if p.ints.is_empty() && p.floats.is_empty() { + error_exit( + &p.loc, + &format!( + "\"{}\": non-numeric values provided for numeric-valued parameter", + p.name + ), + ); + } + } + + String::TYPE_NAME | "texture" => { + if p.strings.is_empty() { + error_exit( + &p.loc, + &format!( + "\"{}\": non-string values provided for string-valued parameter", + p.name + ), + ); + } + } + + "spectrum" => { + if p.strings.is_empty() && p.ints.is_empty() && p.floats.is_empty() { + error_exit( + &p.loc, + &format!( + "\"{}\": expecting string or numeric-valued parameter for spectrum parameter", + p.name + ), + ); + } + } + + unknown => { + error_exit( + &p.loc, + &format!("\"{}\": unknown parameter type '{}'", p.name, unknown), + ); + } } } + } + + fn lookup_single(&self, name: &str, def: T) -> T + where + T: PBRTParameter, + { + if param.name == name && param.type_name == T::TYPE_NAME { + let values = T::get_values(param); + + if values.is_empty() { + error_exit( + ¶m.loc, + &format!("No values provided for parameter \"{}\".", name), + ); + } + + if values.len() != T::N_PER_ITEM { + error_exit( + ¶m.loc, + &format!( + "Expected {} values for parameter \"{}\". Found {}.", + T::N_PER_ITEM, + name, + values.len() + ), + ); + } + + param.looked_up.store(true, Ordering::Relaxed); + return T::convert(values); + } + default_val } - fn lookup_array( - &self, - name: &str, - extractor: impl Fn(&ParsedParameter) -> Option<&Vec>, - ) -> Vec { + pub fn lookup_array(&self, name: &str) -> Vec + where + T: PBRTParameter, + { for param in &self.params { - if param.name == name { - if let Some(vec) = extractor(param) { - param.looked_up.store(true, Ordering::Relaxed); - return vec.clone(); + if param.name == name && param.type_name == T::TYPE_NAME { + let values = T::get_values(param); + + if values.len() % T::N_PER_ITEM != 0 { + error_exit( + ¶m.loc, + &format!( + "Number of values for \"{}\" is not a multiple of {}", + name, + T::N_PER_ITEM + ), + ); } + + param.looked_up.store(true, Ordering::Relaxed); + + return values + .chunks(T::N_PER_ITEM) + .map(|chunk| T::convert(chunk)) + .collect(); } } Vec::new() } pub fn get_one_float(&self, name: &str, def: Float) -> Float { - self.lookup_single(name, def, |p| { - if p.type_name == "float" { - Some(&p.floats) - } else { - None - } - }) + self.lookup_single(name, def) } pub fn get_one_int(&self, name: &str, def: i32) -> i32 { - self.lookup_single(name, def, |p| { - if p.type_name == "integer" { - Some(&p.ints) - } else { - None - } - }) + self.lookup_single(name, def) } pub fn get_one_bool(&self, name: &str, def: bool) -> bool { - self.lookup_single(name, def, |p| { - if p.type_name == "bool" { - Some(&p.bools) - } else { - None - } - }) + self.lookup_single(name, def) } pub fn get_one_string(&self, name: &str, def: &str) -> String { - self.lookup_single(name, def.to_string(), |p| { - if p.type_name == "string" { - Some(&p.strings) - } else { - None - } - }) - } - - pub fn get_float_array(&self, name: &str) -> Vec { - self.lookup_array(name, |p| { - if p.type_name == "float" { - Some(&p.floats) - } else { - None - } - }) - } - - pub fn get_int_array(&self, name: &str) -> Vec { - self.lookup_array(name, |p| { - if p.type_name == "integer" { - Some(&p.ints) - } else { - None - } - }) - } - - pub fn get_bool_array(&self, name: &str) -> Vec { - self.lookup_array(name, |p| { - if p.type_name == "bool" { - Some(&p.bools) - } else { - None - } - }) - } - - pub fn get_string_array(&self, name: &str) -> Vec { - self.lookup_array(name, |p| { - if p.type_name == "string" { - Some(&p.strings) - } else { - None - } - }) - } - - pub fn get_one_point3f(&self, name: &str, def: Point3f) -> Point3f { - let floats = self.get_float_array(name); - if floats.len() == 3 { - Point3f::new(floats[0], floats[1], floats[2]) - } else { - def - } - } - - pub fn get_one_vector3f(&self, name: &str, def: Vector3f) -> Vector3f { - let floats = self.get_float_array(name); - if floats.len() == 3 { - Vector3f::new(floats[0], floats[1], floats[2]) - } else { - def - } - } - - pub fn get_one_normal3f(&self, name: &str, def: Normal3f) -> Normal3f { - let floats = self.get_float_array(name); - if floats.len() == 3 { - Normal3f::new(floats[0], floats[1], floats[2]) - } else { - def - } - } - - pub fn get_point3f_array(&self, name: &str) -> Vec { - let floats = self.get_float_array(name); - floats - .chunks_exact(3) - .map(|c| Point3f::new(c[0], c[1], c[2])) - .collect() - } - - pub fn get_vector3f_array(&self, name: &str) -> Vec { - let floats = self.get_float_array(name); - floats - .chunks_exact(3) - .map(|c| Vector3f::new(c[0], c[1], c[2])) - .collect() - } - - pub fn get_normal3f_array(&self, name: &str) -> Vec { - let floats = self.get_float_array(name); - floats - .chunks_exact(3) - .map(|c| Normal3f::new(c[0], c[1], c[2])) - .collect() + self.lookup_single(name, def) } pub fn get_one_point2f(&self, name: &str, def: Point2f) -> Point2f { - let floats = self.get_float_array(name); - if floats.len() == 2 { - Point2f::new(floats[0], floats[1]) - } else { - def - } + self.lookup_single(name, def) + } + + pub fn get_one_point3f(&self, name: &str, def: Point3f) -> Point3f { + self.lookup_single(name, def) } pub fn get_one_vector2f(&self, name: &str, def: Vector2f) -> Vector2f { - let floats = self.get_float_array(name); - if floats.len() == 2 { - Vector2f::new(floats[0], floats[1]) - } else { - def - } + self.lookup_single(name, def) + } + + pub fn get_one_vector3f(&self, name: &str, def: Vector3f) -> Vector3f { + self.lookup_single(name, def) + } + + pub fn get_one_normal3f(&self, name: &str, def: Normal3f) -> Normal3f { + self.lookup_single(name, def) + } + + pub fn get_float_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_int_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_bool_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_string_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_point2f_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_point3f_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_vector3f_array(&self, name: &str) -> Vec { + self.lookup_array(name) + } + + pub fn get_normal3f_array(&self, name: &str) -> Vec { + self.lookup_array(name) } pub fn get_one_spectrum( @@ -405,105 +550,111 @@ impl ParameterDictionary { param: &ParsedParameter, spectrum_type: SpectrumType, ) -> Vec { - let type_name = param.type_name.as_str(); - if type_name == "rgb" && type_name == "color" { - let color_space = param - .color_space - .as_ref() - .or(self.color_space.as_ref()) - .expect("No color available"); - return param - .floats - .chunks_exact(3) - .map(|chunk| { - let rgb = crate::spectra::RGB::new(chunk[0], chunk[1], chunk[2]); + match param.type_name.as_str() { + "rgb" | "color" => self.extract_rgb_spectrum(param, spectrum_type), + "blackbody" => self.extract_blackbody_spectrum(param), + "spectrum" => self.extract_complex_spectrum(param), + _ => Vec::new(), + } + } - if rgb.r < 0.0 || rgb.g < 0.0 || rgb.b < 0.0 { - panic!( - "{}: RGB parameter '{}' has negative component.", - param.loc, param.name - ); - } + fn extract_rgb_spectrum( + &self, + param: &ParsedParameter, + spectrum_type: SpectrumType, + ) -> Vec { + let color_space = param + .color_space + .as_ref() + .or(self.color_space.as_ref()) + .expect("No color available"); - match spectrum_type { - SpectrumType::Albedo => { - if rgb.r > 1.0 || rgb.g > 1.0 || rgb.b > 1.0 { - panic!( - "{}: RGB parameter '{}' has > 1 component.", - param.loc, param.name - ); - } - Spectrum::RGBAlbedo(RGBAlbedoSpectrum::new(color_space.as_ref(), rgb)) - } - SpectrumType::Unbounded => Spectrum::RGBUnbounded( - RGBUnboundedSpectrum::new(color_space.as_ref(), rgb), - ), - SpectrumType::Illuminant => Spectrum::RGBIlluminant( - RGBIlluminantSpectrum::new(color_space.as_ref(), rgb), - ), - } - }) - .collect(); - } else if type_name == "blackbody" { - return param - .floats - .iter() - .map(|&temp| Spectrum::Blackbody(BlackbodySpectrum::new(temp))) - .collect(); - } else if type_name == "spectrum" && !param.floats.is_empty() { - if param.floats.len() % 2 != 0 { - panic!( - "{}: Found odd number of values for '{}'", - param.loc, param.name - ); - } + param + .floats + .chunks_exact(3) + .map(|chunk| { + let rgb = crate::spectra::RGB::new(chunk[0], chunk[1], chunk[2]); - let n_samples = param.floats.len() / 2; - if n_samples == 1 { - eprintln!( - "{}: Specified spectrum is only non-zero at a single wavelength.", - param.loc - ); - } - - let mut lambdas = Vec::with_capacity(n_samples); - let mut values = Vec::with_capacity(n_samples); - - for i in 0..n_samples { - let lam = param.floats[2 * i]; - let val = param.floats[2 * i + 1]; - - if i > 0 { - let prev_lam = lambdas[i - 1]; - if lam <= prev_lam { - panic!( - "{}: Spectrum description invalid, wavelengths aren't increasing: {} >= {}.", - param.loc, prev_lam, lam - ); - } + if rgb.r < 0.0 || rgb.g < 0.0 || rgb.b < 0.0 { + panic!( + "{}: RGB parameter '{}' has negative component.", + param.loc, param.name + ); } - lambdas.push(lam); - values.push(val); - } - return vec![Spectrum::PiecewiseLinear(PiecewiseLinearSpectrum { - lambdas, - values, - })]; - } else if type_name == "spectrum" && !param.strings.is_empty() { - return param - .strings - .iter() - .map(|s| { - crate::spectra::get_named_spectrum(s) - .ok_or(()) - .or_else(|_| read_spectrum_from_file(s).map_err(|_| ())) - .unwrap_or_else(|_| panic!("{}: {}: unable to read spectrum", param.loc, s)) - }) - .collect(); + match spectrum_type { + SpectrumType::Albedo => { + if rgb.r > 1.0 || rgb.g > 1.0 || rgb.b > 1.0 { + panic!( + "{}: RGB parameter '{}' has > 1 component.", + param.loc, param.name + ); + } + Spectrum::RGBAlbedo(RGBAlbedoSpectrum::new(color_space.as_ref(), rgb)) + } + SpectrumType::Unbounded => { + Spectrum::RGBUnbounded(RGBUnboundedSpectrum::new(color_space.as_ref(), rgb)) + } + SpectrumType::Illuminant => Spectrum::RGBIlluminant( + RGBIlluminantSpectrum::new(color_space.as_ref(), rgb), + ), + } + }) + .collect() + } + + fn extract_sampled_spectrum(&self, param: &ParsedParameter) -> Vec { + if param.floats.len() % 2 != 0 { + panic!( + "{}: Found odd number of values for '{}'", + param.loc, param.name + ); } - Vec::new() + let n_samples = param.floats.len() / 2; + if n_samples == 1 { + eprintln!( + "{}: Specified spectrum is only non-zero at a single wavelength.", + param.loc + ); + } + + let (lambdas, values): (Vec, Vec) = param + .floats + .chunks(2) + .enumerate() + .map(|(i, chunk)| { + let (lam, val) = (chunk[0], chunk[1]); + if i > 0 && lam <= param.floats[(i - 1) * 2] { + panic!( + "{}: Spectrum invalid, wavelengths not increasing: {} >= {}.", + param.loc, + param.floats[(i - 1) * 2], + lam + ); + } + (lam, val) + }) + .unzip(); + + vec![Spectrum::PiecewiseLinear(PiecewiseLinearSpectrum { + lambdas, + values, + count: lambdas.len(), + })] + } + + fn extract_file_spectrum(&self, param: &ParsedParameter) -> Vec { + param + .strings + .iter() + .map(|s| { + crate::spectra::get_named_spectrum(s) + .ok_or(()) + .or_else(|_| read_spectrum_from_file(s).map_err(|_| ())) + .unwrap_or_else(|_| panic!("{}: {}: unable to read spectrum", param.loc, s)) + }) + .collect() } } @@ -529,8 +680,6 @@ fn read_spectrum_from_file(filename: &str) -> Result { Ok(spectrum) } -pub type ParsedParameterVector = Vec; - pub struct TextureParameterDictionary { dict: Arc, textures: Option, @@ -652,7 +801,7 @@ impl TextureParameterDictionary { } } - fn get_spectrum_texture_or_null( + pub fn get_spectrum_texture_or_null( &self, name: &str, stype: SpectrumType, @@ -738,7 +887,7 @@ impl TextureParameterDictionary { return None; } - fn get_float_texture_or_null(&self, name: &str) -> Option> { + pub fn get_float_texture_or_null(&self, name: &str) -> Option> { for p in &self.dict.params { if p.name != name { continue; diff --git a/src/utils/sampling.rs b/src/utils/sampling.rs new file mode 100644 index 0000000..921896c --- /dev/null +++ b/src/utils/sampling.rs @@ -0,0 +1,239 @@ +use shared::Float; +use shared::utils::sampling::{AliasTable, PiecewiseLinear2D}; +use std::sync::Arc; + +struct PiecewiseLinear2DStorage { + data: Vec, + marginal_cdf: Vec, + conditional_cdf: Vec, + param_values: [Vec; D], +} + +pub struct PiecewiseLinear2DHost { + pub view: PiecewiseLinear2D, + _storage: Arc>, +} + +impl PiecewiseLinear2DHost { + pub fn new( + data: &[Float], + x_size: i32, + y_size: i32, + param_res: [usize; D], + param_values: [&[Float]; D], + normalize: bool, + build_cdf: bool, + ) -> Self { + if build_cdf && !normalize { + panic!("PiecewiseLinear2D::new: build_cdf implies normalize=true"); + } + + let size = Vector2i::new(x_size, y_size); + let inv_patch_size = Vector2f::new(1. / (x_size - 1) as Float, 1. / (y_size - 1) as Float); + + let mut param_size = [0u32; N]; + let mut param_strides = [0u32; N]; + let param_values = std::array::from_fn(|i| param_values[i].to_vec()); + + let mut slices: u32 = 1; + for i in (0..N).rev() { + if param_res[i] < 1 { + panic!("PiecewiseLinear2D::new: parameter resolution must be >= 1!"); + } + param_size[i] = param_res[i] as u32; + param_strides[i] = if param_res[i] > 1 { slices } else { 0 }; + slices *= param_size[i]; + } + + let n_values = (x_size * y_size) as usize; + let mut new_data = vec![0.0; slices as usize * n_values]; + let mut marginal_cdf = if build_cdf { + vec![0.0; slices as usize * y_size as usize] + } else { + Vec::new() + }; + let mut conditional_cdf = if build_cdf { + vec![0.0; slices as usize * n_values] + } else { + Vec::new() + }; + + let mut data_offset = 0; + for slice in 0..slices as usize { + let slice_offset = slice * n_values; + let current_data = &data[data_offset..data_offset + n_values]; + let mut sum = 0.; + + // Construct conditional CDF + if normalize { + for y in 0..(y_size - 1) { + for x in 0..(x_size - 1) { + let i = (y * x_size + x) as usize; + let v00 = current_data[i] as f64; + let v10 = current_data[i + 1] as f64; + let v01 = current_data[i + x_size as usize] as f64; + let v11 = current_data[i + 1 + x_size as usize] as f64; + sum += 0.25 * (v00 + v10 + v01 + v11); + } + } + } + + let normalization = if normalize && sum > 0.0 { + 1.0 / sum as Float + } else { + 1.0 + }; + for k in 0..n_values { + new_data[slice_offset + k] = current_data[k] * normalization; + } + + if build_cdf { + let marginal_slice_offset = slice * y_size as usize; + // Construct marginal CDF + for y in 0..y_size as usize { + let mut cdf_sum = 0.0; + let i_base = y * x_size as usize; + conditional_cdf[slice_offset + i_base] = 0.0; + for x in 0..(x_size - 1) as usize { + let i = i_base + x; + cdf_sum += + 0.5 * (new_data[slice_offset + i] + new_data[slice_offset + i + 1]); + conditional_cdf[slice_offset + i + 1] = cdf_sum; + } + } + // Construct marginal CDF + marginal_cdf[marginal_slice_offset] = 0.0; + let mut marginal_sum = 0.0; + for y in 0..(y_size - 1) as usize { + let cdf1 = conditional_cdf[slice_offset + (y + 1) * x_size as usize - 1]; + let cdf2 = conditional_cdf[slice_offset + (y + 2) * x_size as usize - 1]; + marginal_sum += 0.5 * (cdf1 + cdf2); + marginal_cdf[marginal_slice_offset + y + 1] = marginal_sum; + } + } + data_offset += n_values; + } + + let view = PiecewiseLinear2D { + size, + inv_patch_size, + param_size, + param_strides, + param_values, + data: std::ptr::null(), + marginal_cdf, + conditional_cdf, + }; + + let storage = Arc::new(PiecewiseLinear2DStorage { + data: host_data, + marginal_cdf: host_marginal, + conditional_cdf: host_conditional, + param_values: host_param_values, + }); + + let mut final_view = view_struct; + final_view.data = storage.data.as_ptr(); + final_view.marginal_cdf = storage.marginal_cdf.as_ptr(); + final_view.conditional_cdf = storage.conditional_cdf.as_ptr(); + for i in 0..D { + final_view.param_values[i] = storage.param_values[i].as_ptr(); + } + + Self { + view: final_view, + _storage: storage, + } + } +} + +#[derive(Copy, Debug, Clone)] +pub struct AliasTableHost { + pub view: AliasTable, + _storage: Vec, +} + +impl AliasTableHost { + pub fn new(weights: &[Float]) -> Self { + let n = weights.len(); + if n == 0 { + return Self { + view: AliasTable { + bins: ptr::null(), + size: 0, + }, + _storage: Vec::new(), + }; + } + + let sum: f64 = weights.iter().map(|&w| w as f64).sum(); + assert!(sum > 0.0, "Sum of weights must be positive"); + let mut bins = Vec::with_capacity(n); + for &w in weights { + bins.push(Bin { + p: (w as f64 / sum) as Float, + q: 0.0, + alias: 0, + }); + } + + struct Outcome { + p_hat: f64, + index: usize, + } + + let mut under = Vec::with_capacity(n); + let mut over = Vec::with_capacity(n); + + for (i, bin) in bins.iter().enumerate() { + let p_hat = (bin.p as f64) * (n as f64); + if p_hat < 1.0 { + under.push(Outcome { p_hat, index: i }); + } else { + over.push(Outcome { p_hat, index: i }); + } + } + + while !under.is_empty() && !over.is_empty() { + let un = under.pop().unwrap(); + let ov = over.pop().unwrap(); + + bins[un.index].q = un.p_hat as Float; + bins[un.index].alias = ov.index; + + let p_excess = un.p_hat + ov.p_hat - 1.0; + + if p_excess < 1.0 { + under.push(Outcome { + p_hat: p_excess, + index: ov.index, + }); + } else { + over.push(Outcome { + p_hat: p_excess, + index: ov.index, + }); + } + } + + while let Some(ov) = over.pop() { + bins[ov.index].q = 1.0; + bins[ov.index].alias = ov.index; + } + + while let Some(un) = under.pop() { + bins[un.index].q = 1.0; + bins[un.index].alias = un.index; + } + + let view = AliasTable { + bins: bins.as_ptr(), + size: bins.len() as u32, + }; + + Self { + view, + _storage: bins, + } + } +}