Finishing material creators

This commit is contained in:
Wito Wiala 2026-09-02 15:31:12 +01:00
parent 6e23698e2d
commit 28bb963268
9 changed files with 263 additions and 48 deletions

View file

@ -5,7 +5,7 @@ use crate::core::interaction::{InteractionBase, ShadingGeom, SurfaceInteraction}
use crate::core::shape::Shape;
use crate::core::{LightIdx, MaterialIdx};
use crate::spectra::{N_SPECTRUM_SAMPLES, SampledSpectrum};
use crate::utils::math::{catmull_rom_weights, square};
use crate::utils::math::{catmull_rom_weights, invert_catmull_rom, square};
use crate::utils::sampling::sample_catmull_rom_2d;
use crate::{Float, GVec, PI, Ptr, gvec_with_capacity};
use enum_dispatch::enum_dispatch;
@ -147,6 +147,22 @@ impl BSSRDFTable {
}
}
pub fn subsurface_from_diffuse(
t: &BSSRDFTable,
rho_eff: &SampledSpectrum,
mfp: &SampledSpectrum,
) -> (SampledSpectrum, SampledSpectrum) {
// (sigma_a, sigma_s)
let mut sigma_a = SampledSpectrum::zero();
let mut sigma_s = SampledSpectrum::zero();
for c in 0..N_SPECTRUM_SAMPLES {
let rho = invert_catmull_rom(&t.rho_samples, &t.rho_eff, rho_eff[c]);
sigma_s[c] = rho / mfp[c];
sigma_a[c] = (1. - rho) / mfp[c];
}
(sigma_a, sigma_s)
}
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
pub struct BSSRDFProbeSegment {

View file

@ -36,6 +36,15 @@ pub enum Spectrum {
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> {
fn evaluate(&self, lambda: Float) -> Float {
self.get().unwrap().evaluate(lambda)

View file

@ -4,7 +4,7 @@ use crate::bxdfs::{
MeasuredBxDF, MeasuredBxDFData,
};
use crate::core::bsdf::BSDF;
use crate::core::bssrdf::{BSSRDF, BSSRDFTable};
use crate::core::bssrdf::{BSSRDF, BSSRDFTable, TabulatedBSSRDF, subsurface_from_diffuse};
use crate::core::bxdf::BxDF;
use crate::core::image::Image;
use crate::core::material::{Material, MaterialEvalContext, MaterialTrait};
@ -118,12 +118,30 @@ impl MaterialTrait for HairMaterial {
None
}
fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool {
todo!()
fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool {
match self.hair_absorption {
HairAbsorption::SigmaA(t) | HairAbsorption::Color(t) => {
tex_eval.can_evaluate(&[self.eta, self.beta_m, self.beta_n, self.alpha], &[t])
}
HairAbsorption::Melanin {
eumelanin,
pheomelanin,
} => tex_eval.can_evaluate(
&[
self.eta,
self.beta_m,
self.beta_n,
self.alpha,
eumelanin,
pheomelanin,
],
&[],
),
}
}
fn get_normal_map(&self) -> Option<&Image> {
todo!()
None
}
fn get_displacement(&self) -> Ptr<FloatTexture> {
@ -210,31 +228,78 @@ pub struct SubsurfaceMaterial {
impl MaterialTrait for SubsurfaceMaterial {
fn get_bsdf<T: TextureEvaluator>(
&self,
_tex_eval: &T,
_ctx: &MaterialEvalContext,
tex_eval: &T,
ctx: &MaterialEvalContext,
_lambda: &mut SampledWavelengths,
) -> BSDF {
todo!()
}
fn get_bssrdf<T>(
&self,
_tex_eval: &T,
_ctx: &MaterialEvalContext,
_lambda: &SampledWavelengths,
) -> Option<BSSRDF> {
todo!()
let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx);
let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx);
if self.remap_roughness {
u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough);
v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough);
}
let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough);
let bxdf = BxDF::Dielectric(DielectricBxDF::new(self.eta, distrib));
BSDF::new(ctx.ns, ctx.dpdus, bxdf)
}
fn can_evaluate_textures(&self, _tex_eval: &dyn TextureEvaluator) -> bool {
todo!()
fn get_bssrdf<T: TextureEvaluator>(
&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> {
todo!()
Some(&*self.normal_map)
}
fn get_displacement(&self) -> Ptr<FloatTexture> {
todo!()
self.displacement
}
fn has_subsurface_scattering(&self) -> bool {

View file

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

View file

@ -1,9 +1,9 @@
use crate::core::color::{RGB, XYZ};
use crate::core::geometry::{Lerp, MulAdd, Point, Point2f, Point2i, Vector, Vector3f, VectorLike};
use crate::core::pbrt::{Float, FloatBitOps, FloatBits, ONE_MINUS_EPSILON, PI, PI_OVER_4};
use crate::utils::gpu_array_from_fn;
use crate::utils::hash::{hash_buffer, mix_bits};
use crate::utils::sobol::{SOBOL_MATRICES_32, VDC_SOBOL_MATRICES, VDC_SOBOL_MATRICES_INV};
use crate::utils::{find_interval, gpu_array_from_fn};
use crate::{GVec, Ptr, gvec, gvec_with_capacity};
use core::fmt::{self, Display, Write};
use core::iter::{Product, Sum};
@ -379,6 +379,61 @@ pub fn integrate_catmull_rom(nodes: &[Float], f: &[Float], cdf: &mut [Float]) ->
sum
}
pub fn invert_catmull_rom(nodes: &[Float], f: &[Float], u: Float) -> Float {
// Stop when _u_ is out of bounds
if !(u > f[0]) {
return nodes[0];
} else if !(u < f[f.len() - 1]) {
return nodes[nodes.len() - 1];
}
// Map _u_ to a spline interval by inverting _f_
let i = find_interval(f.len() as u32, |j| f[j as usize] <= u) as usize;
// Look up $x_i$ and function values of spline segment _i_
let x0 = nodes[i];
let x1 = nodes[i + 1];
let f0 = f[i];
let f1 = f[i + 1];
let width = x1 - x0;
// Approximate derivatives using finite differences
let d0 = if i > 0 {
width * (f1 - f[i - 1]) / (x1 - nodes[i - 1])
} else {
f1 - f0
};
let d1 = if i + 2 < nodes.len() {
width * (f[i + 2] - f0) / (nodes[i + 2] - x0)
} else {
f1 - f0
};
// Invert the spline interpolant using Newton-Bisection
let eval = |t: Float| -> (Float, Float) {
// Compute powers of _t_
let t2 = t * t;
let t3 = t2 * t;
// Set _Fhat_ using Equation (\ref{eq:cubicspline-as-basisfunctions})
let f_cap_hat = (2. * t3 - 3. * t2 + 1.) * f0
+ (-2. * t3 + 3. * t2) * f1
+ (t3 - 2. * t2 + t) * d0
+ (t3 - t2) * d1;
// Set _fhat_ using Equation (\ref{eq:cubicspline-derivative})
let f_hat = (6. * t2 - 6. * t) * f0
+ (-6. * t2 + 6. * t) * f1
+ (3. * t2 - 4. * t + 1.) * d0
+ (3. * t2 - 2. * t) * d1;
return (f_cap_hat - u, f_hat);
};
let t = newton_bisection(0., 1., eval);
return x0 + t * width;
}
pub fn catmull_rom_weights(nodes: &[Float], x: Float) -> Option<(u32, [Float; 4])> {
if nodes.len() < 4 {
return None;

View file

@ -7,6 +7,7 @@ use shared::Float;
use shared::core::color::ColorEncoding;
use shared::core::geometry::Vector3f;
use shared::core::image::WrapMode;
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType;
use shared::core::texture::{
CylindricalMapping, PlanarMapping, PointTransformMapping, SphericalMapping, TextureEvalContext,
@ -95,6 +96,21 @@ pub enum SpectrumTexture {
DirectionMix(SpectrumDirectionMixTexture),
}
/// A bare `Spectrum` used as a texture is always a constant texture. Saves
/// writing `SpectrumTexture::Constant(SpectrumConstantTexture::new(s))` at every
/// default-value site.
impl From<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 {
fn create(
render_from_texture: Transform,

View file

@ -4,8 +4,8 @@ use crate::core::texture::SpectrumTexture;
use crate::globals::get_options;
use crate::spectra::data::get_named_spectrum;
use crate::utils::TextureParameterDictionary;
use crate::{Arena, FileLoc, ArenaUpload};
use anyhow::{bail, Result};
use crate::{Arena, ArenaUpload, FileLoc};
use anyhow::{Result, bail};
use shared::core::material::Material;
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType;
@ -36,12 +36,13 @@ impl CreateMaterial for CoatedDiffuseMaterial {
parameters.get_float_texture_with_fallback("vroughness", "roughness", 0.5)?;
let thickness = parameters.get_float_texture("thickness", 0.01)?;
let eta = parameters
.get_float_array("eta")?
.first()
.map(|&v| Spectrum::Constant(ConstantSpectrum::new(v)))
.or_else(|| parameters.get_one_spectrum("eta", None, SpectrumType::Unbounded))
.unwrap_or_else(|| Spectrum::Constant(ConstantSpectrum::new(1.5)));
let eta = if let Some(&v) = parameters.get_float_array("eta")?.first() {
Spectrum::from(v)
} else {
parameters
.get_one_spectrum("eta", None, SpectrumType::Unbounded)
.unwrap_or_else(|| Spectrum::from(1.5))
};
let max_depth = parameters.get_one_int("maxdepth", 10)?;
let n_samples = parameters.get_one_int("nsamples", 1)?;

View file

@ -297,6 +297,14 @@ impl CreateMaterial for SubsurfaceMaterial {
}
}
// 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 {
fn create(
parameters: &TextureParameterDictionary,
@ -305,12 +313,14 @@ impl CreateMaterial for MeasuredMaterial {
loc: &FileLoc,
arena: &Arena,
) -> 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)
}
// 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!()
}
}

View file

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