mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
instance3d: the copy's cache key covers DoF, animated models and env light
The referenced model's solo render goes through the comp camera: its depth of field (focus, aperture, iris, highlights) blurs it, none of which is in the projection matrix — the evaluated Dof is hashed in now (CodeRabbit, review of #2410). A model with an animation clip selected is posed at its layer time (Animation Speed and Looping are static and hashed): the layer time is folded into the key, so every time is its own entry. A model without a clip keeps one key across times, as before. An Environment light's image lights Advanced 3D models, and its source can be any layer's render (or any environment layer) — too open-ended to hash: while one is on, the read is not cached. Each is covered by a render test.
This commit is contained in:
+27
-5
@@ -3,7 +3,7 @@
|
||||
//! cyclic/over-budget graphs or expressions sampled at unknown times.
|
||||
use super::{KeyHasher, hash_debug, hash_values};
|
||||
use crate::EvalCtx;
|
||||
use effectcraft_project::{ItemId, ItemKind, Layer, LayerId, LayerSource, Node, PropGroup};
|
||||
use effectcraft_project::{ItemId, ItemKind, Layer, LayerId, LayerSource, LightKind, Node, PropGroup};
|
||||
use std::hash::Hash;
|
||||
|
||||
pub(super) fn hash(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, effects: Option<usize>) -> Option<()> {
|
||||
@@ -65,9 +65,11 @@ impl Dependencies<'_> {
|
||||
|
||||
/// What a referenced 3D model or primitive layer's solo render depends on: the layer itself
|
||||
/// (geometry, materials, switches, in/out), its animated values and transform at the time,
|
||||
/// the mesh item, the comp's camera and lights at the time (the lights are off in draft 3D,
|
||||
/// and the renderer setting changes the pixels), and the in/out gate (outside the range the
|
||||
/// read is transparent). Comp size and render scale are in the owner's key already.
|
||||
/// the mesh item, the comp's camera (with its depth of field) and its lights at the time
|
||||
/// (the lights are off in draft 3D, and the renderer setting changes the pixels), the model's
|
||||
/// layer time while it plays an animation clip, and the in/out gate (outside the range the
|
||||
/// read is transparent). An Environment light's image can be any layer's render: with one on
|
||||
/// the read is not cached. Comp size and render scale are in the owner's key already.
|
||||
fn soloed_model(&mut self, ctx: &EvalCtx, layer: &Layer) -> Option<()> {
|
||||
self.remaining = self.remaining.checked_sub(1)?;
|
||||
// Expressions sampled at other times: not cacheable, as in the ordinary path.
|
||||
@@ -80,11 +82,31 @@ impl Dependencies<'_> {
|
||||
if let Some(item) = layer.source.item() {
|
||||
hash_debug(self.h, &ctx.project.item(item));
|
||||
}
|
||||
// The solo render goes through the comp's camera and lights.
|
||||
// The solo render goes through the comp's camera and lights. The camera's depth of
|
||||
// field blurs it (focus, aperture, iris, highlights) — none of that is in the
|
||||
// projection hashed above.
|
||||
hash_debug(self.h, &ctx.camera());
|
||||
hash_debug(self.h, &crate::three_d::active_camera(ctx).dof);
|
||||
hash_debug(self.h, &crate::three_d::light::lights_at(ctx));
|
||||
hash_debug(self.h, &ctx.comp.draft_3d);
|
||||
hash_debug(self.h, &ctx.comp.renderer);
|
||||
// An Environment light's image lights Advanced 3D models (its source can be any
|
||||
// layer's render, or any environment layer) — too open-ended to hash: while one is
|
||||
// on (and lighting: Draft 3D lights nothing), don't cache the read.
|
||||
if ctx.comp.renderer == effectcraft_project::Renderer::Advanced3D
|
||||
&& !ctx.comp.draft_3d
|
||||
&& ctx.comp.layers.iter().any(|l| {
|
||||
l.is_light() && l.switches.video && l.is_active_at(ctx.time) && matches!(l.source, LayerSource::Light { kind: LightKind::Environment })
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A model with an animation clip selected is posed at its layer time (Animation Speed
|
||||
// and Looping are static, hashed above): every time is its own entry. Without one the
|
||||
// pose is the same at every time: keep the static key.
|
||||
if layer.props.sub("geometryOptions").map_or(0, |g| ctx.e(layer, g, "animation")) != 0 {
|
||||
layer.layer_time(ctx.time).0.hash(self.h);
|
||||
}
|
||||
// Outside the layer's in/out range the read is transparent: fold the gate in, so a
|
||||
// static model renders once inside it and again when it leaves.
|
||||
layer.is_active_at(ctx.time).hash(self.h);
|
||||
|
||||
@@ -1185,3 +1185,82 @@ fn an_instance_of_a_model_is_cached_and_follows_the_camera() {
|
||||
let (a, b) = (cover_x(&here), cover_x(&panned));
|
||||
assert!(b < a - 20.0, "panning the camera right moves the copy left: {a} -> {b}");
|
||||
}
|
||||
|
||||
/// The camera's depth of field blurs the solo render (focus, aperture, iris, highlights) —
|
||||
/// none of it is in the projection matrix: changing any of it must change the copy's key, or
|
||||
/// it would serve the un-blurred pixels.
|
||||
#[test]
|
||||
fn the_copy_key_covers_the_cameras_depth_of_field() {
|
||||
let t = Tick::from_seconds_f64(1.0);
|
||||
let (p, cid) = instance_scene(0.0);
|
||||
let set_dof = |p: &Project, on: bool, focus: f64, aperture: f64| {
|
||||
let mut q = p.clone();
|
||||
let cam = &mut q.comp_mut(cid).unwrap().layers[0];
|
||||
for (path, v) in
|
||||
[("cameraOptions/dof", Value::Bool(on)), ("cameraOptions/focusDistance", Value::Scalar(focus)), ("cameraOptions/aperture", Value::Scalar(aperture))]
|
||||
{
|
||||
cam.props.prop_mut(path).unwrap().value = v;
|
||||
}
|
||||
q
|
||||
};
|
||||
let key_at = |p: &Project| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
crate::cache::layer_key(&crate::EvalCtx::new(p, cid, c, t), &c.layers[1], 1.0, false, false).unwrap()
|
||||
};
|
||||
let ka = key_at(&set_dof(&p, true, 500.0, 20.0));
|
||||
let kb = key_at(&set_dof(&p, true, 500.0, 40.0)); // aperture
|
||||
let kc = key_at(&set_dof(&p, true, 700.0, 20.0)); // focus
|
||||
let kd = key_at(&set_dof(&p, false, 500.0, 20.0)); // the switch off
|
||||
assert!(ka != kb && ka != kc && ka != kd && kb != kc, "aperture, focus and the switch each change the key");
|
||||
}
|
||||
|
||||
/// A model with an animation clip selected is posed at its layer time: every time is its own
|
||||
/// entry. Without one (the default) the pose is the same at every time: one key inside the
|
||||
/// in/out range, as before.
|
||||
#[test]
|
||||
fn the_copy_key_covers_an_animated_models_layer_time() {
|
||||
let (p, cid) = instance_scene(0.0);
|
||||
let (t0, t1) = (Tick::from_seconds_f64(0.5), Tick::from_seconds_f64(1.0));
|
||||
let key_at = |p: &Project, t: Tick| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
crate::cache::layer_key(&crate::EvalCtx::new(p, cid, c, t), &c.layers[1], 1.0, false, false).unwrap()
|
||||
};
|
||||
assert_eq!(key_at(&p, t0), key_at(&p, t1), "a model without a clip keeps one key across times");
|
||||
let mut q = p.clone();
|
||||
q.comp_mut(cid).unwrap().layers[2].props.prop_mut("geometryOptions/animation").unwrap().value = Value::Enum(1);
|
||||
assert_ne!(key_at(&q, t0), key_at(&q, t1), "an animated model's key follows its layer time");
|
||||
// Two times and back: each is rendered fresh, none served another time's pixels.
|
||||
let cache = crate::LayerCache::default();
|
||||
let first = render_models(&q, cid, t0, Some(&cache));
|
||||
let second = render_models(&q, cid, t1, Some(&cache));
|
||||
assert_eq!(cache.stats().hits, 0, "the second time is not served the first time's pixels");
|
||||
assert_same(&second, &render_models(&q, cid, t1, None), "the second time is a fresh render");
|
||||
let again = render_models(&q, cid, t0, Some(&cache));
|
||||
assert_eq!(cache.stats().hits, 1, "the first time is served on the way back");
|
||||
assert_same(&again, &first, "the way back is the first time's pixels");
|
||||
}
|
||||
|
||||
/// An Environment light's image lights Advanced 3D models — its source can be any layer's
|
||||
/// render: with one on, the read is not cached, so changes to it cannot reuse stale pixels.
|
||||
#[test]
|
||||
fn an_environment_light_makes_the_copy_uncached() {
|
||||
let (p, cid) = instance_scene(0.0);
|
||||
let mut q = p.clone();
|
||||
let comp = q.comp(cid).unwrap().clone();
|
||||
let env = build::layer(&mut q, &comp, "Env", LayerSource::Light { kind: effectcraft_project::LightKind::Environment }, (200, 100), None);
|
||||
q.comp_mut(cid).unwrap().layers.insert(0, env);
|
||||
let t = Tick::from_seconds_f64(1.0);
|
||||
let key_at = |p: &Project| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
crate::cache::layer_key(&crate::EvalCtx::new(p, cid, c, t), &c.layers[2], 1.0, false, false)
|
||||
};
|
||||
assert!(key_at(&q).is_none(), "with an Environment light the read is not cached");
|
||||
let cache = crate::LayerCache::default();
|
||||
let a = render_models(&q, cid, t, Some(&cache));
|
||||
let b = render_models(&q, cid, t, Some(&cache));
|
||||
assert_same(&a, &b, "two fresh renders agree");
|
||||
assert_eq!(cache.stats().hits, 0, "nothing is served from the cache");
|
||||
// The light off (not lighting): the cache comes back.
|
||||
q.comp_mut(cid).unwrap().layers[0].switches.video = false;
|
||||
assert!(key_at(&q).is_some(), "the light off, the read is cached again");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user