mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
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:
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user