Compare commits

..

8 commits

Author SHA1 Message Date
4faa3cdc95 Fix colorspace illuminant, LayeredBxDF transmittance, and film pixel bounds
- from_interleaved ignored its  arg, so D65 came from a separate
  hand-normalized table that was scaled to luminance 1 instead of
  CIE_Y_integral and mapped to 300nm when it starts at 360nm. sRGB
  rgb_from_xyz came out ~120x too large with the wrong white point, making
  every render ~120x too bright with a colour cast. Now built as pbrt does:
  from_interleaved(&CIE_ILLUM_D6500, true).

- LayeredBxDF::Tr:  puts the minus on the result,
  not the exponent, so transmittance was negative and grew exponentially.
  killeroo-coated-gold went from 15 Infs / min -4168 / stddev 638 to clean.

- layered.rs used Float::MIN where C++ has numeric_limits<Float>::min(). Those
  differ: the latter is f32::MIN_POSITIVE. Both guards were dead, leaving a
  zero thickness to divide by.

- FilmBase::create expanded pixel_bounds by the filter radius. pbrt never does;
  the radius widens SampleBounds() only. Output was 1372x1030, not 1368x1026.

- Gate the ENQUEUE/DEQUEUE wavefront prints behind cpu_debug; their queues
  reset every depth/batch/sample so they fired constantly (100h renders).
2026-09-02 21:20:38 +01:00
34ea80c030 Bunch of logic errors in port of math and sampling functions 2026-09-02 17:21:16 +01:00
28bb963268 Finishing material creators 2026-09-02 15:31:12 +01:00
6e23698e2d Added scene creator for SubSurface Material 2026-09-02 12:42:49 +01:00
f496c6721e Updated Hair bsdf getter, added enum for clearer variant construction on absorption 2026-09-02 10:45:36 +01:00
0fcfcbd467 Added gpu config flag, completed light sampling methods 2026-09-02 09:41:14 +01:00
2448ab890e Done with textures 2026-09-01 23:04:46 +01:00
aba574219c Added creators to all textures but mix, need to decide now on a bloody architecture, this repo was in an insane state 2026-09-01 22:45:42 +01:00
55 changed files with 1193 additions and 393 deletions

View file

@ -2,4 +2,13 @@ fn main() {
// This allows "spirv" to be used in #[cfg(target_arch = "...")] // This allows "spirv" to be used in #[cfg(target_arch = "...")]
// without triggering a warning. // without triggering a warning.
println!("cargo:rustc-check-cfg=cfg(target_arch, values(\"spirv\"))"); println!("cargo:rustc-check-cfg=cfg(target_arch, values(\"spirv\"))");
// `gpu` is set for every device backend, so host-only code can be gated once
// as #[cfg(not(gpu))] instead of naming each target. Adding a backend means
// editing this line, not 30-odd cfg attributes.
println!("cargo::rustc-check-cfg=cfg(gpu)");
let target = std::env::var("TARGET").unwrap_or_default();
if target.contains("spirv") || target.contains("cuda") {
println!("cargo::rustc-cfg=gpu");
}
} }

View file

@ -8,7 +8,7 @@ use crate::core::scattering::{
TrowbridgeReitzDistribution, fr_complex_from_spectrum, fr_dielectric, fresnel_moment1, reflect, TrowbridgeReitzDistribution, fr_complex_from_spectrum, fr_dielectric, fresnel_moment1, reflect,
refract, refract,
}; };
use crate::spectra::{DeviceStandardColorSpaces, RGBUnboundedSpectrum, SampledSpectrum}; use crate::spectra::{N_SPECTRUM_SAMPLES, RGBColorSpace, RGBUnboundedSpectrum, SampledSpectrum};
use crate::utils::gpu_array_from_fn; use crate::utils::gpu_array_from_fn;
use crate::utils::math::{ use crate::utils::math::{
clamp, fast_exp, i0, lerp, log_i0, radians, safe_acos, safe_asin, safe_sqrt, sample_discrete, clamp, fast_exp, i0, lerp, log_i0, radians, safe_acos, safe_asin, safe_sqrt, sample_discrete,
@ -17,7 +17,7 @@ use crate::utils::math::{
use crate::utils::sampling::{ use crate::utils::sampling::{
cosine_hemisphere_pdf, sample_cosine_hemisphere, sample_trimmed_logistic, cosine_hemisphere_pdf, sample_cosine_hemisphere, sample_trimmed_logistic,
}; };
use crate::{Float, INV_2_PI, INV_PI, PI}; use crate::{Float, INV_2_PI, INV_PI, PI, Ptr};
use core::any::Any; use core::any::Any;
use num_traits::Float as NumFloat; use num_traits::Float as NumFloat;
@ -34,7 +34,6 @@ pub struct HairBxDF {
pub s: Float, pub s: Float,
pub sin_2k_alpha: [Float; P_MAX], pub sin_2k_alpha: [Float; P_MAX],
pub cos_2k_alpha: [Float; P_MAX], pub cos_2k_alpha: [Float; P_MAX],
pub colorspaces: DeviceStandardColorSpaces,
} }
impl HairBxDF { impl HairBxDF {
@ -45,7 +44,6 @@ impl HairBxDF {
beta_m: Float, beta_m: Float,
beta_n: Float, beta_n: Float,
alpha: Float, alpha: Float,
colorspaces: DeviceStandardColorSpaces,
) -> Self { ) -> Self {
let mut sin_2k_alpha = [0.; P_MAX]; let mut sin_2k_alpha = [0.; P_MAX];
let mut cos_2k_alpha = [0.; P_MAX]; let mut cos_2k_alpha = [0.; P_MAX];
@ -67,7 +65,6 @@ impl HairBxDF {
s: 0., s: 0.,
sin_2k_alpha, sin_2k_alpha,
cos_2k_alpha, cos_2k_alpha,
colorspaces,
} }
} }
@ -141,12 +138,25 @@ impl HairBxDF {
pub fn sigma_a_from_concentration( pub fn sigma_a_from_concentration(
ce: Float, ce: Float,
cp: Float, cp: Float,
stdcs: DeviceStandardColorSpaces, srgb: Ptr<RGBColorSpace>,
) -> RGBUnboundedSpectrum { ) -> RGBUnboundedSpectrum {
let eumelanin_sigma_a = RGB::new(0.419, 0.697, 1.37); let eumelanin_sigma_a = RGB::new(0.419, 0.697, 1.37);
let pheomelanin_sigma_a = RGB::new(0.187, 0.4, 1.05); let pheomelanin_sigma_a = RGB::new(0.187, 0.4, 1.05);
let sigma_a = ce * eumelanin_sigma_a + cp * pheomelanin_sigma_a; let sigma_a = ce * eumelanin_sigma_a + cp * pheomelanin_sigma_a;
RGBUnboundedSpectrum::new(&stdcs.srgb, sigma_a) RGBUnboundedSpectrum::new(&srgb, sigma_a)
}
pub fn sigma_a_from_reflectance(c: SampledSpectrum, beta_n: Float) -> SampledSpectrum {
let mut sigma_a = SampledSpectrum::zero();
for i in 0..N_SPECTRUM_SAMPLES {
sigma_a[i] = square(
c[i].ln()
/ (5.969 - 0.215 * beta_n + 2.532 * square(beta_n) - 10.73 * beta_n.powf(3.)
+ 5.574 * beta_n.powf(4.)
+ 0.245 * beta_n.powf(5.)),
);
}
sigma_a
} }
} }

View file

@ -140,7 +140,10 @@ where
Self { Self {
top, top,
bottom, bottom,
thickness: thickness.max(Float::MIN), // pbrt: `std::max(thickness, std::numeric_limits<Float>::min())` -- clamp to the
// smallest positive normal so the `dz / thickness` divisions stay finite.
// `Float::MIN` is the most negative finite value, so it never clamped.
thickness: thickness.max(Float::MIN_POSITIVE),
g, g,
albedo, albedo,
max_depth, max_depth,
@ -150,10 +153,17 @@ where
} }
fn tr(&self, dz: Float, w: Vector3f) -> Float { fn tr(&self, dz: Float, w: Vector3f) -> Float {
if dz.abs() <= Float::MIN { // pbrt: `if (std::abs(dz) <= std::numeric_limits<Float>::min()) return 1;`
// C++ `numeric_limits<Float>::min()` is the smallest positive NORMAL value, which
// is `f32::MIN_POSITIVE` -- `Float::MIN` is the most negative finite value, so the
// guard could never fire.
if dz.abs() <= Float::MIN_POSITIVE {
return 1.; return 1.;
} }
-(dz / w.z()).abs().exp() // pbrt: `FastExp(-std::abs(dz / w.z))`. The minus sign belongs on the EXPONENT;
// `-(x).abs().exp()` negates the result and leaves a growing `exp(+|x|)`, which
// made transmittance negative and unbounded.
fast_exp(-(dz / w.z()).abs())
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]

View file

@ -24,7 +24,7 @@ pub struct OrthographicCamera {
pub dy_camera: Vector3f, pub dy_camera: Vector3f,
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
impl OrthographicCamera { impl OrthographicCamera {
pub fn new( pub fn new(
base: CameraBase, base: CameraBase,

View file

@ -26,7 +26,7 @@ pub struct PerspectiveCamera {
pub cos_total_width: Float, pub cos_total_width: Float,
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
impl PerspectiveCamera { impl PerspectiveCamera {
pub fn new( pub fn new(
base: CameraBase, base: CameraBase,

View file

@ -3,11 +3,11 @@ use crate::core::bsdf::BSDF;
use crate::core::geometry::{Frame, Normal3f, Point2f, Point3f, Point3fi, Vector3f}; use crate::core::geometry::{Frame, Normal3f, Point2f, Point3f, Point3fi, Vector3f};
use crate::core::interaction::{InteractionBase, ShadingGeom, SurfaceInteraction}; use crate::core::interaction::{InteractionBase, ShadingGeom, SurfaceInteraction};
use crate::core::shape::Shape; use crate::core::shape::Shape;
use crate::spectra::{SampledSpectrum, N_SPECTRUM_SAMPLES};
use crate::utils::math::{catmull_rom_weights, square};
use crate::utils::sampling::sample_catmull_rom_2d;
use crate::core::{LightIdx, MaterialIdx}; use crate::core::{LightIdx, MaterialIdx};
use crate::{gvec_with_capacity, Float, GVec, PI, Ptr}; use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum};
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; use enum_dispatch::enum_dispatch;
use num_traits::Float as NumFloat; use num_traits::Float as NumFloat;
@ -105,20 +105,19 @@ pub struct BSSRDFTable {
impl BSSRDFTable { impl BSSRDFTable {
pub fn new(n_rho: usize, n_radius: usize) -> Self { pub fn new(n_rho: usize, n_radius: usize) -> Self {
let rho_samples: GVec<Float> = gvec_with_capacity(n_rho); let filled = |n: usize| {
let radius_samples: GVec<Float> = gvec_with_capacity(n_radius); let mut v: GVec<Float> = gvec_with_capacity(n);
let profile: GVec<Float> = gvec_with_capacity(n_radius * n_rho); v.resize(n, 0.);
let rho_eff: GVec<Float> = gvec_with_capacity(n_rho); v
let profile_cdf: GVec<Float> = gvec_with_capacity(n_radius * n_rho); };
Self { Self {
n_rho: n_rho.try_into().unwrap(), n_rho: n_rho as u32,
n_radius: n_radius.try_into().unwrap(), n_radius: n_radius as u32,
rho_samples, rho_samples: filled(n_rho),
radius_samples, radius_samples: filled(n_radius),
profile, profile: filled(n_rho * n_radius),
rho_eff, rho_eff: filled(n_rho),
profile_cdf, profile_cdf: filled(n_rho * n_radius),
} }
} }
@ -148,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)] #[repr(C)]
#[derive(Copy, Clone, Default, Debug)] #[derive(Copy, Clone, Default, Debug)]
pub struct BSSRDFProbeSegment { pub struct BSSRDFProbeSegment {
@ -236,7 +251,7 @@ impl TabulatedBSSRDF {
sr += weight sr += weight
* self * self
.table .table
.eval_profile(rho_offset + j as u32, radius_offset + k as u32); .eval_profile((rho_offset + j as i32) as u32, (radius_offset + k as i32) as u32);
} }
} }
} }
@ -287,14 +302,14 @@ impl TabulatedBSSRDF {
for (j, rho_weight) in rho_weights.iter().enumerate() { for (j, rho_weight) in rho_weights.iter().enumerate() {
if *rho_weight != 0. { if *rho_weight != 0. {
// Update _rhoEff_ and _sr_ for wavelength // Update _rhoEff_ and _sr_ for wavelength
rho_eff += rhoeff_samples[rho_offset as usize + j] * rho_weight; rho_eff += rhoeff_samples[(rho_offset + j as i32) as usize] * rho_weight;
// Fix: Use .iter().enumerate() for 'k' // Fix: Use .iter().enumerate() for 'k'
for (k, radius_weight) in radius_weights.iter().enumerate() { for (k, radius_weight) in radius_weights.iter().enumerate() {
if *radius_weight != 0. { if *radius_weight != 0. {
sr += self sr += self
.table .table
.eval_profile(rho_offset + j as u32, radius_offset + k as u32) .eval_profile((rho_offset + j as i32) as u32, (radius_offset + k as i32) as u32)
* rho_weight * rho_weight
* radius_weight; * radius_weight;
} }

View file

@ -137,7 +137,7 @@ pub trait CameraTrait {
fn generate_ray(&self, sample: CameraSample, lambda: &SampledWavelengths) -> Option<CameraRay>; fn generate_ray(&self, sample: CameraSample, lambda: &SampledWavelengths) -> Option<CameraRay>;
fn get_film(&self) -> &Film { fn get_film(&self) -> &Film {
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
{ {
if self.base().film.is_null() { if self.base().film.is_null() {
panic!( panic!(

View file

@ -625,7 +625,8 @@ impl RGBSigmoidPolynomial {
} }
pub fn evaluate(&self, lambda: Float) -> Float { pub fn evaluate(&self, lambda: Float) -> Float {
let eval = evaluate_polynomial(lambda, &[self.c0, self.c1, self.c2]); // pbrt: `s(EvaluatePolynomial(lambda, c2, c1, c0))` -- c2 is the constant term.
let eval = evaluate_polynomial(lambda, &[self.c2, self.c1, self.c0]);
Self::s(eval) Self::s(eval)
} }

View file

@ -96,7 +96,7 @@ impl RGBFilm {
} }
pub fn get_sensor(&self) -> &PixelSensor { pub fn get_sensor(&self) -> &PixelSensor {
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
{ {
if self.base.sensor.is_null() { if self.base.sensor.is_null() {
panic!( panic!(
@ -203,7 +203,7 @@ impl RGBFilm {
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(target_os = "cuda", derive(Copy))] #[cfg_attr(gpu, derive(Copy))]
pub struct GBufferPixel { pub struct GBufferPixel {
pub rgb_sum: [AtomicFloat; 3], pub rgb_sum: [AtomicFloat; 3],
pub weight_sum: AtomicFloat, pub weight_sum: AtomicFloat,
@ -240,7 +240,7 @@ impl Default for GBufferPixel {
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(target_os = "cuda", derive(Copy))] #[cfg_attr(gpu, derive(Copy))]
pub struct GBufferFilm { pub struct GBufferFilm {
pub base: FilmBase, pub base: FilmBase,
pub output_from_render: AnimatedTransform, pub output_from_render: AnimatedTransform,
@ -294,7 +294,7 @@ impl GBufferFilm {
} }
pub fn get_sensor(&self) -> &PixelSensor { pub fn get_sensor(&self) -> &PixelSensor {
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
{ {
if self.base.sensor.is_null() { if self.base.sensor.is_null() {
panic!( panic!(
@ -387,7 +387,7 @@ impl GBufferFilm {
#[repr(C)] #[repr(C)]
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(target_os = "cuda", derive(Copy))] #[cfg_attr(gpu, derive(Copy))]
pub struct SpectralPixel { pub struct SpectralPixel {
pub rgb_sum: [AtomicFloat; 3], pub rgb_sum: [AtomicFloat; 3],
pub rgb_weight_sum: AtomicFloat, pub rgb_weight_sum: AtomicFloat,
@ -419,7 +419,7 @@ impl Default for SpectralPixel {
#[repr(C)] #[repr(C)]
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] #[cfg_attr(gpu, derive(Copy, Clone))]
pub struct SpectralFilm { pub struct SpectralFilm {
pub base: FilmBase, pub base: FilmBase,
pub lambda_min: Float, pub lambda_min: Float,
@ -609,7 +609,7 @@ pub struct FilmBase {
#[repr(C)] #[repr(C)]
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(target_os = "cuda", derive(Copy, Clone))] #[cfg_attr(gpu, derive(Copy, Clone))]
pub enum Film { pub enum Film {
RGB(RGBFilm), RGB(RGBFilm),
GBuffer(GBufferFilm), GBuffer(GBufferFilm),

View file

@ -590,7 +590,7 @@ impl SurfaceInteraction {
} }
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
pub fn set_intersection_properties( pub fn set_intersection_properties(
&mut self, &mut self,
mtl: MaterialIdx, mtl: MaterialIdx,

View file

@ -61,7 +61,7 @@ pub struct LightLiSample {
pub p_light: Interaction, pub p_light: Interaction,
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
impl LightLiSample { impl LightLiSample {
pub fn new(l: SampledSpectrum, wi: Vector3f, pdf: Float, p_light: Interaction) -> Self { pub fn new(l: SampledSpectrum, wi: Vector3f, pdf: Float, p_light: Interaction) -> Self {
Self { Self {
@ -188,7 +188,7 @@ pub struct LightBounds {
pub two_sided: bool, pub two_sided: bool,
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
impl LightBounds { impl LightBounds {
pub fn new( pub fn new(
bounds: &Bounds3f, bounds: &Bounds3f,
@ -327,13 +327,13 @@ pub trait LightTrait {
self.base().light_type self.base().light_type
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn bounds(&self) -> Option<LightBounds>; fn bounds(&self) -> Option<LightBounds>;
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn preprocess(&mut self, scene_bounds: &Bounds3f); fn preprocess(&mut self, scene_bounds: &Bounds3f);
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum; fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum;
} }

View file

@ -99,7 +99,7 @@ pub struct MajorantGrid {
impl MajorantGrid { impl MajorantGrid {
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
pub fn new(bounds: Bounds3f, res: Point3i) -> Self { pub fn new(bounds: Bounds3f, res: Point3i) -> Self {
let n_voxels = (res.x() * res.y() * res.z()) as usize; let n_voxels = (res.x() * res.y() * res.z()) as usize;
let voxels = gvec_with_capacity(n_voxels); let voxels = gvec_with_capacity(n_voxels);

View file

@ -192,15 +192,18 @@ impl SamplerTrait for HaltonSampler {
} }
fn get1d(&mut self) -> Float { fn get1d(&mut self) -> Float {
if self.dim + 1 >= PRIME_TABLE_SIZE as u32 { // pbrt: `SampleDimension(dimension++)` -- POST-increment. Pre-incrementing makes
// the next Get2D() reuse the dimension this call just consumed.
if self.dim >= PRIME_TABLE_SIZE as u32 {
self.dim = 2; self.dim = 2;
} }
let dim = self.dim;
self.dim += 1; self.dim += 1;
self.sample_dimension(self.dim) self.sample_dimension(dim)
} }
fn get2d(&mut self) -> Point2f { fn get2d(&mut self) -> Point2f {
if self.dim > PRIME_TABLE_SIZE as u32 { if self.dim + 1 >= PRIME_TABLE_SIZE as u32 {
self.dim = 2; self.dim = 2;
} }
let dim = self.dim; let dim = self.dim;
@ -301,8 +304,9 @@ impl SamplerTrait for StratifiedSampler {
hash as u32, hash as u32,
); );
self.dim += 2; self.dim += 2;
// pbrt: both the modulus and the divisor are xPixelSamples.
let x = stratum % self.x_pixel_samples as u32; let x = stratum % self.x_pixel_samples as u32;
let y = stratum / self.y_pixel_samples as u32; let y = stratum / self.x_pixel_samples as u32;
let dx = if self.jitter { let dx = if self.jitter {
self.rng.uniform::<Float>() self.rng.uniform::<Float>()
} else { } else {
@ -387,6 +391,7 @@ impl SamplerTrait for PaddedSobolSampler {
self.samples_per_pixel as u32, self.samples_per_pixel as u32,
hash as u32, hash as u32,
); );
self.dim += 1;
self.sample_dimension(0, index, (hash >> 32) as u32) self.sample_dimension(0, index, (hash >> 32) as u32)
} }
fn get2d(&mut self) -> Point2f { fn get2d(&mut self) -> Point2f {
@ -512,7 +517,7 @@ impl SamplerTrait for SobolSampler {
) as Float; ) as Float;
u[1] = clamp( u[1] = clamp(
u[1] * self.scale as Float - self.pixel[1] as Float, u[1] * self.scale as Float - self.pixel[1] as Float,
1., 0.,
ONE_MINUS_EPSILON, ONE_MINUS_EPSILON,
) as Float; ) as Float;
u u

View file

@ -1,9 +1,9 @@
use crate::core::geometry::{ use crate::core::geometry::{
abs_cos_theta, cos2_theta, cos_phi, sin_phi, tan2_theta, Normal3f, Point2f, Vector2f, Vector3f, Normal3f, Point2f, Vector2f, Vector3f, VectorLike, abs_cos_theta, cos_phi, cos2_theta, sin_phi,
VectorLike, tan2_theta,
}; };
use crate::core::pbrt::{Float, PI}; use crate::core::pbrt::{Float, INV_4_PI, PI};
use crate::spectra::{SampledSpectrum, N_SPECTRUM_SAMPLES}; use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum};
use crate::utils::math::{clamp, lerp, safe_sqrt, square}; use crate::utils::math::{clamp, lerp, safe_sqrt, square};
use crate::utils::sampling::sample_uniform_disk_polar; use crate::utils::sampling::sample_uniform_disk_polar;
use num_traits::Float as NumFloat; use num_traits::Float as NumFloat;
@ -188,11 +188,9 @@ pub fn fresnel_moment1(eta: Float) -> Float {
let eta4 = eta3 * eta; let eta4 = eta3 * eta;
let eta5 = eta4 * eta; let eta5 = eta4 * eta;
if eta < 1. { if eta < 1. {
return 0.45966 - 1.73965 * eta + 3.37668 * eta2 - 3.904945 * eta3 + 2.49277 * eta4 0.45966 - 1.73965 * eta + 3.37668 * eta2 - 3.904945 * eta3 + 2.49277 * eta4 - 0.68441 * eta5
- 0.68441 * eta5;
} else { } else {
return -4.61686 + 11.1136 * eta - 10.4646 * eta2 + 5.11455 * eta3 - 1.27198 * eta4 -4.61686 + 11.1136 * eta - 10.4646 * eta2 + 5.11455 * eta3 - 1.27198 * eta4 + 0.12746 * eta5
+ 0.12746 * eta5;
} }
} }
@ -203,18 +201,28 @@ pub fn fresnel_moment2(eta: Float) -> Float {
let eta5 = eta4 * eta; let eta5 = eta4 * eta;
if eta < 1. { if eta < 1. {
return 0.27614 - 0.87350 * eta + 1.12077 * eta2 - 0.65095 * eta3 0.27614 - 0.87350 * eta + 1.12077 * eta2 - 0.65095 * eta3 + 0.07883 * eta4 + 0.04860 * eta5
+ 0.07883 * eta4
+ 0.04860 * eta5;
} else { } else {
let r_eta = 1. / eta; let r_eta = 1. / eta;
let r_eta2 = r_eta * r_eta; let r_eta2 = r_eta * r_eta;
let r_eta3 = r_eta2 * r_eta; let r_eta3 = r_eta2 * r_eta;
return -547.033 + 45.3087 * r_eta3 - 218.725 * r_eta2 + 458.843 * r_eta + 404.557 * eta -547.033 + 45.3087 * r_eta3 - 218.725 * r_eta2 + 458.843 * r_eta + 404.557 * eta
- 189.519 * eta2 - 189.519 * eta2
+ 54.9327 * eta3 + 54.9327 * eta3
- 9.00603 * eta4 - 9.00603 * eta4
+ 0.63942 * eta5; + 0.63942 * eta5
} }
} }
#[inline]
pub fn henyey_greenstein(cos_theta: Float, mut g: Float) -> Float {
// The Henyey-Greenstein phase function isn't suitable for |g| \approx
// 1 so we clamp it before it becomes numerically instable. (It's an
// analogous situation to BSDFs: if the BSDF is perfectly specular, one
// should use one based on a Dirac delta distribution rather than a
// very smooth microfacet distribution...)
g = g.clamp(-0.99, 0.99);
let denom = 1. + square(g) + 2. * g * cos_theta;
INV_4_PI * (1. - square(g)) / (denom * safe_sqrt(denom))
}

View file

@ -36,6 +36,15 @@ pub enum Spectrum {
RGBUnbounded(RGBUnboundedSpectrum), RGBUnbounded(RGBUnboundedSpectrum),
} }
/// `enum_dispatch` already generates `From<Variant> 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<Float> for Spectrum {
fn from(c: Float) -> Self {
Spectrum::Constant(ConstantSpectrum::new(c))
}
}
impl<T: SpectrumTrait> SpectrumTrait for Ptr<T> { impl<T: SpectrumTrait> SpectrumTrait for Ptr<T> {
fn evaluate(&self, lambda: Float) -> Float { fn evaluate(&self, lambda: Float) -> Float {
self.get().unwrap().evaluate(lambda) self.get().unwrap().evaluate(lambda)

View file

@ -245,7 +245,7 @@ pub struct PointTransformMapping {
} }
impl PointTransformMapping { impl PointTransformMapping {
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
pub fn new(texture_from_render: Transform) -> Self { pub fn new(texture_from_render: Transform) -> Self {
Self { Self {
texture_from_render, texture_from_render,

View file

@ -130,7 +130,7 @@ impl LightTrait for DiffuseAreaLight {
} }
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
let mut l = SampledSpectrum::new(0.); let mut l = SampledSpectrum::new(0.);
if !self.image.is_null() { if !self.image.is_null() {
@ -153,10 +153,10 @@ impl LightTrait for DiffuseAreaLight {
PI * two_side * self.area * l PI * two_side * self.area * l
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn preprocess(&mut self, _scene_bounds: &Bounds3f) {} fn preprocess(&mut self, _scene_bounds: &Bounds3f) {}
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn bounds(&self) -> Option<LightBounds> { fn bounds(&self) -> Option<LightBounds> {
let mut phi = 0.; let mut phi = 0.;
if !self.image.is_null() { if !self.image.is_null() {

View file

@ -1,5 +1,8 @@
use crate::core::geometry::{Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector3f}; use crate::core::geometry::{
Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector3f, VectorLike,
};
use crate::core::image::Image; use crate::core::image::Image;
use crate::core::interaction::{Interaction, InteractionBase, SimpleInteraction};
use crate::core::light::{ use crate::core::light::{
LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType,
}; };
@ -34,12 +37,20 @@ impl LightTrait for GoniometricLight {
fn sample_li( fn sample_li(
&self, &self,
_ctx: &LightSampleContext, ctx: &LightSampleContext,
_u: Point2f, _u: Point2f,
_lambda: &SampledWavelengths, lambda: &SampledWavelengths,
_allow_incomplete_pdf: bool, _allow_incomplete_pdf: bool,
) -> Option<LightLiSample> { ) -> Option<LightLiSample> {
todo!() let render_from_light = self.base().render_from_light;
let p = render_from_light.apply_to_point(Point3f::new(0., 0., 0.));
let wi = (p - ctx.p()).normalize();
let wl = render_from_light.apply_inverse_vector(-wi);
let li = self.i(wl, lambda) / p.distance_squared(ctx.p());
let base = InteractionBase::new_boundary(p, 0., self.base.medium_interface);
let intr = SimpleInteraction::new(base);
Some(LightLiSample::new(li, wi, 1., Interaction::Simple(intr)))
} }
fn pdf_li( fn pdf_li(
@ -76,7 +87,7 @@ impl LightTrait for GoniometricLight {
)) ))
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
let resolution = self.image.resolution(); let resolution = self.image.resolution();
let mut sum_y = 0.; let mut sum_y = 0.;

View file

@ -109,7 +109,7 @@ impl LightTrait for UniformInfiniteLight {
None None
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
4. * PI * PI * square(self.scene_radius) * self.scale * self.lemit.sample(&lambda) 4. * PI * PI * square(self.scene_radius) * self.scale * self.lemit.sample(&lambda)
} }
@ -227,21 +227,17 @@ impl LightTrait for ImageInfiniteLight {
self.image_le(uv, lambda) self.image_le(uv, lambda)
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
let mut sum_l = SampledSpectrum::new(0.); let mut sum_l = SampledSpectrum::new(0.);
let width = self.image.resolution().x(); let width = self.image.resolution().x();
let height = self.image.resolution().y(); let height = self.image.resolution().y();
for v in 0..height { for v in 0..height {
for u in 0..width { for u in 0..width {
let mut rgb = RGB::default(); let rgb = RGB::from(self.image.get_channels_with_wrap::<3>(
for c in 0..3 { Point2i::new(u, v),
rgb[c] = self.image.get_channel_with_wrap( WrapMode::OctahedralSphere.into(),
Point2i::new(u, v), ));
c,
WrapMode::OctahedralSphere.into(),
);
}
sum_l += RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero()) sum_l += RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero())
.sample(&lambda); .sample(&lambda);
} }
@ -341,7 +337,7 @@ impl PortalInfiniteLight {
(self.portal[1] - self.portal[0]).norm() * (self.portal[3] - self.portal[0]).norm() (self.portal[1] - self.portal[0]).norm() * (self.portal[3] - self.portal[0]).norm()
} }
pub fn render_from_image(portal_frame: Frame, uv: Point2f) -> (Vector3f, Float) { pub fn render_from_image_with(portal_frame: Frame, uv: Point2f) -> (Vector3f, Float) {
let alpha = -PI / 2.0 + uv.x() * PI; let alpha = -PI / 2.0 + uv.x() * PI;
let beta = -PI / 2.0 + uv.y() * PI; let beta = -PI / 2.0 + uv.y() * PI;
@ -354,6 +350,11 @@ impl PortalInfiniteLight {
(portal_frame.from_local(w), duv_dw) (portal_frame.from_local(w), duv_dw)
} }
#[inline]
pub fn render_from_image(&self, uv: Point2f) -> (Vector3f, Float) {
Self::render_from_image_with(self.portal_frame, uv)
}
} }
impl LightTrait for PortalInfiniteLight { impl LightTrait for PortalInfiniteLight {
@ -370,7 +371,7 @@ impl LightTrait for PortalInfiniteLight {
) -> Option<LightLiSample> { ) -> Option<LightLiSample> {
let b = self.image_bounds(ctx.p())?; let b = self.image_bounds(ctx.p())?;
let (uv, map_pdf) = self.distribution.sample(u, b)?; let (uv, map_pdf) = self.distribution.sample(u, b)?;
let (wi, duv_dw) = Self::render_from_image(self.portal_frame, uv); let (wi, duv_dw) = self.render_from_image(uv);
if duv_dw == 0. { if duv_dw == 0. {
return None; return None;
} }
@ -403,17 +404,34 @@ impl LightTrait for PortalInfiniteLight {
} }
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, _lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
todo!() let mut sum_l = SampledSpectrum::new(0.);
let width = self.image.resolution().x();
let height = self.image.resolution().y();
for y in 0..height {
for x in 0..width {
let rgb = RGB::from(self.image.get_channels::<3>(Point2i::new(x, y)));
let st = Point2f::new(
(x as Float + 0.5) / width as Float,
(y as Float + 0.5) / height as Float,
);
let (_, duv_dw) = self.render_from_image(st);
sum_l += RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero())
.sample(&lambda)
/ duv_dw;
}
}
self.scale * self.area() * sum_l / (width * height) as Float
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn preprocess(&mut self, scene_bounds: &Bounds3f) { fn preprocess(&mut self, scene_bounds: &Bounds3f) {
(self.scene_center, self.scene_radius) = scene_bounds.bounding_sphere(); (self.scene_center, self.scene_radius) = scene_bounds.bounding_sphere();
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn bounds(&self) -> Option<LightBounds> { fn bounds(&self) -> Option<LightBounds> {
None None
} }

View file

@ -51,14 +51,14 @@ impl LightTrait for PointLight {
0. 0.
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
4. * PI * self.scale * self.i.sample(&lambda) 4. * PI * self.scale * self.i.sample(&lambda)
} }
fn preprocess(&mut self, _scene_bounds: &Bounds3f) {} fn preprocess(&mut self, _scene_bounds: &Bounds3f) {}
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn bounds(&self) -> Option<LightBounds> { fn bounds(&self) -> Option<LightBounds> {
let p = self let p = self
.base .base

View file

@ -4,6 +4,7 @@ use crate::core::geometry::{
Bounds2f, Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector3f, VectorLike, cos_theta, Bounds2f, Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector3f, VectorLike, cos_theta,
}; };
use crate::core::image::Image; use crate::core::image::Image;
use crate::core::interaction::{Interaction, InteractionBase, SimpleInteraction};
use crate::core::light::{ use crate::core::light::{
LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType,
}; };
@ -33,7 +34,7 @@ pub struct ProjectionLight {
} }
impl ProjectionLight { impl ProjectionLight {
pub fn i(&self, w: Vector3f, lambda: SampledWavelengths) -> SampledSpectrum { pub fn i(&self, w: Vector3f, lambda: &SampledWavelengths) -> SampledSpectrum {
if w.z() < self.hither { if w.z() < self.hither {
return SampledSpectrum::new(0.); return SampledSpectrum::new(0.);
} }
@ -44,10 +45,10 @@ impl ProjectionLight {
let uv = Point2f::from(self.screen_bounds.offset(&Point2f::new(ps.x(), ps.y()))); let uv = Point2f::from(self.screen_bounds.offset(&Point2f::new(ps.x(), ps.y())));
let mut rgb = RGB::default(); let mut rgb = RGB::default();
for c in 0..3 { for c in 0..3 {
rgb[c] = self.image.lookup_nearest_channel(uv, c as i32); rgb[c] = self.image.lookup_nearest_channel(uv, c);
} }
let s = RGBIlluminantSpectrum::new(&*self.image_color_space, rgb.clamp_zero()); let s = RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero());
self.scale * s.sample(&lambda) self.scale * s.sample(lambda)
} }
} }
@ -58,12 +59,23 @@ impl LightTrait for ProjectionLight {
fn sample_li( fn sample_li(
&self, &self,
_ctx: &LightSampleContext, ctx: &LightSampleContext,
_u: Point2f, _u: Point2f,
_lambda: &SampledWavelengths, lambda: &SampledWavelengths,
_allow_incomplete_pdf: bool, _allow_incomplete_pdf: bool,
) -> Option<LightLiSample> { ) -> Option<LightLiSample> {
todo!() let render_from_light = self.base().render_from_light;
let p = render_from_light.apply_to_point(Point3f::new(0., 0., 0.));
let wi = (p - ctx.p()).normalize();
let wl = render_from_light.apply_inverse_vector(-wi);
let li = self.i(wl, lambda) / p.distance_squared(ctx.p());
if li.is_black() {
return None;
}
let base = InteractionBase::new_boundary(p, 0., self.base.medium_interface);
let intr = SimpleInteraction::new(base);
Some(LightLiSample::new(li, wi, 1., Interaction::Simple(intr)))
} }
fn pdf_li( fn pdf_li(
@ -72,7 +84,7 @@ impl LightTrait for ProjectionLight {
_wi: Vector3f, _wi: Vector3f,
_allow_incomplete_pdf: bool, _allow_incomplete_pdf: bool,
) -> Float { ) -> Float {
todo!() 0.
} }
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
@ -93,10 +105,10 @@ impl LightTrait for ProjectionLight {
let dwda = cos_theta(w).powi(3); let dwda = cos_theta(w).powi(3);
let mut rgb = RGB::default(); let mut rgb = RGB::default();
for c in 0..3 { for c in 0..3 {
rgb[c] = self.image.get_channel(Point2i::new(x, y), c as i32); rgb[c] = self.image.get_channel(Point2i::new(x, y), c);
} }
let s = RGBIlluminantSpectrum::new(&*self.image_color_space, rgb.clamp_zero()); let s = RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero());
sum += s.sample(&lambda) * dwda; sum += s.sample(&lambda) * dwda;
} }
} }

View file

@ -65,7 +65,7 @@ impl LightTrait for SpotLight {
0. 0.
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
self.scale self.scale
* self.iemit.sample(&lambda) * self.iemit.sample(&lambda)

View file

@ -4,53 +4,57 @@ use crate::bxdfs::{
MeasuredBxDF, MeasuredBxDFData, MeasuredBxDF, MeasuredBxDFData,
}; };
use crate::core::bsdf::BSDF; 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::bxdf::BxDF;
use crate::core::image::Image; use crate::core::image::Image;
use crate::core::material::{Material, MaterialEvalContext, MaterialTrait}; use crate::core::material::{Material, MaterialEvalContext, MaterialTrait};
use crate::core::scattering::TrowbridgeReitzDistribution; use crate::core::scattering::TrowbridgeReitzDistribution;
use crate::core::spectrum::{Spectrum, SpectrumTrait}; use crate::core::spectrum::{Spectrum, SpectrumTrait};
use crate::core::texture::{FloatTexture, SpectrumTexture, TextureEvaluator}; use crate::core::texture::{FloatTexture, SpectrumTexture, TextureEvaluator};
use crate::spectra::{SampledSpectrum, SampledWavelengths}; use crate::spectra::{RGBColorSpace, SampledSpectrum, SampledWavelengths};
use crate::textures::SpectrumMixTexture; use crate::textures::SpectrumMixTexture;
use crate::utils::Ptr; use crate::utils::Ptr;
use crate::utils::math::clamp; use crate::utils::math::clamp;
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub enum HairAbsorption {
SigmaA(Ptr<SpectrumTexture>),
Color(Ptr<SpectrumTexture>),
Melanin {
eumelanin: Ptr<FloatTexture>,
pheomelanin: Ptr<FloatTexture>,
},
}
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct HairMaterial { pub struct HairMaterial {
pub sigma_a: Ptr<SpectrumTexture>, pub hair_absorption: HairAbsorption,
pub color: Ptr<SpectrumTexture>,
pub eumelanin: Ptr<FloatTexture>,
pub pheomelanin: Ptr<FloatTexture>,
pub eta: Ptr<FloatTexture>, pub eta: Ptr<FloatTexture>,
pub beta_m: Ptr<FloatTexture>, pub beta_m: Ptr<FloatTexture>,
pub beta_n: Ptr<FloatTexture>, pub beta_n: Ptr<FloatTexture>,
pub alpha: Ptr<FloatTexture>, pub alpha: Ptr<FloatTexture>,
pub colorspace: Ptr<RGBColorSpace>,
} }
impl HairMaterial { impl HairMaterial {
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
sigma_a: Ptr<SpectrumTexture>, hair_absorption: HairAbsorption,
color: Ptr<SpectrumTexture>,
eumelanin: Ptr<FloatTexture>,
pheomelanin: Ptr<FloatTexture>,
eta: Ptr<FloatTexture>, eta: Ptr<FloatTexture>,
beta_m: Ptr<FloatTexture>, beta_m: Ptr<FloatTexture>,
beta_n: Ptr<FloatTexture>, beta_n: Ptr<FloatTexture>,
alpha: Ptr<FloatTexture>, alpha: Ptr<FloatTexture>,
colorspace: Ptr<RGBColorSpace>,
) -> Self { ) -> Self {
Self { Self {
sigma_a, hair_absorption,
color,
eumelanin,
pheomelanin,
eta, eta,
beta_m, beta_m,
beta_n, beta_n,
alpha, alpha,
colorspace,
} }
} }
} }
@ -58,27 +62,86 @@ impl HairMaterial {
impl MaterialTrait for HairMaterial { impl MaterialTrait for HairMaterial {
fn get_bsdf<T: TextureEvaluator>( fn get_bsdf<T: TextureEvaluator>(
&self, &self,
_tex_eval: &T, tex_eval: &T,
_ctx: &MaterialEvalContext, ctx: &MaterialEvalContext,
_lambda: &mut SampledWavelengths, lambda: &mut SampledWavelengths,
) -> BSDF { ) -> BSDF {
todo!() let bm = tex_eval.evaluate_float(&self.beta_m, ctx).clamp(1e-2, 1.0);
let bn = tex_eval.evaluate_float(&self.beta_n, ctx).clamp(1e-2, 1.0);
let a = tex_eval.evaluate_float(&self.alpha, ctx);
let e = tex_eval.evaluate_float(&self.eta, ctx);
let sig_a = match self.hair_absorption {
// Absorption coefficient, not reflectance, can be larger than 1
HairAbsorption::SigmaA(sigma_a) => {
SampledSpectrum::clamp_zero(&tex_eval.evaluate_spectrum(&sigma_a, ctx, lambda))
}
HairAbsorption::Color(color) => {
let c = SampledSpectrum::clamp(
&tex_eval.evaluate_spectrum(&color, ctx, lambda),
0.,
1.,
);
HairBxDF::sigma_a_from_reflectance(c, bn)
}
HairAbsorption::Melanin {
eumelanin,
pheomelanin,
} => {
debug_assert!(!eumelanin.is_null() || !pheomelanin.is_null());
let eu = if !eumelanin.is_null() {
tex_eval.evaluate_float(&eumelanin, ctx)
} else {
0.
};
let pheo = if !pheomelanin.is_null() {
tex_eval.evaluate_float(&pheomelanin, ctx)
} else {
0.
};
HairBxDF::sigma_a_from_concentration(eu.max(0.0), pheo.max(0.0), self.colorspace)
.sample(lambda)
}
};
let h = -1. + 2. * ctx.uv[1];
let bxdf = BxDF::Hair(HairBxDF::new(h, e, sig_a, bm, bn, a));
BSDF::new(ctx.ns, ctx.dpdus, bxdf)
} }
fn get_bssrdf<T>( fn get_bssrdf<T>(
&self, &self,
_tex_eval: &T, _tex_eval: &T,
_ctx: &MaterialEvalContext, _ctx: &MaterialEvalContext,
_lambda: &SampledWavelengths, _lambda: &SampledWavelengths,
) -> Option<BSSRDF> { ) -> Option<BSSRDF> {
todo!() None
} }
fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool { fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool {
todo!() 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> { fn get_normal_map(&self) -> Option<&Image> {
todo!() None
} }
fn get_displacement(&self) -> Ptr<FloatTexture> { fn get_displacement(&self) -> Ptr<FloatTexture> {
@ -102,11 +165,11 @@ impl MaterialTrait for MeasuredMaterial {
fn get_bsdf<T: TextureEvaluator>( fn get_bsdf<T: TextureEvaluator>(
&self, &self,
_tex_eval: &T, _tex_eval: &T,
_ctx: &MaterialEvalContext, ctx: &MaterialEvalContext,
_lambda: &mut SampledWavelengths, lambda: &mut SampledWavelengths,
) -> BSDF { ) -> BSDF {
// MeasuredBxDF::new(&self.brdf, lambda) let bxdf = BxDF::Measured(MeasuredBxDF::new(&self.brdf, lambda));
todo!() BSDF::new(ctx.ns, ctx.dpdus, bxdf)
} }
fn get_bssrdf<T>( fn get_bssrdf<T>(
@ -135,15 +198,25 @@ impl MaterialTrait for MeasuredMaterial {
} }
} }
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub enum SubsurfaceScattering {
Coefficients {
sigma_a: Ptr<SpectrumTexture>,
sigma_s: Ptr<SpectrumTexture>,
},
Reflectance {
reflectance: Ptr<SpectrumTexture>,
mfp: Ptr<SpectrumTexture>,
},
}
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct SubsurfaceMaterial { pub struct SubsurfaceMaterial {
pub normal_map: Ptr<Image>, pub normal_map: Ptr<Image>,
pub displacement: Ptr<FloatTexture>, pub displacement: Ptr<FloatTexture>,
pub sigma_a: Ptr<SpectrumTexture>, pub scattering: SubsurfaceScattering,
pub sigma_s: Ptr<SpectrumMixTexture>,
pub reflectance: Ptr<SpectrumMixTexture>,
pub mfp: Ptr<SpectrumMixTexture>,
pub eta: Float, pub eta: Float,
pub scale: Float, pub scale: Float,
pub u_roughness: Ptr<FloatTexture>, pub u_roughness: Ptr<FloatTexture>,
@ -155,31 +228,78 @@ pub struct SubsurfaceMaterial {
impl MaterialTrait for SubsurfaceMaterial { impl MaterialTrait for SubsurfaceMaterial {
fn get_bsdf<T: TextureEvaluator>( fn get_bsdf<T: TextureEvaluator>(
&self, &self,
_tex_eval: &T, tex_eval: &T,
_ctx: &MaterialEvalContext, ctx: &MaterialEvalContext,
_lambda: &mut SampledWavelengths, _lambda: &mut SampledWavelengths,
) -> BSDF { ) -> BSDF {
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);
fn get_bssrdf<T>( if self.remap_roughness {
&self, u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough);
_tex_eval: &T, v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough);
_ctx: &MaterialEvalContext, }
_lambda: &SampledWavelengths,
) -> Option<BSSRDF> { let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough);
todo!() 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 { fn get_bssrdf<T: TextureEvaluator>(
todo!() &self,
tex_eval: &T,
ctx: &MaterialEvalContext,
lambda: &SampledWavelengths,
) -> Option<BSSRDF> {
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> { fn get_normal_map(&self) -> Option<&Image> {
todo!() Some(&*self.normal_map)
} }
fn get_displacement(&self) -> Ptr<FloatTexture> { fn get_displacement(&self) -> Ptr<FloatTexture> {
todo!() self.displacement
} }
fn has_subsurface_scattering(&self) -> bool { fn has_subsurface_scattering(&self) -> bool {

View file

@ -27,6 +27,7 @@ pub struct ConductorMaterial {
} }
impl ConductorMaterial { impl ConductorMaterial {
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
normal_map: Ptr<Image>, normal_map: Ptr<Image>,
reflectance: Ptr<SpectrumTexture>, reflectance: Ptr<SpectrumTexture>,
@ -92,7 +93,7 @@ impl MaterialTrait for ConductorMaterial {
_ctx: &MaterialEvalContext, _ctx: &MaterialEvalContext,
_lambda: &SampledWavelengths, _lambda: &SampledWavelengths,
) -> Option<BSSRDF> { ) -> Option<BSSRDF> {
todo!() None
} }
fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool { fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool {
tex_eval.can_evaluate( tex_eval.can_evaluate(

View file

@ -98,7 +98,7 @@ impl BilinearPatchShape {
Some([mesh.n[v0], mesh.n[v1], mesh.n[v2], mesh.n[v3]]) Some([mesh.n[v0], mesh.n[v1], mesh.n[v2], mesh.n[v3]])
} }
#[cfg(not(target_os = "cuda"))] #[cfg(not(gpu))]
pub fn new(mesh: Ptr<BilinearPatchMesh>, blp_index: i32) -> Self { pub fn new(mesh: Ptr<BilinearPatchMesh>, blp_index: i32) -> Self {
let mut bp = BilinearPatchShape { let mut bp = BilinearPatchShape {
mesh, mesh,

View file

@ -1,8 +1,8 @@
use crate::core::geometry::{spherical_triangle_area, SqrtExt, Tuple, VectorLike};
use crate::core::geometry::{ use crate::core::geometry::{
Bounds3f, DirectionCone, Normal, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3, Bounds3f, DirectionCone, Normal, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3,
Vector3f, Vector3f,
}; };
use crate::core::geometry::{SqrtExt, Tuple, VectorLike, spherical_triangle_area};
use crate::core::interaction::{ use crate::core::interaction::{
Interaction, InteractionBase, InteractionTrait, SimpleInteraction, SurfaceInteraction, Interaction, InteractionBase, InteractionTrait, SimpleInteraction, SurfaceInteraction,
}; };
@ -13,7 +13,7 @@ use crate::utils::sampling::{
bilinear_pdf, invert_spherical_triangle_sample, sample_bilinear, sample_spherical_triangle, bilinear_pdf, invert_spherical_triangle_sample, sample_bilinear, sample_spherical_triangle,
sample_uniform_triangle, sample_uniform_triangle,
}; };
use crate::{gamma, Float, GVec, Ptr}; use crate::{Float, GVec, Ptr, gamma};
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -198,34 +198,34 @@ impl TriangleShape {
// Ensure that computed triangle $t$ is conservatively greater than zero // Ensure that computed triangle $t$ is conservatively greater than zero
// Compute $\delta_z$ term for triangle $t$ error bounds // Compute $\delta_z$ term for triangle $t$ error bounds
let maxZt = Vector3f::new(p0t.z(), p1t.z(), p2t.z()) let max_zt = Vector3f::new(p0t.z(), p1t.z(), p2t.z())
.abs() .abs()
.max_component_value(); .max_component_value();
let deltaZ = gamma(3) * maxZt; let delta_z = gamma(3) * max_zt;
// Compute $\delta_x$ and $\delta_y$ terms for triangle $t$ error bounds // Compute $\delta_x$ and $\delta_y$ terms for triangle $t$ error bounds
let maxXt = Vector3f::new(p0t.x(), p1t.x(), p2t.x()) let max_xt = Vector3f::new(p0t.x(), p1t.x(), p2t.x())
.abs() .abs()
.max_component_value(); .max_component_value();
let maxYt = Vector3f::new(p0t.y(), p1t.y(), p2t.y()) let max_yt = Vector3f::new(p0t.y(), p1t.y(), p2t.y())
.abs() .abs()
.max_component_value(); .max_component_value();
let deltaX = gamma(5) * (maxXt + maxZt); let delta_x = gamma(5) * (max_xt + max_zt);
let deltaY = gamma(5) * (maxYt + maxZt); let delta_y = gamma(5) * (max_yt + max_zt);
// Compute $\delta_e$ term for triangle $t$ error bounds // Compute $\delta_e$ term for triangle $t$ error bounds
let deltaE = 2. * (gamma(2) * maxXt * maxYt + deltaY * maxXt + deltaX * maxYt); let delta_e = 2. * (gamma(2) * max_xt * max_yt + delta_y * max_xt + delta_x * max_yt);
// Compute $\delta_t$ term for triangle $t$ error bounds and check _t_ // Compute $\delta_t$ term for triangle $t$ error bounds and check _t_
let maxE = Vector3f::new(e0, e1, e2).abs().max_component_value(); let max_e = Vector3f::new(e0, e1, e2).abs().max_component_value();
let deltaT = let delta_t =
3. * (gamma(3) * maxE * maxZt + deltaE * maxZt + deltaZ * maxE) * inv_det.abs(); 3. * (gamma(3) * max_e * max_zt + delta_e * max_zt + delta_z * max_e) * inv_det.abs();
if t <= deltaT { if t <= delta_t {
return None; return None;
} }
// Return _TriangleIntersection_ for intersection // Return _TriangleIntersection_ for intersection
return Some(TriangleIntersection { b0, b1, b2, t }); Some(TriangleIntersection { b0, b1, b2, t })
} }
fn interaction_from_intersection( fn interaction_from_intersection(
@ -345,11 +345,11 @@ impl TriangleShape {
let mut ts = ns.cross(ss.into()); let mut ts = ns.cross(ss.into());
if ts.norm_squared() > 0.0 { if ts.norm_squared() > 0.0 {
ss = ts.cross(ns.into()).into(); ss = ts.cross(ns).into();
} else { } else {
let (s, t) = ns.coordinate_system(); let (s, t) = ns.coordinate_system();
ss = s.into(); ss = s.into();
ts = t.into(); ts = t;
} }
let (dndu, dndv) = if let Some(normals) = self.get_shading_normals() { let (dndu, dndv) = if let Some(normals) = self.get_shading_normals() {
@ -539,7 +539,8 @@ impl ShapeTrait for TriangleShape {
fn intersect(&self, ray: &Ray, t_max: Option<Float>) -> Option<ShapeIntersection> { fn intersect(&self, ray: &Ray, t_max: Option<Float>) -> Option<ShapeIntersection> {
let [p0, p1, p2] = self.get_points(); let [p0, p1, p2] = self.get_points();
let tri_isect = self.intersect_triangle(ray, t_max.unwrap_or(Float::INFINITY), p0, p1, p2)?; let tri_isect =
self.intersect_triangle(ray, t_max.unwrap_or(Float::INFINITY), p0, p1, p2)?;
let intr = self.interaction_from_intersection(tri_isect, ray.time, -ray.d); let intr = self.interaction_from_intersection(tri_isect, ray.time, -ray.d);
Some(ShapeIntersection::new(intr, tri_isect.t)) Some(ShapeIntersection::new(intr, tri_isect.t))
} }

View file

@ -237,7 +237,10 @@ impl PiecewiseLinearSpectrum {
} }
} }
pub fn from_interleaved(data: &[Float], _normalize: bool) -> Self { /// pbrt `PiecewiseLinearSpectrum::FromInterleaved` (`util/spectrum.cpp`): `(lambda, value)`
/// pairs, extended flat to cover the full visible range, and -- when `normalize` is set --
/// scaled so that `InnerProduct(spec, Y) == CIE_Y_integral` ("normalize to luminance 1").
pub fn from_interleaved(data: &[Float], normalize: bool) -> Self {
assert!( assert!(
data.len() % 2 == 0, data.len() % 2 == 0,
"Interleaved data must have even length" "Interleaved data must have even length"
@ -250,13 +253,45 @@ impl PiecewiseLinearSpectrum {
} }
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal)); pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
let mut lambdas = gvec_with_capacity(n); let mut lambdas: GVec<Float> = gvec_with_capacity(n + 2);
let mut values = gvec_with_capacity(n); let mut values: GVec<Float> = gvec_with_capacity(n + 2);
// Extend samples to cover the range of visible wavelengths if needed.
if pairs[0].0 > LAMBDA_MIN as Float {
lambdas.push(LAMBDA_MIN as Float - 1.0);
values.push(pairs[0].1);
}
for (l, v) in pairs.iter() { for (l, v) in pairs.iter() {
lambdas.push(*l); lambdas.push(*l);
values.push(*v); values.push(*v);
} }
Self::new(lambdas, values) if *lambdas.last().unwrap() < LAMBDA_MAX as Float {
lambdas.push(LAMBDA_MAX as Float + 1.0);
values.push(*values.last().unwrap());
}
let mut spec = Self::new(lambdas, values);
if normalize {
// Normalize to have luminance of 1.
spec.scale(CIE_Y_INTEGRAL / spec.inner_product_with_cie_y());
}
spec
}
/// `InnerProduct(self, Spectra::Y())` -- pbrt sums over integer wavelengths across the
/// visible range, which is exactly the sampling of the tabulated `CIE_Y` curve.
pub fn inner_product_with_cie_y(&self) -> Float {
let mut integral = 0.0;
for (i, y) in CIE_Y.iter().enumerate() {
integral += *y * self.evaluate(LAMBDA_MIN as Float + i as Float);
}
integral
}
pub fn scale(&mut self, s: Float) {
for v in self.values.iter_mut() {
*v *= s;
}
} }
} }

View file

@ -39,8 +39,12 @@ impl FloatBilerpTexture {
} }
} }
pub fn evaluate(&self, _ctx: &TextureEvalContext) -> Float { pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float {
todo!() let c = self.mapping.map(ctx);
(1. - c.st[0]) * (1. - c.st[1]) * self.v00
+ c.st[0] * (1. - c.st[1]) * self.v10
+ (1. - c.st[0]) * c.st[1] * self.v01
+ c.st[0] * c.st[1] * self.v11
} }
} }

View file

@ -22,8 +22,25 @@ pub struct MarbleTexture {
pub colorspace: Ptr<RGBColorSpace>, pub colorspace: Ptr<RGBColorSpace>,
} }
impl MarbleTexture { impl MarbleTexture {
pub fn new(
mapping: TextureMapping3D,
octaves: i32,
omega: Float,
scale: Float,
variation: Float,
colorspace: Ptr<RGBColorSpace>,
) -> Self {
Self {
mapping,
octaves: octaves.try_into().unwrap(),
omega,
scale,
variation,
colorspace,
}
}
pub fn evaluate( pub fn evaluate(
&self, &self,
ctx: &TextureEvalContext, ctx: &TextureEvalContext,
@ -61,6 +78,6 @@ impl MarbleTexture {
let (rgb_vec, _) = evaluate_cubic_bezier(&colors[first_idx..first_idx + 4], t_segment); let (rgb_vec, _) = evaluate_cubic_bezier(&colors[first_idx..first_idx + 4], t_segment);
let rgb = RGB::new(rgb_vec.x() * 1.5, rgb_vec.y() * 1.5, rgb_vec.z() * 1.5); let rgb = RGB::new(rgb_vec.x() * 1.5, rgb_vec.y() * 1.5, rgb_vec.z() * 1.5);
RGBAlbedoSpectrum::new(&*self.colorspace, rgb).sample(lambda) RGBAlbedoSpectrum::new(&self.colorspace, rgb).sample(lambda)
} }
} }

View file

@ -10,6 +10,9 @@ pub struct WindyTexture {
} }
impl WindyTexture { impl WindyTexture {
pub fn new(mapping: TextureMapping3D) -> Self {
Self { mapping }
}
pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float {
let c = self.mapping.map(ctx); let c = self.mapping.map(ctx);
let wind_strength = fbm( let wind_strength = fbm(

View file

@ -11,6 +11,13 @@ pub struct WrinkledTexture {
} }
impl WrinkledTexture { impl WrinkledTexture {
pub fn new(mapping: TextureMapping3D, octaves: u32, omega: Float) -> Self {
Self {
mapping,
octaves,
omega,
}
}
pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float { pub fn evaluate(&self, ctx: &TextureEvalContext) -> Float {
let c = self.mapping.map(ctx); let c = self.mapping.map(ctx);
turbulence(c.p, c.dpdx, c.dpdy, self.omega, self.octaves) turbulence(c.p, c.dpdx, c.dpdy, self.omega, self.octaves)

View file

@ -1,9 +1,9 @@
use crate::core::color::{RGB, XYZ}; use crate::core::color::{RGB, XYZ};
use crate::core::geometry::{Lerp, MulAdd, Point, Point2f, Point2i, Vector, Vector3f, VectorLike}; 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::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::hash::{hash_buffer, mix_bits};
use crate::utils::sobol::{SOBOL_MATRICES_32, VDC_SOBOL_MATRICES, VDC_SOBOL_MATRICES_INV}; 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 crate::{GVec, Ptr, gvec, gvec_with_capacity};
use core::fmt::{self, Display, Write}; use core::fmt::{self, Display, Write};
use core::iter::{Product, Sum}; use core::iter::{Product, Sum};
@ -61,11 +61,14 @@ where
T::lerp(t, a, b) T::lerp(t, a, b)
} }
/// pbrt `EvaluatePolynomial(t, c, cRemaining...) = FMA(t, EvaluatePolynomial(t, cRemaining...), c)`.
/// `coeffs[0]` is the *constant* term and `coeffs[n-1]` the highest-degree one, so
/// evaluation folds from the back.
#[inline] #[inline]
pub fn evaluate_polynomial(t: Float, coeffs: &[Float]) -> Float { pub fn evaluate_polynomial(t: Float, coeffs: &[Float]) -> Float {
assert!(!coeffs.is_empty()); assert!(!coeffs.is_empty());
let mut result = coeffs[0]; let mut result = coeffs[coeffs.len() - 1];
for &c in &coeffs[1..] { for &c in coeffs[..coeffs.len() - 1].iter().rev() {
result = num_traits::Float::mul_add(t, result, c); result = num_traits::Float::mul_add(t, result, c);
} }
result result
@ -96,7 +99,7 @@ where
#[inline] #[inline]
pub fn safe_sqrt(x: Float) -> Float { pub fn safe_sqrt(x: Float) -> Float {
assert!(x > -1e-3); debug_assert!(x > -1e-3);
0.0_f32.max(x).sqrt() 0.0_f32.max(x).sqrt()
} }
@ -104,20 +107,14 @@ pub fn safe_sqrt(x: Float) -> Float {
pub fn safe_asin<T: NumFloat>(x: T) -> T { pub fn safe_asin<T: NumFloat>(x: T) -> T {
let epsilon = T::from(0.0001).unwrap(); let epsilon = T::from(0.0001).unwrap();
let one = T::one(); let one = T::one();
if x >= -(one + epsilon) && x <= one + epsilon { debug_assert!(x >= -(one + epsilon) && x <= one + epsilon);
clamp(x, -one, one).asin() clamp(x, -one, one).asin()
} else {
panic!("Not valid value for asin")
}
} }
#[inline] #[inline]
pub fn safe_acos(x: Float) -> Float { pub fn safe_acos(x: Float) -> Float {
if (-1.001..1.001).contains(&x) { debug_assert!((-1.001..1.001).contains(&x));
clamp(x, -1., 1.).acos() clamp(x, -1., 1.).acos()
} else {
panic!("Not valid value for acos")
}
} }
#[inline] #[inline]
@ -349,7 +346,93 @@ pub fn wrap_equal_area_square(uv: &mut Point2f) -> Point2f {
*uv *uv
} }
pub fn catmull_rom_weights(nodes: &[Float], x: Float) -> Option<(u32, [Float; 4])> { pub fn integrate_catmull_rom(nodes: &[Float], f: &[Float], cdf: &mut [Float]) -> Float {
debug_assert_eq!(nodes.len(), f.len());
debug_assert_eq!(cdf.len(), nodes.len());
let mut sum = 0.;
cdf[0] = 0.;
for i in 0..nodes.len() - 1 {
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
};
// Keep a running sum and build a cumulative distribution function
sum += width * ((f0 + f1) / 2. + (d0 - d1) / 12.);
cdf[i + 1] = sum;
}
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<(i32, [Float; 4])> {
if nodes.len() < 4 { if nodes.len() < 4 {
return None; return None;
} }
@ -367,7 +450,10 @@ pub fn catmull_rom_weights(nodes: &[Float], x: Float) -> Option<(u32, [Float; 4]
// Safety clamp (though bounds check above handles most cases) // Safety clamp (though bounds check above handles most cases)
let idx = idx.min(nodes.len() - 2); let idx = idx.min(nodes.len() - 2);
let offset = idx.saturating_sub(1); // The C++ code uses idx - 1 for the offset // pbrt: `*offset = idx - 1`, which is legitimately -1 when idx == 0. weights[0] is
// then 0, so callers never dereference the out-of-range slot -- but the remaining
// three weights must still line up with nodes[idx-1+i].
let offset = idx as i32 - 1;
let x0 = nodes[idx]; let x0 = nodes[idx];
let x1 = nodes[idx + 1]; let x1 = nodes[idx + 1];
@ -406,7 +492,7 @@ pub fn catmull_rom_weights(nodes: &[Float], x: Float) -> Option<(u32, [Float; 4]
weights[3] = 0.0; weights[3] = 0.0;
} }
Some((offset as u32, weights)) Some((offset, weights))
} }
pub fn equal_area_sphere_to_square(d: Vector3f) -> Point2f { pub fn equal_area_sphere_to_square(d: Vector3f) -> Point2f {
@ -461,7 +547,7 @@ pub fn equal_area_square_to_sphere(p: Point2f) -> Vector3f {
// Compute angle \phi for square to sphere mapping // Compute angle \phi for square to sphere mapping
let mut phi = if r == 0. { 1. } else { (vp - up) / r + 1. }; let mut phi = if r == 0. { 1. } else { (vp - up) / r + 1. };
phi /= PI_OVER_4; phi *= PI_OVER_4;
// Find z for spherical direction // Find z for spherical direction
let z = (1. - square(r)).copysign(signed_distance); let z = (1. - square(r)).copysign(signed_distance);
@ -500,12 +586,13 @@ pub fn gaussian_integral(x0: Float, x1: Float, mu: Float, sigma: Float) -> Float
0.5 * (erf((mu - x0) / sigma_root2) - erf((mu - x1) / sigma_root2)) 0.5 * (erf((mu - x0) / sigma_root2) - erf((mu - x1) / sigma_root2))
} }
#[inline(always)]
pub fn sample_linear(u: Float, a: Float, b: Float) -> Float { pub fn sample_linear(u: Float, a: Float, b: Float) -> Float {
assert!(a >= 0. && b >= 0.); assert!(a >= 0. && b >= 0.);
if u == 0. && a == 0. { if u == 0. && a == 0. {
return 0.; return 0.;
} }
let x = u * (a + b) / (a + (lerp(u, square(a), square(b)))); let x = u * (a + b) / (a + lerp(u, square(a), square(b)).sqrt());
x.min(ONE_MINUS_EPSILON) x.min(ONE_MINUS_EPSILON)
} }
@ -615,12 +702,13 @@ pub fn sample_discrete(
} }
pub fn sample_tent(u: Float, r: Float) -> Float { pub fn sample_tent(u: Float, r: Float) -> Float {
// pbrt rewrites `u` in place with the remapped sample and feeds *that* to SampleLinear.
let mut u_remapped = 0.0; let mut u_remapped = 0.0;
let offset = sample_discrete(&[0.5, 0.5], u, None, Some(&mut u_remapped)); let offset = sample_discrete(&[0.5, 0.5], u, None, Some(&mut u_remapped));
if offset == 0 { if offset == 0 {
-r + r * sample_linear(u, 0., 1.) -r + r * sample_linear(u_remapped, 0., 1.)
} else { } else {
r * sample_linear(u, 1., 0.) r * sample_linear(u_remapped, 1., 0.)
} }
} }
@ -650,7 +738,7 @@ pub fn encode_morton_2(x: u32, y: u32) -> u64 {
} }
pub fn encode_morton_3(x: Float, y: Float, z: Float) -> u32 { pub fn encode_morton_3(x: Float, y: Float, z: Float) -> u32 {
(left_shift3(x as u32) << 2) | (left_shift3(y as u32) << 1) | left_shift3(z as u32) (left_shift3(z as u32) << 2) | (left_shift3(y as u32) << 1) | left_shift3(x as u32)
} }
pub fn round_up_pow2(mut n: i32) -> i32 { pub fn round_up_pow2(mut n: i32) -> i32 {
@ -1505,7 +1593,7 @@ where
} }
let det: T = (0..N).map(|i| lum[i][i]).product(); let det: T = (0..N).map(|i| lum[i][i]).product();
if parity < 0 { -det } else { det } if parity % 2 == 1 { -det } else { det }
} }
} }
} }
@ -1638,7 +1726,9 @@ pub fn f16_to_f32_software(h: u16) -> f32 {
m <<= 1; m <<= 1;
e += 1; e += 1;
} }
(sign << 31) | ((112 - e) << 23) | ((m & 0x3FF) << 13) // half subnormals have exponent -14; after `e` normalising shifts the
// f32 biased exponent is (-14 - e) + 127 = 113 - e.
(sign << 31) | ((113 - e) << 23) | ((m & 0x3FF) << 13)
} }
} else if exp == 0x1F { } else if exp == 0x1F {
(sign << 31) | (0xFF << 23) | (mant << 13) (sign << 31) | (0xFF << 23) | (mant << 13)

View file

@ -52,7 +52,7 @@ pub fn invert_linear_sample(x: Float, a: Float, b: Float) -> Float {
pub fn sample_bilinear(u: Point2f, w: &[Float]) -> Point2f { pub fn sample_bilinear(u: Point2f, w: &[Float]) -> Point2f {
let y = sample_linear(u[1], w[0] + w[1], w[2] + w[3]); let y = sample_linear(u[1], w[0] + w[1], w[2] + w[3]);
let x = sample_linear(u[1], lerp(y, w[0], w[2]), lerp(y, w[1], w[3])); let x = sample_linear(u[0], lerp(y, w[0], w[2]), lerp(y, w[1], w[3]));
Point2f::new(x, y) Point2f::new(x, y)
} }
@ -333,7 +333,8 @@ pub fn invert_spherical_rectangle_sample(
} }
pub fn sample_exponential(u: Float, a: Float) -> Float { pub fn sample_exponential(u: Float, a: Float) -> Float {
(1. - u).ln() / a debug_assert!(a > 0.);
-(1. - u).ln() / a
} }
pub fn sample_spherical_triangle( pub fn sample_spherical_triangle(
@ -550,7 +551,9 @@ pub fn sample_catmull_rom_2d(
let mut v = 0.; let mut v = 0.;
for i in 0..4 { for i in 0..4 {
if weights[i] != 0. { if weights[i] != 0. {
let ind = (offset + i as u32) * n2 + idx; // `offset` is pbrt's `idx - 1` and may be -1; the matching weight is
// then zero, so the slot is never actually read.
let ind = (offset + i as i32) * n2 as i32 + idx as i32;
v += array[ind as usize] * weights[i]; v += array[ind as usize] * weights[i];
} }
} }
@ -658,10 +661,10 @@ pub struct VarianceEstimator {
impl VarianceEstimator { impl VarianceEstimator {
pub fn add(&mut self, x: Float) { pub fn add(&mut self, x: Float) {
self.n += 1; self.n += 1;
let delta = x / self.mean; let delta = x - self.mean;
self.mean += delta / self.n as Float; self.mean += delta / self.n as Float;
let delta2 = x - self.mean; let delta2 = x - self.mean;
self.s *= delta * delta2; self.s += delta * delta2;
} }
pub fn mean(&self) -> Float { pub fn mean(&self) -> Float {
@ -688,7 +691,7 @@ impl VarianceEstimator {
if ve.n != 0 { if ve.n != 0 {
self.s += ve.s self.s += ve.s
+ square(ve.mean - self.mean) * (self.n * ve.n) as Float / (self.n + ve.n) as Float; + square(ve.mean - self.mean) * (self.n * ve.n) as Float / (self.n + ve.n) as Float;
self.mean += self.mean =
(self.n as Float * self.mean + ve.n as Float * ve.mean) / (self.n + ve.n) as Float; (self.n as Float * self.mean + ve.n as Float * ve.mean) / (self.n + ve.n) as Float;
self.n += ve.n; self.n += ve.n;
} }
@ -719,24 +722,37 @@ impl PiecewiseConstant1D {
} }
pub fn new_with_bounds(f: &[Float], min: Float, max: Float) -> Self { pub fn new_with_bounds(f: &[Float], min: Float, max: Float) -> Self {
debug_assert!(max > min);
let n = f.len(); let n = f.len();
// pbrt takes the absolute value of the function before integrating, so filters
// with negative lobes (Mitchell, LanczosSinc) still give a usable CDF.
let mut func: GVec<Float> = gvec_with_capacity(n);
for &v in f {
func.push(v.abs());
}
let mut cdf = gvec_with_capacity(n + 1); let mut cdf = gvec_with_capacity(n + 1);
cdf.push(0.0); cdf.push(0.0);
let delta = (max - min) / n as Float; let delta = (max - min) / n as Float;
for i in 0..n { for i in 0..n {
cdf.push(cdf[i] + f[i] * delta); cdf.push(cdf[i] + func[i] * delta);
} }
let func_integral = cdf[n]; let func_integral = cdf[n];
if func_integral > 0.0 { if func_integral == 0.0 {
for c in &mut cdf { for (i, c) in cdf.iter_mut().enumerate().skip(1) {
*c = i as Float / n as Float;
}
} else {
for c in cdf.iter_mut().skip(1) {
*c /= func_integral; *c /= func_integral;
} }
} }
Self { Self {
func: gvec_from_slice(f), func,
cdf, cdf,
min, min,
max, max,
@ -748,7 +764,6 @@ impl PiecewiseConstant1D {
where where
F: Fn(Float) -> Float, F: Fn(Float) -> Float,
{ {
let delta = (max - min) / n as Float;
let delta = (max - min) / n as Float; let delta = (max - min) / n as Float;
let mut values = gvec_with_capacity(n); let mut values = gvec_with_capacity(n);
for i in 0..n { for i in 0..n {
@ -816,6 +831,7 @@ impl PiecewiseConstant1D {
pub struct PiecewiseConstant2D { pub struct PiecewiseConstant2D {
pub conditionals: GVec<PiecewiseConstant1D>, pub conditionals: GVec<PiecewiseConstant1D>,
pub marginal: PiecewiseConstant1D, pub marginal: PiecewiseConstant1D,
pub domain: Bounds2f,
pub n_u: u32, pub n_u: u32,
pub n_v: u32, pub n_v: u32,
} }
@ -852,6 +868,7 @@ impl PiecewiseConstant2D {
Self { Self {
conditionals, conditionals,
marginal, marginal,
domain,
n_u: n_u.try_into().unwrap(), n_u: n_u.try_into().unwrap(),
n_v: n_v.try_into().unwrap(), n_v: n_v.try_into().unwrap(),
} }
@ -876,7 +893,10 @@ impl PiecewiseConstant2D {
self.marginal.integral() self.marginal.integral()
} }
pub fn pdf(&self, p: Point2f) -> Float { pub fn pdf(&self, pr: Point2f) -> Float {
// pbrt maps the query point into the unit domain first; the distribution's
// domain is only [0,1]^2 for image distributions, not e.g. filter samplers.
let p = Point2f::from(self.domain.offset(&pr));
let u_offset = ((p.x() * self.n_u as Float) as usize).min(self.n_u as usize - 1); let u_offset = ((p.x() * self.n_u as Float) as usize).min(self.n_u as usize - 1);
let v_offset = ((p.y() * self.n_v as Float) as usize).min(self.n_v as usize - 1); let v_offset = ((p.y() * self.n_v as Float) as usize).min(self.n_v as usize - 1);
let conditional = unsafe { &*self.conditionals.as_ptr().add(v_offset) }; let conditional = unsafe { &*self.conditionals.as_ptr().add(v_offset) };

View file

@ -282,16 +282,17 @@ impl CreateFilmBase for FilmBase {
(full_resolution.y() as Float * crop.p_max.y()).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); let pixel_bounds = Bounds2i::from_points(p_min, p_max);
if pixel_bounds.is_empty() { if pixel_bounds.is_empty() {
eprintln!("{}: Film crop window results in empty pixel bounds.", loc); eprintln!("{}: Film crop window results in empty pixel bounds.", loc);
} }
let rad = filter.radius(); // NOTE: pbrt does NOT expand pixelBounds by the filter radius (film.cpp:97,
let expansion = Point2i::new(rad.x().ceil() as i32, rad.y().ceil() as i32); // `pixelBounds = Bounds2i(Point2i(0, 0), fullResolution)`, then only intersected
pixel_bounds = pixel_bounds.expand(expansion); // with "pixelbounds"/"cropwindow"). The filter radius widens SampleBounds(),
// never the film's stored pixel array. Expanding here made the film 1372x1030
// instead of 1368x1026 and put the pixel origin at (-2,-2).
let diagonal_mm = params.get_one_float("diagonal", 35.0)?; let diagonal_mm = params.get_one_float("diagonal", 35.0)?;
// let filename = params.get_one_string("filename", "pbrt.exr"); // let filename = params.get_one_string("filename", "pbrt.exr");

View file

@ -81,16 +81,15 @@ impl MaterialFactory for Material {
SubsurfaceMaterial::create(parameters, normal_map, named_materials, &loc, arena) SubsurfaceMaterial::create(parameters, normal_map, named_materials, &loc, arena)
} }
"mix" => MixMaterial::create(parameters, normal_map, named_materials, &loc, arena), "mix" => MixMaterial::create(parameters, normal_map, named_materials, &loc, arena),
_ => Err(anyhow!("Material type '{}' unknown at {}", name, &loc)), _ => Err(anyhow!("Material type '{}' unknown at {}", name, &loc)),
} }
} }
} }
pub fn default_diffuse_material(arena: &Arena) -> Material { pub fn default_diffuse_material(arena: &Arena) -> Material {
use shared::core::texture::SpectrumTexture;
use shared::core::texture::SpectrumConstantTexture;
use shared::core::spectrum::{ConstantSpectrum, Spectrum}; use shared::core::spectrum::{ConstantSpectrum, Spectrum};
use shared::core::texture::SpectrumConstantTexture;
use shared::core::texture::SpectrumTexture;
use shared::materials::DiffuseMaterial; use shared::materials::DiffuseMaterial;
use shared::utils::Ptr; use shared::utils::Ptr;

View file

@ -1,7 +1,7 @@
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::spectra::SRGB; use crate::spectra::SRGB;
use crate::{Arena, FileLoc, ParameterDictionary}; use crate::{Arena, FileLoc, ParameterDictionary};
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use shared::core::geometry::{Bounds3f, Point3f, Point3i}; use shared::core::geometry::{Bounds3f, Point3f, Point3i};
use shared::core::medium::{ use shared::core::medium::{
GridMedium, HGPhaseFunction, HomogeneousMedium, MajorantGrid, Medium, RGBGridMedium, GridMedium, HGPhaseFunction, HomogeneousMedium, MajorantGrid, Medium, RGBGridMedium,
@ -262,7 +262,7 @@ static SUBSURFACE_TABLE: &[MeasuredSS] = &[
}, },
]; ];
fn get_medium_scattering_properties(name: &str) -> Option<(Spectrum, Spectrum)> { pub fn get_medium_scattering_properties(name: &str) -> Option<(Spectrum, Spectrum)> {
SUBSURFACE_TABLE.iter().find(|m| m.name == name).map(|m| { SUBSURFACE_TABLE.iter().find(|m| m.name == name).map(|m| {
let sigma_a = Spectrum::RGBUnbounded(RGBUnboundedSpectrum::new(&SRGB, m.sigma_a.into())); let sigma_a = Spectrum::RGBUnbounded(RGBUnboundedSpectrum::new(&SRGB, m.sigma_a.into()));
let sigma_s = let sigma_s =

View file

@ -7,10 +7,11 @@ use shared::Float;
use shared::core::color::ColorEncoding; use shared::core::color::ColorEncoding;
use shared::core::geometry::Vector3f; use shared::core::geometry::Vector3f;
use shared::core::image::WrapMode; use shared::core::image::WrapMode;
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType; use shared::core::texture::SpectrumType;
use shared::core::texture::{ use shared::core::texture::{
CylindricalMapping, PlanarMapping, PointTransformMapping, SphericalMapping, CylindricalMapping, PlanarMapping, PointTransformMapping, SphericalMapping, TextureEvalContext,
TextureEvalContext, TextureMapping2D, TextureMapping3D, UVMapping, TextureMapping2D, TextureMapping3D, UVMapping,
}; };
use shared::spectra::{SampledSpectrum, SampledWavelengths}; use shared::spectra::{SampledSpectrum, SampledWavelengths};
use shared::textures::{ use shared::textures::{
@ -95,6 +96,21 @@ pub enum SpectrumTexture {
DirectionMix(SpectrumDirectionMixTexture), 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<Spectrum> for SpectrumTexture {
fn from(s: Spectrum) -> Self {
SpectrumTexture::Constant(SpectrumConstantTexture::new(s))
}
}
impl From<Float> for FloatTexture {
fn from(v: Float) -> Self {
FloatTexture::Constant(FloatConstantTexture::new(v))
}
}
pub trait CreateSpectrumTexture { pub trait CreateSpectrumTexture {
fn create( fn create(
render_from_texture: Transform, render_from_texture: Transform,
@ -115,26 +131,53 @@ impl SpectrumTexture {
arena: &Arena, arena: &Arena,
) -> Result<Self> { ) -> Result<Self> {
match name { match name {
"constant" => { "constant" => SpectrumConstantTexture::create(
SpectrumConstantTexture::create(render_from_texture, params, spectrum_type, loc, arena) render_from_texture,
} params,
"scale" => { spectrum_type,
SpectrumScaledTexture::create(render_from_texture, params, spectrum_type, loc, arena) loc,
} arena,
"mix" => SpectrumMixTexture::create(render_from_texture, params, spectrum_type, loc, arena), ),
"directionmix" => { "scale" => SpectrumScaledTexture::create(
SpectrumDirectionMixTexture::create(render_from_texture, params, spectrum_type, loc, arena) render_from_texture,
} params,
"bilerp" => { spectrum_type,
SpectrumBilerpTexture::create(render_from_texture, params, spectrum_type, loc, arena) loc,
arena,
),
"mix" => {
SpectrumMixTexture::create(render_from_texture, params, spectrum_type, loc, arena)
} }
"directionmix" => SpectrumDirectionMixTexture::create(
render_from_texture,
params,
spectrum_type,
loc,
arena,
),
"bilerp" => SpectrumBilerpTexture::create(
render_from_texture,
params,
spectrum_type,
loc,
arena,
),
"imagemap" => { "imagemap" => {
SpectrumImageTexture::create(render_from_texture, params, spectrum_type, loc, arena) SpectrumImageTexture::create(render_from_texture, params, spectrum_type, loc, arena)
} }
"checkerboard" => { "checkerboard" => SpectrumCheckerboardTexture::create(
SpectrumCheckerboardTexture::create(render_from_texture, params, spectrum_type, loc, arena) render_from_texture,
params,
spectrum_type,
loc,
arena,
),
"dots" => {
SpectrumDotsTexture::create(render_from_texture, params, spectrum_type, loc, arena)
}
"marble" => {
MarbleTexture::create(render_from_texture, params, spectrum_type, loc, arena)
} }
"dots" => SpectrumDotsTexture::create(render_from_texture, params, spectrum_type, loc, arena),
_ => Err(anyhow!( _ => Err(anyhow!(
"Spectrum texture type '{}' unknown at {}", "Spectrum texture type '{}' unknown at {}",
name, name,
@ -209,7 +252,6 @@ impl CreateTextureMapping for TextureMapping3D {
let mapping = PointTransformMapping::new(render_from_texture.inverse()); let mapping = PointTransformMapping::new(render_from_texture.inverse());
Ok(TextureMapping3D::PointTransform(mapping)) Ok(TextureMapping3D::PointTransform(mapping))
} }
} }
pub static TEXTURE_CACHE: OnceLock<Mutex<HashMap<TexInfo, Arc<MIPMap>>>> = OnceLock::new(); pub static TEXTURE_CACHE: OnceLock<Mutex<HashMap<TexInfo, Arc<MIPMap>>>> = OnceLock::new();

View file

@ -184,7 +184,7 @@ fn create_portal_light(
// Build distribution // Build distribution
let duv_dw = |p: Point2f| -> Float { let duv_dw = |p: Point2f| -> Float {
let (_, jacobian) = PortalInfiniteLight::render_from_image(portal_frame, p); let (_, jacobian) = PortalInfiniteLight::render_from_image_with(portal_frame, p);
jacobian jacobian
}; };
let d = remapped.get_sampling_distribution( let d = remapped.get_sampling_distribution(
@ -247,7 +247,7 @@ fn remap_image_through_portal(
(y as Float + 0.5) / height as Float, (y as Float + 0.5) / height as Float,
); );
let (w_world, _) = PortalInfiniteLight::render_from_image(*portal_frame, uv); let (w_world, _) = PortalInfiniteLight::render_from_image_with(*portal_frame, uv);
let w_local = render_from_light.apply_inverse_vector(w_world).normalize(); let w_local = render_from_light.apply_inverse_vector(w_world).normalize();
let uv_equi = equal_area_sphere_to_square(w_local); let uv_equi = equal_area_sphere_to_square(w_local);

View file

@ -4,8 +4,8 @@ use crate::core::texture::SpectrumTexture;
use crate::globals::get_options; use crate::globals::get_options;
use crate::spectra::data::get_named_spectrum; use crate::spectra::data::get_named_spectrum;
use crate::utils::TextureParameterDictionary; use crate::utils::TextureParameterDictionary;
use crate::{Arena, FileLoc, ArenaUpload}; use crate::{Arena, ArenaUpload, FileLoc};
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use shared::core::material::Material; use shared::core::material::Material;
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType; use shared::core::texture::SpectrumType;
@ -24,10 +24,7 @@ impl CreateMaterial for CoatedDiffuseMaterial {
arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
let reflectance = parameters let reflectance = parameters
.get_spectrum_texture("reflectance", None, SpectrumType::Albedo) .get_spectrum_texture("reflectance", 0.5, SpectrumType::Albedo);
.unwrap_or(Arc::new(SpectrumTexture::Constant(
SpectrumConstantTexture::new(Spectrum::Constant(ConstantSpectrum::new(0.5))),
)));
let u_roughness = let u_roughness =
parameters.get_float_texture_with_fallback("uroughness", "roughness", 0.5)?; parameters.get_float_texture_with_fallback("uroughness", "roughness", 0.5)?;
@ -36,22 +33,19 @@ impl CreateMaterial for CoatedDiffuseMaterial {
parameters.get_float_texture_with_fallback("vroughness", "roughness", 0.5)?; parameters.get_float_texture_with_fallback("vroughness", "roughness", 0.5)?;
let thickness = parameters.get_float_texture("thickness", 0.01)?; let thickness = parameters.get_float_texture("thickness", 0.01)?;
let eta = parameters let eta = if let Some(&v) = parameters.get_float_array("eta")?.first() {
.get_float_array("eta")? Spectrum::from(v)
.first() } else {
.map(|&v| Spectrum::Constant(ConstantSpectrum::new(v))) parameters
.or_else(|| parameters.get_one_spectrum("eta", None, SpectrumType::Unbounded)) .get_one_spectrum("eta", None, SpectrumType::Unbounded)
.unwrap_or_else(|| Spectrum::Constant(ConstantSpectrum::new(1.5))); .unwrap_or_else(|| Spectrum::from(1.5))
};
let max_depth = parameters.get_one_int("maxdepth", 10)?; let max_depth = parameters.get_one_int("maxdepth", 10)?;
let n_samples = parameters.get_one_int("nsamples", 1)?; let n_samples = parameters.get_one_int("nsamples", 1)?;
let g = parameters.get_float_texture("g", 0.)?; let g = parameters.get_float_texture("g", 0.)?;
let albedo = parameters let albedo = parameters
.get_spectrum_texture("albedo", None, SpectrumType::Albedo) .get_spectrum_texture("albedo", 0., SpectrumType::Albedo);
.unwrap_or_else(|| {
let default_spectrum = Spectrum::Constant(ConstantSpectrum::new(0.));
SpectrumTexture::Constant(SpectrumConstantTexture::new(default_spectrum)).into()
});
let displacement = parameters.get_float_texture("displacement", 0.)?; let displacement = parameters.get_float_texture("displacement", 0.)?;
let remap_roughness = parameters.get_one_bool("remaproughness", true)?; let remap_roughness = parameters.get_one_bool("remaproughness", true)?;
@ -145,11 +139,7 @@ impl CreateMaterial for CoatedConductorMaterial {
let n_samples = parameters.get_one_int("nsamples", 1)?; let n_samples = parameters.get_one_int("nsamples", 1)?;
let g = parameters.get_float_texture("g", 0.)?; let g = parameters.get_float_texture("g", 0.)?;
let albedo = parameters let albedo = parameters
.get_spectrum_texture("albedo", None, SpectrumType::Albedo) .get_spectrum_texture("albedo", 0., SpectrumType::Albedo);
.unwrap_or_else(|| {
let spectrum = Spectrum::Constant(ConstantSpectrum::new(0.));
SpectrumTexture::Constant(SpectrumConstantTexture::new(spectrum)).into()
});
let displacement = parameters.get_float_texture_or_null("displacement")?; let displacement = parameters.get_float_texture_or_null("displacement")?;
let remap_roughness = parameters.get_one_bool("remaproughness", true)?; let remap_roughness = parameters.get_one_bool("remaproughness", true)?;

View file

@ -1,16 +1,29 @@
use crate::core::image::HostImage; use crate::core::image::HostImage;
use crate::core::material::CreateMaterial; use crate::core::material::CreateMaterial;
use crate::core::medium::get_medium_scattering_properties;
use crate::core::texture::SpectrumTexture; use crate::core::texture::SpectrumTexture;
use crate::spectra::get_colorspace_device; use crate::spectra::get_colorspace_device;
use crate::utils::TextureParameterDictionary; use crate::utils::{TextureParameterDictionary, resolve_filename};
use crate::{Arena, ArenaUpload, FileLoc}; use crate::{Arena, ArenaUpload, FileLoc};
use anyhow::Result; use anyhow::{Result, anyhow, bail};
use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator};
use rayon::prelude::ParallelSliceMut;
use shared::bxdfs::HairBxDF; use shared::bxdfs::HairBxDF;
use shared::core::bssrdf::BSSRDFTable;
use shared::core::color::RGB;
use shared::core::material::Material; use shared::core::material::Material;
use shared::core::scattering::{
fr_dielectric, fresnel_moment1, fresnel_moment2, henyey_greenstein,
};
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType; use shared::core::texture::SpectrumType;
use shared::materials::complex::*; use shared::materials::complex::*;
use shared::spectra::{ConstantSpectrum, RGBUnboundedSpectrum};
use shared::textures::SpectrumConstantTexture; use shared::textures::SpectrumConstantTexture;
use shared::utils::math::{fast_exp, integrate_catmull_rom, safe_sqrt, square};
use shared::utils::sampling::sample_exponential;
use shared::{Float, INV_4_PI, PI};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@ -29,57 +42,284 @@ impl CreateMaterial for HairMaterial {
let eumelanin = parameters.get_float_texture_or_null("eumelanin")?; let eumelanin = parameters.get_float_texture_or_null("eumelanin")?;
let pheomelanin = parameters.get_float_texture_or_null("pheomelanin")?; let pheomelanin = parameters.get_float_texture_or_null("pheomelanin")?;
let has_melanin = eumelanin.is_some() || pheomelanin.is_some(); let has_melanin = eumelanin.is_some() || pheomelanin.is_some();
let sigma_a = if sigma_a.is_none() && reflectance.is_none() && !has_melanin {
let stdcs = get_colorspace_device();
let default_rgb = HairBxDF::sigma_a_from_concentration(1.3, 0.0, stdcs);
let spectrum = Spectrum::RGBUnbounded(default_rgb);
let texture = SpectrumTexture::Constant(SpectrumConstantTexture::new(spectrum));
Some(Arc::new(texture))
} else {
sigma_a
};
let eta = parameters.get_float_texture("eta", 1.55)?; let eta = parameters.get_float_texture("eta", 1.55)?;
let beta_m = parameters.get_float_texture("beta_m", 0.3)?; let beta_m = parameters.get_float_texture("beta_m", 0.3)?;
let beta_n = parameters.get_float_texture("beta_n", 0.3)?; let beta_n = parameters.get_float_texture("beta_n", 0.3)?;
let alpha = parameters.get_float_texture("alpha", 2.)?; let alpha = parameters.get_float_texture("alpha", 2.)?;
let stdcs = get_colorspace_device();
let absorption = if let Some(s) = sigma_a {
HairAbsorption::SigmaA(arena.upload(s))
} else if let Some(r) = reflectance {
HairAbsorption::Color(arena.upload(r))
} else if has_melanin {
HairAbsorption::Melanin {
eumelanin: arena.upload(eumelanin),
pheomelanin: arena.upload(pheomelanin),
}
} else {
let default_rgb = HairBxDF::sigma_a_from_concentration(1.3, 0.0, stdcs.srgb);
let spectrum = Spectrum::RGBUnbounded(default_rgb);
let texture = SpectrumTexture::Constant(SpectrumConstantTexture::new(spectrum));
HairAbsorption::SigmaA(arena.upload(Arc::new(texture)))
};
let material = HairMaterial::new( let material = HairMaterial::new(
arena.upload(sigma_a), absorption,
arena.upload(reflectance),
arena.upload(eumelanin),
arena.upload(pheomelanin),
arena.upload(eta), arena.upload(eta),
arena.upload(beta_m), arena.upload(beta_m),
arena.upload(beta_n), arena.upload(beta_n),
arena.upload(alpha), arena.upload(alpha),
stdcs.srgb,
); );
Ok(Material::Hair(material)) Ok(Material::Hair(material))
} }
} }
fn cube(x: Float) -> Float {
x * x * x
}
fn beam_diffusion_ms(sigma_s: Float, sigma_a: Float, g: Float, eta: Float, r: Float) -> Float {
const N_SAMPLES: usize = 100;
let mut e_d = 0.;
// Precompute information for dipole integrand
// Compute reduced scattering coefficients $\sigmaps, \sigmapt$ and albedo $\rhop$
let sigmap_s = sigma_s * (1. - g);
let sigmap_t = sigma_a + sigmap_s;
let rhop = sigmap_s / sigmap_t;
// Compute non-classical diffusion coefficient $D_\roman{G}$ using Equation
// $(\ref{eq:diffusion-coefficient-grosjean})$
let d_g = (2. * sigma_a + sigmap_s) / (3. * sigmap_t * sigmap_t);
// Compute effective transport coefficient $\sigmatr$ based on $D_\roman{G}$
let sigma_tr = safe_sqrt(sigma_a / d_g);
// Determine linear extrapolation distance $\depthextrapolation$ using Equation
// $(\ref{eq:dipole-boundary-condition})$
let fm1 = fresnel_moment1(eta);
let fm2 = fresnel_moment2(eta);
let ze = -2. * d_g * (1. + 3. * fm2) / (1. - 2. * fm1);
// Determine exitance scale factors using Equations $(\ref{eq:kp-exitance-phi})$ and
// $(\ref{eq:kp-exitance-e})$
let c_phi = 0.25 * (1. - 2. * fm1);
let c_e = 0.5 * (1. - 3. * fm2);
for i in 0..N_SAMPLES {
// Sample real point source depth $\depthreal$
let zr = sample_exponential((i as Float + 0.5) / N_SAMPLES as Float, sigmap_t);
// Evaluate dipole integrand $E_{\roman{d}}$ at $\depthreal$ and add to _Ed_
let zv = -zr + 2. * ze;
let dr = (square(r) + square(zr)).sqrt();
let dv = (square(r) + square(zv)).sqrt();
// Compute dipole fluence rate $\dipole(r)$ using Equation
// $(\ref{eq:diffusion-dipole})$
let phi_d =
INV_4_PI / d_g * (fast_exp(-sigma_tr * dr) / dr - fast_exp(-sigma_tr * dv) / dv);
// Compute dipole vector irradiance $-\N{}\cdot\dipoleE(r)$ using Equation
// $(\ref{eq:diffusion-dipole-vector-irradiance-normal})$
let e_dn = INV_4_PI
* (zr * (1. + sigma_tr * dr) * fast_exp(-sigma_tr * dr) / cube(dr)
- zv * (1. + sigma_tr * dv) * fast_exp(-sigma_tr * dv) / cube(dv));
// Add contribution from dipole for depth $\depthreal$ to _Ed_
let e = phi_d * c_phi + e_dn * c_e;
let kappa = 1. - fast_exp(-2. * sigmap_t * (dr + zr));
e_d += kappa * rhop * rhop * e;
}
return e_d / N_SAMPLES as Float;
}
fn beam_diffusion_ss(sigma_s: Float, sigma_a: Float, g: Float, eta: Float, r: Float) -> Float {
// Compute material parameters and minimum $t$ below the critical angle
let sigma_t = sigma_a + sigma_s;
let rho = sigma_s / sigma_t;
let t_crit = r * safe_sqrt(square(eta) - 1.);
let mut ess = 0.0;
const N_SAMPLES: usize = 100;
for i in 0..N_SAMPLES {
// Evaluate single-scattering integrand and add to _Ess_
let ti = t_crit + sample_exponential((i as Float + 0.5) / N_SAMPLES as Float, sigma_t);
// Determine length $d$ of connecting segment and $\cos\theta_\roman{o}$
let d = (square(r) + square(ti)).sqrt();
let cos_theta_o = ti / d;
// Add contribution of single scattering at depth $t$
ess += rho * fast_exp(-sigma_t * (d + t_crit)) / square(d)
* henyey_greenstein(cos_theta_o, g)
* (1. - fr_dielectric(-cos_theta_o, eta))
* cos_theta_o.abs();
}
return ess / N_SAMPLES as Float;
}
fn compute_beam_diffusion_bssrdf(g: Float, eta: Float, t: &mut BSSRDFTable) {
let n_rho = t.rho_samples.len();
let n_radius = t.radius_samples.len();
t.radius_samples[0] = 0.;
t.radius_samples[1] = 2.5e-3;
for i in 2..n_radius {
t.radius_samples[i] = t.radius_samples[i - 1] * 1.2;
}
for i in 0..n_rho {
t.rho_samples[i] =
(1. - fast_exp(-8. * i as Float / (n_rho - 1) as Float)) / (1. - fast_exp(-8.));
}
let rho_samples = &t.rho_samples;
let radius_samples = &t.radius_samples;
t.profile
.par_chunks_mut(n_radius)
.zip(t.profile_cdf.par_chunks_mut(n_radius))
.zip(t.rho_eff.par_iter_mut())
.enumerate()
.for_each(|(i, ((profile, cdf), rho_eff))| {
// Compute the diffusion profile for the _i_th albedo sample
// Compute scattering profile for chosen albedo $\rho$
let rho = rho_samples[i];
for j in 0..n_radius {
let r = radius_samples[j];
profile[j] = 2.
* PI
* r
* (beam_diffusion_ss(rho, 1. - rho, g, eta, r)
+ beam_diffusion_ms(rho, 1. - rho, g, eta, r));
}
*rho_eff = integrate_catmull_rom(radius_samples, profile, cdf);
});
}
impl CreateMaterial for SubsurfaceMaterial { impl CreateMaterial for SubsurfaceMaterial {
fn create( fn create(
_parameters: &TextureParameterDictionary, parameters: &TextureParameterDictionary,
_normal_map: Option<Arc<HostImage>>, normal_map: Option<Arc<HostImage>>,
_named_materials: &HashMap<String, Material>, _named_materials: &HashMap<String, Material>,
_loc: &FileLoc, loc: &FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
todo!() let mut g = parameters.get_one_float("g", 0.0)?;
let name = parameters.get_one_string("name", "")?;
let scattering = if !name.is_empty() {
let (sig_a, sig_s) = get_medium_scattering_properties(&name)
.ok_or_else(|| anyhow!("{loc}: named medium {name} not found"))?;
if g != 0. {
log::warn!("{loc}: non-zero \"g\" ignored with named scattering coefficients");
}
g = 0.;
let sigma_a = SpectrumTexture::Constant(SpectrumConstantTexture::new(sig_a));
let sigma_s = SpectrumTexture::Constant(SpectrumConstantTexture::new(sig_s));
SubsurfaceScattering::Coefficients {
sigma_a: arena.upload(&sigma_a),
sigma_s: arena.upload(&sigma_s),
}
} else {
let sigma_a =
parameters.get_spectrum_texture_or_null("sigma_a", SpectrumType::Unbounded);
let sigma_s =
parameters.get_spectrum_texture_or_null("sigma_s", SpectrumType::Unbounded);
match (sigma_a, sigma_s) {
(Some(a), Some(b)) => SubsurfaceScattering::Coefficients {
sigma_a: arena.upload(a),
sigma_s: arena.upload(b),
},
(Some(_), None) => bail!("{loc}: provided \"sigma_a\" without \"sigma_s\""),
(None, Some(_)) => bail!("{loc}: provided \"sigma_s\" without \"sigma_a\""),
(None, None) => match parameters
.get_spectrum_texture_or_null("reflectance", SpectrumType::Albedo)
{
Some(r) => {
let one = Spectrum::Constant(ConstantSpectrum::new(1.));
let mfp = parameters
.get_spectrum_texture("mfp", one, SpectrumType::Unbounded);
SubsurfaceScattering::Reflectance {
reflectance: arena.upload(r),
mfp: arena.upload(mfp),
}
}
None => {
let stdcs = get_colorspace_device();
let default_sigma_a =
RGBUnboundedSpectrum::new(&stdcs.srgb, RGB::new(0.0011, 0.0024, 0.014));
let default_sigma_s =
RGBUnboundedSpectrum::new(&stdcs.srgb, RGB::new(2.55, 3.21, 3.77));
let sigma_a = SpectrumTexture::Constant(SpectrumConstantTexture::new(
Spectrum::RGBUnbounded(default_sigma_a),
));
let sigma_s = SpectrumTexture::Constant(SpectrumConstantTexture::new(
Spectrum::RGBUnbounded(default_sigma_s),
));
SubsurfaceScattering::Coefficients {
sigma_a: arena.upload(&sigma_a),
sigma_s: arena.upload(&sigma_s),
}
}
},
}
};
let scale = parameters.get_one_float("scale", 1.)?;
let eta = parameters.get_one_float("eta", 1.33)?;
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 mut table = BSSRDFTable::new(100, 64);
compute_beam_diffusion_bssrdf(g, eta, &mut table);
let mut ss_material = SubsurfaceMaterial {
scattering,
displacement: arena.upload(displacement),
normal_map: arena.upload(normal_map),
scale,
u_roughness: arena.upload(u_roughness),
v_roughness: arena.upload(v_roughness),
eta,
remap_roughness,
table: arena.alloc(table),
};
Ok(Material::Subsurface(ss_material))
} }
} }
// fn brdf_data_from_filename(filename: String) -> Ptr<MeasuredBxDFData> {
// static std::map<std::string, MeasuredBxDFData *> loadedData;
// if (loadedData.find(filename) == loadedData.end())
// loadedData[filename] = MeasuredBxDFData::Create(filename, alloc);
// return loadedData[filename];
//
// }
impl CreateMaterial for MeasuredMaterial { impl CreateMaterial for MeasuredMaterial {
fn create( fn create(
_parameters: &TextureParameterDictionary, parameters: &TextureParameterDictionary,
_normal_map: Option<Arc<HostImage>>, normal_map: Option<Arc<HostImage>>,
_named_materials: &HashMap<String, Material>, _named_materials: &HashMap<String, Material>,
_loc: &FileLoc, loc: &FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
// 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!() todo!()
} }
} }

View file

@ -1,33 +1,75 @@
use crate::Arena;
use crate::core::image::HostImage; use crate::core::image::HostImage;
use crate::core::material::CreateMaterial; use crate::core::material::CreateMaterial;
use crate::utils::{FileLoc, TextureParameterDictionary}; use crate::utils::{FileLoc, TextureParameterDictionary};
use crate::{Arena, ArenaUpload};
use anyhow::Result; use anyhow::Result;
use shared::core::material::Material; use shared::core::material::Material;
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType;
use shared::materials::{DielectricMaterial, ThinDielectricMaterial}; use shared::materials::{DielectricMaterial, ThinDielectricMaterial};
use shared::spectra::ConstantSpectrum;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
impl CreateMaterial for DielectricMaterial { impl CreateMaterial for DielectricMaterial {
fn create( fn create(
_parameters: &TextureParameterDictionary, parameters: &TextureParameterDictionary,
_normal_map: Option<Arc<HostImage>>, normal_map: Option<Arc<HostImage>>,
_named_materials: &HashMap<String, Material>, _named_materials: &HashMap<String, Material>,
_loc: &FileLoc, loc: &FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
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 { impl CreateMaterial for ThinDielectricMaterial {
fn create( fn create(
_parameters: &TextureParameterDictionary, parameters: &TextureParameterDictionary,
_normal_map: Option<Arc<HostImage>>, normal_map: Option<Arc<HostImage>>,
_named_materials: &HashMap<String, Material>, _named_materials: &HashMap<String, Material>,
_loc: &FileLoc, _loc: &FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
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))
} }
} }

View file

@ -1,9 +1,9 @@
use crate::Arena;
use crate::core::image::HostImage; use crate::core::image::HostImage;
use crate::core::material::CreateMaterial; use crate::core::material::CreateMaterial;
use crate::core::texture::SpectrumTexture; use crate::core::texture::SpectrumTexture;
use crate::utils::upload::ArenaUpload; use crate::utils::upload::ArenaUpload;
use crate::utils::{FileLoc, TextureParameterDictionary}; use crate::utils::{FileLoc, TextureParameterDictionary};
use crate::Arena;
use anyhow::Result; use anyhow::Result;
use shared::core::material::Material; use shared::core::material::Material;
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
@ -22,13 +22,7 @@ impl CreateMaterial for DiffuseMaterial {
_loc: &FileLoc, _loc: &FileLoc,
arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
let reflectance = parameters let reflectance = parameters.get_spectrum_texture("reflectance", 0.5, SpectrumType::Albedo);
.get_spectrum_texture("reflectance", None, SpectrumType::Albedo)
.unwrap_or_else(|| {
Arc::new(SpectrumTexture::Constant(
SpectrumConstantTexture::new(Spectrum::Constant(ConstantSpectrum::new(0.5))),
))
});
let displacement = parameters.get_float_texture_or_null("displacement")?; let displacement = parameters.get_float_texture_or_null("displacement")?;
let specific = DiffuseMaterial { let specific = DiffuseMaterial {
@ -42,12 +36,26 @@ impl CreateMaterial for DiffuseMaterial {
impl CreateMaterial for DiffuseTransmissionMaterial { impl CreateMaterial for DiffuseTransmissionMaterial {
fn create( fn create(
_parameters: &TextureParameterDictionary, parameters: &TextureParameterDictionary,
_normal_map: Option<Arc<HostImage>>, normal_map: Option<Arc<HostImage>>,
_named_materials: &HashMap<String, Material>, _named_materials: &HashMap<String, Material>,
_loc: &FileLoc, _loc: &FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
todo!() let reflectance =
parameters.get_spectrum_texture("reflectance", 0.25, SpectrumType::Albedo);
let transmittance =
parameters.get_spectrum_texture("transmittance", 0.25, SpectrumType::Albedo);
let displacement = parameters.get_float_texture_or_null("displacement")?;
let scale = parameters.get_one_float("scale", 1.)?;
let mat = DiffuseTransmissionMaterial {
normal_map: arena.upload(normal_map),
displacement: arena.upload(displacement),
reflectance: arena.upload(reflectance),
transmittance: arena.upload(transmittance),
scale,
};
Ok(Material::DiffuseTransmission(mat))
} }
} }

View file

@ -1,7 +1,9 @@
use crate::core::image::HostImage; use crate::core::image::HostImage;
use crate::core::material::CreateMaterial; use crate::core::material::CreateMaterial;
use crate::utils::{Arena, FileLoc, TextureParameterDictionary}; use crate::{Arena, ArenaUpload, FileLoc, utils::TextureParameterDictionary};
use anyhow::Result; use anyhow::Result;
use anyhow::anyhow;
use shared::Ptr;
use shared::core::material::Material; use shared::core::material::Material;
use shared::materials::MixMaterial; use shared::materials::MixMaterial;
use std::collections::HashMap; use std::collections::HashMap;
@ -9,12 +11,31 @@ use std::sync::Arc;
impl CreateMaterial for MixMaterial { impl CreateMaterial for MixMaterial {
fn create( fn create(
_parameters: &TextureParameterDictionary, parameters: &TextureParameterDictionary,
_normal_map: Option<Arc<HostImage>>, _normal_map: Option<Arc<HostImage>>,
_named_materials: &HashMap<String, Material>, named_materials: &HashMap<String, Material>,
_loc: &FileLoc, loc: &FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<Material> { ) -> Result<Material> {
todo!() let names = parameters.get_string_array("materials")?;
let [a, b]: [String; 2] = names
.try_into()
.map_err(|_| anyhow!("{loc}: must provide two values for \"string materials\""))?;
let mut resolve = |n: &str| -> Result<Ptr<Material>> {
let m = named_materials
.get(n)
.ok_or_else(|| anyhow!("{loc}: {n}: named material not found"))?;
Ok(arena.alloc(*m))
};
let materials = [resolve(&a)?, resolve(&b)?];
let amount = parameters.get_float_texture("amount", 0.5)?;
let mix_mat = MixMaterial {
materials,
amount: arena.upload(amount),
};
Ok(Material::Mix(mix_mat))
} }
} }

View file

@ -6,9 +6,11 @@ use std::collections::HashMap;
use std::sync::LazyLock; use std::sync::LazyLock;
pub fn create_cie(data: &[Float]) -> DenselySampledSpectrum { pub fn create_cie(data: &[Float]) -> DenselySampledSpectrum {
// The CIE X/Y/Z curves are tabulated at 1nm over [360, 830]. (A 95-entry arm used to map
// the hand-normalized CIE_D65 table onto 300nm/5nm, but that table actually starts at
// 360nm; D65 now goes through PiecewiseLinearSpectrum::from_interleaved like pbrt.)
let (start_lambda, step) = match data.len() { let (start_lambda, step) = match data.len() {
471 => (360.0, 1.0), 471 => (360.0, 1.0),
95 => (300.0, 5.0),
n => panic!("Unexpected CIE data length: {}", n), n => panic!("Unexpected CIE data length: {}", n),
}; };
let lambdas: Vec<Float> = (0..data.len()) let lambdas: Vec<Float> = (0..data.len())

View file

@ -3,8 +3,10 @@ use crate::spectra::colorspace::CreateRGBColorSpace;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use shared::core::geometry::Point2f; use shared::core::geometry::Point2f;
use shared::core::spectrum::{Spectrum, StandardSpectra}; use shared::core::spectrum::{Spectrum, StandardSpectra};
use shared::spectra::cie::{CIE_D65, CIE_X, CIE_Y, CIE_Z}; use shared::spectra::cie::{CIE_ILLUM_D6500, CIE_X, CIE_Y, CIE_Z};
use shared::spectra::{DenselySampledSpectrum, DeviceStandardColorSpaces, RGBColorSpace}; use shared::spectra::{
DenselySampledSpectrum, DeviceStandardColorSpaces, PiecewiseLinearSpectrum, RGBColorSpace,
};
use shared::Ptr; use shared::Ptr;
use std::sync::{Arc, LazyLock, OnceLock}; use std::sync::{Arc, LazyLock, OnceLock};
@ -18,8 +20,14 @@ pub static CIE_Y_DATA: LazyLock<DenselySampledSpectrum> =
LazyLock::new(|| data::create_cie(&CIE_Y)); LazyLock::new(|| data::create_cie(&CIE_Y));
pub static CIE_Z_DATA: LazyLock<DenselySampledSpectrum> = pub static CIE_Z_DATA: LazyLock<DenselySampledSpectrum> =
LazyLock::new(|| data::create_cie(&CIE_Z)); LazyLock::new(|| data::create_cie(&CIE_Z));
pub static CIE_D65_DATA: LazyLock<DenselySampledSpectrum> = /// pbrt builds D65 as `GetNamedSpectrum("stdillum-D65")`, i.e.
LazyLock::new(|| data::create_cie(&CIE_D65)); /// `PiecewiseLinearSpectrum::FromInterleaved(CIE_Illum_D6500, /*normalize=*/true)`, which
/// scales it so `InnerProduct(spec, Y) == CIE_Y_integral`. Do the same rather than carrying a
/// pre-normalized copy of the table.
pub static CIE_D65_DATA: LazyLock<DenselySampledSpectrum> = LazyLock::new(|| {
let pls = PiecewiseLinearSpectrum::from_interleaved(&CIE_ILLUM_D6500, true);
DenselySampledSpectrum::from_spectrum(&Spectrum::Piecewise(shared::leak(pls)))
});
pub fn cie_x() -> Spectrum { pub fn cie_x() -> Spectrum {
Spectrum::Dense(Ptr::from(&*CIE_X_DATA)) Spectrum::Dense(Ptr::from(&*CIE_X_DATA))

View file

@ -64,8 +64,7 @@ impl CreateSpectrumTexture for SpectrumCheckerboardTexture {
let tex = |name: &str, def: Spectrum| { let tex = |name: &str, def: Spectrum| {
arena.upload( arena.upload(
parameters parameters
.get_spectrum_texture(name, Some(def), spectrum_type) .get_spectrum_texture(name, def, spectrum_type),
.expect("default supplied"),
) )
}; };

View file

@ -47,8 +47,7 @@ impl CreateSpectrumTexture for SpectrumDotsTexture {
let get = |name: &str, def: Spectrum| { let get = |name: &str, def: Spectrum| {
let t = parameters let t = parameters
.get_spectrum_texture(name, Some(def), spectrum_type) .get_spectrum_texture(name, def, spectrum_type);
.expect("default supplied");
arena.upload(t) arena.upload(t)
}; };
let tex = SpectrumDotsTexture::new(map, get("inside", one), get("outside", zero)); let tex = SpectrumDotsTexture::new(map, get("inside", one), get("outside", zero));

View file

@ -1,19 +1,29 @@
use crate::core::texture::{CreateSpectrumTexture, SpectrumTexture};
use crate::utils::{FileLoc, TextureParameterDictionary};
use crate::Arena; use crate::Arena;
use crate::core::texture::{CreateSpectrumTexture, CreateTextureMapping, SpectrumTexture};
use crate::spectra::get_colorspace_device;
use crate::utils::{FileLoc, TextureParameterDictionary};
use anyhow::Result; use anyhow::Result;
use shared::Transform; use shared::Transform;
use shared::core::texture::SpectrumType; use shared::core::texture::{SpectrumType, TextureMapping3D};
use shared::textures::MarbleTexture; use shared::textures::MarbleTexture;
impl CreateSpectrumTexture for MarbleTexture { impl CreateSpectrumTexture for MarbleTexture {
fn create( fn create(
_render_from_texture: Transform, render_from_texture: Transform,
_parameters: TextureParameterDictionary, parameters: TextureParameterDictionary,
_spectrum_type: SpectrumType, spectrum_type: SpectrumType,
_loc: FileLoc, loc: FileLoc,
_arena: &Arena, _arena: &Arena,
) -> Result<SpectrumTexture> { ) -> Result<SpectrumTexture> {
todo!() let map = TextureMapping3D::create(&parameters, &render_from_texture, &loc)?;
let tex = MarbleTexture::new(
map,
parameters.get_one_int("octaves", 8)?,
parameters.get_one_float("roughness", 0.5)?,
parameters.get_one_float("scale", 1.)?,
parameters.get_one_float("variation", 0.2)?,
get_colorspace_device().srgb,
);
Ok(SpectrumTexture::Marble(tex))
} }
} }

View file

@ -1,20 +1,20 @@
use crate::core::texture::{
CreateSpectrumTexture, FloatTexture, SpectrumTexture };
use crate::utils::{FileLoc, TextureParameterDictionary};
use crate::Arena; use crate::Arena;
use crate::core::texture::{CreateSpectrumTexture, FloatTexture, SpectrumTexture};
use crate::utils::{FileLoc, TextureParameterDictionary};
use anyhow::Result; use anyhow::Result;
use shared::core::geometry::{Vector3f, VectorLike};
use shared::core::texture::{SpectrumType, TextureEvalContext};
use shared::spectra::{SampledSpectrum, SampledWavelengths};
use shared::utils::Transform;
use shared::Float; use shared::Float;
use shared::core::geometry::{Vector3f, VectorLike};
use shared::core::spectrum::Spectrum;
use shared::core::texture::{SpectrumType, TextureEvalContext};
use shared::spectra::{ConstantSpectrum, SampledSpectrum, SampledWavelengths};
use shared::utils::Transform;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct FloatMixTexture { pub struct FloatMixTexture {
pub tex1: Arc<FloatTexture>, pub tex1: Arc<FloatTexture>,
pub tex2: Arc<FloatTexture>, pub tex2: Arc<FloatTexture>,
pub amount: Arc<FloatTexture> pub amount: Arc<FloatTexture>,
} }
impl FloatMixTexture { impl FloatMixTexture {
@ -44,7 +44,7 @@ impl FloatMixTexture {
pub struct FloatDirectionMixTexture { pub struct FloatDirectionMixTexture {
pub tex1: Arc<FloatTexture>, pub tex1: Arc<FloatTexture>,
pub tex2: Arc<FloatTexture>, pub tex2: Arc<FloatTexture>,
pub dir: Vector3f pub dir: Vector3f,
} }
impl FloatDirectionMixTexture { impl FloatDirectionMixTexture {
@ -71,18 +71,31 @@ impl FloatDirectionMixTexture {
pub struct SpectrumMixTexture { pub struct SpectrumMixTexture {
pub tex1: Arc<SpectrumTexture>, pub tex1: Arc<SpectrumTexture>,
pub tex2: Arc<SpectrumTexture>, pub tex2: Arc<SpectrumTexture>,
pub amount: Arc<FloatTexture> pub amount: Arc<FloatTexture>,
} }
impl CreateSpectrumTexture for SpectrumMixTexture { impl CreateSpectrumTexture for SpectrumMixTexture {
fn create( fn create(
_render_from_texture: Transform, _render_from_texture: Transform,
_parameters: TextureParameterDictionary, parameters: TextureParameterDictionary,
_spectrum_type: SpectrumType, spectrum_type: SpectrumType,
_loc: FileLoc, loc: FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<SpectrumTexture> { ) -> Result<SpectrumTexture> {
todo!() let zero = Spectrum::Constant(ConstantSpectrum::new(0.));
let one = Spectrum::Constant(ConstantSpectrum::new(1.));
let tex = |name: &str, def: Spectrum| {
parameters
.get_spectrum_texture(name, def, spectrum_type)
};
let tex = SpectrumMixTexture {
tex1: tex("tex1", zero),
tex2: tex("tex2", one),
amount: parameters.get_float_texture("amount", 0.5)?,
};
Ok(SpectrumTexture::Mix(tex))
} }
} }
@ -90,18 +103,31 @@ impl CreateSpectrumTexture for SpectrumMixTexture {
pub struct SpectrumDirectionMixTexture { pub struct SpectrumDirectionMixTexture {
pub tex1: Arc<SpectrumTexture>, pub tex1: Arc<SpectrumTexture>,
pub tex2: Arc<SpectrumTexture>, pub tex2: Arc<SpectrumTexture>,
pub dir: Vector3f pub dir: Vector3f,
} }
impl CreateSpectrumTexture for SpectrumDirectionMixTexture { impl CreateSpectrumTexture for SpectrumDirectionMixTexture {
fn create( fn create(
_render_from_texture: Transform, render_from_texture: Transform,
_parameters: TextureParameterDictionary, parameters: TextureParameterDictionary,
_spectrum_type: SpectrumType, spectrum_type: SpectrumType,
_loc: FileLoc, loc: FileLoc,
_arena: &Arena, _arena: &Arena,
) -> Result<SpectrumTexture> { ) -> Result<SpectrumTexture> {
todo!() let dir_raw = parameters.get_one_vector3f("dir", Vector3f::new(0., 1., 0.))?;
let dir = render_from_texture.apply_to_vector(dir_raw).normalize();
let tex = |name: &str, def: Spectrum| {
parameters
.get_spectrum_texture(name, def, spectrum_type)
};
let zero = Spectrum::Constant(ConstantSpectrum::new(0.));
let one = Spectrum::Constant(ConstantSpectrum::new(1.));
let tex = SpectrumDirectionMixTexture {
tex1: tex("tex1", zero),
tex2: tex("tex2", one),
dir,
};
Ok(SpectrumTexture::DirectionMix(tex))
} }
} }

View file

@ -66,9 +66,8 @@ impl CreateSpectrumTexture for SpectrumScaledTexture {
_arena: &Arena, _arena: &Arena,
) -> Result<SpectrumTexture> { ) -> Result<SpectrumTexture> {
let one = Spectrum::Constant(ConstantSpectrum::new(1.0)); let one = Spectrum::Constant(ConstantSpectrum::new(1.0));
let tex = parameters // pbrt defaults "tex" to 1.0 rather than erroring (textures.cpp).
.get_spectrum_texture("tex", Some(one), spectrum_type) let tex = parameters.get_spectrum_texture("tex", one, spectrum_type);
.ok_or_else(|| anyhow::anyhow!("{}: missing \"tex\" parameter", _loc))?;
let scale = parameters.get_float_texture("scale", 1.0)?; let scale = parameters.get_float_texture("scale", 1.0)?;
if let FloatTexture::Constant(ref cscale) = *scale { if let FloatTexture::Constant(ref cscale) = *scale {

View file

@ -1,20 +1,21 @@
use crate::Arena; use crate::Arena;
use anyhow::Result; use anyhow::Result;
use shared::{textures::WindyTexture, utils::Transform}; use shared::{core::texture::TextureMapping3D, textures::WindyTexture, utils::Transform};
use crate::{ use crate::{
core::texture::{CreateFloatTexture, FloatTexture }, core::texture::{CreateFloatTexture, CreateTextureMapping, FloatTexture},
utils::{FileLoc, TextureParameterDictionary} utils::{FileLoc, TextureParameterDictionary},
}; };
impl CreateFloatTexture for WindyTexture { impl CreateFloatTexture for WindyTexture {
fn create( fn create(
_render_from_texture: Transform, render_from_texture: Transform,
_parameters: TextureParameterDictionary, parameters: TextureParameterDictionary,
_loc: FileLoc, loc: FileLoc,
_arena: &Arena, _arena: &Arena,
) -> Result<FloatTexture> { ) -> Result<FloatTexture> {
todo!() let map = TextureMapping3D::create(&parameters, &render_from_texture, &loc)?;
let tex = WindyTexture::new(map);
Ok(FloatTexture::Windy(tex))
} }
} }

View file

@ -1,20 +1,25 @@
use crate::Arena; use crate::Arena;
use anyhow::Result; use anyhow::Result;
use shared::{textures::WrinkledTexture, utils::Transform}; use shared::{core::texture::TextureMapping3D, textures::WrinkledTexture, utils::Transform};
use crate::{ use crate::{
core::texture::{CreateFloatTexture, FloatTexture }, core::texture::{CreateFloatTexture, CreateTextureMapping, FloatTexture},
utils::{FileLoc, TextureParameterDictionary} utils::{FileLoc, TextureParameterDictionary},
}; };
impl CreateFloatTexture for WrinkledTexture { impl CreateFloatTexture for WrinkledTexture {
fn create( fn create(
_render_from_texture: Transform, render_from_texture: Transform,
_parameters: TextureParameterDictionary, parameters: TextureParameterDictionary,
_loc: FileLoc, loc: FileLoc,
_arena: &Arena, arena: &Arena,
) -> Result<FloatTexture> { ) -> Result<FloatTexture> {
todo!() let map = TextureMapping3D::create(&parameters, &render_from_texture, &loc)?;
let tex = WrinkledTexture::new(
map,
parameters.get_one_int("octaves", 8)?.try_into().unwrap(),
parameters.get_one_float("roughness", 0.5)?,
);
Ok(FloatTexture::Wrinkled(tex))
} }
} }

View file

@ -835,22 +835,18 @@ impl TextureParameterDictionary {
self.dict.report_unused() self.dict.report_unused()
} }
/// Fetch a spectrum texture, falling back to a constant built from `def`.
/// A default is always supplied, so this cannot fail -- mirrors
/// `get_float_texture`. Use `get_spectrum_texture_or_null` when absence is
/// meaningful.
pub fn get_spectrum_texture( pub fn get_spectrum_texture(
&self, &self,
name: &str, name: &str,
val: Option<Spectrum>, def: impl Into<Spectrum>,
stype: SpectrumType, stype: SpectrumType,
) -> Option<Arc<SpectrumTexture>> { ) -> Arc<SpectrumTexture> {
let tex = self.get_spectrum_texture_or_null(name, stype); self.get_spectrum_texture_or_null(name, stype)
if tex.is_some() { .unwrap_or_else(|| Arc::new(SpectrumTexture::from(def.into())))
tex
} else if val.is_some() {
Some(Arc::new(SpectrumTexture::Constant(
SpectrumConstantTexture::new(val.unwrap()),
)))
} else {
None
}
} }
pub fn get_float_texture(&self, name: &str, val: Float) -> Result<Arc<FloatTexture>> { pub fn get_float_texture(&self, name: &str, val: Float) -> Result<Arc<FloatTexture>> {

View file

@ -142,6 +142,9 @@ impl WavefrontAggregate for CpuAggregate {
dndvs: intr.shading.dndv, dndvs: intr.shading.dndv,
}; };
if let Some(slot) = eval_q.push(item) { if let Some(slot) = eval_q.push(item) {
// The queue is reset every depth/batch/sample, so `slot < 10` fires on
// every pass -- this print dominated render time. Gated behind cpu_debug.
#[cfg(feature = "cpu_debug")]
if slot < 10 { if slot < 10 {
eprintln!( eprintln!(
"ENQUEUE[{slot}] pixel={:?} depth={} \ "ENQUEUE[{slot}] pixel={:?} depth={} \
@ -156,6 +159,7 @@ impl WavefrontAggregate for CpuAggregate {
item.uv, item.material, item.area_light, item.face_index, item.uv, item.material, item.area_light, item.face_index,
); );
} }
let _ = slot;
} }
}); });
} }

View file

@ -485,6 +485,9 @@ impl CpuWavefrontRenderer {
(0..n as usize).into_par_iter().for_each(|i| { (0..n as usize).into_par_iter().for_each(|i| {
let w = unsafe { queue.storage.get(i) }; let w = unsafe { queue.storage.get(i) };
// Fires on every material-queue pass (reset each depth/batch/sample), so it
// ran continuously and dominated render time. Gated behind cpu_debug.
#[cfg(feature = "cpu_debug")]
if i < 10 { if i < 10 {
eprintln!( eprintln!(
"DEQUEUE[{i}] pixel={:?} depth={} \ "DEQUEUE[{i}] pixel={:?} depth={} \
@ -728,7 +731,6 @@ impl CpuWavefrontRenderer {
if !pixel_bounds.contains_exclusive(p_pixel) { if !pixel_bounds.contains_exclusive(p_pixel) {
return; return;
} }
let l = self.pixel_sample_state.l.get(pixel_index); let l = self.pixel_sample_state.l.get(pixel_index);
let camera_weight = self.pixel_sample_state.camera_ray_weight.get(pixel_index); let camera_weight = self.pixel_sample_state.camera_ray_weight.get(pixel_index);
let weighted_l = l * camera_weight; let weighted_l = l * camera_weight;