From 6d20cc21515db560fab030939dee8a681c468a9f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 20:17:30 +0700 Subject: [PATCH] Engine: adding a 3D model switches a Classic 3D comp to Advanced 3D (#264) Imported GLB/glTF models were invisible in the Composition viewer while text showed. Only the Advanced 3D renderer draws model and primitive layers, and new compositions (and New Comp from Selection) are Classic 3D by default, but adding a model layer left the comp's renderer alone, so the model drew nothing on every backend. It wasn't the reporter's GPU: on this machine the viewer showed nothing for a GLB in a new comp with the GPU compositor (Vulkan) and with Mercury Software Only, and the model in both once the comp used Advanced 3D. As After Effects documents ("Advanced 3D gets automatically selected when you add a 3D model to the composition"), layer.newModel / layer.addItem of a model and layer.newPrimitive now switch a Classic 3D comp to Advanced 3D inside the same edit (one undo step, Geometry Options added to its text and shape layers like Composition Settings does) and toast that the comp changed renderer. Switching a comp that has model layers back to Classic 3D toasts why they disappear. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/model3d.rs | 46 ++++++++++++++++++++------- docs/architecture.md | 3 +- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/engine/src/commands/model3d.rs b/crates/engine/src/commands/model3d.rs index ff2ff1ed..0fd840ff 100644 --- a/crates/engine/src/commands/model3d.rs +++ b/crates/engine/src/commands/model3d.rs @@ -61,6 +61,9 @@ fn comp_renderer(s: &mut Session, p: &Value) -> Result { sync_geometry_options(proj, cid); Ok(()) })?; + if r == Renderer::Classic3D && s.project.comp(cid).is_some_and(|c| c.layers.iter().any(|l| l.source.is_model())) { + s.toast("Classic 3D doesn't draw 3D model layers: they show with the Advanced 3D renderer"); + } Ok(json!({"renderer": renderer_id(r)})) } @@ -71,6 +74,25 @@ fn renderer_id(r: Renderer) -> &'static str { } } +/// Only the Advanced 3D renderer draws model and primitive layers, so adding one switches a +/// Classic 3D comp to it, like After Effects (inside the adding edit: one undo step). Whether +/// the renderer changed. +fn use_advanced_3d(proj: &mut effectcraft_project::Project, cid: ItemId) -> Result { + let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?; + let switched = c.renderer != Renderer::Advanced3D; + c.renderer = Renderer::Advanced3D; + sync_geometry_options(proj, cid); + Ok(switched) +} + +/// Tell the user when [`use_advanced_3d`] changed the comp's renderer. +fn report_switch(s: &mut Session, cid: ItemId, switched: bool) { + if switched { + let name = s.project.item(cid).map_or("the composition", |i| i.name.as_str()).to_string(); + s.toast(format!("\"{name}\" now uses the Advanced 3D renderer, which draws 3D models")); + } +} + // ---------------------------------------------------------------- model layers /// Set a property's value at comp time `t` (adds a key when it is animated). @@ -121,7 +143,7 @@ pub(crate) fn new_model(s: &mut Session, p: &Value) -> Result { let start = fr.snap_nearest(f_p(p, "time").map(Tick::from_seconds_f64).unwrap_or(Tick::ZERO)); let index = index_p(p, "layer.newModel")?; let position = position_p(p, "layer.newModel")?; - let id = s.edit("New 3D Model Layer", None, |proj, st| { + let (id, switched) = s.edit("New 3D Model Layer", None, |proj, st| { let mut l = build::layer(proj, &comp, &name, LayerSource::Model { item }, (comp.width, comp.height), None); let g = build::model_geometry_options(&mut Ids(&mut proj.next_id), scale, &clips); if let Some(slot) = l.props.children.iter_mut().find(|c| c.match_id() == "geometryOptions") { @@ -131,9 +153,11 @@ pub(crate) fn new_model(s: &mut Session, p: &Value) -> Result { l.in_point = start; l.out_point = comp.duration.max(start + fr.frame_duration()); place(&mut l, position); - insert_layer_at(proj, st, cid, l, index) + let id = insert_layer_at(proj, st, cid, l, index)?; + Ok((id, use_advanced_3d(proj, cid)?)) })?; - Ok(json!({"layer": id.0, "item": item.0})) + report_switch(s, cid, switched); + Ok(json!({"layer": id.0, "item": item.0, "advanced3d": true})) } fn kind_p(p: &Value, cmd: &str) -> Result { @@ -187,7 +211,7 @@ fn new_primitive(s: &mut Session, p: &Value) -> Result { let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone(); let name = str_p(p, "name").map(str::to_string).unwrap_or_else(|| kind.label().to_string()); let t = super::time_p(s, p, Some(&comp)); - let id = s.edit(&format!("New {}", kind.label()), None, |proj, st| { + let (id, switched) = s.edit(&format!("New {}", kind.label()), None, |proj, st| { let mut l = build::layer(proj, &comp, &name, LayerSource::Primitive { kind }, (comp.width, comp.height), None); for k in ["width", "height", "depth", "radius", "tubeRadius", "segments", "rings"] { if let Some(v) = f_p(p, k) { @@ -199,11 +223,11 @@ fn new_primitive(s: &mut Session, p: &Value) -> Result { set_at(&mut l, "transform/position", t, KV::Vec3([g(0), g(1), g(2)])); } apply_material(&mut l, p, t, "layer.newPrimitive")?; - insert_layer(proj, st, cid, l) + let id = insert_layer(proj, st, cid, l)?; + Ok((id, use_advanced_3d(proj, cid)?)) })?; - // Primitives are drawn by the Advanced 3D renderer. - let advanced = s.project.comp(cid).is_some_and(|c| c.renderer == Renderer::Advanced3D); - Ok(json!({"layer": id.0, "kind": kind.label(), "advanced3d": advanced})) + report_switch(s, cid, switched); + Ok(json!({"layer": id.0, "kind": kind.label(), "advanced3d": true})) } // ---------------------------------------------------------------- environment layer @@ -352,7 +376,7 @@ pub fn specs() -> Vec { "3D Model Layer", [], None, - "{item?: id or name of a 3D model footage item, path?: a .gltf, .glb or .obj file to import, name?, time? (s), index? (1-based stack position), position? ([x, y] comp px)}", + "{item?: id or name of a 3D model footage item, path?: a .gltf, .glb or .obj file to import, name?, time? (s), index? (1-based stack position), position? ([x, y] comp px)}; switches a Classic 3D comp to Advanced 3D", has_comp, new_model ), @@ -361,7 +385,7 @@ pub fn specs() -> Vec { "3D Primitive", [], None, - "{kind: cube|sphere|plane|torus|cone|cylinder, name?, width?, height?, depth?, radius?, tubeRadius?, segments?, rings?, position? [x,y,z], baseColor?, metallic?, roughness?, emissive?, castsShadows?, acceptsShadows?, acceptsLights?}", + "{kind: cube|sphere|plane|torus|cone|cylinder, name?, width?, height?, depth?, radius?, tubeRadius?, segments?, rings?, position? [x,y,z], baseColor?, metallic?, roughness?, emissive?, castsShadows?, acceptsShadows?, acceptsLights?}; switches a Classic 3D comp to Advanced 3D", has_comp, new_primitive ), @@ -370,7 +394,7 @@ pub fn specs() -> Vec { "New 3D Primitive", [], None, - "{kind: cube|sphere|plane|torus|cone|cylinder, name?, width?, height?, depth?, radius?, tubeRadius?, segments?, rings?, position? [x,y,z], baseColor?, metallic?, roughness?, emissive?, castsShadows?, acceptsShadows?, acceptsLights?}", + "{kind: cube|sphere|plane|torus|cone|cylinder, name?, width?, height?, depth?, radius?, tubeRadius?, segments?, rings?, position? [x,y,z], baseColor?, metallic?, roughness?, emissive?, castsShadows?, acceptsShadows?, acceptsLights?}; switches a Classic 3D comp to Advanced 3D", has_comp, new_primitive ), diff --git a/docs/architecture.md b/docs/architecture.md index 49f9da4a..6d29b294 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -100,7 +100,8 @@ time), primitive layers (Layer ▸ New ▸ Cube/Sphere/Plane/Torus/Cone/Cylinder Geometry Options), text and shape layers with Geometry Options ▸ Extrusion Depth (glyph and fill outlines flattened, triangulated with holes and extruded with Angular/Concave/Convex bevels, per character so per-character 3D carries over), and a textured card for every other -3D layer. Shading is physically based in linear light (glTF metallic-roughness; Cook–Torrance +3D layer. Classic 3D doesn't draw model or primitive layers, so adding one switches a Classic 3D +comp to Advanced 3D in the same undo step, as in After Effects. Shading is physically based in linear light (glTF metallic-roughness; Cook–Torrance GGX specular, Smith–Schlick geometry, Schlick Fresnel, Lambert diffuse, after Karis 2013): parallel, spot and point lights with After Effects' falloffs, ambient lights, image-based light from an Environment light whose Source (or the comp's Environment Layer, Layer ▸