use crate::Arena; use crate::core::image::{Image, ImageIO}; use crate::core::light::lookup_spectrum; use crate::core::spectrum::spectrum_to_photometric; use crate::spectra::get_spectra_context; use crate::utils::sampling::{PiecewiseConstant2D, WindowedPiecewiseConstant2D}; use crate::utils::{FileLoc, ParameterDictionary, Upload, resolve_filename}; use anyhow::{Result, anyhow}; use rayon::prelude::*; use shared::core::camera::CameraTransform; use shared::core::geometry::{Bounds2f, Frame, Point2f, Point2i, Point3f, VectorLike, cos_theta}; use shared::core::image::{DeviceImage, WrapMode}; use shared::core::light::{Light, LightBase, LightType}; use shared::core::medium::MediumInterface; use shared::core::spectrum::Spectrum; use shared::core::texture::SpectrumType; use shared::lights::{ImageInfiniteLight, PortalInfiniteLight, UniformInfiniteLight}; use shared::spectra::{DenselySampledSpectrum, RGBColorSpace}; use shared::utils::math::{equal_area_sphere_to_square, equal_area_square_to_sphere}; use shared::utils::sampling::{DevicePiecewiseConstant2D, DeviceWindowedPiecewiseConstant2D}; use shared::utils::{Ptr, Transform}; use shared::{Float, PI}; use std::path::Path; pub trait CreateImageInfiniteLight { fn new( render_from_light: Transform, scale: Float, image: Ptr, image_color_space: Ptr, distrib: Ptr, compensated_distrib: Ptr, ) -> Self; } impl CreateImageInfiniteLight for ImageInfiniteLight { fn new( render_from_light: Transform, scale: Float, image: Ptr, image_color_space: Ptr, distrib: Ptr, compensated_distrib: Ptr, ) -> Self { let base = LightBase::new( LightType::Infinite, render_from_light, MediumInterface::default(), ); Self { base, image, image_color_space, scale, distrib, compensated_distrib, scene_center: Point3f::default(), scene_radius: 0.0, } } } pub trait CreatePortalInfiniteLight { fn new( render_from_light: Transform, scale: Float, image: Ptr, image_color_space: Ptr, portal: [Point3f; 4], portal_frame: Frame, distribution: Ptr, ) -> Self; } impl CreatePortalInfiniteLight for PortalInfiniteLight { fn new( render_from_light: Transform, scale: Float, image: Ptr, image_color_space: Ptr, portal: [Point3f; 4], portal_frame: Frame, distribution: Ptr, ) -> Self { let base = LightBase::new( LightType::Infinite, render_from_light, MediumInterface::default(), ); Self { base, image, image_color_space, scale, portal, portal_frame, distribution: *distribution, scene_center: Point3f::default(), scene_radius: 0.0, } } } pub trait CreateUniformInfiniteLight { fn new(render_from_light: Transform, scale: Float, lemit: Ptr) -> Self; } impl CreateUniformInfiniteLight for UniformInfiniteLight { fn new(render_from_light: Transform, scale: Float, lemit: Ptr) -> Self { let base = LightBase::new( LightType::Infinite, render_from_light, MediumInterface::default(), ); Self { base, lemit, scale, scene_center: Point3f::default(), scene_radius: 0.0, } } } pub fn create( arena: &mut Arena, render_from_light: Transform, _medium: MediumInterface, camera_transform: CameraTransform, parameters: &ParameterDictionary, colorspace: Option<&RGBColorSpace>, loc: &FileLoc, ) -> Result { let l = parameters.get_spectrum_array("L", SpectrumType::Illuminant); let mut scale = parameters.get_one_float("scale", 1.0); let portal = parameters.get_point3f_array("portal"); let filename = resolve_filename(¶meters.get_one_string("filename", "")); let e_v = parameters.get_one_float("illuminance", -1.0); let has_spectrum = !l.is_empty(); let has_file = !filename.is_empty(); let has_portal = !portal.is_empty(); if has_spectrum && has_file { return Err(anyhow!(loc, "cannot specify both \"L\" and \"filename\"")); } // Uniform infinite light (no image) if !has_file && !has_portal { let spectrum = if has_spectrum { scale /= spectrum_to_photometric(l[0]); l[0] } else { Spectrum::Dense(colorspace.unwrap().illuminant) }; if e_v > 0.0 { scale *= e_v / PI; } let lemit = lookup_spectrum(&spectrum); let light = UniformInfiniteLight::new(render_from_light, scale, lemit.upload(arena)); return Ok(Light::InfiniteUniform(light)); } // Image-based lights let (image, image_cs) = load_image(&filename, &l, colorspace.unwrap(), loc)?; scale /= spectrum_to_photometric(Spectrum::Dense(image_cs.illuminant)); if e_v > 0.0 { let k_e = compute_hemisphere_illuminance(&image, &image_cs); scale *= e_v / k_e; } if has_portal { create_portal_light( arena, render_from_light, scale, image, image_cs, &portal, camera_transform, loc, ) } else { create_image_light(arena, render_from_light, scale, image, image_cs) } } fn create_image_light( arena: &mut Arena, render_from_light: Transform, scale: Float, image: Image, image_cs: RGBColorSpace, ) -> Result { let res = image.resolution(); assert_eq!( res.x(), res.y(), "Image must be square for equal-area mapping" ); let (n_u, n_v) = (res.x() as usize, res.y() as usize); // Extract luminance data let image_ptr = image.upload(arena); let value = ℑ let mut data: Vec = (0..n_v) .flat_map(|v| { (0..n_u).map(move |u| { value .get_channels(Point2i::new(u as i32, v as i32)) .average() }) }) .collect(); let distrib = PiecewiseConstant2D::from_slice(&data_u, n_u, n_v, Bounds2f::unit()); // Build compensated distribution let average = data.iter().sum::() / data.len() as Float; let mut all_zero = true; for v in &mut data { *v = (*v - average).max(0.0); all_zero &= *v == 0.0; } if all_zero { data.fill(1.0); } let compensated_distrib = PiecewiseConstant2D::from_slice(&data, n_u, n_v, Bounds2f::unit()); let light = ImageInfiniteLight::new( render_from_light, scale, image_ptr, image_cs.upload(arena), distrib.upload(arena), compensated_distrib.upload(arena), ); Ok(Light::InfiniteImage(light)) } fn create_portal_light( arena: &mut Arena, render_from_light: Transform, scale: Float, image: Image, image_cs: RGBColorSpace, portal_points: &[Point3f], camera_transform: CameraTransform, loc: &FileLoc, ) -> Result { let res = image.resolution(); if res.x() != res.y() { return Err(anyhow!(loc, "Portal light image must be square")); } // Validate portal if portal_points.len() != 4 { return Err(anyhow!( loc, "Portal requires exactly 4 vertices, got {}", portal_points.len() )); } let portal: [Point3f; 4] = portal_points .iter() .map(|p| camera_transform.camera_from_world(0.0).apply_to_point(*p)) .collect::>() .try_into() .unwrap(); let portal_frame = validate_and_build_portal_frame(&portal, loc)?; // Remap image through portal let remapped = remap_image_through_portal(&image, &render_from_light, &portal_frame); // Build distribution let duv_dw = |p: Point2f| -> Float { let (_, jacobian) = PortalInfiniteLight::render_from_image(portal_frame, p); jacobian }; let d = remapped.get_sampling_distribution( duv_dw, Bounds2f::from_points(Point2f::zero(), Point2f::fill(1.)), ); let distribution = WindowedPiecewiseConstant2D::new(d); let light = PortalInfiniteLight::new( render_from_light, scale, remapped.upload(arena), image_cs.upload(arena), portal, portal_frame, distribution.upload(arena), ); Ok(Light::InfinitePortal(light)) } fn validate_and_build_portal_frame(portal: &[Point3f; 4], loc: &FileLoc) -> Result { let p01 = (portal[1] - portal[0]).normalize(); let p12 = (portal[2] - portal[1]).normalize(); let p32 = (portal[2] - portal[3]).normalize(); let p03 = (portal[3] - portal[0]).normalize(); if (p01.dot(p32) - 1.0).abs() > 0.001 || (p12.dot(p03) - 1.0).abs() > 0.001 { return Err(anyhow!(loc, "Portal edges not parallel")); } if p01.dot(p12).abs() > 0.001 || p12.dot(p32).abs() > 0.001 || p32.dot(p03).abs() > 0.001 || p03.dot(p01).abs() > 0.001 { return Err(anyhow!(loc, "Portal edges not perpendicular")); } Ok(Frame::from_xy(p03, p01)) } fn remap_image_through_portal( image: &Image, render_from_light: &Transform, portal_frame: &Frame, ) -> Image { let res = image.resolution(); let (width, height) = (res.x() as usize, res.y() as usize); let mut pixels = vec![0.0f32; width * height * 3]; pixels .par_chunks_mut(width * 3) .enumerate() .for_each(|(y, row)| { for x in 0..width { let uv = Point2f::new( (x as Float + 0.5) / width as Float, (y as Float + 0.5) / height as Float, ); let (w_world, _) = PortalInfiniteLight::render_from_image(*portal_frame, uv); let w_local = render_from_light.apply_inverse_vector(w_world).normalize(); let uv_equi = equal_area_sphere_to_square(w_local); for c in 0..3 { row[x * 3 + c] = image.bilerp_channel_with_wrap( uv_equi, c as i32, WrapMode::OctahedralSphere.into(), ); } } }); Image::from_f32(pixels, res, &["R", "G", "B"]) } fn load_image( filename: &str, l: &[Spectrum], colorspace: &RGBColorSpace, loc: &FileLoc, ) -> Result<(Image, RGBColorSpace)> { if filename.is_empty() { let stdspec = get_spectra_context(); let rgb = l[0].to_rgb(colorspace, &stdspec); let image = Image::new_constant(Point2i::new(1, 1), &["R", "G", "B"], &[rgb.r, rgb.g, rgb.b]); return Ok((image, colorspace.clone())); } let im = Image::read(Path::new(filename), None) .map_err(|e| anyhow!(loc, "failed to load '{}': {}", filename, e))?; if im.image.has_any_infinite_pixels() || im.image.has_any_nan_pixels() { return Err(anyhow!(loc, "image '{}' has invalid pixels", filename)); } let desc = im .image .get_channel_desc(&["R", "G", "B"]) .map_err(|_| anyhow!(loc, "image '{}' must have R, G, B channels", filename))?; let cs = im.metadata.colorspace.unwrap_or_else(|| colorspace.clone()); Ok((im.image.select_channels(&desc), cs)) } fn compute_hemisphere_illuminance(image: &Image, cs: &RGBColorSpace) -> Float { let lum = cs.luminance_vector(); let res = image.resolution(); let sum: Float = (0..res.y()) .flat_map(|y| (0..res.x()).map(move |x| (x, y))) .filter_map(|(x, y)| { let u = (x as Float + 0.5) / res.x() as Float; let v = (y as Float + 0.5) / res.y() as Float; let w = equal_area_square_to_sphere(Point2f::new(u, v)); if w.z() <= 0.0 { return None; } let p = Point2i::new(x, y); let r = image.get_channel(p, 0); let g = image.get_channel(p, 1); let b = image.get_channel(p, 2); Some((r * lum[0] + g * lum[1] + b * lum[2]) * cos_theta(w)) }) .sum(); sum * 2.0 * PI / (res.x() * res.y()) as Float }