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

djeedai / bevy_hanabi / 14652003207

24 Apr 2025 09:26PM UTC coverage: 39.884% (-0.1%) from 40.029%
14652003207

push

github

web-flow
Add support for Bevy 0.16 (#463)

5 of 73 new or added lines in 6 files covered. (6.85%)

2 existing lines in 2 files now uncovered.

3034 of 7607 relevant lines covered (39.88%)

17.63 hits per line

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

30.43
/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
    asset::weak_handle,
7
    prelude::*,
8
    render::{
9
        extract_component::ExtractComponentPlugin,
10
        render_asset::prepare_assets,
11
        render_graph::RenderGraph,
12
        render_phase::DrawFunctions,
13
        render_resource::{SpecializedComputePipelines, SpecializedRenderPipelines},
14
        renderer::{RenderAdapterInfo, RenderDevice},
15
        texture::GpuImage,
16
        view::{prepare_view_uniforms, visibility::VisibilitySystems},
17
        Render, RenderApp, RenderSet,
18
    },
19
    time::{time_system, TimeSystem},
20
};
21

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

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

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

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

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

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

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

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

98
pub mod main_graph {
99
    pub mod node {
100
        use bevy::render::render_graph::RenderLabel;
101

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

108
pub mod simulate_graph {
109
    use bevy::render::render_graph::RenderSubGraph;
110

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

115
    pub mod node {
116
        use bevy::render::render_graph::RenderLabel;
117

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

125
const HANABI_COMMON_TEMPLATE_HANDLE: Handle<Shader> =
126
    weak_handle!("626E7AD3-4E54-487E-B796-9A90E34CC1EC");
127

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

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

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

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

237
        #[cfg(feature = "serde")]
238
        app.init_asset_loader::<EffectAssetLoader>();
×
239

240
        // Register types with reflection
241
        app.register_type::<EffectAsset>()
×
242
            .register_type::<ParticleEffect>()
243
            .register_type::<EffectProperties>()
244
            .register_type::<SpawnerSettings>()
245
            .register_type::<Time<EffectSimulation>>();
246
    }
247

248
    fn finish(&self, app: &mut App) {
×
249
        let render_device = app
×
250
            .sub_app(RenderApp)
×
251
            .world()
252
            .resource::<RenderDevice>()
253
            .clone();
254

255
        let adapter_name = app
×
256
            .world()
257
            .get_resource::<RenderAdapterInfo>()
258
            .map(|ai| &ai.name[..])
×
259
            .unwrap_or("<unknown>");
260

261
        // Check device limits
262
        let limits = render_device.limits();
×
263
        if limits.max_bind_groups < 4 {
×
264
            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);
×
265
            return;
×
266
        } else {
267
            info!("Initializing Hanabi for GPU adapter {}", adapter_name);
×
268
        }
269

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

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

318
            let mut assets = app.world_mut().resource_mut::<Assets<Shader>>();
319
            let indirect_shader_noevent = assets.add(indirect_shader_noevent);
320
            let indirect_shader_events = assets.add(indirect_shader_events);
321
            let sort_fill_shader = assets.add(sort_fill_shader);
322
            let sort_shader = assets.add(sort_shader);
323
            let sort_copy_shader = assets.add(sort_copy_shader);
324

325
            (
326
                indirect_shader_noevent,
327
                indirect_shader_events,
328
                sort_fill_shader,
329
                sort_shader,
330
                sort_copy_shader,
331
            )
332
        };
333

334
        let effects_meta = EffectsMeta::new(
335
            render_device.clone(),
336
            indirect_shader_noevent,
337
            indirect_shader_events,
338
        );
339

340
        let effect_cache = EffectCache::new(render_device.clone());
341
        let property_cache = PropertyCache::new(render_device.clone());
342
        let event_cache = EventCache::new(render_device);
343

344
        let render_app = app.sub_app_mut(RenderApp);
345
        let sort_bind_groups = SortBindGroups::new(
346
            render_app.world_mut(),
347
            sort_fill_shader,
348
            sort_shader,
349
            sort_copy_shader,
350
        );
351

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

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

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

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

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

485
        // Add the simulation driver node which executes the simulation sub-graph. It
486
        // runs before the camera driver, since rendering needs to access simulated
487
        // particles.
488
        graph.add_node(main_graph::node::HanabiDriverNode, VfxSimulateDriverNode {});
489
        graph.add_node_edge(
490
            main_graph::node::HanabiDriverNode,
491
            bevy::render::graph::CameraDriverLabel,
492
        );
493
    }
494
}
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