Running tests on parsing

This commit is contained in:
Wito Wiala 2026-05-12 15:07:59 +01:00
parent c659ea0f44
commit c8d083df62
26 changed files with 827 additions and 663 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ tests/
*.spv *.spv
*.json *.json
*.txt *.txt
scenes/

View file

@ -1,7 +1,7 @@
use crate::core::pbrt::Float; use crate::core::pbrt::Float;
use crate::utils::math::{next_float_down, next_float_up}; use crate::utils::math::{next_float_down, next_float_up};
use num_traits::Zero;
use core::ops::{Add, Div, Mul, Neg, Sub}; use core::ops::{Add, Div, Mul, Neg, Sub};
use num_traits::Zero;
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -19,123 +19,118 @@ pub fn lookup_spectrum(s: &Spectrum) -> Arc<DenselySampledSpectrumBuffer> {
cache.lookup(dense_spectrum).into() cache.lookup(dense_spectrum).into()
} }
pub trait CreateLight { // Placeholders for non-area lights that never inspect these arguments.
fn create( // TODO: refactor each light's create to only take what it actually needs,
render_from_light: Transform, // then delete these.
medium: Medium, fn dummy_shape() -> Shape {
parameters: &ParameterDictionary, Shape::default()
loc: &FileLoc,
shape: &Shape,
alpha_text: &FloatTexture,
colorspace: Option<&RGBColorSpace>,
arena: &Arena,
) -> Result<Light>;
} }
pub trait LightFactory { fn dummy_alpha() -> FloatTexture {
fn create( FloatTexture::default()
name: &str,
arena: &mut Arena,
render_from_light: Transform,
medium: Medium,
parameters: &ParameterDictionary,
loc: &FileLoc,
shape: &Shape,
alpha_tex: &FloatTexture,
colorspace: Option<&RGBColorSpace>,
camera_transform: CameraTransform,
) -> Result<Self>
where
Self: Sized;
} }
impl LightFactory for Light { /// Create a non-area light from a scene file directive.
fn create( pub fn create_light(
name: &str, name: &str,
arena: &mut Arena, render_from_light: Transform,
render_from_light: Transform, medium: Option<Medium>,
medium: Medium, parameters: &ParameterDictionary,
parameters: &ParameterDictionary, loc: &FileLoc,
loc: &FileLoc, camera_transform: CameraTransform,
shape: &Shape, arena: &mut Arena,
alpha_tex: &FloatTexture, ) -> Result<Light> {
colorspace: Option<&RGBColorSpace>, let shape = dummy_shape();
camera_transform: CameraTransform, let alpha = dummy_alpha();
) -> Result<Self>
where match name {
Self: Sized, "point" => crate::lights::point::create(
{ render_from_light,
match name { medium,
"diffuse" => DiffuseAreaLight::create( parameters,
render_from_light, loc,
medium, &shape,
parameters, &alpha,
loc, None,
shape, arena,
alpha_tex, ),
colorspace, "spot" => crate::lights::spot::create(
arena, render_from_light,
), medium,
"point" => PointLight::create( parameters,
render_from_light, loc,
medium, &shape,
parameters, &alpha,
loc, None,
shape, arena,
alpha_tex, ),
colorspace, "distant" => crate::lights::distant::create(
arena, render_from_light,
), medium,
"spot" => SpotLight::create( parameters,
render_from_light, loc,
medium, &shape,
parameters, &alpha,
loc, None,
shape, arena,
alpha_tex, ),
colorspace, "goniometric" => crate::lights::goniometric::create(
arena, render_from_light,
), medium,
"goniometric" => GoniometricLight::create( parameters,
render_from_light, loc,
medium, &shape,
parameters, &alpha,
loc, None,
shape, arena,
alpha_tex, ),
colorspace, "projection" => crate::lights::projection::create(
arena, render_from_light,
), medium,
"projection" => ProjectionLight::create( parameters,
render_from_light, loc,
medium, &shape,
parameters, &alpha,
loc, None,
shape, arena,
alpha_tex, ),
colorspace, "infinite" => crate::lights::infinite::create(
arena, render_from_light,
), medium.into(),
"distant" => DistantLight::create( camera_transform,
render_from_light, parameters,
medium, None,
parameters, loc,
loc, arena,
shape, ),
alpha_tex, "diffuse" => Err(anyhow!(
colorspace, "{}: \"diffuse\" is an area light; use create_area_light with a shape",
arena, loc
), )),
"infinite" => crate::lights::infinite::create( _ => Err(anyhow!("{}: unknown light type \"{}\"", loc, name)),
render_from_light,
medium.into(),
camera_transform,
parameters,
colorspace,
loc,
arena,
),
_ => Err(anyhow!("{}: unknown light type: \"{}\"", loc, name)),
}
} }
} }
/// Create a diffuse area light bound to a specific shape.
/// Called once per sub-shape (e.g. once per triangle in a mesh).
pub fn create_area_light(
render_from_light: Transform,
medium: Option<Medium>,
parameters: &ParameterDictionary,
loc: &FileLoc,
shape: &Shape,
alpha_tex: &FloatTexture,
colorspace: Option<&RGBColorSpace>,
arena: &mut Arena,
) -> Result<Light> {
crate::lights::diffuse::create(
render_from_light,
medium,
parameters,
loc,
shape,
alpha_tex,
colorspace,
arena,
)
}

View file

@ -39,7 +39,10 @@ impl MaterialFactory for Material {
named_materials: &HashMap<String, Material>, named_materials: &HashMap<String, Material>,
loc: FileLoc, loc: FileLoc,
arena: &Arena, arena: &Arena,
) -> Result<Self> where Self: Sized { ) -> Result<Self>
where
Self: Sized,
{
match name { match name {
"diffuse" => { "diffuse" => {
DiffuseMaterial::create(parameters, normal_map, named_materials, &loc, arena) DiffuseMaterial::create(parameters, normal_map, named_materials, &loc, arena)

View file

@ -641,7 +641,7 @@ impl ParserTarget for BasicSceneBuilder {
arena: Arc<Arena>, arena: Arc<Arena>,
) -> Result<(), ParserError> { ) -> Result<(), ParserError> {
let name = normalize_utf8(orig_name); let name = normalize_utf8(orig_name);
self.verify_world("Texture", &loc); self.verify_world("Texture", &loc)?;
let dict = ParameterDictionary::from_array( let dict = ParameterDictionary::from_array(
params.clone(), params.clone(),
&self.graphics_state.texture_attributes, &self.graphics_state.texture_attributes,
@ -701,7 +701,7 @@ impl ParserTarget for BasicSceneBuilder {
params: ParsedParameterVector, params: ParsedParameterVector,
loc: FileLoc, loc: FileLoc,
) -> Result<(), ParserError> { ) -> Result<(), ParserError> {
self.verify_world("material", &loc); self.verify_world("material", &loc)?;
let entity = SceneEntity { let entity = SceneEntity {
name: name.to_string(), name: name.to_string(),
loc, loc,
@ -733,9 +733,9 @@ impl ParserTarget for BasicSceneBuilder {
let dict = ParameterDictionary::from_array( let dict = ParameterDictionary::from_array(
params.clone(), params.clone(),
&self.graphics_state.medium_attributes, &self.graphics_state.medium_attributes,
self.graphics_state.color_space.clone() self.graphics_state.color_space.clone(),
)?; )?;
let render_from_light = AnimatedTransform::new( let render_from_light = AnimatedTransform::new(
&self.graphics_state.ctm.t[0], &self.graphics_state.ctm.t[0],
self.graphics_state.transform_start_time, self.graphics_state.transform_start_time,
@ -757,7 +757,6 @@ impl ParserTarget for BasicSceneBuilder {
self.scene.add_light(entity); self.scene.add_light(entity);
Ok(()) Ok(())
} }
fn area_light_source( fn area_light_source(
@ -786,7 +785,7 @@ impl ParserTarget for BasicSceneBuilder {
let dict = ParameterDictionary::from_array( let dict = ParameterDictionary::from_array(
params.clone(), params.clone(),
&self.graphics_state.shape_attributes, &self.graphics_state.shape_attributes,
self.graphics_state.color_space.clone() self.graphics_state.color_space.clone(),
)?; )?;
let render_from_object = self.graphics_state.ctm[0]; let render_from_object = self.graphics_state.ctm[0];
@ -797,7 +796,7 @@ impl ParserTarget for BasicSceneBuilder {
let light_entity = SceneEntity { let light_entity = SceneEntity {
name: al.name.clone(), name: al.name.clone(),
loc: al.loc.clone(), loc: al.loc.clone(),
parameters: al_dict parameters: al_dict,
}; };
Some(self.scene.add_area_light(light_entity)) Some(self.scene.add_area_light(light_entity))
} else { } else {
@ -816,7 +815,7 @@ impl ParserTarget for BasicSceneBuilder {
base: SceneEntity { base: SceneEntity {
name: name.to_string(), name: name.to_string(),
loc, loc,
parameters: dict parameters: dict,
}, },
render_from_object: Arc::new(render_from_object), render_from_object: Arc::new(render_from_object),
object_from_render: Arc::new(object_from_render), object_from_render: Arc::new(object_from_render),
@ -828,7 +827,11 @@ impl ParserTarget for BasicSceneBuilder {
}; };
if self.active_instance_definition.is_some() { if self.active_instance_definition.is_some() {
self.active_instance_definition.as_mut().unwrap().shapes.push(entity) self.active_instance_definition
.as_mut()
.unwrap()
.shapes
.push(entity)
} else { } else {
self.scene.add_shape(entity); self.scene.add_shape(entity);
} }

View file

@ -1,9 +1,11 @@
mod builder; pub mod builder;
mod entities; pub mod entities;
mod scene; pub mod scene;
mod state; pub mod state;
pub use builder::BasicSceneBuilder; pub use builder::BasicSceneBuilder;
pub use entities::*; pub use entities::*;
pub use scene::{BasicScene, SceneLookup}; pub use scene::{BasicScene, SceneLookup};
pub use state::*; pub use state::*;

View file

@ -4,7 +4,6 @@ use crate::core::camera::CameraFactory;
use crate::core::film::FilmFactory; use crate::core::film::FilmFactory;
use crate::core::filter::FilterFactory; use crate::core::filter::FilterFactory;
use crate::core::image::{Image, io::ImageIO}; use crate::core::image::{Image, io::ImageIO};
use crate::core::light::LightFactory;
use crate::core::material::MaterialFactory; use crate::core::material::MaterialFactory;
use crate::core::primitive::{CreateGeometricPrimitive, CreateSimplePrimitive}; use crate::core::primitive::{CreateGeometricPrimitive, CreateSimplePrimitive};
use crate::core::sampler::SamplerFactory; use crate::core::sampler::SamplerFactory;
@ -17,7 +16,7 @@ use crate::{Arena, FileLoc};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use parking_lot::Mutex; use parking_lot::Mutex;
use rayon::prelude::*; use rayon::prelude::*;
use shared::core::camera::{CameraTransform, Camera}; use shared::core::camera::{Camera, CameraTransform};
use shared::core::color::LINEAR; use shared::core::color::LINEAR;
use shared::core::film::Film; use shared::core::film::Film;
use shared::core::filter::Filter; use shared::core::filter::Filter;
@ -502,26 +501,114 @@ impl BasicScene {
&self, &self,
camera_transform: &CameraTransform, camera_transform: &CameraTransform,
arena: &mut Arena, arena: &mut Arena,
) -> Result<Vec<Light>> { ) -> Vec<Light> {
let state = self.light_state.lock(); let state = self.light_state.lock();
state.lights.par_iter().map(|entity| { state
let render_from_light = entity.transformed_base.render_from_object.start_transform; .lights
let medium = self.get_medium( .iter()
&entity.medium, .filter_map(|entity| {
&entity.transformed_base.base.loc, let render_from_light = entity.transformed_base.render_from_object.start_transform;
let medium = self
.get_medium(&entity.medium, &entity.transformed_base.base.loc)
.map(|m| *m);
match crate::core::light::create_light(
&entity.transformed_base.base.name,
render_from_light,
medium,
&entity.transformed_base.base.parameters,
&entity.transformed_base.base.loc,
camera_transform.clone(),
arena,
) {
Ok(light) => Some(light),
Err(e) => {
log::error!(
"{}: failed to create light: {}",
entity.transformed_base.base.loc,
e
);
None
}
}
})
.collect()
}
/// Create area lights for shapes that reference one. Produces a map from
/// shape index to a vec of lights (one per sub-shape, e.g. per triangle).
/// Must be called after shapes are loaded but before upload_shapes.
pub fn create_area_lights(
&self,
loaded_shapes: &[Vec<Shape>],
shape_entities: &[ShapeSceneEntity],
textures: &NamedTextures,
arena: &mut Arena,
) -> HashMap<usize, Vec<Light>> {
let light_state = self.light_state.lock();
let mut shape_lights: HashMap<usize, Vec<Light>> = HashMap::new();
for (i, entity) in shape_entities.iter().enumerate() {
let light_idx = match entity.light_index {
Some(idx) => idx,
None => continue,
};
let shapes = match loaded_shapes.get(i) {
Some(s) if !s.is_empty() => s,
_ => continue,
};
let al_entity = &light_state.area_lights[light_idx];
let alpha_tex = self.get_alpha_texture(
&entity.base.parameters,
&entity.base.loc,
&textures.float_textures,
); );
Light::create( // Use the film colorspace as fallback for area light emission
&entity.transformed_base.base.name, let film_cs = self.film_colorspace.lock();
&entity.transformed_base.base.parameters, let colorspace_ref = al_entity
render_from_light, .parameters
camera_transform, .color_space
medium.map(|m| *m), .as_ref()
&entity.transformed_base.base.loc, .or(film_cs.as_ref());
arena,
) let render_from_light = *entity.render_from_object;
}).collect()
let lights: Vec<Light> = shapes
.iter()
.filter_map(|shape| {
match crate::core::light::create_area_light(
render_from_light,
None,
&al_entity.parameters,
&al_entity.loc,
shape,
alpha_tex
.as_ref()
.expect("Alpha texture required for area light"),
colorspace_ref.map(|cs| cs.as_ref()),
arena,
) {
Ok(light) => Some(light),
Err(e) => {
log::error!("{}: failed to create area light: {}", al_entity.loc, e);
None
}
}
})
.collect();
if !lights.is_empty() {
shape_lights.insert(i, lights);
}
}
shape_lights
} }
pub fn create_aggregate( pub fn create_aggregate(

View file

@ -1,4 +1,4 @@
use super::{SceneEntity, TextureSceneEntity, LightSceneEntity}; use super::{LightSceneEntity, SceneEntity, TextureSceneEntity};
use crate::core::image::Image; use crate::core::image::Image;
use crate::core::texture::{FloatTexture, SpectrumTexture}; use crate::core::texture::{FloatTexture, SpectrumTexture};
use crate::utils::parallel::AsyncJob; use crate::utils::parallel::AsyncJob;

View file

@ -45,6 +45,12 @@ pub enum FloatTexture {
Wrinkled(WrinkledTexture), Wrinkled(WrinkledTexture),
} }
impl Default for FloatTexture {
fn default() -> Self {
FloatTexture::Constant(FloatConstantTexture::new(1.0))
}
}
impl FloatTextureTrait for Arc<FloatTexture> { impl FloatTextureTrait for Arc<FloatTexture> {
fn evaluate(&self, ctx: &TextureEvalContext) -> Float { fn evaluate(&self, ctx: &TextureEvalContext) -> Float {
self.as_ref().evaluate(ctx) self.as_ref().evaluate(ctx)

View file

@ -1,7 +1,8 @@
use super::*; use super::*;
use crate::Arena;
use crate::core::film::{CreateFilmBase, PixelSensor}; use crate::core::film::{CreateFilmBase, PixelSensor};
use crate::utils::containers::Array2D; use crate::utils::containers::Array2D;
use std::sync::Arc;
use crate::Arena;
use anyhow::Result; use anyhow::Result;
use shared::core::camera::CameraTransform; use shared::core::camera::CameraTransform;
use shared::core::film::{Film, FilmBase, RGBFilm, RGBPixel}; use shared::core::film::{Film, FilmBase, RGBFilm, RGBPixel};
@ -69,7 +70,10 @@ impl CreateFilm for RGBFilm {
loc: &FileLoc, loc: &FileLoc,
_arena: &Arena, _arena: &Arena,
) -> Result<Film> { ) -> Result<Film> {
let colorspace = params.color_space.as_ref().unwrap(); let colorspace = params.color_space.as_ref().cloned().unwrap_or_else(|| {
let stdcs = crate::spectra::get_colorspace_device();
Arc::new(*stdcs.srgb)
});
let max_component_value = params.get_one_float("maxcomponentvalue", Float::INFINITY)?; let max_component_value = params.get_one_float("maxcomponentvalue", Float::INFINITY)?;
let write_fp16 = params.get_one_bool("savefp16", true)?; let write_fp16 = params.get_one_bool("savefp16", true)?;
let sensor = PixelSensor::create(params, colorspace.clone(), exposure_time, loc)?; let sensor = PixelSensor::create(params, colorspace.clone(), exposure_time, loc)?;

View file

@ -23,20 +23,52 @@ pub static UC_RHO: [Float; N_RHO_SAMPLES] = [
]; ];
pub static U_RHO: [Point2f; N_RHO_SAMPLES] = [ pub static U_RHO: [Point2f; N_RHO_SAMPLES] = [
Point2f { 0: [0.855985, 0.570367]}, Point2f {
Point2f { 0: [0.381823, 0.851844]}, 0: [0.855985, 0.570367],
Point2f { 0: [0.285328, 0.764262]}, },
Point2f { 0: [0.733380, 0.114073]}, Point2f {
Point2f { 0: [0.542663, 0.344465]}, 0: [0.381823, 0.851844],
Point2f { 0: [0.127274, 0.414848]}, },
Point2f { 0: [0.964700, 0.947162]}, Point2f {
Point2f { 0: [0.594089, 0.643463]}, 0: [0.285328, 0.764262],
Point2f { 0: [0.095109, 0.170369]}, },
Point2f { 0: [0.825444, 0.263359]}, Point2f {
Point2f { 0: [0.429467, 0.454469]}, 0: [0.733380, 0.114073],
Point2f { 0: [0.244460, 0.816459]}, },
Point2f { 0: [0.756135, 0.731258]}, Point2f {
Point2f { 0: [0.516165, 0.152852]}, 0: [0.542663, 0.344465],
Point2f { 0: [0.180888, 0.214174]}, },
Point2f { 0: [0.898579, 0.503897]}, Point2f {
0: [0.127274, 0.414848],
},
Point2f {
0: [0.964700, 0.947162],
},
Point2f {
0: [0.594089, 0.643463],
},
Point2f {
0: [0.095109, 0.170369],
},
Point2f {
0: [0.825444, 0.263359],
},
Point2f {
0: [0.429467, 0.454469],
},
Point2f {
0: [0.244460, 0.816459],
},
Point2f {
0: [0.756135, 0.731258],
},
Point2f {
0: [0.516165, 0.152852],
},
Point2f {
0: [0.180888, 0.214174],
},
Point2f {
0: [0.898579, 0.503897],
},
]; ];

View file

@ -1,10 +1,10 @@
use super::RayIntegratorTrait;
use super::base::IntegratorBase; use super::base::IntegratorBase;
use super::constants::*; use super::constants::*;
use super::state::PathState; use super::state::PathState;
use super::RayIntegratorTrait;
use crate::core::interaction::InteractionGetter;
use crate::Arena; use crate::Arena;
use shared::core::bsdf::{BSDFSample, BSDF}; use crate::core::interaction::InteractionGetter;
use shared::core::bsdf::{BSDF, BSDFSample};
use shared::core::bxdf::{BxDFFlags, FArgs, TransportMode}; use shared::core::bxdf::{BxDFFlags, FArgs, TransportMode};
use shared::core::camera::Camera; use shared::core::camera::Camera;
use shared::core::film::VisibleSurface; use shared::core::film::VisibleSurface;

View file

@ -1,14 +1,14 @@
use std::path::Path; use std::path::Path;
use crate::core::image::{Image, ImageIO}; use crate::core::image::{Image, ImageIO};
use crate::core::light::{CreateLight, lookup_spectrum}; use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture; use crate::core::texture::FloatTexture;
use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename}; use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use shared::core::geometry::Point2i; use shared::core::geometry::Point2i;
use shared::core::light::{Light, LightBase, LightType}; use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::Medium; use shared::core::medium::{Medium, MediumInterface};
use shared::core::shape::{Shape, ShapeTrait}; use shared::core::shape::{Shape, ShapeTrait};
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
use shared::core::texture::GPUFloatTexture; use shared::core::texture::GPUFloatTexture;
@ -18,147 +18,155 @@ use shared::spectra::RGBColorSpace;
use shared::utils::{Ptr, Transform}; use shared::utils::{Ptr, Transform};
use shared::{Float, PI}; use shared::{Float, PI};
impl CreateLight for DiffuseAreaLight { pub fn create(
fn create( render_from_light: Transform,
render_from_light: Transform, medium: Option<Medium>,
medium: Medium, params: &ParameterDictionary,
params: &ParameterDictionary, loc: &FileLoc,
loc: &FileLoc, shape: &Shape,
shape: &Shape, alpha: &FloatTexture,
alpha: &FloatTexture, colorspace: Option<&RGBColorSpace>,
colorspace: Option<&RGBColorSpace>, arena: &Arena,
arena: &Arena, ) -> Result<Light> {
) -> Result<Light> { let mut l = params.get_one_spectrum("l", None, SpectrumType::Illuminant);
let mut l = params.get_one_spectrum("l", None, SpectrumType::Illuminant); let illum_spec = Spectrum::Dense(colorspace.unwrap().illuminant);
let illum_spec = Spectrum::Dense(colorspace.unwrap().illuminant); let mut scale = params.get_one_float("scale", 1.)?;
let mut scale = params.get_one_float("scale", 1.)?; let two_sided = params.get_one_bool("twosided", false)?;
let two_sided = params.get_one_bool("twosided", false)?;
let filename = resolve_filename(&params.get_one_string("filename", "")?); let filename = resolve_filename(&params.get_one_string("filename", "")?);
let (image, image_color_space) = if !filename.is_empty() { let (image, image_color_space) = if !filename.is_empty() {
if l.is_some() { if l.is_some() {
return Err(anyhow!("{}: both \"L\" and \"filename\" specified", loc)); return Err(anyhow!("{}: both \"L\" and \"filename\" specified", loc));
}
let im = Image::read(Path::new(&filename), None)?;
if im.image.has_any_infinite_pixels() {
return Err(anyhow!("{}: image has infinite pixel values", loc));
}
if im.image.has_any_nan_pixels() {
return Err(anyhow!("{}: image has NaN pixel values", loc));
}
let channel_desc = im
.image
.get_channel_desc(&["R", "G", "B"])
.map_err(|_| anyhow!("{}: image must have R, G, B channels", loc))?;
let image = im.image.select_channels(&channel_desc);
let cs = im.metadata.get_colorspace();
(Some(image), cs)
} else {
if l.is_none() {
l = Some(illum_spec);
}
(None, None)
};
let l_for_scale = l.as_ref().unwrap_or(&illum_spec);
scale /= spectrum_to_photometric(*l_for_scale);
let phi_v = params.get_one_float("power", -1.0)?;
if phi_v > 0.0 {
// k_e is the emissive power of the light as defined by the spectral
// distribution and texture and is used to normalize the emitted
// radiance such that the user-defined power will be the actual power
// emitted by the light.
let mut k_e: Float = 1.0;
if let Some(ref img) = image {
// Get the appropriate luminance vector from the image colour space
let lum_vec = image_color_space.unwrap().luminance_vector();
let mut sum_k_e = 0.0;
let res = img.resolution();
for y in 0..res.y() {
for x in 0..res.x() {
let r = img.get_channel(Point2i::new(x, y), 0);
let g = img.get_channel(Point2i::new(x, y), 1);
let b = img.get_channel(Point2i::new(x, y), 2);
sum_k_e += r * lum_vec[0] + g * lum_vec[1] + b * lum_vec[2];
}
}
k_e = sum_k_e / (res.x() * res.y()) as Float;
}
let side_factor = if two_sided { 2.0 } else { 1.0 };
k_e *= side_factor * shape.area() * PI;
// now multiply up scale to hit the target power
scale *= phi_v / k_e;
} }
let alpha_ptr = alpha.upload(arena); let im = Image::read(Path::new(&filename), None)?;
let is_constant_zero = match &*alpha_ptr {
GPUFloatTexture::Constant(tex) => tex.evaluate(&TextureEvalContext::default()) == 0.0,
_ => false,
};
let (light_type, stored_alpha) = if is_constant_zero { if im.image.has_any_infinite_pixels() {
(LightType::DeltaPosition, None) return Err(anyhow!("{}: image has infinite pixel values", loc));
} else { }
(LightType::Area, Some(alpha)) if im.image.has_any_nan_pixels() {
}; return Err(anyhow!("{}: image has NaN pixel values", loc));
}
let channel_desc = im
.image
.get_channel_desc(&["R", "G", "B"])
.map_err(|_| anyhow!("{}: image must have R, G, B channels", loc))?;
let image = im.image.select_channels(&channel_desc);
let cs = im.metadata.get_colorspace();
(Some(image), cs)
} else {
if l.is_none() {
l = Some(illum_spec);
}
(None, None)
};
let l_for_scale = l.as_ref().unwrap_or(&illum_spec);
scale /= spectrum_to_photometric(*l_for_scale);
let phi_v = params.get_one_float("power", -1.0)?;
if phi_v > 0.0 {
// k_e is the emissive power of the light as defined by the spectral
// distribution and texture and is used to normalize the emitted
// radiance such that the user-defined power will be the actual power
// emitted by the light.
let mut k_e: Float = 1.0;
let base = LightBase::new(light_type, render_from_light, medium.into());
if let Some(ref img) = image { if let Some(ref img) = image {
let desc = img // Get the appropriate luminance vector from the image colour space
.get_channel_desc(&["R", "G", "B"]) let lum_vec = image_color_space.unwrap().luminance_vector();
.expect("Image used for DiffuseAreaLight doesn't have R, G, B channels");
assert_eq!(3, desc.size()); let mut sum_k_e = 0.0;
assert!(desc.is_identity()); let res = img.resolution();
assert!(
image_color_space.is_some(), for y in 0..res.y() {
"Image provided but ColorSpace is missing" for x in 0..res.x() {
); let r = img.get_channel(Point2i::new(x, y), 0);
let g = img.get_channel(Point2i::new(x, y), 1);
let b = img.get_channel(Point2i::new(x, y), 2);
sum_k_e += r * lum_vec[0] + g * lum_vec[1] + b * lum_vec[2];
}
}
k_e = sum_k_e / (res.x() * res.y()) as Float;
} }
let is_triangle_or_bilinear = let side_factor = if two_sided { 2.0 } else { 1.0 };
matches!(*shape, Shape::Triangle(_) | Shape::BilinearPatch(_)); k_e *= side_factor * shape.area() * PI;
if render_from_light.has_scale(None) && !is_triangle_or_bilinear {
println!(
"Scaling detected in rendering to light space transformation! \
Proceed at your own risk; your image may have errors."
);
}
let shape_ptr = shape.upload(arena); // now multiply up scale to hit the target power
let image_ptr = image scale *= phi_v / k_e;
.as_ref()
.map(|img| arena.alloc(*img.device()))
.unwrap_or(Ptr::null());
let colorspace_ptr = image_color_space
.map(|cs| cs.upload(arena))
.unwrap_or(Ptr::null());
let lemit_ptr = arena.alloc(lookup_spectrum(l_for_scale).device());
let specific = DiffuseAreaLight {
base,
area: shape.area(),
shape: shape_ptr,
alpha: alpha_ptr,
image: image_ptr,
colorspace: colorspace_ptr,
lemit: lemit_ptr,
two_sided,
scale,
};
Ok(Light::DiffuseArea(specific))
} }
let alpha_ptr = alpha.upload(arena);
let is_constant_zero = match &*alpha_ptr {
GPUFloatTexture::Constant(tex) => tex.evaluate(&TextureEvalContext::default()) == 0.0,
_ => false,
};
let (light_type, stored_alpha) = if is_constant_zero {
(LightType::DeltaPosition, None)
} else {
(LightType::Area, Some(alpha))
};
let mi = match medium {
Some(m) => {
let ptr = arena.alloc(m);
MediumInterface {
inside: ptr,
outside: ptr,
}
}
None => MediumInterface::default(),
};
let base = LightBase::new(light_type, render_from_light, mi);
if let Some(ref img) = image {
let desc = img
.get_channel_desc(&["R", "G", "B"])
.expect("Image used for DiffuseAreaLight doesn't have R, G, B channels");
assert_eq!(3, desc.size());
assert!(desc.is_identity());
assert!(
image_color_space.is_some(),
"Image provided but ColorSpace is missing"
);
}
let is_triangle_or_bilinear = matches!(*shape, Shape::Triangle(_) | Shape::BilinearPatch(_));
if render_from_light.has_scale(None) && !is_triangle_or_bilinear {
println!(
"Scaling detected in rendering to light space transformation! \
Proceed at your own risk; your image may have errors."
);
}
let shape_ptr = shape.upload(arena);
let image_ptr = image
.as_ref()
.map(|img| arena.alloc(*img.device()))
.unwrap_or(Ptr::null());
let colorspace_ptr = image_color_space
.map(|cs| cs.upload(arena))
.unwrap_or(Ptr::null());
let lemit_ptr = arena.alloc(lookup_spectrum(l_for_scale).device());
let specific = DiffuseAreaLight {
base,
area: shape.area(),
shape: shape_ptr,
alpha: alpha_ptr,
image: image_ptr,
colorspace: colorspace_ptr,
lemit: lemit_ptr,
two_sided,
scale,
};
Ok(Light::DiffuseArea(specific))
} }

View file

@ -1,4 +1,4 @@
use crate::core::light::{CreateLight, lookup_spectrum}; use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture; use crate::core::texture::FloatTexture;
use crate::utils::{Arena, FileLoc, ParameterDictionary}; use crate::utils::{Arena, FileLoc, ParameterDictionary};
@ -36,59 +36,57 @@ impl CreateDistantLight for DistantLight {
} }
} }
impl CreateLight for DistantLight { pub fn create(
fn create( render_from_light: Transform,
render_from_light: Transform, _medium: Option<Medium>,
_medium: Medium, parameters: &ParameterDictionary,
parameters: &ParameterDictionary, _loc: &FileLoc,
_loc: &FileLoc, _shape: &Shape,
_shape: &Shape, _alpha_text: &FloatTexture,
_alpha_text: &FloatTexture, colorspace: Option<&RGBColorSpace>,
colorspace: Option<&RGBColorSpace>, _arena: &Arena,
_arena: &Arena, ) -> Result<Light> {
) -> Result<Light> { let l = parameters
let l = parameters .get_one_spectrum(
.get_one_spectrum( "L",
"L", Some(Spectrum::Dense(colorspace.unwrap().illuminant)),
Some(Spectrum::Dense(colorspace.unwrap().illuminant)), SpectrumType::Illuminant,
SpectrumType::Illuminant, )
) .unwrap();
.unwrap(); let mut scale = parameters.get_one_float("scale", 1.)?;
let mut scale = parameters.get_one_float("scale", 1.)?;
let from = parameters.get_one_point3f("from", Point3f::new(0., 0., 0.))?; let from = parameters.get_one_point3f("from", Point3f::new(0., 0., 0.))?;
let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?; let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?;
let w = (from - to).normalize(); let w = (from - to).normalize();
let (v1, v2) = w.coordinate_system(); let (v1, v2) = w.coordinate_system();
let m: [Float; 16] = [ let m: [Float; 16] = [
v1.x(), v1.x(),
v2.x(), v2.x(),
w.x(), w.x(),
0., 0.,
v1.y(), v1.y(),
v2.y(), v2.y(),
w.y(), w.y(),
0., 0.,
v1.z(), v1.z(),
v2.z(), v2.z(),
w.z(), w.z(),
0., 0.,
0., 0.,
0., 0.,
0., 0.,
1., 1.,
]; ];
let t = Transform::from_flat(&m).expect("Could not create transform for DistantLight"); let t = Transform::from_flat(&m).expect("Could not create transform for DistantLight");
let final_render = render_from_light * t; let final_render = render_from_light * t;
scale /= spectrum_to_photometric(l); scale /= spectrum_to_photometric(l);
// Adjust scale to meet target illuminance value // Adjust scale to meet target illuminance value
let e_v = parameters.get_one_float("illuminance", -1.)?; let e_v = parameters.get_one_float("illuminance", -1.)?;
if e_v > 0. { if e_v > 0. {
scale *= e_v; scale *= e_v;
}
let specific = DistantLight::new(final_render, l, scale);
Ok(Light::Distant(specific))
} }
let specific = DistantLight::new(final_render, l, scale);
Ok(Light::Distant(specific))
} }

View file

@ -1,7 +1,7 @@
use std::path::Path; use std::path::Path;
use crate::core::image::{Image, ImageIO}; use crate::core::image::{Image, ImageIO};
use crate::core::light::{CreateLight, lookup_spectrum}; use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture; use crate::core::texture::FloatTexture;
use crate::utils::sampling::PiecewiseConstant2D; use crate::utils::sampling::PiecewiseConstant2D;
@ -9,7 +9,7 @@ use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use shared::core::geometry::Point2i; use shared::core::geometry::Point2i;
use shared::core::light::{Light, LightBase, LightType}; use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::Medium; use shared::core::medium::{Medium, MediumInterface};
use shared::core::shape::Shape; use shared::core::shape::Shape;
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType; use shared::core::texture::SpectrumType;
@ -18,95 +18,99 @@ use shared::spectra::RGBColorSpace;
use shared::utils::{Ptr, Transform}; use shared::utils::{Ptr, Transform};
use shared::{Float, PI}; use shared::{Float, PI};
impl CreateLight for GoniometricLight { pub fn create(
fn create( render_from_light: Transform,
render_from_light: Transform, medium: Option<Medium>,
medium: Medium, params: &ParameterDictionary,
params: &ParameterDictionary, loc: &FileLoc,
loc: &FileLoc, _shape: &Shape,
_shape: &Shape, _alpha_text: &FloatTexture,
_alpha_text: &FloatTexture, colorspace: Option<&RGBColorSpace>,
colorspace: Option<&RGBColorSpace>, arena: &Arena,
arena: &Arena, ) -> Result<Light> {
) -> Result<Light> { let i = params
let i = params .get_one_spectrum(
.get_one_spectrum( "I",
"I", Some(Spectrum::Dense(colorspace.unwrap().illuminant)),
Some(Spectrum::Dense(colorspace.unwrap().illuminant)), SpectrumType::Illuminant,
SpectrumType::Illuminant, )
) .expect("Could not retrieve spectrum");
.expect("Could not retrieve spectrum"); let mut scale = params.get_one_float("scale", 1.)?;
let mut scale = params.get_one_float("scale", 1.)?; let filename = resolve_filename(&params.get_one_string("filename", "")?);
let filename = resolve_filename(&params.get_one_string("filename", "")?); let image: Ptr<Image> = if filename.is_empty() {
let image: Ptr<Image> = if filename.is_empty() { Ptr::null()
Ptr::null() } else {
} else { let im = Image::read(Path::new(&filename), None)
let im = Image::read(Path::new(&filename), None) .map_err(|e| anyhow!("could not load image '{}': {}", filename, e))?;
.map_err(|e| anyhow!("could not load image '{}': {}", filename, e))?;
let loaded = im.image; let loaded = im.image;
let res = loaded.resolution(); let res = loaded.resolution();
if loaded.has_any_infinite_pixels() { if loaded.has_any_infinite_pixels() {
return Err(anyhow!( return Err(anyhow!(
"image '{}' has infinite pixels, not suitable for light", "image '{}' has infinite pixels, not suitable for light",
filename filename
)); ));
}
if res.x() != res.y() {
return Err(anyhow!(
"image resolution ({}, {}) is non-square; unlikely to be an equal-area map",
res.x(),
res.y()
));
}
Ptr::from(&convert_to_luminance_image(&loaded, &filename, loc)?)
};
scale /= spectrum_to_photometric(i);
let phi_v = params.get_one_float("power", -1.0)?;
if phi_v > 0.0 {
let k_e = compute_emissive_power(&image);
scale *= phi_v / k_e;
} }
let swap_yz: [Float; 16] = [ if res.x() != res.y() {
1., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., return Err(anyhow!(
]; "image resolution ({}, {}) is non-square; unlikely to be an equal-area map",
let t = Transform::from_flat(&swap_yz) res.x(),
.expect("Could not create transform for GoniometricLight"); res.y()
let final_render_from_light = render_from_light * t; ));
}
let base = LightBase::new( Ptr::from(&convert_to_luminance_image(&loaded, &filename, loc)?)
LightType::DeltaPosition, };
final_render_from_light,
medium.into(),
);
let iemit = lookup_spectrum(&i); scale /= spectrum_to_photometric(i);
let phi_v = params.get_one_float("power", -1.0)?;
let image_ptr = if !image.is_null() { if phi_v > 0.0 {
let distrib = PiecewiseConstant2D::from_image(&image); let k_e = compute_emissive_power(&image);
let distrib_ptr = distrib.upload(arena); scale *= phi_v / k_e;
let img_ptr = image.upload(arena);
(img_ptr, distrib_ptr)
} else {
(Ptr::null(), Ptr::null())
};
let specific = GoniometricLight {
base,
iemit: arena.alloc(iemit.device()),
scale,
image: image_ptr.0,
distrib: image_ptr.1,
};
Ok(Light::Goniometric(specific))
} }
let swap_yz: [Float; 16] = [
1., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1.,
];
let t =
Transform::from_flat(&swap_yz).expect("Could not create transform for GoniometricLight");
let final_render_from_light = render_from_light * t;
let mi = match medium {
Some(m) => {
let ptr = arena.alloc(m);
MediumInterface {
inside: ptr,
outside: ptr,
}
}
None => MediumInterface::default(),
};
let base = LightBase::new(LightType::DeltaPosition, render_from_light, mi);
let iemit = lookup_spectrum(&i);
let image_ptr = if !image.is_null() {
let distrib = PiecewiseConstant2D::from_image(&image);
let distrib_ptr = distrib.upload(arena);
let img_ptr = image.upload(arena);
(img_ptr, distrib_ptr)
} else {
(Ptr::null(), Ptr::null())
};
let specific = GoniometricLight {
base,
iemit: arena.alloc(iemit.device()),
scale,
image: image_ptr.0,
distrib: image_ptr.1,
};
Ok(Light::Goniometric(specific))
} }
fn convert_to_luminance_image(image: &Image, filename: &str, loc: &FileLoc) -> Result<Image> { fn convert_to_luminance_image(image: &Image, filename: &str, loc: &FileLoc) -> Result<Image> {

View file

@ -11,7 +11,7 @@ use shared::core::camera::CameraTransform;
use shared::core::geometry::{Bounds2f, Frame, Point2f, Point2i, Point3f, VectorLike, cos_theta}; use shared::core::geometry::{Bounds2f, Frame, Point2f, Point2i, Point3f, VectorLike, cos_theta};
use shared::core::image::{DeviceImage, WrapMode}; use shared::core::image::{DeviceImage, WrapMode};
use shared::core::light::{Light, LightBase, LightType}; use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::MediumInterface; use shared::core::medium::{Medium, MediumInterface};
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType; use shared::core::texture::SpectrumType;
use shared::lights::{ImageInfiniteLight, PortalInfiniteLight, UniformInfiniteLight}; use shared::lights::{ImageInfiniteLight, PortalInfiniteLight, UniformInfiniteLight};
@ -124,7 +124,7 @@ impl CreateUniformInfiniteLight for UniformInfiniteLight {
pub fn create( pub fn create(
render_from_light: Transform, render_from_light: Transform,
_medium: MediumInterface, _medium: Option<Medium>,
camera_transform: CameraTransform, camera_transform: CameraTransform,
parameters: &ParameterDictionary, parameters: &ParameterDictionary,
colorspace: Option<&RGBColorSpace>, colorspace: Option<&RGBColorSpace>,

View file

@ -1,4 +1,4 @@
use crate::core::light::{CreateLight, lookup_spectrum}; use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture; use crate::core::texture::FloatTexture;
use crate::utils::{Arena, FileLoc, ParameterDictionary}; use crate::utils::{Arena, FileLoc, ParameterDictionary};
@ -42,36 +42,46 @@ impl CreatePointLight for PointLight {
} }
} }
impl CreateLight for PointLight { pub fn create(
fn create( render_from_light: Transform,
render_from_light: Transform, medium: Option<Medium>,
medium: Medium, parameters: &ParameterDictionary,
parameters: &ParameterDictionary, _loc: &FileLoc,
_loc: &FileLoc, _shape: &Shape,
_shape: &Shape, _alpha: &FloatTexture,
_alpha: &FloatTexture, colorspace: Option<&RGBColorSpace>,
colorspace: Option<&RGBColorSpace>, arena: &Arena,
_arena: &Arena, ) -> Result<Light> {
) -> Result<Light> { let l = parameters
let l = parameters .get_one_spectrum(
.get_one_spectrum( "L",
"L", Some(Spectrum::Dense(colorspace.unwrap().illuminant)),
Some(Spectrum::Dense(colorspace.unwrap().illuminant)), SpectrumType::Illuminant,
SpectrumType::Illuminant, )
) .unwrap();
.unwrap(); let mut scale = parameters.get_one_float("scale", 1.)?;
let mut scale = parameters.get_one_float("scale", 1.)?; scale /= spectrum_to_photometric(l);
scale /= spectrum_to_photometric(l); let phi_v = parameters.get_one_float("power", 1.)?;
let phi_v = parameters.get_one_float("power", 1.)?; if phi_v > 0. {
if phi_v > 0. { let k_e = 4. * PI;
let k_e = 4. * PI; scale *= phi_v / k_e;
scale *= phi_v / k_e;
}
let from = parameters.get_one_point3f("from", Point3f::zero())?;
let tf = Transform::translate(from.into());
let final_render = render_from_light * tf;
let specific = PointLight::new(final_render, medium.into(), l, scale);
Ok(Light::Point(specific))
} }
let from = parameters.get_one_point3f("from", Point3f::zero())?;
let tf = Transform::translate(from.into());
let final_render = render_from_light * tf;
let mi = match medium {
Some(m) => {
let ptr = arena.alloc(m);
MediumInterface {
inside: ptr,
outside: ptr,
}
}
None => MediumInterface::default(),
};
let specific = PointLight::new(final_render, mi, l, scale);
Ok(Light::Point(specific))
} }

View file

@ -1,5 +1,4 @@
use crate::core::image::{Image, ImageIO}; use crate::core::image::{Image, ImageIO};
use crate::core::light::CreateLight;
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture; use crate::core::texture::FloatTexture;
use crate::utils::sampling::PiecewiseConstant2D; use crate::utils::sampling::PiecewiseConstant2D;
@ -10,7 +9,7 @@ use shared::core::geometry::{
Bounds2f, Point2f, Point2i, Point3f, Vector3f, VectorLike, cos_theta, Bounds2f, Point2f, Point2i, Point3f, Vector3f, VectorLike, cos_theta,
}; };
use shared::core::light::{Light, LightBase, LightType}; use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::Medium; use shared::core::medium::{Medium, MediumInterface};
use shared::core::shape::Shape; use shared::core::shape::Shape;
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
use shared::lights::ProjectionLight; use shared::lights::ProjectionLight;
@ -19,117 +18,124 @@ use shared::utils::Transform;
use shared::utils::math::{radians, square}; use shared::utils::math::{radians, square};
use std::path::Path; use std::path::Path;
impl CreateLight for ProjectionLight { pub fn create(
fn create( render_from_light: Transform,
render_from_light: Transform, medium: Option<Medium>,
medium: Medium, parameters: &ParameterDictionary,
parameters: &ParameterDictionary, loc: &FileLoc,
loc: &FileLoc, _shape: &Shape,
_shape: &Shape, _alpha_text: &FloatTexture,
_alpha_text: &FloatTexture, _colorspace: Option<&RGBColorSpace>,
_colorspace: Option<&RGBColorSpace>, arena: &Arena,
arena: &Arena, ) -> Result<Light> {
) -> Result<Light> { let mut scale = parameters.get_one_float("scale", 1.)?;
let mut scale = parameters.get_one_float("scale", 1.)?; let power = parameters.get_one_float("power", -1.)?;
let power = parameters.get_one_float("power", -1.)?; let fov = parameters.get_one_float("fov", 90.)?;
let fov = parameters.get_one_float("fov", 90.)?;
let filename = resolve_filename(&parameters.get_one_string("filename", "")?); let filename = resolve_filename(&parameters.get_one_string("filename", "")?);
if filename.is_empty() { if filename.is_empty() {
return Err(anyhow!( return Err(anyhow!(
"{}: must provide filename for projection light", "{}: must provide filename for projection light",
loc loc
)); ));
}
let im = Image::read(Path::new(&filename), None)
.map_err(|e| anyhow!("{}: could not load image '{}': {}", loc, filename, e))?;
if im.image.has_any_infinite_pixels() {
return Err(anyhow!(
"{}: image '{}' has infinite pixels, not suitable for light",
loc,
filename
));
}
if im.image.has_any_nan_pixels() {
return Err(anyhow!(
"{}: image '{}' has NaN pixels, not suitable for light",
loc,
filename
));
}
let channel_desc = im
.image
.get_channel_desc(&["R", "G", "B"])
.map_err(|_| anyhow!("{}: image '{}' must have R, G, B channels", loc, filename))?;
let image = im.image.select_channels(&channel_desc);
let colorspace = im
.metadata
.colorspace
.ok_or_else(|| anyhow!("{}: image '{}' missing colorspace metadata", loc, filename))?;
scale /= spectrum_to_photometric(Spectrum::Dense(colorspace.illuminant));
if power > 0. {
let k_e = compute_emissive_power(&image, &colorspace, fov);
scale /= k_e;
}
let flip = Transform::scale(1., -1., 1.);
let render_from_light_flip = render_from_light * flip;
let base = LightBase::new(LightType::DeltaPosition, render_from_light, medium.into());
let opposite = (radians(fov) / 2.0).tan();
let res = image.resolution();
let aspect = res.x() as Float / res.y() as Float;
let aspect_ratio = if aspect > 1.0 { aspect } else { 1.0 / aspect };
let a = 4.0 * square(opposite) * aspect_ratio;
let screen_bounds = if aspect > 1.0 {
Bounds2f::from_points(Point2f::new(-aspect, -1.0), Point2f::new(aspect, 1.0))
} else {
Bounds2f::from_points(
Point2f::new(-1.0, -1.0 / aspect),
Point2f::new(1.0, 1.0 / aspect),
)
};
let hither = 1e-3;
let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap();
let light_from_screen = screen_from_light.inverse();
let dwda = |p: Point2f| {
let w =
Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.0)));
cos_theta(w.normalize()).powi(3)
};
let d = image.get_sampling_distribution(dwda, screen_bounds);
let distrib = PiecewiseConstant2D::from_slice(
d.as_slice(),
d.x_size() as usize,
d.y_size() as usize,
screen_bounds,
);
let specific = ProjectionLight {
base,
image: image.upload(arena),
image_color_space: colorspace.upload(arena),
distrib: distrib.upload(arena),
screen_bounds,
screen_from_light,
light_from_screen,
scale,
hither,
a,
};
Ok(Light::Projection(specific))
} }
let im = Image::read(Path::new(&filename), None)
.map_err(|e| anyhow!("{}: could not load image '{}': {}", loc, filename, e))?;
if im.image.has_any_infinite_pixels() {
return Err(anyhow!(
"{}: image '{}' has infinite pixels, not suitable for light",
loc,
filename
));
}
if im.image.has_any_nan_pixels() {
return Err(anyhow!(
"{}: image '{}' has NaN pixels, not suitable for light",
loc,
filename
));
}
let channel_desc = im
.image
.get_channel_desc(&["R", "G", "B"])
.map_err(|_| anyhow!("{}: image '{}' must have R, G, B channels", loc, filename))?;
let image = im.image.select_channels(&channel_desc);
let colorspace = im
.metadata
.colorspace
.ok_or_else(|| anyhow!("{}: image '{}' missing colorspace metadata", loc, filename))?;
scale /= spectrum_to_photometric(Spectrum::Dense(colorspace.illuminant));
if power > 0. {
let k_e = compute_emissive_power(&image, &colorspace, fov);
scale /= k_e;
}
let flip = Transform::scale(1., -1., 1.);
let render_from_light_flip = render_from_light * flip;
let mi = match medium {
Some(m) => {
let ptr = arena.alloc(m);
MediumInterface {
inside: ptr,
outside: ptr,
}
}
None => MediumInterface::default(),
};
let base = LightBase::new(LightType::DeltaPosition, render_from_light, mi);
let opposite = (radians(fov) / 2.0).tan();
let res = image.resolution();
let aspect = res.x() as Float / res.y() as Float;
let aspect_ratio = if aspect > 1.0 { aspect } else { 1.0 / aspect };
let a = 4.0 * square(opposite) * aspect_ratio;
let screen_bounds = if aspect > 1.0 {
Bounds2f::from_points(Point2f::new(-aspect, -1.0), Point2f::new(aspect, 1.0))
} else {
Bounds2f::from_points(
Point2f::new(-1.0, -1.0 / aspect),
Point2f::new(1.0, 1.0 / aspect),
)
};
let hither = 1e-3;
let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap();
let light_from_screen = screen_from_light.inverse();
let dwda = |p: Point2f| {
let w = Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.0)));
cos_theta(w.normalize()).powi(3)
};
let d = image.get_sampling_distribution(dwda, screen_bounds);
let distrib = PiecewiseConstant2D::from_slice(
d.as_slice(),
d.x_size() as usize,
d.y_size() as usize,
screen_bounds,
);
let specific = ProjectionLight {
base,
image: image.upload(arena),
image_color_space: colorspace.upload(arena),
distrib: distrib.upload(arena),
screen_bounds,
screen_from_light,
light_from_screen,
scale,
hither,
a,
};
Ok(Light::Projection(specific))
} }
fn compute_screen_bounds(aspect: Float) -> Bounds2f { fn compute_screen_bounds(aspect: Float) -> Bounds2f {

View file

@ -1,4 +1,4 @@
use crate::core::light::{CreateLight, lookup_spectrum}; use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric; use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture; use crate::core::texture::FloatTexture;
use crate::utils::{Arena, FileLoc, ParameterDictionary}; use crate::utils::{Arena, FileLoc, ParameterDictionary};
@ -53,52 +53,53 @@ impl CreateSpotLight for SpotLight {
} }
} }
impl CreateLight for SpotLight { pub fn create(
fn create( render_from_light: Transform,
render_from_light: Transform, medium: Option<Medium>,
medium: Medium, parameters: &ParameterDictionary,
parameters: &ParameterDictionary, _loc: &FileLoc,
_loc: &FileLoc, _shape: &Shape,
_shape: &Shape, _alpha_tex: &FloatTexture,
_alpha_tex: &FloatTexture, colorspace: Option<&RGBColorSpace>,
colorspace: Option<&RGBColorSpace>, arena: &Arena,
arena: &Arena, ) -> Result<Light> {
) -> Result<Light> { let i = parameters
let i = parameters .get_one_spectrum(
.get_one_spectrum( "I",
"I", Some(Spectrum::Dense(colorspace.unwrap().illuminant)),
Some(Spectrum::Dense(colorspace.unwrap().illuminant)), SpectrumType::Illuminant,
SpectrumType::Illuminant, )
) .expect("No spectrum");
.expect("No spectrum"); let mut scale = parameters.get_one_float("scale", 1.)?;
let mut scale = parameters.get_one_float("scale", 1.)?; let coneangle = parameters.get_one_float("coneangle", 30.)?;
let coneangle = parameters.get_one_float("coneangle", 30.)?; let conedelta = parameters.get_one_float("conedelta", 5.)?;
let conedelta = parameters.get_one_float("conedelta", 5.)?; let from = parameters.get_one_point3f("from", Point3f::zero())?;
let from = parameters.get_one_point3f("from", Point3f::zero())?; let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?;
let to = parameters.get_one_point3f("to", Point3f::new(0., 0., 1.))?; let dir_to_z = Transform::from(Frame::from_z((to - from).normalize()));
let dir_to_z = Transform::from(Frame::from_z((to - from).normalize())); let t = Transform::translate(from.into()) * dir_to_z.inverse();
let t = Transform::translate(from.into()) * dir_to_z.inverse(); let final_render = render_from_light * t;
let final_render = render_from_light * t; scale /= spectrum_to_photometric(i);
scale /= spectrum_to_photometric(i);
let phi_v = parameters.get_one_float("power", -1.)?; let phi_v = parameters.get_one_float("power", -1.)?;
if phi_v > 0. { if phi_v > 0. {
let cos_falloff_end = radians(coneangle).cos(); let cos_falloff_end = radians(coneangle).cos();
let cos_falloff_start = radians(coneangle - conedelta).cos(); let cos_falloff_start = radians(coneangle - conedelta).cos();
let k_e = let k_e = 2. * PI * ((1. - cos_falloff_start) + (cos_falloff_start - cos_falloff_end) / 2.);
2. * PI * ((1. - cos_falloff_start) + (cos_falloff_start - cos_falloff_end) / 2.); scale *= phi_v / k_e;
scale *= phi_v / k_e;
}
let specific = SpotLight::new(
final_render,
medium.into(),
i,
scale,
coneangle,
coneangle - conedelta,
);
arena.alloc(specific);
Ok(Light::Spot(specific))
} }
let mi = match medium {
Some(m) => {
let ptr = arena.alloc(m);
MediumInterface {
inside: ptr,
outside: ptr,
}
}
None => MediumInterface::default(),
};
let specific = SpotLight::new(final_render, mi, i, scale, coneangle, coneangle - conedelta);
arena.alloc(specific);
Ok(Light::Spot(specific))
} }

View file

@ -0,0 +1 @@

View file

@ -45,11 +45,7 @@ impl CreateSpectrumTexture for SpectrumBilerpTexture {
} }
impl SpectrumTextureTrait for SpectrumBilerpTexture { impl SpectrumTextureTrait for SpectrumBilerpTexture {
fn evaluate( fn evaluate(&self, _ctx: &TextureEvalContext, _lambda: &SampledWavelengths) -> SampledSpectrum {
&self,
_ctx: &TextureEvalContext,
_lambda: &SampledWavelengths,
) -> SampledSpectrum {
todo!() todo!()
} }
} }

View file

@ -0,0 +1 @@

View file

@ -3,8 +3,7 @@ use crate::core::texture::{FloatTexture, SpectrumTexture};
use crate::spectra::data::get_named_spectrum; use crate::spectra::data::get_named_spectrum;
use crate::spectra::piecewise::PiecewiseLinearSpectrumBuffer; use crate::spectra::piecewise::PiecewiseLinearSpectrumBuffer;
use crate::utils::FileLoc; use crate::utils::FileLoc;
use anyhow::{Result, bail}; use anyhow::{bail, Result};
use shared::Float;
use shared::core::color::RGB; use shared::core::color::RGB;
use shared::core::geometry::{Normal3f, Point2f, Point3f, Vector2f, Vector3f}; use shared::core::geometry::{Normal3f, Point2f, Point3f, Vector2f, Vector3f};
use shared::core::spectrum::Spectrum; use shared::core::spectrum::Spectrum;
@ -13,11 +12,12 @@ use shared::spectra::{
PiecewiseLinearSpectrum, RGBAlbedoSpectrum, RGBColorSpace, RGBIlluminantSpectrum, PiecewiseLinearSpectrum, RGBAlbedoSpectrum, RGBColorSpace, RGBIlluminantSpectrum,
RGBUnboundedSpectrum, RGBUnboundedSpectrum,
}; };
use shared::Float;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{ use std::sync::{
Arc,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc,
}; };
pub fn error_exit(loc: Option<&FileLoc>, message: &str) -> String { pub fn error_exit(loc: Option<&FileLoc>, message: &str) -> String {
@ -350,30 +350,31 @@ impl ParameterDictionary {
where where
T: PBRTParameter, T: PBRTParameter,
{ {
let param = self.params[0].clone(); for param in &self.params {
if param.name == name && param.type_name == T::TYPE_NAME { if param.name == name && param.type_name == T::TYPE_NAME {
let values = T::get_values(&param); let values = T::get_values(param);
if values.is_empty() { if values.is_empty() {
bail!( bail!(
"{}: No values provided for parameter \"{}\".", "{}: No values provided for parameter \"{}\".",
&param.loc, &param.loc,
name name
); );
}
if values.len() != T::N_PER_ITEM {
bail!(
"{}: Expected {} values for parameter \"{}\". Found {}.",
&param.loc,
T::N_PER_ITEM,
name,
values.len()
);
}
param.looked_up.store(true, Ordering::Relaxed);
return Ok(T::convert(values));
} }
if values.len() != T::N_PER_ITEM {
bail!(
"{}: Expected {} values for parameter \"{}\". Found {}.",
&param.loc,
T::N_PER_ITEM,
name,
values.len()
);
}
param.looked_up.store(true, Ordering::Relaxed);
return Ok(T::convert(values));
} }
Ok(default_val) Ok(default_val)

View file

@ -385,6 +385,7 @@ impl Tokenizer {
if first_char == '"' { if first_char == '"' {
self.advance(); self.advance();
let mut val = String::new(); let mut val = String::new();
val.push('"');
loop { loop {
let ch = self.advance().ok_or(ParserError::UnexpectedEof)?; let ch = self.advance().ok_or(ParserError::UnexpectedEof)?;
@ -406,6 +407,8 @@ impl Tokenizer {
val.push(ch); val.push(ch);
} }
} }
val.push('"');
return Ok(Some(Token { return Ok(Some(Token {
text: val, text: val,
loc: start_loc, loc: start_loc,