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 ▸