Light Environment lights: default environment, Casts Shadows, Advanced 3D switch (#2436)

An Environment light had no visible effect, and its Light Settings dialog had
no Casts Shadows option — both halves of #2436.

No effect had two causes:

- Classic 3D (the default for new comps) skips Environment lights entirely,
  so creating one did nothing. Adding one (or changing a light's type to it)
  now switches the comp to Advanced 3D in the same undo step, with a toast,
  exactly as model and primitive layers already do; switching back to
  Classic 3D with an Environment light in the comp explains itself too.
- In Advanced 3D, a light with no Source and no Environment Layer produced no
  environment at all. It now falls back to a flat default environment of the
  light's colour × intensity, as After Effects does, so the light always
  lights the scene per its parameters.

Cast Shadows was missing because the dialog and the light's property groups
shared one gate (Falloff and Shadows together) that excluded Environment.
The two are now split: every light but Ambient gets Casts Shadows, Shadow
Darkness and Shadow Diffusion (stored, edited and carried over on type
changes), while Falloff stays on Parallel/Spot/Point only — an environment
light shines from every direction at once and has no use for a falloff.

Tests: the renderer lights a card from a sourceless Environment light (colour
and intensity drive it, zero intensity lights nothing), the engine switches
Classic comps to Advanced 3D on creation and type change (one undo step,
toast, and the Classic 3D switch-back toast), Environment lights take and
carry shadow options but no Falloff, and the dialog ticks Casts Shadows on
a new Environment light.
This commit is contained in:
Minher0
2026-10-26 09:43:47 +00:00
parent 2961258879
commit 1599790ac9
10 changed files with 199 additions and 28 deletions
+11 -5
View File
@@ -64,6 +64,12 @@ fn comp_renderer(s: &mut Session, p: &Value) -> Result<Value> {
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<bool> {
pub(crate) fn use_advanced_3d(proj: &mut effectcraft_project::Project, cid: ItemId) -> Result<bool> {
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<Value> {
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<Value> {
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}))
}
+18 -7
View File
@@ -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<Value> {
};
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<Value> {
@@ -248,12 +254,14 @@ fn light_settings(s: &mut Session, p: &Value) -> Result<Value> {
),
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<Value> {
}
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<CommandSpec> {
"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<CommandSpec> {
"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
),
+71
View File
@@ -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<String> {
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();
+7 -2
View File
@@ -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
+8 -4
View File
@@ -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);
+39
View File
@@ -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);
+13 -3
View File
@@ -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);
+20 -3
View File
@@ -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();
+7 -2
View File
@@ -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)
+5 -2
View File
@@ -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