Skip to content

Commit 337a739

Browse files
authored
Hierarchical effects and GPU spawn event (#424)
This change introduces hierarchical effects, the ability of an effect to be parented to another effect through the `EffectParent` component. Child effects can inherit attributes from their parent when spawned during the init pass, but are otherwise independent effects. They replace the old group system, which is entirely removed. The parent effect can emit GPU spawn events, which are consumed by the child effect to spawn particles instead of the traditional CPU spawn count. Those GPU spawn events currently are just the ID of the parent particles, to allow read-only access to its attribute in _e.g._ the new `InheritAttributeModifier`. The ribbon/trail system is also reworked. The atomic linked list based on `Attribute::PREV` and `Attribute::NEXT` is abandoned, and replaced with an explicit sort compute pass which orders particles by `Attribute::RIBBON_ID` first, and `Attribute::AGE` next. The ribbon ID is any `u32` value unique to each ribbon/trail. Sorting particles by age inside a given ribbon/trail allows avoiding the edge case where a particle in the middle of a trail dies, leaving a gap in the list. A migration guide is provided from v0.14 to the upcoming v0.15 which will include this change, due to the large change of behavior and APIs.
1 parent c747560 commit 337a739

38 files changed

+9617
-4723
lines changed

.github/workflows/ci.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121
libasound2-dev libudev-dev;
122122
if: runner.os == 'linux'
123123
- uses: actions/checkout@v4
124-
- uses: actions/cache@v2
124+
- uses: actions/cache@v4
125125
with:
126126
path: |
127127
~/.cargo/bin/

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Rust
22
target/
33
Cargo.lock
4+
.cargo/config.toml
45

56
# IDEs
67
.vs*
@@ -9,6 +10,7 @@ Cargo.lock
910
.DS_Store
1011

1112
# Perf
13+
profiling/
1214
*.cap
1315
perf.data
1416
perf.data.old

CHANGELOG.md

+43
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- Added new `DebugSettings` resource, allowing to instruct a GPU debugger to start capturing API commands,
1111
either right away or each time a new effect is spawned.
12+
- Added new `EffectParent` component to declare that an effect is parented to another effect.
13+
Parenting allows the child effect to consume GPU spawn events and read the parent's particle attributes.
14+
See the migration guide about hierarchical parent/child effects.
15+
- Added new `DefaultMesh` resource storing the default mesh used for particles when `EffectAsset::mesh` is `None`.
16+
The default mesh remains a Z-facing unit quad.
17+
- Added the `EmitSpawnEventModifier` to configure GPU spawn event spawning from an effect.
18+
See the migration guide about GPU spawn events.
19+
- Added `ShaderWriter::set_emits_gpu_spawn_events()` to declare that an effect emits GPU spawn events.
20+
This is generally used automatically by `EmitSpawnEventModifier`.
21+
- Added `EventEmitCondition` to determine when to emit GPU spawn events.
22+
- Added `EffectAsset::with_motion_integration()` to assign the `MotionIntegration` of an effect.
23+
- Added new `Attribute::RIBBON_ID` determining which ribbon a particle is part of.
24+
See the migration guide about the new trail and ribbon implementation.
25+
- Added new custom `Attribute::U32_0` to `U32_3`, similar to their float counterparts.
26+
- Added new pseudo-attributes `ID` and `PARTICLE_COUNTER`. Those attributes can be read but not written.
27+
They do not consume any storage space in the particle's layout.
28+
The `ID` is the unique ID of the particle in the effect instance, which may be recycled when the particle dies.
29+
The `PARTICLE_COUNTER` is a monotonically increasing counter which can be considered unique for the duration
30+
of an effect's lifetime (it will wrap at 2^32).
31+
- Added new `Expr::ParentAttribute` expression, which reads a particle attribute like `Expr::Attribute`,
32+
but does so on the parent effect of this effect instead.
33+
This attribute is only valid if the effect has a parent (uses `EffectParent`), and only to read the attribute.
34+
Also added `ExprWriter::parent_attr()` to create that expression with a writer.
35+
- Added new `InheritAttributeModifier` to set the value of a particle attribute by copying the value of its parent.
36+
This modifier is only valid for the `ModifierContext::Init` pass,
37+
when an effect has a parent (uses `EffectParent`).
1238

1339
### Changed
1440

@@ -19,16 +45,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1945
which is `None` if the layout is empty (as opposed to an empty string previously).
2046
- Removed effects are now deallocated from the render world before the extract schedule, instead of during it.
2147
This should have no consequence, unless you were using a system inserted explicitly before Hanabi's extraction.
48+
- `EffectAsset::capacities` is reverted to a single `capacity: u32` value per effect.
49+
See the migration guide about hierarchical parent/child effects replacing groups.
50+
- Renamed the `tick_initializers()` system back into `tick_spawners()` for clarity.
51+
- `Attribute::PREV` and `Attribute::NEXT` are soft-deprecated.
52+
You can continue to use them, but they do not have any influence anymore on ribbons and trails,
53+
nor any other built-in functionality of Hanabi.
2254

2355
### Fixed
2456

2557
- Fixed a bug with opaque effects not rendering.
58+
- Fixed a bug in `ParticleLayout` where some fields may not have been aligned according to the WGSL rules.
59+
- Added a workaround for a `wgpu` bug on macOS/Metal backend related to `ParticleLayout` alignment.
2660

2761
### Removed
2862

2963
- Removed the `EffectSystems::GatherRemovedEffects` system set.
3064
Removed effects are now processed via observers, which execute during the render world sync just before extraction.
3165
Removed the `RemovedEffectsEvent` type too.
66+
- Removed the `EffectInitializers` component. For CPU spawning, use `EffectSpawner` instead.
67+
For GPU spawning, see the migration guide about the removal of groups and use of `EffectParent` and GPU spawn events.
68+
- Similarly, removed `Initializer` and `Cloner`.
69+
- Removed `GroupedModifier` and related `EffectAsset` fields, following the removal of groups.
70+
See the migration guide about hierarchical parent/child effects replacing groups.
71+
- Similarly, removed grouped variant of all `EffectAsset` functions.
72+
- Similarly, removed `ParticleGroupSet`.
73+
- Removed `EffectAsset::ribbon_group` as well as `with_trails()` and `with_ribbons()`.
74+
Use the `Attribute::RIBBON_ID` instead to assign a per-particle ribbon ID.
3275

3376
## [0.14.0] 2024-12-09
3477

Cargo.toml

+10
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ bevy_sprite = "0.15"
105105
bevy_text = "0.15"
106106
bevy_ui = "0.15"
107107
bevy_window = "0.15"
108+
bevy_picking = "0.15"
108109

109110
# For glTF animations (Fox.glb)
110111
bevy_gltf = { version = "0.15", features = [ "bevy_animation" ] }
@@ -216,6 +217,15 @@ required-features = [
216217
"bevy/bevy_window",
217218
]
218219

220+
[[test]]
221+
name = "single_particle"
222+
path = "gpu_tests/single_particle.rs"
223+
harness = false
224+
required-features = [
225+
"bevy/bevy_winit",
226+
"bevy/bevy_window",
227+
]
228+
219229
[[test]]
220230
name = "properties"
221231
path = "gpu_tests/properties.rs"

docs/migration-v0.14-to-v0.15.md

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Migration Guide v0.14 -> v0.15
2+
3+
🎆 Hanabi v0.15 contains a few major API breaking changes.
4+
5+
This guide helps the user migrate from v0.14 to v0.15 of 🎆 Hanabi.
6+
Users are encouraged to also read the [`CHANGELOG`](../CHANGELOG.md)
7+
for an exhaustive list of all changes.
8+
9+
## Hierarchical effects
10+
11+
🎆 Hanabi v0.15 supports a new feature called _hierarchical effects_,
12+
whereby an effect can be parented to another effect.
13+
Parenting an effect unlocks new features for effect authoring:
14+
15+
- A child effect has read-only access to the particles of its parent,
16+
and can therefore inherit _e.g._ their position via the new `InheritAttributeModifier`.
17+
- A child effect's particles can be spawned dynamically on GPU by its parent,
18+
allowing to "chain" effects.
19+
For example, the parent can spawn a particle in a child effect
20+
when one of its own particles dies.
21+
22+
The effect parent/child hierarchy forms a tree (no cycles).
23+
An effect can declare its parent with the `EffectParent` component,
24+
and can have a single parent only.
25+
A parent effect can have multiple children,
26+
and can itself be a child of a third effect.
27+
28+
## GPU spawn events
29+
30+
Parent effects (see [Hierarchical effects](#hierarchical-effects)) can emit _GPU spawn events_,
31+
which are GPU-side "events" to spawn particles into one of their child effects.
32+
With GPU spawn events, child events can be entirely GPU driven,
33+
and react to the behavior of their parent.
34+
For example, a GPU spawn event can be emitted when a particle dies.
35+
This allows _e.g._ a child event to emit an explosion of particles,
36+
which visually looks in direct relation with the death of the parent's particle.
37+
38+
Particles spawned via GPU spawn events often inherit one or more attribute from their parent,
39+
via the new `InheritAttributeModifier`.
40+
This is made possible by the fact that a GPU spawn event contains the ID of the parent particle,
41+
which allows reading its attributes from the child effect's init pass.
42+
43+
To declare a parent effect emitting GPU spawn events, use:
44+
45+
```rust
46+
let parent_effect = EffectAsset::new(32, spawner, module)
47+
.update(EmitSpawnEventModifier {
48+
condition: EventEmitCondition::OnDie,
49+
count: 45,
50+
child_index: 0,
51+
});
52+
let parent_handle = effects.add(parent_effect);
53+
let parent_entity = commands.spawn(ParticleEffect::new(parent_handle)).id();
54+
```
55+
56+
The child index determines which child effect will consume those emitted events,
57+
since a parent effect can have multiple children.
58+
59+
For the child effect, which consumes those GPU spawn events, use:
60+
61+
```rust
62+
let child_effect = EffectAsset::new(250, unused_spawner, module)
63+
// On spawn, copy the POSITION of the particle which emitted the GPU event
64+
.init(InheritAttributeModifier::new(Attribute::POSITION));
65+
let child_handle = effects.add(child_effect);
66+
commands.spawn((
67+
ParticleEffect::new(child_handle),
68+
EffectParent(parent_entity),
69+
));
70+
```
71+
72+
See the updated `firework.rs` example for a full-featured demo.
73+
74+
## New group-less ribbon and trail implementation
75+
76+
Previously in 🎆 Hanabi v0.14, ribbons and trails made use of _groups_,
77+
which allowed partitioning a particle buffer into sub-buffers, one per group,
78+
and spawn particles from one group into the other.
79+
80+
If this sounds familiar, this is because that feature is nearly identical to GPU spawn events,
81+
at least conceptually.
82+
The API and implementation however were extremely confusing for the user,
83+
with things like initializing a group particle from the Update pass instead of the Init one,
84+
and the inability to use init shaders for those particles.
85+
86+
The new ribbon and trail implementation gets rid of all those restrictions,
87+
and restores the common patterns established for all effects:
88+
89+
- particles are initialized on spawn in the init pass, via init modifiers.
90+
- particles are updated every frame while alive in the update pass, via update modifiers.
91+
92+
The entire group feature has been removed.
93+
Instead, a ribbon or trail is now defined by the `Attribute::RIBBON_ID` assigned to each particle.
94+
All particles with a same ribbon ID are part of the same ribbon.
95+
There's no other meaning to that value, so you can use any value that makes sense.
96+
At runtime, after the update pass, all particles are sorted by their `RIBBON_ID` to group them into ribbons,
97+
and inside a given ribbon (same ribbon ID) the particles are sorted by their age.
98+
This sorting not only solves the issue of grouping particles without complex buffer management,
99+
but also gets rid of the annoying edge cases where a particle in a middle of a ribbon would die,
100+
leaving a gap in it, with forced the previous implementation to constraint to lifetime of particles
101+
via an external mechanism instead of using the `Attribute::LIFETIME`.
102+
103+
If you're familiar with trails and ribbons in 🎆 Hanabi v0.14 and earlier,
104+
you may remember about the `Attribute::PREV` and `Attribute::NEXT`.
105+
Those attributes were used to chain together particles into trails and ribbons,
106+
forming a linked list.
107+
Not only did they use a lot of storage space (twice as much as `RIBBON_ID`),
108+
the operations on the linked list were difficult to perform atomically,
109+
and have led to several bugs in the past.
110+
With 🎆 Hanabi v0.15, those attributes are not used anymore, and are soft-deprected.
111+
You can continue to use them for any other purpose,
112+
but they do not have anymore a built-in effect.
113+
114+
To create a trail or ribbon, simply assign the `Attribute::RIBBON_ID`:
115+
116+
```rust
117+
// Example: single trail/ribbon effect
118+
119+
let init_ribbon_id = SetAttributeModifier {
120+
attribute: Attribute::RIBBON_ID,
121+
// We use a constant '0' for all particles; any value works.
122+
value: writer.lit(0u32).expr(),
123+
};
124+
```
125+
126+
When using multi-trail / multi-ribbon, each trail/ribbon needs a unique ID.
127+
You can calculate that value from the parent effect, store it,
128+
and read it back in the child effect.
129+
130+
```rust
131+
// Example: multi-trail/multi-ribbon
132+
133+
// In the parent effect:
134+
let parent_init_ribbon_id = SetAttributeModifier::new(
135+
Attribute::U32_0,
136+
// Store a unique value per parent particle, used as ribbon ID in children
137+
writer.attr(Attribute::PARTICLE_COUNTER).expr(),
138+
);
139+
140+
// In the child effect:
141+
let child_init_ribbon_id = SetAttributeModifier {
142+
attribute: Attribute::RIBBON_ID,
143+
// Read back the unique value from the parent particle
144+
value: writer.parent_attr(Attribute::U32_0).expr(),
145+
};
146+
```
147+
148+
See the updated `ribbon.rs` example for a full-featured demo of a single ribbon,
149+
and the `worms.rs` and `firework.rs` examples
150+
for an example of combining hierarchical effects and ribbons,
151+
and use multiple ribbons in the same effect.

examples/activate.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ fn setup(
153153

154154
fn update(
155155
mut q_balls: Query<(&mut Ball, &mut Transform, &Children)>,
156-
mut q_spawner: Query<&mut EffectInitializers>,
156+
mut q_spawner: Query<&mut EffectSpawner>,
157157
mut q_text: Query<&mut Text, With<StatusText>>,
158158
time: Res<Time>,
159159
) {
@@ -171,8 +171,8 @@ fn update(
171171
// CoreSet::PostUpdate, so will not be available yet. Ignore for a frame
172172
// if so.
173173
let is_active = transform.translation.y < 0.0;
174-
if let Ok(mut spawner) = q_spawner.get_mut(children[0]) {
175-
spawner.set_active(is_active);
174+
if let Ok(mut effect_spawner) = q_spawner.get_mut(children[0]) {
175+
effect_spawner.set_active(is_active);
176176
}
177177

178178
let mut text = q_text.single_mut();

0 commit comments

Comments
 (0)