mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:41:25 +00:00
CC Composite stays After Effects-pure: its Layer parameter is gone, so parameter indices match AE again (scripts and expressions that address parameters by index). The model reference lives in its own effect, ec.3d.instance3d (3D Channel, CPU, one layer parameter), which replaces the layer's pixels with the referenced layer's render — alpha included. A referenced model's solo render is now hashed into the owner's cache key instead of declining the entry (Bflat, review of #2410): the layer, its animated values and transform, the mesh item, the comp camera and lights, draft 3D, the renderer setting and the in/out gate. Covered by a render test: the copy is cached, served on the second render, and re-rendered when the camera pans or the model leaves its in/out range.
166 lines
7.3 KiB
Rust
166 lines
7.3 KiB
Rust
use super::*;
|
|
|
|
/// Layer parameters and audio for effects (see [`effectcraft_effects::EffectHost`]). What it
|
|
/// hands out is in effect space (see [`effect_bounds`]): the other layer's for its pixels, the
|
|
/// running layer's for its own frames and the comp scene.
|
|
pub(crate) struct FxHost<'r, 'a, 'c> {
|
|
pub(crate) r: &'r Renderer<'a>,
|
|
pub(crate) ctx: &'c EvalCtx<'a>,
|
|
pub(crate) layer: &'c Layer,
|
|
/// Where the stack's effect space starts in layer coordinates (zero on adjustment layers,
|
|
/// whose effects run on the comp below).
|
|
pub(crate) origin: [f64; 2],
|
|
/// Index of the effect being rendered (bounds `self_at` to effects before it).
|
|
pub(crate) index: usize,
|
|
pub(crate) original: Option<&'c Buf>,
|
|
}
|
|
|
|
impl FxHost<'_, '_, '_> {
|
|
/// Another layer's pixels for a layer parameter, in its effect space; `layer_space`: `buf`
|
|
/// is still in layer space (its source or its content).
|
|
fn pixels(&self, other: &Layer, mut buf: Buf, layer_space: bool) -> LayerPixels {
|
|
let (size, origin) = self.ctx.effect_bounds(other);
|
|
if layer_space {
|
|
buf.rebase(origin);
|
|
}
|
|
LayerPixels { buf, size }
|
|
}
|
|
}
|
|
|
|
/// The comp's camera and first light relative to `layer`, in its layer pixels (see
|
|
/// [`EffectHost::comp_scene`]): the comp camera's view of the layer (as if 3D at its transform),
|
|
/// brought back into the layer's own pixel grid through the inverse of how the layer itself
|
|
/// composites.
|
|
pub(crate) fn comp_scene(ctx: &EvalCtx, layer: &Layer) -> Option<effectcraft_effects::CompScene> {
|
|
let world = ctx.world_matrix(layer);
|
|
let (cam, _, _) = ctx.camera();
|
|
let p = (cam * world).0;
|
|
let back = ctx.layer_to_comp(layer).0.inverse()?.0;
|
|
let rows = [p[0], p[1], p[3]];
|
|
let camera: [[f64; 4]; 3] = std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| back[i][k] * rows[k][j]).sum()));
|
|
let inv = world.inverse();
|
|
let light = three_d::light::lights_at(ctx).first().and_then(|l| {
|
|
let inv = inv.as_ref()?;
|
|
let pos = inv.apply(l.pos);
|
|
let dir = inv.apply_vec(l.dir);
|
|
let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt().max(1e-12);
|
|
let kind = match l.kind {
|
|
effectcraft_project::LightKind::Parallel => 0,
|
|
effectcraft_project::LightKind::Ambient => 2,
|
|
_ => 1,
|
|
};
|
|
Some(effectcraft_effects::CompLight { pos: [pos.x, pos.y, pos.z], dir: [dir.x / len, dir.y / len, dir.z / len], color: l.color, kind })
|
|
});
|
|
let camera_layer = ctx.comp.active_camera(ctx.time).is_some();
|
|
Some(effectcraft_effects::CompScene { camera: Some(camera), camera_layer, light })
|
|
}
|
|
|
|
/// Nesting limit for effects that render layers (other layers, other times).
|
|
pub(crate) const MAX_FX_DEPTH: usize = 8;
|
|
|
|
impl EffectHost for FxHost<'_, '_, '_> {
|
|
fn original(&self) -> Option<&Buf> {
|
|
self.original
|
|
}
|
|
|
|
fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> {
|
|
self.r.active_accel().and_then(|a| a.particles())
|
|
}
|
|
fn layer(&self, id: u64, masks_and_effects: bool) -> Option<LayerPixels> {
|
|
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
|
if (other.id == self.layer.id && masks_and_effects) || self.r.depth > MAX_FX_DEPTH {
|
|
return None;
|
|
}
|
|
let sub = self.r.nested();
|
|
// A 3D model or primitive layer has no 2D source of its own: an effect referencing one
|
|
// reads it as After Effects does — the layer rendered on its own through the comp's
|
|
// camera (Create 3D Layer Instance's effect), in the layer's effect space (the
|
|
// comp, centred on the layer's anchor).
|
|
if other.source.is_model() {
|
|
let buf = sub.solo_3d(self.ctx, other);
|
|
return Some(self.pixels(other, buf, false));
|
|
}
|
|
// The cached layer buffer is shared; effects get their own copy.
|
|
let buf = if masks_and_effects { (*sub.content_buf(self.ctx, other)?).clone() } else { sub.source(self.ctx, other)? };
|
|
Some(self.pixels(other, buf, true))
|
|
}
|
|
|
|
fn layer_masks(&self, id: u64) -> Option<LayerPixels> {
|
|
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
|
if self.r.depth > MAX_FX_DEPTH {
|
|
return None;
|
|
}
|
|
let sub = self.r.nested();
|
|
let buf = (*sub.layer_input(self.ctx, other, 0)?).clone();
|
|
Some(self.pixels(other, buf, false))
|
|
}
|
|
|
|
fn audio(&self, id: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
|
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
|
let LayerSource::Footage { item } = &other.source else { return None };
|
|
let ItemKind::Footage(f) = &self.r.project.item(*item)?.kind else { return None };
|
|
if !f.has_audio {
|
|
return None;
|
|
}
|
|
let st = other.layer_time(Tick::from_seconds_f64(start));
|
|
self.r.footage.audio(*item, f, st, frames, rate)
|
|
}
|
|
|
|
fn self_at(&self, layer_time: f64, effects: usize) -> Option<Buf> {
|
|
if self.r.depth > MAX_FX_DEPTH || self.layer.switches.adjustment {
|
|
return None;
|
|
}
|
|
let n = effects.min(self.index);
|
|
let t = self.layer.comp_time(Tick::from_seconds_f64(layer_time));
|
|
let sub = self.r.nested();
|
|
sub.layer_input(&self.ctx.at(t), self.layer, n).map(|b| (*b).clone())
|
|
}
|
|
|
|
fn aux(&self) -> Option<Arc<effectcraft_raster::AuxChannels>> {
|
|
self.r.layer_aux(self.ctx, self.layer)
|
|
}
|
|
|
|
fn params_at(&self, layer_time: f64) -> Option<Params> {
|
|
let g = self.layer.effects()?.groups().nth(self.index)?;
|
|
let ctx = self.ctx.at(self.layer.comp_time(Tick::from_seconds_f64(layer_time)));
|
|
Some(effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(self.layer, pr)))
|
|
}
|
|
|
|
fn comp_scene(&self) -> Option<effectcraft_effects::CompScene> {
|
|
Some(comp_scene(self.ctx, self.layer)?.rebased(self.origin))
|
|
}
|
|
|
|
fn layer_at(&self, id: u64, comp_time: f64, masks_and_effects: bool) -> Option<LayerPixels> {
|
|
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
|
if (other.id == self.layer.id && masks_and_effects) || !comp_time.is_finite() || self.r.depth > MAX_FX_DEPTH {
|
|
return None;
|
|
}
|
|
let ctx = self.ctx.at(Tick::from_seconds_f64(comp_time));
|
|
let sub = self.r.nested();
|
|
Some(if masks_and_effects {
|
|
self.pixels(other, (*sub.content_buf(&ctx, other)?).clone(), true)
|
|
} else if other.id == self.layer.id {
|
|
self.pixels(other, sub.source(&ctx, other)?, true)
|
|
} else {
|
|
// Preserve other-layer reads: time effects historically see source + masks.
|
|
self.pixels(other, (*sub.layer_input(&ctx, other, 0)?).clone(), false)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The Essential Properties overrides of a precomp layer, evaluated at the context time.
|
|
pub fn essential_overrides(ctx: &EvalCtx, layer: &Layer) -> Vec<effectcraft_project::essential::Override> {
|
|
use effectcraft_project::essential;
|
|
let over = essential::overridden(layer);
|
|
let Some(g) = essential::group(layer).filter(|_| !over.is_empty()) else { return vec![] };
|
|
let mut out = vec![];
|
|
g.walk("", &mut |_, p| {
|
|
if over.contains(&p.uid)
|
|
&& let Some(control) = essential::control_of(&p.match_id)
|
|
{
|
|
out.push(essential::Override { control, value: ctx.value(layer, p) });
|
|
}
|
|
});
|
|
out
|
|
}
|