use super::{ DenselySampledSpectrum, LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType, RGBIlluminantSpectrum, SampledSpectrum, SampledWavelengths, Spectrum, SpectrumTrait, }; use crate::core::interaction::{ Interaction, InteractionTrait, SimpleInteraction, SurfaceInteraction, }; use crate::core::medium::MediumInterface; use crate::core::pbrt::{Float, PI}; use crate::core::texture::{ FloatTexture, FloatTextureTrait, TextureEvalContext, TextureEvaluator, UniversalTextureEvaluator, }; use crate::geometry::{ Bounds3f, Normal3f, Point2f, Point2fi, Point2i, Point3f, Point3fi, Ray, Vector3f, VectorLike, }; use crate::image::Image; use crate::shapes::{Shape, ShapeSample, ShapeSampleContext, ShapeTrait}; use crate::utils::color::RGB; use crate::utils::colorspace::RGBColorSpace; use crate::utils::hash::hash_float; use crate::utils::transform::Transform; use std::sync::Arc; #[derive(Clone, Debug)] pub struct DiffuseAreaLight { base: LightBase, shape: Shape, alpha: Option, area: Float, two_sided: bool, lemit: Arc, scale: Float, image: Option, image_color_space: Option>, } impl DiffuseAreaLight { #[allow(clippy::too_many_arguments)] pub fn new( render_from_light: Transform, medium_interface: MediumInterface, le: Spectrum, scale: Float, shape: Shape, alpha: FloatTexture, image: Option, image_color_space: Option>, two_sided: bool, ) -> Self { let is_constant_zero = match &alpha { FloatTexture::FloatConstant(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 base = LightBase::new(light_type, &render_from_light, &medium_interface); let lemit = LightBase::lookup_spectrum(&le); if let Some(im) = &image { let desc = im .get_channel_desc(&["R", "G", "B"]) .expect("Image used for DiffuseAreaLight doesn't have R, G, B channels"); assert_eq!(3, desc.size(), "Image channel description size mismatch"); assert!( desc.is_identity(), "Image channel description is not 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! \ The system has numerous assumptions, implicit and explicit, \ that this transform will have no scale factors in it. \ Proceed at your own risk; your image may have errors." ); } Self { base, area: shape.area(), shape, alpha: stored_alpha, two_sided, lemit, scale, image, image_color_space, } } fn alpha_masked(&self, intr: &Interaction) -> bool { let Some(alpha_tex) = &self.alpha else { return false; }; let ctx = TextureEvalContext::from(intr); let a = UniversalTextureEvaluator.evaluate_float(alpha_tex, &ctx); if a >= 1.0 { return false; } if a <= 0.0 { return true; } hash_float(&intr.p()) > a } } impl LightTrait for DiffuseAreaLight { fn base(&self) -> &LightBase { &self.base } fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum { let mut l = SampledSpectrum::new(0.); if let Some(image) = &self.image { for y in 0..image.resolution().y() { for x in 0..image.resolution().x() { let mut rgb = RGB::default(); for c in 0..3 { rgb[c] = image.get_channel(Point2i::new(x, y), c); } l += RGBIlluminantSpectrum::new( self.image_color_space.as_ref().unwrap(), RGB::clamp_zero(rgb), ) .sample(&lambda); } } l *= self.scale / (image.resolution().x() * image.resolution().y()) as Float; } else { l = self.lemit.sample(&lambda) * self.scale; } let two_side = if self.two_sided { 2. } else { 1. }; PI * two_side * self.area * l } fn sample_li( &self, ctx: &LightSampleContext, u: Point2f, lambda: &SampledWavelengths, _allow_incomplete_pdf: bool, ) -> Option { let shape_ctx = ShapeSampleContext::new(ctx.pi, ctx.n, ctx.ns, 0.0); let ss = self.shape.sample_from_context(&shape_ctx, u)?; let mut intr: SurfaceInteraction = ss.intr.as_ref().clone(); intr.common.medium_interface = Some(self.base.medium_interface.clone()); let p = intr.p(); let n = intr.n(); let uv = intr.uv; let generic_intr = Interaction::Surface(intr); if self.alpha_masked(&generic_intr) { return None; } let wi = (p - ctx.p()).normalize(); let le = self.l(p, n, uv, -wi, lambda); if le.is_black() { return None; } Some(LightLiSample::new(le, wi, ss.pdf, generic_intr)) } fn pdf_li(&self, ctx: &LightSampleContext, wi: Vector3f, _allow_incomplete_pdf: bool) -> Float { let shape_ctx = ShapeSampleContext::new(ctx.pi, ctx.n, ctx.ns, 0.); self.shape.pdf_from_context(&shape_ctx, wi) } fn l( &self, p: Point3f, n: Normal3f, mut uv: Point2f, w: Vector3f, lambda: &SampledWavelengths, ) -> SampledSpectrum { if self.two_sided && n.dot(w.into()) < 0. { return SampledSpectrum::new(0.); } let intr = Interaction::Surface(SurfaceInteraction::new_minimal( Point3fi::new_from_point(p), uv, )); if self.alpha_masked(&intr) { return SampledSpectrum::new(0.); } if let Some(image) = &self.image { let mut rgb = RGB::default(); uv[1] = 1. - uv[1]; for c in 0..3 { rgb[c] = image.bilerp_channel(uv, c); } let spec = RGBIlluminantSpectrum::new( self.image_color_space.as_ref().unwrap(), RGB::clamp_zero(rgb), ); self.scale * spec.sample(lambda) } else { self.scale * self.lemit.sample(lambda) } } fn le(&self, _ray: &Ray, _lambda: &SampledWavelengths) -> SampledSpectrum { todo!() } fn preprocess(&mut self, _scene_bounds: &Bounds3f) { unimplemented!() } fn bounds(&self) -> Option { let mut phi = 0.; if let Some(image) = &self.image { for y in 0..image.resolution.y() { for x in 0..image.resolution.x() { for c in 0..3 { phi += image.get_channel(Point2i::new(x, y), c); } } } } else { phi = self.lemit.max_value(); } let nb = self.shape.normal_bounds(); Some(LightBounds::new( &self.shape.bounds(), nb.w, phi, nb.cos_theta, (PI / 2.).cos(), self.two_sided, )) } }