mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
layer.create: the instance3d op - a 2D copy of a 3D model layer (#2100)
Layer > Create > Create 3D Layer Instance, as After Effects (public docs):
3D model layers take no masks or effects, so the command puts a comp-sized
solid at the top of the layer stack with CC Composite referencing the
selected model, and switches the model off (back on to keep its shadow) -
a 2D copy of its render, through the comp's camera, that 2D effects can be
applied to.
- CC Composite (ec.channel.cccomposite) gains an optional Layer parameter:
with a layer chosen it takes the original's place; In Front replaces the
solid's pixels, alpha included, the other Composite Original modes blend
with them. The GPU compositor runs such an instance on the CPU.
- FxHost serves a referenced model or primitive layer as After Effects
does - the layer rendered on its own through the comp's camera
(Renderer::solo_3d); the owner keeps no cache entry for it (it moves with
the camera and lights, without a trace in its own properties).
- layer.create {op: instance3d} skips layers that are not 3D model or
primitive layers (the ones that take no effects); one undo step, the copy
selected, the model's video off.
Tests: tests_instance3d.rs (behaviour, pixels, undo/redo, serde, 2D effects
on the copy), a CC Composite layer-parameter unit test; docs/effects.md
regenerated (3033 parameters).
This commit is contained in:
@@ -171,7 +171,9 @@ const CC_COMPOSITE_MODES: [&str; 16] = [
|
||||
"Stencil Alpha",
|
||||
];
|
||||
|
||||
/// Composites the original stack input over/under/with the result of preceding effects.
|
||||
/// 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.
|
||||
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");
|
||||
@@ -193,11 +195,24 @@ fn cc_composite(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
15 => BlendMode::StencilAlpha,
|
||||
_ => BlendMode::Normal,
|
||||
};
|
||||
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));
|
||||
// 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)));
|
||||
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 m == 1 { blend_pixel(BlendMode::Normal, orig, result, 0.5) } else { blend_pixel(mode, result, orig, 0.5) };
|
||||
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)
|
||||
};
|
||||
o[3] = o[3].clamp(0.0, 1.0);
|
||||
if rgb_only {
|
||||
let (c, _) = unpremul(o);
|
||||
@@ -323,6 +338,7 @@ 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),
|
||||
@@ -423,6 +439,39 @@ 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();
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
//! Layer ▸ Create: Convert to Editable Text (Photoshop type layers), Create Shapes from Text,
|
||||
//! Create Masks from Text and Create Shapes from Vector Layer (SVG footage).
|
||||
//! Create Masks from Text, Create Shapes from Vector Layer (SVG footage) and Create 3D Layer
|
||||
//! Instance (a 2D copy of a 3D model layer that 2D effects can be applied to).
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::Value as KV;
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{ItemKind, LayerSource, Solid};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, has_layers, layers_p, str_p};
|
||||
use crate::{EngineError, Result, Session, cmd};
|
||||
|
||||
const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector";
|
||||
const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector|instance3d";
|
||||
|
||||
fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let op = str_p(p, "op").ok_or_else(|| bad("layer.create", format!("missing `op` ({OPS})")))?;
|
||||
@@ -151,6 +154,49 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})?;
|
||||
created.push(id.0);
|
||||
}
|
||||
"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).
|
||||
if !src.source.is_model() {
|
||||
skipped.push(format!("{}: not a 3D model layer", src.name));
|
||||
continue;
|
||||
}
|
||||
let id = s.edit("Create 3D Layer Instance", None, |proj, st| {
|
||||
let name = comp.unique_layer_name(&format!("{} Instance", src.name));
|
||||
let folder = proj.folder_named("Solids").unwrap_or_else(|| proj.add_item("Solids", Label::Yellow, None, ItemKind::Folder));
|
||||
let sid = proj.add_item(
|
||||
&name,
|
||||
src.label,
|
||||
Some(folder),
|
||||
ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: comp.width, height: comp.height, pixel_aspect: 1.0 }),
|
||||
);
|
||||
let mut l = build::layer(proj, &comp, &name, LayerSource::Solid { item: sid }, (comp.width, comp.height), None);
|
||||
// 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"))?;
|
||||
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));
|
||||
}
|
||||
if let Some(fxg) = l.props.sub_mut("effects") {
|
||||
fxg.children.push(g.into());
|
||||
}
|
||||
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
// The model is switched off (After Effects turns the referenced layer off);
|
||||
// the copy goes at the top of the stack, selected.
|
||||
if let Some(m) = c.layers.iter_mut().find(|x| x.id == lid) {
|
||||
m.switches.video = false;
|
||||
}
|
||||
let id = l.id;
|
||||
c.layers.insert(0, l);
|
||||
st.selected_layers = vec![id];
|
||||
Ok(id)
|
||||
})?;
|
||||
created.push(id.0);
|
||||
}
|
||||
other => return Err(bad("layer.create", format!("`{other}` is not available yet (supported: {OPS})"))),
|
||||
}
|
||||
}
|
||||
@@ -198,5 +244,5 @@ fn insert_above(
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!("layer.create", "Create", [], None, "{op: editableText|shapesFromText|masksFromText|shapesFromVector, layers?}", has_layers, create)]
|
||||
vec![cmd!("layer.create", "Create", [], None, "{op: editableText|shapesFromText|masksFromText|shapesFromVector|instance3d, layers?}", has_layers, create)]
|
||||
}
|
||||
|
||||
@@ -1305,6 +1305,8 @@ mod tests_fuzz_open;
|
||||
#[cfg(test)]
|
||||
mod tests_import_project;
|
||||
#[cfg(test)]
|
||||
mod tests_instance3d;
|
||||
#[cfg(test)]
|
||||
mod tests_key_metadata_order;
|
||||
#[cfg(test)]
|
||||
mod tests_keylight;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
//! 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).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_keyframe::Value as KV;
|
||||
use effectcraft_project::{Footage, FootageKind, GroupKind, ItemId, Layer, LayerId, Project};
|
||||
use effectcraft_render::{FootageSource, RenderOpts};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Importer, Session};
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session { footage: Arc::new(DiskModels), importer: Some(Arc::new(ModelImporter)), ..Default::default() };
|
||||
s.execute("comp.new", json!({"name": "Scene", "width": 640, "height": 360, "frameRate": 30, "duration": 2})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn cid(s: &Session) -> ItemId {
|
||||
s.active_comp_id().unwrap()
|
||||
}
|
||||
|
||||
fn layer_in(s: &Session, comp: ItemId, id: u64) -> Layer {
|
||||
s.project.comp(comp).unwrap().layer(LayerId(id)).unwrap().clone()
|
||||
}
|
||||
|
||||
fn px(img: &effectcraft_raster::Image, x: u32, y: u32) -> [f32; 4] {
|
||||
img.data[(y * img.width + x) as usize]
|
||||
}
|
||||
|
||||
fn close(a: &effectcraft_raster::Image, b: &effectcraft_raster::Image, tol: f32) -> bool {
|
||||
a.data.iter().zip(b.data.iter()).all(|(p, q)| p.iter().zip(q.iter()).all(|(x, y)| (x - y).abs() <= tol))
|
||||
}
|
||||
|
||||
struct DiskModels;
|
||||
|
||||
impl FootageSource for DiskModels {
|
||||
fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<effectcraft_raster::Image>> {
|
||||
None
|
||||
}
|
||||
fn model(&self, _: ItemId, f: &Footage) -> Option<Arc<effectcraft_model::Model>> {
|
||||
let bytes = std::fs::read(&f.path).ok()?;
|
||||
effectcraft_model::load(&f.path, &bytes, &|_| None).ok().map(Arc::new)
|
||||
}
|
||||
}
|
||||
|
||||
struct ModelImporter;
|
||||
|
||||
impl Importer for ModelImporter {
|
||||
fn probe(&self, path: &str) -> Result<Footage, String> {
|
||||
effectcraft_model::format_of(path).ok_or("not a model")?;
|
||||
Ok(Footage { path: path.into(), kind: FootageKind::Model, frame_rate: FrameRate::FPS_30, codec: "glTF".into(), ..Default::default() })
|
||||
}
|
||||
}
|
||||
|
||||
/// A red triangle in the Z = 0 plane (buffer embedded as a data URI).
|
||||
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}
|
||||
]
|
||||
}"#;
|
||||
|
||||
fn triangle_model(s: &mut Session) -> u64 {
|
||||
let dir = std::env::temp_dir().join(format!("ec-instance3d-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("Triangle.gltf");
|
||||
std::fs::write(&path, TRIANGLE).unwrap();
|
||||
s.execute("layer.newModel", json!({"path": path.to_string_lossy(), "name": "Triangle"})).unwrap()["layer"].as_u64().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance3d_creates_a_2d_copy_of_the_model() {
|
||||
let mut s = session();
|
||||
let model = triangle_model(&mut s);
|
||||
// The model renders through the comp's camera.
|
||||
let before = s.render(cid(&s), Tick::ZERO, RenderOpts::default());
|
||||
assert!(px(&before, 320, 180)[3] > 0.9, "the model covers the centre: {:?}", px(&before, 320, 180));
|
||||
assert!(px(&before, 4, 4)[3] < 0.1, "the comp is transparent around it: {:?}", px(&before, 4, 4));
|
||||
// Create 3D Layer Instance.
|
||||
s.execute("layer.select", json!({"layers": [model]})).unwrap();
|
||||
let r = s.execute("layer.create", json!({"op": "instance3d"})).unwrap();
|
||||
let solid = r["layers"][0].as_u64().unwrap();
|
||||
let comp = cid(&s);
|
||||
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.
|
||||
let g = l.effects().unwrap().groups().next().unwrap().clone();
|
||||
assert!(matches!(&g.kind, GroupKind::Effect { effect } if effect == "ec.channel.cccomposite"), "{:?}", 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");
|
||||
assert_eq!(s.state.selected_layers, vec![LayerId(solid)]);
|
||||
// The comp renders the same through the copy: a 2D copy of the 3D layer, transparent
|
||||
// around it.
|
||||
let after = s.render(comp, Tick::ZERO, RenderOpts::default());
|
||||
assert!(close(&before, &after, 2e-2), "the copy shows the model:\n{:?}\n{:?}", px(&before, 320, 180), px(&after, 320, 180));
|
||||
assert!(px(&after, 320, 180)[3] > 0.9 && px(&after, 4, 4)[3] < 0.1, "{:?} {:?}", px(&after, 320, 180), px(&after, 4, 4));
|
||||
// Undo removes the copy and switches the model back on; redo returns it.
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(s.active_comp().unwrap().layer(LayerId(solid)).is_none());
|
||||
assert!(layer_in(&s, comp, model).switches.video);
|
||||
s.execute("edit.redo", json!({})).unwrap();
|
||||
assert!(s.active_comp().unwrap().layer(LayerId(solid)).is_some());
|
||||
assert!(!layer_in(&s, comp, model).switches.video);
|
||||
// Serde round-trips the layer parameter.
|
||||
let j = serde_json::to_string(&*s.project).unwrap();
|
||||
let back: Project = serde_json::from_str(&j).unwrap();
|
||||
assert_eq!(back, *s.project);
|
||||
// 2D effects can be applied to the copy (the model layer itself takes none): Fill in blue
|
||||
// recolours the triangle.
|
||||
s.execute("effect.apply", json!({"layer": solid, "effect": "ec.generate.fill"})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": solid, "path": "effects/#2/color", "value": [0.0, 0.0, 1.0, 1.0]})).unwrap();
|
||||
let with_fx = s.render(comp, Tick::ZERO, RenderOpts::default());
|
||||
let p = px(&with_fx, 320, 180);
|
||||
assert!(p[2] > 0.9 && p[0] < 0.1, "the fill recolours the copy: {p:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance3d_also_takes_3d_primitives() {
|
||||
let mut s = session();
|
||||
let prim = s.execute("layer.new3dPrimitive", json!({"kind": "cube", "width": 200, "height": 200, "depth": 200})).unwrap()["layer"].as_u64().unwrap();
|
||||
let before = s.render(cid(&s), Tick::ZERO, RenderOpts::default());
|
||||
let r = s.execute("layer.create", json!({"op": "instance3d", "layers": [prim]})).unwrap();
|
||||
let solid = r["layers"][0].as_u64().unwrap();
|
||||
let l = layer_in(&s, cid(&s), solid);
|
||||
assert_eq!(l.name, "Cube Instance");
|
||||
assert!(!layer_in(&s, cid(&s), prim).switches.video);
|
||||
let after = s.render(cid(&s), Tick::ZERO, RenderOpts::default());
|
||||
assert!(close(&before, &after, 2e-2), "the copy shows the cube");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance3d_needs_a_3d_layer() {
|
||||
let mut s = session();
|
||||
let solid = s.execute("layer.newSolid", json!({"color": "#808080", "width": 100, "height": 100})).unwrap()["layer"].as_u64().unwrap();
|
||||
let err = s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [solid]}));
|
||||
assert!(err.is_err());
|
||||
let text = s.execute("layer.newText", json!({"text": "hi"})).unwrap()["layer"].as_u64().unwrap();
|
||||
assert!(s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [text]})).is_err());
|
||||
// A 3D-switched text layer is not a model layer either (it has 2D pixels of its own).
|
||||
s.execute("layer.setSwitch", json!({"layers": [text], "switch": "threeD", "value": true})).unwrap();
|
||||
assert!(s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [text]})).is_err());
|
||||
}
|
||||
@@ -247,6 +247,12 @@ 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)?);
|
||||
|
||||
+7
@@ -68,6 +68,13 @@ impl Dependencies<'_> {
|
||||
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.
|
||||
if layer.source.is_model() {
|
||||
return None;
|
||||
}
|
||||
self.remaining = self.remaining.checked_sub(1)?;
|
||||
if (self.other_times || super::reads_other_times(layer)) && super::has_expression(&layer.props) {
|
||||
return None;
|
||||
|
||||
@@ -277,6 +277,14 @@ impl EffectHost for FxHost<'_, '_, '_> {
|
||||
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 CC Composite), 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))
|
||||
@@ -1493,6 +1501,25 @@ impl<'a> Renderer<'a> {
|
||||
self.source(ctx, layer)
|
||||
}
|
||||
|
||||
/// A 3D model or primitive layer rendered on its own, through the comp's camera: what an
|
||||
/// effect layer parameter reads when it references one, as After Effects, which renders 3D
|
||||
/// layers through the active camera. The comp's pixels at this render's scale; the layer's
|
||||
/// blend mode, track matte and video switch don't apply (a source read, not a composite —
|
||||
/// the solo clone of a 3D track matte, see `three_d::adv`).
|
||||
fn solo_3d(&self, ctx: &EvalCtx, layer: &Layer) -> Buf {
|
||||
let s = self.opts.scale;
|
||||
let w = ((ctx.comp.width as f64 * s).ceil() as u32).max(1);
|
||||
let h = ((ctx.comp.height as f64 * s).ceil() as u32).max(1);
|
||||
let mut solo = layer.clone();
|
||||
solo.track_matte = None;
|
||||
solo.blend_mode = effectcraft_color::BlendMode::Normal;
|
||||
solo.preserve_transparency = false;
|
||||
solo.switches.video = true;
|
||||
let mut canvas = Image::new(w, h);
|
||||
three_d::compose::draw_run(self, ctx, &[&solo], &mut canvas);
|
||||
Buf { img: canvas, offset: [0.0; 2], scale: s }
|
||||
}
|
||||
|
||||
/// Layer pixels after source → masks → effects (no layer styles).
|
||||
pub fn content_buf(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Arc<Buf>> {
|
||||
self.content_buf_timed(ctx, layer, None).map(|(b, _)| b)
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
<!-- 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 3032 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 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.
|
||||
|
||||
- **GPU**: 280 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 307 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
@@ -69,7 +69,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` | 3 | GPU | 32 | Implemented |
|
||||
| CC Composite | `ec.channel.cccomposite` | 4 | 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 |
|
||||
|
||||
Reference in New Issue
Block a user