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:
Minher0
2026-10-29 11:50:58 +00:00
parent 7f1ec94810
commit 93aed3bfd6
2 changed files with 106 additions and 5 deletions
+27 -5
View File
@@ -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);