• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_hanabi / 13640457354

03 Mar 2025 09:09PM UTC coverage: 40.055% (-6.7%) from 46.757%
13640457354

push

github

web-flow
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.

409 of 2997 new or added lines in 17 files covered. (13.65%)

53 existing lines in 11 files now uncovered.

3208 of 8009 relevant lines covered (40.05%)

18.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.67
/src/plugin.rs
1
#[cfg(feature = "2d")]
2
use bevy::core_pipeline::core_2d::Transparent2d;
3
#[cfg(feature = "3d")]
4
use bevy::core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d};
5
use bevy::{
6
    prelude::*,
7
    render::{
8
        mesh::allocator::allocate_and_free_meshes,
9
        render_asset::prepare_assets,
10
        render_graph::RenderGraph,
11
        render_phase::DrawFunctions,
12
        render_resource::{SpecializedComputePipelines, SpecializedRenderPipelines},
13
        renderer::{RenderAdapterInfo, RenderDevice},
14
        texture::GpuImage,
15
        view::{check_visibility, prepare_view_uniforms, visibility::VisibilitySystems},
16
        Render, RenderApp, RenderSet,
17
    },
18
    time::{time_system, TimeSystem},
19
};
20

21
#[cfg(feature = "serde")]
22
use crate::asset::EffectAssetLoader;
23
use crate::{
24
    asset::{DefaultMesh, EffectAsset},
25
    compile_effects,
26
    properties::EffectProperties,
27
    render::{
28
        add_effects, batch_effects, clear_all_effects, extract_effect_events, extract_effects,
29
        fixup_parents, on_remove_cached_effect, on_remove_cached_properties, prepare_bind_groups,
30
        prepare_effects, prepare_gpu_resources, prepare_property_buffers, queue_effects,
31
        resolve_parents, DebugSettings, DispatchIndirectPipeline, DrawEffects, EffectAssetEvents,
32
        EffectBindGroups, EffectCache, EffectsMeta, EventCache, ExtractedEffects,
33
        GpuBufferOperationQueue, GpuEffectMetadata, GpuSpawnerParams, ParticlesInitPipeline,
34
        ParticlesRenderPipeline, ParticlesUpdatePipeline, PropertyBindGroups, PropertyCache,
35
        RenderDebugSettings, ShaderCache, SimParams, SortBindGroups, SortedEffectBatches,
36
        StorageType as _, UtilsPipeline, VfxSimulateDriverNode, VfxSimulateNode,
37
    },
38
    spawn::{self, Random},
39
    tick_spawners,
40
    time::effect_simulation_time_system,
41
    update_properties_from_asset, CompiledParticleEffect, EffectSimulation, ParticleEffect,
42
    Spawner, ToWgslString,
43
};
44

45
/// Labels for the Hanabi systems.
46
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)]
47
pub enum EffectSystems {
48
    /// Tick all effect instances to generate particle spawn counts.
49
    ///
50
    /// This system runs during the [`PostUpdate`] schedule. Any system which
51
    /// modifies an effect spawner should run before this set to ensure the
52
    /// spawner takes into account the newly set values during its ticking.
53
    TickSpawners,
54

55
    /// Compile the effect instances, updating the [`CompiledParticleEffect`]
56
    /// components.
57
    ///
58
    /// This system runs during the [`PostUpdate`] schedule. This is largely an
59
    /// internal task which can be ignored by most users.
60
    ///
61
    /// [`CompiledParticleEffect`]: crate::CompiledParticleEffect
62
    CompileEffects,
63

64
    /// Update the properties of the effect instance based on the declared
65
    /// properties in the [`EffectAsset`], updating the associated
66
    /// [`EffectProperties`] component.
67
    ///
68
    /// This system runs during the [`PostUpdate`] schedule, after the assets
69
    /// have been updated. Any system which modifies an [`EffectAsset`]'s
70
    /// declared properties should run before this set in order for changes to
71
    /// be taken into account in the same frame.
72
    UpdatePropertiesFromAsset,
73

74
    /// Prepare effect assets for the extracted effects.
75
    ///
76
    /// Part of Bevy's own [`RenderSet::PrepareAssets`].
77
    PrepareEffectAssets,
78

79
    /// Queue the GPU commands for the extracted effects.
80
    ///
81
    /// Part of Bevy's own [`RenderSet::Queue`].
82
    QueueEffects,
83

84
    /// Prepare GPU data for the queued effects.
85
    ///
86
    /// Part of Bevy's own [`RenderSet::PrepareResources`].
87
    PrepareEffectGpuResources,
88

89
    /// Prepare the GPU bind groups once all buffers have been (re-)allocated
90
    /// and won't change this frame.
91
    ///
92
    /// Part of Bevy's own [`RenderSet::PrepareBindGroups`].
93
    PrepareBindGroups,
94
}
95

96
pub mod main_graph {
97
    pub mod node {
98
        use bevy::render::render_graph::RenderLabel;
99

100
        /// Label for the simulation driver node running the simulation graph.
101
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RenderLabel)]
102
        pub struct HanabiDriverNode;
103
    }
104
}
105

106
pub mod simulate_graph {
107
    use bevy::render::render_graph::RenderSubGraph;
108

109
    /// Name of the simulation sub-graph.
110
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RenderSubGraph)]
111
    pub struct HanabiSimulateGraph;
112

113
    pub mod node {
114
        use bevy::render::render_graph::RenderLabel;
115

116
        /// Label for the simulation node (init and update compute passes;
117
        /// view-independent).
118
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RenderLabel)]
119
        pub struct HanabiSimulateNode;
120
    }
121
}
122

123
// {626E7AD3-4E54-487E-B796-9A90E34CC1EC}
124
const HANABI_COMMON_TEMPLATE_HANDLE: Handle<Shader> =
125
    Handle::weak_from_u128(0x626E7AD34E54487EB7969A90E34CC1ECu128);
126

127
/// Plugin to add systems related to Hanabi.
128
#[derive(Debug, Clone, Copy)]
129
pub struct HanabiPlugin;
130

131
impl HanabiPlugin {
132
    /// Create the `vfx_common.wgsl` shader with proper alignment.
133
    ///
134
    /// This creates a new [`Shader`] from the `vfx_common.wgsl` template file,
135
    /// by applying the given alignment for storage buffers. This produces a
136
    /// shader ready for the specific GPU device associated with that
137
    /// alignment.
138
    pub(crate) fn make_common_shader(min_storage_buffer_offset_alignment: u32) -> Shader {
3✔
139
        let spawner_padding_code =
3✔
140
            GpuSpawnerParams::padding_code(min_storage_buffer_offset_alignment);
3✔
141
        let effect_metadata_padding_code =
3✔
142
            GpuEffectMetadata::padding_code(min_storage_buffer_offset_alignment);
3✔
143
        let render_effect_indirect_size =
3✔
144
            GpuEffectMetadata::aligned_size(min_storage_buffer_offset_alignment);
3✔
145
        let effect_metadata_stride_code =
3✔
146
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
3✔
147
        let common_code = include_str!("render/vfx_common.wgsl")
3✔
148
            .replace("{{SPAWNER_PADDING}}", &spawner_padding_code)
3✔
149
            .replace("{{EFFECT_METADATA_PADDING}}", &effect_metadata_padding_code)
3✔
150
            .replace("{{EFFECT_METADATA_STRIDE}}", &effect_metadata_stride_code);
3✔
151
        Shader::from_wgsl(
152
            common_code,
3✔
153
            std::path::Path::new(file!())
3✔
154
                .parent()
3✔
155
                .unwrap()
3✔
156
                .join(format!(
3✔
157
                    "render/vfx_common_{}.wgsl",
3✔
158
                    min_storage_buffer_offset_alignment
3✔
159
                ))
160
                .to_string_lossy(),
3✔
161
        )
162
    }
163

164
    /// Create the `vfx_indirect.wgsl` shader with proper alignment.
165
    ///
166
    /// This creates a new [`Shader`] from the `vfx_indirect.wgsl` template
167
    /// file, by applying the given alignment for storage buffers. This
168
    /// produces a shader ready for the specific GPU device associated with
169
    /// that alignment.
NEW
170
    pub(crate) fn make_indirect_shader(
×
171
        min_storage_buffer_offset_alignment: u32,
172
        has_events: bool,
173
    ) -> Shader {
NEW
174
        let render_effect_indirect_size =
×
NEW
175
            GpuEffectMetadata::aligned_size(min_storage_buffer_offset_alignment);
×
NEW
176
        let render_effect_indirect_stride_code =
×
NEW
177
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
×
NEW
178
        let indirect_code = include_str!("render/vfx_indirect.wgsl").replace(
×
179
            "{{EFFECT_METADATA_STRIDE}}",
NEW
180
            &render_effect_indirect_stride_code,
×
181
        );
182
        Shader::from_wgsl(
NEW
183
            indirect_code,
×
NEW
184
            std::path::Path::new(file!())
×
NEW
185
                .parent()
×
NEW
186
                .unwrap()
×
NEW
187
                .join(format!(
×
NEW
188
                    "render/vfx_indirect_{}_{}.wgsl",
×
NEW
189
                    min_storage_buffer_offset_alignment,
×
NEW
190
                    if has_events { "events" } else { "noevent" },
×
191
                ))
NEW
192
                .to_string_lossy(),
×
193
        )
194
    }
195
}
196

197
/// A convenient alias for `With<CompiledParticleEffect>`, for use with
198
/// [`bevy_render::view::VisibleEntities`].
199
pub type WithCompiledParticleEffect = With<CompiledParticleEffect>;
200

201
impl Plugin for HanabiPlugin {
202
    fn build(&self, app: &mut App) {
×
203
        // Register asset
204
        app.init_asset::<EffectAsset>()
×
205
            .insert_resource(Random(spawn::new_rng()))
×
206
            .init_resource::<DefaultMesh>()
207
            .init_resource::<ShaderCache>()
208
            .init_resource::<DebugSettings>()
209
            .init_resource::<Time<EffectSimulation>>()
210
            .configure_sets(
211
                PostUpdate,
×
212
                (
213
                    EffectSystems::TickSpawners
×
214
                        // This checks the visibility to skip work, so needs to run after
215
                        // ComputedVisibility was updated.
216
                        .after(VisibilitySystems::VisibilityPropagate),
×
217
                    EffectSystems::CompileEffects,
×
218
                ),
219
            )
220
            .configure_sets(
221
                PreUpdate,
×
222
                EffectSystems::UpdatePropertiesFromAsset.after(bevy::asset::TrackAssets),
×
223
            )
224
            .add_systems(
225
                First,
×
226
                effect_simulation_time_system
×
227
                    .after(time_system)
×
228
                    .in_set(TimeSystem),
×
229
            )
230
            .add_systems(
231
                PostUpdate,
×
232
                (
NEW
233
                    tick_spawners.in_set(EffectSystems::TickSpawners),
×
234
                    compile_effects.in_set(EffectSystems::CompileEffects),
×
235
                    update_properties_from_asset.in_set(EffectSystems::UpdatePropertiesFromAsset),
×
236
                    check_visibility::<WithCompiledParticleEffect>
×
237
                        .in_set(VisibilitySystems::CheckVisibility),
×
238
                ),
239
            );
240

241
        #[cfg(feature = "serde")]
242
        app.init_asset_loader::<EffectAssetLoader>();
×
243

244
        // Register types with reflection
245
        app.register_type::<EffectAsset>()
×
246
            .register_type::<ParticleEffect>()
247
            .register_type::<EffectProperties>()
248
            .register_type::<Spawner>()
249
            .register_type::<Time<EffectSimulation>>();
250
    }
251

252
    fn finish(&self, app: &mut App) {
×
253
        let render_device = app
×
254
            .sub_app(RenderApp)
×
255
            .world()
256
            .resource::<RenderDevice>()
257
            .clone();
258

259
        let adapter_name = app
×
260
            .world()
261
            .get_resource::<RenderAdapterInfo>()
262
            .map(|ai| &ai.name[..])
×
263
            .unwrap_or("<unknown>");
264

265
        // Check device limits
266
        let limits = render_device.limits();
×
267
        if limits.max_bind_groups < 4 {
×
268
            error!("Hanabi requires a GPU device supporting at least 4 bind groups (Limits::max_bind_groups).\n  Current adapter: {}\n  Supported bind groups: {}", adapter_name, limits.max_bind_groups);
×
269
            return;
×
270
        } else {
271
            info!("Initializing Hanabi for GPU adapter {}", adapter_name);
×
272
        }
273

274
        // Insert the properly aligned `vfx_common.wgsl` shader into Assets<Shader>, so
275
        // that the automated Bevy shader processing finds it as an import. This is used
276
        // for init/update/render shaders (but not the indirect one).
277
        {
278
            let common_shader = HanabiPlugin::make_common_shader(
279
                render_device.limits().min_storage_buffer_offset_alignment,
×
280
            );
281
            let mut assets = app.world_mut().resource_mut::<Assets<Shader>>();
×
282
            assets.insert(&HANABI_COMMON_TEMPLATE_HANDLE, common_shader);
×
283
        }
284

285
        // Insert the two variants of the properly aligned `vfx_indirect.wgsl` shaders
286
        // into Assets<Shader>.
NEW
287
        let (
×
NEW
288
            indirect_shader_noevent,
×
NEW
289
            indirect_shader_events,
×
NEW
290
            sort_fill_shader,
×
NEW
291
            sort_shader,
×
NEW
292
            sort_copy_shader,
×
NEW
293
        ) = {
×
NEW
294
            let align = render_device.limits().min_storage_buffer_offset_alignment;
×
NEW
295
            let indirect_shader_noevent = HanabiPlugin::make_indirect_shader(align, false);
×
NEW
296
            let indirect_shader_events = HanabiPlugin::make_indirect_shader(align, true);
×
297
            let sort_fill_shader = Shader::from_wgsl(
NEW
298
                include_str!("render/vfx_sort_fill.wgsl"),
×
NEW
299
                std::path::Path::new(file!())
×
NEW
300
                    .parent()
×
NEW
301
                    .unwrap()
×
NEW
302
                    .join("render/vfx_sort_fill.wgsl")
×
NEW
303
                    .to_string_lossy(),
×
304
            );
305
            let sort_shader = Shader::from_wgsl(
NEW
306
                include_str!("render/vfx_sort.wgsl"),
×
NEW
307
                std::path::Path::new(file!())
×
NEW
308
                    .parent()
×
NEW
309
                    .unwrap()
×
NEW
310
                    .join("render/vfx_sort.wgsl")
×
NEW
311
                    .to_string_lossy(),
×
312
            );
313
            let sort_copy_shader = Shader::from_wgsl(
NEW
314
                include_str!("render/vfx_sort_copy.wgsl"),
×
NEW
315
                std::path::Path::new(file!())
×
NEW
316
                    .parent()
×
NEW
317
                    .unwrap()
×
NEW
318
                    .join("render/vfx_sort_copy.wgsl")
×
NEW
319
                    .to_string_lossy(),
×
320
            );
321

NEW
322
            let mut assets = app.world_mut().resource_mut::<Assets<Shader>>();
×
NEW
323
            let indirect_shader_noevent = assets.add(indirect_shader_noevent);
×
NEW
324
            let indirect_shader_events = assets.add(indirect_shader_events);
×
NEW
325
            let sort_fill_shader = assets.add(sort_fill_shader);
×
NEW
326
            let sort_shader = assets.add(sort_shader);
×
NEW
327
            let sort_copy_shader = assets.add(sort_copy_shader);
×
328

329
            (
NEW
330
                indirect_shader_noevent,
×
NEW
331
                indirect_shader_events,
×
NEW
332
                sort_fill_shader,
×
NEW
333
                sort_shader,
×
NEW
334
                sort_copy_shader,
×
335
            )
336
        };
337

338
        let effects_meta = EffectsMeta::new(
NEW
339
            render_device.clone(),
×
NEW
340
            indirect_shader_noevent,
×
NEW
341
            indirect_shader_events,
×
342
        );
343

344
        let effect_cache = EffectCache::new(render_device.clone());
×
NEW
345
        let property_cache = PropertyCache::new(render_device.clone());
×
NEW
346
        let event_cache = EventCache::new(render_device);
×
347

UNCOV
348
        let render_app = app.sub_app_mut(RenderApp);
×
349
        let sort_bind_groups = SortBindGroups::new(
NEW
350
            render_app.world_mut(),
×
NEW
351
            sort_fill_shader,
×
NEW
352
            sort_shader,
×
NEW
353
            sort_copy_shader,
×
354
        );
355

356
        // Register the custom render pipeline
357
        render_app
×
358
            .insert_resource(effects_meta)
×
359
            .insert_resource(effect_cache)
×
360
            .insert_resource(property_cache)
×
NEW
361
            .insert_resource(event_cache)
×
362
            .init_resource::<RenderDebugSettings>()
363
            .init_resource::<EffectBindGroups>()
364
            .init_resource::<PropertyBindGroups>()
NEW
365
            .insert_resource(sort_bind_groups)
×
366
            .init_resource::<UtilsPipeline>()
367
            .init_resource::<GpuBufferOperationQueue>()
368
            .init_resource::<DispatchIndirectPipeline>()
369
            .init_resource::<SpecializedComputePipelines<DispatchIndirectPipeline>>()
370
            .init_resource::<ParticlesInitPipeline>()
371
            .init_resource::<SpecializedComputePipelines<ParticlesInitPipeline>>()
372
            .init_resource::<ParticlesInitPipeline>()
373
            .init_resource::<SpecializedComputePipelines<ParticlesInitPipeline>>()
374
            .init_resource::<ParticlesUpdatePipeline>()
375
            .init_resource::<SpecializedComputePipelines<ParticlesUpdatePipeline>>()
376
            .init_resource::<ParticlesRenderPipeline>()
377
            .init_resource::<SpecializedRenderPipelines<ParticlesRenderPipeline>>()
378
            .init_resource::<ExtractedEffects>()
379
            .init_resource::<EffectAssetEvents>()
380
            .init_resource::<SimParams>()
381
            .init_resource::<SortedEffectBatches>()
382
            .configure_sets(
383
                Render,
×
384
                (
385
                    EffectSystems::PrepareEffectAssets.in_set(RenderSet::PrepareAssets),
×
386
                    EffectSystems::QueueEffects.in_set(RenderSet::Queue),
×
NEW
387
                    EffectSystems::PrepareEffectGpuResources.in_set(RenderSet::PrepareResources),
×
388
                    EffectSystems::PrepareBindGroups.in_set(RenderSet::PrepareBindGroups),
×
389
                ),
390
            )
391
            .edit_schedule(ExtractSchedule, |schedule| {
×
392
                schedule.add_systems((extract_effects, extract_effect_events));
×
393
            })
394
            .add_systems(
395
                Render,
396
                (
397
                    (
398
                        clear_all_effects,
399
                        add_effects,
400
                        resolve_parents,
401
                        fixup_parents,
402
                        prepare_effects,
403
                        batch_effects,
404
                    )
405
                        .chain()
406
                        .after(prepare_assets::<bevy::render::mesh::RenderMesh>)
407
                        .in_set(EffectSystems::PrepareEffectAssets)
408
                        // Ensure we run after Bevy prepared the render Mesh
409
                        .after(allocate_and_free_meshes),
410
                    queue_effects
411
                        .in_set(EffectSystems::QueueEffects)
412
                        .after(batch_effects),
413
                    prepare_gpu_resources
414
                        .in_set(EffectSystems::PrepareEffectGpuResources)
415
                        .after(prepare_view_uniforms)
416
                        .before(prepare_bind_groups),
417
                    prepare_property_buffers
418
                        .in_set(EffectSystems::PrepareEffectGpuResources)
419
                        .after(add_effects)
420
                        .before(prepare_bind_groups),
421
                    prepare_bind_groups
422
                        .in_set(EffectSystems::PrepareBindGroups)
423
                        .after(queue_effects)
424
                        .after(prepare_assets::<GpuImage>),
425
                ),
426
            );
427
        render_app.world_mut().add_observer(on_remove_cached_effect);
428
        render_app
429
            .world_mut()
430
            .add_observer(on_remove_cached_properties);
431

432
        // Register the draw function for drawing the particles. This will be called
433
        // during the main 2D/3D pass, at the Transparent2d/3d phase, after the
434
        // opaque objects have been rendered (or, rather, commands for those
435
        // have been recorded).
436
        #[cfg(feature = "2d")]
437
        {
438
            let draw_particles = DrawEffects::new(render_app.world_mut());
439
            render_app
440
                .world()
441
                .get_resource::<DrawFunctions<Transparent2d>>()
442
                .unwrap()
443
                .write()
444
                .add(draw_particles);
445
        }
446
        #[cfg(feature = "3d")]
447
        {
448
            let draw_particles = DrawEffects::new(render_app.world_mut());
449
            render_app
450
                .world()
451
                .get_resource::<DrawFunctions<Transparent3d>>()
452
                .unwrap()
453
                .write()
454
                .add(draw_particles);
455

456
            let draw_particles = DrawEffects::new(render_app.world_mut());
457
            render_app
458
                .world()
459
                .get_resource::<DrawFunctions<AlphaMask3d>>()
460
                .unwrap()
461
                .write()
462
                .add(draw_particles);
463

464
            let draw_particles = DrawEffects::new(render_app.world_mut());
465
            render_app
466
                .world()
467
                .get_resource::<DrawFunctions<Opaque3d>>()
468
                .unwrap()
469
                .write()
470
                .add(draw_particles);
471
        }
472

473
        // Add the simulation sub-graph. This render graph runs once per frame no matter
474
        // how many cameras/views are active (view-independent).
475
        let mut simulate_graph = RenderGraph::default();
476
        let simulate_node = VfxSimulateNode::new(render_app.world_mut());
477
        simulate_graph.add_node(simulate_graph::node::HanabiSimulateNode, simulate_node);
478
        let mut graph = render_app
479
            .world_mut()
480
            .get_resource_mut::<RenderGraph>()
481
            .unwrap();
482
        graph.add_sub_graph(simulate_graph::HanabiSimulateGraph, simulate_graph);
483

484
        // Add the simulation driver node which executes the simulation sub-graph. It
485
        // runs before the camera driver, since rendering needs to access simulated
486
        // particles.
487
        graph.add_node(main_graph::node::HanabiDriverNode, VfxSimulateDriverNode {});
488
        graph.add_node_edge(
489
            main_graph::node::HanabiDriverNode,
490
            bevy::render::graph::CameraDriverLabel,
491
        );
492
    }
493
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc