mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Merge pull request #1704
This commit is contained in:
@@ -320,7 +320,12 @@ fn pack(s: &Scene) -> [Vec<u8>; 7] {
|
||||
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]);
|
||||
let mats = s.materials.iter().flat_map(|m| {
|
||||
let flags = (m.double_sided as u32) | (m.unlit as u32) << 1 | (m.accepts_lights as u32) << 2 | (m.receives_shadows as u32) << 3 | (m.alpha_mode << 8);
|
||||
let flags = (m.double_sided as u32)
|
||||
| (m.unlit as u32) << 1
|
||||
| (m.accepts_lights as u32) << 2
|
||||
| (m.receives_shadows as u32) << 3
|
||||
| (m.shadows_only as u32) << 4
|
||||
| (m.alpha_mode << 8);
|
||||
[
|
||||
m.base[0],
|
||||
m.base[1],
|
||||
|
||||
@@ -288,6 +288,53 @@ struct FOut {
|
||||
@location(1) depth: u32,
|
||||
};
|
||||
|
||||
// shade::to_light: unit direction to the light (xyz) and its distance (w).
|
||||
fn to_light(kind: u32, lpos: vec3<f32>, ldir: vec3<f32>, pos: vec3<f32>) -> vec4<f32> {
|
||||
if (kind == 0u) {
|
||||
return vec4<f32>(-ldir, 1e30);
|
||||
}
|
||||
let d = lpos - pos;
|
||||
let len = sqrt(dot(d, d));
|
||||
if (len < 1e-6) {
|
||||
return vec4<f32>(0.0, 0.0, -1.0, 0.0);
|
||||
}
|
||||
return vec4<f32>(d / len, len);
|
||||
}
|
||||
|
||||
// shade::shadow_only: the strongest shadow any light casts on the fragment.
|
||||
fn shadow_only_amount(unlit: bool, normal: vec3<f32>, front: bool, pos: vec3<f32>) -> f32 {
|
||||
var amount = 0.0;
|
||||
if (unlit) {
|
||||
return amount;
|
||||
}
|
||||
var n = norm3(normal);
|
||||
if (!front) {
|
||||
n = -n;
|
||||
}
|
||||
let nl_count = u32(g.fwd.w);
|
||||
for (var li = 0u; li < nl_count; li = li + 1u) {
|
||||
let lb = li * LIGHT_STRIDE;
|
||||
let l0 = lights[lb];
|
||||
let l1 = lights[lb + 1u];
|
||||
let l2 = lights[lb + 2u];
|
||||
let l3 = lights[lb + 3u];
|
||||
let l4 = lights[lb + 4u];
|
||||
let kind = u32(l0.w);
|
||||
let tl = to_light(kind, l0.xyz, l1.xyz, pos);
|
||||
let to_l = tl.xyz;
|
||||
let dist = tl.w;
|
||||
if (attenuation(kind, u32(l3.x), l3.y, l3.z, l2.w, l1.w, l1.xyz, to_l, dist) <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
let n_l = dot(n, to_l);
|
||||
if (n_l <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
amount = max(amount, 1.0 - light_shadow(li, kind, l0.xyz, i32(l3.w), l4.x, pos, n_l));
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
|
||||
let mb = i.material * MAT_STRIDE;
|
||||
@@ -302,6 +349,7 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
|
||||
let unlit = (flags & 2u) != 0u;
|
||||
let accepts_lights = (flags & 4u) != 0u;
|
||||
let receives_shadows = (flags & 8u) != 0u;
|
||||
let shadows_only = (flags & 16u) != 0u;
|
||||
let alpha_mode = (flags >> 8u) & 3u;
|
||||
if (!front && !double_sided) {
|
||||
discard;
|
||||
@@ -330,6 +378,10 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
|
||||
}
|
||||
var out: FOut;
|
||||
out.depth = bitcast<u32>(dot(g.view_z.xyz, i.pos) + g.view_z.w);
|
||||
if (shadows_only) {
|
||||
out.color = vec4<f32>(0.0, 0.0, 0.0, alpha * shadow_only_amount(unlit || !accepts_lights, i.normal, front, i.pos));
|
||||
return out;
|
||||
}
|
||||
if (unlit || !accepts_lights || g.env2.y < 0.5) {
|
||||
let c = rgb + emissive;
|
||||
out.color = vec4<f32>(c * alpha, alpha);
|
||||
@@ -381,22 +433,9 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
|
||||
let l3 = lights[lb + 3u];
|
||||
let l4 = lights[lb + 4u];
|
||||
let kind = u32(l0.w);
|
||||
var to_l: vec3<f32>;
|
||||
var dist: f32;
|
||||
if (kind == 0u) {
|
||||
to_l = -l1.xyz;
|
||||
dist = 1e30;
|
||||
} else {
|
||||
let d = l0.xyz - i.pos;
|
||||
let len = sqrt(dot(d, d));
|
||||
if (len < 1e-6) {
|
||||
to_l = vec3<f32>(0.0, 0.0, -1.0);
|
||||
dist = 0.0;
|
||||
} else {
|
||||
to_l = d / len;
|
||||
dist = len;
|
||||
}
|
||||
}
|
||||
let tl = to_light(kind, l0.xyz, l1.xyz, i.pos);
|
||||
let to_l = tl.xyz;
|
||||
let dist = tl.w;
|
||||
let k = attenuation(kind, u32(l3.x), l3.y, l3.z, l2.w, l1.w, l1.xyz, to_l, dist);
|
||||
if (k <= 0.0) {
|
||||
continue;
|
||||
|
||||
@@ -155,6 +155,18 @@ fn gpu_matches_cpu_unlit_and_full_frames() {
|
||||
assert!((diff as f64) < 0.02 * cpu.data.len() as f64, "{diff} pixels differ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_matches_cpu_shadow_catcher() {
|
||||
// #1691: the floor plane as a shadow catcher ("Accepts Shadows: Only").
|
||||
let Some(g) = gpu() else { return };
|
||||
let (mut p, cid) = scene_project();
|
||||
let floor = p.comp_mut(cid).unwrap().layers.last_mut().unwrap();
|
||||
set(floor, "materialOptions/acceptsShadows", Value::Enum(2));
|
||||
let s = scene(&p, cid);
|
||||
assert!(s.materials.iter().any(|m| m.shadows_only) && !s.shadows.is_empty());
|
||||
compare(&s, &g);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_raster_perf_smoke() {
|
||||
let Some(g) = gpu() else { return };
|
||||
|
||||
@@ -62,6 +62,8 @@ pub struct Material {
|
||||
pub unlit: bool,
|
||||
pub accepts_lights: bool,
|
||||
pub receives_shadows: bool,
|
||||
/// "Accepts Shadows: Only": draws just the shadows it receives, as black × their amount.
|
||||
pub shadows_only: bool,
|
||||
pub casts_shadows: bool,
|
||||
/// Drawn (false for "Casts Shadows: Only").
|
||||
pub visible: bool,
|
||||
@@ -92,6 +94,7 @@ impl Default for Material {
|
||||
unlit: false,
|
||||
accepts_lights: true,
|
||||
receives_shadows: true,
|
||||
shadows_only: false,
|
||||
casts_shadows: false,
|
||||
visible: true,
|
||||
diffuse_k: 1.0,
|
||||
@@ -104,7 +107,7 @@ impl Default for Material {
|
||||
impl Material {
|
||||
/// Drawn in the transparent (sorted, blended) pass.
|
||||
pub fn transparent(&self) -> bool {
|
||||
self.alpha_mode == 2 || self.opacity < 0.999
|
||||
self.alpha_mode == 2 || self.opacity < 0.999 || self.shadows_only
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +363,7 @@ fn card_material(ctx: &EvalCtx, layer: &Layer) -> Material {
|
||||
double_sided: true,
|
||||
accepts_lights: c.accepts_lights,
|
||||
receives_shadows: c.accepts_shadows != 0,
|
||||
shadows_only: c.accepts_shadows == 2,
|
||||
casts_shadows: c.casts_shadows != 0,
|
||||
visible: c.casts_shadows != 2,
|
||||
diffuse_k: c.diffuse,
|
||||
@@ -375,9 +379,11 @@ fn model_options(ctx: &EvalCtx, layer: &Layer, lin: bool) -> (Material, Option<m
|
||||
let e = |m: &str, d: u32| g.and_then(|g| g.get(m)).map_or(d, |p| ctx.value(layer, p).as_enum());
|
||||
let b = |m: &str| g.and_then(|g| g.get(m)).is_none_or(|p| ctx.value(layer, p).as_bool());
|
||||
let casts = e("castsShadows", 0);
|
||||
let accepts = e("acceptsShadows", 1);
|
||||
let opts = Material {
|
||||
accepts_lights: b("acceptsLights"),
|
||||
receives_shadows: e("acceptsShadows", 1) != 0,
|
||||
receives_shadows: accepts != 0,
|
||||
shadows_only: accepts == 2,
|
||||
casts_shadows: casts != 0,
|
||||
visible: casts != 2,
|
||||
..Material::default()
|
||||
|
||||
@@ -262,6 +262,16 @@ pub fn light_shadow(s: &Scene, l: &Light, p: [f32; 3], n_l: f32) -> f32 {
|
||||
1.0 - l.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 {
|
||||
return (scale(l.dir, -1.0), f32::INFINITY);
|
||||
}
|
||||
let d = sub(l.pos, p);
|
||||
let len = dot(d, d).sqrt();
|
||||
if len < 1e-6 { ([0.0, 0.0, -1.0], 0.0) } else { (scale(d, 1.0 / len), len) }
|
||||
}
|
||||
|
||||
/// Distance attenuation × spot cone of a light at `p` (AE falloff modes).
|
||||
pub fn attenuation(l: &Light, p: [f32; 3], to_l: [f32; 3], dist: f32) -> f32 {
|
||||
let mut k = match l.falloff {
|
||||
@@ -341,6 +351,9 @@ pub fn shade(s: &Scene, f: &Frag) -> Option<[f32; 4]> {
|
||||
if let Some(t) = tex(m.tex_emissive) {
|
||||
emissive = [emissive[0] * t[0], emissive[1] * t[1], emissive[2] * t[2]];
|
||||
}
|
||||
if m.shadows_only {
|
||||
return Some(shadow_only(s, m, f, alpha));
|
||||
}
|
||||
if m.unlit || !m.accepts_lights || (s.lights.is_empty() && s.env.is_none() && s.ambient == [0.0; 3]) {
|
||||
let c = add(rgb, emissive);
|
||||
return Some([c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha]);
|
||||
@@ -370,13 +383,7 @@ pub fn shade(s: &Scene, f: &Frag) -> Option<[f32; 4]> {
|
||||
let v = if s.ortho { scale(s.cam_fwd, -1.0) } else { norm(sub(s.eye, f.pos)) };
|
||||
let mut out = [0.0f32; 3];
|
||||
for l in &s.lights {
|
||||
let (to_l, dist) = if l.kind == 0 {
|
||||
(scale(l.dir, -1.0), f32::INFINITY)
|
||||
} else {
|
||||
let d = sub(l.pos, f.pos);
|
||||
let len = dot(d, d).sqrt();
|
||||
if len < 1e-6 { ([0.0, 0.0, -1.0], 0.0) } else { (scale(d, 1.0 / len), len) }
|
||||
};
|
||||
let (to_l, dist) = to_light(l, f.pos);
|
||||
let k = attenuation(l, f.pos, to_l, dist);
|
||||
if k <= 0.0 {
|
||||
continue;
|
||||
@@ -408,3 +415,25 @@ pub fn shade(s: &Scene, f: &Frag) -> Option<[f32; 4]> {
|
||||
let c = add(out, emissive);
|
||||
Some([c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha])
|
||||
}
|
||||
|
||||
/// "Accepts Shadows: Only" (classic `compose::shade_texel`): black at the strongest shadow any
|
||||
/// light casts on the fragment, nothing where it is unshadowed or takes no lights.
|
||||
fn shadow_only(s: &Scene, m: &Material, f: &Frag, alpha: f32) -> [f32; 4] {
|
||||
if m.unlit || !m.accepts_lights {
|
||||
return [0.0; 4];
|
||||
}
|
||||
let n = if f.front { norm(f.normal) } else { scale(norm(f.normal), -1.0) };
|
||||
let mut amount = 0.0f32;
|
||||
for l in &s.lights {
|
||||
let (to_l, dist) = to_light(l, f.pos);
|
||||
if attenuation(l, f.pos, to_l, dist) <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let n_l = dot(n, to_l);
|
||||
if n_l <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
amount = amount.max(1.0 - light_shadow(s, l, f.pos, n_l));
|
||||
}
|
||||
[0.0, 0.0, 0.0, alpha * amount]
|
||||
}
|
||||
|
||||
@@ -237,6 +237,73 @@ fn shadows_darken_the_receiver() {
|
||||
assert!(img.data[75 * 200 + 100][0] > side[0] * 0.8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_shadows_only_draws_just_the_shadow() {
|
||||
// #1691: a shadow catcher (card or plane primitive, "Accepts Shadows: Only") in front of a
|
||||
// red backdrop card, an invisible sphere casting onto it, a point light behind the camera.
|
||||
let (mut p, cid) = project(200, 150);
|
||||
let red = p.add_item(
|
||||
"R",
|
||||
Default::default(),
|
||||
None,
|
||||
ItemKind::Solid(effectcraft_project::Solid { color: [1.0, 0.0, 0.0], width: 400, height: 400, pixel_aspect: 1.0 }),
|
||||
);
|
||||
add(&mut p, cid, LayerSource::Solid { item: red }, |l| {
|
||||
l.switches.three_d = true;
|
||||
set(l, "transform/position", Value::Vec3([100.0, 75.0, 300.0]));
|
||||
set(l, "materialOptions/acceptsShadows", Value::Enum(0));
|
||||
});
|
||||
let white = p.add_item(
|
||||
"W",
|
||||
Default::default(),
|
||||
None,
|
||||
ItemKind::Solid(effectcraft_project::Solid { color: [1.0, 1.0, 1.0], width: 400, height: 400, pixel_aspect: 1.0 }),
|
||||
);
|
||||
add(&mut p, cid, LayerSource::Solid { item: white }, |l| {
|
||||
l.switches.three_d = true;
|
||||
set(l, "transform/position", Value::Vec3([100.0, 75.0, 200.0]));
|
||||
set(l, "materialOptions/acceptsShadows", Value::Enum(2));
|
||||
});
|
||||
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));
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Point }, |l| {
|
||||
set(l, "transform/position", Value::Vec3([100.0, 75.0, -300.0]));
|
||||
set(l, "lightOptions/castsShadows", Value::Bool(true));
|
||||
});
|
||||
let check = |img: &crate::Image, what: &str| {
|
||||
let (mid, side) = (img.data[75 * 200 + 100], img.data[75 * 200 + 60]);
|
||||
// Unshadowed: the catcher is see-through, the lit red card behind it shows.
|
||||
assert!(side[0] > 0.5 && side[1] < side[0] * 0.25 && side[3] > 0.99, "{what}: side {side:?}");
|
||||
// Shadowed: black over the red card.
|
||||
assert!(mid[0] < 0.02 && mid[3] > 0.99, "{what}: shadow {mid:?} vs side {side:?}");
|
||||
};
|
||||
check(&render(&p, cid), "card");
|
||||
// Nothing behind the catcher: only the shadow is opaque.
|
||||
let solo = |p: &Project| {
|
||||
let mut q = p.clone();
|
||||
q.comp_mut(cid).unwrap().layers.pop();
|
||||
let img = render(&q, cid);
|
||||
let (mid, side) = (img.data[75 * 200 + 100], img.data[75 * 200 + 60]);
|
||||
assert!(side[3] < 1e-3, "unshadowed catcher is transparent: {side:?}");
|
||||
assert!(mid[3] > 0.5 && mid[0] < 1e-3, "shadow is black: {mid:?}");
|
||||
};
|
||||
solo(&p);
|
||||
// The same with a plane primitive as the catcher.
|
||||
let layers = &mut p.comp_mut(cid).unwrap().layers;
|
||||
layers.remove(2);
|
||||
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/acceptsShadows", Value::Enum(2));
|
||||
});
|
||||
check(&render(&p, cid), "plane");
|
||||
solo(&p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cards_and_environment_light() {
|
||||
let (mut p, cid) = project(120, 90);
|
||||
|
||||
Reference in New Issue
Block a user