From 7b0771fe5acd5f69e8e6fe34722a88db2f09ddc3 Mon Sep 17 00:00:00 2001 From: tylerjenningsw Date: Tue, 13 Oct 2026 20:45:20 -0400 Subject: [PATCH] Effects: a host evaluates one effect parameter at another layer time (#1373) EffectHost::param_at(effect_index, id, layer_time) evaluates a single parameter (by its flattened id, such as birthRate or group/param) of the effect at that index in the layer's stack. A simulation can then sample one parameter at every step without flattening the whole effect each time. The effect index is passed in, rather than read from the renderer's running-effect index as params_at does: GPU-batched effects never update that index, so a batched effect would read another effect's parameters. The renderer's implementation walks the effect group by match id and evaluates just that property (keyframes and expressions) at the comp time of the layer time. --- crates/effects/src/lib.rs | 7 +++++++ crates/render/src/lib.rs | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 40ed79a7d..388b287c5 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -383,6 +383,13 @@ pub trait EffectHost: Sync { fn params_at(&self, _layer_time: f64) -> Option { None } + /// Parameter `id` (a [`flatten_params`] id such as `birthRate`) of effect `effect_index` in + /// the layer's stack (the running effect's [`EffectEnv::effect_index`]) at another **layer + /// time**: CC Particle World samples its Birth Rate at every step it simulates (#1373). + /// `None` when unavailable. + fn param_at(&self, _effect_index: usize, _id: &str, _layer_time: f64) -> Option { + None + } /// A particle simulation backend (GPU particles; see [`psim`]). `None` = simulate on the /// CPU. fn particles(&self) -> Option<&dyn psim::ParticleSim> { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 41036fe6b..a5597de93 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -343,6 +343,27 @@ impl EffectHost for FxHost<'_, '_, '_> { Some(effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(self.layer, pr))) } + fn param_at(&self, effect_index: usize, id: &str, layer_time: f64) -> Option { + // Only the one property: a simulation samples it at every step. + let mut g = self.layer.effects()?.groups().nth(effect_index)?; + let mut path = id.split('/').peekable(); + while let Some(m) = path.next() { + if path.peek().is_none() { + let p = g.children.iter().find_map(|c| match c { + effectcraft_project::Node::Prop(p) if p.match_id == m => Some(p), + _ => None, + })?; + let ctx = self.ctx.at(self.layer.comp_time(Tick::from_seconds_f64(layer_time))); + return Some(ctx.value(self.layer, p)); + } + g = g.children.iter().find_map(|c| match c { + effectcraft_project::Node::Group(sg) if sg.match_id == m => Some(sg), + _ => None, + })?; + } + None + } + fn comp_scene(&self) -> Option { Some(comp_scene(self.ctx, self.layer)?.rebased(self.origin)) }