diff --git a/shared/src/core/bssrdf.rs b/shared/src/core/bssrdf.rs index 57a107d..3670467 100644 --- a/shared/src/core/bssrdf.rs +++ b/shared/src/core/bssrdf.rs @@ -5,7 +5,7 @@ use crate::core::interaction::{InteractionBase, ShadingGeom, SurfaceInteraction} use crate::core::shape::Shape; use crate::core::{LightIdx, MaterialIdx}; use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum}; -use crate::utils::math::{catmull_rom_weights, square}; +use crate::utils::math::{catmull_rom_weights, invert_catmull_rom, square}; use crate::utils::sampling::sample_catmull_rom_2d; use crate::{Float, GVec, PI, Ptr, gvec_with_capacity}; use enum_dispatch::enum_dispatch; @@ -147,6 +147,22 @@ impl BSSRDFTable { } } +pub fn subsurface_from_diffuse( + t: &BSSRDFTable, + rho_eff: &SampledSpectrum, + mfp: &SampledSpectrum, +) -> (SampledSpectrum, SampledSpectrum) { + // (sigma_a, sigma_s) + let mut sigma_a = SampledSpectrum::zero(); + let mut sigma_s = SampledSpectrum::zero(); + for c in 0..N_SPECTRUM_SAMPLES { + let rho = invert_catmull_rom(&t.rho_samples, &t.rho_eff, rho_eff[c]); + sigma_s[c] = rho / mfp[c]; + sigma_a[c] = (1. - rho) / mfp[c]; + } + (sigma_a, sigma_s) +} + #[repr(C)] #[derive(Copy, Clone, Default, Debug)] pub struct BSSRDFProbeSegment { diff --git a/shared/src/core/spectrum.rs b/shared/src/core/spectrum.rs index d1f2df0..1480633 100644 --- a/shared/src/core/spectrum.rs +++ b/shared/src/core/spectrum.rs @@ -36,6 +36,15 @@ pub enum Spectrum { RGBUnbounded(RGBUnboundedSpectrum), } +/// `enum_dispatch` already generates `From for Spectrum`, so wrapping a +/// `ConstantSpectrum` etc. is `.into()`. Only the plain-`Float` hop is missing, +/// and it is the one written most often at default-value sites. +impl From for Spectrum { + fn from(c: Float) -> Self { + Spectrum::Constant(ConstantSpectrum::new(c)) + } +} + impl SpectrumTrait for Ptr { fn evaluate(&self, lambda: Float) -> Float { self.get().unwrap().evaluate(lambda) diff --git a/shared/src/materials/complex.rs b/shared/src/materials/complex.rs index 70faffc..7dd0b55 100644 --- a/shared/src/materials/complex.rs +++ b/shared/src/materials/complex.rs @@ -4,7 +4,7 @@ use crate::bxdfs::{ MeasuredBxDF, MeasuredBxDFData, }; use crate::core::bsdf::BSDF; -use crate::core::bssrdf::{BSSRDF, BSSRDFTable}; +use crate::core::bssrdf::{BSSRDF, BSSRDFTable, TabulatedBSSRDF, subsurface_from_diffuse}; use crate::core::bxdf::BxDF; use crate::core::image::Image; use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; @@ -118,12 +118,30 @@ impl MaterialTrait for HairMaterial { None } - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + match self.hair_absorption { + HairAbsorption::SigmaA(t) | HairAbsorption::Color(t) => { + tex_eval.can_evaluate(&[self.eta, self.beta_m, self.beta_n, self.alpha], &[t]) + } + HairAbsorption::Melanin { + eumelanin, + pheomelanin, + } => tex_eval.can_evaluate( + &[ + self.eta, + self.beta_m, + self.beta_n, + self.alpha, + eumelanin, + pheomelanin, + ], + &[], + ), + } } fn get_normal_map(&self) -> Option<&Image> { - todo!() + None } fn get_displacement(&self) -> Ptr { @@ -210,31 +228,78 @@ pub struct SubsurfaceMaterial { impl MaterialTrait for SubsurfaceMaterial { fn get_bsdf( &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, + tex_eval: &T, + ctx: &MaterialEvalContext, _lambda: &mut SampledWavelengths, ) -> BSDF { - todo!() - } - fn get_bssrdf( - &self, - _tex_eval: &T, - _ctx: &MaterialEvalContext, - _lambda: &SampledWavelengths, - ) -> Option { - todo!() + let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx); + let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx); + if self.remap_roughness { + u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough); + v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough); + } + + let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough); + let bxdf = BxDF::Dielectric(DielectricBxDF::new(self.eta, distrib)); + BSDF::new(ctx.ns, ctx.dpdus, bxdf) } - fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { - todo!() + fn get_bssrdf( + &self, + tex_eval: &T, + ctx: &MaterialEvalContext, + lambda: &SampledWavelengths, + ) -> Option { + let (sig_a, sig_s) = match self.scattering { + SubsurfaceScattering::Coefficients { sigma_a, sigma_s } => { + let s_a = SampledSpectrum::clamp_zero( + &(self.scale * tex_eval.evaluate_spectrum(&sigma_a, ctx, lambda)), + ); + let s_s = SampledSpectrum::clamp_zero( + &(self.scale * tex_eval.evaluate_spectrum(&sigma_s, ctx, lambda)), + ); + (s_a, s_s) + } + SubsurfaceScattering::Reflectance { reflectance, mfp } => { + debug_assert!(!reflectance.is_null() && !mfp.is_null()); + let mfree = + SampledSpectrum::clamp_zero(&tex_eval.evaluate_spectrum(&mfp, ctx, lambda)); + let r = SampledSpectrum::clamp_zero(&tex_eval.evaluate_spectrum( + &reflectance, + ctx, + lambda, + )); + subsurface_from_diffuse(&self.table, &r, &mfree) + } + }; + + Some(BSSRDF::Tabulated(TabulatedBSSRDF::new( + ctx.p, + ctx.wo, + ctx.ns, + self.eta, + &sig_a, + &sig_s, + &self.table, + ))) + } + + fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { + // Slight divergence from PBRT, we check against reflectance and mfp as well in reflectance + // mode. Test thoroughly, keep as is for now (20260902) + let spectra = match self.scattering { + SubsurfaceScattering::Coefficients { sigma_a, sigma_s } => [sigma_a, sigma_s], + SubsurfaceScattering::Reflectance { reflectance, mfp } => [reflectance, mfp], + }; + tex_eval.can_evaluate(&[self.u_roughness, self.v_roughness], &spectra) } fn get_normal_map(&self) -> Option<&Image> { - todo!() + Some(&*self.normal_map) } fn get_displacement(&self) -> Ptr { - todo!() + self.displacement } fn has_subsurface_scattering(&self) -> bool { diff --git a/shared/src/materials/conductor.rs b/shared/src/materials/conductor.rs index a880259..67a68c7 100644 --- a/shared/src/materials/conductor.rs +++ b/shared/src/materials/conductor.rs @@ -27,6 +27,7 @@ pub struct ConductorMaterial { } impl ConductorMaterial { + #[allow(clippy::too_many_arguments)] pub fn new( normal_map: Ptr, reflectance: Ptr, @@ -92,7 +93,7 @@ impl MaterialTrait for ConductorMaterial { _ctx: &MaterialEvalContext, _lambda: &SampledWavelengths, ) -> Option { - todo!() + None } fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { tex_eval.can_evaluate( diff --git a/shared/src/utils/math.rs b/shared/src/utils/math.rs index 6e9962a..711c458 100644 --- a/shared/src/utils/math.rs +++ b/shared/src/utils/math.rs @@ -1,9 +1,9 @@ use crate::core::color::{RGB, XYZ}; use crate::core::geometry::{Lerp, MulAdd, Point, Point2f, Point2i, Vector, Vector3f, VectorLike}; use crate::core::pbrt::{Float, FloatBitOps, FloatBits, ONE_MINUS_EPSILON, PI, PI_OVER_4}; -use crate::utils::gpu_array_from_fn; use crate::utils::hash::{hash_buffer, mix_bits}; use crate::utils::sobol::{SOBOL_MATRICES_32, VDC_SOBOL_MATRICES, VDC_SOBOL_MATRICES_INV}; +use crate::utils::{find_interval, gpu_array_from_fn}; use crate::{GVec, Ptr, gvec, gvec_with_capacity}; use core::fmt::{self, Display, Write}; use core::iter::{Product, Sum}; @@ -379,6 +379,61 @@ pub fn integrate_catmull_rom(nodes: &[Float], f: &[Float], cdf: &mut [Float]) -> sum } +pub fn invert_catmull_rom(nodes: &[Float], f: &[Float], u: Float) -> Float { + // Stop when _u_ is out of bounds + if !(u > f[0]) { + return nodes[0]; + } else if !(u < f[f.len() - 1]) { + return nodes[nodes.len() - 1]; + } + + // Map _u_ to a spline interval by inverting _f_ + let i = find_interval(f.len() as u32, |j| f[j as usize] <= u) as usize; + + // Look up $x_i$ and function values of spline segment _i_ + let x0 = nodes[i]; + let x1 = nodes[i + 1]; + let f0 = f[i]; + let f1 = f[i + 1]; + let width = x1 - x0; + + // Approximate derivatives using finite differences + let d0 = if i > 0 { + width * (f1 - f[i - 1]) / (x1 - nodes[i - 1]) + } else { + f1 - f0 + }; + let d1 = if i + 2 < nodes.len() { + width * (f[i + 2] - f0) / (nodes[i + 2] - x0) + } else { + f1 - f0 + }; + + // Invert the spline interpolant using Newton-Bisection + let eval = |t: Float| -> (Float, Float) { + // Compute powers of _t_ + let t2 = t * t; + let t3 = t2 * t; + + // Set _Fhat_ using Equation (\ref{eq:cubicspline-as-basisfunctions}) + let f_cap_hat = (2. * t3 - 3. * t2 + 1.) * f0 + + (-2. * t3 + 3. * t2) * f1 + + (t3 - 2. * t2 + t) * d0 + + (t3 - t2) * d1; + + // Set _fhat_ using Equation (\ref{eq:cubicspline-derivative}) + let f_hat = (6. * t2 - 6. * t) * f0 + + (-6. * t2 + 6. * t) * f1 + + (3. * t2 - 4. * t + 1.) * d0 + + (3. * t2 - 2. * t) * d1; + + return (f_cap_hat - u, f_hat); + }; + + let t = newton_bisection(0., 1., eval); + return x0 + t * width; +} + pub fn catmull_rom_weights(nodes: &[Float], x: Float) -> Option<(u32, [Float; 4])> { if nodes.len() < 4 { return None; diff --git a/src/core/texture.rs b/src/core/texture.rs index 7f765ad..a92be55 100644 --- a/src/core/texture.rs +++ b/src/core/texture.rs @@ -7,6 +7,7 @@ use shared::Float; use shared::core::color::ColorEncoding; use shared::core::geometry::Vector3f; use shared::core::image::WrapMode; +use shared::core::spectrum::Spectrum; use shared::core::texture::SpectrumType; use shared::core::texture::{ CylindricalMapping, PlanarMapping, PointTransformMapping, SphericalMapping, TextureEvalContext, @@ -95,6 +96,21 @@ pub enum SpectrumTexture { DirectionMix(SpectrumDirectionMixTexture), } +/// A bare `Spectrum` used as a texture is always a constant texture. Saves +/// writing `SpectrumTexture::Constant(SpectrumConstantTexture::new(s))` at every +/// default-value site. +impl From for SpectrumTexture { + fn from(s: Spectrum) -> Self { + SpectrumTexture::Constant(SpectrumConstantTexture::new(s)) + } +} + +impl From for FloatTexture { + fn from(v: Float) -> Self { + FloatTexture::Constant(FloatConstantTexture::new(v)) + } +} + pub trait CreateSpectrumTexture { fn create( render_from_texture: Transform, diff --git a/src/materials/coated.rs b/src/materials/coated.rs index 4b388b6..13dbccf 100644 --- a/src/materials/coated.rs +++ b/src/materials/coated.rs @@ -4,8 +4,8 @@ use crate::core::texture::SpectrumTexture; use crate::globals::get_options; use crate::spectra::data::get_named_spectrum; use crate::utils::TextureParameterDictionary; -use crate::{Arena, FileLoc, ArenaUpload}; -use anyhow::{bail, Result}; +use crate::{Arena, ArenaUpload, FileLoc}; +use anyhow::{Result, bail}; use shared::core::material::Material; use shared::core::spectrum::Spectrum; use shared::core::texture::SpectrumType; @@ -36,12 +36,13 @@ impl CreateMaterial for CoatedDiffuseMaterial { parameters.get_float_texture_with_fallback("vroughness", "roughness", 0.5)?; let thickness = parameters.get_float_texture("thickness", 0.01)?; - let eta = parameters - .get_float_array("eta")? - .first() - .map(|&v| Spectrum::Constant(ConstantSpectrum::new(v))) - .or_else(|| parameters.get_one_spectrum("eta", None, SpectrumType::Unbounded)) - .unwrap_or_else(|| Spectrum::Constant(ConstantSpectrum::new(1.5))); + let eta = if let Some(&v) = parameters.get_float_array("eta")?.first() { + Spectrum::from(v) + } else { + parameters + .get_one_spectrum("eta", None, SpectrumType::Unbounded) + .unwrap_or_else(|| Spectrum::from(1.5)) + }; let max_depth = parameters.get_one_int("maxdepth", 10)?; let n_samples = parameters.get_one_int("nsamples", 1)?; diff --git a/src/materials/complex.rs b/src/materials/complex.rs index 65c0519..5a38336 100644 --- a/src/materials/complex.rs +++ b/src/materials/complex.rs @@ -297,6 +297,14 @@ impl CreateMaterial for SubsurfaceMaterial { } } +// fn brdf_data_from_filename(filename: String) -> Ptr { +// static std::map loadedData; +// if (loadedData.find(filename) == loadedData.end()) +// loadedData[filename] = MeasuredBxDFData::Create(filename, alloc); +// return loadedData[filename]; +// +// } + impl CreateMaterial for MeasuredMaterial { fn create( parameters: &TextureParameterDictionary, @@ -305,12 +313,14 @@ impl CreateMaterial for MeasuredMaterial { loc: &FileLoc, arena: &Arena, ) -> Result { - let filename = resolve_filename(parameters.get_one_string("filename", "")?); - let displacement = parameters.get_float_texture_or_null("displacement")?; - let brdf = MeasuredBxDF::brdf_data_from_file(filename); - let mat = MeasuredMaterial { - displacement: arena.upload(displacement), - normal_map: arena.upload(normal_map) - } + // let filename = resolve_filename(parameters.get_one_string("filename", "")?); + // let displacement = parameters.get_float_texture_or_null("displacement")?; + // let brdf = MeasuredBxDF::brdf_data_from_file(filename); + // let mat = MeasuredMaterial { + // displacement: arena.upload(displacement), + // normal_map: arena.upload(normal_map) + // brdf + // } + todo!() } } diff --git a/src/materials/dielectric.rs b/src/materials/dielectric.rs index 07fcc22..cb1dd8b 100644 --- a/src/materials/dielectric.rs +++ b/src/materials/dielectric.rs @@ -1,33 +1,75 @@ -use crate::Arena; use crate::core::image::HostImage; use crate::core::material::CreateMaterial; use crate::utils::{FileLoc, TextureParameterDictionary}; +use crate::{Arena, ArenaUpload}; use anyhow::Result; use shared::core::material::Material; +use shared::core::spectrum::Spectrum; +use shared::core::texture::SpectrumType; use shared::materials::{DielectricMaterial, ThinDielectricMaterial}; +use shared::spectra::ConstantSpectrum; use std::collections::HashMap; use std::sync::Arc; impl CreateMaterial for DielectricMaterial { fn create( - _parameters: &TextureParameterDictionary, - _normal_map: Option>, + parameters: &TextureParameterDictionary, + normal_map: Option>, _named_materials: &HashMap, - _loc: &FileLoc, - _arena: &Arena, + loc: &FileLoc, + arena: &Arena, ) -> Result { - todo!() + let eta = if let Some(&v) = parameters.get_float_array("eta")?.first() { + Spectrum::from(v) + } else { + parameters + .get_one_spectrum("eta", None, SpectrumType::Unbounded) + .unwrap_or_else(|| Spectrum::from(1.5)) + }; + + let u_roughness = + parameters.get_float_texture_with_fallback("uroughness", "roughness", 0.)?; + let v_roughness = + parameters.get_float_texture_with_fallback("vroughness", "roughness", 0.)?; + let displacement = parameters.get_float_texture_or_null("displacement")?; + let remap_roughness = parameters.get_one_bool("remaproughness", true)?; + + let mat = DielectricMaterial { + normal_map: arena.upload(normal_map), + displacement: arena.upload(displacement), + u_roughness: arena.upload(u_roughness), + v_roughness: arena.upload(v_roughness), + eta: arena.alloc(eta), + remap_roughness, + }; + + Ok(Material::Dielectric(mat)) } } impl CreateMaterial for ThinDielectricMaterial { fn create( - _parameters: &TextureParameterDictionary, - _normal_map: Option>, + parameters: &TextureParameterDictionary, + normal_map: Option>, _named_materials: &HashMap, - _loc: &FileLoc, - _arena: &Arena, + loc: &FileLoc, + arena: &Arena, ) -> Result { - todo!() + let eta = if let Some(&v) = parameters.get_float_array("eta")?.first() { + Spectrum::from(v) + } else { + parameters + .get_one_spectrum("eta", None, SpectrumType::Unbounded) + .unwrap_or_else(|| Spectrum::from(1.5)) + }; + + let displacement = parameters.get_float_texture_or_null("displacement")?; + let mat = ThinDielectricMaterial { + displacement: arena.upload(displacement), + normal_map: arena.upload(normal_map), + eta: arena.alloc(eta), + }; + + Ok(Material::ThinDielectric(mat)) } }