instance3d: a dedicated 3D Layer Instance effect, and the copy is cached

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.
This commit is contained in:
Minher0
2026-10-29 10:18:17 +00:00
parent 36a50a6366
commit 5b2f305f72
11 changed files with 210 additions and 77 deletions
+3 -52
View File
@@ -171,9 +171,7 @@ const CC_COMPOSITE_MODES: [&str; 16] = [
"Stencil Alpha",
];
/// Composites the original stack input over/under/with the result of preceding effects. With a
/// layer chosen (Create 3D Layer Instance's solid), that layer takes the original's place — a 2D
/// copy of it, rendered through the comp's camera when it is a 3D model layer.
/// Composites the original stack input over/under/with the result of preceding effects.
fn cc_composite(ctx: &EffectCtx, mut b: Buf) -> Buf {
let op = (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0);
let m = ctx.params.e("compositeOriginal");
@@ -195,24 +193,11 @@ fn cc_composite(ctx: &EffectCtx, mut b: Buf) -> Buf {
15 => BlendMode::StencilAlpha,
_ => BlendMode::Normal,
};
// The chosen layer takes the original's place (a 2D copy of it); the layer's own original
// otherwise, as before.
let referenced = ctx.layer_param("layer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, false));
// A chosen layer in front replaces the layer's pixels, alpha included: the copy of a 3D
// layer is transparent where the model isn't. The other modes composite with them.
let replace = referenced.is_some() && m == 0;
let original =
referenced.or_else(|| ctx.env.host.and_then(|h| h.original()).map(|o| crate::util::fit_buffer(o, b.img.width, b.img.height, b.offset, b.scale)));
let original = ctx.env.host.and_then(|h| h.original()).map(|o| crate::util::fit_buffer(o, b.img.width, b.img.height, b.offset, b.scale));
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
let result = *px;
let orig = original.as_ref().and_then(|o| o.data.get(i)).copied().unwrap_or(result).map(|v| v * op);
let mut o = if replace {
orig
} else if m == 1 {
blend_pixel(BlendMode::Normal, orig, result, 0.5)
} else {
blend_pixel(mode, result, orig, 0.5)
};
let mut o = if m == 1 { blend_pixel(BlendMode::Normal, orig, result, 0.5) } else { blend_pixel(mode, result, orig, 0.5) };
o[3] = o[3].clamp(0.0, 1.0);
if rgb_only {
let (c, _) = unpremul(o);
@@ -338,7 +323,6 @@ pub fn specs() -> Vec<EffectSpec> {
"ec.channel.cccomposite",
"CC Composite",
vec![
layer_param("layer", "Layer"),
p("opacity", "Opacity", num(100.0), pct()),
p("compositeOriginal", "Composite Original", Value::Enum(0), popup(&CC_COMPOSITE_MODES)),
p("rgbOnly", "RGB Only", Value::Bool(false), ParamUi::Checkbox),
@@ -439,39 +423,6 @@ pub(crate) mod tests {
assert!((add.img.get(0, 0)[0] - 0.6).abs() < 1e-5);
}
#[test]
fn cc_composite_layer_takes_the_originals_place() {
// Create 3D Layer Instance's solid: the chosen layer takes the original's place. In
// Front replaces the pixels — alpha included, the copy of a 3D layer is transparent
// where the model isn't — and the other modes composite with them.
let solid = || Image::filled(4, 4, [1.0, 1.0, 1.0, 1.0]);
let model = |write: bool| {
let mut img = Image::new(4, 4);
if write {
img.set(1, 1, [1.0, 0.0, 0.0, 1.0]);
}
img
};
let out = with_host(model(true), |env| run_fx("ec.channel.cccomposite", &[("layer", Value::Layer(Some(7)))], solid(), 0.0, env));
assert_eq!(out.img.get(1, 1), [1.0, 0.0, 0.0, 1.0], "the chosen layer's pixel");
assert_eq!(out.img.get(3, 3), [0.0; 4], "transparent where the chosen layer isn't");
// Half opacity keeps the copy's shape at half strength.
let out =
with_host(model(true), |env| run_fx("ec.channel.cccomposite", &[("layer", Value::Layer(Some(7))), ("opacity", num(50.0))], solid(), 0.0, env));
let p = out.img.get(1, 1);
assert!((p[0] - 0.5).abs() < 1e-6 && (p[3] - 0.5).abs() < 1e-6, "{p:?}");
assert_eq!(out.img.get(3, 3), [0.0; 4]);
// In Back: the layer's own pixels over the chosen layer.
let out = with_host(model(true), |env| {
run_fx("ec.channel.cccomposite", &[("layer", Value::Layer(Some(7))), ("compositeOriginal", Value::Enum(1))], solid(), 0.0, env)
});
assert_eq!(out.img.get(1, 1), [1.0, 1.0, 1.0, 1.0]);
assert_eq!(out.img.get(3, 3), [1.0, 1.0, 1.0, 1.0]);
// No layer chosen: the layer's own original, as before.
let out = with_host(model(true), |env| run_fx("ec.channel.cccomposite", &[], Image::filled(4, 4, [0.5, 0.5, 0.5, 1.0]), 0.0, env));
assert_eq!(out.img.get(0, 0), [0.5, 0.5, 0.5, 1.0]);
}
#[test]
fn compound_arithmetic_copy_self_identity_and_add() {
let img = sample();
+19
View File
@@ -7,6 +7,11 @@
//! pass for a precomp of a 3D comp. Without auxiliary channels they pass the layer through
//! (EXtractoR still maps the layer's own R, G, B and A).
//!
//! 3D Layer Instance (Create 3D Layer Instance's own effect, Layer ▸ Create) is not an aux
//! reader: it replaces the layer's pixels with a referenced layer's render — for a 3D model
//! layer, its render on its own through the comp's camera — so 2D effects can be applied to a
//! 3D layer's render.
//!
//! Aux pixels are looked up nearest-neighbour (IDs must not be interpolated) at each buffer
//! pixel's layer position, so render scale and layer padding are respected.
@@ -582,6 +587,19 @@ fn extractor(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
/// A 2D copy of a referenced layer (Create 3D Layer Instance's solid): the layer's pixels
/// replaced with the referenced layer's render, alpha included — the copy is transparent
/// where the referenced layer isn't. A 3D model or primitive layer is rendered on its own
/// through the comp's camera, as After Effects renders 3D layers; another layer is read with
/// its masks and effects. With no layer chosen the effect passes the layer through, as a layer
/// parameter with nothing chosen does.
fn instance_3d(ctx: &EffectCtx, mut b: Buf) -> Buf {
if let Some(o) = ctx.layer_param("layer", true) {
b.img = crate::util::fit_layer(ctx, &b, &o, false);
}
b
}
pub fn specs() -> Vec<EffectSpec> {
let text = |id: &'static str, name: &'static str, d: &str| p(id, name, Value::Str(d.into()), ParamUi::Text);
vec![
@@ -682,6 +700,7 @@ pub fn specs() -> Vec<EffectSpec> {
],
identifier,
),
spec("ec.3d.instance3d", "3D Layer Instance", vec![p("layer", "Layer", Value::Layer(None), ParamUi::Layer)], instance_3d),
]
}
+26
View File
@@ -373,3 +373,29 @@ fn channel_extract_antialias_averages_sub_pixels() {
let soft = run("ec.3d.channelextract", &[vals.as_slice(), &[("antialias", Value::Bool(true))]].concat(), &host);
assert!((soft.get(5, 5)[0] - 0.5).abs() < 1e-5, "{:?}", soft.get(5, 5));
}
#[test]
fn instance3d_replaces_the_pixels_with_the_referenced_layer() {
// Create 3D Layer Instance's copy: the chosen layer's render takes the place of the layer's
// own pixels, alpha included — the copy is transparent where the referenced layer isn't —
// and with no layer chosen the layer passes through.
struct Host(Image);
impl EffectHost for Host {
fn layer(&self, id: u64, _: bool) -> Option<LayerPixels> {
(id == 7).then(|| LayerPixels { buf: Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 }, size: [self.0.width as f64, self.0.height as f64] })
}
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
None
}
}
let solid = || Image::filled(4, 4, [1.0, 1.0, 1.0, 1.0]);
let mut render = Image::new(4, 4);
render.set(1, 1, [1.0, 0.0, 0.0, 1.0]);
let env = EffectEnv { host: Some(&Host(render)), ..Default::default() };
let out = run_fx("ec.3d.instance3d", &[("layer", Value::Layer(Some(7)))], solid(), 0.0, env);
assert_eq!(out.img.get(1, 1), [1.0, 0.0, 0.0, 1.0], "the referenced layer's pixel");
assert_eq!(out.img.get(3, 3), [0.0; 4], "transparent where the referenced layer isn't");
// No layer chosen: the layer's own pixels.
let out = run_fx("ec.3d.instance3d", &[], solid(), 0.0, EffectEnv::default());
assert_eq!(out.img.data, solid().data);
}
+6 -6
View File
@@ -27,9 +27,9 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
if op == "instance3d" {
// After Effects' Create 3D Layer Instance (Layer ▸ Create): 3D model layers take
// no masks or effects, so a comp-sized solid goes at the top of the layer stack
// with CC Composite referencing the model — a 2D copy of its render, through the
// comp's camera, that 2D effects can be applied to. The model is switched off,
// as After Effects does (back on to keep its shadow).
// with a 3D Layer Instance effect referencing the model — a 2D copy of its render,
// through the comp's camera, that 2D effects can be applied to. The model is switched
// off, as After Effects does (back on to keep its shadow).
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone();
let mut eligible = vec![];
for lid in &lids {
@@ -55,9 +55,9 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
// A soloed source keeps its Solo switch with video off, and any_solo hides
// the non-soloed copy: the copy takes the Solo over to stay visible.
l.switches.solo = src.switches.solo;
// CC Composite referencing the 3D layer: the solid shows its render (the 2D
// copy).
let fx = effectcraft_effects::find("ec.channel.cccomposite").ok_or_else(|| bad("layer.create", "the CC Composite effect is missing"))?;
// A 3D Layer Instance effect referencing the 3D layer: the solid shows its
// render (the 2D copy).
let fx = effectcraft_effects::find("ec.3d.instance3d").ok_or_else(|| bad("layer.create", "the 3D Layer Instance effect is missing"))?;
let mut g = effectcraft_effects::instantiate(fx, &mut Ids(&mut proj.next_id), fx.name, [comp.width as f64, comp.height as f64]);
if let Some(pr) = g.get_mut("layer") {
pr.value = KV::Layer(Some(lid.0));
+4 -4
View File
@@ -1,6 +1,6 @@
//! Create 3D Layer Instance (Layer ▸ Create): a comp-sized solid at the top of the layer stack
//! with CC Composite referencing the selected 3D model layer — a 2D copy of its render that 2D
//! effects can be applied to (behaviour, pixels, undo/redo, serde).
//! with a 3D Layer Instance effect referencing the selected 3D model layer — a 2D copy of its
//! render that 2D effects can be applied to (behaviour, pixels, undo/redo, serde).
use std::sync::Arc;
@@ -97,9 +97,9 @@ fn instance3d_creates_a_2d_copy_of_the_model() {
let l = layer_in(&s, comp, solid);
assert_eq!(l.name, "Triangle Instance");
assert_eq!(s.active_comp().unwrap().layers[0].id.0, solid, "at the top of the layer stack");
// CC Composite referencing the 3D layer.
// A 3D Layer Instance effect referencing the 3D layer.
let g = l.effects().unwrap().groups().next().unwrap().clone();
assert!(matches!(&g.kind, GroupKind::Effect { effect } if effect == "ec.channel.cccomposite"), "{:?}", g.kind);
assert!(matches!(&g.kind, GroupKind::Effect { effect } if effect == "ec.3d.instance3d"), "{:?}", g.kind);
assert_eq!(g.get("layer").unwrap().value, KV::Layer(Some(model)));
// The model is switched off, the copy selected.
assert!(!layer_in(&s, comp, model).switches.video, "the referenced 3D layer is off, as After Effects does");
-6
View File
@@ -247,12 +247,6 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
p.f[0] = pr.color("highlightColor");
}
"ec.channel.cccomposite" => {
// A chosen layer (Create 3D Layer Instance's solid) runs on the CPU: its pixels — a
// referenced 3D model rendered through the comp's camera — replace the layer's own
// original, alpha included.
if ctx.layer_param("layer", true).is_some() {
return None;
}
if let Some(source) = ctx.env.host.and_then(|h| h.original()) {
let fitted = effectcraft_effects::util::fit_buffer(source, b.img.width, b.img.height, b.offset, b.scale);
original = Some(e.g.upload_image(&fitted)?);
+32 -4
View File
@@ -63,17 +63,45 @@ impl Dependencies<'_> {
Some(())
}
/// 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.
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.
if (self.other_times || super::reads_other_times(layer)) && super::has_expression(&layer.props) {
return None;
}
hash_debug(self.h, layer);
hash_values(self.h, ctx, layer, &layer.props);
hash_debug(self.h, &ctx.world_matrix(layer));
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.
hash_debug(self.h, &ctx.camera());
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);
// 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);
Some(())
}
fn layer(&mut self, ctx: &EvalCtx, layer: &Layer) -> Option<()> {
let id = (ctx.comp_id, layer.id);
if self.path.len() > crate::MAX_FX_DEPTH || self.path.contains(&id) {
return None;
}
// A referenced 3D model or primitive layer renders through the comp's camera and lights
// (an effect layer parameter reads it that way — Create 3D Layer Instance's CC
// Composite): those move its pixels without a trace in its own properties, so the owner
// keeps no cache entry for it.
// (an effect layer parameter reads it that way — Create 3D Layer Instance): hash what
// moves its pixels, so the owner keeps a cache entry that changes when any of it does.
if layer.source.is_model() {
return None;
self.soloed_model(ctx, layer)?;
return Some(());
}
self.remaining = self.remaining.checked_sub(1)?;
if (self.other_times || super::reads_other_times(layer)) && super::has_expression(&layer.props) {
+1 -1
View File
@@ -74,7 +74,7 @@ impl EffectHost for FxHost<'_, '_, '_> {
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 CC Composite), in the layer's effect space (the
// 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);
+112
View File
@@ -1073,3 +1073,115 @@ fn workers_rendering_a_layer_at_once_share_one_buffer() {
assert!(bufs.iter().all(|b| std::sync::Arc::ptr_eq(b, &bufs[0])), "one shared buffer");
assert_eq!(cache.stats().entries, 1);
}
// ------------------------------------------------------------------------------ 3D layer instances
/// A red triangle in the Z = 0 plane (the engine instance3d tests' glTF, buffer embedded as a
/// data URI), parsed in memory: a FootageSource that knows one model item.
const TRIANGLE: &str = r#"{
"asset": {"version": "2.0"},
"scenes": [{"nodes": [0]}],
"nodes": [{"mesh": 0}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "NORMAL": 1}, "material": 0}]}],
"materials": [{"pbrMetallicRoughness": {"baseColorFactor": [1.0, 0.0, 0.0, 1.0]}}],
"buffers": [{"byteLength": 72, "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAAAAAACAPwAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/"}],
"bufferViews": [
{"buffer": 0, "byteOffset": 0, "byteLength": 36},
{"buffer": 0, "byteOffset": 36, "byteLength": 36}
],
"accessors": [
{"bufferView": 0, "componentType": 5126, "type": "VEC3", "count": 3, "min": [-1.0, -1.0, 0.0], "max": [1.0, 1.0, 0.0]},
{"bufferView": 1, "componentType": 5126, "type": "VEC3", "count": 3}
]
}"#;
struct SoloModels;
impl crate::FootageSource for SoloModels {
fn frame(&self, _: ItemId, _: &effectcraft_project::Footage, _: Tick) -> Option<std::sync::Arc<crate::Image>> {
None
}
fn model(&self, _: ItemId, f: &effectcraft_project::Footage) -> Option<std::sync::Arc<effectcraft_model::Model>> {
effectcraft_model::load(&f.path, TRIANGLE.as_bytes(), &|_| None).ok().map(std::sync::Arc::new)
}
}
/// A red triangle 3D model, a camera panned right by `pan` px and, on top of the stack, the
/// copy: a solid with a 3D Layer Instance effect referencing the model.
fn instance_scene(pan: f64) -> (Project, ItemId) {
let (mut p, cid, comp) = setup();
// Models are Advanced 3D (layer.newModel switches a Classic comp over).
p.comp_mut(cid).unwrap().renderer = effectcraft_project::Renderer::Advanced3D;
let mid = p.add_item(
"Triangle",
Label::Red,
None,
ItemKind::Footage(effectcraft_project::Footage {
path: "Triangle.gltf".into(),
kind: effectcraft_project::FootageKind::Model,
frame_rate: FrameRate::FPS_30,
codec: "glTF".into(),
..Default::default()
}),
);
let model = build::layer(&mut p, &comp, "Triangle", LayerSource::Model { item: mid }, (200, 100), None);
let mut cam = build::layer(&mut p, &comp, "Camera", LayerSource::Camera, (200, 100), None);
for path in ["transform/position", "transform/poi"] {
let pr = cam.props.prop_mut(path).unwrap();
let Value::Vec3(v) = pr.value else { panic!("{path}: {:?}", pr.value) };
pr.value = Value::Vec3([v[0] + pan, v[1], v[2]]);
}
let mut owner = solid(&mut p, &comp, [1.0, 1.0, 1.0], 200, 100);
add_effect_200(&mut p, &mut owner, "ec.3d.instance3d", &[("layer", Value::Layer(Some(model.id.0)))]);
p.comp_mut(cid).unwrap().layers = vec![cam, owner, model];
(p, cid)
}
fn render_models(p: &Project, cid: ItemId, t: Tick, cache: Option<&crate::LayerCache>) -> crate::Image {
let mut r = crate::Renderer::new(p, &SoloModels, crate::RenderOpts::default());
r.cache = cache;
r.comp_frame(cid, t)
}
/// The coverage-weighted centre of the triangle (x): where its drawn pixels are, lit or not.
fn cover_x(img: &crate::Image) -> f32 {
let (mut m, mut sx) = (0.0, 0.0);
for y in 0..img.height {
for x in 0..img.width {
let a = img.get(x as i64, y as i64)[3];
m += a;
sx += a * x as f32;
}
}
assert!(m > 1.0, "the triangle is drawn");
sx / m
}
/// Create 3D Layer Instance: the copy (the owner) is cached, and its key covers what the
/// referenced model's solo render depends on — the camera, and the model's in/out gate.
#[test]
fn an_instance_of_a_model_is_cached_and_follows_the_camera() {
let t = Tick::from_seconds_f64(1.0);
let (p, cid) = instance_scene(0.0);
let (q, _) = instance_scene(40.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_ne!(key_at(&p, t), key_at(&q, t), "moving the camera changes the copy's key");
// The same layer at a later time, outside its in/out range: the read turns transparent.
let mut out = p.clone();
out.comp_mut(cid).unwrap().layers[2].out_point = Tick::from_seconds_f64(0.5);
assert_ne!(key_at(&out, Tick::from_seconds_f64(0.25)), key_at(&out, t), "leaving the model's in/out range changes the copy's key");
// Two renders of one time: the second is served from the cache.
let cache = crate::LayerCache::default();
let here = render_models(&p, cid, t, Some(&cache));
let again = render_models(&p, cid, t, Some(&cache));
assert_same(&again, &render_models(&p, cid, t, None), "the second render is the cached one");
assert!(cache.stats().hits > 0, "the copy was served from the cache");
// Panning the camera: the copy (and the model) move with it.
let panned = render_models(&q, cid, t, Some(&cache));
assert_same(&panned, &render_models(&q, cid, t, None), "cached render after the camera moved");
let (a, b) = (cover_x(&here), cover_x(&panned));
assert!(b < a - 20.0, "panning the camera right moves the copy left: {a} -> {b}");
}
+2
View File
@@ -143,6 +143,7 @@ const SIMPLIFIED_CHINESE: &[Row] = &[
("ec.3d.fog3d", "Fog 3D", "雾 3D"),
("ec.3d.identifier", "IDentifier", "IDentifier"),
("ec.3d.idmatte", "ID Matte", "ID 遮罩"),
("ec.3d.instance3d", "3D Layer Instance", "3D 图层实例"),
("ec.audio.backwards", "Backwards", "倒放"),
("ec.audio.basstreble", "Bass & Treble", "低音和高音"),
("ec.audio.compressor", "Compressor", "压缩程序"),
@@ -519,6 +520,7 @@ const GEORGIAN: &[Row] = &[
("ec.3d.fog3d", "Fog 3D", "3D ნისლი"),
("ec.3d.identifier", "IDentifier", "IDentifier"),
("ec.3d.idmatte", "ID Matte", "ID მატი"),
("ec.3d.instance3d", "3D Layer Instance", "3D Layer Instance"),
("ec.audio.backwards", "Backwards", "უკუღმა"),
("ec.audio.basstreble", "Bass & Treble", "ბასი და ტრებლი"),
("ec.audio.compressor", "Compressor", "კომპრესორი"),
+5 -4
View File
@@ -3,11 +3,11 @@
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
EffectCraft ships 307 effects with 3033 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
EffectCraft ships 308 effects with 3033 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
- **GPU**: 280 effects also run on the GPU compositor with identical results.
- **32**: 307 effects process 32-bit float (HDR, overbright) pixels without clamping.
- **Status**: 307 implemented in full, 0 partial (what is missing is listed).
- **32**: 308 effects process 32-bit float (HDR, overbright) pixels without clamping.
- **Status**: 308 implemented in full, 0 partial (what is missing is listed).
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
@@ -16,6 +16,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| 3D Channel Extract | `ec.3d.channelextract` | 6 | GPU | 32 | Implemented |
| 3D Layer Instance | `ec.3d.instance3d` | 1 | | 32 | Implemented |
| Cryptomatte | `ec.3d.cryptomatte` | 4 | GPU | 32 | Implemented |
| Depth Matte | `ec.3d.depthmatte` | 3 | GPU | 32 | Implemented |
| Depth of Field | `ec.3d.depthoffield` | 4 | GPU | 32 | Implemented |
@@ -69,7 +70,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|---|---|---:|:-:|:-:|---|
| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented |
| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented |
| CC Composite | `ec.channel.cccomposite` | 4 | GPU | 32 | Implemented |
| CC Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented |
| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented |
| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented |
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented |