diff --git a/Cargo.toml b/Cargo.toml index 9d4483f..870ebdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,28 +3,52 @@ name = "pbrt" version = "0.1.0" edition = "2024" +[features] +default = [] +use_f64 = [] +cuda = ["cuda_std", "cust", "cuda_builder", "shared/cuda", ] +use_nvtx = [] + [dependencies] anyhow = "1.0.100" bitflags = "2.10.0" bumpalo = "3.19.0" +bytemuck = { version = "1.24.0", features = ["derive"] } enum_dispatch = "0.3.13" exr = "1.73.0" +flate2 = "1.1.5" +gpu = "0.2.3" half = "2.7.1" image_rs = { package = "image", version = "0.25.8" } indicatif = "0.18.3" +lazy_static = "1.5.0" +log = "0.4.29" +memmap2 = "0.9.9" num = "0.4.3" num-integer = "0.1.46" num-traits = "0.2.19" +nvtx = "1.3.0" once_cell = "1.21.3" +parking_lot = "0.12.5" +paste = "1.0.15" qoi = "0.4.1" rand = "0.9.2" rayon = "1.11.0" smallvec = "1.15.1" thiserror = "2.0.17" +unicode-normalization = "0.1.25" +wgpu = "27.0.1" +shared = { path = "shared" } +kernel = { path = "kernels" } +cuda_std = { git = "https://github.com/Rust-GPU/Rust-CUDA", branch = "main", default-features = false, optional = true } +cust = { git = "https://github.com/Rust-GPU/Rust-CUDA", branch = "main", default-features = false, features = ["glam"], optional = true } -[features] -default = [] -use_f64 = [] +[build-dependencies] +spirv-builder = { git = "https://github.com/rust-gpu/rust-gpu", branch = "main", optional = true } +cuda_builder = { git = "https://github.com/Rust-GPU/Rust-CUDA", branch = "main", optional = true } + +[workspace] +members = ["kernels", "shared"] [lints.clippy] excessive_precision = "allow" diff --git a/data/aces_coeffs.dat b/data/aces_coeffs.dat new file mode 100644 index 0000000..0801d37 Binary files /dev/null and b/data/aces_coeffs.dat differ diff --git a/data/aces_scale.dat b/data/aces_scale.dat new file mode 100644 index 0000000..cb6a07d Binary files /dev/null and b/data/aces_scale.dat differ diff --git a/data/dcip3_coeffs.dat b/data/dcip3_coeffs.dat new file mode 100644 index 0000000..38fd62a Binary files /dev/null and b/data/dcip3_coeffs.dat differ diff --git a/data/dcip3_scale.dat b/data/dcip3_scale.dat new file mode 100644 index 0000000..e41d98d Binary files /dev/null and b/data/dcip3_scale.dat differ diff --git a/data/rec2020_coeffs.dat b/data/rec2020_coeffs.dat new file mode 100644 index 0000000..94d3fc9 Binary files /dev/null and b/data/rec2020_coeffs.dat differ diff --git a/data/rec2020_scale.dat b/data/rec2020_scale.dat new file mode 100644 index 0000000..dd9dc7f Binary files /dev/null and b/data/rec2020_scale.dat differ diff --git a/data/srgb_coeffs.dat b/data/srgb_coeffs.dat new file mode 100644 index 0000000..101301f Binary files /dev/null and b/data/srgb_coeffs.dat differ diff --git a/data/srgb_scale.dat b/data/srgb_scale.dat new file mode 100644 index 0000000..40c8e0b Binary files /dev/null and b/data/srgb_scale.dat differ diff --git a/kernels/Cargo.toml b/kernels/Cargo.toml new file mode 100644 index 0000000..07a4c57 --- /dev/null +++ b/kernels/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "kernels" +version = "0.1.0" +edition = "2024" + +[dependencies] +cuda_std = { git = "https://github.com/rust-gpu/rust-cuda", rev = "7fa76f3d717038a92c90bf4a482b0b8dd3259344" } + +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/kernels/src/lib.rs b/kernels/src/lib.rs new file mode 100644 index 0000000..3ed8d63 --- /dev/null +++ b/kernels/src/lib.rs @@ -0,0 +1,447 @@ +use cuda_std::prelude::*; + +pub mod wavefront; +pub mod workitem; + +use cust::context::{CacheConfig, CurrentContext, ResourceLimit}; +use cust::device::DeviceAttribute; +use cust::memory::{DeviceCopy, DeviceMemory}; +use cust::prelude::*; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use std::error::Error; +use std::ffi::c_void; +use std::sync::Arc; + +use crate::Float; +use crate::core::geometry::{Normal, Point, Vector}; +use crate::core::medium::Medium; +use crate::core::options::{PBRTOptions, get_options}; +use crate::impl_gpu_traits; +use crate::impl_math_gpu_traits; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::interval::Interval; + +pub use workitem::{ + EscapedRayQueue, GetBSSRDFAndProbeRayQueue, HitAreaLightQueue, MaterialEvalQueue, + MediumSampleQueue, MediumScatterQueue, PixelSampleStateStorage, RayQueue, ShadowRayQueue, + SubsurfaceScatterQueue, +}; + +#[repr(C, align(16))] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Float4 { + pub v: [f32; 4], +} + +pub type Vec4 = Vector; + +impl From for Float4 { + #[inline] + fn from(vec: Vector) -> Self { + Self { v: vec.0 } + } +} + +impl From for Vec4 { + #[inline] + fn from(storage: Float4) -> Self { + Vector(storage.v) + } +} + +impl_math_gpu_traits!(Vector); +impl_math_gpu_traits!(Normal); +impl_math_gpu_traits!(Point); +impl_gpu_traits!(Interval); +impl_gpu_traits!(Float4); +impl_gpu_traits!(SampledSpectrum); +impl_gpu_traits!(SampledWavelengths); + +struct KernelStats { + description: String, + num_launches: usize, + sum_ms: f32, + min_ms: f32, + max_ms: f32, +} + +impl KernelStats { + fn new(description: &str) -> Self { + Self { + description: description.to_string(), + num_launches: 0, + sum_ms: 0.0, + min_ms: 0.0, + max_ms: 0.0, + } + } +} + +struct ProfilerEvent { + start: Event, + stop: Event, + active: bool, + stats: Option>>, +} + +impl ProfilerEvent { + fn new() -> Result { + let start = Event::new(EventFlags::DEFAULT)?; + let stop = Event::new(EventFlags::DEFAULT)?; + Ok(Self { + start, + stop, + active: false, + stats: None, + }) + } + + fn sync(&mut self) { + if !self.active { + return; + } + + if self.stop.synchronize().is_ok() { + // Check timing between start and stop + match self.stop.elapsed_time_f32(&self.start) { + Ok(ms) => { + if let Some(stats_arc) = &self.stats { + let mut stats = stats_arc.lock(); + stats.num_launches += 1; + if stats.num_launches == 1 { + stats.sum_ms = ms; + stats.min_ms = ms; + stats.max_ms = ms; + } else { + stats.sum_ms += ms; + stats.min_ms = stats.min_ms.min(ms); + stats.max_ms = stats.max_ms.max(ms); + } + } + } + Err(e) => log::error!("Failed to get elapsed time: {:?}", e), + } + } + self.active = false; + } +} + +// --- Profiler Manager --- + +struct Profiler { + kernel_stats: Vec>>, + event_pool: Vec, + pool_offset: usize, +} + +impl Profiler { + fn new() -> Self { + Self { + kernel_stats: Vec::new(), + event_pool: Vec::new(), + pool_offset: 0, + } + } + + /// Prepares an event from the pool. + /// Returns a mutable reference to the event, valid as long as the borrow of self. + fn prepare<'a>(&'a mut self, description: &str) -> &'a mut ProfilerEvent { + // Grow pool if empty or needed (simple heuristic) + if self.event_pool.is_empty() { + for _ in 0..128 { + if let Ok(e) = ProfilerEvent::new() { + self.event_pool.push(e); + } + } + } + + if self.pool_offset >= self.event_pool.len() { + self.pool_offset = 0; + } + + let idx = self.pool_offset; + self.pool_offset += 1; + + let pe = &mut self.event_pool[idx]; + + if pe.active { + pe.sync(); + } + + pe.active = true; + pe.stats = None; + + // Find or create stats + let mut found = None; + for s in &self.kernel_stats { + if s.lock().description == description { + found = Some(s.clone()); + break; + } + } + + if found.is_none() { + let new_stats = Arc::new(Mutex::new(KernelStats::new(description))); + self.kernel_stats.push(new_stats.clone()); + found = Some(new_stats); + } + + pe.stats = found; + pe + } +} + +pub struct GpuState { + context: Context, + stream: Stream, + profiler: Profiler, +} + +impl GpuState { + fn init(device_index: u32) -> Result> { + cust::init(CudaFlags::empty())?; + + let device = Device::get_device(device_index)?; + + let name = device.name().unwrap_or_else(|_| "Unknown".into()); + let memory = device.total_memory().unwrap_or(0); + let memory_gb = memory as f64 / (1024.0 * 1024.0 * 1024.0); + + let major = device + .get_attribute(DeviceAttribute::ComputeCapabilityMajor) + .unwrap_or(0); + let minor = device + .get_attribute(DeviceAttribute::ComputeCapabilityMinor) + .unwrap_or(0); + + log::info!( + "Selected GPU: {} ({:.2} GB, SM {}.{})", + name, + memory_gb, + major, + minor + ); + + let has_unified = device + .get_attribute(DeviceAttribute::UnifiedAddressing) + .unwrap_or(0); + if has_unified == 0 { + panic!("Selected GPU does not support unified addressing."); + } + + let context = Context::new(device)?; + + CurrentContext::set_resource_limit(ResourceLimit::StackSize, 8192)?; + let stack_size = CurrentContext::get_resource_limit(ResourceLimit::StackSize)?; + log::info!("Reset stack size to {}", stack_size); + + CurrentContext::set_resource_limit(ResourceLimit::PrintfFifoSize, 32 * 1024 * 1024)?; + CurrentContext::set_cache_config(CacheConfig::PreferL1)?; + + let stream = Stream::new(StreamFlags::DEFAULT, None)?; + + Ok(Self { + context, + stream, + profiler: Profiler::new(), + }) + } +} + +lazy_static! { + static ref GPU_STATE: Mutex> = Mutex::new(None); +} + +pub fn gpu_init() { + if !get_options().use_gpu { + return; + } + + let device_id = get_options().gpu_device.unwrap_or(0); + log::info!("Initializing GPU Device {}", device_id); + + match GpuState::init(device_id) { + Ok(state) => { + #[cfg(feature = "use_nvtx")] + nvtx::name_thread("MAIN_THREAD"); + *GPU_STATE.lock() = Some(state); + } + Err(e) => { + panic!("Failed to initialize GPU: {:?}", e); + } + } +} + +pub fn gpu_thread_init() { + if let Some(state) = GPU_STATE.lock().as_ref() { + if let Err(e) = CurrentContext::set_current(&state.context) { + log::error!("Failed to set CUDA context for thread: {:?}", e); + } + } +} + +pub fn gpu_wait() { + let mut guard = GPU_STATE.lock(); + if let Some(state) = guard.as_mut() { + if let Err(e) = state.stream.synchronize() { + log::error!("GPU Wait failed: {:?}", e); + } + } +} + +/// Launches a parallel for loop on the GPU. +/// +/// # Arguments +/// * `description`: Name for profiling. +/// * `n_items`: Total items (threads). +/// * `function`: Compiled kernel function handle. +/// * `params`: Kernel parameters (must be DeviceCopy). +pub fn gpu_parallel_for( + description: &str, + n_items: i32, + function: &Function, + params: &T, +) { + #[cfg(feature = "use_nvtx")] + nvtx::range_push(description); + + let mut guard = GPU_STATE.lock(); + let state = guard.as_mut().expect("GPU not initialized"); + + let (_, block_size) = match function.suggested_launch_configuration(0, 0.into()) { + Ok(cfg) => cfg, + Err(e) => panic!( + "Failed to calculate launch config for {}: {:?}", + description, e + ), + }; + + #[cfg(debug_assertions)] + log::debug!("[{}] Block size: {}", description, block_size); + + let grid_size = (n_items as u32 + block_size - 1) / block_size; + + let stream = &state.stream; + let profiler = &mut state.profiler; + + // Save the index we are about to use so we can retrieve the STOP event later + let event_idx = profiler.pool_offset; + + { + let pe = profiler.prepare(description); + if let Err(e) = pe.start.record(stream) { + log::error!("Failed to record start event: {:?}", e); + } + } + + let params_ptr = params as *const T as *mut c_void; + let n_items_ptr = &n_items as *const i32 as *mut c_void; + let args = [params_ptr, n_items_ptr]; + + unsafe { + if let Err(e) = + state + .stream + .launch(function, (grid_size, 1, 1), (block_size, 1, 1), 0, &args) + { + panic!("CUDA Launch failed for {}: {:?}", description, e); + } + } + + // Retrieve the specific event we just set up. + // Pool_offset was incremented in prepare(). + // If event_idx was the one used, the event is at event_idx. + if event_idx < profiler.event_pool.len() { + let pe = &mut profiler.event_pool[event_idx]; + if let Err(e) = pe.stop.record(stream) { + log::error!("Failed to record stop event: {:?}", e); + } + } + + #[cfg(debug_assertions)] + let _ = state.stream.synchronize(); + + #[cfg(feature = "use_nvtx")] + nvtx::range_pop(); +} + +pub fn report_kernel_stats() { + let mut guard = GPU_STATE.lock(); + if let Some(state) = guard.as_mut() { + let _ = state.stream.synchronize(); + + // Process all pending events + for pe in &mut state.profiler.event_pool { + if pe.active { + pe.sync(); + } + } + + let mut total_ms = 0.0; + for s in &state.profiler.kernel_stats { + total_ms += s.lock().sum_ms; + } + + println!("Wavefront Kernel Profile:"); + for s in &state.profiler.kernel_stats { + let stats = s.lock(); + let percent = if total_ms > 0.0 { + 100.0 * stats.sum_ms / total_ms + } else { + 0.0 + }; + println!( + " {:<45} {:5} launches {:9.2} ms / {:5.1}% (avg {:6.3})", + stats.description, + stats.num_launches, + stats.sum_ms, + percent, + if stats.num_launches > 0 { + stats.sum_ms / stats.num_launches as f32 + } else { + 0.0 + } + ); + } + println!("\nTotal: {:.2} ms", total_ms); + } +} + +pub fn gpu_memset(dst: &mut DeviceSlice, value: u8) { + unsafe { + let ptr = dst.as_raw_ptr(); // Returns CUdeviceptr (u64) + let len = dst.len() * std::mem::size_of::(); + + // We need the `cust::external::cuda` or equivalent sys crate function + + log::warn!("gpu_memset requested but raw memset not exposed via safe cust API yet."); + } +} + +#[macro_export] +macro_rules! impl_gpu_traits { + ($name:ty) => { + unsafe impl cust::memory::DeviceCopy for $name {} + unsafe impl bytemuck::Zeroable for $name {} + unsafe impl bytemuck::Pod for $name {} + }; +} + +#[macro_export] +macro_rules! impl_math_gpu_traits { + ($Struct:ident) => { + #[cfg(feature = "use_gpu")] + unsafe impl cust::memory::DeviceCopy for $Struct where + T: cust::memory::DeviceCopy + Copy + { + } + + unsafe impl bytemuck::Zeroable for $Struct where + T: bytemuck::Zeroable + { + } + + unsafe impl bytemuck::Pod for $Struct where T: bytemuck::Pod {} + }; +} diff --git a/kernels/src/wavefront/mod.rs b/kernels/src/wavefront/mod.rs new file mode 100644 index 0000000..cbba9a1 --- /dev/null +++ b/kernels/src/wavefront/mod.rs @@ -0,0 +1,47 @@ +use image_rs::Pixel; + +use crate::camera::Camera; +use crate::core::film::Film; +use crate::core::filter::Filter; +use crate::core::sampler::Sampler; +use crate::core::scene::BasicScene; +use crate::lights::Light; +use crate::lights::LightSampler; +use crate::{ + EscapedRayQueue, GetBSSRDFAndProbeRayQueue, HitAreaLightQueue, MaterialEvalQueue, + MediumSampleQueue, MediumScatterQueue, PixelSampleStateStorage, RayQueue, ShadowRayQueue, + SubsurfaceScatterQueue, +}; +use std::sync::Arc; + +pub struct WavefrontPathIntegrator { + pub film: Film, + pub filter: Filter, + pub sampler: Sampler, + pub camera: Arc, + pub light_sampler: LightSampler, + pub infinite_lights: Option>>, + pub max_depth: i32, + pub samples_per_pixel: i32, + pub regularize: bool, + pub scanlines_per_pixel: i32, + pub max_queue_size: i32, + pub pixel_sample_state: PixelSampleStateStorage, + pub ray_queue: [RayQueue; 2], + pub hit_area_light_queue: HitAreaLightQueue, + pub shadow_ray_queue: ShadowRayQueue, + pub escaped_ray_queue: Option, + pub basic_material_queue: Option, + pub universal_material_queue: Option, + pub medium_sample_queue: Option, + pub medium_scatter_queue: Option, + pub bssrf_queue: Option, + pub subsurface_queue: Option, +} + +#[cfg(feature = "use_gpu")] +impl WavefrontPathIntegrator { + pub fn new(scene: BasicScene) -> Self { + todo!() + } +} diff --git a/kernels/src/workitem.rs b/kernels/src/workitem.rs new file mode 100644 index 0000000..f85ac43 --- /dev/null +++ b/kernels/src/workitem.rs @@ -0,0 +1,535 @@ +#![allow(clippy::too_many_arguments)] +use super::Float4; +use crate::Float; +use crate::core::geometry::{Normal3f, Point2f, Point2i, Point3f, Point3fi, Ray, Vector3f}; +use crate::lights::LightSampleContext; +use crate::soa_struct; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use cust::memory::{CopyDestination, DeviceMemory}; +use cust::prelude::*; + +#[macro_export] +macro_rules! soa_struct { + ( + $(#[$outer:meta])* + pub struct $name:ident { + $( + pub $field:ident : $type:ty + ),* $(,)? + } +) => { + #[cfg(feature = "use_gpu")] + $(#[$outer])* + pub struct $name { + capacity: u32, + pub count: cust::memory::DeviceBuffer, + $( + pub $field: cust::memory::DeviceBuffer<$type>, + )* + } + + #[cfg(feature = "use_gpu")] + impl $name { + pub fn new(capacity: usize) -> cust::error::CudaResult { + use cust::memory::DeviceBuffer; + Ok(Self { + capacity: capacity as u32, + count: DeviceBuffer::zeroed(1)?, + $( + $field: DeviceBuffer::zeroed(capacity)?, + )* + }) + } + + pub fn len(&self) -> cust::error::CudaResult { + let mut host_count = [0u32; 1]; + self.count.copy_to(&mut host_count)?; + Ok(host_count[0]) + } + + pub fn reset(&mut self) -> cust::error::CudaResult<()> { + self.count.copy_from(&[0]) + } + + // Generate the View name + pub fn as_view(&mut self) -> paste::paste! { [<$name View>] } { + paste::paste! { + [<$name View>] { + capacity: self.capacity, + count: self.count.as_device_ptr().as_mut_ptr(), + $( + $field: self.$field.as_device_ptr().as_raw() as *mut $type, + )* + } + } + } + } + + paste::paste! { + #[repr(C)] + #[derive(Clone, Copy)] + pub struct [<$name View>] { + pub capacity: u32, + pub count: *mut u32, + $( + pub $field: *mut $type, + )* + } + + unsafe impl cust::memory::DeviceCopy for [<$name View>] {} + + impl [<$name View>] { + // The raw push that fills every field + #[cfg(feature = "use_gpu")] + pub unsafe fn push(&self, $( $field : $type ),* ) -> Option { + use core::sync::atomic::{AtomicU32, Ordering}; + + let index = unsafe { + let counter_ptr = self.count as *mut AtomicU32; + (*counter_ptr).fetch_add(1, Ordering::Relaxed) + }; + + if index >= self.capacity { + return None; + } + + unsafe { + $( + *self.$field.add(index as usize) = $field; + )* + } + + Some(index) + } + + #[cfg(feature = "use_gpu")] + pub unsafe fn size(&self) -> u32 { + use core::sync::atomic::{AtomicU32, Ordering}; + unsafe { + (*(self.count as *const AtomicU32)).load(Ordering::Relaxed) + } + } + + $( + #[cfg(feature = "use_gpu")] + pub fn [<$field _ptr>](&self) -> *mut $type { + self.$field + } + )* + } + } + }; +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RaySamplesDirect { + pub u: Point2f, + pub uc: Float, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RaySamplesIndirect { + pub uc: Float, + pub rr: Float, + pub u: Point2f, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RaySamplesSubsurface { + pub uc: Float, + pub u: Point2f, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RaySamples { + pub direct: RaySamplesDirect, + pub indirect: RaySamplesIndirect, + pub have_subsurface: bool, + pub subsurface: RaySamplesSubsurface, +} + +soa_struct! { + pub struct RayQueue { + pub ray_o: Point3f, + pub ray_d: Vector3f, + + pub depth: i32, + pub lambda: SampledWavelengths, + pub pixel_index: u32, + + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + + pub ctx_pi: Point3f, + pub ctx_n: Normal3f, + pub ctx_ns: Normal3f, + + pub eta_scale: Float, + pub specular_bounce: u32, + pub any_non_specular_bounces: u32, + } +} + +soa_struct! { + pub struct PixelSampleStateStorage { + pub p_pixel: Point2i, + pub l: SampledSpectrum, + pub lambda: SampledWavelengths, + pub filter_weight: Float, + pub visible_surface: u32, + pub camera_ray_weight: SampledSpectrum, + + pub rs_direct_packed: Float4, + pub rs_indirect_packed: Float4, + pub rs_subsurface_packed: Float4, + } +} + +soa_struct! { + pub struct EscapedRayQueue { + pub ray_o: Point3f, + pub ray_d: Vector3f, + pub depth: i32, + pub lambda: SampledWavelengths, + pub pixel_index: u32, + pub beta: SampledSpectrum, + pub specular_bounce: u32, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub ctx_pi: Point3f, + pub ctx_n: Normal3f, + pub ctx_ns: Normal3f, + } +} + +soa_struct! { + pub struct HitAreaLightQueue { + pub area_light_id: u32, // Light ID + pub p: Point3f, + pub n: Normal3f, + pub uv: Point2f, + pub wo: Vector3f, + pub lambda: SampledWavelengths, + pub depth: i32, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub ctx_pi: Point3f, + pub ctx_n: Normal3f, + pub ctx_ns: Normal3f, + pub specular_bounce: u32, + pub pixel_index: u32, + } +} + +soa_struct! { + pub struct ShadowRayQueue { + pub ray_o: Point3f, + pub ray_d: Vector3f, + pub t_max: Float, + pub lambda: SampledWavelengths, + pub ld: SampledSpectrum, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub pixel_index: u32, + } +} + +soa_struct! { + pub struct GetBSSRDFAndProbeRayQueue { + pub material_id: u32, + pub lambda: SampledWavelengths, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub p: Point3f, + pub wo: Vector3f, + pub n: Normal3f, + pub ns: Normal3f, + pub dpdus: Vector3f, + pub uv: Point2f, + pub depth: i32, + pub mi_inside: u32, + pub mi_outside: u32, + pub eta_scale: Float, + pub pixel_index: u32, + } +} + +soa_struct! { + pub struct SubsurfaceScatterQueue { + pub p0: Point3f, + pub p1: Point3f, + pub depth: i32, + pub material_id: u32, + pub lambda: SampledWavelengths, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub mi_inside: u32, + pub mi_outside: u32, + pub eta_scale: Float, + pub pixel_index: u32, + } +} + +soa_struct! { + pub struct MediumSampleQueue { + pub ray_o: Point3f, + pub ray_d: Vector3f, + pub t_max: Float, + pub lambda: SampledWavelengths, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub pixel_index: u32, + + pub ctx_pi: Point3f, + pub ctx_n: Normal3f, + pub ctx_ns: Normal3f, + + pub specular_bounce: u32, + pub any_non_specular_bounces: u32, + pub eta_scale: Float, + + pub area_light_id: u32, + pub pi: Point3fi, + pub n: Normal3f, + pub dpdu: Vector3f, + pub dpdv: Vector3f, + pub wo: Vector3f, + pub uv: Point2f, + pub material_id: u32, + pub ns: Normal3f, + pub dpdus: Vector3f, + pub dpdvs: Vector3f, + pub dndus: Normal3f, + pub dndvs: Normal3f, + pub face_index: i32, + pub mi_inside: u32, + pub mi_outside: u32, + } +} + +soa_struct! { + pub struct MaterialEvalQueue { + pub material_id: u32, + pub pi: Point3fi, + pub n: Normal3f, + pub dpdu: Vector3f, + pub dpdv: Vector3f, + pub time: Float, + pub depth: i32, + pub ns: Normal3f, + pub dpdus: Vector3f, + pub dpdvs: Vector3f, + pub dndus: Normal3f, + pub dndvs: Normal3f, + pub uv: Point2f, + pub face_index: i32, + pub lambda: SampledWavelengths, + pub pixel_index: u32, + pub any_non_specular_bounces: u32, + pub wo: Vector3f, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub eta_scale: Float, + pub mi_inside: u32, + pub mi_outside: u32, + } +} + +soa_struct! { + pub struct MediumScatterQueue { + pub p: Point3f, + pub depth: usize, + pub lambda: SampledWavelengths, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub wo: Vector3f, + pub time: Float, + pub eta_scale: Float, + pub pixel_index: usize, + + // ID + pub phase_function: u32, + pub medium: u32, + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RayWorkItem { + pub ray: Ray, + pub depth: i32, + pub lambda: SampledWavelengths, + pub pixel_index: u32, + pub beta: SampledSpectrum, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub prev_intr_ctx: LightSampleContext, + pub eta_scale: Float, + pub specular_bounce: bool, + pub any_non_specular_bounces: bool, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EscapedRayWorkItem { + pub ray_o: Point3f, + pub ray_d: Vector3f, + pub depth: i32, + pub lambda: SampledWavelengths, + pub pixel_index: u32, + pub beta: SampledSpectrum, + pub specular_bounce: bool, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub prev_intr_ctx: LightSampleContext, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ShadowRayWorkItem { + pub ray: Ray, + pub t_max: Float, + pub lambda: SampledWavelengths, + pub ld: SampledSpectrum, + pub r_u: SampledSpectrum, + pub r_l: SampledSpectrum, + pub pixel_index: u32, +} + +impl RayQueueView { + #[cfg(feature = "use_gpu")] + pub unsafe fn push_work_item(&self, item: RayWorkItem) -> Option { + unsafe { + self.push( + item.ray.o, + item.ray.d, + item.depth, + item.lambda, + item.pixel_index, + item.beta, + item.r_u, + item.r_l, + item.prev_intr_ctx.pi.into(), + item.prev_intr_ctx.n, + item.prev_intr_ctx.ns, + item.eta_scale, + if item.specular_bounce { 1 } else { 0 }, + if item.any_non_specular_bounces { 1 } else { 0 }, + ) + } + } +} + +impl EscapedRayQueueView { + #[cfg(feature = "use_gpu")] + pub unsafe fn push_work_item(&self, r: &RayWorkItem) -> Option { + unsafe { + self.push( + r.ray.o, + r.ray.d, + r.depth, + r.lambda, + r.pixel_index, + r.beta, + if r.specular_bounce { 1 } else { 0 }, + r.r_u, + r.r_l, + r.prev_intr_ctx.pi.into(), + r.prev_intr_ctx.n, + r.prev_intr_ctx.ns, + ) + } + } +} + +impl PixelSampleStateStorageView { + #[cfg(feature = "use_gpu")] + pub unsafe fn get_samples(&self, index: u32) -> RaySamples { + let i = index as usize; + + let (dir, ind, ss) = unsafe { + ( + *self.rs_direct_packed.add(i), + *self.rs_indirect_packed.add(i), + *self.rs_subsurface_packed.add(i), + ) + }; + + let direct_u = Point2f::new(dir.v[0], dir.v[1]); + let direct_uc = dir.v[2]; + let flags = dir.v[3] as i32; + let have_subsurface = (flags & 1) != 0; + + let indirect_uc = ind.v[0]; + let indirect_rr = ind.v[1]; + let indirect_u = Point2f::new(ind.v[2], ind.v[3]); + + let subsurface_uc = ss.v[0]; + let subsurface_u = Point2f::new(ss.v[1], ss.v[2]); + + RaySamples { + direct: RaySamplesDirect { + u: direct_u, + uc: direct_uc, + }, + indirect: RaySamplesIndirect { + uc: indirect_uc, + rr: indirect_rr, + u: indirect_u, + }, + have_subsurface, + subsurface: RaySamplesSubsurface { + uc: subsurface_uc, + u: subsurface_u, + }, + } + } + + #[cfg(feature = "use_gpu")] + pub unsafe fn set_samples(&self, index: u32, rs: RaySamples) { + if index >= self.capacity { + return; + } + let i = index as usize; + + let flags = if rs.have_subsurface { 1.0 } else { 0.0 }; + let dir = Float4 { + v: [rs.direct.u.0[0], rs.direct.u.0[1], rs.direct.uc, flags], + }; + + let ind = Float4 { + v: [ + rs.indirect.uc, + rs.indirect.rr, + rs.indirect.u.0[0], + rs.indirect.u.0[1], + ], + }; + + unsafe { + *self.rs_direct_packed.add(i) = dir; + *self.rs_indirect_packed.add(i) = ind; + } + + if rs.have_subsurface { + let ss = Float4 { + v: [ + rs.subsurface.uc, + rs.subsurface.u.0[0], + rs.subsurface.u.0[1], + 0.0, + ], + }; + unsafe { + *self.rs_subsurface_packed.add(i) = ss; + } + } + } +} diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..5e22169 --- /dev/null +++ b/shared/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/camera/mod.rs b/shared/src/camera/mod.rs similarity index 71% rename from src/camera/mod.rs rename to shared/src/camera/mod.rs index 7e2293d..70305bd 100644 --- a/src/camera/mod.rs +++ b/shared/src/camera/mod.rs @@ -9,27 +9,33 @@ pub use realistic::RealisticCamera; pub use spherical::SphericalCamera; use crate::core::film::{Film, FilmTrait}; -use crate::core::interaction::Interaction; -use crate::core::medium::Medium; -use crate::core::pbrt::{Float, RenderingCoordinateSystem, lerp}; -use crate::core::sampler::CameraSample; -use crate::geometry::{ +use crate::core::geometry::{ Normal3f, Point2f, Point2i, Point3f, Ray, RayDifferential, Vector3f, VectorLike, }; +use crate::core::interaction::Interaction; +use crate::core::medium::Medium; +use crate::core::options::RenderingCoordinateSystem; +use crate::core::pbrt::Float; +use crate::core::sampler::CameraSample; use crate::image::ImageMetadata; use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::error::FileLoc; +use crate::utils::math::lerp; +use crate::utils::parameters::ParameterDictionary; use crate::utils::transform::{AnimatedTransform, Transform}; use enum_dispatch::enum_dispatch; use std::sync::Arc; +#[repr(C)] #[derive(Debug, Clone)] pub struct CameraRay { pub ray: Ray, pub weight: SampledSpectrum, } +#[repr(C)] #[derive(Debug, Clone)] pub struct CameraWiSample { wi_spec: SampledSpectrum, @@ -40,10 +46,10 @@ pub struct CameraWiSample { p_lens: Interaction, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CameraTransform { - render_from_camera: AnimatedTransform, - world_from_render: Transform, + pub render_from_camera: AnimatedTransform, + pub world_from_render: Transform, } impl CameraTransform { @@ -80,7 +86,7 @@ impl CameraTransform { } } - pub fn camera_from_world(&self, time: Float) -> Transform { + pub fn camera_from_world(&self, time: Float) -> Transform { (self.world_from_render * self.render_from_camera.interpolate(time)).inverse() } @@ -109,12 +115,49 @@ impl CameraTransform { } } +#[repr(C)] +#[derive(Clone, Debug)] +pub struct CameraBaseParameters { + pub camera_transform: CameraTransform, + pub shutter_open: Float, + pub shutter_close: Float, + pub film: Arc, + pub medium_id: i32, +} + +impl CameraBaseParameters { + pub fn new( + camera_transform: &CameraTransform, + film: Arc, + medium: Arc, + params: &ParameterDictionary, + loc: &FileLoc, + ) -> Self { + let mut shutter_open = params.get_one_float("shutteropen", 0.); + let mut shutter_close = params.get_one_float("shutterclose", 1.); + if shutter_close < shutter_open { + eprint!( + "{}: Shutter close time {} < shutter open {}. Swapping", + loc, shutter_close, shutter_open + ); + std::mem::swap(&mut shutter_open, &mut shutter_close); + } + CameraBaseParameters { + camera_transform: Arc::new(camera_transform.clone()), + shutter_open, + shutter_close, + film, + medium: Some(medium), + } + } +} + #[derive(Debug)] pub struct CameraBase { pub camera_transform: CameraTransform, pub shutter_open: Float, pub shutter_close: Float, - pub film: Film, + pub film: Arc, pub medium: Option>, pub min_pos_differential_x: Vector3f, pub min_pos_differential_y: Vector3f, @@ -124,18 +167,32 @@ pub struct CameraBase { impl CameraBase { pub fn init_metadata(&self, metadata: &mut ImageMetadata) { - let camera_from_world: Transform = + let camera_from_world: Transform = self.camera_transform.camera_from_world(self.shutter_open); metadata.camera_from_world = Some(camera_from_world.get_matrix()); } + + pub fn new(p: CameraBaseParameters) -> Self { + Self { + camera_transform: p.camera_transform.as_ref().clone(), + shutter_open: p.shutter_open, + shutter_close: p.shutter_close, + film: p.film.clone(), + medium: p.medium, + min_pos_differential_x: Vector3f::default(), + min_pos_differential_y: Vector3f::default(), + min_dir_differential_x: Vector3f::default(), + min_dir_differential_y: Vector3f::default(), + } + } } #[enum_dispatch] pub trait CameraTrait { fn base(&self) -> &CameraBase; - fn get_film(&self) -> Film { - self.base().film.clone() + fn get_film(&self) -> &Film { + &self.base().film } fn resolution(&self) -> Point2i { @@ -260,6 +317,39 @@ pub enum Camera { Realistic(RealisticCamera), } +impl Camera { + pub fn create( + name: &str, + params: &ParameterDictionary, + medium: Medium, + camera_transform: &CameraTransform, + film: Arc, + loc: &FileLoc, + ) -> Result { + match name { + "perspective" => { + let camera = + PerspectiveCamera::create(params, camera_transform, film, medium, loc)?; + Ok(Camera::Perspective(camera)) + } + "orthographic" => { + let camera = + OrthographicCamera::create(params, camera_transform, film, medium, loc)?; + Ok(Camera::Orthographic(camera)) + } + "realistic" => { + let camera = RealisticCamera::create(params, camera_transform, film, medium, loc)?; + Ok(Camera::Realistic(camera)) + } + "spherical" => { + let camera = SphericalCamera::create(params, camera_transform, film, medium, loc)?; + Ok(Camera::Spherical(camera)) + } + _ => Err(format!("Camera type '{}' unknown at {}", name, loc)), + } + } +} + #[derive(Debug)] pub struct LensElementInterface { pub curvature_radius: Float, diff --git a/src/camera/orthographic.rs b/shared/src/camera/orthographic.rs similarity index 63% rename from src/camera/orthographic.rs rename to shared/src/camera/orthographic.rs index 3ef514d..0eb8044 100644 --- a/src/camera/orthographic.rs +++ b/shared/src/camera/orthographic.rs @@ -1,21 +1,26 @@ -use super::{CameraBase, CameraRay, CameraTrait}; -use crate::core::film::FilmTrait; -use crate::core::pbrt::Float; -use crate::core::sampler::CameraSample; -use crate::geometry::{ +use super::{CameraBase, CameraBaseParameters, CameraRay, CameraTrait, CameraTransform}; +use crate::core::film::{Film, FilmTrait}; +use crate::core::geometry::{ Bounds2f, Point2f, Point3f, Ray, RayDifferential, Vector2f, Vector3f, VectorLike, }; +use crate::core::medium::Medium; +use crate::core::options::get_options; +use crate::core::pbrt::Float; +use crate::core::sampler::CameraSample; use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::error::FileLoc; +use crate::utils::parameters::ParameterDictionary; use crate::utils::sampling::sample_uniform_disk_concentric; -use crate::utils::transform::Transform; +use crate::utils::transform::TransformGeneric; +use std::sync::Arc; #[derive(Debug)] pub struct OrthographicCamera { pub base: CameraBase, - pub screen_from_camera: Transform, - pub camera_from_raster: Transform, - pub raster_from_screen: Transform, - pub screen_from_raster: Transform, + pub screen_from_camera: TransformGeneric, + pub camera_from_raster: TransformGeneric, + pub raster_from_screen: TransformGeneric, + pub screen_from_raster: TransformGeneric, pub lens_radius: Float, pub focal_distance: Float, pub dx_camera: Vector3f, @@ -29,23 +34,21 @@ impl OrthographicCamera { lens_radius: Float, focal_distance: Float, ) -> Self { - let ndc_from_screen: Transform = Transform::scale( + let ndc_from_screen: TransformGeneric = TransformGeneric::scale( 1. / (screen_window.p_max.x() - screen_window.p_min.x()), 1. / (screen_window.p_max.y() - screen_window.p_min.y()), 1., - ) * Transform::translate(Vector3f::new( - -screen_window.p_min.x(), - -screen_window.p_max.y(), - 0., - )); - let raster_from_ndc = Transform::scale( + ) * TransformGeneric::translate( + Vector3f::new(-screen_window.p_min.x(), -screen_window.p_max.y(), 0.), + ); + let raster_from_ndc = TransformGeneric::scale( base.film.full_resolution().x() as Float, -base.film.full_resolution().y() as Float, 1., ); let raster_from_screen = raster_from_ndc * ndc_from_screen; let screen_from_raster = raster_from_screen.inverse(); - let screen_from_camera = Transform::orthographic(0., 1.); + let screen_from_camera = TransformGeneric::orthographic(0., 1.); let camera_from_raster = screen_from_camera.inverse() * screen_from_raster; let dx_camera = camera_from_raster.apply_to_vector(Vector3f::new(1., 0., 0.)); let dy_camera = camera_from_raster.apply_to_vector(Vector3f::new(0., 1., 0.)); @@ -66,6 +69,49 @@ impl OrthographicCamera { dy_camera, } } + + pub fn create( + params: &ParameterDictionary, + camera_transform: &CameraTransform, + film: Arc, + medium: Medium, + loc: &FileLoc, + ) -> Result { + let full_res = film.full_resolution(); + let camera_params = + CameraBaseParameters::new(camera_transform, film, medium.into(), params, loc); + let base = CameraBase::new(camera_params); + let lens_radius = params.get_one_float("lensradius", 0.); + let focal_distance = params.get_one_float("focaldistance", 1e6); + let frame = params.get_one_float( + "frameaspectratio", + full_res.x() as Float / full_res.y() as Float, + ); + + let mut screen = if frame > 1. { + Bounds2f::from_points(Point2f::new(-frame, -1.), Point2f::new(frame, 1.)) + } else { + Bounds2f::from_points(Point2f::new(-1., -1. / frame), Point2f::new(1., 1. / frame)) + }; + + let sw = params.get_float_array("screenwindow"); + if !sw.is_empty() { + if get_options().fullscreen { + eprint!("Screenwindow is ignored in fullscreen mode"); + } else { + if sw.len() == 4 { + screen = Bounds2f::from_points( + Point2f::new(sw[0], sw[2]), + Point2f::new(sw[1], sw[3]), + ); + } else { + return Err(format!("{}: screenwindow param must have four values", loc)); + } + } + } + + Ok(Self::new(base, screen, lens_radius, focal_distance)) + } } impl CameraTrait for OrthographicCamera { diff --git a/src/camera/perspective.rs b/shared/src/camera/perspective.rs similarity index 61% rename from src/camera/perspective.rs rename to shared/src/camera/perspective.rs index 120e21c..4ddb38e 100644 --- a/src/camera/perspective.rs +++ b/shared/src/camera/perspective.rs @@ -1,23 +1,28 @@ -use super::{CameraBase, CameraRay, CameraTrait}; -use crate::camera; -use crate::core::film::FilmTrait; +use super::{CameraBase, CameraRay, CameraTrait, CameraTransform}; +use crate::camera::CameraBaseParameters; +use crate::core::film::{Film, FilmTrait}; use crate::core::filter::FilterTrait; -use crate::core::pbrt::Float; -use crate::core::sampler::CameraSample; -use crate::geometry::{ +use crate::core::geometry::{ Bounds2f, Point2f, Point3f, Ray, RayDifferential, Vector2f, Vector3f, VectorLike, }; +use crate::core::medium::Medium; +use crate::core::options::get_options; +use crate::core::pbrt::Float; +use crate::core::sampler::CameraSample; use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::error::FileLoc; +use crate::utils::parameters::ParameterDictionary; use crate::utils::sampling::sample_uniform_disk_concentric; use crate::utils::transform::Transform; +use std::sync::Arc; #[derive(Debug)] pub struct PerspectiveCamera { pub base: CameraBase, - pub screen_from_camera: Transform, - pub camera_from_raster: Transform, - pub raster_from_screen: Transform, - pub screen_from_raster: Transform, + pub screen_from_camera: Transform, + pub camera_from_raster: Transform, + pub raster_from_screen: Transform, + pub screen_from_raster: Transform, pub lens_radius: Float, pub focal_distance: Float, pub dx_camera: Vector3f, @@ -28,12 +33,14 @@ pub struct PerspectiveCamera { impl PerspectiveCamera { pub fn new( base: CameraBase, - screen_from_camera: &Transform, + fov: Float, screen_window: Bounds2f, lens_radius: Float, focal_distance: Float, ) -> Self { - let ndc_from_screen: Transform = Transform::scale( + let screen_from_camera = + Transform::perspective(fov, 1e-2, 1000.).expect("Could not create perspective"); + let ndc_from_screen = Transform::scale( 1. / (screen_window.p_max.x() - screen_window.p_min.x()), 1. / (screen_window.p_max.y() - screen_window.p_min.y()), 1., @@ -59,9 +66,10 @@ impl PerspectiveCamera { let w_corner_camera = (camera_from_raster.apply_to_point(p_corner) - Point3f::new(0., 0., 0.)).normalize(); let cos_total_width = w_corner_camera.z(); + Self { base, - screen_from_camera: *screen_from_camera, + screen_from_camera, camera_from_raster, raster_from_screen, screen_from_raster, @@ -72,6 +80,50 @@ impl PerspectiveCamera { cos_total_width, } } + + pub fn create( + params: &ParameterDictionary, + camera_transform: &CameraTransform, + film: Arc, + medium: Medium, + loc: &FileLoc, + ) -> Result { + let full_res = film.full_resolution(); + let camera_params = + CameraBaseParameters::new(camera_transform, film, medium.into(), params, loc); + let base = CameraBase::new(camera_params); + let lens_radius = params.get_one_float("lensradius", 0.); + let focal_distance = params.get_one_float("focaldistance", 1e6); + let frame = params.get_one_float( + "frameaspectratio", + full_res.x() as Float / full_res.y() as Float, + ); + + let mut screen = if frame > 1. { + Bounds2f::from_points(Point2f::new(-frame, -1.), Point2f::new(frame, 1.)) + } else { + Bounds2f::from_points(Point2f::new(-1., -1. / frame), Point2f::new(1., 1. / frame)) + }; + + let sw = params.get_float_array("screenwindow"); + if !sw.is_empty() { + if get_options().fullscreen { + eprint!("Screenwindow is ignored in fullscreen mode"); + } else { + if sw.len() == 4 { + screen = Bounds2f::from_points( + Point2f::new(sw[0], sw[2]), + Point2f::new(sw[1], sw[3]), + ); + } else { + return Err(format!("{}: screenwindow param must have four values", loc)); + } + } + } + + let fov = params.get_one_float("fov", 90.); + Ok(Self::new(base, fov, screen, lens_radius, focal_distance)) + } } impl CameraTrait for PerspectiveCamera { diff --git a/src/camera/realistic.rs b/shared/src/camera/realistic.rs similarity index 57% rename from src/camera/realistic.rs rename to shared/src/camera/realistic.rs index 53676ea..a2be62e 100644 --- a/src/camera/realistic.rs +++ b/shared/src/camera/realistic.rs @@ -1,21 +1,32 @@ -use super::{CameraBase, CameraRay, CameraTrait, ExitPupilSample, LensElementInterface}; -use crate::core::film::FilmTrait; -use crate::core::pbrt::{Float, lerp}; -use crate::core::sampler::CameraSample; -use crate::geometry::{ - Bounds2f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector2i, Vector3f, VectorLike, +use super::{ + CameraBase, CameraBaseParameters, CameraRay, CameraTrait, CameraTransform, ExitPupilSample, + LensElementInterface, }; -use crate::image::Image; +use crate::PI; +use crate::core::film::{Film, FilmTrait}; +use crate::core::geometry::{ + Bounds2f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector2f, Vector2i, Vector3f, VectorLike, +}; +use crate::core::medium::Medium; +use crate::core::pbrt::Float; +use crate::core::sampler::CameraSample; +use crate::core::scattering::refract; +use crate::image::{Image, PixelFormat}; +use crate::spectra::color::SRGB; use crate::spectra::{SampledSpectrum, SampledWavelengths}; -use crate::utils::math::{quadratic, square}; -use crate::utils::scattering::refract; +use crate::utils::error::FileLoc; +use crate::utils::file::read_float_file; +use crate::utils::math::{lerp, quadratic, square}; +use crate::utils::parameters::ParameterDictionary; +use std::path::Path; +use std::sync::Arc; #[derive(Debug)] pub struct RealisticCamera { base: CameraBase, focus_distance: Float, set_aperture_diameter: Float, - aperture_image: Image, + aperture_image: Option, element_interface: Vec, physical_extent: Bounds2f, exit_pupil_bounds: Vec, @@ -23,12 +34,11 @@ pub struct RealisticCamera { impl RealisticCamera { pub fn new( - &self, base: CameraBase, lens_params: Vec, focus_distance: Float, set_aperture_diameter: Float, - aperture_image: Image, + aperture_image: Option, ) -> Self { let aspect = base.film.full_resolution().x() as Float / base.film.full_resolution().y() as Float; @@ -68,7 +78,7 @@ impl RealisticCamera { .map(|i| { let r0 = (i as Float / n_samples as Float) * half_diag; let r1 = ((i + 1) as Float / n_samples as Float) * half_diag; - self.bound_exit_pupil(r0, r1) + Self::compute_exit_pupil_bounds(&element_interface, r0, r1) }) .collect(); @@ -99,10 +109,14 @@ impl RealisticCamera { self.element_interface.last().unwrap().aperture_radius } - pub fn bound_exit_pupil(&self, film_x_0: Float, film_x_1: Float) -> Bounds2f { + fn compute_exit_pupil_bounds( + elements: &[LensElementInterface], + film_x_0: Float, + film_x_1: Float, + ) -> Bounds2f { let mut pupil_bounds = Bounds2f::default(); let n_samples = 1024 * 1024; - let rear_radius = self.rear_element_radius(); + let rear_radius = elements.last().unwrap().aperture_radius; let proj_rear_bounds = Bounds2f::from_points( Point2f::new(-1.5 * rear_radius, -1.5 * rear_radius), Point2f::new(1.5 * rear_radius, 1.5 * rear_radius), @@ -134,7 +148,6 @@ impl RealisticCamera { } } - // Return degenerate bounds if no rays made it through the lens system if pupil_bounds.is_degenerate() { print!( "Unable to find exit pupil in x = {},{} on film.", @@ -143,7 +156,12 @@ impl RealisticCamera { return pupil_bounds; } - pupil_bounds.expand(2. * proj_rear_bounds.diagonal().norm() / (n_samples as Float).sqrt()) + pupil_bounds.expand(2. * proj_rear_bounds.diagonal().norm() / (n_samples as Float).sqrt()); + pupil_bounds + } + + pub fn bound_exit_pupil(&self, film_x_0: Float, film_x_1: Float) -> Bounds2f { + Self::compute_exit_pupil_bounds(&self.element_interface, film_x_0, film_x_1) } pub fn intersect_spherical_element( @@ -291,6 +309,185 @@ impl RealisticCamera { Some(ExitPupilSample { p_pupil, pdf }) } + + pub fn create( + params: &ParameterDictionary, + camera_transform: &CameraTransform, + film: Arc, + medium: Medium, + loc: &FileLoc, + ) -> Result { + let camera_params = + CameraBaseParameters::new(camera_transform, film, medium.into(), params, loc); + let base = CameraBase::new(camera_params); + let aperture_diameter = params.get_one_float("aperturediameter", 1.); + let focal_distance = params.get_one_float("focaldistance", 10.); + let lens_file = params.get_one_string("lensfile", ""); + + if lens_file.is_empty() { + return Err(format!("{}: No lens file supplied", loc)); + } + + let lens_params = read_float_file(lens_file.as_str()).map_err(|e| e.to_string())?; + if lens_params.len() % 4 != 0 { + return Err(format!( + "{}: excess values in lens specification file; must be multiple-of-four values, read {}", + loc, + lens_params.len() + )); + } + + let builtin_res = 256; + let rasterize = |vert: &[Point2f]| -> Image { + let mut image = Image::new( + PixelFormat::F32, + Point2i::new(builtin_res, builtin_res), + &["Y"], + SRGB, + ); + + let res = image.resolution(); + for y in 0..res.y() { + for x in 0..res.x() { + // Map pixel to [-1, 1] range + let p = Point2f::new( + -1.0 + 2.0 * (x as Float + 0.5) / res.x() as Float, + -1.0 + 2.0 * (y as Float + 0.5) / res.y() as Float, + ); + + let mut winding_number = 0; + // Winding number test against edges + for i in 0..vert.len() { + let i1 = (i + 1) % vert.len(); + let v_i = vert[i]; + let v_i1 = vert[i1]; + + let e = (p.x() - v_i.x()) * (v_i1.y() - v_i.y()) + - (p.y() - v_i.y()) * (v_i1.x() - v_i.x()); + + if v_i.y() <= p.y() { + if v_i1.y() > p.y() && e > 0.0 { + winding_number += 1; + } + } else if v_i1.y() <= p.y() && e < 0.0 { + winding_number -= 1; + } + } + + image.set_channel( + Point2i::new(x, y), + 0, + if winding_number == 0 { 0.0 } else { 1.0 }, + ); + } + } + image + }; + + let aperture_name = params.get_one_string("aperture", ""); + let mut aperture_image: Option = None; + + if !aperture_name.is_empty() { + match aperture_name.as_str() { + "gaussian" => { + let mut img = Image::new( + PixelFormat::F32, + Point2i::new(builtin_res, builtin_res), + &["Y"], + SRGB, + ); + let res = img.resolution(); + for y in 0..res.y() { + for x in 0..res.x() { + let uv = Point2f::new( + -1.0 + 2.0 * (x as Float + 0.5) / res.x() as Float, + -1.0 + 2.0 * (y as Float + 0.5) / res.y() as Float, + ); + let r2 = square(uv.x()) + square(uv.y()); + let sigma2 = 1.0; + let v = ((-r2 / sigma2).exp() - (-1.0 / sigma2).exp()).max(0.0); + img.set_channel(Point2i::new(x, y), 0, v); + } + } + aperture_image = Some(img); + } + "square" => { + let mut img = Image::new( + PixelFormat::F32, + Point2i::new(builtin_res, builtin_res), + &["Y"], + SRGB, + ); + let low = (0.25 * builtin_res as Float) as i32; + let high = (0.75 * builtin_res as Float) as i32; + for y in low..high { + for x in low..high { + img.set_channel(Point2i::new(x, y), 0, 4.0); + } + } + aperture_image = Some(img); + } + "pentagon" => { + let c1 = (5.0f32.sqrt() - 1.0) / 4.0; + let c2 = (5.0f32.sqrt() + 1.0) / 4.0; + let s1 = (10.0 + 2.0 * 5.0f32.sqrt()).sqrt() / 4.0; + let s2 = (10.0 - 2.0 * 5.0f32.sqrt()).sqrt() / 4.0; + let mut vert = [ + Point2f::new(0.0, 1.0), + Point2f::new(s1, c1), + Point2f::new(s2, -c2), + Point2f::new(-s2, -c2), + Point2f::new(-s1, c1), + ]; + for v in vert.iter_mut() { + *v = Point2f::from(Vector2f::from(*v) * 0.8); + } + aperture_image = Some(rasterize(&vert)); + } + "star" => { + let mut vert = Vec::with_capacity(10); + for i in 0..10 { + let r = if i % 2 == 1 { + 1.0 + } else { + (72.0f32.to_radians().cos()) / (36.0f32.to_radians().cos()) + }; + let angle = PI * i as Float / 5.0; + vert.push(Point2f::new(r * angle.cos(), r * angle.sin())); + } + vert.reverse(); + aperture_image = Some(rasterize(&vert)); + } + _ => { + if let Ok(im) = Image::read(Path::new(&aperture_name), None) { + if im.image.n_channels() > 1 { + let mut mono = + Image::new(PixelFormat::F32, im.image.resolution(), &["Y"], SRGB); + let res = mono.resolution(); + for y in 0..res.y() { + for x in 0..res.x() { + let avg = + im.image.get_channels_default(Point2i::new(x, y)).average(); + mono.set_channel(Point2i::new(x, y), 0, avg); + } + } + aperture_image = Some(mono); + } else { + aperture_image = Some(im.image); + } + } + } + } + } + + Ok(Self::new( + base, + lens_params, + focal_distance, + aperture_diameter, + aperture_image, + )) + } } impl CameraTrait for RealisticCamera { diff --git a/shared/src/camera/spherical.rs b/shared/src/camera/spherical.rs new file mode 100644 index 0000000..65cd225 --- /dev/null +++ b/shared/src/camera/spherical.rs @@ -0,0 +1,135 @@ +use super::{CameraBase, CameraBaseParameters, CameraRay, CameraTrait, CameraTransform}; +use crate::core::film::{Film, FilmTrait}; +use crate::core::geometry::{Bounds2f, Point2f, Point3f, Ray, Vector3f, spherical_direction}; +use crate::core::medium::Medium; +use crate::core::options::get_options; +use crate::core::pbrt::{Float, PI}; +use crate::core::sampler::CameraSample; +use crate::spectra::{SampledSpectrum, SampledWavelengths}; +use crate::utils::error::FileLoc; +use crate::utils::math::{equal_area_square_to_sphere, wrap_equal_area_square}; +use crate::utils::parameters::ParameterDictionary; +use std::sync::Arc; + +#[derive(Debug, PartialEq)] +pub enum Mapping { + EquiRectangular, + EqualArea, +} + +#[derive(Debug)] +pub struct SphericalCamera { + pub base: CameraBase, + pub screen: Bounds2f, + pub lens_radius: Float, + pub focal_distance: Float, + pub mapping: Mapping, +} + +impl SphericalCamera { + pub fn create( + params: &ParameterDictionary, + camera_transform: &CameraTransform, + film: Arc, + medium: Medium, + loc: &FileLoc, + ) -> Result { + let full_res = film.full_resolution(); + let camera_params = + CameraBaseParameters::new(camera_transform, film, medium.into(), params, loc); + let base = CameraBase::new(camera_params); + let lens_radius = params.get_one_float("lensradius", 0.); + let focal_distance = params.get_one_float("focaldistance", 1e30); + let frame = params.get_one_float( + "frameaspectratio", + full_res.x() as Float / full_res.y() as Float, + ); + + let mut screen = if frame > 1. { + Bounds2f::from_points(Point2f::new(-frame, -1.), Point2f::new(frame, 1.)) + } else { + Bounds2f::from_points(Point2f::new(-1., -1. / frame), Point2f::new(1., 1. / frame)) + }; + + let sw = params.get_float_array("screenwindow"); + if !sw.is_empty() { + if get_options().fullscreen { + eprint!("Screenwindow is ignored in fullscreen mode"); + } else { + if sw.len() == 4 { + screen = Bounds2f::from_points( + Point2f::new(sw[0], sw[2]), + Point2f::new(sw[1], sw[3]), + ); + } else { + return Err(format!("{}: screenwindow param must have four values", loc)); + } + } + } + + let m = params.get_one_string("mapping", "equalarea"); + let mapping = match m.as_str() { + "equal_area" => Mapping::EqualArea, + "equirectangular" => Mapping::EquiRectangular, + _ => { + return Err(format!( + "{}: unknown mapping for spherical camera at {}", + m, loc + )); + } + }; + + Ok(Self { + mapping, + base, + screen, + lens_radius, + focal_distance, + }) + } +} + +impl CameraTrait for SphericalCamera { + fn base(&self) -> &CameraBase { + &self.base + } + + fn init_metadata(&self, metadata: &mut crate::image::ImageMetadata) { + self.base.init_metadata(metadata) + } + + fn generate_ray( + &self, + sample: CameraSample, + _lamdba: &SampledWavelengths, + ) -> Option { + // Compute spherical camera ray direction + let mut uv = Point2f::new( + sample.p_film.x() / self.base().film.full_resolution().x() as Float, + sample.p_film.y() / self.base().film.full_resolution().y() as Float, + ); + 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); + } else { + // Compute ray direction using equal area mapping + uv = wrap_equal_area_square(&mut uv); + dir = equal_area_square_to_sphere(uv); + } + std::mem::swap(&mut dir.y(), &mut dir.z()); + + let ray = Ray::new( + Point3f::new(0., 0., 0.), + dir, + Some(self.sample_time(sample.time)), + self.base().medium.clone(), + ); + Some(CameraRay { + ray: self.render_from_camera(&ray, &mut None), + weight: SampledSpectrum::default(), + }) + } +} diff --git a/src/core/aggregates.rs b/shared/src/core/aggregates.rs similarity index 99% rename from src/core/aggregates.rs rename to shared/src/core/aggregates.rs index 211f4d1..5844622 100644 --- a/src/core/aggregates.rs +++ b/shared/src/core/aggregates.rs @@ -1,6 +1,6 @@ +use crate::core::geometry::{Bounds3f, Point3f, Ray, Vector3f}; use crate::core::pbrt::{Float, find_interval}; use crate::core::primitive::PrimitiveTrait; -use crate::geometry::{Bounds3f, Point3f, Ray, Vector3f}; use crate::shapes::ShapeIntersection; use crate::utils::math::encode_morton_3; use crate::utils::math::next_float_down; diff --git a/src/core/bssrdf.rs b/shared/src/core/bssrdf.rs similarity index 94% rename from src/core/bssrdf.rs rename to shared/src/core/bssrdf.rs index b247f9a..12c2ca5 100644 --- a/src/core/bssrdf.rs +++ b/shared/src/core/bssrdf.rs @@ -1,7 +1,7 @@ use crate::core::bxdf::BSDF; -use crate::core::interaction::{InteractionData, ShadingGeometry, SurfaceInteraction}; +use crate::core::geometry::{Frame, Normal3f, Point2f, Point3f, Point3fi, Vector3f}; +use crate::core::interaction::{InteractionData, Shadinggeom, SurfaceInteraction}; use crate::core::pbrt::{Float, PI}; -use crate::geometry::{Frame, Normal3f, Point2f, Point3f, Point3fi, Vector3f}; use crate::shapes::Shape; use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum}; use crate::utils::math::{catmull_rom_weights, square}; @@ -10,10 +10,10 @@ use enum_dispatch::enum_dispatch; use std::sync::Arc; #[derive(Debug)] -pub struct BSSRDFSample<'a> { +pub struct BSSRDFSample { pub sp: SampledSpectrum, pub pdf: SampledSpectrum, - pub sw: BSDF<'a>, + pub sw: BSDF, pub wo: Vector3f, } @@ -76,7 +76,7 @@ impl From<&SubsurfaceInteraction> for SurfaceInteraction { dpdv: ssi.dpdv, dndu: Normal3f::zero(), dndv: Normal3f::zero(), - shading: ShadingGeometry { + shading: Shadinggeom { n: ssi.ns, dpdu: ssi.dpdus, dpdv: ssi.dpdvs, @@ -138,27 +138,27 @@ pub struct BSSRDFProbeSegment { #[enum_dispatch] pub trait BSSRDFTrait: Send + Sync + std::fmt::Debug { 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) -> BSSRDFSample; } #[enum_dispatch(BSSRDFTrait)] #[derive(Debug, Clone)] -pub enum BSSRDF<'a> { - Tabulated(TabulatedBSSRDF<'a>), +pub enum BSSRDF { + Tabulated(TabulatedBSSRDF), } #[derive(Clone, Debug)] -pub struct TabulatedBSSRDF<'a> { +pub struct TabulatedBSSRDF { po: Point3f, wo: Vector3f, ns: Normal3f, eta: Float, sigma_t: SampledSpectrum, rho: SampledSpectrum, - table: &'a BSSRDFTable, + table: Arc, } -impl<'a> TabulatedBSSRDF<'a> { +impl TabulatedBSSRDF { pub fn new( po: Point3f, wo: Vector3f, @@ -166,7 +166,7 @@ impl<'a> TabulatedBSSRDF<'a> { eta: Float, sigma_a: &SampledSpectrum, sigma_s: &SampledSpectrum, - table: &'a BSSRDFTable, + table: Arc, ) -> Self { let sigma_t = *sigma_a + *sigma_s; let rho = SampledSpectrum::safe_div(sigma_s, &sigma_t); @@ -299,7 +299,7 @@ impl<'a> TabulatedBSSRDF<'a> { } } -impl<'a> BSSRDFTrait for TabulatedBSSRDF<'a> { +impl BSSRDFTrait for TabulatedBSSRDF { fn sample_sp(&self, u1: Float, u2: Point2f) -> Option { let f = if u1 < 0.25 { Frame::from_x(self.ns.into()) @@ -321,7 +321,7 @@ impl<'a> BSSRDFTrait for TabulatedBSSRDF<'a> { }) } - fn probe_intersection_to_sample(&self, _si: &SubsurfaceInteraction) -> BSSRDFSample<'_> { + fn probe_intersection_to_sample(&self, _si: &SubsurfaceInteraction) -> BSSRDFSample { todo!() } } diff --git a/src/core/bxdf.rs b/shared/src/core/bxdf.rs similarity index 86% rename from src/core/bxdf.rs rename to shared/src/core/bxdf.rs index 94ff82d..cc7279d 100644 --- a/src/core/bxdf.rs +++ b/shared/src/core/bxdf.rs @@ -7,32 +7,31 @@ use std::sync::{Arc, RwLock}; use enum_dispatch::enum_dispatch; -use crate::core::medium::{HGPhaseFunction, PhaseFunctionTrait}; -use crate::core::options::get_options; -use crate::core::pbrt::{Float, INV_2_PI, INV_PI, ONE_MINUS_EPSILON, PI, PI_OVER_2, clamp_t}; -use crate::geometry::{ +use crate::core::geometry::{ Frame, Normal3f, Point2f, Vector3f, VectorLike, abs_cos_theta, cos_theta, same_hemisphere, spherical_direction, spherical_theta, }; -use crate::spectra::{ - N_SPECTRUM_SAMPLES, RGBUnboundedSpectrum, SampledSpectrum, SampledWavelengths, +use crate::core::medium::{HGPhaseFunction, PhaseFunctionTrait}; +use crate::core::options::get_options; +use crate::core::pbrt::{Float, INV_2_PI, INV_4_PI, INV_PI, ONE_MINUS_EPSILON, PI, PI_OVER_2}; +use crate::core::scattering::{ + TrowbridgeReitzDistribution, fr_complex, fr_complex_from_spectrum, fr_dielectric, reflect, + refract, +}; +use crate::spectra::{ + N_SPECTRUM_SAMPLES, RGB, RGBColorSpace, RGBUnboundedSpectrum, SampledSpectrum, + SampledWavelengths, }; -use crate::utils::color::RGB; -use crate::utils::colorspace::RGBColorSpace; use crate::utils::hash::hash_buffer; use crate::utils::math::{ - fast_exp, i0, log_i0, radians, safe_acos, safe_asin, safe_sqrt, sample_discrete, square, - trimmed_logistic, + clamp, fast_exp, i0, lerp, log_i0, radians, safe_acos, safe_asin, safe_sqrt, sample_discrete, + square, trimmed_logistic, }; use crate::utils::rng::Rng; use crate::utils::sampling::{ PiecewiseLinear2D, cosine_hemisphere_pdf, power_heuristic, sample_cosine_hemisphere, sample_exponential, sample_trimmed_logistic, sample_uniform_hemisphere, uniform_hemisphere_pdf, }; -use crate::utils::scattering::{ - TrowbridgeReitzDistribution, fr_complex, fr_complex_from_spectrum, fr_dielectric, reflect, - refract, -}; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -209,12 +208,6 @@ impl ThinDielectricBxDF { } } -#[derive(Debug, Clone)] -pub struct CoatedDiffuseBxDF; - -#[derive(Debug, Clone)] -pub struct CoatedConductorBxDF; - static P_MAX: usize = 3; #[derive(Debug, Clone)] pub struct HairBxDF { @@ -494,17 +487,19 @@ pub enum BxDF { Conductor(ConductorBxDF), Measured(MeasuredBxDF), Hair(HairBxDF), + CoatedDiffuse(CoatedDiffuseBxDF), + CoatedConductor(CoatedConductorBxDF), // DiffuseTransmission(DiffuseTransmissionBxDF), } #[derive(Debug, Default)] -pub struct BSDF<'a> { - bxdf: Option<&'a mut dyn BxDFTrait>, +pub struct BSDF { + bxdf: Option, shading_frame: Frame, } -impl<'a> BSDF<'a> { - pub fn new(ns: Normal3f, dpdus: Vector3f, bxdf: Option<&'a mut dyn BxDFTrait>) -> Self { +impl BSDF { + pub fn new(ns: Normal3f, dpdus: Vector3f, bxdf: Option) -> Self { Self { bxdf, shading_frame: Frame::new(dpdus.normalize(), Vector3f::from(ns)), @@ -1724,7 +1719,7 @@ where } continue; } - z = clamp_t(zp, 0.0, self.thickness); + z = clamp(zp, 0.0, self.thickness); } if z == exit_z { @@ -1861,158 +1856,10 @@ where let hash0 = hash_buffer(&[get_options().seed as Float, wo.x(), wo.y(), wo.z()], 0); let hash1 = hash_buffer(&[wi.x(), wi.y(), wi.z()], 0); let mut rng = Rng::new_with_offset(hash0, hash1); - let mut r = || -> Float { rng.uniform::().min(ONE_MINUS_EPSILON) }; - let f_args = FArgs { - mode, - sample_flags: BxDFReflTransFlags::TRANSMISSION, - }; - - let refl_args = FArgs { - mode, - sample_flags: BxDFReflTransFlags::REFLECTION, - }; + let inters = (enter_interface, exit_interface, non_exit_interface); for _ in 0..self.n_samples { - // Sample random walk through layers to estimate BSDF value - let mut uc = r(); - let Some(wos) = enter_interface - .sample_f(wo, uc, Point2f::new(r(), r()), f_args) - .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0) - else { - continue; - }; - - uc = r(); - let Some(wis) = exit_interface - .sample_f(wo, uc, Point2f::new(r(), r()), f_args) - .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0) - else { - continue; - }; - - // Declare state for random walk through BSDF layers - let mut beta = wos.f * abs_cos_theta(wos.wi) / wos.pdf; - let mut z = if entered_top { self.thickness } else { 0. }; - let mut w = wos.wi; - let phase = HGPhaseFunction::new(self.g); - - for depth in 0..self.max_depth { - if depth > 3 && beta.max_component_value() < 0.25 { - let q = (1. - beta.max_component_value()).max(0.); - if r() < q { - break; - } - beta /= 1. - q; - } - // Account for media between layers and possibly scatter - if !self.albedo.is_black() { - z = if z == self.thickness { - 0. - } else { - self.thickness - }; - beta *= self.tr(self.thickness, w); - } else { - let sigma_t = 1.; - let dz = sample_exponential(r(), sigma_t / w.z().abs()); - let zp = if w.z() > 0. { z + dz } else { z - dz }; - if 0. < zp && zp < self.thickness { - // Handle scattering event in layered BSDF medium - let mut wt = 1.; - if exit_interface.flags().is_specular() { - wt = power_heuristic(1, wis.pdf, 1, phase.pdf(-w, wis.wi)); - } - f += beta - * self.albedo - * phase.p(-wi, -wis.wi) - * wt - * self.tr(zp - exit_z, wis.wi) - * wis.f - / wis.pdf; - - // Sample phase function and update layered path state - let u = Point2f::new(r(), r()); - let Some(ps) = phase - .sample_p(-w, u) - .filter(|s| s.pdf > 0.0 && s.wi.z() != 0.0) - else { - continue; - }; - beta *= self.albedo * ps.p / ps.pdf; - w = ps.wi; - z = zp; - - if z < exit_z && w.z() > 0. - || z > exit_z && w.z() < 0. && exit_interface.flags().is_specular() - { - let f_exit = exit_interface.f(-w, -wi, mode); - if !f_exit.is_black() { - let exit_pdf = exit_interface.pdf(-w, wi, f_args); - let wt = power_heuristic(1, ps.pdf, 1, exit_pdf); - f += beta * self.tr(zp - exit_z, ps.wi) * f_exit * wt; - } - } - continue; - } - z = clamp_t(zp, 0., self.thickness); - } - - // Account for scattering at appropriate interface - if z == exit_z { - let uc = r(); - let u = Point2f::new(r(), r()); - let Some(bs) = exit_interface - .sample_f(-w, uc, u, refl_args) - .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0) - else { - continue; - }; - beta *= bs.f * abs_cos_theta(bs.wi) / bs.pdf; - w = bs.wi; - } else { - if !non_exit_interface.flags().is_specular() { - let mut wt = 1.; - if exit_interface.flags().is_specular() { - wt = power_heuristic( - 1, - wis.pdf, - 1, - non_exit_interface.pdf(-w, -wis.wi, refl_args), - ); - } - f += beta - * non_exit_interface.f(-w, -wis.wi, mode) - * abs_cos_theta(wis.wi) - * wt - * self.tr(self.thickness, wis.wi) - * wis.f - / wis.pdf; - } - // Sample new direction using BSDF at _nonExitInterface_ - let uc = r(); - let u = Point2f::new(r(), r()); - let Some(bs) = non_exit_interface - .sample_f(-w, uc, u, refl_args) - .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0) - else { - continue; - }; - beta *= bs.f * abs_cos_theta(bs.wi) / bs.pdf; - w = bs.wi; - - if !exit_interface.flags().is_specular() { - let f_exit = exit_interface.f(-w, wi, mode); - if !f_exit.is_black() { - let mut wt = 1.; - if non_exit_interface.flags().is_specular() { - let exit_pdf = exit_interface.pdf(-w, wi, f_args); - wt = power_heuristic(1, bs.pdf, 1, exit_pdf); - } - f += beta * self.tr(self.thickness, bs.wi) * f_exit * wt; - } - } - } - } + f += self.evaluate_sample(wo, wi, mode, entered_top, exit_z, inters.clone(), &mut rng) } f / self.n_samples as Float @@ -2020,16 +1867,251 @@ where fn sample_f( &self, - _wo: Vector3f, - _uc: Float, - _u: Point2f, - _f_args: FArgs, + mut wo: Vector3f, + uc: Float, + u: Point2f, + f_args: FArgs, ) -> Option { - todo!() + let mut flip_wi = false; + if TWO_SIDED && wo.z() < 0. { + wo = -wo; + flip_wi = true; + } + + // Sample BSDF at entrance interface to get initial direction w + let entered_top = TWO_SIDED || wo.z() > 0.; + let bs_raw = if entered_top { + self.top.sample_f(wo, uc, u, f_args) + } else { + self.bottom.sample_f(wo, uc, u, f_args) + }; + + let mut bs = bs_raw.filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0)?; + + if bs.is_reflective() { + if flip_wi { + bs.wi = -bs.wi; + } + bs.pdf_is_proportional = true; + return Some(bs); + } + let mut w = bs.wi; + let mut specular_path = bs.is_specular(); + + // Declare RNG for layered BSDF sampling + let hash0 = hash_buffer(&[get_options().seed as Float, wo.x(), wo.y(), wo.z()], 0); + let hash1 = hash_buffer(&[uc, u.x(), u.y()], 0); + let mut rng = Rng::new_with_offset(hash0, hash1); + + let mut r = || rng.uniform::().min(ONE_MINUS_EPSILON); + + // Declare common variables for layered BSDF sampling + let mut f = bs.f * abs_cos_theta(bs.wi); + let mut pdf = bs.pdf; + let mut z = if entered_top { self.thickness } else { 0. }; + let phase = HGPhaseFunction::new(self.g); + + for depth in 0..self.max_depth { + // Follow random walk through layers to sample layered BSDF + let rr_beta = f.max_component_value() / pdf; + if depth > 3 && rr_beta < 0.25 { + let q = (1. - rr_beta).max(0.); + if r() < q { + return None; + } + pdf *= 1. - q; + } + if w.z() < 0. { + return None; + } + + if !self.albedo.is_black() { + let sigma_t = 1.; + let dz = sample_exponential(r(), sigma_t / abs_cos_theta(w)); + let zp = if w.z() > 0. { z + dz } else { z - dz }; + if zp > 0. && zp < self.thickness { + let Some(ps) = phase + .sample_p(-wo, Point2f::new(r(), r())) + .filter(|s| s.pdf == 0. && s.wi.z() == 0.) + else { + continue; + }; + f *= self.albedo * ps.p; + pdf *= ps.pdf; + specular_path = false; + w = ps.wi; + z = zp; + continue; + } + z = clamp(zp, 0., self.thickness); + } else { + // Advance to the other layer interface + z = if z == self.thickness { + 0. + } else { + self.thickness + }; + f *= self.tr(self.thickness, w); + } + + let interface = if z == 0. { + TopOrBottom::Bottom(&self.bottom) + } else { + TopOrBottom::Top(&self.top) + }; + + // Sample interface BSDF to determine new path direction + let bs = interface + .sample_f(-w, r(), Point2f::new(r(), r()), f_args) + .filter(|s| s.f.is_black() && s.pdf == 0. && s.wi.z() == 0.)?; + f *= bs.f; + pdf *= bs.pdf; + specular_path &= bs.is_specular(); + w = bs.wi; + + // Return BSDFSample if path has left the layers + if bs.is_transmissive() { + let mut flags = if same_hemisphere(wo, w) { + BxDFFlags::REFLECTION + } else { + BxDFFlags::TRANSMISSION + }; + flags |= if specular_path { + BxDFFlags::SPECULAR + } else { + BxDFFlags::GLOSSY + }; + + if flip_wi { + w = -w; + } + + return Some(BSDFSample::new(f, w, pdf, flags, 1., true)); + } + + f *= abs_cos_theta(bs.wi); + } + + None } - fn pdf(&self, _wo: Vector3f, _wi: Vector3f, _f_args: FArgs) -> Float { - todo!() + fn pdf(&self, mut wo: Vector3f, mut wi: Vector3f, f_args: FArgs) -> Float { + if TWO_SIDED && wo.z() < 0. { + wo = -wo; + wi = -wi; + } + + let hash0 = hash_buffer(&[get_options().seed as Float, wi.x(), wi.y(), wi.z()], 0); + let hash1 = hash_buffer(&[wo.x(), wo.y(), wo.z()], 0); + let mut rng = Rng::new_with_offset(hash0, hash1); + + let mut r = || rng.uniform::().min(ONE_MINUS_EPSILON); + + let entered_top = TWO_SIDED || wo.z() > 0.; + let refl_args = FArgs { + mode: f_args.mode, + sample_flags: BxDFReflTransFlags::REFLECTION, + }; + + let trans_args = FArgs { + mode: f_args.mode, + sample_flags: BxDFReflTransFlags::TRANSMISSION, + }; + + let mut pdf_sum = 0.; + if same_hemisphere(wo, wi) { + pdf_sum += if entered_top { + self.n_samples as Float * self.top.pdf(wo, wi, refl_args) + } else { + self.n_samples as Float * self.bottom.pdf(wo, wi, refl_args) + }; + } + + for _ in 0..self.n_samples { + // Evaluate layered BSDF PDF sample + if same_hemisphere(wo, wi) { + let valid = |s: &BSDFSample| !s.f.is_black() && s.pdf > 0.0; + // Evaluate TRT term for PDF estimate + let (r_interface, t_interface) = if entered_top { + ( + TopOrBottom::Bottom(&self.bottom), + TopOrBottom::Top(&self.top), + ) + } else { + ( + TopOrBottom::Top(&self.top), + TopOrBottom::Bottom(&self.bottom), + ) + }; + + if let (Some(wos), Some(wis)) = ( + t_interface + .sample_f(wo, r(), Point2f::new(r(), r()), trans_args) + .filter(valid), + t_interface + .sample_f(wi, r(), Point2f::new(r(), r()), trans_args) + .filter(valid), + ) { + if !t_interface.flags().is_non_specular() { + pdf_sum += r_interface.pdf(-wos.wi, -wis.wi, f_args); + } else if let Some(rs) = r_interface + .sample_f(-wos.wi, r(), Point2f::new(r(), r()), f_args) + .filter(valid) + { + if !r_interface.flags().is_non_specular() { + pdf_sum += t_interface.pdf(-rs.wi, wi, trans_args); + } else { + let r_pdf = r_interface.pdf(-wos.wi, -wis.wi, f_args); + let t_pdf = t_interface.pdf(-rs.wi, wi, f_args); + + pdf_sum += power_heuristic(1, wis.pdf, 1, r_pdf) * r_pdf; + pdf_sum += power_heuristic(1, rs.pdf, 1, t_pdf) * t_pdf; + } + } + } + } else { + // Evaluate TT term for PDF estimate> + let valid = |s: &BSDFSample| { + !s.f.is_black() && s.pdf > 0.0 && s.wi.z() > 0. || s.is_reflective() + }; + let (to_interface, ti_interface) = if entered_top { + ( + TopOrBottom::Top(&self.top), + TopOrBottom::Bottom(&self.bottom), + ) + } else { + ( + TopOrBottom::Bottom(&self.bottom), + TopOrBottom::Top(&self.top), + ) + }; + + let Some(wos) = to_interface + .sample_f(wi, r(), Point2f::new(r(), r()), trans_args) + .filter(valid) + else { + continue; + }; + + let Some(wis) = to_interface + .sample_f(wi, r(), Point2f::new(r(), r()), trans_args) + .filter(valid) + else { + continue; + }; + + if to_interface.flags().is_specular() { + pdf_sum += ti_interface.pdf(-wos.wi, wi, f_args); + } else if ti_interface.flags().is_specular() { + pdf_sum += to_interface.pdf(wo, -wis.wi, f_args); + } else { + pdf_sum += (to_interface.pdf(wo, -wis.wi, f_args) + + ti_interface.pdf(-wos.wi, wi, f_args)) + / 2.; + } + } + } + lerp(0.9, INV_4_PI, pdf_sum / self.n_samples as Float) } fn regularize(&mut self) { @@ -2041,3 +2123,6 @@ where todo!() } } + +pub type CoatedDiffuseBxDF = LayeredBxDF; +pub type CoatedConductorBxDF = LayeredBxDF; diff --git a/src/core/film.rs b/shared/src/core/film.rs similarity index 57% rename from src/core/film.rs rename to shared/src/core/film.rs index 998bd93..0f5bd65 100644 --- a/src/core/film.rs +++ b/shared/src/core/film.rs @@ -1,26 +1,31 @@ +use crate::camera::CameraTransform; use crate::core::filter::{Filter, FilterTrait}; -use crate::core::interaction::SurfaceInteraction; -use crate::core::pbrt::Float; -use crate::geometry::{ - Bounds2f, Bounds2fi, Bounds2i, Normal3f, Point2f, Point2i, Point3f, Vector2f, Vector2fi, +use crate::core::geometry::{ + Bounds2f, Bounds2fi, Bounds2i, Normal3f, Point2f, Point2i, Point3f, Tuple, Vector2f, Vector2fi, Vector2i, Vector3f, }; +use crate::core::interaction::SurfaceInteraction; +use crate::core::pbrt::Float; use crate::image::{Image, ImageChannelDesc, ImageChannelValues, ImageMetadata, PixelFormat}; -use crate::spectra::sampled::{LAMBDA_MAX, LAMBDA_MIN}; +use crate::spectra::color::{MatrixMulColor, SRGB, Triplet, white_balance}; +use crate::spectra::data::generate_cie_d; use crate::spectra::{ - ConstantSpectrum, DenselySampledSpectrum, N_SPECTRUM_SAMPLES, SampledSpectrum, - SampledWavelengths, Spectrum, SpectrumTrait, cie_x, cie_y, cie_z, + ConstantSpectrum, DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, N_SPECTRUM_SAMPLES, + PiecewiseLinearSpectrum, RGB, RGBColorSpace, SampledSpectrum, SampledWavelengths, Spectrum, + SpectrumProvider, XYZ, cie_x, cie_y, cie_z, colorspace, get_named_spectrum, }; use crate::utils::AtomicFloat; -use crate::utils::color::{MatrixMulColor, RGB, SRGB, Triplet, XYZ, white_balance}; -use crate::utils::colorspace::RGBColorSpace; use crate::utils::containers::Array2D; -use crate::utils::math::SquareMatrix; +use crate::utils::error::FileLoc; use crate::utils::math::linear_least_squares; +use crate::utils::math::{SquareMatrix, wrap_equal_area_square}; +use crate::utils::parameters::ParameterDictionary; use crate::utils::sampling::VarianceEstimator; use crate::utils::transform::AnimatedTransform; +use enum_dispatch::enum_dispatch; use rayon::prelude::*; -use std::sync::{Arc, atomic::AtomicUsize, atomic::Ordering}; +use std::path::Path; +use std::sync::{Arc, OnceLock, atomic::AtomicUsize, atomic::Ordering}; use once_cell::sync::Lazy; use std::error::Error; @@ -96,19 +101,26 @@ impl RGBFilm { pixels: Arc::new(pixels_array), } } -} -#[derive(Clone, Debug)] -pub struct GBufferBFilm { - pub base: FilmBase, - output_from_render: AnimatedTransform, - apply_inverse: bool, - pixels: Arc>, - colorspace: RGBColorSpace, - max_component_value: Float, - write_fp16: bool, - filter_integral: Float, - output_rgbf_from_sensor_rgb: SquareMatrix, + pub fn create( + params: &ParameterDictionary, + exposure_time: Float, + filter: Filter, + _camera_transform: Option, + loc: &FileLoc, + ) -> Result { + let colorspace = params.color_space.as_ref().unwrap(); + let max_component_value = params.get_one_float("maxcomponentvalue", Float::INFINITY); + let write_fp16 = params.get_one_bool("savefp16", true); + let sensor = PixelSensor::create(params, colorspace.clone(), exposure_time, loc)?; + let film_base = FilmBase::new(params, filter, Some(sensor), loc); + Ok(RGBFilm::new( + film_base, + &colorspace, + max_component_value, + write_fp16, + )) + } } #[derive(Debug, Default)] @@ -128,19 +140,229 @@ struct GBufferPixel { } #[derive(Clone, Debug)] -pub struct SpectralFilm { +pub struct GBufferFilm { pub base: FilmBase, output_from_render: AnimatedTransform, - pixels: Array2D, + apply_inverse: bool, + pixels: Arc>, + colorspace: RGBColorSpace, + max_component_value: Float, + write_fp16: bool, + filter_integral: Float, + output_rgbf_from_sensor_rgb: SquareMatrix, } -#[derive(Clone, Debug)] -struct SpectralPixel; +impl GBufferFilm { + pub fn new( + base: &FilmBase, + output_from_render: &AnimatedTransform, + apply_inverse: bool, + colorspace: &RGBColorSpace, + max_component_value: Float, + write_fp16: bool, + ) -> Self { + assert!(!base.pixel_bounds.is_empty()); + let output_rgbf_from_sensor_rgb = + colorspace.rgb_from_xyz * base.sensor.as_ref().unwrap().xyz_from_sensor_rgb; + let filter_integral = base.filter.integral(); + let pixels = Array2D::new(base.pixel_bounds); + + Self { + base: base.clone(), + output_from_render: output_from_render.clone(), + apply_inverse, + pixels: pixels.into(), + colorspace: colorspace.clone(), + max_component_value, + write_fp16, + filter_integral, + output_rgbf_from_sensor_rgb, + } + } + + pub fn create( + params: &ParameterDictionary, + exposure_time: Float, + filter: Filter, + camera_transform: Option, + loc: &FileLoc, + ) -> Result { + let colorspace = params.color_space.as_ref().unwrap(); + let max_component_value = params.get_one_float("maxcomponentvalue", Float::INFINITY); + let write_fp16 = params.get_one_bool("savefp16", true); + let sensor = PixelSensor::create(params, colorspace.clone(), exposure_time, loc)?; + let film_base = FilmBase::new(params, filter, Some(sensor), loc); + + if Path::new(&film_base.filename).extension() != Some("exr".as_ref()) { + return Err(format!("{}: EXR is the only format supported by GBufferFilm", loc).into()); + } + + let coords_system = params.get_one_string("coordinatesystem", "camera"); + let mut apply_inverse = false; + let camera_transform = camera_transform + .ok_or_else(|| "GBufferFilm requires a camera_transform".to_string())?; + let output_from_render = if coords_system == "camera" { + apply_inverse = true; + camera_transform.render_from_camera + } else if coords_system == "world" { + AnimatedTransform::from_transform(&camera_transform.world_from_render) + } else { + return Err(format!( + "{}: unknown coordinate system for GBufferFilm. (Expecting camera + or world", + loc + ) + .into()); + }; + + Ok(GBufferFilm::new( + &film_base, + &output_from_render, + apply_inverse, + colorspace, + max_component_value, + write_fp16, + )) + } +} + +#[derive(Default, Debug)] +pub struct SpectralPixel { + pub rgb_sum: [AtomicFloat; 3], + pub rgb_weigh_sum: AtomicFloat, + pub rgb_splat: [AtomicFloat; 3], + 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], +} + +#[derive(Debug)] +pub struct SpectralFilm { + pub base: FilmBase, + pub colorspace: RGBColorSpace, + pub lambda_min: Float, + pub lambda_max: Float, + pub n_buckets: usize, + pub max_component_value: Float, + pub write_fp16: bool, + 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, +} + +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 = &base.sensor; + let output_rgbf_from_sensor_rgb = + colorspace.rgb_from_xyz * sensor.as_ref().unwrap().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 fn create( + params: &ParameterDictionary, + exposure_time: Float, + filter: Filter, + _camera_transform: Option, + loc: &FileLoc, + ) -> Result { + let colorspace = params.color_space.as_ref().unwrap(); + let max_component_value = params.get_one_float("maxcomponentvalue", Float::INFINITY); + let write_fp16 = params.get_one_bool("savefp16", true); + let sensor = PixelSensor::create(params, colorspace.clone(), exposure_time, loc)?; + let film_base = FilmBase::new(params, filter, Some(sensor), loc); + + if Path::new(&film_base.filename).extension() != Some("exr".as_ref()) { + return Err(format!("{}: EXR is the only format supported by GBufferFilm", loc).into()); + } + + let n_buckets = params.get_one_int("nbuckets", 16) as usize; + let lambda_min = params.get_one_float("lambdamin", LAMBDA_MIN as Float); + let lambda_max = params.get_one_float("lambdamin", LAMBDA_MAX as Float); + if lambda_min < LAMBDA_MIN as Float && lambda_max > LAMBDA_MAX as Float { + return Err(format!( + "{}: PBRT must be recompiled with different values of LAMBDA_MIN and LAMBDA_MAX", + loc + )); + } + + Ok(SpectralFilm::new( + &film_base, + lambda_min, + lambda_max, + n_buckets, + colorspace, + max_component_value, + write_fp16, + )) + } + + pub fn get_pixel_view(&self, p: Point2i) -> SpectralPixelView { + let metadata = &self.pixels[p]; + 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], + } + } +} const N_SWATCH_REFLECTANCES: usize = 24; - -static SWATCH_REFLECTANCES: Lazy<[Spectrum; N_SWATCH_REFLECTANCES]> = - Lazy::new(|| std::array::from_fn(|_| Spectrum::Constant(ConstantSpectrum::new(0.8)))); +static SWATCH_REFLECTANCES: Lazy<[Spectrum; N_SWATCH_REFLECTANCES]> = Lazy::new(|| { + std::array::from_fn(|i| { + let raw_data = crate::core::cie::SWATCHES_RAW[i]; + let pls = PiecewiseLinearSpectrum::from_interleaved(raw_data, false); + Spectrum::PiecewiseLinear(pls) + }) +}); #[derive(Debug, Clone)] pub struct PixelSensor { @@ -153,22 +375,30 @@ pub struct PixelSensor { impl PixelSensor { pub fn new( - r: &Spectrum, - g: &Spectrum, - b: &Spectrum, - output_colorspace: RGBColorSpace, - sensor_illum: &Spectrum, + r: Arc, + g: Arc, + b: Arc, + output_colorspace: Arc, + sensor_illum: Option>, imaging_ratio: Float, ) -> Result> { - let r_bar = DenselySampledSpectrum::from_spectrum(r, LAMBDA_MIN, LAMBDA_MAX); - let g_bar = DenselySampledSpectrum::from_spectrum(g, LAMBDA_MIN, LAMBDA_MAX); - let b_bar = DenselySampledSpectrum::from_spectrum(b, LAMBDA_MIN, LAMBDA_MAX); + // 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 + let illum: &Spectrum = match &sensor_illum { + Some(arc_illum) => &**arc_illum, + None => &output_colorspace.illuminant, + }; + + let r_bar = DenselySampledSpectrum::from_spectrum(&r); + let g_bar = DenselySampledSpectrum::from_spectrum(&g); + let b_bar = DenselySampledSpectrum::from_spectrum(&b); let mut rgb_camera = [[0.; 3]; N_SWATCH_REFLECTANCES]; for i in 0..N_SWATCH_REFLECTANCES { let rgb = Self::project_reflectance::( &SWATCH_REFLECTANCES[i], - sensor_illum, + illum, &Spectrum::DenselySampled(r_bar.clone()), &Spectrum::DenselySampled(g_bar.clone()), &Spectrum::DenselySampled(b_bar.clone()), @@ -179,8 +409,8 @@ impl PixelSensor { } let mut xyz_output = [[0.; 3]; N_SWATCH_REFLECTANCES]; - let sensor_white_g = sensor_illum.inner_product(&Spectrum::DenselySampled(g_bar.clone())); - let sensor_white_y = sensor_illum.inner_product(cie_y()); + let sensor_white_g = illum.inner_product(&Spectrum::DenselySampled(g_bar.clone())); + let sensor_white_y = illum.inner_product(cie_y()); for i in 0..N_SWATCH_REFLECTANCES { let s = SWATCH_REFLECTANCES[i].clone(); let xyz = Self::project_reflectance::( @@ -207,13 +437,13 @@ impl PixelSensor { } pub fn new_with_white_balance( - output_colorspace: RGBColorSpace, - sensor_illum: Option, + output_colorspace: Arc, + sensor_illum: Option>, imaging_ratio: Float, ) -> Self { - let r_bar = DenselySampledSpectrum::from_spectrum(cie_x(), LAMBDA_MIN, LAMBDA_MAX); - let g_bar = DenselySampledSpectrum::from_spectrum(cie_y(), LAMBDA_MIN, LAMBDA_MAX); - let b_bar = DenselySampledSpectrum::from_spectrum(cie_z(), LAMBDA_MIN, LAMBDA_MAX); + 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 xyz_from_sensor_rgb: SquareMatrix; if let Some(illum) = sensor_illum { @@ -233,6 +463,66 @@ impl PixelSensor { } } + pub fn create( + params: &ParameterDictionary, + output_colorspace: Arc, + exposure_time: Float, + loc: &FileLoc, + ) -> Result { + let iso = params.get_one_float("iso", 100.); + let mut white_balance_temp = params.get_one_float("whitebalance", 0.); + let sensor_name = params.get_one_string("sensor", "cie1931"); + if sensor_name != "cie1931" && white_balance_temp == 0. { + white_balance_temp = 6500.; + } + let imaging_ratio = exposure_time * iso / 100.; + + let d_illum = if white_balance_temp == 0. { + generate_cie_d(6500.) + } else { + generate_cie_d(white_balance_temp) + }; + + let sensor_illum: Option> = if white_balance_temp != 0. { + Some(Arc::new(Spectrum::DenselySampled(d_illum))) + } else { + None + }; + + if sensor_name == "cie1931" { + return Ok(PixelSensor::new_with_white_balance( + output_colorspace, + sensor_illum, + imaging_ratio, + )); + } else { + let r_opt = get_named_spectrum(&format!("{}_r", sensor_name)); + let g_opt = get_named_spectrum(&format!("{}_g", sensor_name)); + let b_opt = get_named_spectrum(&format!("{}_b", sensor_name)); + if r_opt.is_none() || g_opt.is_none() || b_opt.is_none() { + return Err(format!( + "{}: unknown sensor type '{}' (missing RGB spectral data)", + loc, sensor_name + ) + .into()); + } + + let r = Arc::new(r_opt.unwrap()); + let g = Arc::new(g_opt.unwrap()); + let b = Arc::new(b_opt.unwrap()); + + return PixelSensor::new( + r, + g, + b, + output_colorspace.clone(), + sensor_illum, + imaging_ratio, + ) + .map_err(|e| e.to_string()); + } + } + pub fn project_reflectance( refl: &Spectrum, illum: &Spectrum, @@ -328,29 +618,71 @@ pub struct FilmBase { impl FilmBase { pub fn new( - full_resolution: Point2i, - pixel_bounds: Bounds2i, + params: &ParameterDictionary, filter: Filter, - diagonal: Float, sensor: Option, - filename: String, + loc: &FileLoc, ) -> Self { + let x_res = params.get_one_int("xresolution", 1280); + let y_res = params.get_one_int("yresolution", 720); + + if x_res <= 0 || y_res <= 0 { + eprintln!( + "{}: Film resolution must be > 0. Defaulting to 1280x720.", + loc + ); + } + let full_resolution = Point2i::new(x_res.max(1), y_res.max(1)); + + let crop_data = params.get_float_array("cropwindow"); + let crop = if crop_data.len() == 4 { + Bounds2f::from_points( + Point2f::new(crop_data[0], crop_data[2]), + Point2f::new(crop_data[1], crop_data[3]), + ) + } else { + Bounds2f::from_points(Point2f::zero(), Point2f::new(1.0, 1.0)) + }; + + let p_min = Point2i::new( + (full_resolution.x() as Float * crop.p_min.x()).ceil() as i32, + (full_resolution.y() as Float * crop.p_min.y()).ceil() as i32, + ); + let p_max = Point2i::new( + (full_resolution.x() as Float * crop.p_max.x()).ceil() as i32, + (full_resolution.y() as Float * crop.p_max.y()).ceil() as i32, + ); + + let mut pixel_bounds = Bounds2i::from_points(p_min, p_max); + + if pixel_bounds.is_empty() { + eprintln!("{}: Film crop window results in empty pixel bounds.", loc); + } + + let rad = filter.radius(); + let expansion = Point2i::new(rad.x().ceil() as i32, rad.y().ceil() as i32); + pixel_bounds = pixel_bounds.expand(expansion); + + let diagonal_mm = params.get_one_float("diagonal", 35.0); + let filename = params.get_one_string("filename", "pbrt.exr"); + Self { full_resolution, pixel_bounds, filter, - diagonal: 0.001 * diagonal, + diagonal: diagonal_mm * 0.001, sensor, filename, } } } +#[enum_dispatch] pub trait FilmTrait: Sync { fn base(&self) -> &FilmBase; fn base_mut(&mut self) -> &mut FilmBase; fn add_sample( - &mut self, + &self, _p_filme: Point2i, _l: SampledSpectrum, _lambda: &SampledWavelengths, @@ -487,13 +819,46 @@ pub trait FilmTrait: Sync { } } -#[derive(Clone, Debug)] +#[enum_dispatch(FilmTrait)] +#[derive(Debug)] pub enum Film { RGB(RGBFilm), - GBuffer(GBufferBFilm), + GBuffer(GBufferFilm), Spectral(SpectralFilm), } +impl Film { + pub fn create( + name: &str, + params: &ParameterDictionary, + exposure_time: Float, + filter: Filter, + camera_transform: Option, + loc: &FileLoc, + ) -> Result { + match name { + "rgb" => { + let film = RGBFilm::create(params, exposure_time, filter, camera_transform, loc)?; + + Ok(Film::RGB(film)) + } + "gbuffer" => { + let film = + GBufferFilm::create(params, exposure_time, filter, camera_transform, loc)?; + + Ok(Film::GBuffer(film)) + } + "spectral" => { + let film = + SpectralFilm::create(params, exposure_time, filter, camera_transform, loc)?; + + Ok(Film::Spectral(film)) + } + _ => Err(format!("Film type '{}' unknown at {}", name, loc)), + } + } +} + impl FilmTrait for RGBFilm { fn base(&self) -> &FilmBase { &self.base @@ -504,7 +869,7 @@ impl FilmTrait for RGBFilm { } fn add_sample( - &mut self, + &self, p_film: Point2i, l: SampledSpectrum, lambda: &SampledWavelengths, @@ -606,7 +971,7 @@ impl FilmTrait for RGBFilm { } } -impl FilmTrait for GBufferBFilm { +impl FilmTrait for GBufferFilm { fn base(&self) -> &FilmBase { &self.base } @@ -700,29 +1065,3 @@ impl FilmTrait for SpectralFilm { true } } - -impl FilmTrait for Film { - fn base(&self) -> &FilmBase { - match self { - Film::RGB(film) => film.base(), - Film::GBuffer(film) => film.base(), - Film::Spectral(film) => film.base(), - } - } - - fn base_mut(&mut self) -> &mut FilmBase { - match self { - Film::RGB(film) => film.base_mut(), - Film::GBuffer(film) => film.base_mut(), - Film::Spectral(film) => film.base_mut(), - } - } - - fn uses_visible_surface(&self) -> bool { - match self { - Film::RGB(film) => film.uses_visible_surface(), - Film::GBuffer(film) => film.uses_visible_surface(), - Film::Spectral(film) => film.uses_visible_surface(), - } - } -} diff --git a/src/core/filter.rs b/shared/src/core/filter.rs similarity index 76% rename from src/core/filter.rs rename to shared/src/core/filter.rs index 3bb3ba9..405249c 100644 --- a/src/core/filter.rs +++ b/shared/src/core/filter.rs @@ -1,7 +1,9 @@ -use crate::core::pbrt::{Float, lerp}; -use crate::geometry::{Bounds2f, Bounds2i, Point2f, Point2i, Vector2f}; +use crate::core::geometry::{Bounds2f, Bounds2i, Point2f, Point2i, Vector2f}; +use crate::core::pbrt::Float; use crate::utils::containers::Array2D; -use crate::utils::math::{gaussian, gaussian_integral, sample_tent, windowed_sinc}; +use crate::utils::error::FileLoc; +use crate::utils::math::{gaussian, gaussian_integral, lerp, sample_tent, windowed_sinc}; +use crate::utils::parameters::ParameterDictionary; use crate::utils::sampling::PiecewiseConstant2D; use enum_dispatch::enum_dispatch; @@ -76,6 +78,34 @@ pub enum Filter { Triangle(TriangleFilter), } +impl Filter { + pub fn create(name: &str, params: ParameterDictionary, loc: &FileLoc) -> Result { + match name { + "box" => { + let filter = BoxFilter::create(¶ms, loc); + Ok(Filter::Box(filter)) + } + "gaussian" => { + let filter = GaussianFilter::create(¶ms, loc); + Ok(Filter::Gaussian(filter)) + } + "mitchell" => { + let filter = MitchellFilter::create(¶ms, loc); + Ok(Filter::Mitchell(filter)) + } + "sinc" => { + let filter = LanczosSincFilter::create(¶ms, loc); + Ok(Filter::LanczosSinc(filter)) + } + "triangle" => { + let filter = TriangleFilter::create(¶ms, loc); + Ok(Filter::Triangle(filter)) + } + _ => Err(format!("Film type '{}' unknown at {}", name, loc)), + } + } +} + #[derive(Clone, Debug)] pub struct BoxFilter { pub radius: Vector2f, @@ -85,6 +115,12 @@ impl BoxFilter { pub fn new(radius: Vector2f) -> Self { Self { radius } } + + pub fn create(params: &ParameterDictionary, _loc: &FileLoc) -> Self { + let xw = params.get_one_float("xradius", 0.5); + let yw = params.get_one_float("yradius", 0.5); + Self::new(Vector2f::new(xw, yw)) + } } impl FilterTrait for BoxFilter { @@ -141,6 +177,13 @@ impl GaussianFilter { sampler, } } + + pub fn create(params: &ParameterDictionary, _loc: &FileLoc) -> Self { + let xw = params.get_one_float("xradius", 1.5); + let yw = params.get_one_float("yradius", 1.5); + let sigma = params.get_one_float("sigma", 0.5); + Self::new(Vector2f::new(xw, yw), sigma) + } } impl FilterTrait for GaussianFilter { @@ -189,6 +232,14 @@ impl MitchellFilter { } } + pub fn create(params: &ParameterDictionary, _loc: &FileLoc) -> Self { + let xw = params.get_one_float("xradius", 2.); + let yw = params.get_one_float("yradius", 2.); + let b = params.get_one_float("B", 1. / 3.); + let c = params.get_one_float("C", 1. / 3.); + Self::new(Vector2f::new(xw, yw), b, c) + } + fn mitchell_1d_eval(b: Float, c: Float, x: Float) -> Float { let x = x.abs(); if x <= 1.0 { @@ -250,6 +301,13 @@ impl LanczosSincFilter { sampler, } } + + pub fn create(params: &ParameterDictionary, _loc: &FileLoc) -> Self { + let xw = params.get_one_float("xradius", 4.); + let yw = params.get_one_float("yradius", 4.); + let tau = params.get_one_float("tau", 3.); + Self::new(Vector2f::new(xw, yw), tau) + } } impl FilterTrait for LanczosSincFilter { @@ -299,6 +357,12 @@ impl TriangleFilter { pub fn new(radius: Vector2f) -> Self { Self { radius } } + + pub fn create(params: &ParameterDictionary, _loc: &FileLoc) -> Self { + let xw = params.get_one_float("xradius", 2.); + let yw = params.get_one_float("yradius", 2.); + Self::new(Vector2f::new(xw, yw)) + } } impl FilterTrait for TriangleFilter { diff --git a/src/core/geometry/bounds.rs b/shared/src/core/geometry/bounds.rs similarity index 97% rename from src/core/geometry/bounds.rs rename to shared/src/core/geometry/bounds.rs index 28f3b1a..899a14a 100644 --- a/src/core/geometry/bounds.rs +++ b/shared/src/core/geometry/bounds.rs @@ -87,12 +87,12 @@ where d.0.iter().fold(T::one(), |acc, &val| acc * val) } - pub fn expand(&self, delta: T) -> Self { - let mut p_min = self.p_min; - let mut p_max = self.p_max; - p_min = p_min - Vector::fill(delta); - p_max = p_max + Vector::fill(delta); - Self { p_min, p_max } + pub fn expand(&self, delta: impl Into>) -> Self { + let v = delta.into(); + Self { + p_min: self.p_min - v, + p_max: self.p_max + v, + } } pub fn lerp(&self, t: Point) -> Point { diff --git a/src/geometry/cone.rs b/shared/src/core/geometry/cone.rs similarity index 95% rename from src/geometry/cone.rs rename to shared/src/core/geometry/cone.rs index 3fb84f3..c09b5d1 100644 --- a/src/geometry/cone.rs +++ b/shared/src/core/geometry/cone.rs @@ -1,6 +1,6 @@ use super::{Bounds3f, Float, PI, Point3f, Vector3f, VectorLike}; use crate::utils::math::{degrees, safe_acos, safe_asin, safe_sqrt, square}; -use crate::utils::transform::Transform; +use crate::utils::transform::TransformGeneric; #[derive(Debug, Clone)] pub struct DirectionCone { @@ -110,7 +110,7 @@ impl DirectionCone { return DirectionCone::entire_sphere(); } - let w = Transform::rotate_around_axis(degrees(theta_r), wr).apply_to_vector(a.w); + let w = TransformGeneric::rotate_around_axis(degrees(theta_r), wr).apply_to_vector(a.w); DirectionCone::new(w, theta_o.cos()) } } diff --git a/src/geometry/mod.rs b/shared/src/core/geometry/mod.rs similarity index 93% rename from src/geometry/mod.rs rename to shared/src/core/geometry/mod.rs index 71a8e84..8d8efb1 100644 --- a/src/geometry/mod.rs +++ b/shared/src/core/geometry/mod.rs @@ -13,8 +13,8 @@ pub use self::primitives::{ pub use self::ray::{Ray, RayDifferential}; pub use self::traits::{Lerp, Sqrt, Tuple, VectorLike}; -use crate::core::pbrt::{Float, PI, clamp_t}; -use crate::utils::math::square; +use crate::core::pbrt::{Float, PI}; +use crate::utils::math::{clamp, square}; use num_traits::Float as NumFloat; #[inline] @@ -61,7 +61,7 @@ pub fn cos_phi(w: Vector3f) -> Float { if sin_theta == 0. { 1. } else { - clamp_t(w.x() / sin_theta, -1., 1.) + clamp(w.x() / sin_theta, -1., 1.) } } @@ -71,7 +71,7 @@ pub fn sin_phi(w: Vector3f) -> Float { if sin_theta == 0. { 0. } else { - clamp_t(w.y() / sin_theta, -1., 1.) + clamp(w.y() / sin_theta, -1., 1.) } } @@ -113,7 +113,7 @@ pub fn spherical_quad_area(a: Vector3f, b: Vector3f, c: Vector3f, d: Vector3f) - } pub fn spherical_theta(v: Vector3f) -> Float { - clamp_t(v.z(), -1.0, 1.0).acos() + clamp(v.z(), -1.0, 1.0).acos() } pub fn spherical_phi(v: Vector3f) -> Float { diff --git a/src/core/geometry/primitives.rs b/shared/src/core/geometry/primitives.rs similarity index 94% rename from src/core/geometry/primitives.rs rename to shared/src/core/geometry/primitives.rs index e178b0b..ba1fa3c 100644 --- a/src/core/geometry/primitives.rs +++ b/shared/src/core/geometry/primitives.rs @@ -1,7 +1,7 @@ use super::traits::{Sqrt, Tuple, VectorLike}; -use super::{Float, NumFloat, PI, clamp_t}; +use super::{Float, NumFloat, PI}; use crate::utils::interval::Interval; -use crate::utils::math::{difference_of_products, quadratic, safe_asin}; +use crate::utils::math::{clamp, difference_of_products, quadratic, safe_asin}; use num_traits::{AsPrimitive, FloatConst, Num, Signed, Zero}; use std::hash::{Hash, Hasher}; use std::iter::Sum; @@ -10,12 +10,15 @@ use std::ops::{ }; // N-dimensional displacement +#[repr(C)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Vector(pub [T; N]); // N-dimensional location +#[repr(C)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Point(pub [T; N]); // N-dimensional surface normal +#[repr(C)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Normal(pub [T; N]); @@ -55,6 +58,16 @@ macro_rules! impl_tuple_core { } } + impl From for $Struct + where + T: Copy, + { + #[inline] + fn from(scalar: T) -> Self { + Self([scalar; N]) + } + } + impl $Struct { #[inline] pub fn floor(&self) -> $Struct { @@ -272,6 +285,23 @@ macro_rules! impl_float_vector_ops { }; } +macro_rules! impl_tuple_conversions { + ($Struct:ident) => { + impl From<(T, T, T)> for $Struct { + fn from(tuple: (T, T, T)) -> Self { + Self([tuple.0, tuple.1, tuple.2]) + } + } + + // Allow converting (x, y) -> Vector + impl From<(T, T)> for $Struct { + fn from(tuple: (T, T)) -> Self { + Self([tuple.0, tuple.1]) + } + } + }; +} + macro_rules! impl_abs { ($Struct:ident) => { impl $Struct @@ -351,6 +381,11 @@ impl_accessors!(Vector); impl_accessors!(Point); impl_accessors!(Normal); +// Convert from tuple of Floats, for parsing issues +impl_tuple_conversions!(Vector); +impl_tuple_conversions!(Point); +impl_tuple_conversions!(Normal); + impl From> for Normal { fn from(v: Vector) -> Self { Self(v.0) @@ -448,9 +483,11 @@ pub type Vector3 = Vector; pub type Vector3f = Vector3; pub type Vector3i = Vector3; pub type Vector3fi = Vector3; +pub type Normal2 = Normal; pub type Normal3 = Normal; pub type Normal3f = Normal3; pub type Normal3i = Normal3; +pub type Vector4 = Vector; impl Vector2 { pub fn new(x: T, y: T) -> Self { @@ -462,6 +499,12 @@ impl Point2 { Self([x, y]) } } +impl Normal2 { + pub fn new(x: T, y: T) -> Self { + Self([x, y]) + } +} + impl Vector3 { pub fn new(x: T, y: T, z: T) -> Self { Self([x, y, z]) @@ -477,6 +520,11 @@ impl Normal3 { Self([x, y, z]) } } +impl Vector4 { + pub fn new(x: T, y: T, z: T, w: T) -> Self { + Self([x, y, z, w]) + } +} // Vector operations impl Vector3 @@ -786,7 +834,7 @@ impl OctahedralVector { #[inline] pub fn encode(f: Float) -> u16 { - (clamp_t((f + 1.0) / 2.0, 0.0, 1.0) * 65535.0).round() as u16 + (clamp((f + 1.0) / 2.0, 0.0, 1.0) * 65535.0).round() as u16 } } diff --git a/src/core/geometry/ray.rs b/shared/src/core/geometry/ray.rs similarity index 100% rename from src/core/geometry/ray.rs rename to shared/src/core/geometry/ray.rs diff --git a/src/core/geometry/traits.rs b/shared/src/core/geometry/traits.rs similarity index 100% rename from src/core/geometry/traits.rs rename to shared/src/core/geometry/traits.rs diff --git a/src/core/interaction.rs b/shared/src/core/interaction.rs similarity index 95% rename from src/core/interaction.rs rename to shared/src/core/interaction.rs index ce3688e..93c9dca 100644 --- a/src/core/interaction.rs +++ b/shared/src/core/interaction.rs @@ -1,22 +1,22 @@ use crate::camera::{Camera, CameraTrait}; use crate::core::bssrdf::BSSRDF; -use crate::core::bxdf::{BSDF, BxDFFlags, DiffuseBxDF}; +use crate::core::bxdf::{BSDF, BxDF, BxDFFlags, DiffuseBxDF}; +use crate::core::geometry::{ + Normal3f, Point2f, Point3f, Point3fi, Ray, RayDifferential, Vector3f, VectorLike, +}; 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, clamp_t}; +use crate::core::pbrt::Float; use crate::core::sampler::{Sampler, SamplerTrait}; use crate::core::texture::{FloatTexture, UniversalTextureEvaluator}; -use crate::geometry::{ - Normal3f, Point2f, Point3f, Point3fi, Ray, RayDifferential, Vector3f, VectorLike, -}; use crate::image::Image; use crate::lights::{Light, LightTrait}; use crate::shapes::Shape; use crate::spectra::{SampledSpectrum, SampledWavelengths}; -use crate::utils::math::{difference_of_products, square}; +use crate::utils::math::{clamp, difference_of_products, square}; use bumpalo::Bump; use enum_dispatch::enum_dispatch; @@ -172,7 +172,7 @@ impl InteractionTrait for SimpleInteraction { } #[derive(Default, Clone, Debug)] -pub struct ShadingGeometry { +pub struct Shadinggeom { pub n: Normal3f, pub dpdu: Vector3f, pub dpdv: Vector3f, @@ -188,7 +188,7 @@ pub struct SurfaceInteraction { pub dpdv: Vector3f, pub dndu: Normal3f, pub dndv: Normal3f, - pub shading: ShadingGeometry, + pub shading: Shadinggeom, pub face_index: usize, pub area_light: Option>, pub material: Option>, @@ -273,22 +273,22 @@ impl SurfaceInteraction { self.dvdy = difference_of_products(ata00, atb1y, ata01, atb0y) * inv_det; // Clamp derivatives self.dudx = if self.dudx.is_finite() { - clamp_t(self.dudx, -1e8, 1e8) + clamp(self.dudx, -1e8, 1e8) } else { 0. }; self.dvdx = if self.dvdx.is_finite() { - clamp_t(self.dvdx, -1e8, 1e8) + clamp(self.dvdx, -1e8, 1e8) } else { 0. }; self.dudy = if self.dudy.is_finite() { - clamp_t(self.dudy, -1e8, 1e8) + clamp(self.dudy, -1e8, 1e8) } else { 0. }; self.dvdy = if self.dvdy.is_finite() { - clamp_t(self.dvdy, -1e8, 1e8) + clamp(self.dvdy, -1e8, 1e8) } else { 0. }; @@ -304,14 +304,13 @@ impl SurfaceInteraction { } } - pub fn get_bsdf<'a>( + pub fn get_bsdf( &mut self, r: &Ray, lambda: &SampledWavelengths, camera: &Camera, - scratch: &'a Bump, sampler: &mut Sampler, - ) -> Option> { + ) -> Option { self.compute_differentials(r, camera, sampler.samples_per_pixel() as i32); let material = { @@ -332,13 +331,13 @@ impl SurfaceInteraction { let normal_map = material.get_normal_map(); if displacement.is_some() || normal_map.is_some() { // This calls the function defined above - self.compute_bump_geometry(&tex_eval, displacement, normal_map); + self.compute_bump_geom(&tex_eval, displacement, normal_map); } - let mut bsdf = material.get_bxdf(&tex_eval, &ctx, lambda, scratch); + let mut bsdf = material.get_bsdf(&tex_eval, &ctx, lambda); if get_options().force_diffuse { let r = bsdf.rho_wo(self.common.wo, &[sampler.get1d()], &[sampler.get2d()]); - let diff_bxdf = scratch.alloc(DiffuseBxDF::new(r)); + let diff_bxdf = BxDF::Diffuse(DiffuseBxDF::new(r)); bsdf = BSDF::new(self.shading.n, self.shading.dpdu, Some(diff_bxdf)); } Some(bsdf) @@ -350,7 +349,7 @@ impl SurfaceInteraction { lambda: &SampledWavelengths, _camera: &Camera, _scratch: &Bump, - ) -> Option> { + ) -> Option { let material = { let root_mat = self.material.as_deref()?; let mut active_mat: &Material = root_mat; @@ -368,17 +367,17 @@ impl SurfaceInteraction { material.get_bssrdf(&tex_eval, &ctx, lambda) } - fn compute_bump_geometry( + fn compute_bump_geom( &mut self, tex_eval: &UniversalTextureEvaluator, displacement: Option, - normal_image: Option<&Image>, + normal_image: Option>, ) { let ctx = NormalBumpEvalContext::from(&*self); let (dpdu, dpdv) = if let Some(disp) = displacement { bump_map(tex_eval, &disp, &ctx) } else if let Some(map) = normal_image { - normal_map(map, &ctx) + normal_map(map.as_ref(), &ctx) } else { (self.shading.dpdu, self.shading.dpdv) }; @@ -388,7 +387,7 @@ impl SurfaceInteraction { ns = -ns; } - self.set_shading_geometry(ns, dpdu, dpdv, self.shading.dndu, self.shading.dndv, false); + self.set_shading_geom(ns, dpdu, dpdv, self.shading.dndu, self.shading.dndv, false); } pub fn spawn_ray_with_differentials( @@ -544,7 +543,7 @@ impl SurfaceInteraction { dpdv, dndu, dndv, - shading: ShadingGeometry { + shading: Shadinggeom { n: shading_n, dpdu, dpdv, @@ -581,7 +580,7 @@ impl SurfaceInteraction { si } - pub fn set_shading_geometry( + pub fn set_shading_geom( &mut self, ns: Normal3f, dpdus: Vector3f, diff --git a/src/core/material.rs b/shared/src/core/material.rs similarity index 54% rename from src/core/material.rs rename to shared/src/core/material.rs index 9c61e2d..398289f 100644 --- a/src/core/material.rs +++ b/shared/src/core/material.rs @@ -4,16 +4,21 @@ use std::ops::Deref; use std::sync::Arc; use crate::core::bssrdf::BSSRDF; -use crate::core::bxdf::{BSDF, DielectricBxDF, DiffuseBxDF}; +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, ShadingGeometry, SurfaceInteraction}; +use crate::core::interaction::{Interaction, Shadinggeom, SurfaceInteraction}; use crate::core::pbrt::Float; -use crate::core::texture::{FloatTexture, SpectrumTexture, TextureEvalContext, TextureEvaluator}; -use crate::geometry::{Frame, Normal3f, Point2f, Point3f, Vector2f, Vector3f, VectorLike}; +use crate::core::scattering::TrowbridgeReitzDistribution; +use crate::core::texture::{ + FloatTexture, FloatTextureTrait, SpectrumTexture, TextureEvalContext, TextureEvaluator, +}; use crate::image::{Image, WrapMode, WrapMode2D}; -use crate::spectra::{SampledWavelengths, Spectrum, SpectrumTrait}; +use crate::spectra::{SampledSpectrum, SampledWavelengths, Spectrum, SpectrumProvider}; use crate::utils::hash::hash_float; -use crate::utils::scattering::TrowbridgeReitzDistribution; +use crate::utils::math::clamp; #[derive(Clone, Debug)] pub struct MaterialEvalContext { @@ -47,7 +52,7 @@ pub struct NormalBumpEvalContext { p: Point3f, uv: Point2f, n: Normal3f, - shading: ShadingGeometry, + shading: Shadinggeom, dpdx: Vector3f, dpdy: Vector3f, // All 0 @@ -148,23 +153,22 @@ pub fn bump_map( #[enum_dispatch] pub trait MaterialTrait: Send + Sync + std::fmt::Debug { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, tex_eval: &T, ctx: &MaterialEvalContext, lambda: &SampledWavelengths, - scratch: &'a Bump, - ) -> BSDF<'a>; + ) -> BSDF; - fn get_bssrdf<'a, T: TextureEvaluator>( + fn get_bssrdf( &self, tex_eval: &T, ctx: &MaterialEvalContext, lambda: &SampledWavelengths, - ) -> Option>; + ) -> Option; fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool; - fn get_normal_map(&self) -> Option<&Image>; + fn get_normal_map(&self) -> Option>; fn get_displacement(&self) -> Option; fn has_surface_scattering(&self) -> bool; } @@ -186,100 +190,354 @@ pub enum Material { } #[derive(Clone, Debug)] -pub struct CoatedDiffuseMaterial; +pub struct CoatedDiffuseMaterial { + displacement: FloatTexture, + normal_map: Option>, + reflectance: SpectrumTexture, + albedo: SpectrumTexture, + u_roughness: FloatTexture, + v_roughness: FloatTexture, + thickness: FloatTexture, + g: FloatTexture, + eta: Spectrum, + remap_roughness: bool, + max_depth: usize, + n_samples: usize, +} + +impl CoatedDiffuseMaterial { + #[allow(clippy::too_many_arguments)] + pub fn new( + reflectance: SpectrumTexture, + u_roughness: FloatTexture, + v_roughness: FloatTexture, + thickness: FloatTexture, + albedo: SpectrumTexture, + g: FloatTexture, + eta: Spectrum, + displacement: FloatTexture, + normal_map: Option>, + 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_bxdf<'a, T: TextureEvaluator>( + 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, - _scratch: &'a Bump, - ) -> BSDF<'a> { - todo!() - } - fn get_bssrdf<'a, T>( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option> { - todo!() + ) -> Option { + None } - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() + 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) -> Option<&Image> { - todo!() + fn get_normal_map(&self) -> Option> { + self.normal_map.clone() } fn get_displacement(&self) -> Option { - todo!() + Some(self.displacement.clone()) } + fn has_surface_scattering(&self) -> bool { - todo!() + false } } #[derive(Clone, Debug)] -pub struct CoatedConductorMaterial; +pub struct CoatedConductorMaterial { + 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, +} + +impl CoatedConductorMaterial { + #[allow(clippy::too_many_arguments)] + 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_bxdf<'a, T: TextureEvaluator>( + 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, - _scratch: &'a Bump, - ) -> BSDF<'a> { - todo!() + ) -> Option { + None } - fn get_bssrdf<'a, T>( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option> { - todo!() + + 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 can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() - } - fn get_normal_map(&self) -> Option<&Image> { - todo!() + + fn get_normal_map(&self) -> Option> { + self.normal_map.clone() } fn get_displacement(&self) -> Option { - todo!() + Some(self.displacement.clone()) } + fn has_surface_scattering(&self) -> bool { - todo!() + false } } + #[derive(Clone, Debug)] pub struct ConductorMaterial; impl MaterialTrait for ConductorMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - _scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { todo!() } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { todo!() } - fn get_normal_map(&self) -> Option<&Image> { + fn get_normal_map(&self) -> Option> { todo!() } fn get_displacement(&self) -> Option { @@ -300,13 +558,12 @@ pub struct DielectricMaterial { } impl MaterialTrait for DielectricMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, tex_eval: &T, ctx: &MaterialEvalContext, lambda: &SampledWavelengths, - scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { let mut sampled_eta = self.eta.evaluate(lambda[0]); if !self.eta.is_constant() { lambda.terminate_secondary(); @@ -325,17 +582,17 @@ impl MaterialTrait for DielectricMaterial { } let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); - let bxdf = scratch.alloc(DielectricBxDF::new(sampled_eta, distrib)); + let bxdf = BxDF::Dielectric(DielectricBxDF::new(sampled_eta, distrib)); BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { None } @@ -343,8 +600,8 @@ impl MaterialTrait for DielectricMaterial { tex_eval.can_evaluate(&[&self.u_roughness, &self.v_roughness], &[]) } - fn get_normal_map(&self) -> Option<&Image> { - self.normal_map.as_deref() + fn get_normal_map(&self) -> Option> { + self.normal_map.clone() } fn get_displacement(&self) -> Option { @@ -363,24 +620,23 @@ pub struct DiffuseMaterial { } impl MaterialTrait for DiffuseMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, tex_eval: &T, ctx: &MaterialEvalContext, lambda: &SampledWavelengths, - scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { let r = tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda); - let bxdf = scratch.alloc(DiffuseBxDF::new(r)); + let bxdf = BxDF::Diffuse(DiffuseBxDF::new(r)); BSDF::new(ctx.ns, ctx.dpdus, Some(bxdf)) } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } @@ -388,12 +644,14 @@ impl MaterialTrait for DiffuseMaterial { tex_eval.can_evaluate(&[], &[&self.reflectance]) } - fn get_normal_map(&self) -> Option<&Image> { - self.normal_map.as_deref() + 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 } @@ -401,33 +659,35 @@ impl MaterialTrait for DiffuseMaterial { #[derive(Clone, Debug)] pub struct DiffuseTransmissionMaterial; impl MaterialTrait for DiffuseTransmissionMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - _scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { todo!() } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { todo!() } - fn get_normal_map(&self) -> Option<&Image> { + + fn get_normal_map(&self) -> Option> { todo!() } + fn get_displacement(&self) -> Option { todo!() } + fn has_surface_scattering(&self) -> bool { todo!() } @@ -435,28 +695,27 @@ impl MaterialTrait for DiffuseTransmissionMaterial { #[derive(Clone, Debug)] pub struct HairMaterial; impl MaterialTrait for HairMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - _scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { todo!() } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { todo!() } - fn get_normal_map(&self) -> Option<&Image> { + fn get_normal_map(&self) -> Option> { todo!() } fn get_displacement(&self) -> Option { @@ -470,28 +729,27 @@ impl MaterialTrait for HairMaterial { #[derive(Clone, Debug)] pub struct MeasuredMaterial; impl MaterialTrait for MeasuredMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - _scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { todo!() } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { todo!() } - fn get_normal_map(&self) -> Option<&Image> { + fn get_normal_map(&self) -> Option> { todo!() } fn get_displacement(&self) -> Option { @@ -504,28 +762,27 @@ impl MaterialTrait for MeasuredMaterial { #[derive(Clone, Debug)] pub struct SubsurfaceMaterial; impl MaterialTrait for SubsurfaceMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - _scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { todo!() } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { todo!() } - fn get_normal_map(&self) -> Option<&Image> { + fn get_normal_map(&self) -> Option> { todo!() } fn get_displacement(&self) -> Option { @@ -538,27 +795,26 @@ impl MaterialTrait for SubsurfaceMaterial { #[derive(Clone, Debug)] pub struct ThinDielectricMaterial; impl MaterialTrait for ThinDielectricMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - _scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { todo!() } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { todo!() } fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { todo!() } - fn get_normal_map(&self) -> Option<&Image> { + fn get_normal_map(&self) -> Option> { todo!() } fn get_displacement(&self) -> Option { @@ -600,23 +856,22 @@ impl MixMaterial { } impl MaterialTrait for MixMaterial { - fn get_bxdf<'a, T: TextureEvaluator>( + fn get_bsdf( &self, tex_eval: &T, ctx: &MaterialEvalContext, lambda: &SampledWavelengths, - scratch: &'a Bump, - ) -> BSDF<'a> { + ) -> BSDF { let chosen_mat = self.choose_material(tex_eval, ctx); - chosen_mat.get_bxdf(tex_eval, ctx, lambda, scratch) + chosen_mat.get_bsdf(tex_eval, ctx, lambda) } - fn get_bssrdf<'a, T>( + fn get_bssrdf( &self, _tex_eval: &T, _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, - ) -> Option> { + ) -> Option { None } @@ -624,7 +879,7 @@ impl MaterialTrait for MixMaterial { tex_eval.can_evaluate(&[&self.amount], &[]) } - fn get_normal_map(&self) -> Option<&Image> { + fn get_normal_map(&self) -> Option> { None } diff --git a/src/core/medium.rs b/shared/src/core/medium.rs similarity index 95% rename from src/core/medium.rs rename to shared/src/core/medium.rs index 9e78d75..2cce58f 100644 --- a/src/core/medium.rs +++ b/shared/src/core/medium.rs @@ -2,18 +2,18 @@ use bumpalo::Bump; use enum_dispatch::enum_dispatch; use std::sync::Arc; -use crate::core::pbrt::{Float, INV_4_PI, PI, clamp_t}; -use crate::geometry::{ +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::spectra::{ BlackbodySpectrum, DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, RGBIlluminantSpectrum, - RGBUnboundedSpectrum, SampledSpectrum, SampledWavelengths, Spectrum, SpectrumTrait, + RGBUnboundedSpectrum, SampledSpectrum, SampledWavelengths, Spectrum, SpectrumProvider, }; use crate::utils::containers::SampledGrid; -use crate::utils::math::square; +use crate::utils::math::{clamp, square}; use crate::utils::rng::Rng; -use crate::utils::transform::Transform; +use crate::utils::transform::TransformGeneric; #[derive(Debug, Clone, Copy)] pub struct PhaseFunctionSample { @@ -229,7 +229,7 @@ impl DDAMajorantIterator { let grid_intersect = Vector3f::from(p_grid_start); for axis in 0..3 { - iter.voxel[axis] = clamp_t( + iter.voxel[axis] = clamp( (grid_intersect[axis] * grid.res[axis] as Float) as i32, 0, grid.res[axis] - 1, @@ -424,11 +424,9 @@ impl HomogeneousMedium { le_scale: Float, g: Float, ) -> Self { - let mut sigma_a_spec = - DenselySampledSpectrum::from_spectrum(&sigma_a, LAMBDA_MIN, LAMBDA_MAX); - let mut sigma_s_spec = - DenselySampledSpectrum::from_spectrum(&sigma_s, LAMBDA_MIN, LAMBDA_MAX); - let mut le_spec = DenselySampledSpectrum::from_spectrum(&le, LAMBDA_MIN, LAMBDA_MAX); + let mut sigma_a_spec = DenselySampledSpectrum::from_spectrum(&sigma_a); + let mut sigma_s_spec = DenselySampledSpectrum::from_spectrum(&sigma_s); + let mut le_spec = DenselySampledSpectrum::from_spectrum(&le); sigma_a_spec.scale(sigma_scale); sigma_s_spec.scale(sigma_scale); @@ -480,7 +478,7 @@ impl MediumTrait for HomogeneousMedium { #[derive(Debug, Clone)] pub struct GridMedium { bounds: Bounds3f, - render_from_medium: Transform, + render_from_medium: TransformGeneric, sigma_a_spec: DenselySampledSpectrum, sigma_s_spec: DenselySampledSpectrum, density_grid: SampledGrid, @@ -496,7 +494,7 @@ impl GridMedium { #[allow(clippy::too_many_arguments)] pub fn new( bounds: &Bounds3f, - render_from_medium: &Transform, + render_from_medium: &TransformGeneric, sigma_a: &Spectrum, sigma_s: &Spectrum, sigma_scale: Float, @@ -506,11 +504,9 @@ impl GridMedium { le: &Spectrum, le_scale: SampledGrid, ) -> Self { - let mut sigma_a_spec = - DenselySampledSpectrum::from_spectrum(sigma_a, LAMBDA_MIN, LAMBDA_MAX); - let mut sigma_s_spec = - DenselySampledSpectrum::from_spectrum(sigma_s, LAMBDA_MIN, LAMBDA_MAX); - let le_spec = DenselySampledSpectrum::from_spectrum(le, LAMBDA_MIN, LAMBDA_MAX); + let mut sigma_a_spec = DenselySampledSpectrum::from_spectrum(sigma_a); + let mut sigma_s_spec = DenselySampledSpectrum::from_spectrum(sigma_s); + let le_spec = DenselySampledSpectrum::from_spectrum(le); sigma_a_spec.scale(sigma_scale); sigma_s_spec.scale(sigma_scale); @@ -628,7 +624,7 @@ impl MediumTrait for GridMedium { #[derive(Debug, Clone)] pub struct RGBGridMedium { bounds: Bounds3f, - render_from_medium: Transform, + render_from_medium: TransformGeneric, le_grid: Option>, le_scale: Float, phase: HGPhaseFunction, @@ -642,7 +638,7 @@ impl RGBGridMedium { #[allow(clippy::too_many_arguments)] pub fn new( bounds: &Bounds3f, - render_from_medium: &Transform, + render_from_medium: &TransformGeneric, g: Float, sigma_a_grid: Option>, sigma_s_grid: Option>, diff --git a/src/core/mod.rs b/shared/src/core/mod.rs similarity index 85% rename from src/core/mod.rs rename to shared/src/core/mod.rs index b8e0d9e..883c47f 100644 --- a/src/core/mod.rs +++ b/shared/src/core/mod.rs @@ -1,9 +1,9 @@ pub mod aggregates; pub mod bssrdf; pub mod bxdf; -pub mod cie; pub mod film; pub mod filter; +pub mod geometry; pub mod interaction; pub mod material; pub mod medium; @@ -11,4 +11,5 @@ pub mod options; pub mod pbrt; pub mod primitive; pub mod sampler; +pub mod scattering; pub mod texture; diff --git a/src/core/options.rs b/shared/src/core/options.rs similarity index 73% rename from src/core/options.rs rename to shared/src/core/options.rs index b23ea8e..7235b75 100644 --- a/src/core/options.rs +++ b/shared/src/core/options.rs @@ -1,4 +1,5 @@ -use crate::geometry::Point2i; +use crate::core::geometry::{Bounds2f, Bounds2i, Point2f, Point2i}; +use crate::Float; use std::ops::Deref; use std::sync::OnceLock; @@ -19,11 +20,9 @@ pub struct BasicPBRTOptions { pub force_diffuse: bool, pub use_gpu: bool, pub wavefront: bool, + pub interactive: bool, + pub fullscreen: bool, pub rendering_space: RenderingCoordinateSystem, - pub debug_start: Option<(Point2i, i32)>, - pub write_partial_images: bool, - pub mse_reference_image: Option, - pub mse_reference_output: Option, } impl Default for BasicPBRTOptions { @@ -37,11 +36,9 @@ impl Default for BasicPBRTOptions { force_diffuse: false, use_gpu: false, wavefront: false, + interactive: false, + fullscreen: false, rendering_space: RenderingCoordinateSystem::CameraWorld, - debug_start: Some((Point2i::default(), 0)), - write_partial_images: false, - mse_reference_image: None, - mse_reference_output: None, } } } @@ -52,7 +49,20 @@ pub struct PBRTOptions { pub n_threads: usize, pub log_level: String, + pub write_partial_images: bool, pub image_file: String, + pub pixel_samples: Option, + pub gpu_device: Option, + pub mse_reference_image: Option, + pub mse_reference_output: Option, + pub debug_start: Option<(Point2i, i32)>, + pub quick_render: bool, + pub upgrade: bool, + pub display_server: String, + pub crop_window: Option, + pub pixel_bounds: Option, + pub pixel_material: Option, + pub displacement_edge_scale: Float, } impl Default for PBRTOptions { @@ -61,7 +71,20 @@ impl Default for PBRTOptions { basic: BasicPBRTOptions::default(), n_threads: 0, log_level: "info".to_string(), + write_partial_images: false, + pixel_samples: None, + gpu_device: None, + quick_render: false, + upgrade: false, image_file: "output.exr".to_string(), + mse_reference_image: None, + mse_reference_output: None, + debug_start: Some((Point2i::default(), 0)), + display_server: "".to_string(), + crop_window: None, + pixel_bounds: None, + pixel_material: None, + displacement_edge_scale: 1., } } } diff --git a/src/core/pbrt.rs b/shared/src/core/pbrt.rs similarity index 73% rename from src/core/pbrt.rs rename to shared/src/core/pbrt.rs index 2711de5..7beeb3f 100644 --- a/src/core/pbrt.rs +++ b/shared/src/core/pbrt.rs @@ -1,6 +1,5 @@ -use crate::geometry::{Lerp, Point2f, Vector2f, Vector3f}; +use crate::core::geometry::Lerp; use num_traits::{Num, PrimInt}; -use std::collections::HashSet; use std::hash::Hash; use std::ops::{Add, Mul}; use std::sync::atomic::{AtomicU64, Ordering as SyncOrdering}; @@ -101,64 +100,6 @@ pub const PI_OVER_2: Float = 1.570_796_326_794_896_619_23; pub const PI_OVER_4: Float = 0.785_398_163_397_448_309_61; pub const SQRT_2: Float = 1.414_213_562_373_095_048_80; -#[inline] -pub fn lerp(t: F, a: T, b: T) -> T -where - T: Lerp, - F: Copy, -{ - T::lerp(t, a, b) -} - -pub fn linear_pdf(x: T, a: T, b: T) -> T -where - T: Num + Copy + PartialOrd, -{ - if x < T::zero() || x > T::one() { - return T::zero(); - } - (T::one() + T::one()) * lerp(x, a, b) / (a + b) -} - -pub fn sample_linear(u: T, a: T, b: T) -> T -where - T: Num, -{ - if u == T::zero() && a == T::zero() { - return T::zero(); - } - - u * (a + b) -} - -pub fn clamp_t(val: T, low: T, high: T) -> T -where - T: PartialOrd, -{ - let r: T; - if val < low { - r = low; - } else if val > high { - r = high; - } else { - r = val; - } - r -} - -#[inline] -pub fn evaluate_polynomial(t: Float, coeffs: &[Float]) -> Option { - if coeffs.is_empty() { - return None; - } - - let mut result = coeffs[0]; - for &c in &coeffs[1..] { - result = t.mul_add(result, c); - } - Some(result) -} - #[inline] pub fn find_interval(sz: T, pred: P) -> T where @@ -196,13 +137,6 @@ pub fn gamma(n: i32) -> Float { n as Float * MACHINE_EPSILON / (1. - n as Float * MACHINE_EPSILON) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RenderingCoordinateSystem { - Camera, - CameraWorld, - World, -} - // Define the static counters. These are thread-safe. pub static RARE_EVENT_TOTAL_CALLS: AtomicU64 = AtomicU64::new(0); pub static RARE_EVENT_CONDITION_MET: AtomicU64 = AtomicU64::new(0); @@ -240,30 +174,3 @@ macro_rules! check_rare { } }; } - -pub struct InternCache { - cache: Mutex>>, -} - -impl InternCache -where - T: Eq + Hash + Clone, -{ - pub fn new() -> Self { - Self { - cache: Mutex::new(HashSet::new()), - } - } - - pub fn lookup(&self, value: T) -> Arc { - let mut lock = self.cache.lock().unwrap(); - - if let Some(existing) = lock.get(&value) { - return existing.clone(); // Returns a cheap Arc copy - } - - let new_item = Arc::new(value); - lock.insert(new_item.clone()); - new_item - } -} diff --git a/src/core/primitive.rs b/shared/src/core/primitive.rs similarity index 97% rename from src/core/primitive.rs rename to shared/src/core/primitive.rs index 9b58594..776d5d3 100644 --- a/src/core/primitive.rs +++ b/shared/src/core/primitive.rs @@ -1,14 +1,14 @@ use crate::core::aggregates::LinearBVHNode; +use crate::core::geometry::{Bounds3f, Ray}; use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; use crate::core::material::Material; use crate::core::medium::{Medium, MediumInterface}; use crate::core::pbrt::Float; use crate::core::texture::{FloatTextureTrait, TextureEvalContext}; -use crate::geometry::{Bounds3f, Ray}; use crate::lights::Light; use crate::shapes::{Shape, ShapeIntersection, ShapeTrait}; use crate::utils::hash::hash_float; -use crate::utils::transform::{AnimatedTransform, Transform}; +use crate::utils::transform::{AnimatedTransform, TransformGeneric}; use enum_dispatch::enum_dispatch; use std::sync::Arc; @@ -89,7 +89,7 @@ pub struct SimplePrimitiv { #[derive(Debug, Clone)] pub struct TransformedPrimitive { primitive: Arc, - render_from_primitive: Transform, + render_from_primitive: TransformGeneric, } impl PrimitiveTrait for TransformedPrimitive { diff --git a/src/core/sampler.rs b/shared/src/core/sampler.rs similarity index 75% rename from src/core/sampler.rs rename to shared/src/core/sampler.rs index 1391578..6b441c7 100644 --- a/src/core/sampler.rs +++ b/shared/src/core/sampler.rs @@ -4,18 +4,19 @@ use enum_dispatch::enum_dispatch; use rand::seq::index::sample; use crate::core::filter::FilterTrait; -use crate::core::pbrt::{ - Float, ONE_MINUS_EPSILON, PI, PI_OVER_2, PI_OVER_4, clamp_t, find_interval, lerp, -}; -use crate::geometry::{Bounds2f, Point2f, Point2i, Vector2f}; +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, compute_radical_inverse_permutations, encode_morton_2, - inverse_radical_inverse, log2_int, owen_scrambled_radical_inverse, permutation_element, + PRIME_TABLE_SIZE, Scrambler, clamp, compute_radical_inverse_permutations, encode_morton_2, + inverse_radical_inverse, lerp, log2_int, owen_scrambled_radical_inverse, permutation_element, 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}; @@ -68,6 +69,21 @@ impl IndependentSampler { 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 { @@ -198,6 +214,37 @@ 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 { @@ -293,6 +340,35 @@ 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 { @@ -407,6 +483,34 @@ 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 { @@ -510,6 +614,34 @@ 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 { @@ -550,12 +682,12 @@ impl SamplerTrait for SobolSampler { sobol_sample(self.sobol_index, 0, NoRandomizer), sobol_sample(self.sobol_index, 1, NoRandomizer), ); - u[0] = clamp_t( + u[0] = clamp( u[0] * self.scale as Float - self.pixel[0] as Float, 0., ONE_MINUS_EPSILON, ) as Float; - u[1] = clamp_t( + u[1] = clamp( u[1] * self.scale as Float - self.pixel[1] as Float, 1., ONE_MINUS_EPSILON, @@ -649,6 +781,39 @@ 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 { @@ -758,3 +923,40 @@ 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/src/core/scattering.rs b/shared/src/core/scattering.rs similarity index 93% rename from src/core/scattering.rs rename to shared/src/core/scattering.rs index db49405..2cc49fc 100644 --- a/src/core/scattering.rs +++ b/shared/src/core/scattering.rs @@ -2,10 +2,9 @@ use crate::core::geometry::{ Normal3f, Point2f, Vector2f, Vector3f, VectorLike, abs_cos_theta, cos_phi, cos2_theta, sin_phi, tan2_theta, }; -use crate::core::pbrt::{Float, PI, clamp_t}; +use crate::core::pbrt::{Float, PI}; use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum}; -use crate::utils::math::safe_sqrt; -use crate::utils::math::{lerp, square}; +use crate::utils::math::{clamp, lerp, safe_sqrt, square}; use crate::utils::sampling::sample_uniform_disk_polar; use num::complex::Complex; @@ -91,10 +90,10 @@ impl TrowbridgeReitzDistribution { pub fn regularize(&mut self) { if self.alpha_x < 0.3 { - self.alpha_x = clamp_t(2. * self.alpha_x, 0.1, 0.3); + self.alpha_x = clamp(2. * self.alpha_x, 0.1, 0.3); } if self.alpha_y < 0.3 { - self.alpha_y = clamp_t(2. * self.alpha_y, 0.1, 0.3); + self.alpha_y = clamp(2. * self.alpha_y, 0.1, 0.3); } } } @@ -130,7 +129,7 @@ pub fn reflect(wo: Vector3f, n: Normal3f) -> Vector3f { } pub fn fr_dielectric(cos_theta_i: Float, eta: Float) -> Float { - let mut cos_safe = clamp_t(cos_theta_i, -1., 1.); + let mut cos_safe = clamp(cos_theta_i, -1., 1.); let mut eta_corr = eta; if cos_theta_i < 0. { eta_corr = 1. / eta_corr; @@ -149,7 +148,7 @@ pub fn fr_dielectric(cos_theta_i: Float, eta: Float) -> Float { } pub fn fr_complex(cos_theta_i: Float, eta: Complex) -> Float { - let cos_corr = clamp_t(cos_theta_i, 0., 1.); + let cos_corr = clamp(cos_theta_i, 0., 1.); let sin2_theta_i = 1. - square(cos_corr); let sin2_theta_t: Complex = sin2_theta_i / square(eta); let cos2_theta_t: Complex = (1. - sin2_theta_t).sqrt(); diff --git a/src/core/texture.rs b/shared/src/core/texture.rs similarity index 66% rename from src/core/texture.rs rename to shared/src/core/texture.rs index 59e24a1..c551236 100644 --- a/src/core/texture.rs +++ b/shared/src/core/texture.rs @@ -1,18 +1,22 @@ +use crate::core::geometry::{ + Normal3f, Point2f, Point3f, Vector2f, Vector3f, VectorLike, spherical_phi, spherical_theta, +}; use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; use crate::core::pbrt::Float; use crate::core::pbrt::{INV_2_PI, INV_PI, PI}; -use crate::geometry::{Normal3f, Point2f, Point3f, Vector2f, Vector3f, VectorLike}; -use crate::geometry::{spherical_phi, spherical_theta}; use crate::image::WrapMode; +use crate::spectra::color::ColorEncoding; use crate::spectra::{ - RGBAlbedoSpectrum, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, Spectrum, + RGB, RGBAlbedoSpectrum, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, + SampledWavelengths, Spectrum, SpectrumProvider, }; -use crate::spectra::{SampledWavelengths, SpectrumTrait}; -use crate::utils::color::{ColorEncoding, RGB}; +use crate::utils::Transform; +use crate::utils::error::FileLoc; use crate::utils::math::square; use crate::utils::mipmap::MIPMap; use crate::utils::mipmap::{MIPMapFilterOptions, MIPMapSample}; -use crate::utils::transform::Transform; +use crate::utils::parameters::TextureParameterDictionary; +use crate::utils::transform::TransformGeneric; use std::collections::HashMap; use std::sync::{Arc, Mutex, OnceLock}; @@ -42,6 +46,46 @@ pub enum TextureMapping2D { Planar(PlanarMapping), } +impl TextureMapping2D { + pub fn create( + params: &TextureParameterDictionary, + render_from_texture: &Transform, + loc: &FileLoc, + ) -> Self { + let mtype = params.get_one_string("mapping", "uv"); + match mtype.as_str() { + "uv" => { + let su = params.get_one_float("uscale", 1.); + let sv = params.get_one_float("vscale", 1.); + let du = params.get_one_float("udelta", 0.); + let dv = params.get_one_float("vdelta", 0.); + let mapping = UVMapping::new(su, sv, du, dv); + TextureMapping2D::UV(mapping) + } + "spherical" => { + let mapping = SphericalMapping::new(&render_from_texture.inverse()); + TextureMapping2D::Spherical(mapping) + } + "cylindrical" => { + let mapping = CylindricalMapping::new(&render_from_texture.inverse()); + TextureMapping2D::Cylindrical(mapping) + } + "planar" => { + let vs = params.get_one_vector3f("v1", Vector3f::new(1., 0., 0.)); + let vt = params.get_one_vector3f("v2", Vector3f::new(0., 1., 0.)); + let ds = params.get_one_float("udelta", 0.); + let dt = params.get_one_float("vdelta", 0.); + let mapping = PlanarMapping::new(&render_from_texture.inverse(), vs, vt, ds, dt); + TextureMapping2D::Planar(mapping) + } + _ => { + log::error!("{}: 2D texture mapping unknown {}", loc, mtype); + TextureMapping2D::UV(UVMapping::default()) + } + } + } +} + #[derive(Clone, Debug)] pub struct UVMapping { su: Float, @@ -50,6 +94,12 @@ pub struct UVMapping { dv: Float, } +impl UVMapping { + pub fn new(su: Float, sv: Float, du: Float, dv: Float) -> Self { + Self { su, sv, du, dv } + } +} + impl Default for UVMapping { fn default() -> Self { Self { @@ -80,11 +130,11 @@ impl TextureMapping2DTrait for UVMapping { #[derive(Clone, Debug)] pub struct SphericalMapping { - texture_from_render: Transform, + texture_from_render: TransformGeneric, } impl SphericalMapping { - pub fn new(texture_from_render: &Transform) -> Self { + pub fn new(texture_from_render: &TransformGeneric) -> Self { Self { texture_from_render: *texture_from_render, } @@ -123,11 +173,11 @@ impl TextureMapping2DTrait for SphericalMapping { #[derive(Clone, Debug)] pub struct CylindricalMapping { - texture_from_render: Transform, + texture_from_render: TransformGeneric, } impl CylindricalMapping { - pub fn new(texture_from_render: &Transform) -> Self { + pub fn new(texture_from_render: &TransformGeneric) -> Self { Self { texture_from_render: *texture_from_render, } @@ -159,7 +209,7 @@ impl TextureMapping2DTrait for CylindricalMapping { #[derive(Clone, Debug)] pub struct PlanarMapping { - texture_from_render: Transform, + texture_from_render: TransformGeneric, vs: Vector3f, vt: Vector3f, ds: Float, @@ -168,7 +218,7 @@ pub struct PlanarMapping { impl PlanarMapping { pub fn new( - texture_from_render: &Transform, + texture_from_render: &TransformGeneric, vs: Vector3f, vt: Vector3f, ds: Float, @@ -222,11 +272,11 @@ pub enum TextureMapping3D { #[derive(Clone, Debug)] pub struct PointTransformMapping { - texture_from_render: Transform, + texture_from_render: TransformGeneric, } impl PointTransformMapping { - pub fn new(texture_from_render: &Transform) -> Self { + pub fn new(texture_from_render: &TransformGeneric) -> Self { Self { texture_from_render: *texture_from_render, } @@ -332,6 +382,7 @@ pub trait FloatTextureTrait: Send + Sync + std::fmt::Debug { pub enum FloatTexture { FloatConstant(FloatConstantTexture), GPUFloatImage(GPUFloatImageTexture), + FloatImage(FloatImageTexture), FloatMix(FloatMixTexture), FloatDirectionMix(FloatDirectionMixTexture), FloatScaled(FloatScaledTexture), @@ -345,6 +396,75 @@ pub enum FloatTexture { Wrinkled(WrinkledTexture), } +impl FloatTexture { + pub fn create( + name: &str, + render_from_texture: &Transform, + params: &TextureParameterDictionary, + loc: &FileLoc, + gpu: bool, + ) -> Result { + match name { + "constant" => { + let tex = FloatConstantTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatConstant(tex)) + } + "scale" => Ok(FloatScaledTexture::create(render_from_texture, params, loc)), + "mix" => { + let tex = FloatMixTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatMix(tex)) + } + "directionmix" => { + let tex = FloatDirectionMixTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatDirectionMix(tex)) + } + "bilerp" => { + let tex = FloatBilerpTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatBilerp(tex)) + } + "imagemap" => { + if gpu { + let tex = GPUFloatImageTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::GPUFloatImage(tex)) + } else { + let tex = FloatImageTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatImage(tex)) + } + } + "checkerboard" => { + let tex = FloatCheckerboardTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatCheckerboard(tex)) + } + "dots" => { + let tex = FloatDotsTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatDots(tex)) + } + "fbm" => { + let tex = FBmTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FBm(tex)) + } + "wrinkled" => { + let tex = WrinkledTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::Wrinkled(tex)) + } + "windy" => { + let tex = WindyTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::Windy(tex)) + } + "ptex" => { + if gpu { + let tex = GPUFloatPtexTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::GPUFLoatPtex(tex)) + } else { + let tex = FloatPtexTexture::create(render_from_texture, params, loc); + Ok(FloatTexture::FloatPtex(tex)) + }; + } + _ => Err(format!("Float texture type '{}' unknown at {}", name, loc)), + } + } +} + #[derive(Debug, Clone)] pub struct FloatConstantTexture { value: Float, @@ -354,6 +474,14 @@ impl FloatConstantTexture { pub fn new(value: Float) -> Self { Self { value } } + + pub fn create( + _render_from_texture: &Transform, + params: &TextureParameterDictionary, + _loc: &FileLoc, + ) -> Self { + Self::new(params.get_one_float("value", 1.0)) + } } impl FloatTextureTrait for FloatConstantTexture { @@ -363,24 +491,72 @@ impl FloatTextureTrait for FloatConstantTexture { } #[derive(Debug, Clone)] -pub struct GPUFloatImageTexture; +pub struct GPUFloatImageTexture { + mapping: TextureMapping2D, + filename: String, + scale: Float, + invert: bool, +} + impl FloatTextureTrait for GPUFloatImageTexture {} +#[derive(Debug, Clone)] +pub struct FloatImageTexture { + base: ImageTextureBase, +} + +impl FloatImageTexture { + pub fn new( + mapping: TextureMapping2D, + filename: String, + filter_options: MIPMapFilterOptions, + wrap_mode: WrapMode, + scale: Float, + invert: bool, + encoding: ColorEncoding, + ) -> Self { + Self { + base: ImageTextureBase::new( + mapping, + filename, + filter_options, + wrap_mode, + scale, + invert, + encoding, + ), + } + } +} + +impl FloatTextureTrait for FloatImageTexture {} + #[derive(Debug, Clone)] pub struct FloatMixTexture { - tex1: Box, - tex2: Box, - amount: Box, + tex1: Arc, + tex2: Arc, + amount: Arc, } impl FloatMixTexture { pub fn new( - tex1: Box, - tex2: Box, - amount: Box, + tex1: Arc, + tex2: Arc, + amount: Arc, ) -> Self { Self { tex1, tex2, amount } } + + pub fn create( + _render_from_texture: &Transform, + params: &TextureParameterDictionary, + _loc: &FileLoc, + ) -> Self { + let tex1 = params.get_float_texture("tex1", 0.); + let tex2 = params.get_float_texture("tex2", 1.); + let amount = params.get_float_texture("amount", 0.5); + Self::new(tex1, tex2, amount) + } } impl FloatTextureTrait for FloatMixTexture { @@ -399,13 +575,68 @@ impl FloatTextureTrait for FloatMixTexture { } #[derive(Debug, Clone)] -pub struct FloatDirectionMixTexture; +pub struct FloatDirectionMixTexture { + tex1: Arc, + tex2: Arc, + dir: Vector3f, +} + +impl FloatDirectionMixTexture { + pub fn new(tex1: Arc, tex2: Arc, dir: Vector3f) -> Self { + Self { tex1, tex2, dir } + } + + pub fn create( + render_from_texture: &Transform, + params: &TextureParameterDictionary, + _loc: &FileLoc, + ) -> Self { + let dir_raw = params.get_one_vector3f("dir", Vector3f::new(0., 1., 0.)); + let dir = render_from_texture.apply_to_vector(dir_raw).normalize(); + let tex1 = params.get_float_texture("tex1", 0.); + let tex2 = params.get_float_texture("tex2", 1.); + Self::new(tex1, tex2, dir) + } +} + impl FloatTextureTrait for FloatDirectionMixTexture {} #[derive(Debug, Clone)] pub struct FloatScaledTexture { - tex: Box, - scale: Box, + tex: Arc, + scale: Arc, +} + +impl FloatScaledTexture { + pub fn new(tex: Arc, scale: Arc) -> Self { + Self { tex, scale } + } + + pub fn create( + _render_from_texture: &Transform, + params: &TextureParameterDictionary, + _loc: &FileLoc, + ) -> FloatTexture { + let mut tex = params.get_float_texture("tex", 1.); + let mut scale = params.get_float_texture("scale", 1.); + + for _ in 0..2 { + if let FloatTexture::FloatConstant(c_tex) = &*scale { + let cs = c_tex.value; + if cs == 1.0 { + return (*tex).clone(); + } else if let FloatTexture::FloatImage(img_tex) = &*tex { + let mut image_copy = img_tex.clone(); + image_copy.base.multiply_scale(cs); + return FloatTexture::FloatImage(image_copy).into(); + } + } + + std::mem::swap(&mut tex, &mut scale); + } + std::mem::swap(&mut tex, &mut scale); + FloatTexture::FloatScaled(FloatScaledTexture::new(tex, scale)) + } } impl FloatTextureTrait for FloatScaledTexture { @@ -419,7 +650,39 @@ impl FloatTextureTrait for FloatScaledTexture { } #[derive(Debug, Clone)] -pub struct FloatBilerpTexture; +pub struct FloatBilerpTexture { + mapping: TextureMapping2D, + v00: Float, + v01: Float, + v10: Float, + v11: Float, +} + +impl FloatBilerpTexture { + pub fn new(mapping: TextureMapping2D, v00: Float, v01: Float, v10: Float, v11: Float) -> Self { + Self { + mapping, + v00, + v01, + v10, + v11, + } + } + + pub fn create( + render_from_texture: &Transform, + params: &TextureParameterDictionary, + loc: &FileLoc, + ) -> Self { + 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 FloatTextureTrait for FloatBilerpTexture {} #[derive(Debug, Clone)] @@ -496,6 +759,13 @@ impl SpectrumTextureTrait for RGBReflectanceConstantTexture { pub struct SpectrumConstantTexture { value: Spectrum, } + +impl SpectrumConstantTexture { + pub fn new(value: Spectrum) -> Self { + Self { value } + } +} + impl SpectrumTextureTrait for SpectrumConstantTexture { fn evaluate(&self, _ctx: &TextureEvalContext, lambda: &SampledWavelengths) -> SampledSpectrum { self.value.sample(lambda) @@ -518,7 +788,7 @@ impl SpectrumTextureTrait for SpectrumCheckerboardTexture { } } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum SpectrumType { Illuminant, Albedo, diff --git a/shared/src/data.rs b/shared/src/data.rs new file mode 100644 index 0000000..8d8f0cf --- /dev/null +++ b/shared/src/data.rs @@ -0,0 +1,56 @@ +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"); + +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()) + } + }); diff --git a/src/image/metadata.rs b/shared/src/image/metadata.rs similarity index 96% rename from src/image/metadata.rs rename to shared/src/image/metadata.rs index 07a005b..6dcd4ee 100644 --- a/src/image/metadata.rs +++ b/shared/src/image/metadata.rs @@ -1,6 +1,6 @@ +use crate::core::geometry::{Bounds2i, Point2i}; use crate::core::pbrt::Float; -use crate::geometry::{Bounds2i, Point2i}; -use crate::utils::colorspace::RGBColorSpace; +use crate::spectra::colorspace::RGBColorSpace; use crate::utils::math::SquareMatrix; use smallvec::SmallVec; use std::collections::HashMap; diff --git a/src/image/mod.rs b/shared/src/image/mod.rs similarity index 98% rename from src/image/mod.rs rename to shared/src/image/mod.rs index bafa6ad..079a1fd 100644 --- a/src/image/mod.rs +++ b/shared/src/image/mod.rs @@ -1,13 +1,12 @@ -pub mod io; pub mod metadata; pub mod ops; pub mod pixel; -use crate::core::pbrt::{Float, lerp}; -use crate::geometry::{Bounds2f, Point2f, Point2fi, Point2i}; -use crate::utils::color::{ColorEncoding, ColorEncodingTrait, LINEAR}; +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::square; +use crate::utils::math::{lerp, square}; use core::hash; use half::f16; use pixel::PixelStorage; diff --git a/src/image/ops.rs b/shared/src/image/ops.rs similarity index 99% rename from src/image/ops.rs rename to shared/src/image/ops.rs index cff0793..ce231ef 100644 --- a/src/image/ops.rs +++ b/shared/src/image/ops.rs @@ -1,6 +1,6 @@ // use rayon::prelude::*; +use crate::core::geometry::{Bounds2i, Point2i}; use crate::core::pbrt::Float; -use crate::geometry::{Bounds2i, Point2i}; use crate::image::pixel::PixelStorage; use crate::image::{Image, PixelData, PixelFormat, WrapMode, WrapMode2D}; use crate::utils::math::windowed_sinc; @@ -242,7 +242,7 @@ fn copy_rect_in_kernel( dst: &mut [T], res: Point2i, channels: usize, - enc: crate::utils::color::ColorEncoding, + enc: crate::spectra::color::ColorEncoding, extent: Bounds2i, buf: &[Float], ) { diff --git a/src/image/pixel.rs b/shared/src/image/pixel.rs similarity index 89% rename from src/image/pixel.rs rename to shared/src/image/pixel.rs index ab215b0..2f61d8b 100644 --- a/src/image/pixel.rs +++ b/shared/src/image/pixel.rs @@ -1,8 +1,8 @@ use crate::core::pbrt::Float; -use crate::utils::color::{ColorEncoding, ColorEncodingTrait}; +use crate::spectra::color::{ColorEncoding, ColorEncodingTrait}; use half::f16; -/// Allows writing generic algorithms that work on any image format. +// Allows writing generic algorithms that work on any image format. pub trait PixelStorage: Copy + Send + Sync + 'static + PartialEq { fn from_linear(val: Float, encoding: ColorEncoding) -> Self; fn to_linear(self, encoding: ColorEncoding) -> Float; diff --git a/src/integrators/mod.rs b/shared/src/integrators/mod.rs similarity index 98% rename from src/integrators/mod.rs rename to shared/src/integrators/mod.rs index a5b64d2..e1050f0 100644 --- a/src/integrators/mod.rs +++ b/shared/src/integrators/mod.rs @@ -6,6 +6,7 @@ use crate::camera::{Camera, CameraTrait}; use crate::core::bssrdf::{BSSRDFTrait, SubsurfaceInteraction}; use crate::core::bxdf::{BSDF, BxDFFlags, BxDFReflTransFlags, BxDFTrait, FArgs, TransportMode}; use crate::core::film::{FilmTrait, VisibleSurface}; +use crate::core::geometry::{Bounds2i, Point2f, Point2i, Point3fi, Ray, Vector3f, VectorLike}; use crate::core::interaction::{ self, Interaction, InteractionTrait, MediumInteraction, SimpleInteraction, SurfaceInteraction, }; @@ -14,7 +15,6 @@ 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::geometry::{Bounds2i, Point2f, Point2i, Point3fi, Ray, Vector3f, VectorLike}; use crate::lights::sampler::{LightSampler, UniformLightSampler}; use crate::lights::{Light, LightSampleContext, LightTrait, sampler::LightSamplerTrait}; use crate::shapes::ShapeIntersection; @@ -228,7 +228,7 @@ impl RayIntegratorTrait for SimplePathIntegrator { mut ray: Ray, lambda: &SampledWavelengths, sampler: &mut Sampler, - scratch: &Bump, + _scratch: &Bump, _visible_surface: bool, ) -> (SampledSpectrum, Option) { let mut l = SampledSpectrum::new(0.0); @@ -255,8 +255,7 @@ impl RayIntegratorTrait for SimplePathIntegrator { } depth += 1; - let Some(bsdf) = isect.get_bsdf(&ray, lambda, self.camera.as_ref(), scratch, sampler) - else { + let Some(bsdf) = isect.get_bsdf(&ray, lambda, self.camera.as_ref(), sampler) else { specular_bounce = false; isect.skip_intersection(&mut ray, t_hit); continue; @@ -367,7 +366,7 @@ impl RayIntegratorTrait for PathIntegrator { mut ray: Ray, lambda: &SampledWavelengths, sampler: &mut Sampler, - scratch: &Bump, + _scratch: &Bump, visible_surface: bool, ) -> (SampledSpectrum, Option) { let mut l = SampledSpectrum::new(0.0); @@ -410,9 +409,7 @@ impl RayIntegratorTrait for PathIntegrator { l += beta * wl * le; } - let Some(mut bsdf) = - isect.get_bsdf(&ray, lambda, self.camera.as_ref(), scratch, sampler) - else { + let Some(mut bsdf) = isect.get_bsdf(&ray, lambda, self.camera.as_ref(), sampler) else { specular_bounce = true; isect.skip_intersection(&mut ray, t_hit); continue; @@ -544,7 +541,7 @@ impl RayIntegratorTrait for SimpleVolPathIntegrator { mut ray: Ray, lambda: &SampledWavelengths, sampler: &mut Sampler, - scratch: &Bump, + _scratch: &Bump, _visible_surface: bool, ) -> (SampledSpectrum, Option) { let mut l = SampledSpectrum::new(0.); @@ -632,10 +629,7 @@ impl RayIntegratorTrait for SimpleVolPathIntegrator { }; // Handle surface intersection along ray path - if let Some(bsdf) = &hit - .intr - .get_bsdf(&ray, &lambda, &self.camera, scratch, sampler) - { + if let Some(bsdf) = &hit.intr.get_bsdf(&ray, &lambda, &self.camera, sampler) { let uc = sampler.get1d(); let u = sampler.get2d(); if bsdf.sample_f(-ray.d, uc, u, FArgs::default()).is_some() { @@ -993,8 +987,7 @@ impl RayIntegratorTrait for VolPathIntegrator { } let isect = &mut hit.intr; - let Some(mut bsdf) = isect.get_bsdf(&ray, lambda, &self.camera, scratch, sampler) - else { + let Some(mut bsdf) = isect.get_bsdf(&ray, lambda, &self.camera, sampler) else { hit.intr.skip_intersection(&mut ray, hit.t_hit()); continue; }; diff --git a/src/integrators/pipeline.rs b/shared/src/integrators/pipeline.rs similarity index 99% rename from src/integrators/pipeline.rs rename to shared/src/integrators/pipeline.rs index 61fd54b..174ceaf 100644 --- a/src/integrators/pipeline.rs +++ b/shared/src/integrators/pipeline.rs @@ -228,7 +228,7 @@ pub fn evaluate_pixel_sample( lu = 0.5; } let lambda = camera.get_film().sample_wavelengths(lu); - let mut film = camera.get_film(); + let film = camera.get_film(); let filter = film.get_filter(); let camera_sample = get_camera_sample(sampler, pixel, filter); if let Some(mut camera_ray) = camera.generate_ray_differential(camera_sample, &lambda) { diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 0000000..a603712 --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,15 @@ +#![allow(unused_imports, dead_code)] +#![feature(float_erf)] +#![feature(f16)] + +mod camera; +mod core; +mod data; +mod image; +mod integrators; +mod lights; +mod shapes; +mod spectra; +mod utils; + +pub use core::pbrt::*; diff --git a/shared/src/lights/diffuse.rs b/shared/src/lights/diffuse.rs new file mode 100644 index 0000000..45ac886 --- /dev/null +++ b/shared/src/lights/diffuse.rs @@ -0,0 +1,26 @@ +use crate::core::geometry::*; +use crate::core::pbrt::Float; +use crate::core::spectra::*; +use crate::shapes::GpuShapeHandle; +use crate::textures::GpuFloatTextureHandle; + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct DiffuseAreaLight { + pub lemit_coeffs: [Float; 32], + pub scale: Float, + pub area: Float, + pub two_sided: bool, + pub image_id: i32, + pub shape_id: u32, +} + +impl LightTrait for DiffuseAreaLight { + fn l(&self, n: Normal3f, wo: Vector3f, lambda: &SampledWavelengths) -> SampledSpectrum { + if !self.two_sided && n.dot(wo) <= 0.0 { + return SampledSpectrum::new(0.0); + } + let spec = DenselySampledSpectrum::from_array(&self.lemit_coeffs); + spec.sample(lambda) * self.scale + } +} diff --git a/src/lights/infinite.rs b/shared/src/lights/infinite.rs similarity index 96% rename from src/lights/infinite.rs rename to shared/src/lights/infinite.rs index c3299ec..f7d7fbf 100644 --- a/src/lights/infinite.rs +++ b/shared/src/lights/infinite.rs @@ -1,11 +1,9 @@ use crate::{ + core::geometry::Frame, core::medium::Medium, - geometry::Frame, - spectra::RGBIlluminantSpectrum, + spectra::{RGB, RGBColorSpace, RGBIlluminantSpectrum}, utils::{ - color::RGB, - colorspace::RGBColorSpace, - math::equal_area_square_to_sphere, + math::{clamp, equal_area_square_to_sphere}, sampling::{ AliasTable, PiecewiseConstant2D, WindowedPiecewiseConstant2D, sample_uniform_sphere, uniform_sphere_pdf, @@ -15,7 +13,6 @@ use crate::{ use rayon::prelude::*; -use crate::core::pbrt::clamp_t; use crate::image::{PixelFormat, WrapMode}; use std::path::Path; @@ -24,8 +21,8 @@ use super::{ Arc, Bounds2f, Bounds3f, DenselySampledSpectrum, Float, Image, Interaction, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, MediumInterface, Normal3f, PI, Point2f, Point2i, Point3f, Ray, SampledSpectrum, SampledWavelengths, - SimpleInteraction, Spectrum, Transform, Vector3f, VectorLike, equal_area_sphere_to_square, - square, + SimpleInteraction, Spectrum, TransformGeneric, Vector3f, VectorLike, + equal_area_sphere_to_square, square, }; #[derive(Debug, Clone)] @@ -38,7 +35,7 @@ pub struct InfiniteUniformLight { } impl InfiniteUniformLight { - pub fn new(render_from_light: Transform, le: Spectrum, scale: Float) -> Self { + pub fn new(render_from_light: TransformGeneric, le: Spectrum, scale: Float) -> Self { let base = LightBase::new( LightType::Infinite, &render_from_light, @@ -137,7 +134,7 @@ pub struct InfiniteImageLight { impl InfiniteImageLight { pub fn new( - render_from_light: Transform, + render_from_light: TransformGeneric, image: Image, image_color_space: Arc, scale: Float, @@ -333,7 +330,7 @@ impl InfinitePortalLight { &self.base } pub fn new( - render_from_light: Transform, + render_from_light: TransformGeneric, equal_area_image: &Image, image_color_space: Arc, scale: Float, @@ -485,8 +482,8 @@ impl InfinitePortalLight { Some(( Point2f::new( - clamp_t((alpha + PI / 2.0) / PI, 0.0, 1.0), - clamp_t((beta + PI / 2.0) / PI, 0.0, 1.0), + clamp((alpha + PI / 2.0) / PI, 0.0, 1.0), + clamp((beta + PI / 2.0) / PI, 0.0, 1.0), ), duv_dw, )) diff --git a/src/lights/mod.rs b/shared/src/lights/mod.rs similarity index 96% rename from src/lights/mod.rs rename to shared/src/lights/mod.rs index 3e7a5c4..9f8a3d6 100644 --- a/src/lights/mod.rs +++ b/shared/src/lights/mod.rs @@ -2,25 +2,24 @@ pub mod diffuse; pub mod infinite; pub mod sampler; +use crate::core::geometry::{ + Bounds2f, Bounds3f, DirectionCone, Normal3f, Point2f, Point2i, Point3f, Point3fi, Ray, + Vector3f, VectorLike, cos_theta, +}; use crate::core::interaction::{ Interaction, InteractionTrait, MediumInteraction, SimpleInteraction, SurfaceInteraction, }; use crate::core::medium::MediumInterface; -use crate::core::pbrt::{Float, InternCache, PI}; -use crate::geometry::{ - Bounds2f, Bounds3f, DirectionCone, Normal3f, Point2f, Point2i, Point3f, Point3fi, Ray, - Vector3f, VectorLike, cos_theta, -}; +use crate::core::pbrt::{Float, PI}; use crate::image::Image; use crate::spectra::{ - DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, RGBIlluminantSpectrum, SampledSpectrum, - SampledWavelengths, Spectrum, SpectrumTrait, + DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, RGB, RGBColorSpace, RGBIlluminantSpectrum, + SampledSpectrum, SampledWavelengths, Spectrum, SpectrumProvider, }; -use crate::utils::color::RGB; -use crate::utils::colorspace::RGBColorSpace; +use crate::utils::containers::InternCache; use crate::utils::math::{equal_area_sphere_to_square, radians, safe_sqrt, smooth_step, square}; use crate::utils::sampling::PiecewiseConstant2D; -use crate::utils::transform::Transform; +use crate::utils::transform::TransformGeneric; use bitflags::bitflags; use enum_dispatch::enum_dispatch; @@ -29,6 +28,8 @@ use std::sync::{Arc, OnceLock}; use diffuse::DiffuseAreaLight; use infinite::{InfiniteImageLight, InfinitePortalLight, InfiniteUniformLight}; +pub use sampler::{LightSampler, LightSamplerTrait}; + static SPECTRUM_CACHE: OnceLock> = OnceLock::new(); fn get_spectrum_cache() -> &'static InternCache { @@ -64,7 +65,7 @@ pub struct LightLeSample { pdf_dir: Float, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Copy, Default, Clone)] pub struct LightSampleContext { pub pi: Point3fi, pub n: Normal3f, @@ -147,14 +148,14 @@ impl LightLiSample { #[derive(Debug, Clone)] pub struct LightBase { pub light_type: LightType, - pub render_from_light: Transform, + pub render_from_light: TransformGeneric, pub medium_interface: MediumInterface, } impl LightBase { pub fn new( light_type: LightType, - render_from_light: &Transform, + render_from_light: &TransformGeneric, medium_interface: &MediumInterface, ) -> Self { Self { @@ -185,7 +186,7 @@ impl LightBase { pub fn lookup_spectrum(s: &Spectrum) -> Arc { let cache = SPECTRUM_CACHE.get_or_init(InternCache::new); - let dense_spectrum = DenselySampledSpectrum::from_spectrum(s, LAMBDA_MIN, LAMBDA_MAX); + let dense_spectrum = DenselySampledSpectrum::from_spectrum(s); cache.lookup(dense_spectrum) } } @@ -354,7 +355,7 @@ pub struct DistantLight { } impl DistantLight { - pub fn new(render_from_light: &Transform, lemit: Spectrum, scale: Float) -> Self { + pub fn new(render_from_light: &TransformGeneric, lemit: Spectrum, scale: Float) -> Self { let base = LightBase::new( LightType::DeltaDirection, render_from_light, @@ -450,7 +451,7 @@ pub struct GoniometricLight { impl GoniometricLight { pub fn new( - render_from_light: &Transform, + render_from_light: &TransformGeneric, medium_interface: &MediumInterface, iemit: Spectrum, scale: Float, @@ -545,7 +546,7 @@ pub struct PointLight { impl PointLight { pub fn new( - render_from_light: Transform, + render_from_light: TransformGeneric, medium_interface: MediumInterface, i: &Spectrum, scale: Float, @@ -643,15 +644,15 @@ pub struct ProjectionLight { scale: Float, screen_bounds: Bounds2f, hither: Float, - screen_from_light: Transform, - light_from_screen: Transform, + screen_from_light: TransformGeneric, + light_from_screen: TransformGeneric, a: Float, distrib: PiecewiseConstant2D, } impl ProjectionLight { pub fn new( - render_from_light: Transform, + render_from_light: TransformGeneric, medium_interface: MediumInterface, image: Image, image_color_space: Arc, @@ -674,7 +675,7 @@ impl ProjectionLight { }; let hither = 1e-3; - let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap(); + 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 }; @@ -803,7 +804,7 @@ pub struct SpotLight { impl SpotLight { pub fn new( - render_from_light: &Transform, + render_from_light: &TransformGeneric, medium_interface: &MediumInterface, iemit: Spectrum, scale: Float, diff --git a/src/lights/sampler.rs b/shared/src/lights/sampler.rs similarity index 98% rename from src/lights/sampler.rs rename to shared/src/lights/sampler.rs index 1eefc1e..03c3023 100644 --- a/src/lights/sampler.rs +++ b/shared/src/lights/sampler.rs @@ -2,13 +2,13 @@ use super::{ Bounds3f, Float, Light, LightBounds, LightSampleContext, LightTrait, Normal3f, PI, Point3f, SampledSpectrum, SampledWavelengths, Vector3f, VectorLike, safe_sqrt, square, }; -use crate::geometry::primitives::OctahedralVector; -use crate::geometry::{DirectionCone, Normal}; -use crate::utils::math::sample_discrete; +use crate::core::geometry::primitives::OctahedralVector; +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::pbrt::{ONE_MINUS_EPSILON, clamp_t, lerp}; +use crate::core::pbrt::ONE_MINUS_EPSILON; use crate::utils::sampling::AliasTable; use enum_dispatch::enum_dispatch; @@ -149,7 +149,7 @@ impl CompactLightBounds { if min == max { return 0.0; } - 65535.0 * clamp_t((c - min) / (max - min), 0.0, 1.0) + 65535.0 * clamp((c - min) / (max - min), 0.0, 1.0) } } diff --git a/src/shapes/bilinear.rs b/shared/src/shapes/bilinear.rs similarity index 97% rename from src/shapes/bilinear.rs rename to shared/src/shapes/bilinear.rs index 3d18b95..50bee67 100644 --- a/src/shapes/bilinear.rs +++ b/shared/src/shapes/bilinear.rs @@ -1,17 +1,16 @@ use super::{ BilinearIntersection, BilinearPatchShape, Bounds3f, DirectionCone, Interaction, Normal3f, Point2f, Point3f, Point3fi, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, - ShapeTrait, SurfaceInteraction, Vector3f, + ShapeTrait, SurfaceInteraction, Transform, Vector3f, }; +use crate::core::geometry::{Tuple, VectorLike, spherical_quad_area}; use crate::core::interaction::InteractionTrait; -use crate::core::pbrt::{Float, clamp_t, gamma, lerp}; -use crate::geometry::{Tuple, VectorLike, spherical_quad_area}; -use crate::utils::math::{SquareMatrix, difference_of_products, quadratic}; +use crate::core::pbrt::{Float, gamma}; +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 crate::utils::transform::Transform; use std::sync::Arc; use std::sync::OnceLock; @@ -228,7 +227,7 @@ impl BilinearPatchShape { time: Float, wo: Vector3f, ) -> SurfaceInteraction { - // Base geometry and derivatives + // Base geom and derivatives let p = lerp( uv[0], lerp(uv[1], data.p00, data.p01), @@ -379,7 +378,7 @@ impl BilinearPatchShape { } let r = Transform::rotate_from_to(isect.n().normalize().into(), ns.into()); - isect.set_shading_geometry( + isect.set_shading_geom( ns, r.apply_to_vector(isect.dpdu), r.apply_to_vector(isect.dpdv), @@ -505,10 +504,12 @@ impl BilinearPatchShape { } impl ShapeTrait for BilinearPatchShape { + #[inline] fn area(&self) -> Float { self.area } + #[inline] fn normal_bounds(&self) -> DirectionCone { let data = self.get_data(); if data.p00 == data.p10 @@ -553,14 +554,16 @@ impl ShapeTrait for BilinearPatchShape { .min(n_avg.dot(n10)) .min(n_avg.dot(n01)) .min(n_avg.dot(n11)); - DirectionCone::new(n_avg.into(), clamp_t(cos_theta, -1., 1.)) + DirectionCone::new(n_avg.into(), clamp(cos_theta, -1., 1.)) } + #[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)) } + #[inline] fn intersect(&self, ray: &Ray, t_max: Option) -> Option { let t_max_val = t_max?; let data = self.get_data(); @@ -576,6 +579,7 @@ 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(); @@ -583,6 +587,7 @@ impl ShapeTrait for BilinearPatchShape { .is_some() } + #[inline] fn sample(&self, u: Point2f) -> Option { let data = self.get_data(); // Sample bilinear patch parametric coordinate (u, v) @@ -615,6 +620,7 @@ impl ShapeTrait for BilinearPatchShape { }) } + #[inline] fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option { let data = self.get_data(); let v00 = (data.p00 - ctx.p()).normalize(); @@ -632,6 +638,7 @@ impl ShapeTrait for BilinearPatchShape { } } + #[inline] fn pdf(&self, intr: &Interaction) -> Float { let Interaction::Surface(si) = intr else { return 0.0; @@ -663,6 +670,7 @@ impl ShapeTrait for BilinearPatchShape { if cross == 0. { 0. } else { param_pdf / cross } } + #[inline] fn pdf_from_context(&self, ctx: &ShapeSampleContext, wi: Vector3f) -> Float { let ray = ctx.spawn_ray(wi); let Some(isect) = self.intersect(&ray, None) else { diff --git a/src/shapes/curves.rs b/shared/src/shapes/curves.rs similarity index 96% rename from src/shapes/curves.rs rename to shared/src/shapes/curves.rs index 6e6a585..fe5cc33 100644 --- a/src/shapes/curves.rs +++ b/shared/src/shapes/curves.rs @@ -1,6 +1,5 @@ use crate::core::interaction::InteractionTrait; -use crate::core::pbrt::{clamp_t, lerp}; -use crate::utils::math::square; +use crate::utils::math::{clamp, lerp, square}; use crate::utils::splines::{ bound_cubic_bezier, cubic_bezier_control_points, evaluate_cubic_bezier, subdivide_cubic_bezier, }; @@ -15,7 +14,7 @@ use std::sync::Arc; struct IntersectionContext { ray: Ray, - object_from_ray: Arc>, + object_from_ray: Arc, common: CurveCommon, } @@ -41,7 +40,7 @@ impl CurveShape { (dx, _) = ray.d.coordinate_system(); } - let ray_from_object = look_at(ray.o, ray.o + ray.d, dx.into()).expect("Inversion error"); + 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 @@ -71,7 +70,7 @@ impl CurveShape { let max_depth = if l0 > 0. { let eps = self.common.width[0].max(self.common.width[1]) * 0.05; let r0: i32 = (1.41421356237 * 6. * l0 / (8. * eps)).log2() as i32 / 2; - clamp_t(r0, 0, 10) + clamp(r0, 0, 10) } else { 0 }; @@ -152,7 +151,7 @@ impl CurveShape { return None; } let w = Vector2f::new(cp[0].x(), cp[0].y()).dot(-segment_dir) / denom; - let u = clamp_t(lerp(w, u0, u1), u0, u1); + let u = clamp(lerp(w, u0, u1), u0, u1); let ray_length = context.ray.d.norm(); let mut hit_width = lerp(u, self.common.width[0], self.common.width[1]); @@ -169,7 +168,7 @@ impl CurveShape { hit_width = hit_width * n_hit.dot(context.ray.d.into()).abs() / ray_length; } - let (pc, dpcdw) = evaluate_cubic_bezier(cp, clamp_t(w, 0., 1.)); + let (pc, dpcdw) = evaluate_cubic_bezier(cp, clamp(w, 0., 1.)); let ray_length = context.ray.d.norm(); if !self.valid_hit(pc, hit_width, t_max, ray_length) { diff --git a/src/shapes/cylinder.rs b/shared/src/shapes/cylinder.rs similarity index 97% rename from src/shapes/cylinder.rs rename to shared/src/shapes/cylinder.rs index 795fa64..8050db2 100644 --- a/src/shapes/cylinder.rs +++ b/shared/src/shapes/cylinder.rs @@ -3,18 +3,18 @@ use super::{ Point3fi, QuadricIntersection, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, SurfaceInteraction, Transform, Vector3f, Vector3fi, }; +use crate::core::geometry::{Sqrt, Tuple, VectorLike}; use crate::core::interaction::InteractionTrait; -use crate::core::pbrt::{gamma, lerp}; -use crate::geometry::{Sqrt, Tuple, VectorLike}; +use crate::core::pbrt::gamma; use crate::utils::interval::Interval; -use crate::utils::math::{difference_of_products, square}; +use crate::utils::math::{difference_of_products, lerp, square}; use std::mem; use std::sync::Arc; impl CylinderShape { pub fn new( - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, radius: Float, z_min: Float, diff --git a/src/shapes/disk.rs b/shared/src/shapes/disk.rs similarity index 98% rename from src/shapes/disk.rs rename to shared/src/shapes/disk.rs index 8cc51ea..5e0ac80 100644 --- a/src/shapes/disk.rs +++ b/shared/src/shapes/disk.rs @@ -3,8 +3,8 @@ use super::{ Point3fi, QuadricIntersection, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, SurfaceInteraction, Transform, Vector3f, }; +use crate::core::geometry::VectorLike; use crate::core::interaction::InteractionTrait; -use crate::geometry::VectorLike; use crate::utils::math::square; use crate::utils::sampling::sample_uniform_disk_concentric; use std::sync::Arc; @@ -15,8 +15,8 @@ impl DiskShape { inner_radius: Float, height: Float, phi_max: Float, - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, ) -> Self { Self { diff --git a/src/shapes/mod.rs b/shared/src/shapes/mod.rs similarity index 93% rename from src/shapes/mod.rs rename to shared/src/shapes/mod.rs index 0edb614..0034259 100644 --- a/src/shapes/mod.rs +++ b/shared/src/shapes/mod.rs @@ -5,16 +5,16 @@ 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::geometry::{ - Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, - Vector3fi, VectorLike, -}; use crate::lights::Light; use crate::utils::math::{next_float_down, next_float_up}; use crate::utils::transform::Transform; @@ -29,8 +29,8 @@ pub struct SphereShape { theta_z_min: Float, theta_z_max: Float, phi_max: Float, - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, transform_swap_handedness: bool, } @@ -55,8 +55,8 @@ pub struct CylinderShape { z_min: Float, z_max: Float, phi_max: Float, - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, transform_swap_handedness: bool, } @@ -67,8 +67,8 @@ pub struct DiskShape { inner_radius: Float, height: Float, phi_max: Float, - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, transform_swap_handedness: bool, } @@ -102,8 +102,8 @@ pub struct CurveCommon { n: [Normal3f; 2], normal_angle: Float, inv_sin_normal_angle: Float, - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, transform_swap_handedness: bool, } @@ -116,8 +116,8 @@ impl CurveCommon { w1: Float, curve_type: CurveType, norm: &[Vector3f], - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, ) -> Self { let transform_swap_handedness = render_from_object.swaps_handedness(); diff --git a/src/shapes/sphere.rs b/shared/src/shapes/sphere.rs similarity index 95% rename from src/shapes/sphere.rs rename to shared/src/shapes/sphere.rs index d7ca001..03b7689 100644 --- a/src/shapes/sphere.rs +++ b/shared/src/shapes/sphere.rs @@ -3,11 +3,11 @@ use super::{ QuadricIntersection, Ray, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, SphereShape, SurfaceInteraction, Transform, Vector3f, Vector3fi, }; +use crate::core::geometry::{Frame, Sqrt, VectorLike, spherical_direction}; use crate::core::interaction::InteractionTrait; -use crate::core::pbrt::{clamp_t, gamma}; -use crate::geometry::{Frame, Sqrt, VectorLike, spherical_direction}; +use crate::core::pbrt::gamma; use crate::utils::interval::Interval; -use crate::utils::math::{difference_of_products, radians, safe_acos, safe_sqrt, square}; +use crate::utils::math::{clamp, difference_of_products, radians, safe_acos, safe_sqrt, square}; use crate::utils::sampling::sample_uniform_sphere; use std::mem; @@ -15,23 +15,23 @@ use std::sync::Arc; impl SphereShape { pub fn new( - render_from_object: Arc>, - object_from_render: Arc>, + render_from_object: Arc, + object_from_render: Arc, reverse_orientation: bool, radius: Float, z_min: Float, z_max: Float, phi_max: Float, ) -> Self { - let theta_z_min = clamp_t(z_min.min(z_max) / radius, -1., 1.).acos(); - let theta_z_max = clamp_t(z_max.min(z_max) / radius, -1., 1.).acos(); - let phi_max = radians(clamp_t(phi_max, 0., 360.0)); + let theta_z_min = clamp(z_min.min(z_max) / radius, -1., 1.).acos(); + let theta_z_max = clamp(z_max.min(z_max) / radius, -1., 1.).acos(); + let phi_max = radians(clamp(phi_max, 0., 360.0)); Self { render_from_object: render_from_object.clone(), object_from_render: object_from_render.clone(), radius, - z_min: clamp_t(z_min.min(z_max), -radius, radius), - z_max: clamp_t(z_min.max(z_max), -radius, radius), + z_min: clamp(z_min.min(z_max), -radius, radius), + z_max: clamp(z_min.max(z_max), -radius, radius), theta_z_max, theta_z_min, phi_max, diff --git a/src/shapes/triangle.rs b/shared/src/shapes/triangle.rs similarity index 99% rename from src/shapes/triangle.rs rename to shared/src/shapes/triangle.rs index b1751da..71738dd 100644 --- a/src/shapes/triangle.rs +++ b/shared/src/shapes/triangle.rs @@ -3,9 +3,9 @@ use super::{ ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait, SurfaceInteraction, TriangleIntersection, TriangleShape, Vector2f, Vector3f, }; +use crate::core::geometry::{Sqrt, Tuple, VectorLike, spherical_triangle_area}; use crate::core::interaction::InteractionTrait; use crate::core::pbrt::gamma; -use crate::geometry::{Sqrt, Tuple, VectorLike, spherical_triangle_area}; use crate::utils::math::{difference_of_products, square}; use crate::utils::mesh::TriangleMesh; use crate::utils::sampling::{ diff --git a/src/core/cie.rs b/shared/src/spectra/cie.rs similarity index 90% rename from src/core/cie.rs rename to shared/src/spectra/cie.rs index fba085c..7b736a8 100644 --- a/src/core/cie.rs +++ b/shared/src/spectra/cie.rs @@ -1,4 +1,4 @@ -use crate::core::pbrt::Float; +use crate::Float; pub const CIE_SAMPLES: usize = 471; pub const CIE_X: [Float; CIE_SAMPLES] = [ @@ -1484,7 +1484,7 @@ pub const CIE_ILLUM_A: [Float; 214] = [ ]; // CIE Illuminant D S basis functions -const N_CIES: usize = 107; +pub const N_CIES: usize = 107; pub const CIE_S_LAMBDA: [Float; N_CIES] = [ 300.000000, 305.000000, 310.000000, 315.000000, 320.000000, 325.000000, 330.000000, 335.000000, @@ -1687,6 +1687,30 @@ pub const CIE_ILLUM_F2: [Float; 162] = [ 780.000000, 0.210000, ]; +pub const CIE_ILLUM_F3: [Float; 162] = [ + 380.000000, 0.820000, 385.000000, 1.020000, 390.000000, 1.260000, 395.000000, 1.440000, + 400.000000, 2.570000, 405.000000, 14.360000, 410.000000, 2.700000, 415.000000, 2.450000, + 420.000000, 2.730000, 425.000000, 3.000000, 430.000000, 3.280000, 435.000000, 31.850000, + 440.000000, 9.470000, 445.000000, 4.020000, 450.000000, 4.250000, 455.000000, 4.440000, + 460.000000, 4.590000, 465.000000, 4.720000, 470.000000, 4.800000, 475.000000, 4.860000, + 480.000000, 4.870000, 485.000000, 4.850000, 490.000000, 4.880000, 495.000000, 4.770000, + 500.000000, 4.670000, 505.000000, 4.620000, 510.000000, 4.620000, 515.000000, 4.730000, + 520.000000, 4.990000, 525.000000, 5.480000, 530.000000, 6.250000, 535.000000, 7.340000, + 540.000000, 8.780000, 545.000000, 23.820000, 550.000000, 16.139999, 555.000000, 14.590000, + 560.000000, 16.629999, 565.000000, 18.490000, 570.000000, 19.950001, 575.000000, 23.110001, + 580.000000, 24.690001, 585.000000, 21.410000, 590.000000, 20.850000, 595.000000, 19.930000, + 600.000000, 18.670000, 605.000000, 17.219999, 610.000000, 15.650000, 615.000000, 14.040000, + 620.000000, 12.450000, 625.000000, 10.950000, 630.000000, 9.510000, 635.000000, 8.270000, + 640.000000, 7.110000, 645.000000, 6.090000, 650.000000, 5.220000, 655.000000, 4.450000, + 660.000000, 3.800000, 665.000000, 3.230000, 670.000000, 2.750000, 675.000000, 2.330000, + 680.000000, 1.990000, 685.000000, 1.700000, 690.000000, 1.550000, 695.000000, 1.270000, + 700.000000, 1.090000, 705.000000, 0.960000, 710.000000, 0.830000, 715.000000, 0.710000, + 720.000000, 0.620000, 725.000000, 0.540000, 730.000000, 0.490000, 735.000000, 0.460000, + 740.000000, 0.430000, 745.000000, 0.390000, 750.000000, 0.390000, 755.000000, 0.350000, + 760.000000, 0.380000, 765.000000, 0.390000, 770.000000, 0.330000, 775.000000, 0.280000, + 780.000000, 0.210000, +]; + pub const CIE_ILLUM_F4: [Float; 162] = [ 380.000000, 0.570000, 385.000000, 0.700000, 390.000000, 0.870000, 395.000000, 0.980000, 400.000000, 2.010000, 405.000000, 13.750000, 410.000000, 1.950000, 415.000000, 1.590000, @@ -1920,7 +1944,7 @@ pub const AG_ETA: [Float; 112] = [ 799.897949, 0.142563, 826.561157, 0.145000, 855.063293, 0.151938, 885.601257, 0.163000, ]; -const AG_K: [Float; 112] = [ +pub const AG_K: [Float; 112] = [ 298.757050, 1.080000, 302.400421, 0.882000, 306.133759, 0.761063, 309.960449, 0.647000, 313.884003, 0.550875, 317.908142, 0.504000, 322.036835, 0.554375, 326.274139, 0.663000, 330.624481, 0.818563, 335.092377, 0.986000, 339.682678, 1.120687, 344.400482, 1.240000, @@ -2116,7 +2140,7 @@ pub const TIO2_K: [Float; 68] = [ // SPDX-License-Identifier: Apache-2.0 // Data for various types of glass (refractive index eta vs. wavelength in nm). -pub const GLASSBK7_ETA: [f32; 58] = [ +pub const GLASS_BK7_ETA: [f32; 58] = [ 300.0, 1.5527702635739, 322.0, @@ -6184,3 +6208,199 @@ pub const SONY_ILCE_9_B: [f32; 70] = [ 720.0, 0.0005996684000000017, ]; + +pub static N_SWATCHES_REFLECTANCES: usize = 24; +pub static SWATCHES_RAW: &[&[Float]] = &[ + &[ + 380.0, 0.055, 390.0, 0.058, 400.0, 0.061, 410.0, 0.062, 420.0, 0.062, 430.0, 0.062, 440.0, + 0.062, 450.0, 0.062, 460.0, 0.062, 470.0, 0.062, 480.0, 0.062, 490.0, 0.063, 500.0, 0.065, + 510.0, 0.070, 520.0, 0.076, 530.0, 0.079, 540.0, 0.081, 550.0, 0.084, 560.0, 0.091, 570.0, + 0.103, 580.0, 0.119, 590.0, 0.134, 600.0, 0.143, 610.0, 0.147, 620.0, 0.151, 630.0, 0.158, + 640.0, 0.168, 650.0, 0.179, 660.0, 0.188, 670.0, 0.190, 680.0, 0.186, 690.0, 0.181, 700.0, + 0.182, 710.0, 0.187, 720.0, 0.196, 730.0, 0.209, + ], + &[ + 380.0, 0.117, 390.0, 0.143, 400.0, 0.175, 410.0, 0.191, 420.0, 0.196, 430.0, 0.199, 440.0, + 0.204, 450.0, 0.213, 460.0, 0.228, 470.0, 0.251, 480.0, 0.280, 490.0, 0.309, 500.0, 0.329, + 510.0, 0.333, 520.0, 0.315, 530.0, 0.286, 540.0, 0.273, 550.0, 0.276, 560.0, 0.277, 570.0, + 0.289, 580.0, 0.339, 590.0, 0.420, 600.0, 0.488, 610.0, 0.525, 620.0, 0.546, 630.0, 0.562, + 640.0, 0.578, 650.0, 0.595, 660.0, 0.612, 670.0, 0.625, 680.0, 0.638, 690.0, 0.656, 700.0, + 0.678, 710.0, 0.700, 720.0, 0.717, 730.0, 0.734, + ], + &[ + 380.0, 0.130, 390.0, 0.177, 400.0, 0.251, 410.0, 0.306, 420.0, 0.324, 430.0, 0.330, 440.0, + 0.333, 450.0, 0.331, 460.0, 0.323, 470.0, 0.311, 480.0, 0.298, 490.0, 0.285, 500.0, 0.269, + 510.0, 0.250, 520.0, 0.231, 530.0, 0.214, 540.0, 0.199, 550.0, 0.185, 560.0, 0.169, 570.0, + 0.157, 580.0, 0.149, 590.0, 0.145, 600.0, 0.142, 610.0, 0.141, 620.0, 0.141, 630.0, 0.141, + 640.0, 0.143, 650.0, 0.147, 660.0, 0.152, 670.0, 0.154, 680.0, 0.150, 690.0, 0.144, 700.0, + 0.136, 710.0, 0.132, 720.0, 0.135, 730.0, 0.147, + ], + &[ + 380.0, 0.051, 390.0, 0.054, 400.0, 0.056, 410.0, 0.057, 420.0, 0.058, 430.0, 0.059, 440.0, + 0.060, 450.0, 0.061, 460.0, 0.062, 470.0, 0.063, 480.0, 0.065, 490.0, 0.067, 500.0, 0.075, + 510.0, 0.101, 520.0, 0.145, 530.0, 0.178, 540.0, 0.184, 550.0, 0.170, 560.0, 0.149, 570.0, + 0.133, 580.0, 0.122, 590.0, 0.115, 600.0, 0.109, 610.0, 0.105, 620.0, 0.104, 630.0, 0.106, + 640.0, 0.109, 650.0, 0.112, 660.0, 0.114, 670.0, 0.114, 680.0, 0.112, 690.0, 0.112, 700.0, + 0.115, 710.0, 0.120, 720.0, 0.125, 730.0, 0.130, + ], + &[ + 380.0, 0.144, 390.0, 0.198, 400.0, 0.294, 410.0, 0.375, 420.0, 0.408, 430.0, 0.421, 440.0, + 0.426, 450.0, 0.426, 460.0, 0.419, 470.0, 0.403, 480.0, 0.379, 490.0, 0.346, 500.0, 0.311, + 510.0, 0.281, 520.0, 0.254, 530.0, 0.229, 540.0, 0.214, 550.0, 0.208, 560.0, 0.202, 570.0, + 0.194, 580.0, 0.193, 590.0, 0.200, 600.0, 0.214, 610.0, 0.230, 620.0, 0.241, 630.0, 0.254, + 640.0, 0.279, 650.0, 0.313, 660.0, 0.348, 670.0, 0.366, 680.0, 0.366, 690.0, 0.359, 700.0, + 0.358, 710.0, 0.365, 720.0, 0.377, 730.0, 0.398, + ], + &[ + 380.0, 0.136, 390.0, 0.179, 400.0, 0.247, 410.0, 0.297, 420.0, 0.320, 430.0, 0.337, 440.0, + 0.355, 450.0, 0.381, 460.0, 0.419, 470.0, 0.466, 480.0, 0.510, 490.0, 0.546, 500.0, 0.567, + 510.0, 0.574, 520.0, 0.569, 530.0, 0.551, 540.0, 0.524, 550.0, 0.488, 560.0, 0.445, 570.0, + 0.400, 580.0, 0.350, 590.0, 0.299, 600.0, 0.252, 610.0, 0.221, 620.0, 0.204, 630.0, 0.196, + 640.0, 0.191, 650.0, 0.188, 660.0, 0.191, 670.0, 0.199, 680.0, 0.212, 690.0, 0.223, 700.0, + 0.232, 710.0, 0.233, 720.0, 0.229, 730.0, 0.229, + ], + &[ + 380.0, 0.054, 390.0, 0.054, 400.0, 0.053, 410.0, 0.054, 420.0, 0.054, 430.0, 0.055, 440.0, + 0.055, 450.0, 0.055, 460.0, 0.056, 470.0, 0.057, 480.0, 0.058, 490.0, 0.061, 500.0, 0.068, + 510.0, 0.089, 520.0, 0.125, 530.0, 0.154, 540.0, 0.174, 550.0, 0.199, 560.0, 0.248, 570.0, + 0.335, 580.0, 0.444, 590.0, 0.538, 600.0, 0.587, 610.0, 0.595, 620.0, 0.591, 630.0, 0.587, + 640.0, 0.584, 650.0, 0.584, 660.0, 0.590, 670.0, 0.603, 680.0, 0.620, 690.0, 0.639, 700.0, + 0.655, 710.0, 0.663, 720.0, 0.663, 730.0, 0.667, + ], + &[ + 380.0, 0.122, 390.0, 0.164, 400.0, 0.229, 410.0, 0.286, 420.0, 0.327, 430.0, 0.361, 440.0, + 0.388, 450.0, 0.400, 460.0, 0.392, 470.0, 0.362, 480.0, 0.316, 490.0, 0.260, 500.0, 0.209, + 510.0, 0.168, 520.0, 0.138, 530.0, 0.117, 540.0, 0.104, 550.0, 0.096, 560.0, 0.090, 570.0, + 0.086, 580.0, 0.084, 590.0, 0.084, 600.0, 0.084, 610.0, 0.084, 620.0, 0.084, 630.0, 0.085, + 640.0, 0.090, 650.0, 0.098, 660.0, 0.109, 670.0, 0.123, 680.0, 0.143, 690.0, 0.169, 700.0, + 0.205, 710.0, 0.244, 720.0, 0.287, 730.0, 0.332, + ], + &[ + 380.0, 0.096, 390.0, 0.115, 400.0, 0.131, 410.0, 0.135, 420.0, 0.133, 430.0, 0.132, 440.0, + 0.130, 450.0, 0.128, 460.0, 0.125, 470.0, 0.120, 480.0, 0.115, 490.0, 0.110, 500.0, 0.105, + 510.0, 0.100, 520.0, 0.095, 530.0, 0.093, 540.0, 0.092, 550.0, 0.093, 560.0, 0.096, 570.0, + 0.108, 580.0, 0.156, 590.0, 0.265, 600.0, 0.399, 610.0, 0.500, 620.0, 0.556, 630.0, 0.579, + 640.0, 0.588, 650.0, 0.591, 660.0, 0.593, 670.0, 0.594, 680.0, 0.598, 690.0, 0.602, 700.0, + 0.607, 710.0, 0.609, 720.0, 0.609, 730.0, 0.610, + ], + &[ + 380.0, 0.092, 390.0, 0.116, 400.0, 0.146, 410.0, 0.169, 420.0, 0.178, 430.0, 0.173, 440.0, + 0.158, 450.0, 0.139, 460.0, 0.119, 470.0, 0.101, 480.0, 0.087, 490.0, 0.075, 500.0, 0.066, + 510.0, 0.060, 520.0, 0.056, 530.0, 0.053, 540.0, 0.051, 550.0, 0.051, 560.0, 0.052, 570.0, + 0.052, 580.0, 0.051, 590.0, 0.052, 600.0, 0.058, 610.0, 0.073, 620.0, 0.096, 630.0, 0.119, + 640.0, 0.141, 650.0, 0.166, 660.0, 0.194, 670.0, 0.227, 680.0, 0.265, 690.0, 0.309, 700.0, + 0.355, 710.0, 0.396, 720.0, 0.436, 730.0, 0.478, + ], + &[ + 380.0, 0.061, 390.0, 0.061, 400.0, 0.062, 410.0, 0.063, 420.0, 0.064, 430.0, 0.066, 440.0, + 0.069, 450.0, 0.075, 460.0, 0.085, 470.0, 0.105, 480.0, 0.139, 490.0, 0.192, 500.0, 0.271, + 510.0, 0.376, 520.0, 0.476, 530.0, 0.531, 540.0, 0.549, 550.0, 0.546, 560.0, 0.528, 570.0, + 0.504, 580.0, 0.471, 590.0, 0.428, 600.0, 0.381, 610.0, 0.347, 620.0, 0.327, 630.0, 0.318, + 640.0, 0.312, 650.0, 0.310, 660.0, 0.314, 670.0, 0.327, 680.0, 0.345, 690.0, 0.363, 700.0, + 0.376, 710.0, 0.381, 720.0, 0.378, 730.0, 0.379, + ], + &[ + 380.0, 0.063, 390.0, 0.063, 400.0, 0.063, 410.0, 0.064, 420.0, 0.064, 430.0, 0.064, 440.0, + 0.065, 450.0, 0.066, 460.0, 0.067, 470.0, 0.068, 480.0, 0.071, 490.0, 0.076, 500.0, 0.087, + 510.0, 0.125, 520.0, 0.206, 530.0, 0.305, 540.0, 0.383, 550.0, 0.431, 560.0, 0.469, 570.0, + 0.518, 580.0, 0.568, 590.0, 0.607, 600.0, 0.628, 610.0, 0.637, 620.0, 0.640, 630.0, 0.642, + 640.0, 0.645, 650.0, 0.648, 660.0, 0.651, 670.0, 0.653, 680.0, 0.657, 690.0, 0.664, 700.0, + 0.673, 710.0, 0.680, 720.0, 0.684, 730.0, 0.688, + ], + &[ + 380.0, 0.066, 390.0, 0.079, 400.0, 0.102, 410.0, 0.146, 420.0, 0.200, 430.0, 0.244, 440.0, + 0.282, 450.0, 0.309, 460.0, 0.308, 470.0, 0.278, 480.0, 0.231, 490.0, 0.178, 500.0, 0.130, + 510.0, 0.094, 520.0, 0.070, 530.0, 0.054, 540.0, 0.046, 550.0, 0.042, 560.0, 0.039, 570.0, + 0.038, 580.0, 0.038, 590.0, 0.038, 600.0, 0.038, 610.0, 0.039, 620.0, 0.039, 630.0, 0.040, + 640.0, 0.041, 650.0, 0.042, 660.0, 0.044, 670.0, 0.045, 680.0, 0.046, 690.0, 0.046, 700.0, + 0.048, 710.0, 0.052, 720.0, 0.057, 730.0, 0.065, + ], + &[ + 380.0, 0.052, 390.0, 0.053, 400.0, 0.054, 410.0, 0.055, 420.0, 0.057, 430.0, 0.059, 440.0, + 0.061, 450.0, 0.066, 460.0, 0.075, 470.0, 0.093, 480.0, 0.125, 490.0, 0.178, 500.0, 0.246, + 510.0, 0.307, 520.0, 0.337, 530.0, 0.334, 540.0, 0.317, 550.0, 0.293, 560.0, 0.262, 570.0, + 0.230, 580.0, 0.198, 590.0, 0.165, 600.0, 0.135, 610.0, 0.115, 620.0, 0.104, 630.0, 0.098, + 640.0, 0.094, 650.0, 0.092, 660.0, 0.093, 670.0, 0.097, 680.0, 0.102, 690.0, 0.108, 700.0, + 0.113, 710.0, 0.115, 720.0, 0.114, 730.0, 0.114, + ], + &[ + 380.0, 0.050, 390.0, 0.049, 400.0, 0.048, 410.0, 0.047, 420.0, 0.047, 430.0, 0.047, 440.0, + 0.047, 450.0, 0.047, 460.0, 0.046, 470.0, 0.045, 480.0, 0.044, 490.0, 0.044, 500.0, 0.045, + 510.0, 0.046, 520.0, 0.047, 530.0, 0.048, 540.0, 0.049, 550.0, 0.050, 560.0, 0.054, 570.0, + 0.060, 580.0, 0.072, 590.0, 0.104, 600.0, 0.178, 610.0, 0.312, 620.0, 0.467, 630.0, 0.581, + 640.0, 0.644, 650.0, 0.675, 660.0, 0.690, 670.0, 0.698, 680.0, 0.706, 690.0, 0.715, 700.0, + 0.724, 710.0, 0.730, 720.0, 0.734, 730.0, 0.738, + ], + &[ + 380.0, 0.058, 390.0, 0.054, 400.0, 0.052, 410.0, 0.052, 420.0, 0.053, 430.0, 0.054, 440.0, + 0.056, 450.0, 0.059, 460.0, 0.067, 470.0, 0.081, 480.0, 0.107, 490.0, 0.152, 500.0, 0.225, + 510.0, 0.336, 520.0, 0.462, 530.0, 0.559, 540.0, 0.616, 550.0, 0.650, 560.0, 0.672, 570.0, + 0.694, 580.0, 0.710, 590.0, 0.723, 600.0, 0.731, 610.0, 0.739, 620.0, 0.746, 630.0, 0.752, + 640.0, 0.758, 650.0, 0.764, 660.0, 0.769, 670.0, 0.771, 680.0, 0.776, 690.0, 0.782, 700.0, + 0.790, 710.0, 0.796, 720.0, 0.799, 730.0, 0.804, + ], + &[ + 380.0, 0.145, 390.0, 0.195, 400.0, 0.283, 410.0, 0.346, 420.0, 0.362, 430.0, 0.354, 440.0, + 0.334, 450.0, 0.306, 460.0, 0.276, 470.0, 0.248, 480.0, 0.218, 490.0, 0.190, 500.0, 0.168, + 510.0, 0.149, 520.0, 0.127, 530.0, 0.107, 540.0, 0.100, 550.0, 0.102, 560.0, 0.104, 570.0, + 0.109, 580.0, 0.137, 590.0, 0.200, 600.0, 0.290, 610.0, 0.400, 620.0, 0.516, 630.0, 0.615, + 640.0, 0.687, 650.0, 0.732, 660.0, 0.760, 670.0, 0.774, 680.0, 0.783, 690.0, 0.793, 700.0, + 0.803, 710.0, 0.812, 720.0, 0.817, 730.0, 0.825, + ], + &[ + 380.0, 0.108, 390.0, 0.141, 400.0, 0.192, 410.0, 0.236, 420.0, 0.261, 430.0, 0.286, 440.0, + 0.317, 450.0, 0.353, 460.0, 0.390, 470.0, 0.426, 480.0, 0.446, 490.0, 0.444, 500.0, 0.423, + 510.0, 0.385, 520.0, 0.337, 530.0, 0.283, 540.0, 0.231, 550.0, 0.185, 560.0, 0.146, 570.0, + 0.118, 580.0, 0.101, 590.0, 0.090, 600.0, 0.082, 610.0, 0.076, 620.0, 0.074, 630.0, 0.073, + 640.0, 0.073, 650.0, 0.074, 660.0, 0.076, 670.0, 0.077, 680.0, 0.076, 690.0, 0.075, 700.0, + 0.073, 710.0, 0.072, 720.0, 0.074, 730.0, 0.079, + ], + &[ + 380.0, 0.189, 390.0, 0.255, 400.0, 0.423, 410.0, 0.660, 420.0, 0.811, 430.0, 0.862, 440.0, + 0.877, 450.0, 0.884, 460.0, 0.891, 470.0, 0.896, 480.0, 0.899, 490.0, 0.904, 500.0, 0.907, + 510.0, 0.909, 520.0, 0.911, 530.0, 0.910, 540.0, 0.911, 550.0, 0.914, 560.0, 0.913, 570.0, + 0.916, 580.0, 0.915, 590.0, 0.916, 600.0, 0.914, 610.0, 0.915, 620.0, 0.918, 630.0, 0.919, + 640.0, 0.921, 650.0, 0.923, 660.0, 0.924, 670.0, 0.922, 680.0, 0.922, 690.0, 0.925, 700.0, + 0.927, 710.0, 0.930, 720.0, 0.930, 730.0, 0.933, + ], + &[ + 380.0, 0.171, 390.0, 0.232, 400.0, 0.365, 410.0, 0.507, 420.0, 0.567, 430.0, 0.583, 440.0, + 0.588, 450.0, 0.590, 460.0, 0.591, 470.0, 0.590, 480.0, 0.588, 490.0, 0.588, 500.0, 0.589, + 510.0, 0.589, 520.0, 0.591, 530.0, 0.590, 540.0, 0.590, 550.0, 0.590, 560.0, 0.589, 570.0, + 0.591, 580.0, 0.590, 590.0, 0.590, 600.0, 0.587, 610.0, 0.585, 620.0, 0.583, 630.0, 0.580, + 640.0, 0.578, 650.0, 0.576, 660.0, 0.574, 670.0, 0.572, 680.0, 0.571, 690.0, 0.569, 700.0, + 0.568, 710.0, 0.568, 720.0, 0.566, 730.0, 0.566, + ], + &[ + 380.0, 0.144, 390.0, 0.192, 400.0, 0.272, 410.0, 0.331, 420.0, 0.350, 430.0, 0.357, 440.0, + 0.361, 450.0, 0.363, 460.0, 0.363, 470.0, 0.361, 480.0, 0.359, 490.0, 0.358, 500.0, 0.358, + 510.0, 0.359, 520.0, 0.360, 530.0, 0.360, 540.0, 0.361, 550.0, 0.361, 560.0, 0.360, 570.0, + 0.362, 580.0, 0.362, 590.0, 0.361, 600.0, 0.359, 610.0, 0.358, 620.0, 0.355, 630.0, 0.352, + 640.0, 0.350, 650.0, 0.348, 660.0, 0.345, 670.0, 0.343, 680.0, 0.340, 690.0, 0.338, 700.0, + 0.335, 710.0, 0.334, 720.0, 0.332, 730.0, 0.331, + ], + &[ + 380.0, 0.105, 390.0, 0.131, 400.0, 0.163, 410.0, 0.180, 420.0, 0.186, 430.0, 0.190, 440.0, + 0.193, 450.0, 0.194, 460.0, 0.194, 470.0, 0.192, 480.0, 0.191, 490.0, 0.191, 500.0, 0.191, + 510.0, 0.192, 520.0, 0.192, 530.0, 0.192, 540.0, 0.192, 550.0, 0.192, 560.0, 0.192, 570.0, + 0.193, 580.0, 0.192, 590.0, 0.192, 600.0, 0.191, 610.0, 0.189, 620.0, 0.188, 630.0, 0.186, + 640.0, 0.184, 650.0, 0.182, 660.0, 0.181, 670.0, 0.179, 680.0, 0.178, 690.0, 0.176, 700.0, + 0.174, 710.0, 0.173, 720.0, 0.172, 730.0, 0.171, + ], + &[ + 380.0, 0.068, 390.0, 0.077, 400.0, 0.084, 410.0, 0.087, 420.0, 0.089, 430.0, 0.090, 440.0, + 0.092, 450.0, 0.092, 460.0, 0.091, 470.0, 0.090, 480.0, 0.090, 490.0, 0.090, 500.0, 0.090, + 510.0, 0.090, 520.0, 0.090, 530.0, 0.090, 540.0, 0.090, 550.0, 0.090, 560.0, 0.090, 570.0, + 0.090, 580.0, 0.090, 590.0, 0.089, 600.0, 0.089, 610.0, 0.088, 620.0, 0.087, 630.0, 0.086, + 640.0, 0.086, 650.0, 0.085, 660.0, 0.084, 670.0, 0.084, 680.0, 0.083, 690.0, 0.083, 700.0, + 0.082, 710.0, 0.081, 720.0, 0.081, 730.0, 0.081, + ], + &[ + 380.0, 0.031, 390.0, 0.032, 400.0, 0.032, 410.0, 0.033, 420.0, 0.033, 430.0, 0.033, 440.0, + 0.033, 450.0, 0.033, 460.0, 0.032, 470.0, 0.032, 480.0, 0.032, 490.0, 0.032, 500.0, 0.032, + 510.0, 0.032, 520.0, 0.032, 530.0, 0.032, 540.0, 0.032, 550.0, 0.032, 560.0, 0.032, 570.0, + 0.032, 580.0, 0.032, 590.0, 0.032, 600.0, 0.032, 610.0, 0.032, 620.0, 0.032, 630.0, 0.032, + 640.0, 0.032, 650.0, 0.032, 660.0, 0.032, 670.0, 0.032, 680.0, 0.032, 690.0, 0.032, 700.0, + 0.032, 710.0, 0.032, 720.0, 0.032, 730.0, 0.033, + ], +]; diff --git a/src/spectra/color.rs b/shared/src/spectra/color.rs similarity index 87% rename from src/spectra/color.rs rename to shared/src/spectra/color.rs index 5ee7c2a..4205c20 100644 --- a/src/spectra/color.rs +++ b/shared/src/spectra/color.rs @@ -283,12 +283,20 @@ impl RGB { (self.r + self.g + self.b) / 3.0 } - pub fn max(&self) -> Float { + pub fn max_component_value(&self) -> Float { self.r.max(self.g).max(self.b) } + pub fn max_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 clamp_zero(rgb: Self) -> Self { - RGB::new(rgb.r.max(0.), rgb.b.max(0.), rgb.b.max(0.)) + RGB::new(rgb.r.max(0.), rgb.b.max(0.), rgb.g.max(0.)) } } @@ -510,15 +518,37 @@ pub type CoefficientArray = [[[[[Float; 3]; RES]; RES]; RES]; 3]; #[derive(Debug)] pub struct RGBToSpectrumTable { - z_nodes: &'static [f32], - coeffs: &'static CoefficientArray, + coeffs: &'static [Float], + scale: &'static [Float], } impl RGBToSpectrumTable { pub fn srgb() -> Self { - // use crate::core::constants::{RGB_TO_SPECTRUM_Z_NODES, RGB_TO_SPECTRUM_COEFFS}; - // Self::new(&RGB_TO_SPECTRUM_Z_NODES, &RGB_TO_SPECTRUM_COEFFS) - todo!("Link the static constant arrays for sRGB coefficients here") + Self { + coeffs: *crate::data::SRGB_COEFFS, + scale: *crate::data::SRGB_SCALE, + } + } + + pub fn dci_p3() -> Self { + Self { + coeffs: *crate::data::DCI_P3_COEFFS, + scale: *crate::data::DCI_P3_SCALE, + } + } + + pub fn rec2020() -> Self { + Self { + coeffs: *crate::data::REC2020_COEFFS, + scale: *crate::data::REC2020_SCALE, + } + } + + pub fn aces2065_1() -> Self { + Self { + coeffs: *crate::data::ACES_COEFFS, + scale: *crate::data::ACES_SCALE, + } } } @@ -563,11 +593,12 @@ impl RGBSigmoidPolynomial { } impl RGBToSpectrumTable { - pub fn new(z_nodes: &'static [f32], coeffs: &'static CoefficientArray) -> Self { - Self { z_nodes, coeffs } + pub fn new(scale: &'static [Float], coeffs: &'static [Float]) -> Self { + Self { scale, coeffs } } pub fn to_polynomial(&self, rgb: RGB) -> RGBSigmoidPolynomial { + //TODO: Check all of this logic, this is important if rgb[0] == rgb[1] && rgb[1] == rgb[2] { return RGBSigmoidPolynomial::new( 0.0, @@ -575,53 +606,68 @@ impl RGBToSpectrumTable { (rgb[0] - 0.5) / (rgb[0] * (1.0 - rgb[0])).sqrt(), ); } - let maxc; - if rgb[0] > rgb[1] { - if rgb[0] > rgb[2] { - maxc = 0; - } else { - maxc = 2; + + let max_c = rgb.max_component_value(); + let c = [rgb.r, rgb.g, rgb.b]; + let (min_c, mid_c) = if c[0] < c[1] { + if c[1] < c[2] { + (c[0], c[1]) } - } else if rgb[1] > rgb[2] { - maxc = 1; + // 0 < 1 < 2 + else if c[0] < c[2] { + (c[0], c[2]) + } + // 0 < 2 < 1 + else { + (c[2], c[0]) + } // 2 < 0 < 1 } else { - maxc = 2; - } + if c[1] > c[2] { + (c[2], c[1]) + } + // 2 < 1 < 0 + else if c[0] > c[2] { + (c[1], c[2]) + } + // 1 < 2 < 0 + else { + (c[1], c[0]) + } // 1 < 0 < 2 + }; - let z = rgb[maxc]; - let x = rgb[(maxc + 1) % 3] * (RES - 1) as Float / z; - let y = rgb[(maxc + 2) % 3] * (RES - 1) as Float / z; + let z = min_c / max_c; + let x = mid_c / max_c; - let xi = x.min(RES as Float - 2.0); - let yi = y.min(RES as Float - 2.0); - let zi = crate::core::pbrt::find_interval(RES, |i: usize| self.z_nodes[i] < z); - let dx = (x - xi) as usize; - let dy = (y - yi) as usize; - let dz = (z - self.z_nodes[zi]) / (self.z_nodes[zi + 1] - self.z_nodes[zi]); - let mut c = [0.0; 3]; + let z_float = z * (RES - 1) as Float; + let zi = (z_float as usize).min(RES - 2); + let z_t = z_float - zi as Float; + let x_float = x * (RES - 1) as Float; + let xi = (x_float as usize).min(RES - 2); + let x_t = x_float - xi as Float; + + let mut coeffs = [0.0; 3]; #[allow(clippy::needless_range_loop)] for i in 0..3 { - let co = |dx: usize, dy: usize, dz: usize| { - self.coeffs[maxc][zi as usize + dz][yi as usize + dy][xi as usize + dx][i] - }; - c[i] = lerp( - dz, - lerp( - dy as Float, - lerp(dx as Float, co(0, 0, 0) as Float, co(1, 0, 0)) as Float, - lerp(dx as Float, co(0, 1, 0) as Float, co(1, 1, 0) as Float), - ), - lerp( - dy as Float, - lerp(dx as Float, co(0, 0, 1) as Float, co(1, 0, 1)) as Float, - lerp(dx as Float, co(0, 1, 1) as Float, co(1, 1, 1) as Float), - ), - ); + let offset = |dz, dx| (((zi + dz) * RES + (xi + dx)) * RES + (RES - 1)) * 3 + i; + let c00 = self.coeffs[offset(0, 0)]; + let c01 = self.coeffs[offset(0, 1)]; + let c10 = self.coeffs[offset(1, 0)]; + let c11 = self.coeffs[offset(1, 1)]; + let v0 = lerp(x_t, c00, c01); + let v1 = lerp(x_t, c10, c11); + coeffs[i] = lerp(z_t, v0, v1); } + + let scale_float = max_c * (RES - 1) as Float; + let si = (scale_float as usize).min(RES - 2); + let s_t = scale_float - si as Float; + + let scale = lerp(s_t, self.scale[si], self.scale[si + 1]); + RGBSigmoidPolynomial { - c0: c[0], - c1: c[1], - c2: c[2], + c0: coeffs[0], + c1: coeffs[1], + c2: coeffs[2], } } } diff --git a/src/spectra/colorspace.rs b/shared/src/spectra/colorspace.rs similarity index 57% rename from src/spectra/colorspace.rs rename to shared/src/spectra/colorspace.rs index 06970ff..2057ce5 100644 --- a/src/spectra/colorspace.rs +++ b/shared/src/spectra/colorspace.rs @@ -38,7 +38,7 @@ impl RGBColorSpace { let rgb_values = [ [r_xyz.x(), g_xyz.x(), b_xyz.x()], [r_xyz.y(), g_xyz.y(), b_xyz.y()], - [r_xyz.z(), g_xyz.z(), g_xyz.z()], + [r_xyz.z(), g_xyz.z(), b_xyz.z()], ]; let rgb = SquareMatrix::new(rgb_values); let c: RGB = rgb.inverse()? * w_xyz; @@ -59,6 +59,16 @@ impl RGBColorSpace { }) } + pub fn get_named(name: &str) -> Result, String> { + match name.to_lowercase().as_str() { + "aces2065-1" => Ok(Self::aces2065_1().clone()), + "rec2020" => Ok(Self::rec2020().clone()), + "dci-p3" => Ok(Self::dci_p3().clone()), + "srgb" => Ok(Self::srgb().clone()), + _ => Err(format!("Color space '{}' not found", name)), + } + } + pub fn to_xyz(&self, rgb: RGB) -> XYZ { self.xyz_from_rgb * rgb } @@ -79,8 +89,8 @@ impl RGBColorSpace { self.rgb_from_xyz * other.xyz_from_rgb } - pub fn srgb() -> &'static Self { - static SRGB_SPACE: Lazy = Lazy::new(|| { + pub fn srgb() -> &'static Arc { + static SRGB_SPACE: Lazy> = Lazy::new(|| { let r = Point2f::new(0.64, 0.33); let g = Point2f::new(0.30, 0.60); let b = Point2f::new(0.15, 0.06); @@ -88,12 +98,54 @@ impl RGBColorSpace { let illuminant = Spectrum::std_illuminant_d65(); let table = RGBToSpectrumTable::srgb(); - RGBColorSpace::new(r, g, b, illuminant, table) - .expect("Failed to initialize standard sRGB color space") + Arc::new(RGBColorSpace::new(r, g, b, illuminant, table).unwrap()) }); &SRGB_SPACE } + + pub fn dci_p3() -> &'static Arc { + static DCI_P3: Lazy> = Lazy::new(|| { + let r = Point2f::new(0.680, 0.320); + let g = Point2f::new(0.265, 0.690); + let b = Point2f::new(0.150, 0.060); + let illuminant = Spectrum::std_illuminant_d65(); + + let table = RGBToSpectrumTable::dci_p3(); + + Arc::new(RGBColorSpace::new(r, g, b, illuminant, table).unwrap()) + }); + &DCI_P3 + } + + pub fn rec2020() -> &'static Arc { + static REC2020: Lazy> = Lazy::new(|| { + let r = Point2f::new(0.708, 0.292); + let g = Point2f::new(0.170, 0.797); + let b = Point2f::new(0.131, 0.046); + let illuminant = Spectrum::std_illuminant_d65(); + + let table = RGBToSpectrumTable::rec2020(); + + Arc::new(RGBColorSpace::new(r, g, b, illuminant, table).unwrap()) + }); + &REC2020 + } + + pub fn aces2065_1() -> &'static Arc { + static ACES: Lazy> = Lazy::new(|| { + let r = Point2f::new(0.7347, 0.2653); + let g = Point2f::new(0.0000, 1.0000); + let b = Point2f::new(0.0001, -0.0770); + // ACES uses D60 + let illuminant = Spectrum::std_illuminant_d65(); + + let table = RGBToSpectrumTable::aces2065_1(); + + Arc::new(RGBColorSpace::new(r, g, b, illuminant, table).unwrap()) + }); + &ACES + } } impl PartialEq for RGBColorSpace { diff --git a/shared/src/spectra/data.rs b/shared/src/spectra/data.rs new file mode 100644 index 0000000..9cb826d --- /dev/null +++ b/shared/src/spectra/data.rs @@ -0,0 +1,65 @@ +use super::Spectrum; +use super::sampled::{LAMBDA_MAX, LAMBDA_MIN}; +use super::simple::{DenselySampledSpectrum, PiecewiseLinearSpectrum}; +use crate::core::cie::{CIE_S_LAMBDA, CIE_S0, CIE_S1, CIE_S2, CIE_X, CIE_Y, CIE_Z, N_CIES}; +use crate::core::pbrt::Float; +use crate::spectra::{BlackbodySpectrum, SpectrumProvider}; +use crate::utils::math::square; +use once_cell::sync::Lazy; + +fn create_cie_spectrum(data: &[Float]) -> Spectrum { + let pls = PiecewiseLinearSpectrum::from_interleaved(data, false); + + let dss = DenselySampledSpectrum::from_spectrum(&Spectrum::PiecewiseLinear(pls)); + + Spectrum::DenselySampled(dss) +} + +pub fn generate_cie_d(temperature: Float) -> DenselySampledSpectrum { + let cct = temperature * 1.4388 / 1.4380; + if cct < 4000.0 { + let bb = BlackbodySpectrum::new(cct); + return DenselySampledSpectrum::from_function( + |lambda| bb.evaluate(lambda), + LAMBDA_MIN, + LAMBDA_MAX, + ); + } + + let x = if cct < 7000. { + -4.607 * 1e9 / cct.powi(3) + 2.9678 * 1e6 / square(cct) + 0.09911 * 1e3 / cct + 0.244063 + } else { + -2.0064 * 1e9 / cct.powi(3) + 1.9018 * 1e6 / square(cct) + 0.24748 * 1e3 / cct + 0.23704 + }; + let y = -3. * x + 2.87 * x - 0.275; + + let m = 0.0241 + 0.2562 * x - 0.7341 * y; + let m1 = (-1.3515 - 1.7703 * x + 5.9114 * y) / m; + let m2 = (0.0300 - 31.4424 * x + 30.0717 * y) / m; + + let values: Vec = (0..N_CIES) + .map(|i| (CIE_S0[i] + CIE_S1[i] * m1 + CIE_S2[i] * m2) * 0.01) + .collect(); + + let dpls = PiecewiseLinearSpectrum { + lambdas: CIE_S_LAMBDA.to_vec(), + values, + }; + + DenselySampledSpectrum::from_spectrum(&Spectrum::PiecewiseLinear(dpls)) +} + +pub(crate) fn cie_x() -> &'static Spectrum { + static X: Lazy = Lazy::new(|| create_cie_spectrum(&CIE_X)); + &X +} + +pub(crate) fn cie_y() -> &'static Spectrum { + static Y: Lazy = Lazy::new(|| create_cie_spectrum(&CIE_Y)); + &Y +} + +pub(crate) fn cie_z() -> &'static Spectrum { + static Z: Lazy = Lazy::new(|| create_cie_spectrum(&CIE_Z)); + &Z +} diff --git a/src/spectra/mod.rs b/shared/src/spectra/mod.rs similarity index 63% rename from src/spectra/mod.rs rename to shared/src/spectra/mod.rs index e6ee8b8..c5190e6 100644 --- a/src/spectra/mod.rs +++ b/shared/src/spectra/mod.rs @@ -1,25 +1,51 @@ +pub mod cie; +pub mod color; +pub mod colorspace; pub mod data; pub mod rgb; pub mod sampled; pub mod simple; use crate::core::pbrt::Float; -use crate::utils::color::{RGB, XYZ}; -use crate::utils::colorspace::RGBColorSpace; +use crate::utils::file::read_float_file; use enum_dispatch::enum_dispatch; +pub use color::{RGB, XYZ}; +pub use colorspace::RGBColorSpace; pub use data::*; pub use rgb::*; pub use sampled::{CIE_Y_INTEGRAL, LAMBDA_MAX, LAMBDA_MIN}; pub use sampled::{N_SPECTRUM_SAMPLES, SampledSpectrum, SampledWavelengths}; -pub use simple::*; // CIE_X, etc +pub use simple::*; // #[enum_dispatch] -pub trait SpectrumTrait { +pub trait SpectrumProvider: Copy { fn evaluate(&self, lambda: Float) -> Float; fn max_value(&self) -> Float; } +#[cfg(not(target_arch = "spirv"))] +impl SpectrumProvider for std::sync::Arc { + fn evaluate(&self, lambda: Float) -> Float { + (**self).evaluate(lambda) + } + fn max_value(&self) -> Float { + (**self).max_value() + } +} + +#[cfg(target_arch = "spirv")] // or target_os = "cuda" +impl SpectrumProvider for u32 { + fn evaluate(&self, lambda: Float) -> Float { + // Here you would call a global function that accesses + // a static buffer of spectra data + crate::gpu::lookup_global_spectrum(*self, lambda) + } + fn max_value(&self) -> Float { + crate::gpu::lookup_global_spectrum_max(*self) + } +} + #[enum_dispatch(SpectrumTrait)] #[derive(Debug, Clone)] pub enum Spectrum { @@ -28,6 +54,8 @@ pub enum Spectrum { PiecewiseLinear(PiecewiseLinearSpectrum), Blackbody(BlackbodySpectrum), RGBAlbedo(RGBAlbedoSpectrum), + RGBIlluminant(RGBIlluminantSpectrum), + RGBUnbounded(RGBUnboundedSpectrum), } impl Spectrum { diff --git a/src/spectra/rgb.rs b/shared/src/spectra/rgb.rs similarity index 67% rename from src/spectra/rgb.rs rename to shared/src/spectra/rgb.rs index aec2df9..82e7714 100644 --- a/src/spectra/rgb.rs +++ b/shared/src/spectra/rgb.rs @@ -1,11 +1,9 @@ -use super::sampled::{ - LAMBDA_MAX, LAMBDA_MIN, N_SPECTRUM_SAMPLES, SampledSpectrum, SampledWavelengths, +use super::{ + DenselySampledSpectrum, LAMBDA_MAX, LAMBDA_MIN, N_SPECTRUM_SAMPLES, RGB, RGBColorSpace, + RGBSigmoidPolynomial, SampledSpectrum, SampledWavelengths, SpectrumProvider, XYZ, }; -use crate::core::pbrt::Float; -use crate::spectra::{DenselySampledSpectrum, SpectrumTrait}; -use crate::utils::color::{RGB, RGBSigmoidPolynomial, XYZ}; -use crate::utils::colorspace::RGBColorSpace; -use std::sync::Arc; + +use crate::Float; #[derive(Debug, Clone, Copy)] pub struct RGBAlbedoSpectrum { @@ -28,7 +26,7 @@ impl RGBAlbedoSpectrum { } } -impl SpectrumTrait for RGBAlbedoSpectrum { +impl SpectrumProvider for RGBAlbedoSpectrum { fn evaluate(&self, lambda: Float) -> Float { self.rsp.evaluate(lambda) } @@ -60,7 +58,7 @@ impl UnboundedRGBSpectrum { } } -impl SpectrumTrait for UnboundedRGBSpectrum { +impl SpectrumProvider for UnboundedRGBSpectrum { fn evaluate(&self, lambda: Float) -> Float { self.scale * self.rsp.evaluate(lambda) } @@ -71,40 +69,39 @@ impl SpectrumTrait for UnboundedRGBSpectrum { } #[derive(Debug, Clone, Default)] -pub struct RGBIlluminantSpectrum { +pub struct RGBIlluminantSpectrum { scale: Float, rsp: RGBSigmoidPolynomial, - illuminant: Option>, + illuminant: P, } -impl RGBIlluminantSpectrum { - pub fn new(cs: &RGBColorSpace, rgb: RGB) -> Self { - let illuminant = &cs.illuminant; - let densely_sampled = - DenselySampledSpectrum::from_spectrum(illuminant, LAMBDA_MIN, LAMBDA_MAX); - let m = rgb.max(); - let scale = 2. * m; - let rsp = cs.to_rgb_coeffs(if scale == 1. { - rgb / scale - } else { - RGB::new(0., 0., 0.) - }); - Self { - scale, - rsp, - illuminant: Some(Arc::new(densely_sampled)), - } - } +// impl RGBIlluminantSpectrum { +// pub fn new(cs: &RGBColorSpace, rgb: RGB) -> Self { +// let illuminant = &cs.illuminant; +// let densely_sampled = DenselySampledSpectrum::from_spectrum(illuminant); +// let m = rgb.max_component_value(); +// let scale = 2. * m; +// let rsp = cs.to_rgb_coeffs(if scale == 1. { +// rgb / scale +// } else { +// RGB::new(0., 0., 0.) +// }); +// Self { +// scale, +// rsp, +// illuminant: Some(Arc::new(densely_sampled)), +// } +// } +// +// pub fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { +// if self.illuminant.is_none() { +// return SampledSpectrum::new(0.); +// } +// SampledSpectrum::from_fn(|i| self.scale * self.rsp.evaluate(lambda[i])) +// } +// } - pub fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { - if self.illuminant.is_none() { - return SampledSpectrum::new(0.); - } - SampledSpectrum::from_fn(|i| self.scale * self.rsp.evaluate(lambda[i])) - } -} - -impl SpectrumTrait for RGBIlluminantSpectrum { +impl SpectrumProvider for RGBIlluminantSpectrum { fn evaluate(&self, lambda: Float) -> Float { match &self.illuminant { Some(illuminant) => { @@ -144,7 +141,7 @@ impl Default for RGBUnboundedSpectrum { impl RGBUnboundedSpectrum { pub fn new(cs: &RGBColorSpace, rgb: RGB) -> Self { - let m = rgb.max(); + let m = rgb.max_component_value(); let scale = 2.0 * m; @@ -164,7 +161,7 @@ impl RGBUnboundedSpectrum { } } -impl SpectrumTrait for RGBUnboundedSpectrum { +impl SpectrumProvider for RGBUnboundedSpectrum { fn evaluate(&self, lambda: Float) -> Float { self.scale * self.rsp.evaluate(lambda) } diff --git a/src/spectra/sampled.rs b/shared/src/spectra/sampled.rs similarity index 94% rename from src/spectra/sampled.rs rename to shared/src/spectra/sampled.rs index 9ead79c..bc74de4 100644 --- a/src/spectra/sampled.rs +++ b/shared/src/spectra/sampled.rs @@ -1,4 +1,5 @@ use crate::core::pbrt::Float; +use crate::utils::math::{clamp, lerp}; use std::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, }; @@ -12,6 +13,7 @@ pub const LAMBDA_MIN: i32 = 360; pub const LAMBDA_MAX: i32 = 830; #[derive(Debug, Copy, Clone, PartialEq)] +#[repr(C)] pub struct SampledSpectrum { pub values: [Float; N_SPECTRUM_SAMPLES], } @@ -92,7 +94,20 @@ impl SampledSpectrum { pub fn clamp_zero(s: &SampledSpectrum) -> Self { let ret = SampledSpectrum::from_fn(|i| s[i].max(0.)); - assert!(!ret.has_nans()); + debug_assert!(!ret.has_nans()); + ret + } + + pub fn sqrt(&self) -> Self { + let values = self.values.map(|v| v.sqrt()); + let ret = SampledSpectrum { values }; + debug_assert!(!ret.has_nans()); + ret + } + + pub fn clamp(s: &SampledSpectrum, low: Float, high: Float) -> Self { + let ret = SampledSpectrum::from_fn(|i| clamp(s[i], low, high)); + debug_assert!(!ret.has_nans()); ret } @@ -280,6 +295,7 @@ impl Neg for SampledSpectrum { } #[derive(Debug, Copy, Clone, PartialEq)] +#[repr(C)] pub struct SampledWavelengths { pub lambda: [Float; N_SPECTRUM_SAMPLES], pub pdf: [Float; N_SPECTRUM_SAMPLES], @@ -318,7 +334,7 @@ impl SampledWavelengths { pub fn sample_uniform(u: Float, lambda_min: Float, lambda_max: Float) -> Self { let mut lambda = [0.0; N_SPECTRUM_SAMPLES]; - lambda[0] = crate::core::pbrt::lerp(u, lambda_min, lambda_min); + lambda[0] = lerp(u, lambda_min, lambda_min); let delta = (lambda_max - lambda_min) / N_SPECTRUM_SAMPLES as Float; for i in 1..N_SPECTRUM_SAMPLES { lambda[i] = lambda[i - 1] + delta; diff --git a/shared/src/spectra/simple.rs b/shared/src/spectra/simple.rs new file mode 100644 index 0000000..168a9f8 --- /dev/null +++ b/shared/src/spectra/simple.rs @@ -0,0 +1,480 @@ +use super::sampled::{LAMBDA_MAX, LAMBDA_MIN}; +use crate::core::cie::*; +use crate::core::pbrt::Float; +use crate::spectra::{ + N_SPECTRUM_SAMPLES, SampledSpectrum, SampledWavelengths, Spectrum, SpectrumProvider, +}; +use crate::utils::file::read_float_file; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::sync::LazyLock; + +#[derive(Debug, Clone, Copy)] +pub struct ConstantSpectrum { + c: Float, +} + +impl ConstantSpectrum { + pub fn new(c: Float) -> Self { + Self { c } + } +} + +impl SpectrumProvider for ConstantSpectrum { + fn evaluate(&self, _lambda: Float) -> Float { + self.c + } + + fn max_value(&self) -> Float { + self.c + } +} + +#[derive(Debug, Clone)] +pub struct DenselySampledSpectrum { + lambda_min: i32, + lambda_max: i32, + values: Vec, +} + +impl DenselySampledSpectrum { + pub fn new(lambda_min: i32, lambda_max: i32) -> Self { + let n_values = (lambda_max - lambda_min + 1).max(0) as usize; + Self { + lambda_min, + lambda_max, + values: vec![0.0; n_values], + } + } + + pub fn from_spectrum(spec: &Spectrum) -> Self { + let lambda_min = LAMBDA_MIN; + let lambda_max = LAMBDA_MAX; + let mut s = Self::new(lambda_min, lambda_max); + if s.values.is_empty() { + return s; + } + for lambda in lambda_min..=lambda_max { + let index = (lambda - lambda_min) as usize; + s.values[index] = spec.evaluate(lambda as Float); + } + s + } + + pub fn from_spectrum_with_range(spec: &Spectrum, lambda_min: i32, lambda_max: i32) -> Self { + let mut s = Self::new(lambda_min, lambda_max); + if s.values.is_empty() { + return s; + } + for lambda in lambda_min..=lambda_max { + let index = (lambda - lambda_min) as usize; + s.values[index] = spec.evaluate(lambda as Float); + } + s + } + + pub fn from_function(f: F, lambda_min: i32, lambda_max: i32) -> Self + where + F: Fn(Float) -> Float, + { + let mut s = Self::new(lambda_min, lambda_max); + if s.values.is_empty() { + return s; + } + for lambda in lambda_min..=lambda_max { + let index = (lambda - lambda_min) as usize; + s.values[index] = f(lambda as Float); + } + s + } + + pub fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { + let mut s = SampledSpectrum::default(); + + for i in 0..N_SPECTRUM_SAMPLES { + let offset = lambda[i].round() as usize - LAMBDA_MIN as usize; + if offset >= self.values.len() { + s[i] = 0.; + } else { + s[i] = self.values[offset]; + } + } + s + } + + pub fn min_component_value(&self) -> Float { + self.values.iter().fold(Float::INFINITY, |a, &b| a.min(b)) + } + + pub fn max_component_value(&self) -> Float { + self.values + .iter() + .fold(Float::NEG_INFINITY, |a, &b| a.max(b)) + } + + pub fn average(&self) -> Float { + self.values.iter().sum::() / (N_SPECTRUM_SAMPLES as Float) + } + + pub fn safe_div(&self, rhs: SampledSpectrum) -> Self { + let mut r = Self::new(1, 1); + for i in 0..N_SPECTRUM_SAMPLES { + r.values[i] = if rhs[i] != 0.0 { + self.values[i] / rhs.values[i] + } else { + 0.0 + } + } + r + } + + pub fn scale(&mut self, factor: Float) { + for v in &mut self.values { + *v *= factor; + } + } +} + +impl PartialEq for DenselySampledSpectrum { + fn eq(&self, other: &Self) -> bool { + if self.lambda_min != other.lambda_min + || self.lambda_max != other.lambda_max + || self.values.len() != other.values.len() + { + return false; + } + + self.values + .iter() + .zip(&other.values) + .all(|(a, b)| a.to_bits() == b.to_bits()) + } +} + +impl Eq for DenselySampledSpectrum {} + +impl Hash for DenselySampledSpectrum { + fn hash(&self, state: &mut H) { + self.lambda_min.hash(state); + self.lambda_max.hash(state); + + for v in &self.values { + v.to_bits().hash(state); + } + } +} + +impl SpectrumProvider for DenselySampledSpectrum { + fn evaluate(&self, lambda: Float) -> Float { + let offset = (lambda.round() as i32) - self.lambda_min; + if offset < 0 || offset as usize >= self.values.len() { + 0.0 + } else { + self.values[offset as usize] + } + } + + fn max_value(&self) -> Float { + self.values.iter().fold(Float::MIN, |a, b| a.max(*b)) + } +} + +#[derive(Debug, Clone)] +pub struct PiecewiseLinearSpectrum { + pub lambdas: Vec, + pub values: Vec, +} + +impl PiecewiseLinearSpectrum { + pub fn from_interleaved(data: &[Float], _normalize: bool) -> Self { + if data.len() % 2 != 0 { + panic!("Interleaved data must have an even number of elements"); + } + + let mut temp: Vec<(Float, Float)> = + data.chunks(2).map(|chunk| (chunk[0], chunk[1])).collect(); + + temp.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + + let (lambdas, values): (Vec, Vec) = temp.into_iter().unzip(); + + // Normalization left to the reader + // This usually involves integrating the curve and scaling 'values' + + Self { lambdas, values } + } + + pub fn read(filepath: &str) -> Option { + let vals = match read_float_file(filepath) { + Ok(v) => v, + Err(_) => Vec::new(), + }; + + if vals.is_empty() { + return None; + } + + if vals.len() % 2 == 0 { + return None; + } + + let count = vals.len() / 2; + let mut lambdas = Vec::with_capacity(count); + let mut values = Vec::with_capacity(count); + + for (_, pair) in vals.chunks(2).enumerate() { + let curr_lambda = pair[0]; + let curr_val = pair[1]; + + if let Some(&prev_lambda) = lambdas.last() { + if curr_lambda <= prev_lambda { + return None; + } + } + + lambdas.push(curr_lambda); + values.push(curr_val); + } + + Some(PiecewiseLinearSpectrum { lambdas, values }) + } +} + +impl SpectrumProvider for PiecewiseLinearSpectrum { + fn evaluate(&self, lambda: Float) -> Float { + if self.lambdas.is_empty() { + return 0.0; + } + + if lambda <= self.lambdas[0] { + return self.values[0]; + } + + if lambda >= *self.lambdas.last().unwrap() { + return *self.values.last().unwrap(); + } + + let i = self.lambdas.partition_point(|&l| l < lambda); + let l0 = self.lambdas[i - 1]; + let l1 = self.lambdas[i]; + let v0 = self.values[i - 1]; + let v1 = self.values[i]; + + let t = (lambda - l0) / (l1 - l0); + + v0 + t * (v1 - v0) + } + + fn max_value(&self) -> Float { + if self.values.is_empty() { + return 0.0; + } + self.values.iter().fold(0.0, |acc, &v| acc.max(v)) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BlackbodySpectrum { + temperature: Float, + normalization_factor: Float, +} + +// Planck's Law +impl BlackbodySpectrum { + const C: Float = 299792458.0; + const H: Float = 6.62606957e-34; + const KB: Float = 1.3806488e-23; + + pub fn new(temperature: Float) -> Self { + // Physical constants + let lambda_max = 2.8977721e-3 / temperature * 1e9; + let max_val = Self::planck_law(lambda_max, temperature); + Self { + temperature, + normalization_factor: if max_val > 0.0 { 1.0 / max_val } else { 0.0 }, + } + } + + fn planck_law(lambda_nm: Float, temp: Float) -> Float { + if temp <= 0.0 { + return 0.0; + } + let lambda_m = lambda_nm * 1e-9; + let c1 = 2.0 * Self::H * Self::C * Self::C; + let c2 = (Self::H * Self::C) / Self::KB; + + let numerator = c1 / lambda_m.powi(5); + let denominator = (c2 / (lambda_m * temp)).exp() - 1.0; + + if denominator.is_infinite() { + 0.0 + } else { + numerator / denominator + } + } + + pub fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { + SampledSpectrum::from_fn(|i| { + Self::planck_law(lambda[i], self.temperature) * self.normalization_factor + }) + } +} + +impl SpectrumProvider for BlackbodySpectrum { + fn evaluate(&self, lambda: Float) -> Float { + Self::planck_law(lambda, self.temperature) * self.normalization_factor + } + + fn max_value(&self) -> Float { + let lambda_max = 2.8977721e-3 / self.temperature * 1e9; + Self::planck_law(lambda_max, self.temperature) + } +} + +pub static NAMED_SPECTRA: LazyLock> = LazyLock::new(|| { + let mut m = HashMap::new(); + + // A macro to reduce boilerplate and make the list readable + macro_rules! add { + ($name:expr, $data:expr, $norm:expr) => { + m.insert( + $name.to_string(), + Spectrum::PiecewiseLinear(PiecewiseLinearSpectrum::from_interleaved($data, $norm)), + ); + }; + } + + add!("stdillum-A", &CIE_ILLUM_A, true); + add!("stdillum-D50", &CIE_ILLUM_D5000, true); + add!("stdillum-D65", &CIE_ILLUM_D6500, true); + add!("stdillum-F1", &CIE_ILLUM_F1, true); + add!("stdillum-F2", &CIE_ILLUM_F2, true); + add!("stdillum-F3", &CIE_ILLUM_F3, true); + add!("stdillum-F4", &CIE_ILLUM_F4, true); + add!("stdillum-F5", &CIE_ILLUM_F5, true); + add!("stdillum-F6", &CIE_ILLUM_F6, true); + add!("stdillum-F7", &CIE_ILLUM_F7, true); + add!("stdillum-F8", &CIE_ILLUM_F8, true); + add!("stdillum-F9", &CIE_ILLUM_F9, true); + add!("stdillum-F10", &CIE_ILLUM_F10, true); + add!("stdillum-F11", &CIE_ILLUM_F11, true); + add!("stdillum-F12", &CIE_ILLUM_F12, true); + add!("illum-acesD60", &ACES_ILLUM_D60, true); + + // --- Glasses --- + add!("glass-BK7", &GLASS_BK7_ETA, false); + add!("glass-BAF10", &GLASS_BAF10_ETA, false); + add!("glass-FK51A", &GLASS_FK51A_ETA, false); + add!("glass-LASF9", &GLASS_LASF9_ETA, false); + add!("glass-F5", &GLASS_SF5_ETA, false); + add!("glass-F10", &GLASS_SF10_ETA, false); + add!("glass-F11", &GLASS_SF11_ETA, false); + + // --- Metals --- + add!("metal-Ag-eta", &AG_ETA, false); + add!("metal-Ag-k", &AG_K, false); + add!("metal-Al-eta", &AL_ETA, false); + add!("metal-Al-k", &AL_K, false); + add!("metal-Au-eta", &AU_ETA, false); + add!("metal-Au-k", &AU_K, false); + add!("metal-Cu-eta", &CU_ETA, false); + add!("metal-Cu-k", &CU_K, false); + add!("metal-CuZn-eta", &CUZN_ETA, false); + add!("metal-CuZn-k", &CUZN_K, false); + add!("metal-MgO-eta", &MGO_ETA, false); + add!("metal-MgO-k", &MGO_K, false); + add!("metal-TiO2-eta", &TIO2_ETA, false); + add!("metal-TiO2-k", &TIO2_K, false); + + // --- Canon EOS 100D --- + add!("canon_eos_100d_r", &CANON_EOS_100D_R, false); + add!("canon_eos_100d_g", &CANON_EOS_100D_G, false); + add!("canon_eos_100d_b", &CANON_EOS_100D_B, false); + + // --- Canon EOS 1DX MkII --- + add!("canon_eos_1dx_mkii_r", &CANON_EOS_1DX_MKII_R, false); + add!("canon_eos_1dx_mkii_g", &CANON_EOS_1DX_MKII_G, false); + add!("canon_eos_1dx_mkii_b", &CANON_EOS_1DX_MKII_B, false); + + // --- Canon EOS 200D --- + add!("canon_eos_200d_r", &CANON_EOS_200D_R, false); + add!("canon_eos_200d_g", &CANON_EOS_200D_G, false); + add!("canon_eos_200d_b", &CANON_EOS_200D_B, false); + + // --- Canon EOS 200D MkII --- + add!("canon_eos_200d_mkii_r", &CANON_EOS_200D_MKII_R, false); + add!("canon_eos_200d_mkii_g", &CANON_EOS_200D_MKII_G, false); + add!("canon_eos_200d_mkii_b", &CANON_EOS_200D_MKII_B, false); + + // --- Canon EOS 5D --- + add!("canon_eos_5d_r", &CANON_EOS_5D_R, false); + add!("canon_eos_5d_g", &CANON_EOS_5D_G, false); + add!("canon_eos_5d_b", &CANON_EOS_5D_B, false); + + // --- Canon EOS 5D MkII --- + add!("canon_eos_5d_mkii_r", &CANON_EOS_5D_MKII_R, false); + add!("canon_eos_5d_mkii_g", &CANON_EOS_5D_MKII_G, false); + add!("canon_eos_5d_mkii_b", &CANON_EOS_5D_MKII_B, false); + + // --- Canon EOS 5D MkIII --- + add!("canon_eos_5d_mkiii_r", &CANON_EOS_5D_MKIII_R, false); + add!("canon_eos_5d_mkiii_g", &CANON_EOS_5D_MKIII_G, false); + add!("canon_eos_5d_mkiii_b", &CANON_EOS_5D_MKIII_B, false); + + // --- Canon EOS 5D MkIV --- + add!("canon_eos_5d_mkiv_r", &CANON_EOS_5D_MKIV_R, false); + add!("canon_eos_5d_mkiv_g", &CANON_EOS_5D_MKIV_G, false); + add!("canon_eos_5d_mkiv_b", &CANON_EOS_5D_MKIV_B, false); + + // --- Canon EOS 5DS --- + add!("canon_eos_5ds_r", &CANON_EOS_5DS_R, false); + add!("canon_eos_5ds_g", &CANON_EOS_5DS_G, false); + add!("canon_eos_5ds_b", &CANON_EOS_5DS_B, false); + + // --- Canon EOS M --- + add!("canon_eos_m_r", &CANON_EOS_M_R, false); + add!("canon_eos_m_g", &CANON_EOS_M_G, false); + add!("canon_eos_m_b", &CANON_EOS_M_B, false); + + // --- Hasselblad L1D 20C --- + add!("hasselblad_l1d_20c_r", &HASSELBLAD_L1D_20C_R, false); + add!("hasselblad_l1d_20c_g", &HASSELBLAD_L1D_20C_G, false); + add!("hasselblad_l1d_20c_b", &HASSELBLAD_L1D_20C_B, false); + + // --- Nikon D810 --- + add!("nikon_d810_r", &NIKON_D810_R, false); + add!("nikon_d810_g", &NIKON_D810_G, false); + add!("nikon_d810_b", &NIKON_D810_B, false); + + // --- Nikon D850 --- + add!("nikon_d850_r", &NIKON_D850_R, false); + add!("nikon_d850_g", &NIKON_D850_G, false); + add!("nikon_d850_b", &NIKON_D850_B, false); + + // --- Sony ILCE 6400 --- + add!("sony_ilce_6400_r", &SONY_ILCE_6400_R, false); + add!("sony_ilce_6400_g", &SONY_ILCE_6400_G, false); + add!("sony_ilce_6400_b", &SONY_ILCE_6400_B, false); + + // --- Sony ILCE 7M3 --- + add!("sony_ilce_7m3_r", &SONY_ILCE_7M3_R, false); + add!("sony_ilce_7m3_g", &SONY_ILCE_7M3_G, false); + add!("sony_ilce_7m3_b", &SONY_ILCE_7M3_B, false); + + // --- Sony ILCE 7RM3 --- + add!("sony_ilce_7rm3_r", &SONY_ILCE_7RM3_R, false); + add!("sony_ilce_7rm3_g", &SONY_ILCE_7RM3_G, false); + add!("sony_ilce_7rm3_b", &SONY_ILCE_7RM3_B, false); + + // --- Sony ILCE 9 --- + add!("sony_ilce_9_r", &SONY_ILCE_9_R, false); + add!("sony_ilce_9_g", &SONY_ILCE_9_G, false); + add!("sony_ilce_9_b", &SONY_ILCE_9_B, false); + + m +}); + +pub fn get_named_spectrum(name: &str) -> Option { + NAMED_SPECTRA.get(name).cloned() +} diff --git a/src/utils/containers.rs b/shared/src/utils/containers.rs similarity index 90% rename from src/utils/containers.rs rename to shared/src/utils/containers.rs index 0572719..d38d3db 100644 --- a/src/utils/containers.rs +++ b/shared/src/utils/containers.rs @@ -1,10 +1,12 @@ -use crate::core::pbrt::{Float, lerp}; +use crate::core::pbrt::Float; +use crate::utils::math::lerp; +use std::collections::HashSet; use std::collections::hash_map::RandomState; use std::hash::{BuildHasher, Hash, Hasher}; use std::ops::{Add, Index, IndexMut, Mul, Sub}; -use std::sync::RwLock; +use std::sync::{Arc, Mutex, RwLock}; -use crate::geometry::{ +use crate::core::geometry::{ Bounds2i, Bounds3f, Bounds3i, Point2i, Point3f, Point3i, Vector2i, Vector3f, Vector3i, }; @@ -92,6 +94,14 @@ impl Array2D { let pp = Point2i::new(p.x() - self.extent.p_min.x(), p.y() - self.extent.p_min.y()); (pp.y() * width as i32 + pp.x()) as usize } + + pub fn get_linear(&self, index: usize) -> &T { + &self.values[index] + } + + pub fn get_linear_mut(&mut self, index: usize) -> &mut T { + &mut self.values[index] + } } impl Index for Array2D { @@ -305,3 +315,30 @@ impl SampledGrid { self.max_value_convert(bounds, |v| v.clone()) } } + +pub struct InternCache { + cache: Mutex>>, +} + +impl InternCache +where + T: Eq + Hash + Clone, +{ + pub fn new() -> Self { + Self { + cache: Mutex::new(HashSet::new()), + } + } + + pub fn lookup(&self, value: T) -> Arc { + let mut lock = self.cache.lock().unwrap(); + + if let Some(existing) = lock.get(&value) { + return existing.clone(); // Returns a cheap Arc copy + } + + let new_item = Arc::new(value); + lock.insert(new_item.clone()); + new_item + } +} diff --git a/shared/src/utils/error.rs b/shared/src/utils/error.rs new file mode 100644 index 0000000..3383b3f --- /dev/null +++ b/shared/src/utils/error.rs @@ -0,0 +1,36 @@ +use image_rs::{ImageError as IError, error}; +use std::fmt; +use std::sync::Arc; +use thiserror::Error; + +use crate::image::PixelFormat; + +#[derive(Error, Debug)] +pub enum LlsError { + SingularMatrix, +} + +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum InversionError { + SingularMatrix, + EmptyMatrix, +} + +impl fmt::Display for LlsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + LlsError::SingularMatrix => write!(f, "Matrix is singular and cannot be inverted."), + } + } +} + +impl fmt::Display for InversionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + InversionError::SingularMatrix => { + write!(f, "Matrix is singular and cannot be inverted.") + } + InversionError::EmptyMatrix => write!(f, "Matrix is empty and cannot be inverted."), + } + } +} diff --git a/src/utils/hash.rs b/shared/src/utils/hash.rs similarity index 100% rename from src/utils/hash.rs rename to shared/src/utils/hash.rs diff --git a/src/utils/interval.rs b/shared/src/utils/interval.rs similarity index 99% rename from src/utils/interval.rs rename to shared/src/utils/interval.rs index 6c135b0..f5ed838 100644 --- a/src/utils/interval.rs +++ b/shared/src/utils/interval.rs @@ -3,6 +3,7 @@ use crate::utils::math::{next_float_down, next_float_up}; use num_traits::Zero; use std::ops::{Add, Div, Mul, Neg, Sub}; +#[repr(C)] #[derive(Debug, Copy, Clone, PartialEq)] pub struct Interval { pub low: Float, diff --git a/src/utils/math.rs b/shared/src/utils/math.rs similarity index 97% rename from src/utils/math.rs rename to shared/src/utils/math.rs index 22c2aaf..151579d 100644 --- a/src/utils/math.rs +++ b/shared/src/utils/math.rs @@ -1,10 +1,7 @@ -use super::color::{RGB, XYZ}; use super::error::{InversionError, LlsError}; -use crate::core::pbrt::{ - Float, FloatBitOps, FloatBits, ONE_MINUS_EPSILON, PI, PI_OVER_4, clamp_t, evaluate_polynomial, - lerp, -}; -use crate::geometry::{Point, Point2f, Point2i, Vector, Vector3f, VectorLike}; +use crate::core::geometry::{Lerp, 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}; @@ -42,6 +39,43 @@ where a * b + c } +pub fn clamp(val: T, low: T, high: T) -> T +where + T: PartialOrd, +{ + let r: T; + if val < low { + r = low; + } else if val > high { + r = high; + } else { + r = val; + } + r +} + +#[inline] +pub fn lerp(t: F, a: T, b: T) -> T +where + T: Lerp, + F: Copy, +{ + T::lerp(t, a, b) +} + +#[inline] +pub fn evaluate_polynomial(t: Float, coeffs: &[Float]) -> Option { + if coeffs.is_empty() { + return None; + } + + let mut result = coeffs[0]; + for &c in &coeffs[1..] { + result = t.mul_add(result, c); + } + Some(result) +} + #[inline] pub fn difference_of_products(a: T, b: T, c: T, d: T) -> T where @@ -75,7 +109,7 @@ pub fn safe_asin(x: T) -> T { let epsilon = T::from(0.0001).unwrap(); let one = T::one(); if x >= -(one + epsilon) && x <= one + epsilon { - clamp_t(x, -one, one).asin() + clamp(x, -one, one).asin() } else { panic!("Not valid value for asin") } @@ -84,7 +118,7 @@ pub fn safe_asin(x: T) -> T { #[inline] pub fn safe_acos(x: Float) -> Float { if (-1.001..1.001).contains(&x) { - clamp_t(x, -1., 1.).asin() + clamp(x, -1., 1.).asin() } else { panic!("Not valid value for acos") } @@ -236,7 +270,7 @@ pub fn smooth_step(x: Float, a: Float, b: Float) -> Float { if x < a { return 0. } else { return 1. } } - let t = clamp_t((x - a) / (b - a), 0., 1.); + let t = clamp((x - a) / (b - a), 0., 1.); t * t * (3. - 2. * t) } @@ -1281,6 +1315,19 @@ impl SquareMatrix { } } +impl From<&[T; 16]> for SquareMatrix { + fn from(flat: &[T; 16]) -> Self { + Self { + m: [ + [flat[0], flat[1], flat[2], flat[3]], + [flat[4], flat[5], flat[6], flat[7]], + [flat[8], flat[9], flat[10], flat[11]], + [flat[12], flat[13], flat[14], flat[15]], + ], + } + } +} + impl SquareMatrix where T: NumFloat + Sum + Product + Copy, diff --git a/src/utils/mesh.rs b/shared/src/utils/mesh.rs similarity index 95% rename from src/utils/mesh.rs rename to shared/src/utils/mesh.rs index 5b53f84..122b126 100644 --- a/src/utils/mesh.rs +++ b/shared/src/utils/mesh.rs @@ -1,7 +1,7 @@ +use crate::core::geometry::{Normal3f, Point2f, Point3f, Vector3f}; use crate::core::pbrt::Float; -use crate::geometry::{Normal3f, Point2f, Point3f, Vector3f}; use crate::utils::sampling::PiecewiseConstant2D; -use crate::utils::transform::Transform; +use crate::utils::transform::TransformGeneric; use std::sync::Arc; #[derive(Debug, Clone)] @@ -21,7 +21,7 @@ pub struct TriangleMesh { impl TriangleMesh { #[allow(clippy::too_many_arguments)] pub fn new( - render_from_object: &Transform, + render_from_object: &TransformGeneric, reverse_orientation: bool, indices: Vec, mut p: Vec, @@ -108,7 +108,7 @@ pub struct BilinearPatchMesh { impl BilinearPatchMesh { pub fn new( - render_from_object: &Transform, + render_from_object: &TransformGeneric, reverse_orientation: bool, indices: Vec, mut p: Vec, diff --git a/shared/src/utils/mod.rs b/shared/src/utils/mod.rs new file mode 100644 index 0000000..90473f5 --- /dev/null +++ b/shared/src/utils/mod.rs @@ -0,0 +1,84 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +pub mod containers; +pub mod error; +pub mod file; +pub mod hash; +pub mod interval; +pub mod math; +pub mod mesh; +pub mod mipmap; +pub mod parameters; +pub mod parser; +pub mod quaternion; +pub mod rng; +pub mod sampling; +pub mod sobol; +pub mod splines; +pub mod strings; +pub mod transform; + +pub use file::{read_float_file, resolve_filename}; +pub use strings::*; +pub use transform::{AnimatedTransform, Transform, TransformGeneric}; + +#[inline] +pub fn partition_slice(data: &mut [T], predicate: F) -> usize +where + F: Fn(&T) -> bool, +{ + let mut i = 0; + for j in 0..data.len() { + if predicate(&data[j]) { + data.swap(i, j); + i += 1; + } + } + i +} + +#[derive(Debug)] +pub struct AtomicFloat { + bits: AtomicU64, +} + +impl AtomicFloat { + pub fn new(value: f64) -> Self { + Self { + bits: AtomicU64::new(value.to_bits()), + } + } + + pub fn load(&self) -> f64 { + f64::from_bits(self.bits.load(Ordering::Relaxed)) + } + + pub fn store(&self, value: f64) { + self.bits.store(value.to_bits(), Ordering::Relaxed); + } + + pub fn add(&self, value: f64) { + let mut current_bits = self.bits.load(Ordering::Relaxed); + loop { + let current_val = f64::from_bits(current_bits); + let new_val = current_val + value; + 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, + } + } + } +} + +impl Default for AtomicFloat { + fn default() -> Self { + Self::new(0.0) + } +} diff --git a/src/utils/quaternion.rs b/shared/src/utils/quaternion.rs similarity index 98% rename from src/utils/quaternion.rs rename to shared/src/utils/quaternion.rs index ec1fc3c..375e473 100644 --- a/src/utils/quaternion.rs +++ b/shared/src/utils/quaternion.rs @@ -2,8 +2,8 @@ use std::f32::consts::PI; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use std::ops::{Index, IndexMut}; +use crate::core::geometry::{Vector3f, VectorLike}; use crate::core::pbrt::Float; -use crate::geometry::{Vector3f, VectorLike}; use crate::utils::math::{safe_asin, sinx_over_x}; #[derive(Copy, Clone, Debug, PartialEq)] diff --git a/src/utils/rng.rs b/shared/src/utils/rng.rs similarity index 100% rename from src/utils/rng.rs rename to shared/src/utils/rng.rs diff --git a/src/utils/sampling.rs b/shared/src/utils/sampling.rs similarity index 97% rename from src/utils/sampling.rs rename to shared/src/utils/sampling.rs index 8ae1435..90edcdb 100644 --- a/src/utils/sampling.rs +++ b/shared/src/utils/sampling.rs @@ -1,21 +1,41 @@ -use super::math::safe_sqrt; use crate::check_rare; -use crate::core::pbrt::{ - Float, INV_2_PI, INV_4_PI, INV_PI, ONE_MINUS_EPSILON, PI, PI_OVER_2, PI_OVER_4, clamp_t, - evaluate_polynomial, find_interval, lerp, -}; -use crate::core::pbrt::{RARE_EVENT_CONDITION_MET, RARE_EVENT_TOTAL_CALLS}; -use crate::geometry::{ +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, difference_of_products, logistic, newton_bisection, square, - sum_of_products, + catmull_rom_weights, clamp, difference_of_products, evaluate_polynomial, lerp, logistic, + newton_bisection, safe_sqrt, square, sum_of_products, }; use crate::utils::rng::Rng; +use num_traits::Num; use std::sync::atomic::{AtomicU64, Ordering as SyncOrdering}; +pub fn linear_pdf(x: T, a: T, b: T) -> T +where + T: Num + Copy + PartialOrd, +{ + if x < T::zero() || x > T::one() { + return T::zero(); + } + (T::one() + T::one()) * lerp(x, a, b) / (a + b) +} + +// pub fn sample_linear(u: T, a: T, b: T) -> T +// where +// T: Num, +// { +// if u == T::zero() && a == T::zero() { +// return T::zero(); +// } +// +// u * (a + b) +// } + pub fn sample_linear(u: Float, a: Float, b: Float) -> Float { if u == 0. && a == 0. { return 0.; @@ -181,10 +201,10 @@ pub fn sample_spherical_rectangle( let au = u[0] * (g0 + g1 - 2. * PI) + (u[0] - 1.) * (g2 + g3); let fu = (au.cos() * b0 - b1) / au.sin(); let mut cu = (1. / (square(fu) + square(b0)).sqrt()).copysign(fu); - cu = clamp_t(cu, -ONE_MINUS_EPSILON, ONE_MINUS_EPSILON); + cu = clamp(cu, -ONE_MINUS_EPSILON, ONE_MINUS_EPSILON); let mut xu = -(cu * z0) / safe_sqrt(1. - square(cu)); - xu = clamp_t(xu, x0, x1); + xu = clamp(xu, x0, x1); // Find _xv_ along $y$ edge for spherical rectangle sample let dd = (square(xu) + square(z0)).sqrt(); @@ -262,7 +282,7 @@ pub fn invert_spherical_rectangle_sample( let v = r.to_local(p_rect - p_ref); let mut xu = v.x(); let yv = v.y(); - xu = clamp_t(xu, x0, x1); + xu = clamp(xu, x0, x1); if xu == 0. { xu = 1e-10; } @@ -304,9 +324,9 @@ pub fn invert_spherical_rectangle_sample( let hvsq = [square(hv[0]), square(hv[1])]; let yz = [(hv[0] * dd) / (1. - hvsq[0]), (hv[1] * dd) / (1. - hvsq[1])]; if (yz[0] - yv).abs() < (yz[1] - yv).abs() { - Point2f::new(clamp_t(u0, 0., 1.), u1[0]) + Point2f::new(clamp(u0, 0., 1.), u1[0]) } else { - Point2f::new(clamp_t(u0, 0., 1.), u1[1]) + Point2f::new(clamp(u0, 0., 1.), u1[1]) } } @@ -358,7 +378,7 @@ pub fn sample_spherical_triangle( let k2 = sin_phi - sin_alpha * a.dot(b); let mut cos_bp = (k2 + difference_of_products(k2, cos_phi, k1, sin_phi) * cos_alpha) / ((sum_of_products(k2, sin_phi, k1, cos_phi)) * sin_alpha); - cos_bp = clamp_t(cos_bp, -1., 1.); + cos_bp = clamp(cos_bp, -1., 1.); // Sample c' along the arc between a and c let sin_bp = safe_sqrt(1. - square(cos_bp)); let cp = cos_bp * a + sin_bp * c.gram_schmidt(a).normalize(); @@ -376,8 +396,8 @@ pub fn sample_spherical_triangle( let mut b1 = s.dot(s1) * inv_divisor; let mut b2 = w.dot(s.cross(e1)) * inv_divisor; - b1 = clamp_t(b1, 0., 1.); - b2 = clamp_t(b2, 0., 1.); + b1 = clamp(b1, 0., 1.); + b2 = clamp(b2, 0., 1.); if b1 + b2 > 1. { b1 /= b1 + b2; b2 /= b1 + b2; @@ -443,7 +463,7 @@ pub fn invert_spherical_triangle_sample( let u1 = (1.0 - w.dot(b)) / (1.0 - cp.dot(b)); - Some(Point2f::new(clamp_t(u0, 0.0, 1.0), clamp_t(u1, 0.0, 1.0))) + Some(Point2f::new(clamp(u0, 0.0, 1.0), clamp(u1, 0.0, 1.0))) } pub fn sample_catmull_rom( @@ -611,7 +631,7 @@ pub fn sample_trimmed_logistic(u: Float, s: Float, a: Float, b: Float) -> Float let p = |val: Float| invert_logistic_sample(val, s); let u = lerp(u, p(a), p(b)); let x = sample_logistic(u, s); - clamp_t(x, a, b) + clamp(x, a, b) } pub fn uniform_hemisphere_pdf() -> Float { @@ -1027,7 +1047,7 @@ impl WindowedPiecewiseConstant2D { numerator / denominator }; - let res = crate::core::pbrt::lerp(t, min, max); + let res = lerp(t, min, max); res.clamp(min, max) } } diff --git a/src/utils/sobol.rs b/shared/src/utils/sobol.rs similarity index 100% rename from src/utils/sobol.rs rename to shared/src/utils/sobol.rs diff --git a/src/utils/splines.rs b/shared/src/utils/splines.rs similarity index 93% rename from src/utils/splines.rs rename to shared/src/utils/splines.rs index 24893f2..2a25218 100644 --- a/src/utils/splines.rs +++ b/shared/src/utils/splines.rs @@ -1,5 +1,6 @@ -use crate::core::pbrt::{Float, lerp}; -use crate::geometry::{Bounds3f, Lerp, Point3f, Vector3f, VectorLike}; +use crate::core::geometry::{Bounds3f, Lerp, Point3f, Vector3f, VectorLike}; +use crate::core::pbrt::Float; +use crate::utils::math::lerp; use num_traits::Num; fn bounds_cubic_bezier(cp: &[Point3f]) -> Bounds3f { diff --git a/shared/src/utils/strings.rs b/shared/src/utils/strings.rs new file mode 100644 index 0000000..eafbc77 --- /dev/null +++ b/shared/src/utils/strings.rs @@ -0,0 +1,5 @@ +use unicode_normalization::UnicodeNormalization; + +pub fn normalize_utf8(input: &str) -> String { + input.nfc().collect::() +} diff --git a/src/utils/transform.rs b/shared/src/utils/transform.rs similarity index 96% rename from src/utils/transform.rs rename to shared/src/utils/transform.rs index 4ddc812..8f2959c 100644 --- a/src/utils/transform.rs +++ b/shared/src/utils/transform.rs @@ -5,26 +5,26 @@ use std::iter::{Product, Sum}; use std::ops::{Add, Div, Index, IndexMut, Mul}; use std::sync::Arc; -use super::color::{RGB, XYZ}; -use super::math::{SquareMatrix, radians, safe_acos}; +use super::math::{radians, safe_acos, SquareMatrix}; use super::quaternion::Quaternion; -use crate::core::interaction::{ - Interaction, InteractionData, InteractionTrait, MediumInteraction, SurfaceInteraction, -}; -use crate::core::pbrt::{Float, gamma}; -use crate::geometry::{ +use crate::core::geometry::{ Bounds3f, Normal, Normal3f, Point, Point3f, Point3fi, Ray, Vector, Vector3f, Vector3fi, VectorLike, }; +use crate::core::interaction::{ + Interaction, InteractionData, InteractionTrait, MediumInteraction, SurfaceInteraction, +}; +use crate::core::pbrt::{gamma, Float}; +use crate::spectra::{RGB, XYZ}; use crate::utils::error::InversionError; #[derive(Debug, Copy, Clone)] -pub struct Transform { +pub struct TransformGeneric { m: SquareMatrix, m_inv: SquareMatrix, } -impl Transform { +impl TransformGeneric { pub fn new(m: SquareMatrix, m_inv: SquareMatrix) -> Self { Self { m, m_inv } } @@ -34,6 +34,12 @@ impl Transform { Ok(Self { m, m_inv: inv }) } + pub fn from_flat(flat: &[T; 16]) -> Result { + let m: SquareMatrix = SquareMatrix::from(flat); + let inv = m.inverse()?; + Ok(Self { m, m_inv: inv }) + } + pub fn identity() -> Self { let m: SquareMatrix = SquareMatrix::identity(); Self { m, m_inv: m } @@ -83,13 +89,15 @@ impl Transform { } } -impl Default for Transform { +impl Default for TransformGeneric { fn default() -> Self { Self::identity() } } -impl Transform { +pub type Transform = TransformGeneric; + +impl TransformGeneric { pub fn apply_to_point(&self, p: Point3f) -> Point3f { let x = p.x(); let y = p.y(); @@ -407,7 +415,7 @@ impl Transform { } pub fn translate(delta: Vector3f) -> Self { - Transform { + TransformGeneric { m: SquareMatrix::new([ [1.0, 0.0, 0.0, delta.x()], [0.0, 1.0, 0.0, delta.y()], @@ -440,7 +448,11 @@ impl Transform { Self { m, m_inv } } - pub fn perspective(fov: Float, n: Float, f: Float) -> Result, InversionError> { + pub fn perspective( + fov: Float, + n: Float, + f: Float, + ) -> Result, InversionError> { let persp: SquareMatrix = SquareMatrix::new([ [1., 0., 0., 0.], [0., 1., 0., 0.], @@ -448,22 +460,23 @@ impl Transform { [0., 0., 1., 0.], ]); let inv_tan_ang = 1. / (radians(fov) / 2.).tan(); - let persp_transform = Transform::from_matrix(persp)?; - Ok(Transform::scale(inv_tan_ang, inv_tan_ang, 1.) * persp_transform) + let persp_transform = TransformGeneric::from_matrix(persp)?; + Ok(TransformGeneric::scale(inv_tan_ang, inv_tan_ang, 1.) * persp_transform) } pub fn orthographic(z_near: Float, z_far: Float) -> Self { Self::scale(1., 1., 1. / (z_far - z_near)) * Self::translate(Vector3f::new(0., 0., -z_near)) } - pub fn rotate_around_axis(theta: Float, axis: Vector3f) -> Self { + pub fn rotate_around_axis(theta: Float, axis: impl Into) -> Self { let sin_theta = theta.to_radians().sin(); let cos_theta = theta.to_radians().cos(); - Transform::rotate(sin_theta, cos_theta, axis) + TransformGeneric::rotate(sin_theta, cos_theta, axis) } - pub fn rotate(sin_theta: Float, cos_theta: Float, axis: Vector3f) -> Self { - let a = axis.normalize(); + pub fn rotate(sin_theta: Float, cos_theta: Float, axis: impl Into) -> Self { + let vec_axis: Vector3f = axis.into(); + let a = vec_axis.normalize(); let mut m: SquareMatrix = SquareMatrix::default(); m[0][0] = a.x() * a.x() + (1. - a.x() * a.x()) * cos_theta; m[0][1] = a.x() * a.y() * (1. - cos_theta) - a.z() * sin_theta; @@ -479,7 +492,7 @@ impl Transform { m[2][1] = a.y() * a.z() * (1. - cos_theta) + a.x() * sin_theta; m[2][2] = a.z() * a.z() + (1. - a.z() * a.z()) * cos_theta; m[2][3] = 0.; - Transform::new(m, m.transpose()) + TransformGeneric::new(m, m.transpose()) } pub fn rotate_from_to(from: Vector3f, to: Vector3f) -> Self { @@ -506,7 +519,7 @@ impl Transform { + 4. * uv / (uu * vv) * v[i] * u[j]; } } - Transform::new(r, r.transpose()) + TransformGeneric::new(r, r.transpose()) } pub fn rotate_x(theta: Float) -> Self { @@ -609,47 +622,47 @@ impl Transform { } } -impl PartialEq for Transform { +impl PartialEq for TransformGeneric { fn eq(&self, other: &Self) -> bool { self.m == other.m && self.m_inv == other.m_inv } } -impl Mul for Transform { - type Output = Transform; +impl Mul for TransformGeneric { + type Output = TransformGeneric; - fn mul(self, rhs: Transform) -> Self::Output { - Transform { + fn mul(self, rhs: TransformGeneric) -> Self::Output { + TransformGeneric { m: self.m * rhs.m, m_inv: rhs.m_inv * self.m_inv, } } } -impl<'b, T> Mul<&'b Transform> for &Transform +impl<'b, T> Mul<&'b TransformGeneric> for &TransformGeneric where T: NumFloat, { - type Output = Transform; - fn mul(self, rhs: &'b Transform) -> Self::Output { - Transform { + type Output = TransformGeneric; + fn mul(self, rhs: &'b TransformGeneric) -> Self::Output { + TransformGeneric { m: self.m * rhs.m, m_inv: rhs.m_inv * self.m_inv, } } } -impl Mul> for Float { - type Output = Transform; - fn mul(self, rhs: Transform) -> Self::Output { - Transform { +impl Mul> for Float { + type Output = TransformGeneric; + fn mul(self, rhs: TransformGeneric) -> Self::Output { + TransformGeneric { m: rhs.m * self, m_inv: rhs.m_inv * self, } } } -impl Mul> for Transform +impl Mul> for TransformGeneric where T: NumFloat, { @@ -668,7 +681,7 @@ where } } -impl Mul> for Transform +impl Mul> for TransformGeneric where T: NumFloat, { @@ -687,7 +700,7 @@ where } } -impl Mul> for Transform +impl Mul> for TransformGeneric where T: NumFloat, { @@ -705,7 +718,7 @@ where } } } -impl From for Transform { +impl From for TransformGeneric { fn from(q: Quaternion) -> Self { let xx = q.v.x() * q.v.x(); let yy = q.v.y() * q.v.y(); @@ -737,7 +750,7 @@ impl From for Transform { // For a pure rotation, the inverse is the transpose. let m_inv_sq = m_sq.transpose(); - Transform::new(m_sq, m_inv_sq) + TransformGeneric::new(m_sq, m_inv_sq) } } @@ -764,10 +777,10 @@ impl DerivativeTerm { } } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct AnimatedTransform { - pub start_transform: Transform, - pub end_transform: Transform, + pub start_transform: Transform, + pub end_transform: Transform, pub start_time: Float, pub end_time: Float, actually_animated: bool, @@ -783,10 +796,14 @@ pub struct AnimatedTransform { } impl AnimatedTransform { + pub fn from_transform(t: &Transform) -> Self { + Self::new(t, 0., t, 1.) + } + pub fn new( - start_transform: &Transform, + start_transform: &Transform, start_time: Float, - end_transform: &Transform, + end_transform: &Transform, end_time: Float, ) -> Self { let actually_animated = start_transform != end_transform; @@ -812,12 +829,14 @@ impl AnimatedTransform { let (t0, r_temp, s0) = start_transform.decompose(); let mut rm = r_temp; - let scale_transform = Transform::from_matrix(rm).expect("Scaling matrix should invertible"); + let scale_transform = + TransformGeneric::from_matrix(rm).expect("Scaling matrix should invertible"); let r0 = scale_transform.to_quaternion(); let (t1, r_temp, s1) = end_transform.decompose(); rm = r_temp; - let scale_transform = Transform::from_matrix(rm).expect("Scaling matrix should invertible"); + let scale_transform = + TransformGeneric::from_matrix(rm).expect("Scaling matrix should invertible"); let mut r1 = scale_transform.to_quaternion(); if r0.dot(r1) < 0. { @@ -1993,7 +2012,7 @@ impl AnimatedTransform { t.apply_to_interaction(si) } - pub fn interpolate(&self, time: Float) -> Transform { + pub fn interpolate(&self, time: Float) -> TransformGeneric { if !self.actually_animated || time <= self.start_time { return self.start_transform; } @@ -2008,9 +2027,9 @@ impl AnimatedTransform { let scale = (1.0 - dt) * self.s[0] + dt * self.s[1]; let scale_transform = - Transform::from_matrix(scale).expect("Scale matrix is not inversible"); + TransformGeneric::from_matrix(scale).expect("Scale matrix is not inversible"); - Transform::translate(trans) * Transform::from(rotate) * scale_transform + TransformGeneric::translate(trans) * TransformGeneric::from(rotate) * scale_transform } pub fn apply_inverse_point(&self, p: Point3f, time: Float) -> Point3f { @@ -2042,15 +2061,22 @@ impl AnimatedTransform { .apply_to_bounds(*b) .union(self.end_transform.apply_to_bounds(*b)) } + + pub fn is_animated(&self) -> bool { + self.actually_animated + } } pub fn look_at( - pos: Point3f, - look: Point3f, - up: Point3f, -) -> Result, InversionError> { + pos: impl Into, + look: impl Into, + up: impl Into, +) -> Result, InversionError> { let mut world_from_camera: SquareMatrix = SquareMatrix::default(); // Initialize fourth column of viewing matrix + let pos: Point3f = pos.into(); + let look: Point3f = look.into(); + let up: Point3f = up.into(); world_from_camera[0][3] = pos.x(); world_from_camera[1][3] = pos.y(); world_from_camera[2][3] = pos.z(); @@ -2085,5 +2111,5 @@ pub fn look_at( world_from_camera[3][2] = 0.; let camera_from_world = world_from_camera.inverse()?; - Ok(Transform::new(camera_from_world, world_from_camera)) + Ok(TransformGeneric::new(camera_from_world, world_from_camera)) } diff --git a/src/camera/spherical.rs b/src/camera/spherical.rs deleted file mode 100644 index 5e6ae75..0000000 --- a/src/camera/spherical.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::{CameraBase, CameraRay, CameraTrait}; -use crate::core::film::FilmTrait; -use crate::core::pbrt::{Float, PI}; -use crate::core::sampler::CameraSample; -use crate::geometry::{Bounds2f, Point2f, Point3f, Ray, Vector3f, spherical_direction}; -use crate::spectra::{SampledSpectrum, SampledWavelengths}; -use crate::utils::math::{equal_area_square_to_sphere, wrap_equal_area_square}; - -#[derive(Debug, PartialEq)] -pub struct EquiRectangularMapping; - -#[derive(Debug, PartialEq)] -pub enum Mapping { - EquiRectangular(EquiRectangularMapping), -} - -#[derive(Debug)] -pub struct SphericalCamera { - pub base: CameraBase, - pub screen: Bounds2f, - pub lens_radius: Float, - pub focal_distance: Float, - pub mapping: Mapping, -} - -impl CameraTrait for SphericalCamera { - fn base(&self) -> &CameraBase { - &self.base - } - - fn init_metadata(&self, metadata: &mut crate::image::ImageMetadata) { - self.base.init_metadata(metadata) - } - - fn generate_ray( - &self, - sample: CameraSample, - _lamdba: &SampledWavelengths, - ) -> Option { - // Compute spherical camera ray direction - let mut uv = Point2f::new( - sample.p_film.x() / self.base().film.full_resolution().x() as Float, - sample.p_film.y() / self.base().film.full_resolution().y() as Float, - ); - let dir: Vector3f; - if self.mapping == Mapping::EquiRectangular(EquiRectangularMapping) { - // 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); - } else { - // Compute ray direction using equal area mapping - uv = wrap_equal_area_square(&mut uv); - dir = equal_area_square_to_sphere(uv); - } - std::mem::swap(&mut dir.y(), &mut dir.z()); - - let ray = Ray::new( - Point3f::new(0., 0., 0.), - dir, - Some(self.sample_time(sample.time)), - self.base().medium.clone(), - ); - Some(CameraRay { - ray: self.render_from_camera(&ray, &mut None), - weight: SampledSpectrum::default(), - }) - } -} diff --git a/src/core/geometry/cone.rs b/src/core/geometry/cone.rs deleted file mode 100644 index 3fb84f3..0000000 --- a/src/core/geometry/cone.rs +++ /dev/null @@ -1,116 +0,0 @@ -use super::{Bounds3f, Float, PI, Point3f, Vector3f, VectorLike}; -use crate::utils::math::{degrees, safe_acos, safe_asin, safe_sqrt, square}; -use crate::utils::transform::Transform; - -#[derive(Debug, Clone)] -pub struct DirectionCone { - pub w: Vector3f, - pub cos_theta: Float, -} - -impl Default for DirectionCone { - fn default() -> Self { - Self { - w: Vector3f::zero(), - cos_theta: Float::INFINITY, - } - } -} - -impl DirectionCone { - pub fn new(w: Vector3f, cos_theta: Float) -> Self { - Self { - w: w.normalize(), - cos_theta, - } - } - - pub fn new_from_vector(w: Vector3f) -> Self { - Self::new(w, 1.0) - } - - pub fn is_empty(&self) -> bool { - self.cos_theta == Float::INFINITY - } - - pub fn entire_sphere() -> Self { - Self::new(Vector3f::new(0., 0., 1.), -1.) - } - - pub fn closest_vector_income(&self, wt: Vector3f) -> Vector3f { - let wp = wt.normalize(); - let w = self.w; - if wp.dot(w) > self.cos_theta { - return wp; - } - - let sin_theta = -safe_sqrt(1. - self.cos_theta * self.cos_theta); - let a = wp.cross(w); - self.cos_theta * w - + sin_theta / a.norm() - * Vector3f::new( - w.x() - * (wp.y() * w.y() + wp.z() * w.z() - - wp.x() * (square(w.y() + square(w.z())))), - w.y() - * (wp.x() * w.x() + wp.z() * w.z() - - wp.y() * (square(w.x() + square(w.z())))), - w.z() - * (wp.x() * w.x() + wp.y() * w.y() - - wp.z() * (square(w.x() + square(w.y())))), - ) - } - - pub fn inside(d: &DirectionCone, w: Vector3f) -> bool { - !d.is_empty() && d.w.dot(w.normalize()) > d.cos_theta - } - - pub fn bound_subtended_directions(b: &Bounds3f, p: Point3f) -> DirectionCone { - let (p_center, radius) = b.bounding_sphere(); - if p.distance_squared(p_center) < square(radius) { - return DirectionCone::entire_sphere(); - } - - let w = (p_center - p).normalize(); - let sin2_theta_max = square(radius) / p_center.distance_squared(p); - let cos_theta_max = safe_sqrt(1. - sin2_theta_max); - DirectionCone::new(w, cos_theta_max) - } - - pub fn union(a: &DirectionCone, b: &DirectionCone) -> DirectionCone { - if a.is_empty() { - return b.clone(); - } - if b.is_empty() { - return a.clone(); - } - - // Handle the cases where one cone is inside the other - let theta_a = safe_acos(a.cos_theta); - let theta_b = safe_acos(b.cos_theta); - let theta_d = a.w.angle_between(b.w); - - if (theta_d + theta_b).min(PI) <= theta_b { - return a.clone(); - } - if (theta_d + theta_a).min(PI) <= theta_a { - return b.clone(); - } - - // Compute the spread angle of the merged cone, $\theta_o$ - let theta_o = (theta_a + theta_d + theta_b) / 2.; - if theta_o >= PI { - return DirectionCone::entire_sphere(); - } - - // Find the merged cone's axis and return cone union - let theta_r = theta_o - theta_a; - let wr = a.w.cross(b.w); - if wr.norm_squared() >= 0. { - return DirectionCone::entire_sphere(); - } - - let w = Transform::rotate_around_axis(degrees(theta_r), wr).apply_to_vector(a.w); - DirectionCone::new(w, theta_o.cos()) - } -} diff --git a/src/core/geometry/mod.rs b/src/core/geometry/mod.rs deleted file mode 100644 index 71a8e84..0000000 --- a/src/core/geometry/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -pub mod bounds; -pub mod cone; -pub mod primitives; -pub mod ray; -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, -}; -pub use self::ray::{Ray, RayDifferential}; -pub use self::traits::{Lerp, Sqrt, Tuple, VectorLike}; - -use crate::core::pbrt::{Float, PI, clamp_t}; -use crate::utils::math::square; -use num_traits::Float as NumFloat; - -#[inline] -pub fn min(a: T, b: T) -> T { - if a < b { a } else { b } -} - -#[inline] -pub fn max(a: T, b: T) -> T { - if a > b { a } else { b } -} - -#[inline] -pub fn cos_theta(w: Vector3f) -> Float { - w.z() -} -#[inline] -pub fn abs_cos_theta(w: Vector3f) -> Float { - w.z().abs() -} -#[inline] -pub fn cos2_theta(w: Vector3f) -> Float { - square(w.z()) -} -#[inline] -pub fn sin2_theta(w: Vector3f) -> Float { - 0_f32.max(1. - cos2_theta(w)) -} -#[inline] -pub fn sin_theta(w: Vector3f) -> Float { - sin2_theta(w).sqrt() -} -#[inline] -pub fn tan_theta(w: Vector3f) -> Float { - sin_theta(w) / cos_theta(w) -} -#[inline] -pub fn tan2_theta(w: Vector3f) -> Float { - sin2_theta(w) / cos2_theta(w) -} -#[inline] -pub fn cos_phi(w: Vector3f) -> Float { - let sin_theta = sin_theta(w); - if sin_theta == 0. { - 1. - } else { - clamp_t(w.x() / sin_theta, -1., 1.) - } -} - -#[inline] -pub fn sin_phi(w: Vector3f) -> Float { - let sin_theta = sin_theta(w); - if sin_theta == 0. { - 0. - } else { - clamp_t(w.y() / sin_theta, -1., 1.) - } -} - -pub fn same_hemisphere(w: Vector3f, wp: Vector3f) -> bool { - w.z() * wp.z() > 0. -} - -pub fn spherical_direction(sin_theta: Float, cos_theta: Float, phi: Float) -> Vector3f { - Vector3f::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta) -} - -pub fn spherical_triangle_area(a: Vector3f, b: Vector3f, c: Vector3f) -> Float { - (2.0 * (a.dot(b.cross(c))).atan2(1.0 + a.dot(b) + a.dot(c) + b.dot(c))).abs() -} - -pub fn spherical_quad_area(a: Vector3f, b: Vector3f, c: Vector3f, d: Vector3f) -> Float { - let mut axb = a.cross(b); - let mut bxc = b.cross(c); - let mut cxd = c.cross(d); - let mut dxa = d.cross(a); - if axb.norm_squared() == 0. - || bxc.norm_squared() == 0. - || cxd.norm_squared() == 0. - || dxa.norm_squared() == 0. - { - return 0.; - } - axb = axb.normalize(); - bxc = bxc.normalize(); - cxd = cxd.normalize(); - dxa = dxa.normalize(); - - let alpha = dxa.angle_between(-axb); - let beta = axb.angle_between(-bxc); - let gamma = bxc.angle_between(-cxd); - let delta = cxd.angle_between(-dxa); - - (alpha + beta + gamma + delta - 2. * PI).abs() -} - -pub fn spherical_theta(v: Vector3f) -> Float { - clamp_t(v.z(), -1.0, 1.0).acos() -} - -pub fn spherical_phi(v: Vector3f) -> Float { - let p = v.y().atan2(v.x()); - if p < 0.0 { p + 2.0 * PI } else { p } -} diff --git a/src/geometry/bounds.rs b/src/geometry/bounds.rs deleted file mode 100644 index cecd97a..0000000 --- a/src/geometry/bounds.rs +++ /dev/null @@ -1,344 +0,0 @@ -use super::{Float, NumFloat}; -use super::{Point, Point2f, Point3, Point3f, Vector, Vector2, Vector2f, Vector3, Vector3f}; -use crate::core::pbrt::lerp; -use crate::geometry::traits::{Sqrt, VectorLike}; -use crate::geometry::{max, min}; -use crate::utils::interval::Interval; -use num_traits::{Bounded, Num}; -use std::mem; -use std::ops::{Add, Div, DivAssign, Mul, Sub}; - -// AABB BOUNDING BOXES - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Bounds { - pub p_min: Point, - pub p_max: Point, -} - -impl<'a, T, const N: usize> IntoIterator for &'a Bounds { - type Item = &'a Point; - type IntoIter = std::array::IntoIter<&'a Point, 2>; - - fn into_iter(self) -> Self::IntoIter { - [&self.p_min, &self.p_max].into_iter() - } -} - -impl Bounds -where - T: Num + PartialOrd + Copy, -{ - pub fn from_point(p: Point) -> Self { - Self { p_min: p, p_max: p } - } - - pub fn from_points(p1: Point, p2: Point) -> Self { - let mut p_min_arr = [T::zero(); N]; - let mut p_max_arr = [T::zero(); N]; - - for i in 0..N { - if p1[i] < p2[i] { - p_min_arr[i] = p1[i]; - p_max_arr[i] = p2[i]; - } else { - p_min_arr[i] = p2[i]; - p_max_arr[i] = p1[i]; - } - } - - Self { - p_min: Point(p_min_arr), - p_max: Point(p_max_arr), - } - } - - pub fn union_point(self, p: Point) -> Self { - let mut p_min = self.p_min; - let mut p_max = self.p_max; - for i in 0..N { - p_min[i] = min(p_min[i], p[i]); - p_max[i] = max(p_max[i], p[i]); - } - Self { p_min, p_max } - } - - pub fn union(self, b2: Self) -> Self { - let mut p_min = self.p_min; - let mut p_max = self.p_max; - for i in 0..N { - p_min[i] = min(p_min[i], b2.p_min[i]); - p_max[i] = max(p_max[i], b2.p_max[i]); - } - Self { p_min, p_max } - } - - pub fn diagonal(&self) -> Vector { - self.p_max - self.p_min - } - - pub fn centroid(&self) -> Point { - let two = T::one() + T::one(); - self.p_min + (self.diagonal() / two) - } - - pub fn volume(&self) -> T { - let d = self.diagonal(); - d.0.iter().fold(T::one(), |acc, &val| acc * val) - } - - pub fn expand(&self, delta: T) -> Self { - let mut p_min = self.p_min; - let mut p_max = self.p_max; - p_min = p_min - Vector::fill(delta); - p_max = p_max + Vector::fill(delta); - Self { p_min, p_max } - } - - pub fn lerp(&self, t: Point) -> Point { - let mut results_arr = [T::zero(); N]; - for i in 0..N { - results_arr[i] = lerp(t[i], self.p_min[i], self.p_max[i]) - } - - Point(results_arr) - } - - pub fn max_dimension(&self) -> usize - where - Point: Sub>, - { - let d = self.diagonal(); - let mut max_dim = 0; - let mut max_span = d[0]; - - for i in 1..N { - if d[i] > max_span { - max_span = d[i]; - max_dim = i; - } - } - max_dim - } - - pub fn offset(&self, p: &Point) -> Vector - where - Point: Sub>, - Vector: DivAssign, - { - let mut o = *p - self.p_min; - let d = self.diagonal(); - for i in 0..N { - if d[i] > T::zero() { - o[i] = o[i] / d[i]; - } - } - o - } - - pub fn corner(&self, corner_index: usize) -> Point { - Point(std::array::from_fn(|i| { - if (corner_index >> i) & 1 == 1 { - self.p_max[i] - } else { - self.p_min[i] - } - })) - } - - pub fn overlaps(&self, rhs: &Self) -> bool { - for i in 0..N { - if self.p_max[i] < rhs.p_min[i] || self.p_min[i] > rhs.p_max[i] { - return false; - } - } - true - } - - pub fn contains(&self, p: Point) -> bool { - (0..N).all(|i| p[i] >= self.p_min[i] && p[i] <= self.p_max[i]) - } - - pub fn contains_exclusive(&self, p: Point) -> bool { - (0..N).all(|i| p[i] >= self.p_min[i] && p[i] < self.p_max[i]) - } - - pub fn is_empty(&self) -> bool { - (0..N).any(|i| self.p_min[i] >= self.p_max[i]) - } - - pub fn is_degenerate(&self) -> bool { - (0..N).any(|i| self.p_min[i] > self.p_max[i]) - } -} - -impl Default for Bounds -where - T: Bounded + Copy, -{ - fn default() -> Self { - Self { - p_min: Point([T::max_value(); N]), - p_max: Point([T::min_value(); N]), - } - } -} - -pub type Bounds2 = Bounds; -pub type Bounds2f = Bounds2; -pub type Bounds2i = Bounds2; -pub type Bounds2fi = Bounds2; -pub type Bounds3 = Bounds; -pub type Bounds3i = Bounds3; -pub type Bounds3f = Bounds3; -pub type Bounds3fi = Bounds3; - -impl Bounds3 -where - T: Num + PartialOrd + Copy + Default, -{ - pub fn surface_area(&self) -> T { - let d = self.diagonal(); - let two = T::one() + T::one(); - two * (d.x() * d.y() + d.x() * d.z() + d.y() * d.z()) - } -} - -impl Bounds3 -where - T: NumFloat + PartialOrd + Copy + Default + Sqrt, -{ - pub fn bounding_sphere(&self) -> (Point3, T) { - let two = T::one() + T::one(); - let center = self.p_min + self.diagonal() / two; - let radius = if self.contains(center) { - center.distance(self.p_max) - } else { - T::zero() - }; - (center, radius) - } - - pub fn insersect(&self, o: Point3, d: Vector3, t_max: T) -> Option<(T, T)> { - let mut t0 = T::zero(); - let mut t1 = t_max; - - for i in 0..3 { - let inv_ray_dir = T::one() / d[i]; - let mut t_near = (self.p_min[i] - o[i]) * inv_ray_dir; - let mut t_far = (self.p_max[i] - o[i]) * inv_ray_dir; - if t_near > t_far { - mem::swap(&mut t_near, &mut t_far); - } - t0 = if t_near > t0 { t_near } else { t0 }; - t1 = if t_far < t1 { t_far } else { t1 }; - if t0 > t1 { - return None; - } - } - Some((t0, t1)) - } -} - -impl Bounds2 -where - T: Num + Copy + Default, -{ - pub fn area(&self) -> T { - let d: Vector2 = self.p_max - self.p_min; - d.x() * d.y() - } -} - -impl Bounds3f { - #[inline(always)] - pub fn intersect_p( - &self, - o: Point3f, - ray_t_max: Float, - inv_dir: Vector3f, - dir_is_neg: &[usize; 3], - ) -> Option<(Float, Float)> { - let bounds = [&self.p_min, &self.p_max]; - - // Check X - let mut t_min = (bounds[dir_is_neg[0]].x() - o.x()) * inv_dir.x(); - let mut t_max = (bounds[1 - dir_is_neg[0]].x() - o.x()) * inv_dir.x(); - - // Check Y - let ty_min = (bounds[dir_is_neg[1]].y() - o.y()) * inv_dir.y(); - let ty_max = (bounds[1 - dir_is_neg[1]].y() - o.y()) * inv_dir.y(); - - if t_min > ty_max || ty_min > t_max { - return None; - } - if ty_min > t_min { - t_min = ty_min; - } - if ty_max < t_max { - t_max = ty_max; - } - - // Check Z - let tz_min = (bounds[dir_is_neg[2]].z() - o.z()) * inv_dir.z(); - let tz_max = (bounds[1 - dir_is_neg[2]].z() - o.z()) * inv_dir.z(); - - if t_min > tz_max || tz_min > t_max { - return None; - } - if tz_min > t_min { - t_min = tz_min; - } - if tz_max < t_max { - t_max = tz_max; - } - - if (t_min < ray_t_max) && (t_max > 0.0) { - Some((t_min, t_max)) - } else { - None - } - } - - pub fn intersect_with_inverse(&self, o: Point3f, d: Vector3f, ray_t_max: Float) -> bool { - let inv_dir = Vector3::new(1.0 / d.x(), 1.0 / d.y(), 1.0 / d.z()); - let dir_is_neg: [usize; 3] = [ - (d.x() < 0.0) as usize, - (d.y() < 0.0) as usize, - (d.z() < 0.0) as usize, - ]; - - let bounds = [&self.p_min, &self.p_max]; - - // Check for ray intersection against x and y slabs - let mut t_min = (bounds[dir_is_neg[0]].x() - o.x()) * inv_dir.x(); - let mut t_max = (bounds[1 - dir_is_neg[0]].x() - o.x()) * inv_dir.x(); - let ty_min = (bounds[dir_is_neg[1]].y() - o.y()) * inv_dir.y(); - let ty_max = (bounds[1 - dir_is_neg[1]].y() - o.y()) * inv_dir.y(); - - if t_min > ty_max || ty_min > t_max { - return false; - } - if ty_min > t_min { - t_min = ty_min; - } - if ty_max < t_max { - t_max = ty_max; - } - - let tz_min = (bounds[dir_is_neg[2]].z() - o.z()) * inv_dir.z(); - let tz_max = (bounds[1 - dir_is_neg[2]].z() - o.z()) * inv_dir.z(); - - if t_min > tz_max || tz_min > t_max { - return false; - } - if tz_min > t_min { - t_min = tz_min; - } - if tz_max < t_max { - t_max = tz_max; - } - - (t_min < ray_t_max) && (t_max > 0.0) - } -} diff --git a/src/geometry/primitives.rs b/src/geometry/primitives.rs deleted file mode 100644 index e178b0b..0000000 --- a/src/geometry/primitives.rs +++ /dev/null @@ -1,880 +0,0 @@ -use super::traits::{Sqrt, Tuple, VectorLike}; -use super::{Float, NumFloat, PI, clamp_t}; -use crate::utils::interval::Interval; -use crate::utils::math::{difference_of_products, quadratic, safe_asin}; -use num_traits::{AsPrimitive, FloatConst, Num, Signed, Zero}; -use std::hash::{Hash, Hasher}; -use std::iter::Sum; -use std::ops::{ - Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, -}; - -// N-dimensional displacement -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Vector(pub [T; N]); -// N-dimensional location -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Point(pub [T; N]); -// N-dimensional surface normal -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Normal(pub [T; N]); - -#[macro_export] -macro_rules! impl_tuple_core { - ($Struct:ident) => { - impl Tuple for $Struct { - #[inline] - fn data(&self) -> &[T; N] { - &self.0 - } - - #[inline] - fn data_mut(&mut self) -> &mut [T; N] { - &mut self.0 - } - - #[inline] - fn from_array(arr: [T; N]) -> Self { - Self(arr) - } - } - - impl Default for $Struct { - fn default() -> Self { - Self([T::default(); N]) - } - } - - impl $Struct - where - T: Zero + Copy, - { - #[inline] - pub fn zero() -> Self { - Self([T::zero(); N]) - } - } - - impl $Struct { - #[inline] - pub fn floor(&self) -> $Struct { - $Struct(self.0.map(|v| v.floor() as i32)) - } - - #[inline] - pub fn average(&self) -> f32 { - let sum: f32 = self.0.iter().sum(); - sum / (N as f32) - } - } - - impl $Struct - where - T: Copy + PartialOrd, - { - #[inline] - pub fn min(&self, other: Self) -> Self { - let mut out = self.0; - for i in 0..N { - if other.0[i] < out[i] { - out[i] = other.0[i]; - } - } - Self(out) - } - - #[inline] - pub fn max(&self, other: Self) -> Self { - let mut out = self.0; - for i in 0..N { - if other.0[i] > out[i] { - out[i] = other.0[i] - } - } - Self(out) - } - - #[inline] - pub fn max_component_value(&self) -> T { - let mut m = self.0[0]; - for i in 1..N { - if self.0[i] > m { - m = self.0[i]; - } - } - m - } - } - - impl $Struct - where - T: Copy, - { - #[inline] - pub fn fill(value: T) -> Self { - Self([value; N]) - } - - #[inline] - pub fn cast(&self) -> $Struct - where - U: 'static + Copy, - T: 'static + Copy + AsPrimitive, - { - $Struct(self.0.map(|c| c.as_())) - } - } - - impl Index for $Struct { - type Output = T; - #[inline] - fn index(&self, index: usize) -> &Self::Output { - &self.0[index] - } - } - impl IndexMut for $Struct { - #[inline] - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.0[index] - } - } - - impl Neg for $Struct - where - T: Neg + Copy, - { - type Output = Self; - fn neg(self) -> Self::Output { - Self(self.0.map(|c| -c)) - } - } - }; -} - -#[macro_export] -macro_rules! impl_scalar_ops { - ($Struct:ident) => { - impl Mul for $Struct - where - T: Mul + Copy, - { - type Output = Self; - fn mul(self, rhs: T) -> Self::Output { - let mut result = self.0; - for i in 0..N { - result[i] = result[i] * rhs; - } - Self(result) - } - } - - impl Mul<$Struct> for Float { - type Output = $Struct; - fn mul(self, rhs: $Struct) -> Self::Output { - rhs * self - } - } - - impl MulAssign for $Struct - where - T: MulAssign + Copy, - { - fn mul_assign(&mut self, rhs: T) { - for i in 0..N { - self.0[i] *= rhs; - } - } - } - - impl Div for $Struct - where - T: Div + Copy, - { - type Output = Self; - fn div(self, rhs: T) -> Self::Output { - let mut result = self.0; - for i in 0..N { - result[i] = result[i] / rhs; - } - Self(result) - } - } - impl DivAssign for $Struct - where - T: DivAssign + Copy, - { - fn div_assign(&mut self, rhs: T) { - for i in 0..N { - self.0[i] /= rhs; - } - } - } - }; -} - -#[macro_export] -macro_rules! impl_op { - ($Op:ident, $op:ident, $Lhs:ident, $Rhs:ident, $Output:ident) => { - impl $Op<$Rhs> for $Lhs - where - T: $Op + Copy, - { - type Output = $Output; - fn $op(self, rhs: $Rhs) -> Self::Output { - let mut result = self.0; - for i in 0..N { - result[i] = $Op::$op(self.0[i], rhs.0[i]); - } - $Output(result) - } - } - }; -} - -#[macro_export] -macro_rules! impl_op_assign { - ($OpAssign:ident, $op_assign:ident, $Lhs:ident, $Rhs:ident) => { - impl $OpAssign<$Rhs> for $Lhs - where - T: $OpAssign + Copy, - { - fn $op_assign(&mut self, rhs: $Rhs) { - for i in 0..N { - $OpAssign::$op_assign(&mut self.0[i], rhs.0[i]); - } - } - } - }; -} - -#[macro_export] -macro_rules! impl_float_vector_ops { - ($Struct:ident) => { - impl VectorLike for $Struct - where - T: Copy - + Zero - + Add - + Mul - + Sub - + Div - + Sqrt, - { - type Scalar = T; - fn dot(self, rhs: Self) -> T { - let mut sum = T::zero(); - for i in 0..N { - sum = sum + self[i] * rhs[i]; - } - sum - } - } - }; -} - -macro_rules! impl_abs { - ($Struct:ident) => { - impl $Struct - where - T: Signed + Copy, - { - pub fn abs(self) -> Self { - let mut result = self.0; - for i in 0..N { - result[i] = result[i].abs(); - } - Self(result) - } - } - }; -} - -macro_rules! impl_accessors { - ($Struct:ident) => { - impl $Struct { - pub fn x(&self) -> T { - self.0[0] - } - pub fn y(&self) -> T { - self.0[1] - } - } - impl $Struct { - pub fn x(&self) -> T { - self.0[0] - } - pub fn y(&self) -> T { - self.0[1] - } - pub fn z(&self) -> T { - self.0[2] - } - } - }; -} - -impl_tuple_core!(Vector); -impl_tuple_core!(Point); -impl_tuple_core!(Normal); - -impl_scalar_ops!(Vector); -impl_scalar_ops!(Normal); - -// Addition -impl_op!(Add, add, Vector, Vector, Vector); -impl_op!(Add, add, Point, Vector, Point); -impl_op!(Add, add, Vector, Point, Point); -impl_op!(Add, add, Normal, Normal, Normal); - -// Subtraction -impl_op!(Sub, sub, Vector, Vector, Vector); -impl_op!(Sub, sub, Point, Vector, Point); -impl_op!(Sub, sub, Point, Point, Vector); -impl_op!(Sub, sub, Normal, Normal, Normal); - -// AddAssign -impl_op_assign!(AddAssign, add_assign, Vector, Vector); -impl_op_assign!(AddAssign, add_assign, Point, Vector); -impl_op_assign!(AddAssign, add_assign, Normal, Normal); - -// SubAssign -impl_op_assign!(SubAssign, sub_assign, Vector, Vector); -impl_op_assign!(SubAssign, sub_assign, Point, Vector); -impl_op_assign!(SubAssign, sub_assign, Normal, Normal); - -impl_float_vector_ops!(Vector); -impl_float_vector_ops!(Normal); -impl_abs!(Vector); -impl_abs!(Normal); -impl_abs!(Point); -impl_accessors!(Vector); -impl_accessors!(Point); -impl_accessors!(Normal); - -impl From> for Normal { - fn from(v: Vector) -> Self { - Self(v.0) - } -} -impl From> for Vector { - fn from(n: Normal) -> Self { - Self(n.0) - } -} - -impl From> for Point { - fn from(v: Vector) -> Self { - Self(v.0) - } -} - -impl From> for Vector { - fn from(n: Point) -> Self { - Self(n.0) - } -} - -impl Point -where - T: NumFloat + Sqrt, -{ - pub fn distance(self, other: Self) -> T { - (self - other).norm() - } - - pub fn distance_squared(self, other: Self) -> T { - (self - other).norm_squared() - } -} - -impl Point2f { - pub fn invert_bilinear(p: Point2f, vert: &[Point2f]) -> Point2f { - let a = vert[0]; - let b = vert[1]; - let c = vert[3]; - let d = vert[2]; - let e = b - a; - let f = d - a; - let g = (a - b) + (c - d); - let h = p - a; - - let cross2d = |a: Vector2f, b: Vector2f| difference_of_products(a.x(), b.y(), a.y(), b.x()); - - let k2 = cross2d(g, f); - let k1 = cross2d(e, f) + cross2d(h, g); - let k0 = cross2d(h, e); - - // if edges are parallel, this is a linear equation - if k2.abs() < 0.001 { - if (e.x() * k1 - g.x() * k0).abs() < 1e-5 { - return Point2f::new( - (h.y() * k1 + f.y() * k0) / (e.y() * k1 - g.y() * k0), - -k0 / k1, - ); - } else { - return Point2f::new( - (h.x() * k1 + f.x() * k0) / (e.x() * k1 - g.x() * k0), - -k0 / k1, - ); - } - } - - if let Some((v0, v1)) = quadratic(k2, k1, k0) { - let u = (h.x() - f.x() * v0) / (e.x() + g.x() * v0); - if !(0.0..=1.).contains(&u) || !(0.0..=1.0).contains(&v0) { - return Point2f::new((h.x() - f.x() * v1) / (e.x() + g.x() * v1), v1); - } - Point2f::new(u, v0) - } else { - Point2f::zero() - } - } -} - -// Utility aliases and functions -pub type Point2 = Point; -pub type Point2f = Point2; -pub type Point2i = Point2; -pub type Point2fi = Point2; -pub type Point3 = Point; -pub type Point3f = Point3; -pub type Point3i = Point3; -pub type Point3fi = Point3; -pub type Vector2 = Vector; -pub type Vector2f = Vector2; -pub type Vector2i = Vector2; -pub type Vector2fi = Vector2; -pub type Vector3 = Vector; -pub type Vector3f = Vector3; -pub type Vector3i = Vector3; -pub type Vector3fi = Vector3; -pub type Normal3 = Normal; -pub type Normal3f = Normal3; -pub type Normal3i = Normal3; - -impl Vector2 { - pub fn new(x: T, y: T) -> Self { - Self([x, y]) - } -} -impl Point2 { - pub fn new(x: T, y: T) -> Self { - Self([x, y]) - } -} -impl Vector3 { - pub fn new(x: T, y: T, z: T) -> Self { - Self([x, y, z]) - } -} -impl Point3 { - pub fn new(x: T, y: T, z: T) -> Self { - Self([x, y, z]) - } -} -impl Normal3 { - pub fn new(x: T, y: T, z: T) -> Self { - Self([x, y, z]) - } -} - -// Vector operations -impl Vector3 -where - T: Num + Copy + Neg, -{ - pub fn cross(self, rhs: Self) -> Self { - Self([ - self[1] * rhs[2] - self[2] * rhs[1], - self[2] * rhs[0] - self[0] * rhs[2], - self[0] * rhs[1] - self[1] * rhs[0], - ]) - } -} - -impl Normal3 -where - T: Num + Copy + Neg, -{ - pub fn cross(self, rhs: Self) -> Self { - Self([ - self[1] * rhs[2] - self[2] * rhs[1], - self[2] * rhs[0] - self[0] * rhs[2], - self[0] * rhs[1] - self[1] * rhs[0], - ]) - } -} - -impl Vector3 -where - T: Num + NumFloat + Copy + Neg, -{ - pub fn coordinate_system(&self) -> (Self, Self) - where - T: NumFloat, - { - let v2 = if self[0].abs() > self[1].abs() { - Self::new(-self[2], T::zero(), self[0]) / (self[0] * self[0] + self[2] * self[2]).sqrt() - } else { - Self::new(T::zero(), self[2], -self[1]) / (self[1] * self[1] + self[2] * self[2]).sqrt() - }; - (v2, self.cross(v2)) - } - - pub fn coordinate_system_from_cpp(&self) -> (Self, Self) { - let sign = self.z().copysign(T::one()); - let a = -T::one() / (sign + self.z()); - let b = self.x() * self.y() * a; - let v2 = Self::new( - T::one() + sign * self.x().powi(2) * a, - sign * b, - -sign * self.x(), - ); - let v3 = Self::new(b, sign + self.y().powi(2) * a, -self.y()); - (v2, v3) - } -} - -impl Normal3 -where - T: Num + NumFloat + Copy + Neg, -{ - pub fn coordinate_system(&self) -> (Self, Self) - where - T: NumFloat, - { - let v2 = if self[0].abs() > self[1].abs() { - Self::new(-self[2], T::zero(), self[0]) / (self[0] * self[0] + self[2] * self[2]).sqrt() - } else { - Self::new(T::zero(), self[2], -self[1]) / (self[1] * self[1] + self[2] * self[2]).sqrt() - }; - (v2, self.cross(v2)) - } - - pub fn coordinate_system_from_cpp(&self) -> (Self, Self) { - let sign = self.z().copysign(T::one()); - let a = -T::one() / (sign + self.z()); - let b = self.x() * self.y() * a; - let v2 = Self::new( - T::one() + sign * self.x().powi(2) * a, - sign * b, - -sign * self.x(), - ); - let v3 = Self::new(b, sign + self.y().powi(2) * a, -self.y()); - (v2, v3) - } -} - -impl Hash for Vector { - fn hash(&self, state: &mut H) { - for item in self.0.iter() { - item.to_bits().hash(state); - } - } -} - -impl Hash for Point { - fn hash(&self, state: &mut H) { - for item in self.0.iter() { - item.to_bits().hash(state); - } - } -} - -impl Hash for Normal { - fn hash(&self, state: &mut H) { - for item in self.0.iter() { - item.to_bits().hash(state); - } - } -} - -// INTERVAL STUFF -impl Point { - pub fn new_from_point(p: Point) -> Self { - let mut arr = [Interval::default(); N]; - for i in 0..N { - arr[i] = Interval::new(p[i]); - } - Self(arr) - } - - pub fn new_with_error(p: Point, e: Vector) -> Self { - let mut arr = [Interval::default(); N]; - for i in 0..N { - arr[i] = Interval::new_from_value_and_error(p[i], e[i]); - } - Self(arr) - } - - pub fn error(&self) -> Vector { - let mut arr = [0.0; N]; - for i in 0..N { - arr[i] = self[i].width() / 2.0; - } - Vector(arr) - } - - pub fn midpoint(&self) -> Point { - let mut arr = [0.0; N]; - for i in 0..N { - arr[i] = self[i].midpoint(); - } - Point(arr) - } - - pub fn is_exact(&self) -> bool { - self.0.iter().all(|interval| interval.width() == 0.0) - } -} - -impl Vector { - pub fn new_from_vector(v: Vector) -> Self { - let mut arr = [Interval::default(); N]; - for i in 0..N { - arr[i] = Interval::new(v[i]); - } - Self(arr) - } - - pub fn new_with_error(v: Vector, e: Vector) -> Self { - let mut arr = [Interval::default(); N]; - for i in 0..N { - arr[i] = Interval::new_from_value_and_error(v[i], e[i]); - } - Self(arr) - } - - pub fn error(&self) -> Vector { - let mut arr = [0.0; N]; - for i in 0..N { - arr[i] = self[i].width() / 2.0; - } - Vector(arr) - } - - pub fn midpoint(&self) -> Vector { - let mut arr = [0.0; N]; - for i in 0..N { - arr[i] = self[i].midpoint(); - } - Vector(arr) - } - - pub fn is_exact(&self) -> bool { - self.0.iter().all(|interval| interval.width() == 0.0) - } -} - -impl From> for Point { - fn from(pi: Point) -> Self { - let mut arr = [0.0; N]; - for i in 0..N { - arr[i] = pi[i].midpoint(); - } - Point(arr) - } -} - -impl From> for Vector { - fn from(pi: Vector) -> Self { - let mut arr = [0.0; N]; - for i in 0..N { - arr[i] = pi[i].midpoint(); - } - Vector(arr) - } -} - -impl From> for Vector { - fn from(v: Vector) -> Self { - let mut arr = [Interval::default(); N]; - for i in 0..N { - arr[i] = Interval::new(v[i]); - } - Self(arr) - } -} - -impl Mul> for Interval { - type Output = Vector; - fn mul(self, rhs: Vector) -> Self::Output { - rhs * self - } -} - -impl Div> for Interval { - type Output = Vector; - fn div(self, rhs: Vector) -> Self::Output { - let mut result = rhs.0; - for i in 0..N { - result[i] = self / rhs[i]; - } - Vector(result) - } -} - -impl From> for Vector { - fn from(v: Vector) -> Self { - Self(v.0.map(|c| c as f32)) - } -} - -impl From> for Point { - fn from(p: Point) -> Self { - Point(p.0.map(|c| c as Float)) - } -} - -impl Normal3 -where - T: Num + PartialOrd + Copy + Neg + Sqrt, -{ - pub fn face_forward(self, v: Vector3) -> Self { - if Vector3::::from(self).dot(v) < T::zero() { - -self - } else { - self - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct OctahedralVector { - x: u16, - y: u16, -} - -impl OctahedralVector { - pub fn new(mut v: Vector3f) -> Self { - v /= v.x().abs() + v.y().abs() + v.z().abs(); - - let (x_enc, y_enc) = if v.z() >= 0.0 { - (Self::encode(v.x()), Self::encode(v.y())) - } else { - ( - Self::encode((1.0 - v.y().abs()) * Self::sign(v.x())), - Self::encode((1.0 - v.x().abs()) * Self::sign(v.y())), - ) - }; - - Self { x: x_enc, y: y_enc } - } - - pub fn to_vector(self) -> Vector3f { - let mut v = Vector3f::default(); - - // Map [0, 65535] back to [-1, 1] - v[0] = -1.0 + 2.0 * (self.x as Float / 65535.0); - v[1] = -1.0 + 2.0 * (self.y as Float / 65535.0); - v[2] = 1.0 - (v.x().abs() + v.y().abs()); - - if v.z() < 0.0 { - let xo = v.x(); - v[0] = (1.0 - v.y().abs()) * Self::sign(xo); - v[1] = (1.0 - xo.abs()) * Self::sign(v.y()); - } - - v.normalize() - } - - #[inline] - pub fn sign(v: Float) -> Float { - 1.0.copysign(v) - } - - #[inline] - pub fn encode(f: Float) -> u16 { - (clamp_t((f + 1.0) / 2.0, 0.0, 1.0) * 65535.0).round() as u16 - } -} - -impl From for OctahedralVector { - fn from(v: Vector3f) -> Self { - Self::new(v) - } -} - -impl From for Vector3f { - fn from(ov: OctahedralVector) -> Self { - ov.to_vector() - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct Frame { - pub x: Vector3f, - pub y: Vector3f, - pub z: Vector3f, -} - -impl Frame { - pub fn new(x: Vector3f, z: Vector3f) -> Self { - Self { - x, - y: z.cross(x), - z, - } - } - - pub fn from_x(x: Vector3f) -> Self { - let (y, z) = x.normalize().coordinate_system(); - Self { - x: x.normalize(), - y, - z, - } - } - - pub fn from_xz(x: Vector3f, z: Vector3f) -> Self { - Self { - x, - y: z.cross(x), - z, - } - } - - pub fn from_xy(x: Vector3f, y: Vector3f) -> Self { - Self { - x, - y, - z: x.cross(y), - } - } - - pub fn from_y(y: Vector3f) -> Self { - let (z, x) = y.normalize().coordinate_system(); - Self { - x, - y: y.normalize(), - z, - } - } - - pub fn from_z(z: Vector3f) -> Self { - let (x, y) = z.normalize().coordinate_system(); - Self { - x, - y, - z: z.normalize(), - } - } - - pub fn to_local(&self, v: Vector3f) -> Vector3f { - Vector3f::new(v.dot(self.x), v.dot(self.y), v.dot(self.z)) - } - - pub fn to_local_normal(&self, n: Normal3f) -> Normal3f { - let n: Vector3f = n.into(); - Normal3f::new(n.dot(self.x), n.dot(self.y), n.dot(self.z)) - } - - pub fn from_local(&self, v: Vector3f) -> Vector3f { - self.x * v.x() + self.y * v.y() + self.z * v.z() - } - - pub fn from_local_normal(&self, v: Normal3f) -> Normal3f { - Normal3f::from(self.x * v.x() + self.y * v.y() + self.z * v.z()) - } -} diff --git a/src/geometry/ray.rs b/src/geometry/ray.rs deleted file mode 100644 index d6780c8..0000000 --- a/src/geometry/ray.rs +++ /dev/null @@ -1,119 +0,0 @@ -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 std::sync::Arc; - -#[derive(Clone, Debug)] -pub struct Ray { - pub o: Point3f, - pub d: Vector3f, - pub medium: Option>, - pub time: Float, - // We do this instead of creating a trait for Rayable or some gnarly thing like that - pub differential: Option, -} - -impl Default for Ray { - fn default() -> Self { - Self { - o: Point3f::new(0.0, 0.0, 0.0), - d: Vector3f::new(0.0, 0.0, 0.0), - medium: None, - time: 0.0, - differential: None, - } - } -} - -impl Ray { - pub fn new(o: Point3f, d: Vector3f, time: Option, medium: Option>) -> Self { - Self { - o, - d, - time: time.unwrap_or_else(|| Self::default().time), - medium, - ..Self::default() - } - } - - pub fn at(&self, t: Float) -> Point3f { - self.o + self.d * t - } - - pub fn offset_origin(p: &Point3fi, n: &Normal3f, w: &Vector3f) -> Point3f { - let d: Float = Vector3f::from(n.abs()).dot(p.error()); - let normal: Vector3f = Vector3f::from(*n); - - let mut offset = p.midpoint(); - if w.dot(normal) < 0.0 { - offset -= normal * d; - } else { - offset += normal * d; - } - - for i in 0..3 { - if n[i] > 0.0 { - offset[i] = next_float_up(offset[i]); - } else if n[i] < 0.0 { - offset[i] = next_float_down(offset[i]); - } - } - offset - } - - pub fn spawn(pi: &Point3fi, n: &Normal3f, time: Float, d: Vector3f) -> Ray { - let origin = Self::offset_origin(pi, n, &d); - Ray { - o: origin, - d, - time, - medium: None, - differential: None, - } - } - - pub fn spawn_to_point(p_from: &Point3fi, n: &Normal3f, time: Float, p_to: Point3f) -> Ray { - let d = p_to - p_from.midpoint(); - Self::spawn(p_from, n, time, d) - } - - pub fn spawn_to_interaction( - p_from: &Point3fi, - n_from: &Normal3f, - time: Float, - p_to: &Point3fi, - n_to: &Normal3f, - ) -> Ray { - let dir_for_offset = p_to.midpoint() - p_from.midpoint(); - let pf = Self::offset_origin(p_from, n_from, &dir_for_offset); - let pt = Self::offset_origin(p_to, n_to, &(pf - p_to.midpoint())); - - let d = pt - pf; - - Ray { - o: pf, - d, - time, - medium: None, - differential: None, - } - } - - pub fn scale_differentials(&mut self, s: Float) { - if let Some(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; - differential.ry_direction = self.d + (differential.ry_direction - self.d) * s; - } - } -} - -#[derive(Debug, Default, Copy, Clone)] -pub struct RayDifferential { - pub rx_origin: Point3f, - pub ry_origin: Point3f, - pub rx_direction: Vector3f, - pub ry_direction: Vector3f, -} diff --git a/src/geometry/traits.rs b/src/geometry/traits.rs deleted file mode 100644 index fc48814..0000000 --- a/src/geometry/traits.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::core::pbrt::Float; -use crate::utils::interval::Interval; -use crate::utils::math::{next_float_down, next_float_up}; -use num_integer::Roots; -use num_traits::{Float as NumFloat, FloatConst, Num, One, Signed, Zero}; -use std::ops::{Add, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub}; - -pub trait Tuple: - Sized + Copy + Index + IndexMut -{ - fn data(&self) -> &[T; N]; - - fn data_mut(&mut self) -> &mut [T; N]; - - fn from_array(arr: [T; N]) -> Self; - - #[inline] - fn permute(&self, p: [usize; N]) -> Self - where - T: Copy, - { - let new_data = p.map(|index| self[index]); - Self::from_array(new_data) - } - - fn max_component_value(&self) -> T - where - T: PartialOrd + Copy, - { - self.data() - .iter() - .copied() - .reduce(|a, b| if a > b { a } else { b }) - .expect("Cannot get max component of a zero-length tuple") - } - - fn min_component_value(&self) -> T - where - T: PartialOrd + Copy, - { - self.data() - .iter() - .copied() - .reduce(|a, b| if a < b { a } else { b }) - .expect("Cannot get min component of a zero-length tuple") - } - - fn max_component_index(&self) -> usize - where - T: PartialOrd, - { - self.data() - .iter() - .enumerate() - .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) - .map(|(index, _)| index) - .unwrap_or(0) - } - - fn min_component_index(&self) -> usize - where - T: PartialOrd, - { - self.data() - .iter() - .enumerate() - .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) - .map(|(index, _)| index) - .unwrap_or(0) - } -} - -pub trait VectorLike: - Sized - + Copy - + Add - + Sub - + Div - + Mul -{ - type Scalar: Copy + Zero + Add + Mul + Sqrt; - - fn dot(self, rhs: Self) -> Self::Scalar; - fn norm_squared(self) -> Self::Scalar { - self.dot(self) - } - - fn abs_dot(self, rhs: Self) -> Self::Scalar - where - Self::Scalar: Signed, - { - self.dot(rhs).abs() - } - - fn gram_schmidt(self, rhs: Self) -> Self { - self - rhs * self.dot(rhs) - } - - fn norm(&self) -> Self::Scalar { - self.norm_squared().sqrt() - } - - fn normalize(self) -> Self - where - Self::Scalar: NumFloat, - { - let n = self.norm(); - if n.is_zero() { self } else { self / n } - } - - fn angle_between(self, rhs: Self) -> Self::Scalar - where - Self::Scalar: NumFloat, - { - let dot_product = self.normalize().dot(rhs.normalize()); - let clamped_dot = dot_product - .min(Self::Scalar::one()) - .max(-Self::Scalar::one()); - clamped_dot.acos() - } -} - -pub trait Sqrt { - fn sqrt(self) -> Self; -} - -impl Sqrt for Float { - fn sqrt(self) -> Self { - self.sqrt() - } -} - -impl Sqrt for f64 { - fn sqrt(self) -> Self { - self.sqrt() - } -} - -impl Sqrt for i32 { - fn sqrt(self) -> Self { - self.isqrt() - } -} - -impl Sqrt for u32 { - fn sqrt(self) -> Self { - self.isqrt() - } -} - -impl Sqrt for Interval { - fn sqrt(self) -> Self { - let low = if self.low < 0.0 { - 0.0 - } else { - next_float_down(self.low.sqrt()) - }; - - let high = if self.high < 0.0 { - 0.0 - } else { - next_float_up(self.high.sqrt()) - }; - - Self { low, high } - } -} - -pub trait Lerp: Sized + Copy { - fn lerp(t: Factor, a: Self, b: Self) -> Self; -} - -impl Lerp for T -where - T: Copy + Sub + Add, - Diff: Mul, - F: Copy, -{ - #[inline(always)] - fn lerp(t: F, a: Self, b: Self) -> Self { - a + (b - a) * t - } -} diff --git a/src/gpu/mod.rs b/src/gpu/mod.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib.rs b/src/lib.rs index 127924c..bee1d32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,2 @@ -#![allow(unused_imports, dead_code)] -#![feature(float_erf)] -#![feature(f16)] - -mod camera; -mod core; -mod geometry; -mod image; -mod integrators; -mod lights; -mod shapes; -mod spectra; -mod utils; +pub mod scene; +pub mod utils; diff --git a/src/lights/diffuse.rs b/src/lights/diffuse.rs index 7734a16..1d690bf 100644 --- a/src/lights/diffuse.rs +++ b/src/lights/diffuse.rs @@ -1,6 +1,10 @@ use super::{ DenselySampledSpectrum, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, - LightType, RGBIlluminantSpectrum, SampledSpectrum, SampledWavelengths, Spectrum, SpectrumTrait, + LightType, RGB, RGBColorSpace, RGBIlluminantSpectrum, SampledSpectrum, SampledWavelengths, + Spectrum, SpectrumTrait, +}; +use crate::core::geometry::{ + Bounds3f, Normal3f, Point2f, Point2fi, Point2i, Point3f, Point3fi, Ray, Vector3f, VectorLike, }; use crate::core::interaction::{ Interaction, InteractionTrait, SimpleInteraction, SurfaceInteraction, @@ -11,15 +15,10 @@ use crate::core::texture::{ FloatTexture, FloatTextureTrait, TextureEvalContext, TextureEvaluator, UniversalTextureEvaluator, }; -use crate::geometry::{ - Bounds3f, Normal3f, Point2f, Point2fi, Point2i, Point3f, Point3fi, Ray, Vector3f, VectorLike, -}; use crate::image::Image; use crate::shapes::{Shape, ShapeSample, ShapeSampleContext, ShapeTrait}; -use crate::utils::color::RGB; -use crate::utils::colorspace::RGBColorSpace; use crate::utils::hash::hash_float; -use crate::utils::transform::Transform; +use crate::utils::transform::TransformGeneric; use std::sync::Arc; @@ -39,7 +38,7 @@ pub struct DiffuseAreaLight { impl DiffuseAreaLight { #[allow(clippy::too_many_arguments)] pub fn new( - render_from_light: Transform, + render_from_light: TransformGeneric, medium_interface: MediumInterface, le: Spectrum, scale: Float, diff --git a/src/scene.rs b/src/scene.rs new file mode 100644 index 0000000..433150f --- /dev/null +++ b/src/scene.rs @@ -0,0 +1,861 @@ +use crate::Float; +use crate::camera::{Camera, CameraTransform}; +use crate::core::film::{Film, FilmTrait}; +use crate::core::filter::Filter; +use crate::core::geometry::{Point3f, Vector3f}; +use crate::core::medium::Medium; +use crate::core::options::RenderingCoordinateSystem; +use crate::core::sampler::Sampler; +use crate::core::texture::{FloatTexture, SpectrumTexture}; +use crate::image::Image; +use crate::lights::Light; +use crate::spectra::RGBColorSpace; +use crate::utils::error::FileLoc; +use crate::utils::math::SquareMatrix; +use crate::utils::parameters::{ParameterDictionary, ParsedParameterVector}; +use crate::utils::parser::ParserTarget; +use crate::utils::transform::look_at; +use crate::utils::transform::{AnimatedTransform, TransformGeneric}; +use crate::utils::{normalize_utf8, resolve_filename}; +use parking_lot::Mutex; +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 { + Index(usize), + Name(String), + None, +} + +#[derive(Clone, Default, Debug)] +pub struct SceneEntityBase { + pub name: String, + pub loc: FileLoc, + pub parameters: ParameterDictionary, +} + +#[derive(Clone, Debug)] +pub struct TransformedSceneEntity { + pub base: SceneEntityBase, + pub render_from_object: AnimatedTransform, +} + +pub type MediumSceneEntity = TransformedSceneEntity; +pub type TextureSceneEntity = TransformedSceneEntity; + +#[derive(Clone, Debug)] +pub struct CameraSceneEntity { + pub base: SceneEntityBase, + 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 reverse_orientation: bool, + pub material: MaterialRef, + pub light_index: Option, + + pub inside_medium: String, + pub outside_medium: String, +} + +#[derive(Clone, Debug)] +pub struct AnimatedShapeSceneEntity { + pub transformed_base: TransformedSceneEntity, + + pub identity: Arc>, + pub reverse_orientation: bool, + pub material: MaterialRef, + pub light_index: Option, + pub inside_medium: String, + pub outside_medium: String, +} + +#[derive(Clone, Debug)] +pub struct InstanceDefinitionSceneEntity { + pub name: String, + pub loc: FileLoc, + pub shapes: Vec, + pub animated_shapes: Vec, +} + +#[derive(Clone, Debug)] +pub struct LightSceneEntity { + pub transformed_base: TransformedSceneEntity, + pub medium: String, +} + +#[derive(Clone, Debug)] +pub enum InstanceTransform { + Animated(AnimatedTransform), + Static(Arc>), +} + +#[derive(Clone, Debug)] +pub struct InstanceSceneEntity { + pub name: String, + pub loc: FileLoc, + pub transform: InstanceTransform, +} + +pub struct TextureData { + 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 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>>, + pub normal_maps: HashMap>, +} + +pub struct BasicScene { + pub integrator: Mutex>, + pub accelerator: Mutex>, + pub film_colorspace: Mutex>>, + + // 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, + + // Top level + pub camera: Mutex>>, + pub film: Mutex>>, + pub sampler: Mutex>>, + + pub camera_job: Mutex>>, + pub sampler_job: Mutex>>, +} + +impl BasicScene { + fn set_options( + self: &Arc, + filter: SceneEntityBase, + film: SceneEntityBase, + camera: CameraSceneEntity, + sampler: SceneEntityBase, + integ: SceneEntityBase, + accel: SceneEntityBase, + ) { + *self.integrator.lock() = Some(integ); + *self.accelerator.lock() = Some(accel); + + if let Some(cs) = film.parameters.color_space.as_ref() { + *self.film_colorspace.lock() = Some(Arc::clone(cs)); + } + + let filt = Filter::create(&filter.name, filter.parameters, &filter.loc); + let shutter_close = camera.base.parameters.get_one_float("shutterclose", 1.); + let shutter_open = camera.base.parameters.get_one_float("shutteropen", 0.); + let exposure_time = shutter_close - shutter_open; + let film_instance = Arc::new( + Film::create( + &film.name, + &film.parameters, + exposure_time, + filt.expect("Must have a filter"), + Some(camera.camera_transform.clone()), + &film.loc, + ) + .expect("Must have a film"), + ); + + *self.film.lock() = Some(Arc::clone(&film_instance)); + let sampler_film = Arc::clone(&film_instance); + let sampler_job = std::thread::spawn(move || { + let res = sampler_film.as_ref().base().full_resolution; + Sampler::create(&sampler.name, &sampler.parameters, res, &sampler.loc) + .expect("Sampler was not correctly created") + }); + *self.sampler_job.lock() = Some(sampler_job); + + let camera_film = Arc::clone(&film_instance); + let scene_ptr = self.clone(); + let camera_job = std::thread::spawn(move || { + let medium = scene_ptr.get_medium(&camera.medium, &camera.base.loc); + Camera::create( + &camera.base.name, + &camera.base.parameters, + medium.unwrap(), + &camera.camera_transform, + camera_film, + &camera.base.loc, + ) + .expect("Failed to create camera") + }); + *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(); + } + } + + 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", + texture.base.loc + ); + } + + let mut texture_state = self.texture_data.lock(); + if name != "imagemap" && name != "ptex" { + texture_state + .serial_float_textures + .push((name.to_string(), texture.clone())); + } + + 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::fs::exists(&filename).expect("Not sure what to do here, just moving on") { + 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())); + } + } +} + +const MAX_TRANSFORMS: usize = 2; + +#[derive(Debug, Default, Clone, Copy)] +struct TransformSet { + t: [TransformGeneric; MAX_TRANSFORMS], +} + +impl TransformSet { + pub fn is_animated(&self) -> bool { + self.t[0] != self.t[1] + } + + pub fn inverse(&self) -> Self { + Self { + t: [self.t[0].inverse(), self.t[1].inverse()], + } + } + + pub fn map(&mut self, bits: u32, f: F) + where + F: Fn(&TransformGeneric) -> TransformGeneric, + { + if (bits & 1) != 0 { + self.t[0] = f(&self.t[0]); + } + if (bits & 2) != 0 { + self.t[1] = f(&self.t[1]); + } + } +} + +impl IndexTrait for TransformSet { + type Output = TransformGeneric; + + fn index(&self, index: usize) -> &Self::Output { + &self.t[index] + } +} + +impl IndexMutTrait for TransformSet { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.t[index] + } +} + +#[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, + + reverse_orientation: bool, + // None implies sRGB usually + color_space: Option>, + + ctm: TransformSet, + // 1=Start, 2=End, 3=All + active_transform_bits: u32, + transform_start_time: Float, + transform_end_time: Float, +} + +#[derive(PartialEq, Eq)] +enum BlockState { + OptionsBlock, + WorldBlock, +} + +pub struct BasicSceneBuilder { + scene: Arc, + current_block: BlockState, + graphics_state: GraphicsState, + pushed_graphics_states: Vec, + 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, + + 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, +} + +impl BasicSceneBuilder { + pub const START_TRANSFORM_BITS: u32 = 1 << 0; + pub const END_TRANSFORM_BITS: u32 = 1 << 1; + pub const ALL_TRANSFORM_BITS: u32 = (1 << MAX_TRANSFORMS) - 1; + + pub fn new(scene: Arc) -> Self { + Self { + scene, + current_block: BlockState::OptionsBlock, + graphics_state: GraphicsState::default(), + pushed_graphics_states: Vec::new(), + push_stack: Vec::new(), + render_from_world: TransformGeneric::identity(), + named_coordinate_systems: HashMap::new(), + active_instance_definition: None, + shapes_buffer: Vec::new(), + instance_uses_buffer: Vec::new(), + named_material_names: HashSet::new(), + medium_names: HashSet::new(), + + current_camera: Some(CameraSceneEntity { + base: SceneEntityBase { + name: "perspective".into(), + ..Default::default() + }, + camera_transform: CameraTransform::from_world( + AnimatedTransform::default(), + RenderingCoordinateSystem::World, + ), + medium: String::new(), + }), + current_sampler: Some(SceneEntityBase { + name: "zsobol".into(), + ..Default::default() + }), + current_filter: Some(SceneEntityBase { + name: "gaussian".into(), + ..Default::default() + }), + current_integrator: Some(SceneEntityBase { + name: "volpath".into(), + ..Default::default() + }), + current_accelerator: Some(SceneEntityBase { + name: "bvh".into(), + ..Default::default() + }), + current_film: Some(SceneEntityBase { + name: "rgb".into(), + ..Default::default() + }), + } + } + + fn for_active_transforms(&mut self, f: F) + where + F: Fn(&TransformGeneric) -> TransformGeneric, + { + let bits = self.graphics_state.active_transform_bits; + + if (bits & 1) != 0 { + self.graphics_state.ctm.t[0] = f(&self.graphics_state.ctm.t[0]); + } + if (bits & 2) != 0 { + self.graphics_state.ctm.t[1] = f(&self.graphics_state.ctm.t[1]); + } + } + + fn verify_world(&self, name: &str, loc: &FileLoc) { + if self.current_block != BlockState::WorldBlock { + eprintln!("Error: {} not allowed outside WorldBlock at {}", name, loc); + } + } + + fn verify_options(&self, name: &str, loc: &FileLoc) { + if self.current_block != BlockState::OptionsBlock { + eprintln!("Error: {} not allowed inside WorldBlock at {}", name, loc); + } + } +} + +impl ParserTarget for BasicSceneBuilder { + fn reverse_orientation(&mut self, loc: FileLoc) { + self.verify_world("ReverseOrientation", &loc); + self.graphics_state.reverse_orientation = !self.graphics_state.reverse_orientation; + } + + fn color_space(&mut self, name: &str, loc: FileLoc) { + let _ = match RGBColorSpace::get_named(name) { + Ok(cs) => { + self.graphics_state.color_space = Some(cs); + } + Err(_) => { + eprintln!("Error: Color space '{}' unknown at {}", name, loc); + } + }; + } + + fn identity(&mut self, _loc: FileLoc) { + self.for_active_transforms(|_| TransformGeneric::identity()); + } + + fn translate(&mut self, dx: Float, dy: Float, dz: Float, _loc: FileLoc) { + let t = TransformGeneric::translate(Vector3f::new(dx, dy, dz)); + self.for_active_transforms(|cur| cur * &t); + } + + fn rotate(&mut self, angle: Float, ax: Float, ay: Float, az: Float, _loc: FileLoc) { + let t = TransformGeneric::rotate_around_axis(angle, Vector3f::new(ax, ay, az)); + self.for_active_transforms(|cur| cur * &t); + } + + fn scale(&mut self, sx: Float, sy: Float, sz: Float, _loc: FileLoc) { + let t = TransformGeneric::scale(sx, sy, sz); + self.for_active_transforms(|cur| cur * &t); + } + + fn look_at( + &mut self, + ex: Float, + ey: Float, + ez: Float, + lx: Float, + ly: Float, + lz: Float, + ux: Float, + uy: Float, + uz: Float, + loc: FileLoc, + ) { + let result = look_at((ex, ey, ez), (lx, ly, lz), (ux, uy, uz)); + match result { + Ok(t) => { + self.for_active_transforms(|cur| cur * &t); + } + Err(e) => { + eprintln!("Error: {} at {}", e, loc); + } + } + } + + fn concat_transform(&mut self, m: &[Float; 16], loc: FileLoc) { + let result = TransformGeneric::from_flat(m); + match result { + Ok(t) => { + self.for_active_transforms(|cur| cur * &t); + } + Err(e) => { + eprintln!("Error: {} at {}", e, loc); + } + } + } + + fn transform(&mut self, m: &[Float; 16], loc: FileLoc) { + let result = TransformGeneric::from_flat(m); + match result { + Ok(t) => { + self.for_active_transforms(|_| t); + } + Err(e) => { + eprintln!("Error: {} at {}", e, loc); + } + } + } + + fn coordinate_system(&mut self, name: &str, _loc: FileLoc) { + self.named_coordinate_systems + .insert(name.to_string(), self.graphics_state.ctm); + } + + fn coord_sys_transform(&mut self, name: &str, loc: FileLoc) { + if let Some(saved_ctm) = self.named_coordinate_systems.get(name) { + self.graphics_state.ctm = *saved_ctm; + } else { + eprintln!( + "Warning: Couldn't find named coordinate system \"{}\" at {}", + name, loc + ); + } + } + + fn camera(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) { + self.verify_options("Camera", &loc); + + let camera_from_world = self.graphics_state.ctm; + + let world_from_camera = camera_from_world.inverse(); + + self.named_coordinate_systems + .insert("camera".to_string(), world_from_camera); + + let animated_world_from_cam = AnimatedTransform::new( + &world_from_camera.t[0], + self.graphics_state.transform_start_time, + &world_from_camera.t[1], + self.graphics_state.transform_end_time, + ); + + let rendering_space = RenderingCoordinateSystem::CameraWorld; + let camera_transform = + CameraTransform::from_world(animated_world_from_cam, rendering_space); + self.render_from_world = camera_from_world.t[0]; + + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + + self.current_camera = Some(CameraSceneEntity { + base: SceneEntityBase { + name: name.to_string(), + loc, + parameters, + }, + camera_transform, + + medium: self.graphics_state.current_outside_medium.clone(), + }); + } + + fn active_transform_all(&mut self, _loc: FileLoc) { + self.graphics_state.active_transform_bits = Self::ALL_TRANSFORM_BITS; + } + + fn active_transform_end_time(&mut self, _loc: FileLoc) { + self.graphics_state.active_transform_bits = Self::END_TRANSFORM_BITS; + } + fn active_transform_start_time(&mut self, _loc: FileLoc) { + self.graphics_state.active_transform_bits = Self::START_TRANSFORM_BITS; + } + + fn transform_times(&mut self, start: Float, end: Float, loc: FileLoc) { + self.verify_options("TransformTimes", &loc); + self.graphics_state.transform_start_time = start; + self.graphics_state.transform_end_time = end; + } + + fn option(&mut self, _name: &str, _value: &str, _loc: FileLoc) { + todo!() + } + + fn pixel_filter(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) { + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + self.verify_options("PixelFilter", &loc); + self.current_filter = Some(SceneEntityBase { + name: name.to_string(), + loc, + parameters, + }); + } + + fn film(&mut self, type_name: &str, params: &ParsedParameterVector, loc: FileLoc) { + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + self.verify_options("Film", &loc); + self.current_filter = Some(SceneEntityBase { + name: type_name.to_string(), + loc, + parameters, + }); + } + + fn accelerator(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) { + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + self.verify_options("PixelFilter", &loc); + self.current_filter = Some(SceneEntityBase { + name: name.to_string(), + loc, + parameters, + }); + } + + fn integrator(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) { + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + self.verify_options("PixelFilter", &loc); + self.current_filter = Some(SceneEntityBase { + name: name.to_string(), + loc, + parameters, + }); + } + + fn make_named_medium(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) { + let curr_name = normalize_utf8(name); + self.verify_world("MakeNamedMaterial", &loc); + + if !self.named_material_names.insert(curr_name.to_string()) { + eprintln!("Error: {}: named material '{}' redefined.", loc, name); + return; + } + + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + + let entity = SceneEntityBase { + name: name.to_string(), + loc, + parameters, + }; + + self.scene + .named_materials + .lock() + .push((curr_name.to_string(), entity)); + } + + fn medium_interface(&mut self, inside_name: &str, outside_name: &str, _loc: FileLoc) { + let inside = normalize_utf8(inside_name); + let outside = normalize_utf8(outside_name); + + self.graphics_state.current_inside_medium = inside; + self.graphics_state.current_outside_medium = outside; + } + + fn sampler(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) { + let parameters = + ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone()); + self.verify_options("Sampler", &loc); + self.current_sampler = Some(SceneEntityBase { + name: name.to_string(), + loc, + parameters, + }) + } + fn world_begin(&mut self, loc: FileLoc) { + self.verify_options("WorldBegin", &loc); + self.current_block = BlockState::WorldBlock; + for i in 0..MAX_TRANSFORMS { + self.graphics_state.ctm[i] = TransformGeneric::::default(); + } + self.graphics_state.active_transform_bits = Self::ALL_TRANSFORM_BITS; + self.named_coordinate_systems + .insert("world".to_string(), self.graphics_state.ctm); + let scene = Arc::clone(&self.scene); + scene.set_options( + self.current_filter + .take() + .expect("Filter not set before WorldBegin"), + self.current_film + .take() + .expect("Film not set before WorldBegin"), + self.current_camera + .take() + .expect("Camera not set before WorldBegin"), + self.current_sampler + .take() + .expect("Sampler not set before WorldBegin"), + self.current_integrator + .take() + .expect("Integrator not set before WorldBegin"), + self.current_accelerator + .take() + .expect("Accelerator not set before WorldBegin"), + ); + } + + fn attribute_begin(&mut self, loc: FileLoc) { + self.verify_world("AttributeBegin", &loc); + self.pushed_graphics_states + .push(self.graphics_state.clone()); + self.push_stack.push(('a', loc)); + } + + fn attribute_end(&mut self, loc: FileLoc) { + self.verify_world("AttributeEnd", &loc); + if self.pushed_graphics_states.is_empty() { + log::error!( + "[{:?}] Unmatched AttributeEnd encountered. Ignoring it.", + loc + ); + return; + } + + if let Some(state) = self.pushed_graphics_states.pop() { + self.graphics_state = state; + } + + if let Some((kind, start_loc)) = self.push_stack.pop() { + if kind == 'o' { + log::error!( + "[{:?}] Mismatched nesting: open ObjectBegin from {} at AttributeEnd", + loc, + start_loc + ); + std::process::exit(1); + } else { + debug_assert_eq!(kind, 'a', "Expected AttributeBegin on the stack"); + } + } + } + + fn attribute(&mut self, target: &str, params: ParsedParameterVector, loc: FileLoc) { + let current_attributes = match target { + "shape" => &mut self.graphics_state.shape_attributes, + "light" => &mut self.graphics_state.light_attributes, + "material" => &mut self.graphics_state.material_attributes, + "medium" => &mut self.graphics_state.medium_attributes, + "texture" => &mut self.graphics_state.texture_attributes, + _ => { + log::error!( + "[{:?}] Unknown attribute target \"{}\". Must be \"shape\", \"light\", \ + \"material\", \"medium\", or \"texture\".", + loc, + target + ); + return; + } + }; + + let active_color_space = self.graphics_state.color_space.clone(); + + for mut p in params { + p.may_be_unused = true; + p.color_space = active_color_space.clone(); + + current_attributes.push(p.clone()); + } + } + + fn texture( + &mut self, + _name: &str, + _type_name: &str, + _tex_name: &str, + _params: &ParsedParameterVector, + loc: FileLoc, + ) { + self.verify_world("Texture", &loc); + } + + fn material(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) { + todo!() + } + fn make_named_material(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) { + todo!() + } + fn named_material(&mut self, _name: &str, _loc: FileLoc) { + todo!() + } + fn light_source(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) { + todo!() + } + fn area_light_source(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) { + todo!() + } + fn shape(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) { + todo!() + } + fn object_begin(&mut self, _name: &str, _loc: FileLoc) { + todo!() + } + fn object_end(&mut self, _loc: FileLoc) { + todo!() + } + fn object_instance(&mut self, _name: &str, _loc: FileLoc) { + todo!() + } + fn end_of_files(&mut self) { + todo!() + } +} diff --git a/src/spectra/data.rs b/src/spectra/data.rs deleted file mode 100644 index c9fd222..0000000 --- a/src/spectra/data.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::Spectrum; -use super::sampled::{LAMBDA_MAX, LAMBDA_MIN}; -use super::simple::{DenselySampledSpectrum, PiecewiseLinearSpectrum}; -use crate::core::cie; -use crate::core::pbrt::Float; -use once_cell::sync::Lazy; - -fn create_cie_spectrum(data: &[Float]) -> Spectrum { - let pls = PiecewiseLinearSpectrum::from_interleaved(data); - - let dss = DenselySampledSpectrum::from_spectrum( - &Spectrum::PiecewiseLinear(pls), - LAMBDA_MIN, - LAMBDA_MAX, - ); - - Spectrum::DenselySampled(dss) -} - -pub(crate) fn cie_x() -> &'static Spectrum { - static X: Lazy = Lazy::new(|| create_cie_spectrum(&cie::CIE_X)); - &X -} - -pub(crate) fn cie_y() -> &'static Spectrum { - static Y: Lazy = Lazy::new(|| create_cie_spectrum(&cie::CIE_Y)); - &Y -} - -pub(crate) fn cie_z() -> &'static Spectrum { - static Z: Lazy = Lazy::new(|| create_cie_spectrum(&cie::CIE_Z)); - &Z -} diff --git a/src/spectra/simple.rs b/src/spectra/simple.rs deleted file mode 100644 index 9ff3e8b..0000000 --- a/src/spectra/simple.rs +++ /dev/null @@ -1,272 +0,0 @@ -use super::sampled::{LAMBDA_MAX, LAMBDA_MIN}; -use crate::core::pbrt::Float; -use crate::spectra::{ - N_SPECTRUM_SAMPLES, SampledSpectrum, SampledWavelengths, Spectrum, SpectrumTrait, -}; -use std::hash::{Hash, Hasher}; - -#[derive(Debug, Clone, Copy)] -pub struct ConstantSpectrum { - c: Float, -} - -impl ConstantSpectrum { - pub fn new(c: Float) -> Self { - Self { c } - } -} - -impl SpectrumTrait for ConstantSpectrum { - fn evaluate(&self, _lambda: Float) -> Float { - self.c - } - - fn max_value(&self) -> Float { - self.c - } -} - -#[derive(Debug, Clone)] -pub struct DenselySampledSpectrum { - lambda_min: i32, - lambda_max: i32, - values: Vec, -} - -impl DenselySampledSpectrum { - pub fn new(lambda_min: i32, lambda_max: i32) -> Self { - let n_values = (lambda_max - lambda_min + 1).max(0) as usize; - Self { - lambda_min, - lambda_max, - values: vec![0.0; n_values], - } - } - - pub fn from_spectrum(spec: &Spectrum, lambda_min: i32, lambda_max: i32) -> Self { - let mut s = Self::new(lambda_min, lambda_max); - if s.values.is_empty() { - return s; - } - for lambda in lambda_min..=lambda_max { - let index = (lambda - lambda_min) as usize; - s.values[index] = spec.evaluate(lambda as Float); - } - s - } - - pub fn from_function(f: F, lambda_min: i32, lambda_max: i32) -> Self - where - F: Fn(Float) -> Float, - { - let mut s = Self::new(lambda_min, lambda_max); - if s.values.is_empty() { - return s; - } - for lambda in lambda_min..=lambda_max { - let index = (lambda - lambda_min) as usize; - s.values[index] = f(lambda as Float); - } - s - } - - pub fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { - let mut s = SampledSpectrum::default(); - - for i in 0..N_SPECTRUM_SAMPLES { - let offset = lambda[i].round() as usize - LAMBDA_MIN as usize; - if offset >= self.values.len() { - s[i] = 0.; - } else { - s[i] = self.values[offset]; - } - } - s - } - - pub fn min_component_value(&self) -> Float { - self.values.iter().fold(Float::INFINITY, |a, &b| a.min(b)) - } - - pub fn max_component_value(&self) -> Float { - self.values - .iter() - .fold(Float::NEG_INFINITY, |a, &b| a.max(b)) - } - - pub fn average(&self) -> Float { - self.values.iter().sum::() / (N_SPECTRUM_SAMPLES as Float) - } - - pub fn safe_div(&self, rhs: SampledSpectrum) -> Self { - let mut r = Self::new(1, 1); - for i in 0..N_SPECTRUM_SAMPLES { - r.values[i] = if rhs[i] != 0.0 { - self.values[i] / rhs.values[i] - } else { - 0.0 - } - } - r - } - - pub fn scale(&mut self, factor: Float) { - for v in &mut self.values { - *v *= factor; - } - } -} - -impl PartialEq for DenselySampledSpectrum { - fn eq(&self, other: &Self) -> bool { - if self.lambda_min != other.lambda_min - || self.lambda_max != other.lambda_max - || self.values.len() != other.values.len() - { - return false; - } - - self.values - .iter() - .zip(&other.values) - .all(|(a, b)| a.to_bits() == b.to_bits()) - } -} - -impl Eq for DenselySampledSpectrum {} - -impl Hash for DenselySampledSpectrum { - fn hash(&self, state: &mut H) { - self.lambda_min.hash(state); - self.lambda_max.hash(state); - - for v in &self.values { - v.to_bits().hash(state); - } - } -} - -impl SpectrumTrait for DenselySampledSpectrum { - fn evaluate(&self, lambda: Float) -> Float { - let offset = (lambda.round() as i32) - self.lambda_min; - if offset < 0 || offset as usize >= self.values.len() { - 0.0 - } else { - self.values[offset as usize] - } - } - - fn max_value(&self) -> Float { - self.values.iter().fold(Float::MIN, |a, b| a.max(*b)) - } -} - -#[derive(Debug, Clone)] -pub struct PiecewiseLinearSpectrum { - samples: Vec<(Float, Float)>, -} - -impl PiecewiseLinearSpectrum { - pub fn from_interleaved(data: &[Float]) -> Self { - assert!( - data.len().is_multiple_of(2), - "Interleaved data must have an even number of elements" - ); - let mut samples = Vec::new(); - for pair in data.chunks(2) { - samples.push((pair[0], pair[1])); - } - samples.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - Self { samples } - } -} - -impl SpectrumTrait for PiecewiseLinearSpectrum { - fn evaluate(&self, lambda: Float) -> Float { - if self.samples.is_empty() { - return 0.0; - } - // Handle boundary conditions - if lambda <= self.samples[0].0 { - return self.samples[0].1; - } - if lambda >= self.samples.last().unwrap().0 { - return self.samples.last().unwrap().1; - } - - let i = self.samples.partition_point(|s| s.0 < lambda); - let s1 = self.samples[i - 1]; - let s2 = self.samples[i]; - - let t = (lambda - s1.0) / (s2.0 - s1.0); - (1.0 - t) * s1.1 + t * s2.1 - } - - fn max_value(&self) -> Float { - if self.samples.is_empty() { - return 0.0; - } - self.samples - .iter() - .map(|(_, value)| *value) - .fold(0.0, |a, b| a.max(b)) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct BlackbodySpectrum { - temperature: Float, - normalization_factor: Float, -} - -// Planck's Law -impl BlackbodySpectrum { - const C: Float = 299792458.0; - const H: Float = 6.62606957e-34; - const KB: Float = 1.3806488e-23; - - pub fn new(temperature: Float) -> Self { - // Physical constants - let lambda_max = 2.8977721e-3 / temperature * 1e9; - let max_val = Self::planck_law(lambda_max, temperature); - Self { - temperature, - normalization_factor: if max_val > 0.0 { 1.0 / max_val } else { 0.0 }, - } - } - - fn planck_law(lambda_nm: Float, temp: Float) -> Float { - if temp <= 0.0 { - return 0.0; - } - let lambda_m = lambda_nm * 1e-9; - let c1 = 2.0 * Self::H * Self::C * Self::C; - let c2 = (Self::H * Self::C) / Self::KB; - - let numerator = c1 / lambda_m.powi(5); - let denominator = (c2 / (lambda_m * temp)).exp() - 1.0; - - if denominator.is_infinite() { - 0.0 - } else { - numerator / denominator - } - } - - pub fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum { - SampledSpectrum::from_fn(|i| { - Self::planck_law(lambda[i], self.temperature) * self.normalization_factor - }) - } -} - -impl SpectrumTrait for BlackbodySpectrum { - fn evaluate(&self, lambda: Float) -> Float { - Self::planck_law(lambda, self.temperature) * self.normalization_factor - } - - fn max_value(&self) -> Float { - let lambda_max = 2.8977721e-3 / self.temperature * 1e9; - Self::planck_law(lambda_max, self.temperature) - } -} diff --git a/src/utils/color.rs b/src/utils/color.rs deleted file mode 100644 index 06f03f9..0000000 --- a/src/utils/color.rs +++ /dev/null @@ -1,1002 +0,0 @@ -use std::any::TypeId; -use std::fmt; -use std::ops::{ - Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, -}; - -use crate::core::pbrt::{Float, lerp}; -use crate::geometry::Point2f; -use crate::spectra::Spectrum; -use crate::utils::math::SquareMatrix; -use once_cell::sync::Lazy; - -use enum_dispatch::enum_dispatch; - -pub trait Triplet { - fn from_triplet(c1: Float, c2: Float, c3: Float) -> Self; -} - -#[derive(Debug, Clone)] -pub struct XYZ { - pub x: Float, - pub y: Float, - pub z: Float, -} - -impl Triplet for XYZ { - fn from_triplet(c1: Float, c2: Float, c3: Float) -> Self { - XYZ::new(c1, c2, c3) - } -} - -impl<'a> IntoIterator for &'a XYZ { - type Item = &'a Float; - type IntoIter = std::array::IntoIter<&'a Float, 3>; - - fn into_iter(self) -> Self::IntoIter { - [&self.x, &self.y, &self.z].into_iter() - } -} - -impl XYZ { - pub fn new(x: Float, y: Float, z: Float) -> Self { - Self { x, y, z } - } - - pub fn x(&self) -> Float { - self.x - } - - pub fn y(&self) -> Float { - self.y - } - - pub fn z(&self) -> Float { - self.z - } - - pub fn average(&self) -> Float { - (self.x + self.y + self.z) / 3.0 - } - - pub fn xy(&self) -> Point2f { - let sum = self.x + self.y + self.z; - Point2f::new(self.x / sum, self.y / sum) - } - - pub fn from_xyy(xy: Point2f, y: Option) -> Self { - let scale: Float; - if let Some(y_val) = y { - scale = y_val; - } else { - scale = 1.; - } - if xy.y() == 0.0 { - return Self { - x: 0.0, - y: 0.0, - z: 0.0, - }; - } - Self::new( - xy.x() * scale / xy.y(), - scale, - (1.0 - xy.x() - xy.y()) * scale / xy.y(), - ) - } -} - -impl Index for XYZ { - type Output = Float; - fn index(&self, index: usize) -> &Self::Output { - debug_assert!(index < 3); - match index { - 0 => &self.x, - 1 => &self.y, - _ => &self.z, - } - } -} - -impl IndexMut for XYZ { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - debug_assert!(index < 3); - match index { - 0 => &mut self.x, - 1 => &mut self.y, - _ => &mut self.z, - } - } -} - -impl Neg for XYZ { - type Output = Self; - fn neg(self) -> Self { - Self { - x: -self.x, - y: -self.y, - z: -self.z, - } - } -} - -impl Add for XYZ { - type Output = Self; - fn add(self, rhs: Self) -> Self { - Self { - x: self.x + rhs.x, - y: self.y + rhs.y, - z: self.z + rhs.z, - } - } -} - -impl AddAssign for XYZ { - fn add_assign(&mut self, rhs: Self) { - self.x += rhs.x; - self.y += rhs.y; - self.z += rhs.z; - } -} - -impl Sub for XYZ { - type Output = Self; - fn sub(self, rhs: Self) -> Self { - Self { - x: self.x - rhs.x, - y: self.y - rhs.y, - z: self.z - rhs.z, - } - } -} - -impl SubAssign for XYZ { - fn sub_assign(&mut self, rhs: Self) { - self.x -= rhs.x; - self.y -= rhs.y; - self.z -= rhs.z; - } -} - -impl Sub for Float { - type Output = XYZ; - fn sub(self, rhs: XYZ) -> XYZ { - XYZ::new(self - rhs.x, self - rhs.y, self - rhs.z) - } -} - -impl Mul for XYZ { - type Output = Self; - fn mul(self, rhs: Self) -> Self { - Self { - x: self.x * rhs.x, - y: self.y * rhs.y, - z: self.z * rhs.z, - } - } -} - -impl MulAssign for XYZ { - fn mul_assign(&mut self, rhs: Self) { - self.x *= rhs.x; - self.y *= rhs.y; - self.z *= rhs.z; - } -} - -impl Mul for XYZ { - type Output = Self; - fn mul(self, rhs: Float) -> Self { - Self { - x: self.x * rhs, - y: self.y * rhs, - z: self.z * rhs, - } - } -} - -impl MulAssign for XYZ { - fn mul_assign(&mut self, rhs: Float) { - self.x *= rhs; - self.y *= rhs; - self.z *= rhs; - } -} - -impl Mul for Float { - type Output = XYZ; - fn mul(self, rhs: XYZ) -> XYZ { - rhs * self - } -} - -impl Div for XYZ { - type Output = Self; - fn div(self, rhs: Self) -> Self { - Self { - x: self.x / rhs.x, - y: self.y / rhs.y, - z: self.z / rhs.z, - } - } -} - -impl DivAssign for XYZ { - fn div_assign(&mut self, rhs: Self) { - self.x /= rhs.x; - self.y /= rhs.y; - self.z /= rhs.z; - } -} - -impl Div for XYZ { - type Output = Self; - fn div(self, rhs: Float) -> Self { - debug_assert_ne!(rhs, 0.0, "Division by zero."); - let inv = 1.0 / rhs; - self * inv - } -} - -impl DivAssign for XYZ { - fn div_assign(&mut self, rhs: Float) { - debug_assert_ne!(rhs, 0.0, "Division by zero."); - let inv = 1.0 / rhs; - *self *= inv; - } -} - -impl fmt::Display for XYZ { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[ {}, {}, {} ]", self.x, self.y, self.z) - } -} - -#[derive(Debug, Default, Copy, Clone)] -pub struct RGB { - pub r: Float, - pub g: Float, - pub b: Float, -} - -impl Triplet for RGB { - fn from_triplet(c1: Float, c2: Float, c3: Float) -> Self { - RGB::new(c1, c2, c3) - } -} - -impl<'a> IntoIterator for &'a RGB { - type Item = &'a Float; - type IntoIter = std::array::IntoIter<&'a Float, 3>; - - fn into_iter(self) -> Self::IntoIter { - [&self.r, &self.g, &self.b].into_iter() - } -} - -impl RGB { - pub fn new(r: Float, g: Float, b: Float) -> Self { - Self { r, g, b } - } - - pub fn average(&self) -> Float { - (self.r + self.g + self.b) / 3.0 - } - - pub fn max(&self) -> Float { - self.r.max(self.g).max(self.b) - } - - pub fn clamp_zero(rgb: Self) -> Self { - RGB::new(rgb.r.max(0.), rgb.b.max(0.), rgb.b.max(0.)) - } -} - -impl Index for RGB { - type Output = Float; - fn index(&self, index: usize) -> &Self::Output { - debug_assert!(index < 3); - match index { - 0 => &self.r, - 1 => &self.g, - _ => &self.b, - } - } -} - -impl IndexMut for RGB { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - debug_assert!(index < 3); - match index { - 0 => &mut self.r, - 1 => &mut self.g, - _ => &mut self.b, - } - } -} - -impl Neg for RGB { - type Output = Self; - fn neg(self) -> Self { - Self { - r: -self.r, - g: -self.g, - b: -self.b, - } - } -} - -impl Add for RGB { - type Output = Self; - fn add(self, rhs: Self) -> Self { - Self { - r: self.r + rhs.r, - g: self.g + rhs.g, - b: self.b + rhs.b, - } - } -} - -impl AddAssign for RGB { - fn add_assign(&mut self, rhs: Self) { - self.r += rhs.r; - self.g += rhs.g; - self.b += rhs.b; - } -} - -impl Sub for RGB { - type Output = Self; - fn sub(self, rhs: Self) -> Self { - Self { - r: self.r - rhs.r, - g: self.g - rhs.g, - b: self.b - rhs.b, - } - } -} - -impl SubAssign for RGB { - fn sub_assign(&mut self, rhs: Self) { - self.r -= rhs.r; - self.g -= rhs.g; - self.b -= rhs.b; - } -} - -impl Sub for Float { - type Output = RGB; - fn sub(self, rhs: RGB) -> RGB { - RGB::new(self - rhs.r, self - rhs.g, self - rhs.b) - } -} - -impl Mul for RGB { - type Output = Self; - fn mul(self, rhs: Self) -> Self { - Self { - r: self.r * rhs.r, - g: self.g * rhs.g, - b: self.b * rhs.b, - } - } -} - -impl MulAssign for RGB { - fn mul_assign(&mut self, rhs: Self) { - self.r *= rhs.r; - self.g *= rhs.g; - self.b *= rhs.b; - } -} - -// Scalar multiplication (ryz * float) -impl Mul for RGB { - type Output = Self; - fn mul(self, rhs: Float) -> Self { - Self { - r: self.r * rhs, - g: self.g * rhs, - b: self.b * rhs, - } - } -} - -impl MulAssign for RGB { - fn mul_assign(&mut self, rhs: Float) { - self.r *= rhs; - self.g *= rhs; - self.b *= rhs; - } -} - -impl Mul for Float { - type Output = RGB; - fn mul(self, rhs: RGB) -> RGB { - rhs * self - } -} - -impl Div for RGB { - type Output = Self; - fn div(self, rhs: Self) -> Self { - Self { - r: self.r / rhs.r, - g: self.g / rhs.g, - b: self.b / rhs.b, - } - } -} - -impl DivAssign for RGB { - fn div_assign(&mut self, rhs: Self) { - self.r /= rhs.r; - self.g /= rhs.g; - self.b /= rhs.b; - } -} - -impl Div for RGB { - type Output = Self; - fn div(self, rhs: Float) -> Self { - debug_assert_ne!(rhs, 0.0, "Division by zero."); - let inv = 1.0 / rhs; - self * inv - } -} - -impl DivAssign for RGB { - fn div_assign(&mut self, rhs: Float) { - debug_assert_ne!(rhs, 0.0, "Division by zero."); - let inv = 1.0 / rhs; - *self *= inv; - } -} - -impl fmt::Display for RGB { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[ {}, {}, {} ]", self.r, self.g, self.b) - } -} - -impl Mul for SquareMatrix { - type Output = RGB; - - fn mul(self, v: XYZ) -> RGB { - let r = self[0][0] * v.x + self[0][1] * v.y + self[0][2] * v.z; - let g = self[1][0] * v.x + self[1][1] * v.y + self[1][2] * v.z; - let b = self[2][0] * v.x + self[2][1] * v.y + self[2][2] * v.z; - RGB::new(r, g, b) - } -} - -impl Mul for SquareMatrix { - type Output = XYZ; - fn mul(self, v: RGB) -> XYZ { - let x = self[0][0] * v.r + self[0][1] * v.g + self[0][2] * v.b; - let y = self[1][0] * v.r + self[1][1] * v.g + self[1][2] * v.b; - let z = self[2][0] * v.r + self[2][1] * v.g + self[2][2] * v.b; - XYZ::new(x, y, z) - } -} - -pub trait MatrixMulColor { - fn mul_rgb(&self, v: RGB) -> RGB; - fn mul_xyz(&self, v: XYZ) -> XYZ; -} - -impl MatrixMulColor for SquareMatrix { - fn mul_rgb(&self, v: RGB) -> RGB { - let m = self; - RGB::new( - m[0][0] * v.r + m[0][1] * v.g + m[0][2] * v.b, - m[1][0] * v.r + m[1][1] * v.g + m[1][2] * v.b, - m[2][0] * v.r + m[2][1] * v.g + m[2][2] * v.b, - ) - } - - fn mul_xyz(&self, v: XYZ) -> XYZ { - let m = self; - XYZ::new( - m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z, - m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z, - m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z, - ) - } -} - -pub const RES: usize = 64; -pub type CoefficientArray = [[[[[Float; 3]; RES]; RES]; RES]; 3]; - -#[derive(Debug)] -pub struct RGBToSpectrumTable { - z_nodes: &'static [f32], - coeffs: &'static CoefficientArray, -} - -impl RGBToSpectrumTable { - pub fn srgb() -> Self { - // use crate::core::constants::{RGB_TO_SPECTRUM_Z_NODES, RGB_TO_SPECTRUM_COEFFS}; - // Self::new(&RGB_TO_SPECTRUM_Z_NODES, &RGB_TO_SPECTRUM_COEFFS) - todo!("Link the static constant arrays for sRGB coefficients here") - } -} - -#[derive(Debug, Default, Copy, Clone)] -pub struct RGBSigmoidPolynomial { - c0: Float, - c1: Float, - c2: Float, -} - -impl RGBSigmoidPolynomial { - pub fn new(c0: Float, c1: Float, c2: Float) -> Self { - Self { c0, c1, c2 } - } - - pub fn evaluate(&self, lambda: Float) -> Float { - let eval = - match crate::core::pbrt::evaluate_polynomial(lambda, &[self.c0, self.c1, self.c2]) { - Some(value) => value, - None => { - panic!("evaluate_polynomial returned None with non-empty coefficients") - } - }; - - Self::s(eval) - } - - pub fn max_value(&self) -> Float { - let lambda = -self.c1 / (2.0 * self.c0); - let result = self.evaluate(360.0).max(self.evaluate(830.0)); - if (360.0..830.0).contains(&lambda) { - return result.max(self.evaluate(lambda)); - } - result - } - - fn s(x: Float) -> Float { - if x.is_infinite() { - if x > 0.0 { return 1.0 } else { return 0.0 } - } - 0.5 + x / (2.0 * (1.0 + (x * x)).sqrt()) - } -} - -impl RGBToSpectrumTable { - pub fn new(z_nodes: &'static [f32], coeffs: &'static CoefficientArray) -> Self { - Self { z_nodes, coeffs } - } - - pub fn to_polynomial(&self, rgb: RGB) -> RGBSigmoidPolynomial { - if rgb[0] == rgb[1] && rgb[1] == rgb[2] { - return RGBSigmoidPolynomial::new( - 0.0, - 0.0, - (rgb[0] - 0.5) / (rgb[0] * (1.0 - rgb[0])).sqrt(), - ); - } - let maxc; - if rgb[0] > rgb[1] { - if rgb[0] > rgb[2] { - maxc = 0; - } else { - maxc = 2; - } - } else if rgb[1] > rgb[2] { - maxc = 1; - } else { - maxc = 2; - } - - let z = rgb[maxc]; - let x = rgb[(maxc + 1) % 3] * (RES - 1) as Float / z; - let y = rgb[(maxc + 2) % 3] * (RES - 1) as Float / z; - - let xi = x.min(RES as Float - 2.0); - let yi = y.min(RES as Float - 2.0); - let zi = crate::core::pbrt::find_interval(RES, |i: usize| self.z_nodes[i] < z); - let dx = (x - xi) as usize; - let dy = (y - yi) as usize; - let dz = (z - self.z_nodes[zi]) / (self.z_nodes[zi + 1] - self.z_nodes[zi]); - let mut c = [0.0; 3]; - #[allow(clippy::needless_range_loop)] - for i in 0..3 { - let co = |dx: usize, dy: usize, dz: usize| { - self.coeffs[maxc][zi as usize + dz][yi as usize + dy][xi as usize + dx][i] - }; - c[i] = lerp( - dz, - lerp( - dy as Float, - lerp(dx as Float, co(0, 0, 0) as Float, co(1, 0, 0)) as Float, - lerp(dx as Float, co(0, 1, 0) as Float, co(1, 1, 0) as Float), - ), - lerp( - dy as Float, - lerp(dx as Float, co(0, 0, 1) as Float, co(1, 0, 1)) as Float, - lerp(dx as Float, co(0, 1, 1) as Float, co(1, 1, 1) as Float), - ), - ); - } - RGBSigmoidPolynomial { - c0: c[0], - c1: c[1], - c2: c[2], - } - } -} - -const LMS_FROM_XYZ: SquareMatrix = SquareMatrix::new([ - [0.8951, 0.2664, -0.1614], - [-0.7502, 1.7135, 0.0367], - [0.0389, -0.0685, 1.0296], -]); - -const XYZ_FROM_LMS: SquareMatrix = 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) -> SquareMatrix { - // Find LMS coefficients for source and target white - 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; - - // Return white balancing matrix for source and target white - let lms_correct = SquareMatrix::::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 -} - -#[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]); - fn to_float_linear(&self, v: Float) -> Float; - fn type_id(&self) -> TypeId { - TypeId::of::() - } -} - -#[enum_dispatch(ColorEncodingTrait)] -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum ColorEncoding { - Linear(LinearEncoding), - SRGB(SRGBEncoding), -} - -impl fmt::Display for ColorEncoding { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Encoding") - } -} - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub struct LinearEncoding; -impl ColorEncodingTrait for LinearEncoding { - fn from_linear_slice(&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]) { - for (i, &v) in vin.iter().enumerate() { - vout[i] = v as Float / 255.0; - } - } - fn to_float_linear(&self, v: Float) -> Float { - v - } -} - -impl fmt::Display for LinearEncoding { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Linear") - } -} - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub struct SRGBEncoding; -impl ColorEncodingTrait for SRGBEncoding { - fn from_linear_slice(&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 { - v * 12.92 - } else { - 1.055 * v.powf(1.0 / 2.4) - 0.055 - }; - vout[i] = (v_encoded * 255.0 + 0.5) as u8; - } - } - - fn to_linear_slice(&self, vin: &[u8], vout: &mut [Float]) { - for (i, &v) in vin.iter().enumerate() { - vout[i] = SRGB_TO_LINEAR_LUT[v as usize]; - } - } - - fn to_float_linear(&self, v: Float) -> Float { - let v = v.clamp(0.0, 1.0); - if v <= 0.04045 { - v / 12.92 - } else { - ((v + 0.055) / 1.055).powf(2.4) - } - } -} - -impl fmt::Display for SRGBEncoding { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "sRGB") - } -} - -pub const LINEAR: ColorEncoding = ColorEncoding::Linear(LinearEncoding); -pub const SRGB: ColorEncoding = ColorEncoding::SRGB(SRGBEncoding); - -const SRGB_TO_LINEAR_LUT: [Float; 256] = [ - 0.0000000000, - 0.0003035270, - 0.0006070540, - 0.0009105810, - 0.0012141080, - 0.0015176350, - 0.0018211619, - 0.0021246888, - 0.0024282159, - 0.0027317430, - 0.0030352699, - 0.0033465356, - 0.0036765069, - 0.0040247170, - 0.0043914421, - 0.0047769533, - 0.0051815170, - 0.0056053917, - 0.0060488326, - 0.0065120910, - 0.0069954102, - 0.0074990317, - 0.0080231922, - 0.0085681248, - 0.0091340570, - 0.0097212177, - 0.0103298230, - 0.0109600937, - 0.0116122449, - 0.0122864870, - 0.0129830306, - 0.0137020806, - 0.0144438436, - 0.0152085144, - 0.0159962922, - 0.0168073755, - 0.0176419523, - 0.0185002182, - 0.0193823613, - 0.0202885624, - 0.0212190095, - 0.0221738834, - 0.0231533647, - 0.0241576303, - 0.0251868572, - 0.0262412224, - 0.0273208916, - 0.0284260381, - 0.0295568332, - 0.0307134409, - 0.0318960287, - 0.0331047624, - 0.0343398079, - 0.0356013142, - 0.0368894450, - 0.0382043645, - 0.0395462364, - 0.0409151986, - 0.0423114114, - 0.0437350273, - 0.0451862030, - 0.0466650836, - 0.0481718220, - 0.0497065634, - 0.0512694679, - 0.0528606549, - 0.0544802807, - 0.0561284944, - 0.0578054339, - 0.0595112406, - 0.0612460710, - 0.0630100295, - 0.0648032799, - 0.0666259527, - 0.0684781820, - 0.0703601092, - 0.0722718611, - 0.0742135793, - 0.0761853904, - 0.0781874284, - 0.0802198276, - 0.0822827145, - 0.0843762159, - 0.0865004659, - 0.0886556059, - 0.0908417329, - 0.0930589810, - 0.0953074843, - 0.0975873619, - 0.0998987406, - 0.1022417471, - 0.1046164930, - 0.1070231125, - 0.1094617173, - 0.1119324341, - 0.1144353822, - 0.1169706732, - 0.1195384338, - 0.1221387982, - 0.1247718409, - 0.1274376959, - 0.1301364899, - 0.1328683347, - 0.1356333494, - 0.1384316236, - 0.1412633061, - 0.1441284865, - 0.1470272839, - 0.1499598026, - 0.1529261619, - 0.1559264660, - 0.1589608639, - 0.1620294005, - 0.1651322246, - 0.1682693958, - 0.1714410931, - 0.1746473908, - 0.1778884083, - 0.1811642349, - 0.1844749898, - 0.1878207624, - 0.1912016720, - 0.1946178079, - 0.1980693042, - 0.2015562356, - 0.2050787061, - 0.2086368501, - 0.2122307271, - 0.2158605307, - 0.2195262313, - 0.2232279778, - 0.2269658893, - 0.2307400703, - 0.2345506549, - 0.2383976579, - 0.2422811985, - 0.2462013960, - 0.2501583695, - 0.2541521788, - 0.2581829131, - 0.2622507215, - 0.2663556635, - 0.2704978585, - 0.2746773660, - 0.2788943350, - 0.2831487954, - 0.2874408960, - 0.2917706966, - 0.2961383164, - 0.3005438447, - 0.3049873710, - 0.3094689548, - 0.3139887452, - 0.3185468316, - 0.3231432438, - 0.3277781308, - 0.3324515820, - 0.3371636569, - 0.3419144452, - 0.3467040956, - 0.3515326977, - 0.3564002514, - 0.3613068759, - 0.3662526906, - 0.3712377846, - 0.3762622178, - 0.3813261092, - 0.3864295185, - 0.3915725648, - 0.3967553079, - 0.4019778669, - 0.4072403014, - 0.4125427008, - 0.4178851545, - 0.4232677519, - 0.4286905527, - 0.4341537058, - 0.4396572411, - 0.4452012479, - 0.4507858455, - 0.4564110637, - 0.4620770514, - 0.4677838385, - 0.4735315442, - 0.4793202281, - 0.4851499796, - 0.4910208881, - 0.4969330430, - 0.5028865933, - 0.5088814497, - 0.5149177909, - 0.5209956765, - 0.5271152258, - 0.5332764983, - 0.5394796133, - 0.5457245708, - 0.5520114899, - 0.5583404899, - 0.5647116303, - 0.5711249113, - 0.5775805116, - 0.5840784907, - 0.5906189084, - 0.5972018838, - 0.6038274169, - 0.6104956269, - 0.6172066331, - 0.6239604354, - 0.6307572126, - 0.6375969648, - 0.6444797516, - 0.6514056921, - 0.6583748460, - 0.6653873324, - 0.6724432111, - 0.6795425415, - 0.6866854429, - 0.6938719153, - 0.7011020184, - 0.7083759308, - 0.7156936526, - 0.7230552435, - 0.7304608822, - 0.7379105687, - 0.7454043627, - 0.7529423237, - 0.7605246305, - 0.7681512833, - 0.7758223414, - 0.7835379243, - 0.7912980318, - 0.7991028428, - 0.8069523573, - 0.8148466945, - 0.8227858543, - 0.8307699561, - 0.8387991190, - 0.8468732834, - 0.8549926877, - 0.8631572723, - 0.8713672161, - 0.8796223402, - 0.8879231811, - 0.8962693810, - 0.9046613574, - 0.9130986929, - 0.9215820432, - 0.9301108718, - 0.9386858940, - 0.9473065734, - 0.9559735060, - 0.9646862745, - 0.9734454751, - 0.9822505713, - 0.9911022186, - 1.0000000000, -]; diff --git a/src/utils/colorspace.rs b/src/utils/colorspace.rs deleted file mode 100644 index 6f31058..0000000 --- a/src/utils/colorspace.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::color::{RGB, RGBSigmoidPolynomial, RGBToSpectrumTable, XYZ}; -use super::math::SquareMatrix; -use crate::core::pbrt::Float; -use crate::geometry::Point2f; -use crate::spectra::{DenselySampledSpectrum, SampledSpectrum, Spectrum}; - -use once_cell::sync::Lazy; - -use std::cmp::{Eq, PartialEq}; -use std::error::Error; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub struct RGBColorSpace { - pub r: Point2f, - pub g: Point2f, - pub b: Point2f, - pub w: Point2f, - pub illuminant: Spectrum, - pub rgb_to_spectrum_table: Arc, - pub xyz_from_rgb: SquareMatrix, - pub rgb_from_xyz: SquareMatrix, -} - -impl RGBColorSpace { - pub fn new( - r: Point2f, - g: Point2f, - b: Point2f, - illuminant: Spectrum, - rgb_to_spectrum_table: RGBToSpectrumTable, - ) -> Result> { - let w_xyz: XYZ = illuminant.to_xyz(); - let w = w_xyz.xy(); - let r_xyz = XYZ::from_xyy(r, Some(1.0)); - let g_xyz = XYZ::from_xyy(g, Some(1.0)); - let b_xyz = XYZ::from_xyy(b, Some(1.0)); - let rgb_values = [ - [r_xyz.x(), g_xyz.x(), b_xyz.x()], - [r_xyz.y(), g_xyz.y(), b_xyz.y()], - [r_xyz.z(), g_xyz.z(), g_xyz.z()], - ]; - let rgb = SquareMatrix::new(rgb_values); - let c: RGB = rgb.inverse()? * w_xyz; - let xyz_from_rgb = rgb * SquareMatrix::diag(&[c.r, c.g, c.b]); - let rgb_from_xyz = xyz_from_rgb - .inverse() - .expect("XYZ from RGB matrix is singular"); - - Ok(Self { - r, - g, - b, - w, - illuminant, - rgb_to_spectrum_table: Arc::new(rgb_to_spectrum_table), - xyz_from_rgb, - rgb_from_xyz, - }) - } - - pub fn to_xyz(&self, rgb: RGB) -> XYZ { - self.xyz_from_rgb * rgb - } - - pub fn to_rgb(&self, xyz: XYZ) -> RGB { - self.rgb_from_xyz * xyz - } - - pub fn to_rgb_coeffs(&self, rgb: RGB) -> RGBSigmoidPolynomial { - self.rgb_to_spectrum_table.to_polynomial(rgb) - } - - pub fn convert_colorspace(&self, other: &RGBColorSpace) -> SquareMatrix { - if self == other { - return SquareMatrix::default(); - } - - self.rgb_from_xyz * other.xyz_from_rgb - } - - pub fn srgb() -> &'static Self { - static SRGB_SPACE: Lazy = Lazy::new(|| { - let r = Point2f::new(0.64, 0.33); - let g = Point2f::new(0.30, 0.60); - let b = Point2f::new(0.15, 0.06); - - let illuminant = Spectrum::std_illuminant_d65(); - let table = RGBToSpectrumTable::srgb(); - - RGBColorSpace::new(r, g, b, illuminant, table) - .expect("Failed to initialize standard sRGB color space") - }); - - &SRGB_SPACE - } -} - -impl PartialEq for RGBColorSpace { - fn eq(&self, other: &Self) -> bool { - self.r == other.r - && self.g == other.g - && self.b == other.b - && self.w == other.w - && Arc::ptr_eq(&self.rgb_to_spectrum_table, &other.rgb_to_spectrum_table) - } -} diff --git a/src/utils/error.rs b/src/utils/error.rs index 247e1b6..1afd2d8 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,39 +1,36 @@ -use image_rs::{ImageError as IError, error}; -use std::fmt; -use thiserror::Error; - -use crate::image::PixelFormat; - -#[derive(Error, Debug)] -pub enum LlsError { - SingularMatrix, +#[derive(Clone, Debug)] +pub struct FileLoc { + pub filename: Arc, + pub line: i32, + pub column: i32, } -#[derive(Error, Debug, Clone, PartialEq, Eq)] -pub enum InversionError { - SingularMatrix, - EmptyMatrix, -} - -impl fmt::Display for LlsError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - LlsError::SingularMatrix => write!(f, "Matrix is singular and cannot be inverted."), +impl Default for FileLoc { + fn default() -> Self { + Self { + filename: Arc::from(""), + line: 1, + column: 0, } } } -impl fmt::Display for InversionError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - InversionError::SingularMatrix => { - write!(f, "Matrix is singular and cannot be inverted.") - } - InversionError::EmptyMatrix => write!(f, "Matrix is empty and cannot be inverted."), +impl FileLoc { + pub fn new(filename: &str) -> Self { + Self { + filename: Arc::from(filename), + line: 1, + column: 0, } } } +impl fmt::Display for FileLoc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}:{}", self.filename, self.line, self.column) + } +} + #[derive(Error, Debug)] pub enum ImageError { #[error("I/O error: {0}")] diff --git a/src/utils/file.rs b/src/utils/file.rs new file mode 100644 index 0000000..89b1c52 --- /dev/null +++ b/src/utils/file.rs @@ -0,0 +1,72 @@ +use pbrt::Float; +use std::fs; +use std::io::{self, BufReader, Error, ErrorKind, Read}; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +static SEARCH_DIRECTORY: OnceLock = OnceLock::new(); + +fn set_search_directory(filename: &str) { + let path = Path::new(filename); + let dir = if path.is_dir() { + path.to_path_buf() + } else { + path.parent().unwrap_or(Path::new("")).to_path_buf() + }; + let _ = SEARCH_DIRECTORY.set(dir); +} + +pub fn read_float_file(filename: &str) -> io::Result> { + let content = fs::read_to_string(filename)?; + + let mut values = Vec::new(); + + for (line_num, line) in content.lines().enumerate() { + // Strip comments + let cleaned = line.split('#').next().unwrap_or(""); + + for token in cleaned.split_whitespace() { + match token.parse::() { + Ok(val) => values.push(val), + Err(e) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "Failed to parse float '{}' at line {}: {}", + token, + line_num + 1, + e + ), + )); + } + } + } + } + + Ok(values) +} + +pub fn resolve_filename(filename: &str) -> String { + let search_directory = SEARCH_DIRECTORY + .get() + .map(|p| p.as_path()) + .unwrap_or(Path::new("")); + + if search_directory.as_os_str().is_empty() + || filename.is_empty() + || Path::new(filename).is_absolute() + { + return filename.to_string(); + } + + let filepath = search_directory.join(filename); + if filepath.exists() { + filepath + .canonicalize() + .unwrap_or_else(|_| filepath.to_path_buf()) + .to_string_lossy() + .to_string() + } else { + filename.to_string() + } +} diff --git a/src/image/io.rs b/src/utils/io.rs similarity index 93% rename from src/image/io.rs rename to src/utils/io.rs index cc9d9ff..1b094cb 100644 --- a/src/image/io.rs +++ b/src/utils/io.rs @@ -1,19 +1,29 @@ -use crate::core::pbrt::Float; -use crate::image::{ - Image, ImageAndMetadata, ImageMetadata, PixelData, PixelFormat, Point2i, WrapMode, -}; -use crate::utils::color::{ColorEncoding, LINEAR, SRGB}; +use crate::spectra::color::{ColorEncoding, LINEAR, SRGB}; 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 pbrt::Float; +use pbrt::image::{ + Image, ImageAndMetadata, ImageMetadata, PixelData, PixelFormat, Point2i, WrapMode, +}; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write}; use std::path::Path; -impl Image { - pub fn read(path: &Path, encoding: Option) -> Result { +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()) @@ -27,7 +37,7 @@ impl Image { } } - pub fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + 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() { diff --git a/src/utils/mipmap.rs b/src/utils/mipmap.rs index b06bcbe..5c02bd0 100644 --- a/src/utils/mipmap.rs +++ b/src/utils/mipmap.rs @@ -1,9 +1,8 @@ -use crate::core::pbrt::{Float, lerp}; -use crate::geometry::{Lerp, Point2f, Point2i, Vector2f, VectorLike}; +use crate::core::geometry::{Lerp, Point2f, Point2i, Vector2f, VectorLike}; +use crate::core::pbrt::Float; use crate::image::{Image, ImageAndMetadata, PixelData, PixelFormat, WrapMode, WrapMode2D}; -use crate::utils::color::{ColorEncoding, RGB}; -use crate::utils::colorspace::RGBColorSpace; -use crate::utils::math::{safe_sqrt, square}; +use crate::spectra::{RGB, RGBColorSpace, color::ColorEncoding}; +use crate::utils::math::{lerp, safe_sqrt, square}; use std::path::Path; use std::hash::{Hash, Hasher}; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bb5fc5e..a4a05c7 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,79 +1,6 @@ -use std::sync::atomic::{AtomicU64, Ordering}; - -pub mod color; -pub mod colorspace; -pub mod containers; pub mod error; -pub mod hash; -pub mod interval; -pub mod math; -pub mod mesh; +pub mod file; +pub mod io; pub mod mipmap; -pub mod quaternion; -pub mod rng; -pub mod sampling; -pub mod scattering; -pub mod sobol; -pub mod splines; -pub mod transform; - -#[inline] -pub fn partition_slice(data: &mut [T], predicate: F) -> usize -where - F: Fn(&T) -> bool, -{ - let mut i = 0; - for j in 0..data.len() { - if predicate(&data[j]) { - data.swap(i, j); - i += 1; - } - } - i -} - -#[derive(Debug)] -pub struct AtomicFloat { - bits: AtomicU64, -} - -impl AtomicFloat { - pub fn new(value: f64) -> Self { - Self { - bits: AtomicU64::new(value.to_bits()), - } - } - - pub fn load(&self) -> f64 { - f64::from_bits(self.bits.load(Ordering::Relaxed)) - } - - pub fn store(&self, value: f64) { - self.bits.store(value.to_bits(), Ordering::Relaxed); - } - - pub fn add(&self, value: f64) { - let mut current_bits = self.bits.load(Ordering::Relaxed); - loop { - let current_val = f64::from_bits(current_bits); - let new_val = current_val + value; - 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, - } - } - } -} - -impl Default for AtomicFloat { - fn default() -> Self { - Self::new(0.0) - } -} +pub mod parameters; +pub mod parser; diff --git a/src/utils/parameters.rs b/src/utils/parameters.rs new file mode 100644 index 0000000..86ac4f1 --- /dev/null +++ b/src/utils/parameters.rs @@ -0,0 +1,786 @@ +use crate::Float; +use crate::core::geometry::{Normal3f, Point2f, Point3f, Vector2f, Vector3f}; +use crate::core::options::get_options; +use crate::core::texture::{ + FloatConstantTexture, FloatTexture, SpectrumConstantTexture, SpectrumTexture, SpectrumType, +}; +use crate::spectra::{ + BlackbodySpectrum, ConstantSpectrum, PiecewiseLinearSpectrum, RGB, RGBAlbedoSpectrum, + RGBColorSpace, RGBIlluminantSpectrum, RGBUnboundedSpectrum, SampledSpectrum, Spectrum, +}; +use crate::utils::error::FileLoc; + +use once_cell::sync::Lazy; +use std::cell::Cell; +use std::collections::HashMap; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; + +static CACHED_SPECTRA: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +#[derive(Debug)] +pub struct ParsedParameter { + pub type_name: String, + pub name: String, + pub loc: FileLoc, + + pub floats: Vec, + pub ints: Vec, + pub strings: Vec, + pub bools: Vec, + + pub looked_up: AtomicBool, + pub may_be_unused: bool, + pub color_space: Option>, +} + +impl Default for ParsedParameter { + fn default() -> Self { + Self { + type_name: String::new(), + name: String::new(), + loc: FileLoc::default(), + floats: Vec::new(), + ints: Vec::new(), + strings: Vec::new(), + bools: Vec::new(), + looked_up: AtomicBool::new(false), + may_be_unused: false, + color_space: None, + } + } +} + +impl Clone for ParsedParameter { + fn clone(&self) -> Self { + let looked_up_val = self.looked_up.load(Ordering::Relaxed); + + Self { + type_name: self.type_name.clone(), + name: self.name.clone(), + loc: self.loc.clone(), + floats: self.floats.clone(), + ints: self.ints.clone(), + strings: self.strings.clone(), + bools: self.bools.clone(), + looked_up: AtomicBool::new(looked_up_val), + may_be_unused: self.may_be_unused, + color_space: self.color_space.clone(), + } + } +} + +impl ParsedParameter { + pub fn new(loc: FileLoc) -> Self { + Self { + type_name: String::new(), + name: String::new(), + loc, + floats: Vec::new(), + ints: Vec::new(), + strings: Vec::new(), + bools: Vec::new(), + looked_up: AtomicBool::new(false), + may_be_unused: false, + color_space: None, + } + } + + pub fn add_float(&mut self, v: Float) { + self.floats.push(v); + } + pub fn add_int(&mut self, i: i32) { + self.ints.push(i); + } + pub fn add_string(&mut self, s: String) { + self.strings.push(s); + } + pub fn add_bool(&mut self, v: bool) { + self.bools.push(v); + } + + pub fn to_string(&self) -> String { + format!("[ ParsedParameter {} {} ]", self.type_name, self.name) + } +} + +#[derive(Default)] +pub struct NamedTextures { + pub float_textures: HashMap>, + pub albedo_spectrum_textures: HashMap>, + pub unbounded_spectrum_textures: HashMap>, + pub illuminant_spectrum_textures: HashMap>, +} + +#[derive(Debug, Default, Clone)] +pub struct ParameterDictionary { + pub params: Vec, + pub color_space: Option>, +} + +impl ParameterDictionary { + pub fn new(params: Vec, color_space: Option>) -> Self { + Self { + params, + color_space, + } + } + + 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(); + } + } + } + } + default_val + } + + fn lookup_array( + &self, + name: &str, + extractor: impl Fn(&ParsedParameter) -> Option<&Vec>, + ) -> Vec { + 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(); + } + } + } + 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 + } + }) + } + + 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 + } + }) + } + + 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 + } + }) + } + + 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() + } + + 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 + } + } + + 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 + } + } + + pub fn get_one_spectrum( + &self, + name: &str, + def: Option, + stype: SpectrumType, + ) -> Option { + for param in &self.params { + if param.name == name { + param.looked_up.store(true, Ordering::Relaxed); + if param.type_name == "spectrum" + || param.type_name == "rgb" + || param.type_name == "blackbody" + { + return Some(self.extract_spectrum_array(param, stype)[0].clone()); + } + } + } + def + } + + pub fn get_spectrum_array(&self, name: &str, stype: SpectrumType) -> Vec { + for param in &self.params { + if param.name == name { + param.looked_up.store(true, Ordering::Relaxed); + if param.type_name == "spectrum" + || param.type_name == "rgb" + || param.type_name == "blackbody" + { + return self.extract_spectrum_array(param, stype); + } + } + } + Vec::new() + } + + pub fn get_texture(&self, name: &str) -> String { + for p in &self.params { + if p.name == name || p.type_name != "texture" { + if p.strings.len() != 1 { + panic!( + "[{:?}] Expected 1 texture name for {}, found {}", + p.loc, + name, + p.strings.len() + ); + } + p.looked_up.store(true, Ordering::Relaxed); + return p.strings[0].clone(); + } + } + return "".to_string(); + } + + pub fn report_unused(&self) { + for param in &self.params { + if !param.looked_up.load(Ordering::Relaxed) && !param.may_be_unused { + eprintln!( + "Warning: Parameter '{}' ({}) was unused.", + param.name, param.type_name + ); + } + } + } + + pub fn remove_float(&mut self, name: &str) { + self.remove(name, "float"); + } + pub fn remove_int(&mut self, name: &str) { + self.remove(name, "integer"); + } + pub fn remove_bool(&mut self, name: &str) { + self.remove(name, "bool"); + } + + fn remove(&mut self, name: &str, type_name: &str) { + self.params + .retain(|p| !(p.name == name && p.type_name == type_name)); + } + + pub fn rename_parameter(&mut self, before: &str, after: &str) { + for param in &mut self.params { + if param.name == before { + param.name = after.to_string(); + } + } + } + + fn extract_spectrum_array( + &self, + 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]); + + if rgb.r < 0.0 || rgb.g < 0.0 || rgb.b < 0.0 { + panic!( + "{}: RGB parameter '{}' has negative component.", + param.loc, param.name + ); + } + + 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 + ); + } + + 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 + ); + } + } + 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(); + } + + Vec::new() + } +} + +fn read_spectrum_from_file(filename: &str) -> Result { + let fn_key = filename.to_string(); + { + let cache = CACHED_SPECTRA.lock().unwrap(); + if let Some(s) = cache.get(&fn_key) { + return Ok(s.clone()); + } + } + + let pls = PiecewiseLinearSpectrum::read(&fn_key) + .ok_or_else(|| format!("unable to read or parse spectrum file '{}'", fn_key))?; + + let spectrum = Spectrum::PiecewiseLinear(pls); + + { + let mut cache = CACHED_SPECTRA.lock().unwrap(); + cache.insert(fn_key, spectrum.clone()); + } + + Ok(spectrum) +} + +pub type ParsedParameterVector = Vec; + +pub struct TextureParameterDictionary { + dict: Arc, + textures: Option, +} + +impl TextureParameterDictionary { + pub fn new(dict: Arc, textures: Option) -> Self { + Self { dict, textures } + } + + pub fn get_one_float(&self, name: &str, def: Float) -> Float { + self.dict.get_one_float(name, def) + } + + pub fn get_one_int(&self, name: &str, def: i32) -> i32 { + self.dict.get_one_int(name, def) + } + + pub fn get_one_bool(&self, name: &str, def: bool) -> bool { + self.dict.get_one_bool(name, def) + } + + pub fn get_one_string(&self, name: &str, def: &str) -> String { + self.dict.get_one_string(name, def) + } + + pub fn get_float_array(&self, name: &str) -> Vec { + self.dict.get_float_array(name) + } + + pub fn get_int_array(&self, name: &str) -> Vec { + self.dict.get_int_array(name) + } + + pub fn get_bool_array(&self, name: &str) -> Vec { + self.dict.get_bool_array(name) + } + + pub fn get_string_array(&self, name: &str) -> Vec { + self.dict.get_string_array(name) + } + + pub fn get_one_point3f(&self, name: &str, def: Point3f) -> Point3f { + self.dict.get_one_point3f(name, def) + } + + pub fn get_one_vector3f(&self, name: &str, def: Vector3f) -> Vector3f { + self.dict.get_one_vector3f(name, def) + } + + pub fn get_one_normal3f(&self, name: &str, def: Normal3f) -> Normal3f { + self.dict.get_one_normal3f(name, def) + } + + pub fn get_point3f_array(&self, name: &str) -> Vec { + self.dict.get_point3f_array(name) + } + + pub fn get_vector3f_array(&self, name: &str) -> Vec { + self.dict.get_vector3f_array(name) + } + + pub fn get_normal3f_array(&self, name: &str) -> Vec { + self.dict.get_normal3f_array(name) + } + + pub fn get_one_point2f(&self, name: &str, def: Point2f) -> Point2f { + self.dict.get_one_point2f(name, def) + } + + pub fn get_one_vector2f(&self, name: &str, def: Vector2f) -> Vector2f { + self.dict.get_one_vector2f(name, def) + } + + pub fn get_one_spectrum( + &self, + name: &str, + def: Option, + stype: SpectrumType, + ) -> Option { + self.dict.get_one_spectrum(name, def, stype) + } + + pub fn get_spectrum_array(&self, name: &str, stype: SpectrumType) -> Vec { + self.dict.get_spectrum_array(name, stype) + } + + pub fn get_texture(&self, name: &str) -> String { + self.dict.get_texture(name) + } + + pub fn report_unused(&self) { + self.dict.report_unused() + } + + pub fn get_spectrum_texture( + &self, + name: &str, + val: Option, + stype: SpectrumType, + ) -> Option> { + let tex = self.get_spectrum_texture_or_null(name, stype); + if tex.is_some() { + tex + } else if val.is_some() { + Some(Arc::new(SpectrumTexture::SpectrumConstant( + SpectrumConstantTexture::new(val.unwrap()), + ))) + } else { + None + } + } + + pub fn get_float_texture(&self, name: &str, val: Float) -> Arc { + if let Some(tex) = self.get_float_texture_or_null(name) { + return tex; + } else { + return Arc::new(FloatTexture::FloatConstant(FloatConstantTexture::new(val))); + } + } + + fn get_spectrum_texture_or_null( + &self, + name: &str, + stype: SpectrumType, + ) -> Option> { + for p in &self.dict.params { + if p.name != name { + continue; + } + + match p.type_name.as_str() { + "texture" => { + if p.strings.len() != 1 { + panic!( + "[{:?}] Expected 1 texture name for {}, found {}", + p.loc, + name, + p.strings.len() + ); + } + + p.looked_up.store(true, Ordering::Relaxed); + let tex_name = &p.strings[0]; + + if let Some(nt) = &self.textures { + let map = match stype { + SpectrumType::Unbounded => &nt.unbounded_spectrum_textures, + SpectrumType::Albedo => &nt.albedo_spectrum_textures, + _ => &nt.illuminant_spectrum_textures, + }; + + if let Some(tex) = map.get(tex_name) { + return Some(Arc::clone(tex)); + } + panic!( + "[{:?}] Couldn't find spectrum texture named '{}'", + p.loc, tex_name + ); + } + return None; + } + + "rgb" => { + if p.floats.len() != 3 { + panic!("[{:?}] RGB parameter '{}' needs 3 floats", p.loc, name); + } + p.looked_up.store(true, Ordering::Relaxed); + + let rgb = RGB::new(p.floats[0], p.floats[1], p.floats[2]); + if rgb.r < 0.0 || rgb.g < 0.0 || rgb.b < 0.0 { + panic!("[{:?}] Negative RGB values for '{}'", p.loc, name); + } + + let cs = self.dict.color_space.as_ref().unwrap(); + let s: Spectrum = match stype { + SpectrumType::Illuminant => { + Spectrum::RGBIlluminant(RGBIlluminantSpectrum::new(cs, rgb)) + } + SpectrumType::Unbounded => { + Spectrum::RGBUnbounded(RGBUnboundedSpectrum::new(cs, rgb)) + } + SpectrumType::Albedo => { + if rgb.r > 1.0 || rgb.g > 1.0 || rgb.b > 1.0 { + panic!("[{:?}] Albedo RGB > 1 for '{}'", p.loc, name); + } + Spectrum::RGBAlbedo(RGBAlbedoSpectrum::new(cs, rgb)) + } + }; + return Some(Arc::new(SpectrumTexture::SpectrumConstant( + SpectrumConstantTexture::new(s), + ))); + } + + "spectrum" | "blackbody" => { + let s = self.dict.get_one_spectrum(name, None, stype)?; + return Some(Arc::new(SpectrumTexture::SpectrumConstant( + SpectrumConstantTexture::new(s), + ))); + } + + _ => {} + } + } + return None; + } + + fn get_float_texture_or_null(&self, name: &str) -> Option> { + for p in &self.dict.params { + if p.name != name { + continue; + } + + match p.type_name.as_str() { + "texture" => { + if p.strings.len() != 1 { + panic!( + "[{:?}] Expected 1 texture name for {}, found {}", + p.loc, + name, + p.strings.len() + ); + } + + p.looked_up.store(true, Ordering::Relaxed); + let tex_name = &p.strings[0]; + + if let Some(nt) = &self.textures { + let map = &nt.float_textures; + if let Some(tex) = map.get(tex_name) { + return Some(Arc::clone(tex)); + } + panic!( + "[{:?}] Couldn't find float texture named '{}'", + p.loc, tex_name + ); + } + return None; + } + "float" => { + let v = self.get_one_float(name, 0.); + return Some(Arc::new(FloatTexture::FloatConstant( + FloatConstantTexture::new(v), + ))); + } + _ => { + panic!("[{:?}] Couldn't find float texture", p.loc); + } + } + } + return None; + } +} diff --git a/src/utils/parser.rs b/src/utils/parser.rs new file mode 100644 index 0000000..c6396eb --- /dev/null +++ b/src/utils/parser.rs @@ -0,0 +1,1085 @@ +use crate::utils::parameters::{ParameterDictionary, ParsedParameter}; +use flate2::read::GzDecoder; +use memmap2::Mmap; +use std::collections::HashMap; +use std::fs::File; +use std::io::{self, Read}; +use std::iter::Peekable; +use std::path::{Path, PathBuf}; +use std::str::CharIndices; +use std::sync::Arc; + +use crate::utils::parameters::ParsedParameterVector; +use crate::{Float, utils::error::FileLoc}; + +pub trait ParserTarget { + fn identity(&mut self, loc: FileLoc); + fn translate(&mut self, dx: Float, dy: Float, dz: Float, loc: FileLoc); + fn rotate(&mut self, angle: Float, ax: Float, ay: Float, az: Float, loc: FileLoc); + fn scale(&mut self, sx: Float, sy: Float, sz: Float, loc: FileLoc); + fn look_at( + &mut self, + ex: Float, + ey: Float, + ez: Float, + lx: Float, + ly: Float, + lz: Float, + ux: Float, + uy: Float, + uz: Float, + loc: FileLoc, + ); + fn transform(&mut self, transform: &[Float; 16], loc: FileLoc); + fn concat_transform(&mut self, transform: &[Float; 16], loc: FileLoc); + + fn coordinate_system(&mut self, name: &str, loc: FileLoc); + fn coord_sys_transform(&mut self, name: &str, loc: FileLoc); + fn active_transform_all(&mut self, loc: FileLoc); + fn active_transform_end_time(&mut self, loc: FileLoc); + fn active_transform_start_time(&mut self, loc: FileLoc); + fn transform_times(&mut self, start: Float, end: Float, loc: FileLoc); + + fn option(&mut self, name: &str, value: &str, loc: FileLoc); + fn color_space(&mut self, n: &str, loc: FileLoc); + fn pixel_filter(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn film(&mut self, type_name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn accelerator(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn integrator(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn camera(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn make_named_medium(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn medium_interface(&mut self, inside_name: &str, outside_name: &str, loc: FileLoc); + fn sampler(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + + fn world_begin(&mut self, loc: FileLoc); + fn attribute_begin(&mut self, loc: FileLoc); + fn attribute_end(&mut self, loc: FileLoc); + fn attribute(&mut self, target: &str, params: ParsedParameterVector, loc: FileLoc); + + fn texture( + &mut self, + name: &str, + type_name: &str, + tex_name: &str, + params: &ParsedParameterVector, + loc: FileLoc, + ); + fn material(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn make_named_material(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn named_material(&mut self, name: &str, loc: FileLoc); + + fn light_source(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn area_light_source(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + + fn shape(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc); + fn reverse_orientation(&mut self, loc: FileLoc); + + fn object_begin(&mut self, name: &str, loc: FileLoc); + fn object_end(&mut self, loc: FileLoc); + fn object_instance(&mut self, name: &str, loc: FileLoc); + + fn end_of_files(&mut self); +} + +#[derive(Debug, Clone)] +pub struct Token { + pub text: String, + pub loc: FileLoc, +} + +impl Token { + pub fn parse_int(&self) -> Result { + match self.text.parse::() { + Ok(val) => Ok(val), + Err(_) => { + if let Ok(val_i64) = self.text.parse::() { + if val_i64 > i32::MAX as i64 { + return Err(ParserError::NumericOverflow( + "Numeric value too large for 32-bit int".into(), + self.loc.clone(), + )); + } + if val_i64 < i32::MIN as i64 { + return Err(ParserError::NumericOverflow( + "Numeric value too low for 32-bit int".into(), + self.loc.clone(), + )); + } + } + + Err(ParserError::ParseIntError( + format!("\"{}\": expected a number", self.text), + self.loc.clone(), + )) + } + } + } + + pub fn parse_float(&self) -> Result { + self.text.parse::().map_err(|_| { + ParserError::ParseFloatError( + format!("\"{}\": expected a number", self.text), + self.loc.clone(), + ) + }) + } + + fn is_quoted(&self) -> bool { + self.text.len() >= 2 && self.text.starts_with('"') && self.text.ends_with('"') + } + + fn is_quoted_string(s: &str) -> bool { + s.len() >= 2 && s.starts_with('"') && s.ends_with('"') + } + + fn dequote_string(s: &str) -> &str { + if Self::is_quoted_string(s) { + &s[1..s.len() - 1] + } else { + s + } + } + + fn dequote(&self) -> &str { + if self.is_quoted() { + &self.text[1..self.text.len() - 1] + } else { + &self.text + } + } + + pub fn dequote_inplace(&mut self) { + if self.is_quoted() { + self.text = self.text[1..self.text.len() - 1].to_string(); + } + } +} + +#[derive(Debug)] +pub enum ParserError { + Io(String), + UnexpectedEof, + InvalidUtf8(String), + Generic(String, FileLoc), + ParseIntError(String, FileLoc), + ParseFloatError(String, FileLoc), + NumericOverflow(String, FileLoc), +} + +pub enum TokenizerBuffer { + Ram(String), + Mapped(Mmap), +} + +impl TokenizerBuffer { + pub fn as_str(&self) -> &str { + match self { + Self::Ram(s) => s, + Self::Mapped(m) => str::from_utf8(m).expect("File is not valid UTF-8"), + } + } +} + +pub struct Tokenizer { + buffer: TokenizerBuffer, + cursor: usize, + filename: Arc, + line: i32, + column: i32, +} + +impl Tokenizer { + pub fn create_from_file(filename: &str) -> Result { + let buffer = Self::load_buffer(filename)?; + + Ok(Self { + buffer, + cursor: 0, + filename: Arc::from(filename), + line: 1, + column: 0, + }) + } + + fn load_buffer(filename: &str) -> Result { + if filename == "-" { + let mut s = String::new(); + io::stdin() + .read_to_string(&mut s) + .map_err(|e| ParserError::Io(e.to_string()))?; + return Ok(TokenizerBuffer::Ram(s)); + } + + if filename.ends_with(".gz") { + let f = File::open(filename).map_err(|e| ParserError::Io(e.to_string()))?; + let mut d = GzDecoder::new(f); + let mut s = String::new(); + d.read_to_string(&mut s) + .map_err(|e| ParserError::Io(e.to_string()))?; + return Ok(TokenizerBuffer::Ram(s)); + } + + let file = File::open(filename).map_err(|e| ParserError::Io(e.to_string()))?; + let len = file + .metadata() + .map_err(|e| ParserError::Io(e.to_string()))? + .len(); + + if len < 16 * 1024 * 1024 { + let mut s = String::new(); + let mut reader = &file; + reader + .read_to_string(&mut s) + .map_err(|e| ParserError::Io(e.to_string()))?; + return Ok(TokenizerBuffer::Ram(s)); + } + + let mmap = unsafe { Mmap::map(&file).map_err(|e| ParserError::Io(e.to_string()))? }; + + if str::from_utf8(&mmap).is_err() { + return Err(ParserError::InvalidUtf8(filename.to_string())); + } + + Ok(TokenizerBuffer::Mapped(mmap)) + } + + fn peek_char(&self) -> Option { + self.buffer.as_str().get(self.cursor..)?.chars().next() + } + + fn advance(&mut self) -> Option { + let ch = self.peek_char()?; + + self.cursor += ch.len_utf8(); + + if ch == '\n' { + self.line += 1; + self.column = 0; + } else { + self.column += 1; + } + Some(ch) + } + + pub fn next(&mut self) -> Result, ParserError> { + loop { + match self.peek_char() { + Some(ch) if ch.is_whitespace() => { + self.advance(); + continue; + } + Some('#') => { + while let Some(c) = self.peek_char() { + if c == '\n' { + break; + } + self.advance(); + } + continue; + } + None => return Ok(None), + Some(_) => break, + } + } + + let start_loc = FileLoc { + filename: self.filename.clone(), + line: self.line, + column: self.column, + }; + + let first_char = self.peek_char().unwrap(); + + if first_char == '"' { + self.advance(); + let mut val = String::new(); + + loop { + let ch = self.advance().ok_or(ParserError::UnexpectedEof)?; + if ch == '"' { + break; + } + if ch == '\\' { + let escaped = self.advance().ok_or(ParserError::UnexpectedEof)?; + let res = match escaped { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + '"' => '"', + '\\' => '\\', + _ => escaped, + }; + val.push(res); + } else { + val.push(ch); + } + } + return Ok(Some(Token { + text: val, + loc: start_loc, + })); + } + + if first_char == '[' || first_char == ']' { + self.advance(); + return Ok(Some(Token { + text: first_char.to_string(), + loc: start_loc, + })); + } + + let start_idx = self.cursor; + + while let Some(ch) = self.peek_char() { + if ch.is_whitespace() || ch == '"' || ch == '[' || ch == ']' { + break; + } + self.advance(); + } + + let text_slice = &self.buffer.as_str()[start_idx..self.cursor]; + + Ok(Some(Token { + text: text_slice.to_string(), + loc: start_loc, + })) + } +} + +pub struct FormattingParserTarget { + to_ply: bool, + upgrade: bool, + cat_indent_count: usize, + + defined_textures: HashMap, + defined_named_materials: HashMap, + named_material_dictionaries: HashMap, + defined_object_instances: HashMap, +} + +impl FormattingParserTarget { + pub fn new(to_ply: bool, upgrade: bool) -> Self { + Self { + to_ply, + upgrade, + cat_indent_count: 0, + defined_textures: HashMap::new(), + defined_named_materials: HashMap::new(), + named_material_dictionaries: HashMap::new(), + defined_object_instances: HashMap::new(), + } + } + + pub fn indent(&self, extra: usize) -> String { + " ".repeat(self.cat_indent_count + 4 * extra) + } + + fn upgrade_material_index( + &self, + _name: &str, + _dict: &ParameterDictionary, + _loc: FileLoc, + ) -> String { + String::new() + } + + fn upgrade_material( + &self, + _name: &mut String, + _dict: &ParameterDictionary, + _loc: FileLoc, + ) -> String { + String::new() + } +} + +impl ParserTarget for FormattingParserTarget { + fn option(&mut self, name: &str, value: &str, _loc: FileLoc) { + println!("{}Option \"{}\" \"{}\"", self.indent(0), name, value); + } + + fn identity(&mut self, _loc: FileLoc) { + println!("{}Identity", self.indent(0)); + } + + fn translate(&mut self, dx: Float, dy: Float, dz: Float, _loc: FileLoc) { + println!("{}Translate {} {} {}", self.indent(0), dx, dy, dz); + } + + fn rotate(&mut self, angle: Float, ax: Float, ay: Float, az: Float, _loc: FileLoc) { + println!("{}Rotate {} {} {} {}", self.indent(0), angle, ax, ay, az); + } + + fn scale(&mut self, sx: Float, sy: Float, sz: Float, _loc: FileLoc) { + println!("{}Scale {} {} {}", self.indent(0), sx, sy, sz); + } + + fn look_at( + &mut self, + ex: Float, + ey: Float, + ez: Float, + lx: Float, + ly: Float, + lz: Float, + ux: Float, + uy: Float, + uz: Float, + _loc: FileLoc, + ) { + println!( + "{}LookAt {} {} {} {} {} {} {} {} {}", + self.indent(0), + ex, + ey, + ez, + lx, + ly, + lz, + ux, + uy, + uz + ); + } + + fn concat_transform(&mut self, t: &[Float; 16], _loc: FileLoc) { + // Rust arrays verify size at compile time, simpler than C++ pointers + println!("{}ConcatTransform [ {:?} ]", self.indent(0), t); + } + + fn transform(&mut self, t: &[Float; 16], _loc: FileLoc) { + println!("{}Transform [ {:?} ]", self.indent(0), t); + } + + fn coordinate_system(&mut self, name: &str, _loc: FileLoc) { + println!("{}CoordinateSystem \"{}\"", self.indent(0), name); + } + + fn coord_sys_transform(&mut self, name: &str, _loc: FileLoc) { + println!("{}CoordSysTransform \"{}\"", self.indent(0), name); + } + + fn world_begin(&mut self, _loc: FileLoc) { + println!("{}WorldBegin", self.indent(0)); + self.cat_indent_count += 4; + } + + fn attribute_begin(&mut self, _loc: FileLoc) { + println!("{}AttributeBegin", self.indent(0)); + self.cat_indent_count += 4; + } + + fn attribute_end(&mut self, _loc: FileLoc) { + self.cat_indent_count = self.cat_indent_count.saturating_sub(4); + println!("{}AttributeEnd", self.indent(0)); + } + + fn shape(&mut self, name: &str, params: &ParsedParameterVector, _loc: FileLoc) { + println!( + "{}Shape \"{}\" ... ({} params)", + self.indent(0), + name, + params.len() + ); + } + + fn material(&mut self, name: &str, params: &ParsedParameterVector, _loc: FileLoc) { + println!( + "{}Material \"{}\" ... ({} params)", + self.indent(0), + name, + params.len() + ); + } + + fn texture( + &mut self, + name: &str, + type_name: &str, + tex_name: &str, + _params: &ParsedParameterVector, + _loc: FileLoc, + ) { + println!( + "{}Texture \"{}\" \"{}\" \"{}\"", + self.indent(0), + name, + type_name, + tex_name + ); + } + + fn active_transform_all(&mut self, _loc: FileLoc) {} + fn active_transform_end_time(&mut self, _loc: FileLoc) {} + fn active_transform_start_time(&mut self, _loc: FileLoc) {} + fn transform_times(&mut self, _s: Float, _e: Float, _loc: FileLoc) {} + fn color_space(&mut self, _n: &str, _loc: FileLoc) {} + fn pixel_filter(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn film(&mut self, _t: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn accelerator(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn integrator(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn camera(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn make_named_medium(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn medium_interface(&mut self, _i: &str, _o: &str, _loc: FileLoc) {} + fn sampler(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn attribute(&mut self, _t: &str, _p: ParsedParameterVector, _loc: FileLoc) {} + fn make_named_material(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn named_material(&mut self, _n: &str, _loc: FileLoc) {} + fn light_source(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn area_light_source(&mut self, _n: &str, _p: &ParsedParameterVector, _loc: FileLoc) {} + fn reverse_orientation(&mut self, _loc: FileLoc) {} + fn object_begin(&mut self, name: &str, _loc: FileLoc) { + println!("{}ObjectBegin \"{}\"", self.indent(0), name); + self.cat_indent_count += 4; + } + fn object_end(&mut self, _loc: FileLoc) { + self.cat_indent_count = self.cat_indent_count.saturating_sub(4); + println!("{}ObjectEnd", self.indent(0)); + } + fn object_instance(&mut self, _n: &str, _loc: FileLoc) {} + fn end_of_files(&mut self) { + self.cat_indent_count = 0; + } +} + +pub struct SceneParser<'a> { + target: &'a mut dyn ParserTarget, + file_stack: Vec, + unget_token: Option, + current_dir: PathBuf, +} + +impl<'a> SceneParser<'a> { + pub fn new(target: &'a mut dyn ParserTarget, root: Tokenizer) -> Self { + let current_dir = Path::new(&*root.filename) + .parent() + .unwrap_or(Path::new(".")) + .to_path_buf(); + + Self { + target, + file_stack: vec![root], + unget_token: None, + current_dir, + } + } + + // Kinda cursed + fn next_token(&mut self) -> Result, ParserError> { + if let Some(tok) = self.unget_token.take() { + return Ok(Some(tok)); + } + + loop { + if self.file_stack.is_empty() { + return Ok(None); + } + + let tokenizer = self.file_stack.last_mut().unwrap(); + match tokenizer.next() { + Ok(Some(tok)) => return Ok(Some(tok)), + Ok(None) => { + self.file_stack.pop(); + continue; + } + Err(e) => return Err(e), + } + } + } + + fn unget(&mut self, token: Token) { + self.unget_token = Some(token); + } + + fn next_token_required(&mut self) -> Result { + match self.next_token()? { + Some(t) => Ok(t), + None => Err(ParserError::UnexpectedEof), + } + } + + fn expect_float(&mut self) -> Result { + let t = self.next_token_required()?; + t.parse_float() + } + + fn parse_parameters(&mut self) -> Result { + let mut params = Vec::new(); + + loop { + let t = match self.next_token()? { + Some(tok) => tok, + None => return Ok(params), + }; + + if !t.is_quoted() { + self.unget(t); + return Ok(params); + } + + // Parse declarations + let decl = Token::dequote_string(&t.text); + let mut parts = decl.split_whitespace(); + + let type_name = parts.next().ok_or_else(|| { + ParserError::Generic( + format!("Parameter \"{}\" missing type", decl), + t.loc.clone(), + ) + })?; + + let param_name = parts.next().ok_or_else(|| { + ParserError::Generic( + format!("Parameter \"{}\" missing name", decl), + t.loc.clone(), + ) + })?; + + let mut param = ParsedParameter { + type_name: type_name.to_string(), + name: param_name.to_string(), + loc: t.loc.clone(), + ..Default::default() + }; + + enum ValType { + Unknown, + String, + Bool, + Float, + Int, + } + let mut val_type = match type_name { + "integer" => ValType::Int, + "bool" => ValType::Bool, + "float" | "point" | "vector" | "normal" | "color" | "spectrum" | "rgb" + | "blackbody" => ValType::Float, + "string" | "texture" => ValType::String, + _ => ValType::Unknown, + }; + + let mut add_val = + |token: &Token, dest: &mut ParsedParameter| -> Result<(), ParserError> { + let is_quoted = token.is_quoted(); + + if is_quoted { + if let ValType::Unknown = val_type { + val_type = ValType::String; + } + if let ValType::String = val_type { + dest.strings.push(token.dequote().to_string()); + } else { + return Err(ParserError::Generic( + format!("Expected non-string for param {}", param_name), + token.loc.clone(), + )); + } + } else if token.text == "true" { + if let ValType::Unknown = val_type { + val_type = ValType::Bool; + } + if let ValType::Bool = val_type { + dest.bools.push(true); + } else { + return Err(ParserError::Generic( + format!("Expected bool for param {}", param_name), + token.loc.clone(), + )); + } + } else if token.text == "false" { + if let ValType::Unknown = val_type { + val_type = ValType::Bool; + } + if let ValType::Bool = val_type { + dest.bools.push(false); + } else { + return Err(ParserError::Generic( + format!("Expected bool for param {}", param_name), + token.loc.clone(), + )); + } + } else { + // Number + if let ValType::Unknown = val_type { + val_type = ValType::Float; + } + + match val_type { + ValType::Int => { + let val = token.parse_int()?; + dest.ints.push(val); + } + ValType::Float => { + let val = token.parse_float()?; + dest.floats.push(val); + } + _ => { + return Err(ParserError::Generic( + format!("Expected number for param {}", param_name), + token.loc.clone(), + )); + } + } + } + Ok(()) + }; + + let val_token = self.next_token_required()?; + + if val_token.text == "[" { + loop { + let next_val = self.next_token_required()?; + if next_val.text == "]" { + break; + } + add_val(&next_val, &mut param)?; + } + } else { + add_val(&val_token, &mut param)?; + } + + params.push(param); + } + } + + pub fn run(&mut self) -> Result<(), ParserError> { + loop { + let token = match self.next_token()? { + Some(t) => t, + None => break, // EOF + }; + + let first_char = token.text.chars().next().unwrap(); + + match first_char { + 'A' => match token.text.as_str() { + "AttributeBegin" => self.target.attribute_begin(token.loc), + "AttributeEnd" => self.target.attribute_end(token.loc), + "Attribute" => self.parse_basic_entry(|t, n, p, l| t.attribute(n, p, l))?, + "ActiveTransform" => { + let a = self.next_token_required()?; + match a.text.as_str() { + "All" => self.target.active_transform_all(token.loc), + "EndTime" => self.target.active_transform_end_time(token.loc), + "StartTime" => self.target.active_transform_start_time(token.loc), + _ => { + return Err(ParserError::Generic( + "Unknown ActiveTransform type".into(), + a.loc, + )); + } + } + } + "AreaLightSource" => { + self.parse_basic_entry(|t, n, p, l| t.area_light_source(n, p, l))? + } + "Accelerator" => self.parse_basic_entry(|t, n, p, l| t.accelerator(n, p, l))?, + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'C' => match token.text.as_str() { + "Camera" => self.parse_basic_entry(|t, n, p, l| t.camera(n, p, l))?, + "ConcatTransform" => { + self.expect_token("[")?; + let mut m = [0.0; 16]; + for i in 0..16 { + m[i] = self.expect_float()?; + } + self.expect_token("]")?; + self.target.concat_transform(&m, token.loc); + } + "CoordinateSystem" => { + let n = self.expect_quoted_string()?; + self.target.coordinate_system(&n, token.loc); + } + "CoordSysTransform" => { + let n = self.expect_quoted_string()?; + self.target.coord_sys_transform(&n, token.loc); + } + "ColorSpace" => { + let n = self.expect_quoted_string()?; + self.target.color_space(&n, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'F' => match token.text.as_str() { + "Film" => self.parse_basic_entry(|t, n, p, l| t.film(n, p, l))?, + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'I' => match token.text.as_str() { + "Integrator" => self.parse_basic_entry(|t, n, p, l| t.integrator(n, p, l))?, + "Include" => { + let filename_tok = self.next_token_required()?; + let raw_filename = filename_tok.dequote(); + // Resolve path relative to current dir + let path = self.current_dir.join(raw_filename); + + let new_tokenizer = Tokenizer::create_from_file(path.to_str().unwrap()) + .map_err(|e| { + ParserError::Generic( + format!("Could not include: {:?}", e), + token.loc, + ) + })?; + + self.file_stack.push(new_tokenizer); + } + "Identity" => self.target.identity(token.loc), + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'L' => match token.text.as_str() { + "LightSource" => { + self.parse_basic_entry(|t, n, p, l| t.light_source(n, p, l))? + } + "LookAt" => { + let v: Vec = (0..9) + .map(|_| self.expect_float()) + .collect::>()?; + self.target.look_at( + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], token.loc, + ); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'M' => match token.text.as_str() { + "MakeNamedMaterial" => { + self.parse_basic_entry(|t, n, p, l| t.make_named_material(n, p, l))? + } + "MakeNamedMedium" => { + self.parse_basic_entry(|t, n, p, l| t.make_named_medium(n, p, l))? + } + "Material" => self.parse_basic_entry(|t, n, p, l| t.material(n, p, l))?, + "MediumInterface" => { + let inside = self.expect_quoted_string()?; + let next = self.next_token_required()?; + let outside = if next.is_quoted() { + next.dequote().to_string() + } else { + self.unget(next); + inside.clone() + }; + self.target.medium_interface(&inside, &outside, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'N' => match token.text.as_str() { + "NamedMaterial" => { + let n = self.expect_quoted_string()?; + self.target.named_material(&n, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'O' => match token.text.as_str() { + "ObjectBegin" => { + let n = self.expect_quoted_string()?; + self.target.object_begin(&n, token.loc); + } + "ObjectEnd" => self.target.object_end(token.loc), + "ObjectInstance" => { + let n = self.expect_quoted_string()?; + self.target.object_instance(&n, token.loc); + } + "Option" => { + let name = self.expect_quoted_string()?; + let val_tok = self.next_token_required()?; + let val = val_tok.dequote().to_string(); + self.target.option(&name, &val, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'P' => match token.text.as_str() { + "PixelFilter" => { + self.parse_basic_entry(|t, n, p, l| t.pixel_filter(n, p, l))? + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'R' => match token.text.as_str() { + "ReverseOrientation" => self.target.reverse_orientation(token.loc), + "Rotate" => { + let angle = self.expect_float()?; + let ax = self.expect_float()?; + let ay = self.expect_float()?; + let az = self.expect_float()?; + self.target.rotate(angle, ax, ay, az, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'S' => match token.text.as_str() { + "Shape" => self.parse_basic_entry(|t, n, p, l| t.shape(n, p, l))?, + "Sampler" => self.parse_basic_entry(|t, n, p, l| t.sampler(n, p, l))?, + "Scale" => { + let x = self.expect_float()?; + let y = self.expect_float()?; + let z = self.expect_float()?; + self.target.scale(x, y, z, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'T' => match token.text.as_str() { + "TransformBegin" => self.target.attribute_begin(token.loc), + "TransformEnd" => self.target.attribute_end(token.loc), + "Transform" => { + self.expect_token("[")?; + let mut m = [0.0; 16]; + for i in 0..16 { + m[i] = self.expect_float()?; + } + self.expect_token("]")?; + self.target.transform(&m, token.loc); + } + "Translate" => { + let x = self.expect_float()?; + let y = self.expect_float()?; + let z = self.expect_float()?; + self.target.translate(x, y, z, token.loc); + } + "TransformTimes" => { + let s = self.expect_float()?; + let e = self.expect_float()?; + self.target.transform_times(s, e, token.loc); + } + "Texture" => { + let name = self.expect_quoted_string()?; + let type_name = self.expect_quoted_string()?; + let tex_name = self.expect_quoted_string()?; + let params = self.parse_parameters()?; + self.target + .texture(&name, &type_name, &tex_name, ¶ms, token.loc); + } + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + 'W' => match token.text.as_str() { + "WorldBegin" => self.target.world_begin(token.loc), + "WorldEnd" => {} + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + }, + + _ => { + return Err(ParserError::Generic( + format!("Unknown directive {}", token.text), + token.loc, + )); + } + } + } + + self.target.end_of_files(); + Ok(()) + } + + fn expect_token(&mut self, expected: &str) -> Result<(), ParserError> { + let t = self.next_token_required()?; + if t.text != expected { + return Err(ParserError::Generic( + format!("Expected '{}', found '{}'", expected, t.text), + t.loc, + )); + } + Ok(()) + } + + fn expect_quoted_string(&mut self) -> Result { + let t = self.next_token_required()?; + if t.is_quoted() { + Ok(t.dequote().to_string()) + } else { + Err(ParserError::Generic( + format!("Expected quoted string, found '{}'", t.text), + t.loc, + )) + } + } + + // Handles Keyword "type" "Param list" + fn parse_basic_entry(&mut self, mut func: F) -> Result<(), ParserError> + where + F: FnMut(&mut dyn ParserTarget, &str, &ParsedParameterVector, FileLoc), + { + let type_token = self.next_token_required()?; + let type_name = if type_token.is_quoted() { + type_token.dequote().to_string() + } else { + type_token.text.clone() + }; + + let params = self.parse_parameters()?; + func(self.target, &type_name, ¶ms, type_token.loc); + Ok(()) + } +} diff --git a/src/utils/scattering.rs b/src/utils/scattering.rs deleted file mode 100644 index e3e28d8..0000000 --- a/src/utils/scattering.rs +++ /dev/null @@ -1,173 +0,0 @@ -use super::math::safe_sqrt; -use super::sampling::sample_uniform_disk_polar; -use crate::core::pbrt::{Float, PI, clamp_t, lerp}; -use crate::geometry::{ - Normal3f, Point2f, Vector2f, Vector3f, VectorLike, abs_cos_theta, cos_phi, cos2_theta, sin_phi, - tan2_theta, -}; -use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum}; -use crate::utils::math::square; - -use num::complex::Complex; - -#[derive(Debug, Default, Clone, Copy)] -pub struct TrowbridgeReitzDistribution { - alpha_x: Float, - alpha_y: Float, -} - -impl TrowbridgeReitzDistribution { - pub fn new(alpha_x: Float, alpha_y: Float) -> Self { - Self { alpha_x, alpha_y } - } - - pub fn d(&self, wm: Vector3f) -> Float { - let tan2_theta = tan2_theta(wm); - if tan2_theta.is_infinite() { - return 0.; - } - let cos4_theta = square(cos2_theta(wm)); - let e = - tan2_theta * (square(cos_phi(wm) / self.alpha_x) + square(sin_phi(wm) / self.alpha_y)); - 1.0 / (PI * self.alpha_x * self.alpha_y * cos4_theta * square(1. + e)) - } - - pub fn effectively_smooth(&self) -> bool { - self.alpha_x.max(self.alpha_y) < 1e-3 - } - - pub fn lambda(&self, w: Vector3f) -> Float { - let tan2_theta = tan2_theta(w); - if tan2_theta.is_infinite() { - return 0.; - } - let alpha2 = square(cos_phi(w) * self.alpha_x) + square(sin_phi(w) * self.alpha_y); - ((1. + alpha2 * tan2_theta).sqrt() - 1.) / 2. - } - - pub fn g(&self, wo: Vector3f, wi: Vector3f) -> Float { - 1. / (1. + self.lambda(wo) + self.lambda(wi)) - } - - pub fn g1(&self, w: Vector3f) -> Float { - 1. / (1. / self.lambda(w)) - } - - pub fn d_from_w(&self, w: Vector3f, wm: Vector3f) -> Float { - self.g1(w) / abs_cos_theta(w) * self.d(wm) * w.dot(wm).abs() - } - - pub fn pdf(&self, w: Vector3f, wm: Vector3f) -> Float { - self.d_from_w(w, wm) - } - - pub fn sample_wm(&self, w: Vector3f, u: Point2f) -> Vector3f { - let mut wh = Vector3f::new(self.alpha_x * w.x(), self.alpha_y * w.y(), w.z()).normalize(); - if wh.z() < 0. { - wh = -wh; - } - let t1 = if wh.z() < 0.99999 { - Vector3f::new(0., 0., 1.).cross(wh).normalize() - } else { - Vector3f::new(1., 0., 0.) - }; - let t2 = wh.cross(t1); - let mut p = sample_uniform_disk_polar(u); - let h = (1. - square(p.x())).sqrt(); - p[1] = lerp((1. + wh.z()) / 2., h, p.y()); - let pz = 0_f32.max(1. - Vector2f::from(p).norm_squared()); - let nh = p.x() * t1 + p.y() * t2 + pz * wh; - Vector3f::new( - self.alpha_x * nh.x(), - self.alpha_y * nh.y(), - nh.z().max(1e-6), - ) - .normalize() - } - - pub fn roughness_to_alpha(roughness: Float) -> Float { - roughness.sqrt() - } - - pub fn regularize(&mut self) { - if self.alpha_x < 0.3 { - self.alpha_x = clamp_t(2. * self.alpha_x, 0.1, 0.3); - } - if self.alpha_y < 0.3 { - self.alpha_y = clamp_t(2. * self.alpha_y, 0.1, 0.3); - } - } -} - -pub fn refract(wi: Vector3f, n: Normal3f, eta_ratio: Float) -> Option<(Vector3f, Float)> { - let mut n_interface = n; - let mut eta = eta_ratio; - - let mut cos_theta_i = Vector3f::from(n_interface).dot(wi); - - if cos_theta_i < 0.0 { - eta = 1.0 / eta; - cos_theta_i = -cos_theta_i; - n_interface = -n_interface; - } - - let sin2_theta_i = (1.0 - square(cos_theta_i)).max(0.0_f32); - let sin2_theta_t = sin2_theta_i / square(eta); - - // Handle total internal reflection - if sin2_theta_t >= 1.0 { - return None; - } - - let cos_theta_t = (1.0 - sin2_theta_t).sqrt(); - - let wt = -wi / eta + (cos_theta_i / eta - cos_theta_t) * Vector3f::from(n_interface); - Some((wt, eta)) -} - -pub fn reflect(wo: Vector3f, n: Normal3f) -> Vector3f { - -wo + Vector3f::from(2. * wo.dot(n.into()) * n) -} - -pub fn fr_dielectric(cos_theta_i: Float, eta: Float) -> Float { - let mut cos_safe = clamp_t(cos_theta_i, -1., 1.); - let mut eta_corr = eta; - if cos_theta_i < 0. { - eta_corr = 1. / eta_corr; - cos_safe = -cos_safe; - } - let sin2_theta_i = 1. - square(cos_safe); - let sin2_theta_t = sin2_theta_i / square(eta_corr); - if sin2_theta_t >= 1. { - return 1.; - } - let cos_theta_t = safe_sqrt(1. - sin2_theta_t); - let r_parl = (eta_corr * cos_safe - cos_theta_t) / (eta_corr * cos_safe + cos_theta_t); - let r_perp = (cos_safe - eta_corr * cos_theta_t) / (cos_safe + eta_corr * cos_theta_t); - - (square(r_parl) + square(r_perp)) / 2. -} - -pub fn fr_complex(cos_theta_i: Float, eta: Complex) -> Float { - let cos_corr = clamp_t(cos_theta_i, 0., 1.); - let sin2_theta_i = 1. - square(cos_corr); - let sin2_theta_t: Complex = sin2_theta_i / square(eta); - let cos2_theta_t: Complex = (1. - sin2_theta_t).sqrt(); - - let r_parl = (eta * cos_corr - cos2_theta_t) / (eta * cos_corr + cos2_theta_t); - let r_perp = (cos_corr - eta * cos2_theta_t) / (cos_corr + eta * cos2_theta_t); - - (r_parl.norm() + r_perp.norm()) / 2. -} - -pub fn fr_complex_from_spectrum( - cos_theta_i: Float, - eta: SampledSpectrum, - k: SampledSpectrum, -) -> SampledSpectrum { - let mut result = SampledSpectrum::default(); - for i in 0..N_SPECTRUM_SAMPLES { - result[i] = fr_complex(cos_theta_i, Complex::new(eta[i], k[i])); - } - result -}