pbrt/src/lights/projection.rs
2026-01-18 16:29:27 +00:00

201 lines
6.6 KiB
Rust

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>,
image_color_space: Ptr<RGBColorSpace>,
fov: Float,
) -> Self;
}
impl CreateProjectionLight for ProjectionLight {
fn new(
render_from_light: Transform,
medium_interface: MediumInterface,
le: Spectrum,
scale: Float,
image: Ptr<Image>,
image_color_space: Ptr<RGBColorSpace>,
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<Light, Error> {
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
}