mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
154 lines
7.4 KiB
Rust
154 lines
7.4 KiB
Rust
//! Auto-orient (along path / towards camera) and camera parenting.
|
|
|
|
use effectcraft_color::Label;
|
|
use effectcraft_geom::vec3;
|
|
use effectcraft_keyframe::{Keyframe, Value};
|
|
use effectcraft_project::build;
|
|
use effectcraft_project::{AutoOrient, Comp, ItemId, ItemKind, Layer, LayerSource, Project, Solid};
|
|
use effectcraft_time::{FrameRate, Tick};
|
|
|
|
use super::active_camera;
|
|
use crate::EvalCtx;
|
|
|
|
fn setup() -> (Project, ItemId, Comp) {
|
|
let mut p = Project::default();
|
|
let comp = Comp::new(200, 100, 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)
|
|
}
|
|
|
|
fn solid(p: &mut Project, comp: &Comp, three_d: bool, pos: [f64; 3]) -> Layer {
|
|
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0; 3], width: 10, height: 10, pixel_aspect: 1.0 }));
|
|
let mut l = build::layer(p, comp, "S", LayerSource::Solid { item: sid }, (10, 10), None);
|
|
l.switches.three_d = three_d;
|
|
l.props.prop_mut("transform/position").unwrap().value = Value::Vec3(pos);
|
|
l
|
|
}
|
|
|
|
#[test]
|
|
fn orient_towards_camera_faces_the_eye() {
|
|
let (mut p, cid, comp) = setup();
|
|
let mut l = solid(&mut p, &comp, true, [600.0, -200.0, 300.0]);
|
|
l.auto_orient = AutoOrient::TowardsCamera;
|
|
p.comp_mut(cid).unwrap().layers.push(l);
|
|
let c = p.comp(cid).unwrap();
|
|
let ctx = EvalCtx::new(&p, cid, c, Tick::ZERO);
|
|
let w = ctx.world_matrix(&c.layers[0]);
|
|
let n = w.apply_vec(vec3(0.0, 0.0, 1.0)).normalize();
|
|
let eye = active_camera(&ctx).eye;
|
|
let to_layer = (w.apply(vec3(5.0, 5.0, 0.0)) - eye).normalize();
|
|
assert!((n - to_layer).length() < 1e-6, "{n:?} vs {to_layer:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn orient_along_path_2d_follows_motion() {
|
|
let (mut p, cid, comp) = setup();
|
|
let mut l = solid(&mut p, &comp, false, [50.0, 10.0, 0.0]);
|
|
l.auto_orient = AutoOrient::AlongPath;
|
|
l.props.prop_mut("transform/position").unwrap().keys =
|
|
vec![Keyframe::new(Tick::ZERO, Value::Vec3([50.0, 10.0, 0.0])), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Vec3([50.0, 90.0, 0.0]))];
|
|
p.comp_mut(cid).unwrap().layers.push(l);
|
|
let c = p.comp(cid).unwrap();
|
|
let ctx = EvalCtx::new(&p, cid, c, Tick::from_seconds_f64(0.5));
|
|
let x_axis = ctx.world_matrix(&c.layers[0]).apply_vec(vec3(1.0, 0.0, 0.0));
|
|
// Moving straight down (+y): the layer's x axis points down the screen.
|
|
assert!(x_axis.x.abs() < 1e-6 && (x_axis.y - 1.0).abs() < 1e-6, "{x_axis:?}");
|
|
}
|
|
|
|
/// #1081: a layer travelling left kept its heading until the last keyframe, then (no motion left
|
|
/// to measure) snapped back to 0 degrees and looked flipped. It must keep its last direction of
|
|
/// travel at the final key and after it, in 2D and 3D.
|
|
#[test]
|
|
fn orient_along_path_keeps_the_last_heading_at_and_after_the_end() {
|
|
for three_d in [false, true] {
|
|
let (mut p, cid, comp) = setup();
|
|
let mut l = solid(&mut p, &comp, three_d, [150.0, 50.0, 0.0]);
|
|
l.auto_orient = AutoOrient::AlongPath;
|
|
let end = Tick::from_seconds_f64(1.0);
|
|
l.props.prop_mut("transform/position").unwrap().keys =
|
|
vec![Keyframe::new(Tick::ZERO, Value::Vec3([150.0, 50.0, 0.0])), Keyframe::new(end, Value::Vec3([50.0, 50.0, 0.0]))];
|
|
p.comp_mut(cid).unwrap().layers.push(l);
|
|
let c = p.comp(cid).unwrap();
|
|
for secs in [0.5, 0.999, 1.0, 1.2, 1.9] {
|
|
let ctx = EvalCtx::new(&p, cid, c, Tick::from_seconds_f64(secs));
|
|
let x_axis = ctx.world_matrix(&c.layers[0]).apply_vec(vec3(1.0, 0.0, 0.0));
|
|
// Moving left (-x): the layer's x axis keeps pointing left.
|
|
assert!((x_axis.x + 1.0).abs() < 1e-6 && x_axis.y.abs() < 1e-6 && x_axis.z.abs() < 1e-6, "3d={three_d} t={secs}: {x_axis:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn layers_parented_to_a_camera_follow_its_rotation() {
|
|
let (mut p, cid, comp) = setup();
|
|
let mut cam = build::layer(&mut p, &comp, "Camera", LayerSource::Camera, (200, 100), None);
|
|
cam.auto_orient = AutoOrient::Off;
|
|
cam.props.prop_mut("transform/position").unwrap().value = Value::Vec3([0.0, 0.0, 0.0]);
|
|
cam.props.prop_mut("transform/orientation").unwrap().value = Value::Vec3([0.0, 90.0, 0.0]);
|
|
let mut child = solid(&mut p, &comp, true, [0.0, 0.0, 100.0]);
|
|
child.parent = Some(cam.id);
|
|
let c = p.comp_mut(cid).unwrap();
|
|
c.layers.push(cam);
|
|
c.layers.push(child);
|
|
let c = p.comp(cid).unwrap();
|
|
let ctx = EvalCtx::new(&p, cid, c, Tick::ZERO);
|
|
// 100 px in front of a camera turned to +x is at x = 100 (anchor at the solid's centre).
|
|
let at = ctx.world_matrix(&c.layers[1]).apply(vec3(5.0, 5.0, 0.0));
|
|
assert!((at - vec3(100.0, 0.0, 0.0)).length() < 1e-6, "{at:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn camera_parented_to_towards_camera_layer_does_not_recurse() {
|
|
let (mut p, cid, comp) = setup();
|
|
let mut l = solid(&mut p, &comp, true, [100.0, 50.0, 0.0]);
|
|
l.auto_orient = AutoOrient::TowardsCamera;
|
|
let mut cam = build::layer(&mut p, &comp, "Camera", LayerSource::Camera, (200, 100), None);
|
|
cam.parent = Some(l.id);
|
|
let c = p.comp_mut(cid).unwrap();
|
|
c.layers.push(cam);
|
|
c.layers.push(l);
|
|
let _ = crate::render_frame(&p, cid, Tick::ZERO, 0.5);
|
|
}
|
|
|
|
/// The layer cache holds layer pixels only; camera, lights, DOF and the view override act at
|
|
/// compositing time, so cached renders must match uncached ones after any of them change.
|
|
#[test]
|
|
fn layer_cache_is_independent_of_camera_lights_and_view() {
|
|
let (mut p, cid, comp) = setup();
|
|
let mut cam = build::layer(&mut p, &comp, "Camera", LayerSource::Camera, (200, 100), None);
|
|
cam.props.prop_mut("cameraOptions/dof").unwrap().value = Value::Bool(true);
|
|
cam.props.prop_mut("cameraOptions/aperture").unwrap().value = Value::Scalar(40.0);
|
|
let light = build::layer(&mut p, &comp, "Light", LayerSource::Light { kind: effectcraft_project::LightKind::Point }, (200, 100), None);
|
|
let a = solid(&mut p, &comp, true, [80.0, 50.0, 0.0]);
|
|
let b = solid(&mut p, &comp, true, [120.0, 50.0, 150.0]);
|
|
let c = p.comp_mut(cid).unwrap();
|
|
c.layers.extend([cam, light, a, b]);
|
|
let cache = crate::LayerCache::new(64 << 20);
|
|
let render = |p: &Project, cached: bool, view: Option<super::CameraState>| {
|
|
let opts = crate::RenderOpts { view, motion_blur: false, ..Default::default() };
|
|
let mut r = crate::Renderer::new(p, &crate::NoFootage, opts);
|
|
if cached {
|
|
r.cache = Some(&cache);
|
|
}
|
|
r.comp_frame(cid, Tick::ZERO)
|
|
};
|
|
let check = |p: &Project, view: Option<super::CameraState>| {
|
|
let warm = render(p, true, view);
|
|
assert_eq!(render(p, true, view), warm);
|
|
assert_eq!(render(p, false, view), warm, "cached render differs from uncached");
|
|
warm
|
|
};
|
|
let first = check(&p, None);
|
|
// Move the camera, refocus, change the light: cached pixels must follow.
|
|
let c = p.comp_mut(cid).unwrap();
|
|
c.layers[0].props.prop_mut("transform/position").unwrap().value = Value::Vec3([40.0, 20.0, -250.0]);
|
|
c.layers[0].props.prop_mut("cameraOptions/focusDistance").unwrap().value = Value::Scalar(400.0);
|
|
c.layers[1].props.prop_mut("lightOptions/intensity").unwrap().value = Value::Scalar(40.0);
|
|
let second = check(&p, None);
|
|
assert_ne!(first, second, "camera/light changes re-render");
|
|
let top = super::default_view_cam(super::View3D::Top, 200.0, 100.0).state();
|
|
let third = check(&p, Some(top));
|
|
assert_ne!(third, second);
|
|
assert!(cache.stats().hits > 0, "the cache was actually used");
|
|
}
|