mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Render the Environment light's shadow from its image (#2436)
The Casts Shadows option Environment lights got is only half a fix if nothing shades for it: an image lights the scene from every direction, and what occludes that light is the image itself — its dominant bright region, the sun in a sky map. - dominant_direction finds it: the radiance-weighted mean texel direction (solid-angle weighted, rotation applied). A uniform image cancels to zero — a flat default environment shines from everywhere at once and casts no shadow, which a uniform environment test pins. - The shadow is one orthographic map over the scene from that direction, after the lights' maps, like a parallel light's: PCF widened by Shadow Diffusion, darkened by Shadow Darkness, cleared when nothing casts, the image is directionless, or the texel budget drops it. - It darkens the environment's own diffuse and specular light on Accepts Shadows surfaces and counts in Cast Shadows Only. CPU and GPU (WGSL) do the same; Globals carries the environment's sun direction and shadow settings (40 → 44 floats, preflight updated). A bright-disc environment turned to shine from behind the camera now shadows a sphere onto the floor behind it, Cast Shadows off lights it evenly again, and the GPU scene's sky gradient takes a shadow-casting Environment light. Also: the renderer-switch toasts read "...renderer, which draws 3D models", the dialog's Casts Shadows checkbox gets an auto id (dialog.light.casts) the tests click, and the docs' Environment notes cover the shadow.
This commit is contained in:
@@ -92,10 +92,10 @@ pub(crate) fn use_advanced_3d(proj: &mut effectcraft_project::Project, cid: Item
|
||||
}
|
||||
|
||||
/// Tell the user when [`use_advanced_3d`] changed the comp's renderer.
|
||||
pub(crate) fn report_switch(s: &mut Session, cid: ItemId, switched: bool, because: &str) {
|
||||
pub(crate) fn report_switch(s: &mut Session, cid: ItemId, switched: bool, why: &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 {because}"));
|
||||
s.toast(format!("\"{name}\" now uses the Advanced 3D renderer, {why}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,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, "draws 3D models");
|
||||
report_switch(s, cid, switched, "which draws 3D models");
|
||||
Ok(json!({"layer": id.0, "item": item.0, "advanced3d": true}))
|
||||
}
|
||||
|
||||
@@ -232,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, "draws 3D models");
|
||||
report_switch(s, cid, switched, "which draws 3D models");
|
||||
Ok(json!({"layer": id.0, "kind": kind.label(), "advanced3d": true}))
|
||||
}
|
||||
|
||||
|
||||
@@ -228,16 +228,16 @@ 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));
|
||||
// Only the Advanced 3D renderer lights with Environment lights, so adding one switches a
|
||||
// Classic 3D comp to it, like After Effects (inside the adding edit: one undo step).
|
||||
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")?;
|
||||
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((id, if env { use_advanced_3d(proj, cid)? } else { false }))
|
||||
})?;
|
||||
report_switch(s, cid, switched, "lights Environment lights");
|
||||
report_switch(s, cid, switched, "which Environment lights need to light the scene");
|
||||
Ok(json!({"layer": id.0, "advanced3d": env}))
|
||||
}
|
||||
|
||||
@@ -257,11 +257,9 @@ fn light_settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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();
|
||||
@@ -296,12 +294,13 @@ fn light_settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
l.name = n.clone();
|
||||
}
|
||||
apply_light(&mut l, p, t, "layer.lightSettings")?;
|
||||
// The resulting kind (kept or changed to): an Environment light in a Classic 3D comp
|
||||
// switches it, like adding one does.
|
||||
let env = matches!(l.source, LayerSource::Light { kind: LightKind::Environment });
|
||||
*layer_mut(proj, cid, lid)? = l;
|
||||
// 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)?)
|
||||
Ok(if env { use_advanced_3d(proj, cid)? } else { false })
|
||||
})?;
|
||||
report_switch(s, cid, switched, "lights Environment lights");
|
||||
report_switch(s, cid, switched, "which Environment lights need to light the scene");
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 3D commands: cameras, lights, views, camera tools (with undo/redo), and the 3D demo comp.
|
||||
|
||||
use effectcraft_project::{AutoOrient, LayerSource, LightKind};
|
||||
use effectcraft_project::{AutoOrient, LayerSource, LightKind, Renderer};
|
||||
use effectcraft_render::three_d::View3D;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -148,41 +148,43 @@ 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).
|
||||
/// #2436: only the Advanced 3D renderer lights with Environment lights, and new comps are
|
||||
/// Classic 3D, so one had no visible effect. Adding one (or changing a light into one) switches
|
||||
/// the comp to Advanced 3D (in the same undo step, with a toast), as After Effects does.
|
||||
#[test]
|
||||
fn environment_light_switches_classic_comps_to_advanced_3d() {
|
||||
fn environment_lights_switch_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);
|
||||
assert_eq!(s.active_comp().unwrap().renderer, 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");
|
||||
assert_eq!(s.active_comp().unwrap().renderer, Renderer::Advanced3D);
|
||||
// As with Composition Settings: the text layer gets Geometry Options.
|
||||
assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(text)).unwrap().props.sub("geometryOptions").is_some());
|
||||
assert!(toasts(&mut s).iter().any(|m| m.contains("Advanced 3D")));
|
||||
// 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.
|
||||
// Undoing the creations takes the lights and the switch back 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.
|
||||
assert_eq!(s.active_comp().unwrap().renderer, Renderer::Classic3D);
|
||||
assert_eq!(layer_count(&s), 1);
|
||||
// Changing a light to Environment switches too, and undoes with the type.
|
||||
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);
|
||||
assert_eq!(s.active_comp().unwrap().renderer, Renderer::Classic3D);
|
||||
s.execute("layer.lightSettings", json!({"layer": spot, "kind": "Environment"})).unwrap();
|
||||
assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Advanced3D);
|
||||
assert_eq!(s.active_comp().unwrap().renderer, Renderer::Advanced3D);
|
||||
s.undo();
|
||||
assert_eq!(s.active_comp().unwrap().renderer, effectcraft_project::Renderer::Classic3D);
|
||||
assert_eq!(s.active_comp().unwrap().renderer, Renderer::Classic3D);
|
||||
assert_eq!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(spot)).unwrap().source, LayerSource::Light { kind: LightKind::Spot });
|
||||
// Other light types never switch it.
|
||||
s.execute("layer.newLight", json!({"kind": "Point"})).unwrap();
|
||||
assert_eq!(s.active_comp().unwrap().renderer, Renderer::Classic3D);
|
||||
// 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);
|
||||
@@ -202,8 +204,10 @@ fn environment_light_takes_shadow_options_but_no_falloff() {
|
||||
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");
|
||||
assert!(
|
||||
s.active_comp().unwrap().layer(effectcraft_project::LayerId(env)).unwrap().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());
|
||||
|
||||
+22
-9
@@ -77,7 +77,7 @@ fn buffer_bytes(count: u64, stride: u64, limit: u64) -> Option<u64> {
|
||||
/// Checking these before serialization bounds host packing as well as the GPU uploads.
|
||||
fn packed_bytes(counts: [u64; 6], uniform_limit: u64, storage_limit: u64, buffer_limit: u64) -> Option<[u64; 7]> {
|
||||
let mut sizes = [0; 7];
|
||||
sizes[0] = buffer_bytes(40, 4, uniform_limit.min(buffer_limit))?;
|
||||
sizes[0] = buffer_bytes(44, 4, uniform_limit.min(buffer_limit))?;
|
||||
for (index, (count, stride)) in counts.into_iter().zip([96, 16, 16, 80, 80, 4]).enumerate() {
|
||||
let size = count.checked_mul(stride)?.max(16);
|
||||
sizes[index + 1] = buffer_bytes(size, 1, storage_limit.min(buffer_limit))?;
|
||||
@@ -164,11 +164,11 @@ mod capability_tests {
|
||||
|
||||
#[test]
|
||||
fn packed_arrays_preflight_exact_strides_and_empty_padding() {
|
||||
assert_eq!(packed_bytes([0; 6], 160, 16, 160), Some([160, 16, 16, 16, 16, 16, 16]));
|
||||
assert_eq!(packed_bytes([2; 6], 160, 192, 192), Some([160, 192, 32, 32, 160, 160, 16]));
|
||||
assert!(packed_bytes([0; 6], 159, 16, 160).is_none());
|
||||
assert!(packed_bytes([0; 6], 160, 15, 160).is_none());
|
||||
assert!(packed_bytes([2; 6], 160, 192, 191).is_none());
|
||||
assert_eq!(packed_bytes([0; 6], 176, 16, 176), Some([176, 16, 16, 16, 16, 16, 16]));
|
||||
assert_eq!(packed_bytes([2; 6], 176, 192, 192), Some([176, 192, 32, 32, 160, 160, 16]));
|
||||
assert!(packed_bytes([0; 6], 175, 16, 176).is_none());
|
||||
assert!(packed_bytes([0; 6], 176, 15, 176).is_none());
|
||||
assert!(packed_bytes([2; 6], 176, 192, 191).is_none());
|
||||
for index in 0..6 {
|
||||
let mut counts = [0; 6];
|
||||
counts[index] = u64::MAX;
|
||||
@@ -203,7 +203,7 @@ mod capability_tests {
|
||||
};
|
||||
for (scene, counts) in [(Scene::default(), [0; 6]), (scene, [1; 6])] {
|
||||
let actual = pack(&scene).map(|bytes| bytes.len() as u64);
|
||||
assert_eq!(Some(actual), packed_bytes(counts, 160, 160, 160));
|
||||
assert_eq!(Some(actual), packed_bytes(counts, 176, 176, 176));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,10 +315,23 @@ fn pack(s: &Scene) -> [Vec<u8>; 7] {
|
||||
globals.extend([s.eye[0], s.eye[1], s.eye[2], s.ortho as u32 as f32]);
|
||||
globals.extend([s.cam_fwd[0], s.cam_fwd[1], s.cam_fwd[2], s.lights.len() as f32]);
|
||||
globals.extend([s.ambient[0], s.ambient[1], s.ambient[2], s.env.is_some() as u32 as f32]);
|
||||
let e = s.env.unwrap_or(effectcraft_render::three_d::adv::EnvInfo { radiance: 0, mips: 1, irradiance: 0, intensity: 0.0, rotation: 0.0 });
|
||||
let e = s.env.unwrap_or(effectcraft_render::three_d::adv::EnvInfo {
|
||||
radiance: 0,
|
||||
mips: 1,
|
||||
irradiance: 0,
|
||||
intensity: 0.0,
|
||||
rotation: 0.0,
|
||||
sun: [0.0; 3],
|
||||
shadow: -1,
|
||||
shadow_darkness: 0.0,
|
||||
shadow_diffusion: 0.0,
|
||||
});
|
||||
globals.extend([e.radiance as f32, e.mips as f32, e.irradiance as f32, e.intensity]);
|
||||
let lit = !(s.lights.is_empty() && s.env.is_none() && s.ambient == [0.0; 3]);
|
||||
globals.extend([e.rotation, lit as u32 as f32, 0.0, 0.0]);
|
||||
// env2: rotation, any lighting (0/1), shadow map index (-1 none), shadow darkness.
|
||||
globals.extend([e.rotation, lit as u32 as f32, e.shadow as f32, e.shadow_darkness]);
|
||||
// The environment's dominant direction (its shadow's source), for the shadow bias slope.
|
||||
globals.extend([e.sun[0], e.sun[1], e.sun[2], 0.0]);
|
||||
let mats = s.materials.iter().flat_map(|m| {
|
||||
let flags = (m.double_sided as u32)
|
||||
| (m.unlit as u32) << 1
|
||||
|
||||
@@ -14,8 +14,10 @@ struct Globals {
|
||||
ambient: vec4<f32>,
|
||||
// radiance first texture, mip count, irradiance texture, intensity
|
||||
env: vec4<f32>,
|
||||
// rotation, any lighting (0/1), -, -
|
||||
// rotation, any lighting (0/1), shadow map index (-1 none), shadow darkness
|
||||
env2: vec4<f32>,
|
||||
// the environment's dominant direction (its shadow's source)
|
||||
env_sun: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> g: Globals;
|
||||
@@ -318,6 +320,16 @@ fn light_shadow(li: u32, kind: u32, lpos: vec3<f32>, shadow: i32, darkness: f32,
|
||||
return 1.0 - darkness * (1.0 - f);
|
||||
}
|
||||
|
||||
// The Environment light's shadow from its dominant direction (shade::env_shadow).
|
||||
fn env_shadow(p: vec3<f32>, ng: vec3<f32>, n: vec3<f32>) -> f32 {
|
||||
if (g.env2.z < 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
let n_l = clamp(dot(n, g.env_sun.xyz), 0.0, 1.0);
|
||||
let f = shadow_factor(u32(g.env2.z + 0.5) * SHADOW_STRIDE, p, ng, n_l);
|
||||
return 1.0 - g.env2.w * (1.0 - f);
|
||||
}
|
||||
|
||||
fn attenuation(kind: u32, falloff: u32, radius: f32, falloff_distance: f32, cos_inner: f32, cos_outer: f32, dir: vec3<f32>, to_l: vec3<f32>, dist: f32) -> f32 {
|
||||
var k = 1.0;
|
||||
if (falloff == 1u && kind != 0u) {
|
||||
@@ -404,6 +416,10 @@ fn shadow_only_amount(unlit: bool, normal: vec3<f32>, front: bool, pos: vec3<f32
|
||||
}
|
||||
amount = max(amount, 1.0 - light_shadow(li, kind, l0.xyz, i32(l3.w), l4.x, pos, n, n_l));
|
||||
}
|
||||
// The Environment light's shadow counts too (it reaches every surface: no falloff).
|
||||
if (g.ambient.w > 0.5) {
|
||||
amount = max(amount, 1.0 - env_shadow(pos, n, n));
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
@@ -543,7 +559,11 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
|
||||
let f0 = mix(vec3<f32>(0.04 * spec_k), rgb, metallic);
|
||||
let ab = env_brdf(n_v, rough);
|
||||
let fs = f0 * ab.x + vec3<f32>(ab.y);
|
||||
acc = acc + (irr * rgb * (vec3<f32>(1.0) - fs) * (1.0 - metallic) * diff_k + spec * fs) * ao;
|
||||
var esh = 1.0;
|
||||
if (receives_shadows) {
|
||||
esh = env_shadow(i.pos, ng, n);
|
||||
}
|
||||
acc = acc + (irr * rgb * (vec3<f32>(1.0) - fs) * (1.0 - metallic) * diff_k + spec * fs) * ao * esh;
|
||||
}
|
||||
let c = acc + emissive;
|
||||
out.color = vec4<f32>(c * alpha, alpha);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! edges may be covered by the other neighbour. Interior pixels agree within 2/255; at most
|
||||
//! 1.5 % of pixels (silhouettes) may differ more. Skipped without an adapter.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_keyframe::{Gradient, Value};
|
||||
use effectcraft_project::build;
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, LightKind, PrimitiveKind, Project, Renderer as R3, Solid};
|
||||
use effectcraft_render::three_d::adv::{self, Scene};
|
||||
@@ -38,7 +38,8 @@ fn set(l: &mut effectcraft_project::Layer, path: &str, v: Value) {
|
||||
}
|
||||
|
||||
/// Primitives with metal/dielectric materials, a shadow-casting spot light and a point light,
|
||||
/// an environment light, a semi-transparent textured card.
|
||||
/// a shadow-casting Environment light over a sky-gradient environment layer, a semi-transparent
|
||||
/// textured card.
|
||||
pub(crate) fn scene_project() -> (Project, ItemId) {
|
||||
let mut p = Project::default();
|
||||
let mut c = Comp::new(240, 160, FrameRate::FPS_30, Tick::from_seconds_f64(1.0));
|
||||
@@ -73,12 +74,29 @@ pub(crate) fn scene_project() -> (Project, ItemId) {
|
||||
set(l, "transform/position", Value::Vec3([120.0, 80.0, -60.0]));
|
||||
set(l, "transform/rotationY", Value::Scalar(30.0));
|
||||
});
|
||||
let env = p.add_item("E", Default::default(), None, ItemKind::Solid(Solid { color: [0.6, 0.7, 1.0], width: 32, height: 16, pixel_aspect: 1.0 }));
|
||||
add(&mut p, cid, LayerSource::Solid { item: env }, |l| {
|
||||
l.environment = true;
|
||||
// The Environment Layer: a sky-to-ground gradient (its dominant region, the bright sky,
|
||||
// shadows the scene from above).
|
||||
add(&mut p, cid, LayerSource::Shape, |l| {
|
||||
l.switches.three_d = true;
|
||||
let mut next = 10_000u64;
|
||||
let ids = &mut build::Ids(&mut next);
|
||||
let rect = build::shape_rect(ids, [240.0, 160.0], [0.0, 0.0], 0.0);
|
||||
let gfill = build::shape_gradient_fill(
|
||||
ids,
|
||||
false,
|
||||
[0.0, -80.0],
|
||||
[0.0, 80.0],
|
||||
Gradient { colors: vec![(0.0, [1.0, 1.0, 1.0, 1.0]), (1.0, [0.04, 0.05, 0.08, 1.0])], opacities: vec![(0.0, 1.0), (1.0, 1.0)] },
|
||||
);
|
||||
let g = build::shape_group(ids, "Sky", vec![rect, gfill]);
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
l.environment = true;
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |l| {
|
||||
set(l, "lightOptions/intensity", Value::Scalar(50.0));
|
||||
set(l, "lightOptions/castsShadows", Value::Bool(true));
|
||||
set(l, "lightOptions/shadowDiffusion", Value::Scalar(6.0));
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |l| set(l, "lightOptions/intensity", Value::Scalar(50.0)));
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Spot }, |l| {
|
||||
set(l, "transform/position", Value::Vec3([40.0, -120.0, -200.0]));
|
||||
set(l, "transform/poi", Value::Vec3([120.0, 100.0, 0.0]));
|
||||
@@ -135,6 +153,8 @@ fn gpu_matches_cpu_lit_scene() {
|
||||
let (p, cid) = scene_project();
|
||||
let s = scene(&p, cid);
|
||||
assert!(!s.shadows.is_empty() && s.env.is_some() && s.opaque_count < s.indices.len() as u32);
|
||||
// The Environment light casts its shadow (the sky gradient's dominant direction).
|
||||
assert!(s.env.is_some_and(|e| e.shadow >= 0 && e.sun != [0.0; 3]), "the environment shadow map is built");
|
||||
compare(&s, &g);
|
||||
}
|
||||
|
||||
|
||||
@@ -656,7 +656,8 @@ pub fn light_options(ids: &mut Ids, kind: LightKind) -> PropGroup {
|
||||
// The environment image: a layer (footage, comp…) mapped as an equirectangular panorama;
|
||||
// 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).
|
||||
// every direction at once. Environment lights cast shadows too (After Effects' Light
|
||||
// Settings shows Cast Shadows for them; not for Ambient) (#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))
|
||||
|
||||
@@ -500,11 +500,19 @@ pub(crate) fn shadow_sizes(width: u32, height: u32, draft: bool, kinds: &[u32])
|
||||
sizes.into_iter().map(|z| (z > 0).then_some(z)).collect()
|
||||
}
|
||||
|
||||
/// Build the shadow maps of every shadow-casting light (lights marked `shadow == 0`).
|
||||
/// Build the shadow maps of every shadow-casting light (lights marked `shadow == 0`), and of
|
||||
/// the Environment light when it casts shadows and its image has a dominant direction (marked
|
||||
/// `env.shadow == 0`, `env.sun != [0; 3]`): one map from that direction, like a parallel light.
|
||||
pub(crate) fn shadow_maps(s: &mut Scene, draft: bool) {
|
||||
let casters: Vec<usize> = (0..s.indices.len() / 3)
|
||||
.filter(|&t| s.materials.get(s.vertices[s.indices[3 * t] as usize].material as usize).is_some_and(|m| m.casts_shadows))
|
||||
.collect();
|
||||
// The Environment light casts a shadow when marked and its image has a dominant direction;
|
||||
// with nothing to cast one, its mark is cleared either way.
|
||||
let env_shadow = !casters.is_empty() && s.env.is_some_and(|e| e.shadow == 0 && e.sun != [0.0; 3]);
|
||||
if !env_shadow && let Some(e) = &mut s.env {
|
||||
e.shadow = -1;
|
||||
}
|
||||
if casters.is_empty() {
|
||||
for l in &mut s.lights {
|
||||
l.shadow = -1;
|
||||
@@ -536,6 +544,8 @@ pub(crate) fn shadow_maps(s: &mut Scene, draft: bool) {
|
||||
1
|
||||
}
|
||||
})
|
||||
// The Environment light's map is a single orthographic one.
|
||||
.chain(std::iter::once(if env_shadow { 1 } else { 0 }))
|
||||
.collect();
|
||||
let sizes = shadow_sizes(s.width, s.height, draft, &kinds);
|
||||
let mut maps: Vec<ShadowMap> = vec![];
|
||||
@@ -583,6 +593,31 @@ pub(crate) fn shadow_maps(s: &mut Scene, draft: bool) {
|
||||
}
|
||||
s.lights[li].shadow = first;
|
||||
}
|
||||
// The Environment light's shadow: one orthographic map over the scene from its dominant
|
||||
// direction (the light travels from the bright region into the scene), after the lights'.
|
||||
if env_shadow && let Some(e) = s.env {
|
||||
match sizes.get(s.lights.len()).copied().flatten() {
|
||||
Some(size) => {
|
||||
let first = maps.len() as i32;
|
||||
let soft = 1.0 + (e.shadow_diffusion * 0.5).min(24.0);
|
||||
let sun = e.sun.map(|v| v as f64);
|
||||
let dir = [-sun[0], -sun[1], -sun[2]];
|
||||
let eye = [centre[0] + sun[0] * radius * 2.0, centre[1] + sun[1] * radius * 2.0, centre[2] + sun[2] * radius * 2.0];
|
||||
let hint = if dir[1].abs() < 0.9 { [0.0, 1.0, 0.0] } else { [0.0, 0.0, 1.0] };
|
||||
let focal = (size as f64 * 0.5 / radius) as f32;
|
||||
maps.push(ShadowMap { view: basis(eye, dir, hint), focal, ortho: true, size, offset: 0, bias: 1.5, radius: soft });
|
||||
if let Some(e) = &mut s.env {
|
||||
e.shadow = first;
|
||||
}
|
||||
}
|
||||
// The texel budget dropped it.
|
||||
None => {
|
||||
if let Some(e) = &mut s.env {
|
||||
e.shadow = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let texels: Vec<Vec<f32>> = maps.par_iter().map(|m| shadow_raster(s, m, &casters)).collect();
|
||||
for (m, t) in maps.iter_mut().zip(texels) {
|
||||
m.offset = s.shadow_texels.len() as u32;
|
||||
|
||||
@@ -172,6 +172,16 @@ pub struct EnvInfo {
|
||||
pub intensity: f32,
|
||||
/// Radians about the vertical axis.
|
||||
pub rotation: f32,
|
||||
/// Unit direction from the scene towards the environment's dominant (radiance-weighted)
|
||||
/// bright region — the direction its shadow is cast from, like a parallel light's
|
||||
/// `to_light`. `[0; 3]` when the image is directionless (uniform): no shadow then.
|
||||
pub sun: [f32; 3],
|
||||
/// Shadow map index into [`Scene::shadows`]: 0 marked (assigned by `raster::shadow_maps`),
|
||||
/// -1 casts no shadow. Gated by the light's Cast Shadows.
|
||||
pub shadow: i32,
|
||||
pub shadow_darkness: f32,
|
||||
/// The light's Shadow Diffusion (the penumbra width the shadow map's PCF widens to).
|
||||
pub shadow_diffusion: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -670,9 +680,20 @@ fn card_layer(r: &Renderer, ctx: &EvalCtx, layer: &Layer, b: &mut Builder) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluated lights: (scene lights, ambient sum, environment light (layer, intensity × colour,
|
||||
/// rotation)).
|
||||
fn lights(ctx: &EvalCtx, lin: bool) -> (Vec<Light>, [f32; 3], Option<(Option<u64>, [f32; 3], f32)>) {
|
||||
/// The evaluated Environment light: its image source, intensity × colour, rotation, and
|
||||
/// shadow settings (After Effects' Light Settings shows Cast Shadows for Environment lights).
|
||||
#[derive(Clone, Copy)]
|
||||
struct EnvLight {
|
||||
src: Option<u64>,
|
||||
color: [f32; 3],
|
||||
rot: f32,
|
||||
casts_shadows: bool,
|
||||
shadow_darkness: f64,
|
||||
shadow_diffusion: f64,
|
||||
}
|
||||
|
||||
/// Evaluated lights: (scene lights, ambient sum, environment light).
|
||||
fn lights(ctx: &EvalCtx, lin: bool) -> (Vec<Light>, [f32; 3], Option<EnvLight>) {
|
||||
let mut out = vec![];
|
||||
let mut ambient = [0.0f32; 3];
|
||||
let mut env = None;
|
||||
@@ -693,7 +714,14 @@ fn lights(ctx: &EvalCtx, lin: bool) -> (Vec<Light>, [f32; 3], Option<(Option<u64
|
||||
if env.is_none() {
|
||||
let src = g.get("source").and_then(|p| ctx.value(layer, p).as_layer());
|
||||
let rot = ctx.f(layer, g, "rotation", 0.0).to_radians() as f32;
|
||||
env = Some((src, color, rot));
|
||||
env = Some(EnvLight {
|
||||
src,
|
||||
color,
|
||||
rot,
|
||||
casts_shadows: ctx.b(layer, g, "castsShadows"),
|
||||
shadow_darkness: ctx.f(layer, g, "shadowDarkness", 100.0).clamp(0.0, 100.0) / 100.0,
|
||||
shadow_diffusion: ctx.f(layer, g, "shadowDiffusion", 0.0).max(0.0),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -783,8 +811,47 @@ pub fn irradiance_map(sw: u32, sh: u32, src: &[[f32; 4]], w: u32, h: u32) -> Vec
|
||||
out
|
||||
}
|
||||
|
||||
/// The environment image's dominant direction: the radiance-weighted mean texel direction
|
||||
/// (solid-angle weighted, rotation applied), or `[0; 3]` when the image is directionless
|
||||
/// (a uniform environment cancels to zero). The mean of a single bright spot on black points
|
||||
/// straight at it; a gradient points somewhere between its ends.
|
||||
fn dominant_direction(w: u32, h: u32, d: &[[f32; 4]], rot: f32) -> [f32; 3] {
|
||||
use super::shade::equirect_dir;
|
||||
let (mut x, mut y, mut z) = (0.0f32, 0.0f32, 0.0f32);
|
||||
let mut weight = 0.0f32;
|
||||
for (i, p) in d.iter().enumerate() {
|
||||
let (tx, ty) = ((i % w as usize) as u32, (i / w as usize) as u32);
|
||||
let (u, v) = ((tx as f32 + 0.5) / w as f32, (ty as f32 + 0.5) / h as f32);
|
||||
// Luminance × solid angle (∝ sin θ).
|
||||
let lum = (p[0] + p[1] + p[2]) * (v * std::f32::consts::PI).sin();
|
||||
let dir = equirect_dir(u, v, rot);
|
||||
x += dir[0] * lum;
|
||||
y += dir[1] * lum;
|
||||
z += dir[2] * lum;
|
||||
weight += lum.abs();
|
||||
}
|
||||
// Under 1 % directional, the image is uniform to the eye: no dominant region to shadow from.
|
||||
let len = (x * x + y * y + z * z).sqrt();
|
||||
if weight <= 1e-9 || len < weight * 0.01 {
|
||||
return [0.0; 3];
|
||||
}
|
||||
[x / len, y / len, z / len]
|
||||
}
|
||||
|
||||
/// Prefilter an environment image into the scene's textures.
|
||||
pub(crate) fn add_environment(b: &mut Builder, w: u32, h: u32, data: Vec<[f32; 4]>, intensity: [f32; 3], rotation: f32) {
|
||||
pub(crate) fn add_environment(
|
||||
b: &mut Builder,
|
||||
w: u32,
|
||||
h: u32,
|
||||
data: Vec<[f32; 4]>,
|
||||
intensity: [f32; 3],
|
||||
rotation: f32,
|
||||
casts_shadows: bool,
|
||||
shadow_darkness: f64,
|
||||
shadow_diffusion: f64,
|
||||
) {
|
||||
// The shadow direction from the full-resolution image, before the mip chain averages it.
|
||||
let sun = dominant_direction(w, h, &data, rotation);
|
||||
// Radiance level 0 at most 1024 wide.
|
||||
let (mut w, mut h, mut d) = (w, h, data);
|
||||
while w > 1024 {
|
||||
@@ -820,7 +887,18 @@ pub(crate) fn add_environment(b: &mut Builder, w: u32, h: u32, data: Vec<[f32; 4
|
||||
}
|
||||
}
|
||||
}
|
||||
b.s.env = Some(EnvInfo { radiance: first, mips: levels, irradiance: ii, intensity: k, rotation });
|
||||
b.s.env = Some(EnvInfo {
|
||||
radiance: first,
|
||||
mips: levels,
|
||||
irradiance: ii,
|
||||
intensity: k,
|
||||
rotation,
|
||||
sun,
|
||||
// Marked; `raster::shadow_maps` assigns the map (a directionless image casts none).
|
||||
shadow: if casts_shadows { 0 } else { -1 },
|
||||
shadow_darkness: shadow_darkness as f32,
|
||||
shadow_diffusion: shadow_diffusion as f32,
|
||||
});
|
||||
}
|
||||
|
||||
/// The environment layer's pixels as linear straight RGBA.
|
||||
@@ -935,13 +1013,24 @@ 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 {
|
||||
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 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,
|
||||
),
|
||||
}
|
||||
}
|
||||
if r.opts.shadows() {
|
||||
@@ -951,6 +1040,9 @@ pub(crate) fn build_at(r: &Renderer, ctx: &EvalCtx, sub: Option<&EvalCtx>, run:
|
||||
for l in &mut b.s.lights {
|
||||
l.shadow = -1;
|
||||
}
|
||||
if let Some(e) = &mut b.s.env {
|
||||
e.shadow = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Camera matrices.
|
||||
|
||||
@@ -351,6 +351,19 @@ pub fn light_shadow(s: &Scene, l: &Light, p: [f32; 3], ng: [f32; 3], n_l: f32) -
|
||||
1.0 - l.shadow_darkness * (1.0 - f)
|
||||
}
|
||||
|
||||
/// The visibility of the Environment light's dominant direction at `p` (1 = unshadowed),
|
||||
/// darkened by its Shadow Darkness. A directionless image (or a non-casting light) has no map
|
||||
/// and shades unshadowed.
|
||||
pub fn env_shadow(s: &Scene, e: &EnvInfo, p: [f32; 3], ng: [f32; 3], n: [f32; 3]) -> f32 {
|
||||
if e.shadow < 0 || e.sun == [0.0; 3] {
|
||||
return 1.0;
|
||||
}
|
||||
let Some(m) = s.shadows.get(e.shadow as usize) else { return 1.0 };
|
||||
let n_l = dot(n, e.sun).clamp(0.0, 1.0);
|
||||
let f = shadow_factor(m, &s.shadow_texels, p, ng, n_l);
|
||||
1.0 - e.shadow_darkness * (1.0 - f)
|
||||
}
|
||||
|
||||
/// Unit direction from `p` to light `l`, and the distance (infinite for a parallel light).
|
||||
fn to_light(l: &Light, p: [f32; 3]) -> ([f32; 3], f32) {
|
||||
if l.kind == 0 {
|
||||
@@ -498,9 +511,11 @@ pub fn shade(s: &Scene, f: &Frag) -> Option<[f32; 4]> {
|
||||
let (irr, spec) = env_light(s, e, n, r, rough);
|
||||
let f0 = [mix(0.04 * m.specular_k, rgb[0], metallic), mix(0.04 * m.specular_k, rgb[1], metallic), mix(0.04 * m.specular_k, rgb[2], metallic)];
|
||||
let (a, b) = env_brdf(n_v, rough);
|
||||
// The Environment light's shadow darkens its own diffuse and specular.
|
||||
let sh = if m.receives_shadows { env_shadow(s, e, f.pos, ng, n) } else { 1.0 };
|
||||
for c in 0..3 {
|
||||
let fs = f0[c] * a + b;
|
||||
out[c] += (irr[c] * rgb[c] * (1.0 - fs) * (1.0 - metallic) * m.diffuse_k + spec[c] * fs) * ao;
|
||||
out[c] += (irr[c] * rgb[c] * (1.0 - fs) * (1.0 - metallic) * m.diffuse_k + spec[c] * fs) * ao * sh;
|
||||
}
|
||||
}
|
||||
let c = add(out, emissive);
|
||||
@@ -526,5 +541,9 @@ fn shadow_only(s: &Scene, m: &Material, f: &Frag, alpha: f32) -> [f32; 4] {
|
||||
}
|
||||
amount = amount.max(1.0 - light_shadow(s, l, f.pos, n, n_l));
|
||||
}
|
||||
// The Environment light's shadow counts too (it reaches every surface: no falloff).
|
||||
if let Some(e) = &s.env {
|
||||
amount = amount.max(1.0 - env_shadow(s, e, f.pos, n, n));
|
||||
}
|
||||
[0.0, 0.0, 0.0, alpha * amount]
|
||||
}
|
||||
|
||||
@@ -338,8 +338,8 @@ fn cards_and_environment_light() {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// default environment of its colour × intensity — After Effects "uses a default environment
|
||||
/// map" for an Environment light — not nothing at all (#2436).
|
||||
#[test]
|
||||
fn environment_light_without_a_source_lights_the_scene() {
|
||||
let (mut p, cid) = project(120, 90);
|
||||
@@ -376,6 +376,91 @@ fn environment_light_without_a_source_lights_the_scene() {
|
||||
assert!(c5[0] > 0.15 && c5[1] < 0.05 && c5[2] < 0.05, "red environment: {c5:?}");
|
||||
}
|
||||
|
||||
/// #2436: an Environment light's Cast Shadows works like After Effects' — its image's dominant
|
||||
/// (brightest) region shadows the scene. A bright disc turned to shine from behind the camera
|
||||
/// (rotation 180°) shadows a sphere onto the plane behind it; Cast Shadows off lights it evenly.
|
||||
#[test]
|
||||
fn environment_lights_cast_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, 200.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, 75.0, 50.0]));
|
||||
set(l, "materialOptions/castsShadows", Value::Enum(2));
|
||||
});
|
||||
// The Environment Layer: a bright disc at the image's centre on black.
|
||||
add(&mut p, cid, LayerSource::Shape, |l| {
|
||||
l.switches.three_d = true;
|
||||
let mut next = 10_000u64;
|
||||
let ids = &mut build::Ids(&mut next);
|
||||
let rect = build::shape_rect(ids, [200.0, 150.0], [0.0, 0.0], 0.0);
|
||||
let gfill = build::shape_gradient_fill(
|
||||
ids,
|
||||
true,
|
||||
[0.0, 0.0],
|
||||
[0.0, 75.0],
|
||||
effectcraft_keyframe::Gradient { colors: vec![(0.0, [1.0, 1.0, 1.0, 1.0]), (1.0, [0.0, 0.0, 0.0, 1.0])], opacities: vec![(0.0, 1.0), (1.0, 1.0)] },
|
||||
);
|
||||
let g = build::shape_group(ids, "Sky", vec![rect, gfill]);
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
l.environment = true;
|
||||
});
|
||||
// Turned 180°, the disc shines from behind the camera, straight at the floor.
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |l| {
|
||||
set(l, "lightOptions/rotation", Value::Scalar(180.0));
|
||||
set(l, "lightOptions/castsShadows", Value::Bool(true));
|
||||
});
|
||||
let img = render(&p, cid);
|
||||
let (mid, side) = (img.data[75 * 200 + 100], img.data[75 * 200 + 20]);
|
||||
assert!(mid[3] > 0.99 && side[3] > 0.99);
|
||||
assert!(mid[0] < side[0] * 0.5, "the disc 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[75 * 200 + 100];
|
||||
assert!(mid[0] > side[0] * 0.8, "no shadow without Cast Shadows: {mid:?} vs {side:?}");
|
||||
}
|
||||
|
||||
/// #2436: a uniform environment has no dominant region, so even with Cast Shadows on it casts
|
||||
/// nothing — the render is the same as with the option off.
|
||||
#[test]
|
||||
fn a_uniform_environment_casts_no_shadow() {
|
||||
let (mut p, cid) = project(120, 90);
|
||||
let card = p.add_item(
|
||||
"S",
|
||||
Default::default(),
|
||||
None,
|
||||
ItemKind::Solid(effectcraft_project::Solid { color: [1.0, 1.0, 1.0], width: 60, height: 40, pixel_aspect: 1.0 }),
|
||||
);
|
||||
add(&mut p, cid, LayerSource::Solid { item: card }, |l| l.switches.three_d = true);
|
||||
let env = p.add_item(
|
||||
"E",
|
||||
Default::default(),
|
||||
None,
|
||||
ItemKind::Solid(effectcraft_project::Solid { color: [1.0, 1.0, 1.0], width: 64, height: 32, pixel_aspect: 1.0 }),
|
||||
);
|
||||
add(&mut p, cid, LayerSource::Solid { item: env }, |l| {
|
||||
l.environment = true;
|
||||
l.switches.three_d = true;
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |l| {
|
||||
set(l, "lightOptions/castsShadows", Value::Bool(true));
|
||||
});
|
||||
let casting = render(&p, cid);
|
||||
let comp = p.comp_mut(cid).unwrap();
|
||||
set(&mut comp.layers[0], "lightOptions/castsShadows", Value::Bool(false));
|
||||
let off = render(&p, cid);
|
||||
assert!(casting.data[20 * 120 + 30][0] > 0.3, "the card is lit");
|
||||
assert_eq!(casting.data, off.data, "a directionless image casts no shadow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depth_of_field_blurs_out_of_focus_surfaces() {
|
||||
let mut img = Image::new(64, 64);
|
||||
|
||||
@@ -44,14 +44,15 @@ pub struct LightDraft {
|
||||
|
||||
const FALLOFFS: [&str; 3] = ["None", "Smooth", "Inverse Square Clamped"];
|
||||
|
||||
/// 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.
|
||||
/// Whether a light type has Falloff options (distance attenuation): image-based Environment
|
||||
/// lights and Ambient come from no one place, so they have none.
|
||||
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).
|
||||
/// Whether a light type has Shadows options in the dialog. After Effects' Light Settings shows
|
||||
/// Cast Shadows (with Shadow Darkness and Diffusion) for every light but Ambient — an
|
||||
/// Environment light's shadow comes from its image, not a falloff.
|
||||
fn casts_shadows(kind: LightKind) -> bool {
|
||||
kind != LightKind::Ambient
|
||||
}
|
||||
@@ -315,7 +316,8 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
}
|
||||
if casts_shadows(d.kind) {
|
||||
ui.label(tr("Shadows"));
|
||||
ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
|
||||
let cb = ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
|
||||
app.auto.add_response("dialog.light.casts", &cb, "Casts Shadows");
|
||||
ui.end_row();
|
||||
ui.label(tr("Shadow Darkness"));
|
||||
ui.add_enabled(d.casts_shadows, crate::widgets::drag_value(egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %")));
|
||||
|
||||
@@ -177,28 +177,31 @@ 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);
|
||||
// Falloff is distance attenuation: image-based Environment lights and Ambient have none.
|
||||
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());
|
||||
// Cast Shadows shows for every light but Ambient, Environment included (#2436).
|
||||
let shadows = k != LightKind::Ambient;
|
||||
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.
|
||||
/// #2436: an Environment light created through the dialog keeps its Cast Shadows, which the
|
||||
/// engine stores on the layer.
|
||||
#[test]
|
||||
fn an_environment_light_created_with_casts_shadow_takes_it() {
|
||||
fn an_environment_light_takes_cast_shadows_from_the_dialog() {
|
||||
let mut h = harness();
|
||||
invoke(&mut h, "layer.newLight", json!({}));
|
||||
choose(&mut h, LightKind::Environment);
|
||||
h.get_by_label("Casts Shadows").click();
|
||||
click_id(&mut h, "dialog.light.casts");
|
||||
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");
|
||||
let l = layer(&h, id);
|
||||
assert!(l.props.prop("lightOptions/castsShadows").unwrap().value.as_bool(), "the dialog's checkbox reached the layer");
|
||||
assert!(l.props.prop("lightOptions/shadowDarkness").is_some() && l.props.prop("lightOptions/falloff").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+5
-3
@@ -214,9 +214,11 @@ Things that tripped up a long agent-driven session (a multi-scene 3D piece built
|
||||
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, 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
|
||||
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
|
||||
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)).
|
||||
|
||||
|
||||
@@ -161,9 +161,11 @@ light from an Environment light whose Source (or the comp's Environment Layer, L
|
||||
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). 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
|
||||
(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,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user