diff --git a/crates/effects/src/channel2.rs b/crates/effects/src/channel2.rs index 1c56ea2bd..5be3c94e7 100644 --- a/crates/effects/src/channel2.rs +++ b/crates/effects/src/channel2.rs @@ -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 { "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(); diff --git a/crates/effects/src/channel3d.rs b/crates/effects/src/channel3d.rs index 08241107c..dc65c3255 100644 --- a/crates/effects/src/channel3d.rs +++ b/crates/effects/src/channel3d.rs @@ -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 { 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 { ], identifier, ), + spec("ec.3d.instance3d", "3D Layer Instance", vec![p("layer", "Layer", Value::Layer(None), ParamUi::Layer)], instance_3d), ] } diff --git a/crates/effects/src/channel3d/tests.rs b/crates/effects/src/channel3d/tests.rs index 4fc84440e..e69c2a6aa 100644 --- a/crates/effects/src/channel3d/tests.rs +++ b/crates/effects/src/channel3d/tests.rs @@ -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 { + (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> { + 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); +} diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 246108371..6c4b02c87 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -27,9 +27,9 @@ fn create(s: &mut Session, p: &Value) -> Result { 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 { // 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)); diff --git a/crates/engine/src/tests/instance3d.rs b/crates/engine/src/tests/instance3d.rs index 73577a0a8..48f4b7730 100644 --- a/crates/engine/src/tests/instance3d.rs +++ b/crates/engine/src/tests/instance3d.rs @@ -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"); diff --git a/crates/gpu/src/fx_pixel2.rs b/crates/gpu/src/fx_pixel2.rs index e764e83d0..acba936d0 100644 --- a/crates/gpu/src/fx_pixel2.rs +++ b/crates/gpu/src/fx_pixel2.rs @@ -247,12 +247,6 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { 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)?); diff --git a/crates/render/src/cache/dependencies.rs b/crates/render/src/cache/dependencies.rs index 903a68a14..5c45c9652 100644 --- a/crates/render/src/cache/dependencies.rs +++ b/crates/render/src/cache/dependencies.rs @@ -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) { diff --git a/crates/render/src/fx_host.rs b/crates/render/src/fx_host.rs index b111daf64..f09db80b3 100644 --- a/crates/render/src/fx_host.rs +++ b/crates/render/src/fx_host.rs @@ -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); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 079274499..8857a739c 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -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> { + None + } + fn model(&self, _: ItemId, f: &effectcraft_project::Footage) -> Option> { + 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}"); +} diff --git a/crates/ui-egui/src/i18n/effects.rs b/crates/ui-egui/src/i18n/effects.rs index 162300f9c..41a4c762f 100644 --- a/crates/ui-egui/src/i18n/effects.rs +++ b/crates/ui-egui/src/i18n/effects.rs @@ -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", "კომპრესორი"), diff --git a/docs/effects.md b/docs/effects.md index 7ed4a6d62..c6fb40922 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -3,11 +3,11 @@ -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..`) 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..`) 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..`) 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 |