mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
Refraction samples are clamped to the layer rectangle instead of the buffer, so a sample past the layer's edge in a buffer with transparent padding (a half-resolution preview's, a text or shape layer's) repeats the layer's edge pixels rather than reaching the pale colour that stands in for transparent ones. Transparent pixels inside the layer still show it. Same bounds on the GPU path. Re-pins the four half-resolution CC Bubbles golden hashes (aarch64 macOS); the full-resolution ones are unchanged. Adds a CPU test and a GPU-vs-CPU parity test on padded buffers.
213 lines
10 KiB
Rust
213 lines
10 KiB
Rust
//! GPU simulation render passes vs the CPU effects (the oracle): the frame's plan (sprites,
|
|
//! pieces, blobs, drops, height grid) is shared, the raster / shading pass runs on each side.
|
|
//! Direct on a buffer (full and half resolution) and composited at 8 and 32 bpc, at a time
|
|
//! where the simulations have something to show.
|
|
|
|
use effectcraft_keyframe::Value;
|
|
use effectcraft_project::BitDepth;
|
|
use effectcraft_render::RenderOpts;
|
|
use effectcraft_time::Tick;
|
|
|
|
use crate::tests::{Scene, c, check, compare_at, diff, gpu, n, opts, pattern, set};
|
|
|
|
pub(crate) fn e(v: u32) -> Value {
|
|
Value::Enum(v)
|
|
}
|
|
|
|
pub(crate) fn on() -> Value {
|
|
Value::Bool(true)
|
|
}
|
|
|
|
pub(crate) fn off() -> Value {
|
|
Value::Bool(false)
|
|
}
|
|
|
|
/// The effect alone on a buffer at layer time `t` (`allow` = fraction of pixels over 1e-3).
|
|
pub(crate) fn direct(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
|
let Some(g) = gpu() else { return };
|
|
let spec = effectcraft_effects::find(id).unwrap();
|
|
let size = [70.0, 44.0];
|
|
let mut params =
|
|
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
|
for (k, v) in vals {
|
|
params.values.insert(k.to_string(), v.clone());
|
|
}
|
|
for scale in [1.0, 0.5] {
|
|
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
|
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
|
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: t, layer_size: size, seed: 11, adjustment: false, env: Default::default() };
|
|
let cpu = (spec.render)(&ctx(), buf.clone());
|
|
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
|
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
|
let d = diff(&cpu.img, &out.img, 1e-3);
|
|
assert!(
|
|
d.over as f64 <= allow * d.total as f64,
|
|
"{id} {vals:?} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
|
|
d.over,
|
|
d.total,
|
|
d.max,
|
|
d.worst
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A footage layer with the effect over a background and a second footage layer (the target of
|
|
/// `Value::Layer(Some(0))` parameters), composited at 8 and 32 bpc at comp time `t`.
|
|
pub(crate) fn composited(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
|
let mut s = Scene::new(depth);
|
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
|
s.push(bg);
|
|
let other = s.footage(60, 50);
|
|
let other_id = s.push(other);
|
|
let mut l = s.footage(70, 44);
|
|
let vals: Vec<(&str, Value)> =
|
|
vals.iter().map(|(k, v)| (*k, if *v == Value::Layer(Some(0)) { Value::Layer(Some(other_id.0)) } else { v.clone() })).collect();
|
|
s.effect(&mut l, id, &vals);
|
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
|
s.push(l);
|
|
let at = Tick::from_seconds_f64(t);
|
|
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), at), allow);
|
|
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, at), allow);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn case(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
|
direct(id, vals, t, allow);
|
|
composited(id, vals, t, allow);
|
|
}
|
|
|
|
#[test]
|
|
fn weather_and_star_burst() {
|
|
case("ec.sim.ccrainfall", &[("drops", n(800.0)), ("size", n(3.0))], 0.5, 0.0);
|
|
case("ec.sim.ccrainfall", &[("drops", n(500.0)), ("extras/appearance", e(1)), ("transferMode", e(0)), ("wind", n(800.0))], 0.7, 0.0);
|
|
case("ec.sim.ccrainfall", &[("drops", n(300.0)), ("compositeWithOriginal", off()), ("size", n(6.0))], 0.3, 0.0);
|
|
case("ec.sim.ccsnowfall", &[("flakes", n(600.0)), ("size", n(6.0))], 0.5, 0.0);
|
|
case("ec.sim.ccsnowfall", &[("flakes", n(400.0)), ("transferMode", e(1)), ("wiggle/wiggleAmount", n(8.0))], 1.2, 0.0);
|
|
case("ec.sim.ccstarburst", &[("gridSpacing", n(6.0))], 0.4, 0.0);
|
|
case("ec.sim.ccstarburst", &[("gridSpacing", n(4.0)), ("blendWithOriginal", n(40.0)), ("size", n(160.0))], 0.9, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn bubbles_drizzle_mercury() {
|
|
case("ec.sim.ccbubbles", &[("bubbleAmount", n(60.0)), ("bubbleSize", n(3.0))], 0.5, 0.0);
|
|
for shading in 1..5 {
|
|
case(
|
|
"ec.sim.ccbubbles",
|
|
&[("bubbleAmount", n(40.0)), ("bubbleSize", n(4.0)), ("shadingType", e(shading)), ("reflectionType", e(shading % 2))],
|
|
0.8,
|
|
0.0,
|
|
);
|
|
}
|
|
case("ec.sim.ccdrizzle", &[("dripRate", n(12.0)), ("spreading", n(300.0))], 0.6, 0.0);
|
|
case("ec.sim.ccdrizzle", &[("dripRate", n(20.0)), ("shading/diffuse", n(40.0)), ("shading/ambient", n(-20.0)), ("light/lightHeight", n(60.0))], 1.1, 0.0);
|
|
case("ec.sim.ccmrmercury", &[("birthRate", n(4.0)), ("blobBirthSize", n(1.5)), ("blobDeathSize", n(1.0))], 1.0, 0.0);
|
|
case("ec.sim.ccmrmercury", &[("birthRate", n(3.0)), ("blobInfluence", n(60.0)), ("shading/metal", n(80.0)), ("gravity", n(2.0))], 1.5, 0.0);
|
|
}
|
|
|
|
/// CC Bubbles on a buffer with transparent padding around the layer: both sides clamp the
|
|
/// refraction samples to the layer rectangle, not to the buffer (#2024).
|
|
#[test]
|
|
fn bubbles_on_padded_buffers() {
|
|
let Some(g) = gpu() else { return };
|
|
let spec = effectcraft_effects::find("ec.sim.ccbubbles").unwrap();
|
|
let size = [70.0, 44.0];
|
|
for reflection in [0, 1] {
|
|
let mut params =
|
|
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
|
for (k, v) in [("bubbleAmount", n(80.0)), ("bubbleSize", n(4.0)), ("reflectionType", e(reflection))] {
|
|
params.values.insert(k.to_string(), v);
|
|
}
|
|
for scale in [1.0, 0.5] {
|
|
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
|
let mut buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
|
buf.pad(6);
|
|
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.5, layer_size: size, seed: 11, adjustment: false, env: Default::default() };
|
|
let cpu = (spec.render)(&ctx(), buf.clone());
|
|
let out =
|
|
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
|
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "geometry");
|
|
let d = diff(&cpu.img, &out.img, 1e-3);
|
|
assert!(d.over == 0, "reflection {reflection} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}", d.over, d.total, d.max, d.worst);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hair_wave_world_foam() {
|
|
case("ec.sim.cchair", &[("density", n(300.0)), ("length", n(10.0))], 0.0, 0.0);
|
|
case("ec.sim.cchair", &[("density", n(200.0)), ("length", n(14.0)), ("hairColor/colorInheritance", n(30.0)), ("thickness", n(1.5))], 0.0, 0.0);
|
|
case("ec.sim.waveworld", &[], 0.5, 0.0);
|
|
case("ec.sim.waveworld", &[("view", e(1))], 0.8, 0.0);
|
|
composited("ec.sim.waveworld", &[("view", e(1)), ("ground/ground", Value::Layer(Some(0))), ("heightMapControls/renderDryAreasAs", e(1))], 0.6, 0.0);
|
|
case("ec.sim.foam", &[], 1.0, 0.0);
|
|
case("ec.sim.foam", &[("view", e(2))], 1.5, 0.0);
|
|
case("ec.sim.foam", &[("view", e(2)), ("rendering/bubbleTexture", e(4)), ("rendering/blendMode", e(2))], 1.2, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn pieces() {
|
|
case("ec.sim.carddance", &[("rows", n(5.0)), ("columns", n(7.0)), ("zRotation/zRotMultiplier", n(20.0))], 0.0, 0.0);
|
|
composited("ec.sim.carddance", &[("rows", n(4.0)), ("backLayer", Value::Layer(Some(0))), ("yRotation/yRotMultiplier", n(150.0))], 0.0, 0.0);
|
|
case("ec.sim.shatter", &[("view", e(0))], 1.0, 0.0);
|
|
case("ec.sim.shatter", &[("view", e(0)), ("shape/pattern", e(2)), ("physics/gravity", n(4.0))], 1.6, 0.0);
|
|
case("ec.transition.cardwipe", &[("completion", n(40.0))], 0.0, 0.0);
|
|
composited("ec.transition.cardwipe", &[("completion", n(60.0)), ("backLayer", Value::Layer(Some(0))), ("flipAxis", e(1))], 0.0, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn caustics() {
|
|
case("ec.sim.caustics", &[], 0.0, 0.0);
|
|
composited(
|
|
"ec.sim.caustics",
|
|
&[("water/waterSurface", Value::Layer(Some(0))), ("water/waveHeight", n(0.3)), ("water/smoothing", n(4.0)), ("lighting/lightType", e(1))],
|
|
0.0,
|
|
0.0,
|
|
);
|
|
composited(
|
|
"ec.sim.caustics",
|
|
&[
|
|
("water/waterSurface", Value::Layer(Some(0))),
|
|
("sky/sky", Value::Layer(Some(0))),
|
|
("water/surfaceOpacity", n(0.6)),
|
|
("bottom/repeatMode", e(2)),
|
|
("bottom/scaling", n(1.4)),
|
|
("bottom/blur", n(3.0)),
|
|
("sky/repeatMode", e(1)),
|
|
],
|
|
0.0,
|
|
0.0,
|
|
);
|
|
let _ = (c(0.0, 0.0, 0.0), on());
|
|
}
|
|
|
|
#[test]
|
|
fn shatter_wireframes_and_foam_extras() {
|
|
// Shatter's wireframe views (Wireframe Front View is the default) draw lines as sprites.
|
|
for view in 1..5 {
|
|
case("ec.sim.shatter", &[("view", e(view))], 1.0, 0.0);
|
|
}
|
|
case("ec.sim.shatter", &[("view", e(4)), ("force2/force2Radius", n(0.3)), ("shape/pattern", e(3))], 1.5, 0.0);
|
|
// Foam's User Defined texture, Environment Map and flow-map preview.
|
|
composited("ec.sim.foam", &[("view", e(2)), ("rendering/bubbleTexture", e(5)), ("rendering/bubbleTextureLayer", Value::Layer(Some(0)))], 1.0, 0.0);
|
|
composited(
|
|
"ec.sim.foam",
|
|
&[("view", e(2)), ("rendering/bubbleTexture", e(5)), ("rendering/bubbleTextureLayer", Value::Layer(Some(0))), ("rendering/bubbleOrientation", e(2))],
|
|
1.4,
|
|
0.0,
|
|
);
|
|
composited("ec.sim.foam", &[("view", e(2)), ("rendering/environmentMap", Value::Layer(Some(0))), ("rendering/reflectionStrength", n(0.5))], 1.2, 0.0);
|
|
composited(
|
|
"ec.sim.foam",
|
|
&[
|
|
("view", e(2)),
|
|
("rendering/environmentMap", Value::Layer(Some(0))),
|
|
("rendering/reflectionStrength", n(0.8)),
|
|
("rendering/reflectionConvergence", n(1.0)),
|
|
],
|
|
1.2,
|
|
0.0,
|
|
);
|
|
composited("ec.sim.foam", &[("view", e(1)), ("flowMap/flowMap", Value::Layer(Some(0))), ("flowMap/flowMapSteepness", n(0.5))], 1.0, 0.0);
|
|
}
|