diff --git a/crates/engine/src/commands/model3d.rs b/crates/engine/src/commands/model3d.rs index 28b7274ef..1994aef13 100644 --- a/crates/engine/src/commands/model3d.rs +++ b/crates/engine/src/commands/model3d.rs @@ -64,6 +64,12 @@ fn comp_renderer(s: &mut Session, p: &Value) -> Result { 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"); } + if r == Renderer::Classic3D { + let env = |l: &Layer| matches!(l.source, LayerSource::Light { kind: effectcraft_project::LightKind::Environment }); + if s.project.comp(cid).is_some_and(|c| c.layers.iter().any(env)) { + s.toast("Classic 3D doesn't light Environment light layers: they light with the Advanced 3D renderer"); + } + } Ok(json!({"renderer": renderer_id(r)})) } @@ -77,7 +83,7 @@ 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 { +pub(crate) 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; @@ -86,10 +92,10 @@ fn use_advanced_3d(proj: &mut effectcraft_project::Project, cid: ItemId) -> Resu } /// Tell the user when [`use_advanced_3d`] changed the comp's renderer. -fn report_switch(s: &mut Session, cid: ItemId, switched: bool) { +pub(crate) fn report_switch(s: &mut Session, cid: ItemId, switched: bool, because: &str) { 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")); + s.toast(format!("\"{name}\" now uses the Advanced 3D renderer, which {because}")); } } @@ -156,7 +162,7 @@ pub(crate) fn new_model(s: &mut Session, p: &Value) -> Result { let id = insert_layer_at(proj, st, cid, l, index)?; Ok((id, use_advanced_3d(proj, cid)?)) })?; - report_switch(s, cid, switched); + report_switch(s, cid, switched, "draws 3D models"); Ok(json!({"layer": id.0, "item": item.0, "advanced3d": true})) } @@ -226,7 +232,7 @@ fn new_primitive(s: &mut Session, p: &Value) -> Result { let id = insert_layer(proj, st, cid, l)?; Ok((id, use_advanced_3d(proj, cid)?)) })?; - report_switch(s, cid, switched); + report_switch(s, cid, switched, "draws 3D models"); Ok(json!({"layer": id.0, "kind": kind.label(), "advanced3d": true})) } diff --git a/crates/engine/src/commands/three_d.rs b/crates/engine/src/commands/three_d.rs index e143690d3..6e1f2d936 100644 --- a/crates/engine/src/commands/three_d.rs +++ b/crates/engine/src/commands/three_d.rs @@ -12,6 +12,7 @@ use effectcraft_time::Tick; use serde_json::{Value, json}; use super::layer::{color_p, insert_layer}; +use super::model3d::{report_switch, use_advanced_3d}; use super::{CommandSpec, b_p, bad, comp_id, f_p, has_comp, layer_mut, layer_p, merge_p, str_p, time_p}; use crate::{EngineError, Result, Session, cmd, query}; @@ -227,12 +228,17 @@ fn new_light(s: &mut Session, p: &Value) -> Result { }; let name = str_p(p, "name").unwrap_or("Light 1").to_string(); let t = time_p(s, p, Some(&comp)); - let id = s.edit("New Light", None, |proj, st| { + let env = kind == LightKind::Environment; + let (id, switched) = s.edit("New Light", None, |proj, st| { let mut l = build::layer(proj, &comp, &name, LayerSource::Light { kind }, (comp.width, comp.height), None); apply_light(&mut l, p, t, "layer.newLight")?; - insert_layer(proj, st, cid, l) + let id = insert_layer(proj, st, cid, l)?; + // An Environment light's image-based light only exists in Advanced 3D, so adding one + // switches a Classic 3D comp to it, as model layers do (#2436). + Ok((id, env && use_advanced_3d(proj, cid)?)) })?; - Ok(json!({"layer": id.0})) + report_switch(s, cid, switched, "lights Environment lights"); + Ok(json!({"layer": id.0, "advanced3d": env})) } fn light_settings(s: &mut Session, p: &Value) -> Result { @@ -248,12 +254,14 @@ fn light_settings(s: &mut Session, p: &Value) -> Result { ), None => None, }; - s.edit("Light Settings", None, |proj, _| { + let switched = s.edit("Light Settings", None, |proj, _| { let old = proj.comp(cid).and_then(|c| c.layer(lid)).cloned().ok_or(EngineError::NoComp)?; let mut l = old.clone(); + let mut env = false; if let Some(k) = kind && old.source != (LayerSource::Light { kind: k }) { + env = k == LightKind::Environment; // Light Type change: rebuild the light's groups, carrying over shared values. let fresh = build::layer(proj, &comp, &old.name, LayerSource::Light { kind: k }, (comp.width, comp.height), None); l.source = fresh.source.clone(); @@ -289,8 +297,11 @@ fn light_settings(s: &mut Session, p: &Value) -> Result { } apply_light(&mut l, p, t, "layer.lightSettings")?; *layer_mut(proj, cid, lid)? = l; - Ok(()) + // Changing the type to Environment: its image-based light only exists in Advanced 3D, + // so the comp switches to it, as model layers do (#2436). + Ok(env && use_advanced_3d(proj, cid)?) })?; + report_switch(s, cid, switched, "lights Environment lights"); Ok(Value::Null) } @@ -681,7 +692,7 @@ pub fn specs() -> Vec { "Light...", ["Layer", "New"], Some("Cmd+Alt+Shift+L"), - "{kind?: Parallel|Spot|Point|Ambient|Environment (default Spot; Environment lights and reflects from an equirectangular image layer: prop.set its lightOptions/source to that layer's id, or leave it empty for the comp's Environment Layer, see layer.environment), name?, color?, intensity?, coneAngle?, coneFeather?, falloff?: None|Smooth|Inverse Square Clamped, radius?, falloffDistance?, castsShadows?, shadowDarkness?, shadowDiffusion?, position? [x,y,z], poi? [x,y,z]}", + "{kind?: Parallel|Spot|Point|Ambient|Environment (default Spot; Environment lights and reflects from an equirectangular image layer: prop.set its lightOptions/source to that layer's id, or leave it empty for the comp's Environment Layer or a flat default environment of its colour and intensity, see layer.environment; an Environment light switches a Classic 3D comp to Advanced 3D), name?, color?, intensity?, coneAngle?, coneFeather?, falloff?: None|Smooth|Inverse Square Clamped, radius?, falloffDistance?, castsShadows?, shadowDarkness?, shadowDiffusion?, position? [x,y,z], poi? [x,y,z]}", has_comp, new_light ), @@ -708,7 +719,7 @@ pub fn specs() -> Vec { "Light Settings...", [], None, - "{layer?, name?, kind?: Parallel|Spot|Point|Ambient|Environment, color?, intensity?, coneAngle?, coneFeather?, falloff?, radius?, falloffDistance?, castsShadows?, shadowDarkness?, shadowDiffusion?, position?, poi?}", + "{layer?, name?, kind?: Parallel|Spot|Point|Ambient|Environment (changing to Environment switches a Classic 3D comp to Advanced 3D), color?, intensity?, coneAngle?, coneFeather?, falloff?, radius?, falloffDistance?, castsShadows?, shadowDarkness?, shadowDiffusion?, position?, poi?}", has_comp, light_settings ), diff --git a/crates/engine/src/tests_3d.rs b/crates/engine/src/tests_3d.rs index 07a48af1c..1b5deceda 100644 --- a/crates/engine/src/tests_3d.rs +++ b/crates/engine/src/tests_3d.rs @@ -148,6 +148,77 @@ fn deleting_a_source_layer_clears_the_reference() { assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(sky)).is_some()); } +/// An Environment light only lights Advanced 3D comps, so adding one (or changing a light's type +/// to Environment) switches a Classic 3D comp to it in the same undo step, with a toast, as +/// model layers do (#2436). +#[test] +fn environment_light_switches_classic_comps_to_advanced_3d() { + let toasts = |s: &mut Session| -> Vec { + s.drain_events().into_iter().filter_map(|e| if let crate::Event::Toast { message, .. } = e { Some(message) } else { None }).collect() + }; + let mut s = session(); + let text = s.execute("layer.newText", json!({"text": "Hi"})).unwrap()["layer"].as_u64().unwrap(); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Classic3D); + toasts(&mut s); + let r = s.execute("layer.newLight", json!({"kind": "Environment"})).unwrap(); + assert_eq!(r["advanced3d"], true); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Advanced3D); + assert!(toasts(&mut s).iter().any(|m| m.contains("Advanced 3D")), "the switch is explained"); + // A second Environment light leaves the comp (and the user) alone. + s.execute("layer.newLight", json!({"kind": "Environment"})).unwrap(); + assert!(toasts(&mut s).is_empty()); + // One undo step removes the light and the switch together. + s.undo(); + assert_eq!(s.active_comp().unwrap().layers.len(), 2, "the second light is gone, the first stays"); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Advanced3D, "the first light's switch still stands"); + s.undo(); + assert_eq!(s.active_comp().unwrap().layers.len(), 1); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Classic3D); + assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(text)).is_some()); + // Changing a light's type to Environment switches too, and undoes back. + let spot = s.execute("layer.newLight", json!({"kind": "Spot"})).unwrap()["layer"].as_u64().unwrap(); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Classic3D); + s.execute("layer.lightSettings", json!({"layer": spot, "kind": "Environment"})).unwrap(); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Advanced3D); + s.undo(); + assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Classic3D); + assert_eq!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(spot)).unwrap().source, LayerSource::Light { kind: LightKind::Spot }); + // Switching back to Classic 3D with an Environment light in the comp explains itself. + s.execute("layer.newLight", json!({"kind": "Environment"})).unwrap(); + toasts(&mut s); + s.execute("comp.renderer", json!({"renderer": "classic3d"})).unwrap(); + assert!(toasts(&mut s).iter().any(|m| m.contains("Environment light")), "the Classic 3D limit is explained"); +} + +/// An Environment light takes Casts Shadows (with Shadow Darkness and Diffusion) like every +/// other light — the Light Settings dialog offers them for all types but Ambient — and keeps no +/// Falloff, which an environment light shining from every direction has no use for (#2436). +#[test] +fn environment_light_takes_shadow_options_but_no_falloff() { + let mut s = session(); + let env = s.execute("layer.newLight", json!({"kind": "Environment", "castsShadows": true, "shadowDarkness": 60, "shadowDiffusion": 12})).unwrap()["layer"] + .as_u64() + .unwrap(); + assert!(prop(&s, env, "lightOptions/castsShadows").as_bool()); + assert_eq!(prop(&s, env, "lightOptions/shadowDarkness").as_f64(), 60.0); + assert_eq!(prop(&s, env, "lightOptions/shadowDiffusion").as_f64(), 12.0); + let l = s.active_comp().unwrap().layer(effectcraft_project::LayerId(env)).unwrap(); + assert!(l.props.prop("lightOptions/falloff").is_none(), "an environment light has no Falloff"); + // The shadow values carry over when the type changes, both ways. + s.execute("layer.lightSettings", json!({"layer": env, "kind": "Spot", "coneAngle": 70})).unwrap(); + assert!(prop(&s, env, "lightOptions/castsShadows").as_bool()); + assert_eq!(prop(&s, env, "lightOptions/shadowDarkness").as_f64(), 60.0); + assert_eq!(prop(&s, env, "lightOptions/coneAngle").as_f64(), 70.0); + s.execute("layer.lightSettings", json!({"layer": env, "kind": "Environment"})).unwrap(); + assert!(prop(&s, env, "lightOptions/castsShadows").as_bool()); + assert_eq!(prop(&s, env, "lightOptions/source").as_layer(), None, "back to Environment: it has a Source again"); + s.undo(); + assert!( + s.active_comp().unwrap().layer(effectcraft_project::LayerId(env)).unwrap().props.prop("lightOptions/coneAngle").is_some(), + "back to Spot: it has a Cone Angle again" + ); +} + #[test] fn three_d_switch_undo_redo() { let mut s = session(); diff --git a/crates/project/src/build.rs b/crates/project/src/build.rs index 935c03b0e..d01116816 100644 --- a/crates/project/src/build.rs +++ b/crates/project/src/build.rs @@ -654,10 +654,15 @@ pub fn light_options(ids: &mut Ids, kind: LightKind) -> PropGroup { } if kind == LightKind::Environment { // The environment image: a layer (footage, comp…) mapped as an equirectangular panorama; - // none = the comp's Environment Layer. + // none = the comp's Environment Layer, or a flat default environment of the light's + // colour × intensity when there is none. No Falloff: an environment light shines from + // every direction at once (#2436). return g .with(ids.prop("source", "Source", Value::Layer(None)).with_ui(ParamUi::Layer)) - .with(ids.prop("rotation", "Environment Rotation", Value::Scalar(0.0)).with_ui(ParamUi::Angle)); + .with(ids.prop("rotation", "Environment Rotation", Value::Scalar(0.0)).with_ui(ParamUi::Angle)) + .with(ids.prop("castsShadows", "Casts Shadows", Value::Bool(false)).with_ui(ParamUi::Checkbox)) + .with(ids.prop("shadowDarkness", "Shadow Darkness", Value::Scalar(100.0)).with_ui(ParamUi::Percent)) + .with(ids.prop("shadowDiffusion", "Shadow Diffusion", Value::Scalar(0.0)).with_ui(ParamUi::Pixels)); } if kind != LightKind::Ambient { g = g diff --git a/crates/render/src/three_d/adv/scene.rs b/crates/render/src/three_d/adv/scene.rs index a97272434..56a5e6605 100644 --- a/crates/render/src/three_d/adv/scene.rs +++ b/crates/render/src/three_d/adv/scene.rs @@ -935,10 +935,14 @@ pub(crate) fn build_at(r: &Renderer, ctx: &EvalCtx, sub: Option<&EvalCtx>, run: let (ls, amb, env) = lights(ctx, lin); b.s.lights = ls; b.s.ambient = amb; - if let Some((src, k, rot)) = env - && let Some((w, h, d)) = env_pixels(r, ctx, src, lin) - { - add_environment(&mut b, w, h, d, k, rot); + if let Some((src, k, rot)) = env { + match env_pixels(r, ctx, src, lin) { + Some((w, h, d)) => add_environment(&mut b, w, h, d, k, rot), + // No Source and no Environment Layer: a flat default environment, as After + // Effects does — the light's colour × intensity light the scene from every + // direction (#2436). + None => add_environment(&mut b, 8, 4, vec![[1.0, 1.0, 1.0, 1.0]; 32], k, rot), + } } if r.opts.shadows() { super::raster::shadow_maps(&mut b.s, r.opts.draft); diff --git a/crates/render/src/three_d/adv/tests.rs b/crates/render/src/three_d/adv/tests.rs index 3f5ee4813..2fb22a6e4 100644 --- a/crates/render/src/three_d/adv/tests.rs +++ b/crates/render/src/three_d/adv/tests.rs @@ -337,6 +337,45 @@ fn cards_and_environment_light() { assert_eq!(lit.data[80 * 120 + 110][3], 0.0); } +/// An Environment light with no Source and no Environment Layer lights the scene with a flat +/// default environment of its colour × intensity, as After Effects does (#2436) — not nothing +/// at all. +#[test] +fn environment_light_without_a_source_lights_the_scene() { + let (mut p, cid) = project(120, 90); + let sid = p.add_item( + "S", + Default::default(), + None, + ItemKind::Solid(effectcraft_project::Solid { color: [0.5, 0.5, 0.5], width: 60, height: 40, pixel_aspect: 1.0 }), + ); + add(&mut p, cid, LayerSource::Solid { item: sid }, |l| l.switches.three_d = true); + let unlit = render(&p, cid); + let c = unlit.data[20 * 120 + 30]; + assert!((c[0] - 0.5).abs() < 0.01 && c[3] > 0.99, "unlit card keeps its colour: {c:?}"); + // No Source, no Environment Layer: the light still lights the card (a flat white + // environment at the light's colour × intensity; a card's Material Options shade it). + add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |_| {}); + let lit = render(&p, cid); + let c2 = lit.data[20 * 120 + 30]; + assert!((c2[0] - c[0]).abs() > 0.05 && c2[3] > 0.99, "the light has an effect: {c2:?} vs {c:?}"); + // The light's parameters drive it: half intensity darkens towards zero intensity's black. + set(p.comp_mut(cid).unwrap().layers.first_mut().unwrap(), "lightOptions/intensity", Value::Scalar(50.0)); + let half = render(&p, cid); + let c3 = half.data[20 * 120 + 30]; + assert!(c3[0] < c2[0] - 0.02, "half intensity lights less: {c3:?} vs {c2:?}"); + set(p.comp_mut(cid).unwrap().layers.first_mut().unwrap(), "lightOptions/intensity", Value::Scalar(0.0)); + let dark = render(&p, cid); + let c4 = dark.data[20 * 120 + 30]; + assert!(c4[0] < 0.02 && c4[3] > 0.99, "zero intensity lights nothing: {c4:?}"); + // And its colour tints the light red. + set(p.comp_mut(cid).unwrap().layers.first_mut().unwrap(), "lightOptions/intensity", Value::Scalar(100.0)); + set(p.comp_mut(cid).unwrap().layers.first_mut().unwrap(), "lightOptions/color", Value::Color([1.0, 0.0, 0.0, 1.0])); + let red = render(&p, cid); + let c5 = red.data[20 * 120 + 30]; + assert!(c5[0] > 0.15 && c5[1] < 0.05 && c5[2] < 0.05, "red environment: {c5:?}"); +} + #[test] fn depth_of_field_blurs_out_of_focus_surfaces() { let mut img = Image::new(64, 64); diff --git a/crates/ui-egui/src/panels/dialogs_3d.rs b/crates/ui-egui/src/panels/dialogs_3d.rs index 01fd5b9e8..80e18ce30 100644 --- a/crates/ui-egui/src/panels/dialogs_3d.rs +++ b/crates/ui-egui/src/panels/dialogs_3d.rs @@ -44,12 +44,18 @@ pub struct LightDraft { const FALLOFFS: [&str; 3] = ["None", "Smooth", "Inverse Square Clamped"]; -/// Whether a light type has Falloff and Shadows options. Ambient and Environment lights come -/// from no one place, so they have neither. +/// Whether a light type has Falloff options: Ambient and Environment lights come from no one +/// place, so they have none — an environment light shines from every direction at once. fn falls_off(kind: LightKind) -> bool { !matches!(kind, LightKind::Ambient | LightKind::Environment) } +/// Whether a light type has Shadows options. Every light but Ambient can cast shadows, +/// Environment lights included (their image-based light occludes) — as in After Effects (#2436). +fn casts_shadows(kind: LightKind) -> bool { + kind != LightKind::Ambient +} + fn comp_w(app: &EffectcraftApp) -> f64 { app.session.active_comp().map(|c| c.width as f64).unwrap_or(1920.0) } @@ -306,6 +312,8 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px"))); ui.end_row(); } + } + if casts_shadows(d.kind) { ui.label(tr("Shadows")); ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows")); ui.end_row(); @@ -318,7 +326,7 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { } }); ui.add_space(14.0); - if falls_off(d.kind) { + if casts_shadows(d.kind) { ui.label(egui::RichText::new(tr("Note: Shadows are only cast from layers with Cast Shadows enabled.")).font(Tokens::ui(11.0)).color(t.text_dim)); ui.add_space(8.0); } @@ -343,6 +351,8 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { p["falloff"] = json!(d.falloff); p["radius"] = json!(d.radius); p["falloffDistance"] = json!(d.falloff_distance); + } + if casts_shadows(kind) { p["castsShadows"] = json!(d.casts_shadows); p["shadowDarkness"] = json!(d.shadow_darkness); p["shadowDiffusion"] = json!(d.shadow_diffusion); diff --git a/crates/ui-egui/tests/it/ui_light_settings.rs b/crates/ui-egui/tests/it/ui_light_settings.rs index fe1392be1..0183e0324 100644 --- a/crates/ui-egui/tests/it/ui_light_settings.rs +++ b/crates/ui-egui/tests/it/ui_light_settings.rs @@ -177,13 +177,30 @@ fn falloff_and_shadow_options_show_only_for_lights_that_have_them() { invoke(&mut h, "layer.newLight", json!({})); for k in LightKind::ALL { choose(&mut h, k); - let shown = !matches!(k, LightKind::Ambient | LightKind::Environment); - for label in ["Falloff", "Casts Shadows", "Shadow Darkness"] { - assert_eq!(h.query_by_label(label).is_some(), shown, "{label} for {}", k.label()); + let falloff = !matches!(k, LightKind::Ambient | LightKind::Environment); + let shadows = k != LightKind::Ambient; + assert_eq!(h.query_by_label("Falloff").is_some(), falloff, "Falloff for {}", k.label()); + for label in ["Casts Shadows", "Shadow Darkness"] { + assert_eq!(h.query_by_label(label).is_some(), shadows, "{label} for {}", k.label()); } } } +/// An Environment light created through the dialog takes a Casts Shadows tick, like the other +/// light types (only Ambient has none) — #2436. +#[test] +fn an_environment_light_created_with_casts_shadow_takes_it() { + let mut h = harness(); + invoke(&mut h, "layer.newLight", json!({})); + choose(&mut h, LightKind::Environment); + h.get_by_label("Casts Shadows").click(); + ok(&mut h); + let id = last_layer(&h); + assert_eq!(kind(&h, id), LightKind::Environment); + assert!(layer(&h, id).props.prop("lightOptions/castsShadows").unwrap().value.as_bool()); + assert!(layer(&h, id).props.prop("lightOptions/falloff").is_none(), "but no Falloff"); +} + #[test] fn changing_an_environment_light_to_spot_undoes_to_its_source_and_rotation() { let mut h = harness(); diff --git a/docs/agents.md b/docs/agents.md index aae8be12c..b14c135fc 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -212,8 +212,13 @@ Things that tripped up a long agent-driven session (a multi-scene 3D piece built or call `save_project {"path": …}` after each milestone. - **Environment light.** `layer.newLight` also accepts `"kind":"Environment"`: an equirectangular image that metallic surfaces reflect. Set its `lightOptions/source` to a footage, comp or solid - layer by layer id; with no source it uses the comp's Environment Layer (`layer.environment`) - ([#261](https://github.com/storytold/effectcraft/issues/261)). + layer by layer id; with no source it uses the comp's Environment Layer (`layer.environment`), + or a flat default environment of the light's colour × intensity when there is none — either + way the light brightens the scene, and it has `castsShadows`/`shadowDarkness`/`shadowDiffusion` + like the other lights. It only lights Advanced 3D comps, so creating one (or changing a light's + type to Environment) switches a Classic 3D comp to Advanced 3D + ([#261](https://github.com/storytold/effectcraft/issues/261), + [#2436](https://github.com/storytold/effectcraft/issues/2436)). ### Cookbook (from end-to-end QA) diff --git a/docs/architecture.md b/docs/architecture.md index d3dd4db69..9b8964505 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -158,9 +158,12 @@ comp to Advanced 3D in the same undo step, as in After Effects. Shading is physi 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 ▸ -Environment Layer) is an equirectangular image (cosine irradiance map; roughness-indexed mip +Environment Layer, or a flat default environment of the light's colour × intensity when +there is neither) is an equirectangular image (cosine irradiance map; roughness-indexed mip chain with the split-sum environment BRDF fit), and percentage-closer-filtered shadow maps -(spot: perspective, parallel: orthographic over the scene, point: six cube faces). Cards keep +(spot: perspective, parallel: orthographic over the scene, point: six cube faces). An +Environment light only lights Advanced 3D comps, so adding one (or changing a light's type to +it) switches a Classic 3D comp to Advanced 3D, as model layers do. Cards keep Material Options (Accepts Lights/Shadows, Casts Shadows On/Only, Ambient, Diffuse, Specular); models and primitives add Base Color, Metallic, Roughness, Emissive. Rasterisation runs at 2×2 supersampling with a reversed-Z depth buffer; opaque triangles resolve visibility first, the