use crate::core::image::{Image, ImageIO}; use crate::core::light::CreateLight; use crate::core::spectrum::spectrum_to_photometric; use crate::spectra::colorspace::new; use crate::utils::{Arena, ParameterDictionary, Upload, resolve_filename}; use log::error; use shared::Float; use shared::core::geometry::{Bounds2f, VectorLike}; use shared::core::image::ImageAccess; use shared::core::light::{Light, LightBase}; use shared::core::medium::MediumInterface; use shared::core::spectrum::Spectrum; use shared::lights::ProjectionLight; use shared::spectra::RGBColorSpace; use shared::utils::math::{radians, square}; use shared::utils::{Ptr, Transform}; pub trait CreateProjectionLight { fn new( render_from_light: Transform, medium_interface: MediumInterface, le: Spectrum, scale: Float, image: Ptr, image_color_space: Ptr, fov: Float, ) -> Self; } impl CreateProjectionLight for ProjectionLight { fn new( render_from_light: Transform, medium_interface: MediumInterface, le: Spectrum, scale: Float, image: Ptr, image_color_space: Ptr, fov: Float, ) -> Self { let base = LightBase::new( LightType::DeltaPosition, render_from_light, medium_interface, ); let aspect = image.resolution().x() as Float / image.resolution().y() as Float; let screen_bounds = if aspect > 1. { Bounds2f::from_points(Point2f::new(-aspect, -1.), Point2f::new(aspect, 1.)) } else { Bounds2f::from_points( Point2f::new(-1., 1. / aspect), Point2f::new(1., 1. / aspect), ) }; let hither = 1e-3; let screen_from_light = Transform::perspective(fov.unwrap(), hither, 1e30).unwrap(); let light_from_screen = screen_from_light.inverse(); let opposite = (radians(fov.unwrap()) / 2.).tan(); let aspect_ratio = if aspect > 1. { aspect } else { 1. / aspect }; let a = 4. * square(opposite) * aspect_ratio; let dwda = |p: Point2f| { let w = Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.))); cos_theta(w.normalize()).powi(3) }; let d = image.get_sampling_distribution(dwda, screen_bounds); let distrib = PiecewiseConstant2D::new_with_bounds(&d, screen_bounds); Self { base, image, image_color_space, distrib, screen_bounds, screen_from_light, light_from_screen, scale, hither, a, } } } impl CreateLight for ProjectionLight { fn create( arena: &mut Arena, render_from_light: Transform, medium: Medium, parameters: &ParameterDictionary, loc: &FileLoc, _shape: &Shape, _alpha_text: &FloatTexture, colorspace: Option<&RGBColorSpace>, ) -> Result { let mut scale = parameters.get_one_float("scale", 1.); let power = parameters.get_one_float("power", -1.); let fov = parameters.get_one_float("fov", 90.); let filename = resolve_filename(parameters.get_one_string("filename", "")); if filename.is_empty() { return Err(error!(loc, "must provide filename for projection light")); } let im = Image::read(&filename, None) .map_err(|e| error!(loc, "could not load image '{}': {}", filename, e))?; if im.image.has_any_infinite_pixels() { return Err(error!( loc, "image '{}' has infinite pixels, not suitable for light", filename )); } if im.image.has_any_nan_pixels() { return Err(error!( loc, "image '{}' has NaN pixels, not suitable for light", filename )); } let channel_desc = im .image .get_channel_desc(&["R", "G", "B"]) .map_err(|_| error!(loc, "image '{}' must have R, G, B channels", filename))?; let image = im.image.select_channels(&channel_desc); let colorspace = im .metadata .colorspace .ok_or_else(|| error!(loc, "image '{}' missing colorspace metadata", filename))?; scale /= spectrum_to_photometric(colorspace.illuminant); if power > 0. { let k_e = compute_emissive_power(&image, colorspace, fov); } let flip = Transform::scale(1., -1., 1.); let render_from_light_flip = render_from_light * flip; let specific = ProjectionLight::new( render_from_light_flip, medium_interface, le, scale, image.upload(arena), colorspace.upload(arena), fov, ); Ok(Light::Projection(specific)) } } fn compute_screen_bounds(aspect: Float) -> Bounds2f { 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), ) } } fn compute_emissive_power(image: &Image, colorspace: &RGBColorSpace, fov: Float) -> Float { let res = image.resolution(); let aspect = res.x() as Float / res.y() as Float; let screen_bounds = compute_screen_bounds(aspect); let hither = 1e-3; let screen_from_light = Transform::perspective(fov, hither, 1e30).expect("Failed to create perspective transform"); let light_from_screen = screen_from_light.inverse(); let opposite = (radians(fov) / 2.0).tan(); let aspect_factor = if aspect > 1.0 { aspect } else { 1.0 / aspect }; let a = 4.0 * square(opposite) * aspect_factor; let luminance = colorspace.luminance_vector(); let mut sum: Float = 0.0; for y in 0..res.y() { for x in 0..res.x() { let lerp_factor = Point2f::new( (x as Float + 0.5) / res.x() as Float, (y as Float + 0.5) / res.y() as Float, ); let ps = screen_bounds.lerp(lerp_factor); let w_point = light_from_screen.apply_to_point(Point3f::new(ps.x(), ps.y(), 0.0)); let w = Vector3f::from(w_point).normalize(); let dwda = w.z().powi(3); for c in 0..3 { sum += image.get_channel(Point2i::new(x, y), c) * luminance[c] * dwda; } } } a * sum / (res.x() * res.y()) as Float }