Files
effectcraft/crates/gpu/src/tests_fx_particles.rs
T
Brandon ThomasandClaude Opus 5.5 1c855c3e07 M13.29: GPU effects part D (particles and remaining fallbacks)
GPU render passes for CC Particle World / Systems II, Particle Playground,
CC Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise (fx_particles,
chunked multi-pass raster); Shatter wireframe views, Foam extras, Timewarp
with a Matte Layer, strong Warp Fisheye/Twist (CPU f64 inverse map) and
custom .ocio configs no longer fall back to the CPU. CPU refactors pinned by
particle_golden / timewarp_golden / sim_golden hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:35:56 -04:00

146 lines
5.4 KiB
Rust

//! GPU particle render passes (`fx_particles`) vs the CPU effects (the oracle): the frame's
//! plan is shared, the raster runs on each side. Direct on a buffer (full and half resolution)
//! and composited at 8 and 32 bpc (`tests_fx_sim`'s harness).
use effectcraft_keyframe::Value;
use crate::tests::n;
use crate::tests_fx_sim::{case, composited, e, off, on};
fn v2(x: f64, y: f64) -> Value {
Value::Vec2([x, y])
}
#[test]
fn particle_world_types_and_modes() {
for kind in 0..9 {
case(
"ec.sim.ccparticleworld",
&[("birthRate", n(1.5)), ("particle/particleType", e(kind)), ("particle/transferMode", e(kind % 4)), ("particle/birthSize", n(0.3))],
1.0,
0.0,
);
}
for anim in [1, 2, 5, 6, 8] {
case("ec.sim.ccparticleworld", &[("physics/animation", e(anim)), ("particle/particleType", e(1)), ("particle/sizeVariation", n(40.0))], 0.8, 0.0);
}
// Nothing born yet: the layer as is.
case("ec.sim.ccparticleworld", &[("birthRate", n(0.0))], 0.0, 0.0);
}
#[test]
fn particle_systems_ii_types_and_modes() {
for kind in 0..9 {
case("ec.sim.ccparticlesystems2", &[("birthRate", n(3.0)), ("particle/particleType", e(kind)), ("particle/transferMode", e(kind % 4))], 1.0, 0.0);
}
case("ec.sim.ccparticlesystems2", &[("physics/animation", e(4)), ("producer/radiusX", n(8.0)), ("physics/direction", n(70.0))], 1.3, 0.0);
}
/// A Particle Playground grid (cannon off) plus `more`.
fn grid(more: &[(&'static str, Value)]) -> Vec<(&'static str, Value)> {
let mut g = vec![
("cannonEnabled", off()),
("gridEnabled", on()),
("grid/gridPosition", v2(35.0, 22.0)),
("grid/gridWidth", n(40.0)),
("grid/gridHeight", n(20.0)),
("grid/particlesAcross", n(5.0)),
("grid/particlesDown", n(3.0)),
("grid/gridParticleRadius", n(3.0)),
("gravity/gravityForce", n(20.0)),
];
g.extend(more.iter().cloned());
g
}
#[test]
fn particle_playground() {
case("ec.sim.particleplayground", &[], 1.0, 0.0);
case(
"ec.sim.particleplayground",
&[("cannon/cannonPosition", v2(20.0, 40.0)), ("cannon/particlesPerSecond", n(150.0)), ("cannon/velocity", n(90.0)), ("gravity/gravityForce", n(40.0))],
1.2,
0.0,
);
case("ec.sim.particleplayground", &grid(&[]), 0.7, 0.0);
case(
"ec.sim.particleplayground",
&grid(&[("options/gridText", Value::Str("Ab C".into())), ("grid/gridParticleRadius", n(12.0)), ("options/autoOrientRotation", on())]),
0.6,
0.0,
);
case("ec.sim.particleplayground", &grid(&[("repel/repelForce", n(5.0)), ("repel/repelForceRadius", n(12.0))]), 0.9, 0.0);
composited(
"ec.sim.particleplayground",
&[
("cannonEnabled", off()),
("exploderEnabled", on()),
("layerExploder/explodeLayer", Value::Layer(Some(0))),
("layerExploder/radiusOfNewParticles", n(3.0)),
],
0.5,
0.0,
);
// Layer Map: every grid particle shows the other layer, at frames set by the offset type.
for (kind, offset) in [(0, 0.0), (2, 0.5), (3, 1.0)] {
composited(
"ec.sim.particleplayground",
&grid(&[
("layerMapEnabled", on()),
("layerMap/layerMapLayer", Value::Layer(Some(0))),
("layerMap/timeOffsetType", e(kind)),
("layerMap/timeOffset", n(offset)),
("grid/particlesAcross", n(3.0)),
("grid/particlesDown", n(2.0)),
]),
0.8,
0.0,
);
}
composited(
"ec.sim.particleplayground",
&grid(&[
("mapperEnabled", on()),
("persistentPropertyMapper/useLayerAsMap", Value::Layer(Some(0))),
("persistentPropertyMapper/mapRedTo", e(8)),
("persistentPropertyMapper/redMin", n(-50.0)),
("persistentPropertyMapper/redMax", n(50.0)),
]),
0.7,
0.0,
);
}
#[test]
fn ball_action_pixel_polly_scatterize() {
case("ec.sim.ccballaction", &[], 0.0, 0.0);
case(
"ec.sim.ccballaction",
&[("scatter", n(20.0)), ("rotationAxis", e(6)), ("rotation", n(40.0)), ("twistProperty", e(3)), ("twistAngle", n(90.0))],
0.0,
0.0,
);
case("ec.sim.ccballaction", &[("gridSpacing", n(2.0)), ("ballSize", n(80.0)), ("instabilityState", n(45.0)), ("twistProperty", e(2))], 0.0, 0.0);
for object in 0..4 {
case("ec.sim.ccpixelpolly", &[("object", e(object)), ("gridSpacing", n(6.0))], 0.6, 0.0);
}
case("ec.sim.ccpixelpolly", &[("enableDepthSort", off()), ("spinning", n(90.0)), ("gravity", n(2.0))], 1.0, 0.0);
case("ec.sim.ccpixelpolly", &[("startTime", n(1.0))], 0.5, 0.0);
case("ec.sim.ccscatterize", &[("scatter", n(10.0))], 0.0, 0.0);
case("ec.sim.ccscatterize", &[("rightTwist", n(60.0)), ("leftTwist", n(-30.0)), ("transferMode", e(1)), ("scatter", n(4.0))], 0.0, 0.0);
case("ec.sim.ccscatterize", &[], 0.0, 0.0);
}
#[test]
fn curl_noise() {
case("ec.noise.curlnoise", &[], 0.0, 0.0);
case("ec.noise.curlnoise", &[("view", e(1))], 0.0, 0.0);
case(
"ec.noise.curlnoise",
&[("edgeBehavior", e(1)), ("steps", n(8.0)), ("displacementAmount", n(60.0)), ("scale", n(20.0)), ("rotation", n(30.0)), ("evolution", n(90.0))],
0.0,
0.0,
);
case("ec.noise.curlnoise", &[("displacementAmount", n(0.0))], 0.0, 0.0);
}