Cast shadows from the Environment light's default environment (#2436)

After Effects' default environment map casts shadows out of the box: an
Environment light with Cast Shadows on shadows the scene with no Source
set. The flat default cancelled its dominant direction to zero, so the
option did nothing until a Source image arrived. Give the default a soft
studio gradient (bright above and toward the initial camera, dark below
and behind), level-normalised so the light's colour x intensity still
sets the overall brightness; its dominant region drives the existing
shadow pipeline, CPU and GPU alike.
This commit is contained in:
Minher0
2026-10-26 20:26:03 +00:00
parent 95e6ca9a2d
commit aff46dabb6
4 changed files with 106 additions and 21 deletions
+43 -15
View File
@@ -927,6 +927,41 @@ fn env_pixels(r: &Renderer, ctx: &EvalCtx, src: Option<u64>, lin: bool) -> Optio
Some((img.width, img.height, data))
}
/// The default environment when the light has no Source and no Environment Layer: a soft studio
/// gradient, bright above and toward the initial camera, dark below and behind it — After
/// Effects "uses a default environment map" for an Environment light too, and its default
/// casts shadows out of the box. The solid-angle-weighted mean is 1, so the light's colour ×
/// intensity still sets the overall level, like the flat default it replaces; the light's
/// Rotation turns the key around the scene (#2436).
pub(crate) fn default_environment() -> (u32, u32, Vec<[f32; 4]>) {
use super::shade::{dot, equirect_dir};
// The key region's centre: three quarters up, two thirds toward the viewer (the default
// camera looks +z from -z).
let key = [0.0f32, -0.75, -0.66];
let (w, h) = (8u32, 4u32);
let mut data = Vec::with_capacity((w * h) as usize);
let (mut mean, mut weight) = (0.0f32, 0.0f32);
for y in 0..h {
let v = (y as f32 + 0.5) / h as f32;
let sa = (v * std::f32::consts::PI).sin();
for x in 0..w {
let u = (x as f32 + 0.5) / w as f32;
let l = 0.5 + 0.9 * dot(equirect_dir(u, v, 0.0), key).max(0.0);
data.push([l, l, l, 1.0]);
mean += l * sa;
weight += sa;
}
}
// Keep the flat default's level: scale the gradient to a weighted mean of exactly 1.
let k = if mean.is_finite() && mean > 1e-6 { weight / mean } else { 1.0 };
for p in &mut data {
for c in 0..3 {
p[c] *= k;
}
}
(w, h, data)
}
/// Supersampling factor: 2×2 unless draft or very large.
fn ssaa_for(r: &Renderer, out: (u32, u32)) -> u32 {
if r.opts.draft || (out.0 as u64 * out.1 as u64) > 8_000_000 { 1 } else { 2 }
@@ -1019,21 +1054,14 @@ pub(crate) fn build_at(r: &Renderer, ctx: &EvalCtx, sub: Option<&EvalCtx>, run:
if let Some(env) = env {
match env_pixels(r, ctx, env.src, lin) {
Some((w, h, d)) => add_environment(&mut b, w, h, d, env.color, env.rot, env.casts_shadows, env.shadow_darkness, env.shadow_diffusion),
// No Source and no Environment Layer: a flat default environment of the light's
// colour × intensity — After Effects "uses a default environment map" for an
// Environment light — so the light always lights the scene per its parameters.
// A uniform environment has no dominant region, so it casts no shadow (#2436).
None => add_environment(
&mut b,
8,
4,
vec![[1.0, 1.0, 1.0, 1.0]; 32],
env.color,
env.rot,
env.casts_shadows,
env.shadow_darkness,
env.shadow_diffusion,
),
// No Source and no Environment Layer: the default environment, a soft studio
// gradient of the light's colour × intensity — After Effects "uses a default
// environment map" for an Environment light — so the light always lights the
// scene per its parameters, and its dominant region casts shadows (#2436).
None => {
let (w, h, d) = default_environment();
add_environment(&mut b, w, h, d, env.color, env.rot, env.casts_shadows, env.shadow_darkness, env.shadow_diffusion)
}
}
}
if r.opts.shadows() {
+54
View File
@@ -472,6 +472,60 @@ fn a_uniform_environment_casts_no_shadow() {
assert_eq!(casting.data, off.data, "a directionless image casts no shadow");
}
/// #2436: the default environment (no Source, no Environment Layer) is level-preserving and
/// directional — its dominant region points up and toward the initial camera, so a
/// shadow-casting Environment light casts shadows out of the box, like After Effects' default.
#[test]
fn default_environment_keeps_the_level_and_points_up() {
let (w, h, d) = scene::default_environment();
assert_eq!((w, h), (8, 4));
let (mut mean, mut weight) = (0.0f32, 0.0f32);
for (i, p) in d.iter().enumerate() {
let v = ((i / w as usize) as f32 + 0.5) / h as f32;
let sa = (v * PI).sin();
mean += (p[0] + p[1] + p[2]) / 3.0 * sa;
weight += sa;
}
assert!((mean / weight - 1.0).abs() < 0.01, "level-preserving mean: {}", mean / weight);
let sun = dominant_direction(w, h, &d, 0.0);
assert!(sun[1] < -0.5, "the key is up: {sun:?}");
assert!(sun[2] < -0.3, "the key is toward the camera: {sun:?}");
assert!(sun[0].abs() < 0.1, "symmetric left-right: {sun:?}");
}
/// #2436: with no Source and no Environment Layer, the Environment light's default environment
/// casts shadows like After Effects' default environment map — a sphere shadows the plane behind
/// it; Cast Shadows off lights the floor evenly.
#[test]
fn the_default_environment_casts_shadows() {
let (mut p, cid) = project(200, 150);
// The floor facing the camera, a sphere in front of it casting shadows only.
add(&mut p, cid, LayerSource::Primitive { kind: PrimitiveKind::Plane }, |l| {
set(l, "geometryOptions/width", Value::Scalar(400.0));
set(l, "geometryOptions/height", Value::Scalar(400.0));
set(l, "transform/position", Value::Vec3([100.0, 75.0, 120.0]));
set(l, "materialOptions/baseColor", Value::Color([1.0, 1.0, 1.0, 1.0]));
});
add(&mut p, cid, LayerSource::Primitive { kind: PrimitiveKind::Sphere }, |l| {
set(l, "geometryOptions/radius", Value::Scalar(30.0));
set(l, "transform/position", Value::Vec3([100.0, 60.0, 50.0]));
set(l, "materialOptions/castsShadows", Value::Enum(2));
});
add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |l| {
set(l, "lightOptions/castsShadows", Value::Bool(true));
});
let img = render(&p, cid);
let (mid, side) = (img.data[120 * 200 + 100], img.data[120 * 200 + 30]);
assert!(mid[3] > 0.99 && side[3] > 0.99, "the floor covers both: {mid:?} {side:?}");
assert!(mid[0] < side[0] * 0.5, "the default environment shadows the sphere onto the floor: {mid:?} vs {side:?}");
// Cast Shadows off: the floor is lit everywhere.
let comp = p.comp_mut(cid).unwrap();
set(&mut comp.layers[0], "lightOptions/castsShadows", Value::Bool(false));
let img = render(&p, cid);
let mid = img.data[120 * 200 + 100];
assert!(mid[0] > side[0] * 0.8, "no shadow without Cast Shadows: {mid:?} vs {side:?}");
}
#[test]
fn depth_of_field_blurs_out_of_focus_surfaces() {
let mut img = Image::new(64, 64);
+5 -3
View File
@@ -213,10 +213,12 @@ Things that tripped up a long agent-driven session (a multi-scene 3D piece built
- **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`),
or a flat default environment of the light's colour × intensity when there is none — either
way the light brightens the scene. It has `castsShadows`/`shadowDarkness`/`shadowDiffusion`
or a default studio environment — bright above and toward the initial camera, dark below and
behind — of the light's colour × intensity when there is none; either way the light brightens
the scene. It has `castsShadows`/`shadowDarkness`/`shadowDiffusion`
like the other lights: Cast Shadows shades the scene from the image's dominant bright region
(a flat or otherwise uniform environment has none, so it casts no shadow). It only lights
(the default environment's key, or a source image's brightest area; only a uniform image has
none, so it casts no shadow). 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),
+4 -3
View File
@@ -158,12 +158,13 @@ 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, or a flat default environment of the light's colour × intensity when
Environment Layer, or a default studio environment — bright above and toward the initial
camera, dark below and behind — 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, environment:
one orthographic from the image's dominant bright region — none when the image is
directionless, as a flat environment is). An Environment light only lights Advanced 3D comps,
one orthographic from the image's dominant bright region — none only when the image is
directionless, as a uniform one is). 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);