219 lines
7.3 KiB
Rust
219 lines
7.3 KiB
Rust
use crate::core::image::{Image, ImageIO};
|
|
use crate::core::light::CreateLight;
|
|
use crate::core::spectrum::spectrum_to_photometric;
|
|
use crate::core::texture::FloatTexture;
|
|
use crate::utils::sampling::PiecewiseConstant2D;
|
|
use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename};
|
|
use anyhow::{Result, anyhow};
|
|
use shared::Float;
|
|
use shared::core::geometry::{
|
|
Bounds2f, Point2f, Point2i, Point3f, Vector3f, VectorLike, cos_theta,
|
|
};
|
|
use shared::core::image::DeviceImage;
|
|
use shared::core::light::{Light, LightBase, LightType};
|
|
use shared::core::medium::{Medium, MediumInterface};
|
|
use shared::core::shape::Shape;
|
|
use shared::core::spectrum::Spectrum;
|
|
use shared::lights::ProjectionLight;
|
|
use shared::spectra::RGBColorSpace;
|
|
use shared::utils::math::{radians, square};
|
|
use shared::utils::sampling::DeviceWindowedPiecewiseConstant2D;
|
|
use shared::utils::{Ptr, Transform};
|
|
use std::path::Path;
|
|
|
|
pub trait CreateProjectionLight {
|
|
fn new(
|
|
render_from_light: Transform,
|
|
medium_interface: MediumInterface,
|
|
scale: Float,
|
|
image: Ptr<DeviceImage>,
|
|
image_color_space: Ptr<RGBColorSpace>,
|
|
distrib: Ptr<DeviceWindowedPiecewiseConstant2D>,
|
|
fov: Float,
|
|
) -> Self;
|
|
}
|
|
|
|
impl CreateProjectionLight for ProjectionLight {
|
|
fn new(
|
|
render_from_light: Transform,
|
|
medium_interface: MediumInterface,
|
|
scale: Float,
|
|
image: Ptr<DeviceImage>,
|
|
image_color_space: Ptr<RGBColorSpace>,
|
|
distrib: Ptr<DeviceWindowedPiecewiseConstant2D>,
|
|
fov: Float,
|
|
) -> Self {
|
|
let base = LightBase::new(
|
|
LightType::DeltaPosition,
|
|
render_from_light,
|
|
medium_interface,
|
|
);
|
|
|
|
let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap();
|
|
let light_from_screen = screen_from_light.inverse();
|
|
let hither = 1e-3;
|
|
let res = image.resolution();
|
|
let aspect = res.x() as Float / res.y() as Float;
|
|
let opposite = (radians(fov) / 2.0).tan();
|
|
let aspect_ratio = if aspect > 1.0 { aspect } else { 1.0 / aspect };
|
|
let a = 4.0 * square(opposite) * aspect_ratio;
|
|
|
|
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> {
|
|
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(¶meters.get_one_string("filename", ""));
|
|
if filename.is_empty() {
|
|
return Err(anyhow!(loc, "must provide filename for projection light"));
|
|
}
|
|
|
|
let im = Image::read(Path::new(&filename), None)
|
|
.map_err(|e| anyhow!(loc, "could not load image '{}': {}", filename, e))?;
|
|
|
|
if im.image.has_any_infinite_pixels() {
|
|
return Err(anyhow!(
|
|
loc,
|
|
"image '{}' has infinite pixels, not suitable for light",
|
|
filename
|
|
));
|
|
}
|
|
|
|
if im.image.has_any_nan_pixels() {
|
|
return Err(anyhow!(
|
|
loc,
|
|
"image '{}' has NaN pixels, not suitable for light",
|
|
filename
|
|
));
|
|
}
|
|
|
|
let channel_desc = im
|
|
.image
|
|
.get_channel_desc(&["R", "G", "B"])
|
|
.map_err(|_| anyhow!(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(|| anyhow!(loc, "image '{}' missing colorspace metadata", filename))?;
|
|
|
|
scale /= spectrum_to_photometric(Spectrum::Dense(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 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 res = image.resolution();
|
|
let aspect = res.x() as Float / res.y() as Float;
|
|
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 d = image.get_sampling_distribution(dwda, screen_bounds);
|
|
let distrib =
|
|
PiecewiseConstant2D::new(d.as_slice(), d.x_size() as usize, d.y_size() as usize);
|
|
|
|
let specific = ProjectionLight::new(
|
|
render_from_light_flip,
|
|
medium.into(),
|
|
scale,
|
|
image.upload(arena),
|
|
colorspace.upload(arena),
|
|
distrib.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
|
|
}
|