mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 22:20:00 -06:00
Merge pull request #2118 from ChristopherLinnett/fix/issue-2092-camera-focus-crash
fix: bound Classic 3D depth-of-field blur and reject non-finite focus distances (#2092)
This commit is contained in:
@@ -13,10 +13,15 @@ const W: u32 = 200;
|
||||
const H: u32 = 100;
|
||||
|
||||
fn setup() -> (Project, ItemId, Comp) {
|
||||
setup_sized(W, H)
|
||||
}
|
||||
|
||||
/// [`setup`] with a `w`×`h` comp.
|
||||
fn setup_sized(w: u32, h: u32) -> (Project, ItemId, Comp) {
|
||||
let mut p = Project::default();
|
||||
// Exact float maths; 8/16 bpc quantisation has its own tests (tests_color).
|
||||
p.settings.bit_depth = effectcraft_project::BitDepth::Bpc32;
|
||||
let comp = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let comp = Comp::new(w, h, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
(p, cid, comp)
|
||||
}
|
||||
@@ -374,7 +379,7 @@ fn shadow_diffusion_is_a_soft_ramp_not_copies() {
|
||||
// ---------------------------------------------------------------- cameras
|
||||
|
||||
fn camera(p: &mut Project, comp: &Comp) -> Layer {
|
||||
build::layer(p, comp, "Camera", LayerSource::Camera, (W, H), None)
|
||||
build::layer(p, comp, "Camera", LayerSource::Camera, (comp.width, comp.height), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -450,9 +455,155 @@ fn depth_of_field_blurs_out_of_focus_layers() {
|
||||
assert!(b.get(66, 50)[3] > 0.01);
|
||||
}
|
||||
|
||||
/// A non-finite Focus Distance doesn't reach the blur: the camera focuses on the property's
|
||||
/// default plane (`zoom`) instead.
|
||||
#[test]
|
||||
fn dof_falls_back_when_focus_distance_is_not_finite() {
|
||||
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut cam = camera(&mut p, &comp);
|
||||
set(&mut cam, "cameraOptions/dof", Value::Bool(true));
|
||||
set(&mut cam, "cameraOptions/aperture", Value::Scalar(60.0));
|
||||
set(&mut cam, "cameraOptions/focusDistance", Value::Scalar(bad));
|
||||
let s = solid3(&mut p, &comp, [1.0, 1.0, 1.0], 60, 60, [100.0, 50.0, 0.0]);
|
||||
push(&mut p, cid, cam);
|
||||
push(&mut p, cid, s);
|
||||
let cp = p.comp(cid).unwrap();
|
||||
let ctx = crate::EvalCtx::new(&p, cid, cp, Tick::ZERO);
|
||||
let cs = crate::three_d::camera::layer_camera(&ctx, &cp.layers[0]);
|
||||
let f = cs.dof.unwrap().focus;
|
||||
assert!((f - zoom()).abs() < 1e-9, "{bad}: evaluated to {f}, expected the fallback to zoom {}", zoom());
|
||||
// The z = 0 plane is in focus: hard edge, finite pixels.
|
||||
let img = render(&p, cid);
|
||||
assert!(img.get(71, 50)[3] > 0.99 && img.get(69, 50)[3] < 0.01, "{bad}: the edge should be hard");
|
||||
assert!(img.data.iter().all(|px| px.iter().all(|c| c.is_finite())), "{bad}: non-finite pixels");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Classic 3D blur stays bounded for degenerate inputs: a 0%-scaled 3D layer off the focal
|
||||
/// plane (e.g. the first frame of a scale-in) or a huge but finite Focus Distance asks for a
|
||||
/// radius of billions of buffer pixels, whose padded buffer can't be allocated.
|
||||
#[test]
|
||||
fn dof_blur_radius_is_bounded_for_degenerate_inputs() {
|
||||
let scene = |focus: f64, scale: f64| {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut cam = camera(&mut p, &comp);
|
||||
set(&mut cam, "cameraOptions/dof", Value::Bool(true));
|
||||
set(&mut cam, "cameraOptions/aperture", Value::Scalar(60.0));
|
||||
set(&mut cam, "cameraOptions/focusDistance", Value::Scalar(focus));
|
||||
let mut s = solid3(&mut p, &comp, [1.0, 1.0, 1.0], 60, 60, [100.0, 50.0, 0.0]);
|
||||
set(&mut s, "transform/scale", Value::Vec3([scale, scale, 100.0]));
|
||||
push(&mut p, cid, cam);
|
||||
push(&mut p, cid, s);
|
||||
render(&p, cid)
|
||||
};
|
||||
let finite = |img: &effectcraft_raster::Image| img.data.iter().all(|px| px.iter().all(|c| c.is_finite()));
|
||||
// A 0%-scaled layer 10 px off the focal plane: an ordinary defocus times 1/sx (sx = 1e-9).
|
||||
assert!(finite(&scene(zoom() + 10.0, 0.0)), "zero-scaled: non-finite pixels");
|
||||
// Huge but finite Focus Distances (typed in, or a large expression result): the blur is as
|
||||
// large as the buffer budget allows, never refused (which would render the layer sharp).
|
||||
// 1e307 overflows the circle of confusion itself to infinity: still the largest blur.
|
||||
for focus in [1.0e12, 1.0e307] {
|
||||
let img = scene(focus, 100.0);
|
||||
assert!(finite(&img), "focus {focus:e}: non-finite pixels");
|
||||
assert!(img.get(100, 50)[3] < 0.5, "focus {focus:e}: the layer should be spread out, not sharp ({})", img.get(100, 50)[3]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The blur radius map of a plane: a 0%-scaled plane is degenerate on screen and isn't blurred;
|
||||
/// any other radius stays within the buffer budget's cap.
|
||||
#[test]
|
||||
fn dof_radii_skip_degenerate_planes_and_respect_the_cap() {
|
||||
let radii = |focus: f64, scale: f64| {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut cam = camera(&mut p, &comp);
|
||||
set(&mut cam, "cameraOptions/dof", Value::Bool(true));
|
||||
set(&mut cam, "cameraOptions/aperture", Value::Scalar(60.0));
|
||||
set(&mut cam, "cameraOptions/focusDistance", Value::Scalar(focus));
|
||||
let mut s = solid3(&mut p, &comp, [1.0, 1.0, 1.0], 60, 60, [100.0, 50.0, 0.0]);
|
||||
set(&mut s, "transform/scale", Value::Vec3([scale, scale, 100.0]));
|
||||
push(&mut p, cid, cam);
|
||||
push(&mut p, cid, s);
|
||||
let cp = p.comp(cid).unwrap();
|
||||
let ctx = crate::EvalCtx::new(&p, cid, cp, Tick::ZERO);
|
||||
let cs = super::camera::layer_camera(&ctx, &cp.layers[0]);
|
||||
let dof = cs.dof.unwrap();
|
||||
let cap = super::compose::dof_radius_cap(&dof, 60, 60);
|
||||
(super::compose::dof_radii_at(&cs, &dof, ctx.world_matrix(&cp.layers[1]), (60, 60), [0.0; 2], 1.0, cap), cap)
|
||||
};
|
||||
let (r, _) = radii(zoom() + 10.0, 0.0);
|
||||
assert!(r.iter().all(|&r| r == 0.0), "a 0%-scaled plane is not blurred");
|
||||
let (r, cap) = radii(1.0e12, 100.0);
|
||||
assert!(cap > 1000.0, "a 60×60 buffer may blur by well over 1000 px: {cap}");
|
||||
assert!(r.iter().all(|&r| r as f64 <= cap && r as f64 > cap - 1.0), "huge focus: every radius at the cap {cap}");
|
||||
}
|
||||
|
||||
/// Two layers that look the same on screen (400×100 at 100%, 1600×400 at 25%) at the same depth
|
||||
/// blur by the same amount on screen: the radius bound must not depend on the layer's scale.
|
||||
#[test]
|
||||
fn dof_blur_is_independent_of_layer_scale() {
|
||||
let ramp = |w: u32, h: u32, scale: f64| {
|
||||
let (mut p, cid, comp) = setup_sized(800, 400);
|
||||
let zoom = 800.0 * 50.0 / 36.0;
|
||||
let mut cam = camera(&mut p, &comp);
|
||||
set(&mut cam, "cameraOptions/dof", Value::Bool(true));
|
||||
set(&mut cam, "cameraOptions/aperture", Value::Scalar(200.0));
|
||||
// coc = aperture·|z − f| / z: a 40 px comp radius on the z = 0 plane (depth = zoom).
|
||||
set(&mut cam, "cameraOptions/focusDistance", Value::Scalar(zoom + 2.0 * 40.0 * zoom / 200.0));
|
||||
let mut s = solid3(&mut p, &comp, [1.0, 1.0, 1.0], w, h, [400.0, 200.0, 0.0]);
|
||||
set(&mut s, "transform/scale", Value::Vec3([scale, scale, 100.0]));
|
||||
push(&mut p, cid, cam);
|
||||
push(&mut p, cid, s);
|
||||
let img = render(&p, cid);
|
||||
soft_count(&img, (0..400).map(|x| (x, 200)))
|
||||
};
|
||||
let (full, quarter) = (ramp(400, 100, 100.0), ramp(1600, 400, 25.0));
|
||||
assert!(full > 60, "the out-of-focus edge should be soft: {full}");
|
||||
assert!(full.abs_diff(quarter) <= 2, "same on-screen layer, same depth: {full} px at 100% vs {quarter} px at 25%");
|
||||
}
|
||||
|
||||
/// The same frame at Full and Half resolution shows the same blur (in comp pixels). The radius
|
||||
/// (~100 px) is large so that any bound that depends on the resolution would show.
|
||||
#[test]
|
||||
fn dof_blur_matches_across_render_resolutions() {
|
||||
let (mut p, cid, comp) = setup_sized(800, 400);
|
||||
let zoom = 800.0 * 50.0 / 36.0;
|
||||
let mut cam = camera(&mut p, &comp);
|
||||
set(&mut cam, "cameraOptions/dof", Value::Bool(true));
|
||||
set(&mut cam, "cameraOptions/aperture", Value::Scalar(200.0));
|
||||
set(&mut cam, "cameraOptions/focusDistance", Value::Scalar(2.0 * zoom));
|
||||
let s = solid3(&mut p, &comp, [1.0, 1.0, 1.0], 400, 100, [400.0, 200.0, 0.0]);
|
||||
push(&mut p, cid, cam);
|
||||
push(&mut p, cid, s);
|
||||
let ramp = |scale: f64| {
|
||||
let img = render_frame(&p, cid, Tick::ZERO, scale);
|
||||
let (w, h) = (img.width as i64, img.height as i64);
|
||||
soft_count(&img, (0..w / 2).map(|x| (x, h / 2))) as f64 / scale
|
||||
};
|
||||
let (full, half) = (ramp(1.0), ramp(0.5));
|
||||
assert!(full > 150.0, "a ~100 px radius must not be clamped: {full} comp px");
|
||||
assert!((full - half).abs() <= 4.0, "Full {full} vs Half {half} comp px");
|
||||
}
|
||||
|
||||
/// `Dof::coc` treats a non-finite focus or depth as in focus.
|
||||
#[test]
|
||||
fn dof_coc_is_zero_for_non_finite_inputs() {
|
||||
let d = |focus: f64| super::camera::Dof { focus, aperture: 50.0, blur_level: 1.0, iris: Default::default(), highlight: Default::default() };
|
||||
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
assert_eq!(d(bad).coc(500.0), 0.0, "focus {bad}");
|
||||
assert_eq!(d(400.0).coc(bad), 0.0, "depth {bad}");
|
||||
}
|
||||
assert!(d(400.0).coc(500.0) > 0.0);
|
||||
}
|
||||
|
||||
/// Pixels among `pts` whose alpha is strictly between 0.05 and 0.95.
|
||||
fn soft_count(img: &effectcraft_raster::Image, pts: impl Iterator<Item = (i64, i64)>) -> usize {
|
||||
pts.filter(|&(x, y)| (0.05..0.95).contains(&img.get(x, y)[3])).count()
|
||||
}
|
||||
|
||||
/// Pixels in column `x` whose alpha is strictly between 0.05 and 0.95.
|
||||
fn soft_px(img: &effectcraft_raster::Image, x: i64) -> usize {
|
||||
(0..img.height as i64).filter(|&y| (0.05..0.95).contains(&img.get(x, y)[3])).count()
|
||||
soft_count(img, (0..img.height as i64).map(|y| (x, y)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user