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

djeedai / bevy_hanabi / 14348872530

09 Apr 2025 04:18AM UTC coverage: 39.38% (-0.7%) from 40.116%
14348872530

Pull #444

github

web-flow
Merge 5f906b79b into 027286d2a
Pull Request #444: Make the number of particles to emit in a GPU event an expression instead of a constant.

0 of 3 new or added lines in 1 file covered. (0.0%)

139 existing lines in 8 files now uncovered.

3022 of 7674 relevant lines covered (39.38%)

17.34 hits per line

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

62.09
/src/modifier/mod.rs
1
//! Building blocks to create a visual effect.
2
//!
3
//! A **modifier** is a building block used to define the behavior of an effect.
4
//! Particles effects are composed of multiple modifiers, which put together and
5
//! configured produce the desired visual effect. Each modifier changes a
6
//! specific part of the behavior of an effect. Modifiers are grouped in three
7
//! categories:
8
//!
9
//! - **Init modifiers** influence the initializing of particles when they
10
//!   spawn. They typically configure the initial position and/or velocity of
11
//!   particles. Init modifiers implement the [`Modifier`] trait, and act on the
12
//!   [`ModifierContext::Init`] modifier context.
13
//! - **Update modifiers** influence the particle update loop each frame. For
14
//!   example, an update modifier can apply a gravity force to all particles.
15
//!   Update modifiers implement the [`Modifier`] trait, and act on the
16
//!   [`ModifierContext::Update`] modifier context.
17
//! - **Render modifiers** influence the rendering of each particle. They can
18
//!   change the particle's color, or orient it to face the camera. Render
19
//!   modifiers implement the [`RenderModifier`] trait, and act on the
20
//!   [`ModifierContext::Render`] modifier context.
21
//!
22
//! A single modifier can be part of multiple categories. For example, the
23
//! [`SetAttributeModifier`] can be used either to initialize a particle's
24
//! attribute on spawning, or to assign a value to that attribute each frame
25
//! during simulation (update).
26
//!
27
//! # Modifiers and expressions
28
//!
29
//! Modifiers are configured by assigning values to their field(s). Some values
30
//! are compile-time constants, like which attribute a [`SetAttributeModifier`]
31
//! mutates. Others however can take the form of
32
//! [expressions](crate::graph::expr), which form a mini language designed to
33
//! emit shader code and provide extended customization. For example, a 3D
34
//! vector position can be assigned to a [property](crate::properties) and
35
//! mutated each frame, giving CPU-side control over the behavior of the GPU
36
//! particle effect. See [expressions](crate::graph::expr) for more details.
37
//!
38
//! # Limitations
39
//!
40
//! At this time, serialization and deserialization of modifiers is not
41
//! supported on Wasm. This means assets authored and saved on a non-Wasm target
42
//! cannot be read back into an application running on Wasm.
43

44
use std::{
45
    collections::hash_map::DefaultHasher,
46
    hash::{Hash, Hasher},
47
};
48

49
use bevy::{
50
    asset::Handle,
51
    image::Image,
52
    math::{UVec2, Vec3, Vec4},
53
    reflect::Reflect,
54
    utils::HashMap,
55
};
56
use bitflags::bitflags;
57
use serde::{Deserialize, Serialize};
58

59
pub mod accel;
60
pub mod attr;
61
pub mod force;
62
pub mod kill;
63
pub mod output;
64
pub mod position;
65
pub mod velocity;
66

67
pub use accel::*;
68
pub use attr::*;
69
pub use force::*;
70
pub use kill::*;
71
pub use output::*;
72
pub use position::*;
73
pub use velocity::*;
74

75
use crate::{
76
    Attribute, EvalContext, ExprError, ExprHandle, Gradient, Module, ParticleLayout,
77
    PropertyLayout, TextureLayout,
78
};
79

80
/// The dimension of a shape to consider.
81
///
82
/// The exact meaning depends on the context where this enum is used.
83
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
84
pub enum ShapeDimension {
85
    /// Consider the surface of the shape only.
86
    #[default]
87
    Surface,
88
    /// Consider the entire shape volume.
89
    Volume,
90
}
91

92
/// Calculate a function ID by hashing the given value representative of the
93
/// function.
94
pub(crate) fn calc_func_id<T: Hash>(value: &T) -> u64 {
24✔
95
    let mut hasher = DefaultHasher::default();
24✔
96
    value.hash(&mut hasher);
24✔
97
    hasher.finish()
24✔
98
}
99

100
bitflags! {
101
    /// Context a modifier applies to.
102
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
103
    pub struct ModifierContext : u8 {
104
        /// Particle initializing on spawning.
105
        ///
106
        /// Modifiers in the init context are executed for each newly spawned
107
        /// particle, to initialize that particle.
108
        const Init = 0b001;
109
        /// Particle simulation (update).
110
        ///
111
        /// Modifiers in the update context are executed each frame to simulate
112
        /// the particle behavior.
113
        const Update = 0b010;
114
        /// Particle rendering.
115
        ///
116
        /// Modifiers in the render context are executed for each view (camera)
117
        /// where a particle is visible, each frame.
118
        const Render = 0b100;
119
    }
120
}
121

122
impl std::fmt::Display for ModifierContext {
123
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8✔
124
        let mut s = if self.contains(ModifierContext::Init) {
16✔
125
            "Init".to_string()
4✔
126
        } else {
127
            String::new()
4✔
128
        };
129
        if self.contains(ModifierContext::Update) {
8✔
130
            if s.is_empty() {
6✔
131
                s = "Update".to_string();
2✔
132
            } else {
133
                s += " | Update";
2✔
134
            }
135
        }
136
        if self.contains(ModifierContext::Render) {
8✔
137
            if s.is_empty() {
5✔
138
                s = "Render".to_string();
1✔
139
            } else {
140
                s += " | Render";
3✔
141
            }
142
        }
143
        if s.is_empty() {
9✔
144
            s = "None".to_string();
1✔
145
        }
146
        write!(f, "{}", s)
8✔
147
    }
148
}
149

150
/// Trait describing a modifier customizing an effect pipeline.
151
#[cfg_attr(feature = "serde", typetag::serde)]
152
pub trait Modifier: Reflect + Send + Sync + 'static {
153
    /// Get the context this modifier applies to.
154
    fn context(&self) -> ModifierContext;
155

156
    /// Try to cast this modifier to a [`RenderModifier`].
157
    fn as_render(&self) -> Option<&dyn RenderModifier> {
×
158
        None
×
159
    }
160

161
    /// Try to cast this modifier to a [`RenderModifier`].
162
    fn as_render_mut(&mut self) -> Option<&mut dyn RenderModifier> {
×
163
        None
×
164
    }
165

166
    /// Get the list of attributes required for this modifier to be used.
167
    fn attributes(&self) -> &[Attribute];
168

169
    /// Clone self.
170
    fn boxed_clone(&self) -> BoxedModifier;
171

172
    /// Apply the modifier to generate code.
173
    fn apply(&self, module: &mut Module, context: &mut ShaderWriter) -> Result<(), ExprError>;
174
}
175

176
/// Boxed version of [`Modifier`].
177
pub type BoxedModifier = Box<dyn Modifier>;
178

179
impl Clone for BoxedModifier {
180
    fn clone(&self) -> Self {
×
181
        self.boxed_clone()
×
182
    }
183
}
184

185
/// Shader code writer.
186
///
187
/// Writer utility to generate shader code. The writer works in a defined
188
/// context, for a given [`ModifierContext`] and a particular effect setup
189
/// ([`ParticleLayout`] and [`PropertyLayout`]).
190
#[derive(Debug, PartialEq)]
191
pub struct ShaderWriter<'a> {
192
    /// Main shader compute code emitted.
193
    ///
194
    /// This is the WGSL code emitted into the target [`ModifierContext`]. The
195
    /// context dictates what variables are available (this is currently
196
    /// implicit and requires knownledge of the target context; there's little
197
    /// validation that the emitted code is valid).
198
    pub main_code: String,
199
    /// Extra functions emitted at shader top level.
200
    ///
201
    /// This contains optional WGSL code emitted at shader top level. This
202
    /// generally contains functions called from `main_code`.
203
    pub extra_code: String,
204
    /// Layout of properties for the current effect.
205
    pub property_layout: &'a PropertyLayout,
206
    /// Layout of attributes of a particle for the current effect.
207
    pub particle_layout: &'a ParticleLayout,
208
    /// Modifier context the writer is being used from.
209
    modifier_context: ModifierContext,
210
    /// Counter for unique variable names.
211
    var_counter: u32,
212
    /// Cache of evaluated expressions.
213
    expr_cache: HashMap<ExprHandle, String>,
214
    /// Is the attribute struct a pointer?
215
    is_attribute_pointer: bool,
216
    /// Is the shader using GPU spawn events?
217
    emits_gpu_spawn_events: Option<bool>,
218
}
219

220
impl<'a> ShaderWriter<'a> {
221
    /// Create a new init context.
222
    pub fn new(
105✔
223
        modifier_context: ModifierContext,
224
        property_layout: &'a PropertyLayout,
225
        particle_layout: &'a ParticleLayout,
226
    ) -> Self {
227
        Self {
228
            main_code: String::new(),
105✔
229
            extra_code: String::new(),
105✔
230
            property_layout,
231
            particle_layout,
232
            modifier_context,
233
            var_counter: 0,
234
            expr_cache: Default::default(),
105✔
235
            is_attribute_pointer: false,
236
            emits_gpu_spawn_events: None,
237
        }
238
    }
239

240
    /// Mark the attribute struct as being available through a pointer.
241
    pub fn with_attribute_pointer(mut self) -> Self {
18✔
242
        self.is_attribute_pointer = true;
18✔
243
        self
18✔
244
    }
245

246
    /// Mark the shader as emitting GPU spawn events.
247
    ///
248
    /// This is used by the [`EmitSpawnEventModifier`] to declare that the
249
    /// current effect emits GPU spawn events, and therefore needs an event
250
    /// buffer to be allocated and the appropriate compute work to be executed
251
    /// to fill that buffer with events.
252
    ///
253
    /// # Returns
254
    ///
255
    /// Returns an error if another modifier previously called this function
256
    /// with a different value of `use_events`. Calling this function with the
257
    /// same value is a no-op, and doesn't generate any error.
258
    pub fn set_emits_gpu_spawn_events(&mut self, use_events: bool) -> Result<(), ExprError> {
×
259
        if let Some(was_using_events) = self.emits_gpu_spawn_events {
×
260
            if was_using_events == use_events {
×
261
                Ok(())
×
262
            } else {
263
                Err(ExprError::GraphEvalError(
×
264
                    "Conflicting use of GPU spawn events.".to_string(),
×
265
                ))
266
                // FIXME - Should probably be a validation error instead...
267
                // Err(ShaderGenerateError::Validate(
268
                //     "Conflicting use of GPU spawn events.".to_string(),
269
                // ))
270
            }
271
        } else {
272
            self.emits_gpu_spawn_events = Some(use_events);
×
273
            Ok(())
×
274
        }
275
    }
276

277
    /// Check whether this shader emits GPU spawn events.
278
    ///
279
    /// If no modifier called [`set_emits_gpu_spawn_events()`], this returns
280
    /// `None`. Otherwise this returns `Some(value)` where `value` was the value
281
    /// passed to [`set_emits_gpu_spawn_events()`].
282
    ///
283
    /// [`set_emits_gpu_spawn_events()`]: crate::ShaderWriter::set_emits_gpu_spawn_events
284
    pub fn emits_gpu_spawn_events(&self) -> Option<bool> {
12✔
285
        self.emits_gpu_spawn_events
12✔
286
    }
287
}
288

289
impl EvalContext for ShaderWriter<'_> {
290
    fn modifier_context(&self) -> ModifierContext {
14✔
291
        self.modifier_context
14✔
292
    }
293

294
    fn property_layout(&self) -> &PropertyLayout {
2✔
295
        self.property_layout
2✔
296
    }
297

298
    fn particle_layout(&self) -> &ParticleLayout {
6✔
299
        self.particle_layout
6✔
300
    }
301

302
    fn eval(&mut self, module: &Module, handle: ExprHandle) -> Result<String, ExprError> {
316✔
303
        // On cache hit, don't re-evaluate the expression to prevent any duplicate
304
        // side-effect.
305
        if let Some(s) = self.expr_cache.get(&handle) {
397✔
306
            Ok(s.clone())
307
        } else {
308
            module.try_get(handle)?.eval(module, self).inspect(|s| {
705✔
309
                self.expr_cache.insert(handle, s.clone());
235✔
310
            })
311
        }
312
    }
313

314
    fn make_local_var(&mut self) -> String {
123✔
315
        let index = self.var_counter;
123✔
316
        self.var_counter += 1;
123✔
317
        format!("var{}", index)
123✔
318
    }
319

320
    fn push_stmt(&mut self, stmt: &str) {
21✔
321
        self.main_code += stmt;
21✔
322
        self.main_code += "\n";
21✔
323
    }
324

325
    fn make_fn(
17✔
326
        &mut self,
327
        func_name: &str,
328
        args: &str,
329
        module: &mut Module,
330
        f: &mut dyn FnMut(&mut Module, &mut dyn EvalContext) -> Result<String, ExprError>,
331
    ) -> Result<(), ExprError> {
332
        // Generate a temporary context for the function content itself
333
        // FIXME - Dynamic with_attribute_pointer()!
334
        let mut ctx = ShaderWriter::new(
335
            self.modifier_context,
17✔
336
            self.property_layout,
17✔
337
            self.particle_layout,
17✔
338
        )
339
        .with_attribute_pointer();
340

341
        // Evaluate the function content
342
        let body = f(module, &mut ctx)?;
34✔
343

344
        // Append any extra
345
        self.extra_code += &ctx.extra_code;
346

347
        // Append the function itself
348
        self.extra_code += &format!(
349
            r##"fn {0}({1}) {{
350
{2}{3}}}"##,
351
            func_name, args, ctx.main_code, body
352
        );
353

354
        Ok(())
355
    }
356

357
    fn is_attribute_pointer(&self) -> bool {
26✔
358
        self.is_attribute_pointer
26✔
359
    }
360
}
361

362
/// Particle rendering shader code generation context.
363
#[derive(Debug, PartialEq)]
364
pub struct RenderContext<'a> {
365
    /// Layout of properties for the current effect.
366
    pub property_layout: &'a PropertyLayout,
367
    /// Layout of attributes of a particle for the current effect.
368
    pub particle_layout: &'a ParticleLayout,
369
    /// Main particle rendering code for the vertex shader.
370
    pub vertex_code: String,
371
    /// Main particle rendering code for the fragment shader.
372
    pub fragment_code: String,
373
    /// Extra functions emitted at top level, which `vertex_code` and
374
    /// `fragment_code` can call.
375
    pub render_extra: String,
376
    /// Texture layout.
377
    pub texture_layout: &'a TextureLayout,
378
    /// Effect textures.
379
    pub textures: Vec<Handle<Image>>,
380
    /// Flipbook sprite sheet grid size, if any.
381
    pub sprite_grid_size: Option<UVec2>,
382
    /// Color gradients.
383
    pub gradients: HashMap<u64, Gradient<Vec4>>,
384
    /// Size gradients.
385
    pub size_gradients: HashMap<u64, Gradient<Vec3>>,
386
    /// The particle needs UV coordinates to sample one or more texture(s).
387
    pub needs_uv: bool,
388
    /// The particle needs normals for lighting effects.
389
    pub needs_normal: bool,
390
    /// Counter for unique variable names.
391
    var_counter: u32,
392
    /// Cache of evaluated expressions.
393
    expr_cache: HashMap<ExprHandle, String>,
394
    /// Is the attriubute struct a pointer?
395
    is_attribute_pointer: bool,
396
}
397

398
impl<'a> RenderContext<'a> {
399
    /// Create a new update context.
400
    pub fn new(
24✔
401
        property_layout: &'a PropertyLayout,
402
        particle_layout: &'a ParticleLayout,
403
        texture_layout: &'a TextureLayout,
404
    ) -> Self {
405
        Self {
406
            property_layout,
407
            particle_layout,
408
            vertex_code: String::new(),
24✔
409
            fragment_code: String::new(),
24✔
410
            render_extra: String::new(),
24✔
411
            texture_layout,
412
            textures: vec![],
24✔
413
            sprite_grid_size: None,
414
            gradients: HashMap::new(),
24✔
415
            size_gradients: HashMap::new(),
24✔
416
            needs_uv: false,
417
            needs_normal: false,
418
            var_counter: 0,
419
            expr_cache: Default::default(),
24✔
420
            is_attribute_pointer: false,
421
        }
422
    }
423

424
    /// Mark the rendering shader as needing UVs.
425
    pub fn set_needs_uv(&mut self) {
3✔
426
        self.needs_uv = true;
3✔
427
    }
428

429
    /// Mark the rendering shader as needing normals.
430
    pub fn set_needs_normal(&mut self) {
×
431
        self.needs_normal = true;
×
432
    }
433

434
    /// Add a color gradient.
435
    ///
436
    /// # Returns
437
    ///
438
    /// Returns the unique name of the gradient, to be used as function name in
439
    /// the shader code.
440
    fn add_color_gradient(&mut self, gradient: Gradient<Vec4>) -> String {
3✔
441
        let func_id = calc_func_id(&gradient);
3✔
442
        self.gradients.insert(func_id, gradient);
3✔
443
        let func_name = format!("color_gradient_{0:016X}", func_id);
3✔
444
        func_name
3✔
445
    }
446

447
    /// Add a size gradient.
448
    ///
449
    /// # Returns
450
    ///
451
    /// Returns the unique name of the gradient, to be used as function name in
452
    /// the shader code.
453
    fn add_size_gradient(&mut self, gradient: Gradient<Vec3>) -> String {
3✔
454
        let func_id = calc_func_id(&gradient);
3✔
455
        self.size_gradients.insert(func_id, gradient);
3✔
456
        let func_name = format!("size_gradient_{0:016X}", func_id);
3✔
457
        func_name
3✔
458
    }
459

460
    /// Mark the attribute struct as being available through a pointer.
461
    pub fn with_attribute_pointer(mut self) -> Self {
×
462
        self.is_attribute_pointer = true;
×
463
        self
×
464
    }
465
}
466

467
impl EvalContext for RenderContext<'_> {
468
    fn modifier_context(&self) -> ModifierContext {
2✔
469
        ModifierContext::Render
2✔
470
    }
471

472
    fn property_layout(&self) -> &PropertyLayout {
×
473
        self.property_layout
×
474
    }
475

476
    fn particle_layout(&self) -> &ParticleLayout {
×
477
        self.particle_layout
×
478
    }
479

480
    fn eval(&mut self, module: &Module, handle: ExprHandle) -> Result<String, ExprError> {
4✔
481
        // On cache hit, don't re-evaluate the expression to prevent any duplicate
482
        // side-effect.
483
        if let Some(s) = self.expr_cache.get(&handle) {
5✔
484
            Ok(s.clone())
485
        } else {
486
            module.try_get(handle)?.eval(module, self).inspect(|s| {
9✔
487
                self.expr_cache.insert(handle, s.clone());
3✔
488
            })
489
        }
490
    }
491

492
    fn make_local_var(&mut self) -> String {
1✔
493
        let index = self.var_counter;
1✔
494
        self.var_counter += 1;
1✔
495
        format!("var{}", index)
1✔
496
    }
497

498
    fn push_stmt(&mut self, stmt: &str) {
1✔
499
        // FIXME - vertex vs. fragment code, can't differentiate here currently
500
        self.vertex_code += stmt;
1✔
501
        self.vertex_code += "\n";
1✔
502
    }
503

504
    fn make_fn(
×
505
        &mut self,
506
        func_name: &str,
507
        args: &str,
508
        module: &mut Module,
509
        f: &mut dyn FnMut(&mut Module, &mut dyn EvalContext) -> Result<String, ExprError>,
510
    ) -> Result<(), ExprError> {
511
        // Generate a temporary context for the function content itself
512
        // FIXME - Dynamic with_attribute_pointer()!
513
        let texture_layout = module.texture_layout();
×
514
        let mut ctx =
×
515
            RenderContext::new(self.property_layout, self.particle_layout, &texture_layout)
×
516
                .with_attribute_pointer();
517

518
        // Evaluate the function content
519
        let body = f(module, &mut ctx)?;
×
520

521
        // Append any extra
522
        self.render_extra += &ctx.render_extra;
523

524
        // Append the function itself
525
        self.render_extra += &format!(
526
            r##"fn {0}({1}) {{
527
            {2};
528
        }}
529
        "##,
530
            func_name, args, body
531
        );
532

533
        Ok(())
534
    }
535

536
    fn is_attribute_pointer(&self) -> bool {
×
537
        self.is_attribute_pointer
×
538
    }
539
}
540

541
/// Trait to customize the rendering of alive particles each frame.
542
#[cfg_attr(feature = "serde", typetag::serde)]
543
pub trait RenderModifier: Modifier {
544
    /// Apply the rendering code.
545
    fn apply_render(
546
        &self,
547
        module: &mut Module,
548
        context: &mut RenderContext,
549
    ) -> Result<(), ExprError>;
550

551
    /// Clone into boxed self.
552
    fn boxed_render_clone(&self) -> Box<dyn RenderModifier>;
553

554
    /// Upcast to [`Modifier`] trait.
555
    fn as_modifier(&self) -> &dyn Modifier;
556
}
557

558
impl Clone for Box<dyn RenderModifier> {
559
    fn clone(&self) -> Self {
×
560
        self.boxed_render_clone()
×
561
    }
562
}
563

564
/// Macro to implement the [`Modifier`] trait for a render modifier.
565
macro_rules! impl_mod_render {
566
    ($t:ty, $attrs:expr) => {
567
        #[cfg_attr(feature = "serde", typetag::serde)]
×
568
        impl $crate::Modifier for $t {
569
            fn context(&self) -> $crate::ModifierContext {
9✔
570
                $crate::ModifierContext::Render
9✔
571
            }
572

573
            fn as_render(&self) -> Option<&dyn $crate::RenderModifier> {
2✔
574
                Some(self)
2✔
575
            }
576

577
            fn as_render_mut(&mut self) -> Option<&mut dyn $crate::RenderModifier> {
2✔
578
                Some(self)
2✔
579
            }
580

581
            fn attributes(&self) -> &[$crate::Attribute] {
3✔
582
                $attrs
3✔
583
            }
584

585
            fn boxed_clone(&self) -> $crate::BoxedModifier {
3✔
586
                Box::new(self.clone())
3✔
587
            }
588

589
            fn apply(
×
590
                &self,
×
591
                _module: &mut Module,
×
592
                context: &mut ShaderWriter,
×
593
            ) -> Result<(), ExprError> {
×
594
                Err(ExprError::InvalidModifierContext(
×
595
                    context.modifier_context(),
×
596
                    ModifierContext::Render,
×
597
                ))
598
            }
599
        }
600
    };
601
}
602

603
pub(crate) use impl_mod_render;
604

605
/// Condition to emit a GPU spawn event.
606
///
607
/// Determines when a GPU spawn event is emitted by a parent effect. See
608
/// the [`EffectParent`] component for details about the parent-child effect
609
/// relationship and its use.
610
///
611
/// [`EffectParent`]: crate::EffectParent
612
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
613
pub enum EventEmitCondition {
614
    /// Always emit events each time the particle is updated, each simulation
615
    /// frame.
616
    Always,
617
    /// Only emit events if the particle died during this frame update.
618
    OnDie,
619
}
620

621
/// Emit GPU spawn events to spawn new particle(s) in a child effect.
622
///
623
/// This update modifier is used to spawn new particles into a child effect
624
/// instance based on a condition applied to particles of the current effect
625
/// instance. The most common use case is to spawn one or more child particles
626
/// into a child effect when a particle in this effect dies; this is achieved
627
/// with [`EventEmitCondition::OnDie`].
628
///
629
/// An effect instance with this modifier will emit GPU spawn events. Those
630
/// events are read by all child effects (those effects with an [`EffectParent`]
631
/// component pointing at the current effect instance). GPU spawn events are
632
/// stored internally in a GPU buffer; they're **unrelated** to Bevy ECS events.
633
///
634
/// [`EffectParent`]: crate::EffectParent
635
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
636
pub struct EmitSpawnEventModifier {
637
    /// Emit condition for the GPU spawn events.
638
    pub condition: EventEmitCondition,
639
    /// A `u32` expression specifying the number of particles to spawn if the
640
    /// emit condition is met.
641
    pub count: ExprHandle,
642
    /// Index of the event channel / child the events are emitted into.
643
    ///
644
    /// GPU spawn events emitted by this parent event are associated with a
645
    /// single event channel. When the N-th child effect of a parent effect
646
    /// consumes those event, it implicitly reads events from channel #N. In
647
    /// general if a parent has a single child, use `0` here.
648
    pub child_index: u32,
649
}
650

651
impl EmitSpawnEventModifier {
652
    fn eval(
×
653
        &self,
654
        module: &mut Module,
655
        context: &mut dyn EvalContext,
656
    ) -> Result<String, ExprError> {
657
        // FIXME - mixing (ex-)channel and event buffer index; this should be automated
658
        let channel_index = self.child_index;
×
659
        // TODO - validate GPU spawn events are in use in the eval context...
NEW
660
        let count = context.eval(module, self.count)?;
×
661
        let cond = match self.condition {
662
            EventEmitCondition::Always => format!(
×
663
                "if (is_alive) {{ append_spawn_events_{channel_index}(particle_index, {}); }}",
NEW
664
                count
×
665
            ),
666
            EventEmitCondition::OnDie => format!(
×
667
                "if (was_alive && !is_alive) {{ append_spawn_events_{channel_index}(particle_index, {}); }}",
NEW
668
                count
×
669
            ),
670
        };
671
        Ok(cond)
672
    }
673
}
674

675
#[cfg_attr(feature = "serde", typetag::serde)]
676
impl Modifier for EmitSpawnEventModifier {
677
    fn context(&self) -> ModifierContext {
×
678
        ModifierContext::Update
×
679
    }
680

681
    fn attributes(&self) -> &[Attribute] {
×
682
        &[]
×
683
    }
684

685
    fn boxed_clone(&self) -> BoxedModifier {
×
686
        Box::new(*self)
×
687
    }
688

689
    fn apply(&self, module: &mut Module, context: &mut ShaderWriter) -> Result<(), ExprError> {
×
690
        let code = self.eval(module, context)?;
×
691
        context.main_code += &code;
692
        context.set_emits_gpu_spawn_events(true)?;
×
693
        Ok(())
×
694
    }
695
}
696

697
#[cfg(test)]
698
mod tests {
699
    use bevy::prelude::*;
700
    use naga::front::wgsl::Frontend;
701

702
    use super::*;
703
    use crate::{BuiltInOperator, ExprWriter, ScalarType};
704

705
    fn make_test_modifier() -> SetPositionSphereModifier {
706
        // We use a dummy module here because we don't care about the values and won't
707
        // evaluate the modifier.
708
        let mut m = Module::default();
709
        SetPositionSphereModifier {
710
            center: m.lit(Vec3::ZERO),
711
            radius: m.lit(1.),
712
            dimension: ShapeDimension::Surface,
713
        }
714
    }
715

716
    #[test]
717
    fn modifier_context_display() {
718
        assert_eq!("None", format!("{}", ModifierContext::empty()));
719
        assert_eq!("Init", format!("{}", ModifierContext::Init));
720
        assert_eq!("Update", format!("{}", ModifierContext::Update));
721
        assert_eq!("Render", format!("{}", ModifierContext::Render));
722
        assert_eq!(
723
            "Init | Update",
724
            format!("{}", ModifierContext::Init | ModifierContext::Update)
725
        );
726
        assert_eq!(
727
            "Update | Render",
728
            format!("{}", ModifierContext::Update | ModifierContext::Render)
729
        );
730
        assert_eq!(
731
            "Init | Render",
732
            format!("{}", ModifierContext::Init | ModifierContext::Render)
733
        );
734
        assert_eq!(
735
            "Init | Update | Render",
736
            format!("{}", ModifierContext::all())
737
        );
738
    }
739

740
    #[test]
741
    fn reflect() {
742
        let m = make_test_modifier();
743

744
        // Reflect
745
        let reflect: &dyn Reflect = m.as_reflect();
746
        assert!(reflect.is::<SetPositionSphereModifier>());
747
        let m_reflect = reflect.downcast_ref::<SetPositionSphereModifier>().unwrap();
748
        assert_eq!(*m_reflect, m);
749
    }
750

751
    #[cfg(feature = "serde")]
752
    #[test]
753
    fn serde() {
754
        let m = make_test_modifier();
755
        let bm: BoxedModifier = Box::new(m);
756

757
        // Ser
758
        let s = ron::to_string(&bm).unwrap();
759
        println!("modifier: {:?}", s);
760

761
        // De
762
        let m_serde: BoxedModifier = ron::from_str(&s).unwrap();
763

764
        let rm: &dyn Reflect = m.as_reflect();
765
        let rm_serde: &dyn Reflect = m_serde.as_reflect();
766
        assert_eq!(
767
            rm.get_represented_type_info().unwrap().type_id(),
768
            rm_serde.get_represented_type_info().unwrap().type_id()
769
        );
770

771
        assert!(rm_serde.is::<SetPositionSphereModifier>());
772
        let rm_reflect = rm_serde
773
            .downcast_ref::<SetPositionSphereModifier>()
774
            .unwrap();
775
        assert_eq!(*rm_reflect, m);
776
    }
777

778
    #[test]
779
    fn validate_init() {
780
        let mut module = Module::default();
781
        let center = module.lit(Vec3::ZERO);
782
        let axis = module.lit(Vec3::Y);
783
        let radius = module.lit(1.);
784
        let modifiers: &[&dyn Modifier] = &[
785
            &SetPositionCircleModifier {
786
                center,
787
                axis,
788
                radius,
789
                dimension: ShapeDimension::Volume,
790
            },
791
            &SetPositionSphereModifier {
792
                center,
793
                radius,
794
                dimension: ShapeDimension::Volume,
795
            },
796
            &SetPositionCone3dModifier {
797
                base_radius: radius,
798
                top_radius: radius,
799
                height: radius,
800
                dimension: ShapeDimension::Volume,
801
            },
802
            &SetVelocityCircleModifier {
803
                center,
804
                axis,
805
                speed: radius,
806
            },
807
            &SetVelocitySphereModifier {
808
                center,
809
                speed: radius,
810
            },
811
            &SetVelocityTangentModifier {
812
                origin: center,
813
                axis,
814
                speed: radius,
815
            },
816
        ];
817
        for &modifier in modifiers.iter() {
818
            assert!(modifier.context().contains(ModifierContext::Init));
819
            let property_layout = PropertyLayout::default();
820
            let particle_layout = ParticleLayout::default();
821
            let mut context =
822
                ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
823
            assert!(modifier.apply(&mut module, &mut context).is_ok());
824
            let main_code = context.main_code;
825
            let extra_code = context.extra_code;
826

827
            let mut particle_layout = ParticleLayout::new();
828
            for &attr in modifier.attributes() {
829
                particle_layout = particle_layout.append(attr);
830
            }
831
            let particle_layout = particle_layout.build();
832
            let attributes_code = particle_layout.generate_code();
833

834
            let code = format!(
835
                r##"fn frand() -> f32 {{
836
    return 0.0;
837
}}
838

839
const tau: f32 = 6.283185307179586476925286766559;
840

841
struct Particle {{
842
    {attributes_code}
843
}};
844

845
{extra_code}
846

847
@compute @workgroup_size(64)
848
fn main() {{
849
    var particle = Particle();
850
    var transform: mat4x4<f32> = mat4x4<f32>();
851
{main_code}
852
}}"##
853
            );
854
            // println!("code: {:?}", code);
855

856
            let mut frontend = Frontend::new();
857
            let res = frontend.parse(&code);
858
            if let Err(err) = &res {
859
                println!(
860
                    "Modifier: {:?}",
861
                    modifier.get_represented_type_info().unwrap().type_path()
862
                );
863
                println!("Code: {:?}", code);
864
                println!("Err: {:?}", err);
865
            }
866
            assert!(res.is_ok());
867
        }
868
    }
869

870
    #[test]
871
    fn validate_update() {
872
        let writer = ExprWriter::new();
873
        let origin = writer.lit(Vec3::ZERO).expr();
874
        let center = origin;
875
        let axis = origin;
876
        let y_axis = writer.lit(Vec3::Y).expr();
877
        let one = writer.lit(1.).expr();
878
        let radius = one;
879
        let modifiers: &[&dyn Modifier] = &[
880
            &AccelModifier::new(origin),
881
            &RadialAccelModifier::new(origin, one),
882
            &TangentAccelModifier::new(origin, y_axis, one),
883
            &ConformToSphereModifier::new(origin, one, one, one, one),
884
            &LinearDragModifier::new(writer.lit(3.5).expr()),
885
            &KillAabbModifier::new(writer.lit(Vec3::ZERO).expr(), writer.lit(Vec3::ONE).expr()),
886
            &SetPositionCircleModifier {
887
                center,
888
                axis,
889
                radius,
890
                dimension: ShapeDimension::Volume,
891
            },
892
            &SetPositionSphereModifier {
893
                center,
894
                radius,
895
                dimension: ShapeDimension::Volume,
896
            },
897
            &SetPositionCone3dModifier {
898
                base_radius: radius,
899
                top_radius: radius,
900
                height: radius,
901
                dimension: ShapeDimension::Volume,
902
            },
903
            &SetVelocityCircleModifier {
904
                center,
905
                axis,
906
                speed: radius,
907
            },
908
            &SetVelocitySphereModifier {
909
                center,
910
                speed: radius,
911
            },
912
            &SetVelocityTangentModifier {
913
                origin: center,
914
                axis,
915
                speed: radius,
916
            },
917
        ];
918
        let mut module = writer.finish();
919
        for &modifier in modifiers.iter() {
920
            assert!(modifier.context().contains(ModifierContext::Update));
921
            let property_layout = PropertyLayout::default();
922
            let particle_layout = ParticleLayout::default();
923
            let mut context =
924
                ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
925
            assert!(modifier.apply(&mut module, &mut context).is_ok());
926
            let update_code = context.main_code;
927
            let update_extra = context.extra_code;
928

929
            let mut particle_layout = ParticleLayout::new();
930
            for &attr in modifier.attributes() {
931
                particle_layout = particle_layout.append(attr);
932
            }
933
            let particle_layout = particle_layout.build();
934
            let attributes_code = particle_layout.generate_code();
935

936
            let code = format!(
937
                r##"fn frand() -> f32 {{
938
    return 0.0;
939
}}
940

941
const tau: f32 = 6.283185307179586476925286766559;
942

943
struct Particle {{
944
    {attributes_code}
945
}};
946

947
struct ParticleBuffer {{
948
    particles: array<Particle>,
949
}};
950

951
struct SimParams {{
952
    delta_time: f32,
953
    time: f32,
954
    virtual_delta_time: f32,
955
    virtual_time: f32,
956
    real_delta_time: f32,
957
    real_time: f32,
958
}};
959

960
struct Spawner {{
961
    transform: mat3x4<f32>, // transposed (row-major)
962
    spawn: atomic<i32>,
963
    seed: u32,
964
    count_unused: u32,
965
    effect_index: u32,
966
}};
967

968
fn proj(u: vec3<f32>, v: vec3<f32>) -> vec3<f32> {{
969
    return dot(v, u) / dot(u,u) * u;
970
}}
971

972
{update_extra}
973

974
@group(0) @binding(0) var<uniform> sim_params : SimParams;
975
@group(1) @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer;
976
@group(2) @binding(0) var<storage, read_write> spawner : Spawner; // NOTE - same group as init
977

978
@compute @workgroup_size(64)
979
fn main() {{
980
    var particle: Particle = particle_buffer.particles[0];
981
    var transform: mat4x4<f32> = mat4x4<f32>();
982
    var is_alive = true;
983
{update_code}
984
}}"##
985
            );
986

987
            let mut frontend = Frontend::new();
988
            let res = frontend.parse(&code);
989
            if let Err(err) = &res {
990
                println!(
991
                    "Modifier: {:?}",
992
                    modifier.get_represented_type_info().unwrap().type_path()
993
                );
994
                println!("Code: {:?}", code);
995
                println!("Err: {:?}", err);
996
            }
997
            assert!(res.is_ok());
998
        }
999
    }
1000

1001
    #[test]
1002
    fn validate_render() {
1003
        let mut base_module = Module::default();
1004
        let slot_zero = base_module.lit(0u32);
1005
        let modifiers: &[&dyn RenderModifier] = &[
1006
            &ParticleTextureModifier::new(slot_zero),
1007
            &ColorOverLifetimeModifier::default(),
1008
            &SizeOverLifetimeModifier::default(),
1009
            &OrientModifier::new(OrientMode::ParallelCameraDepthPlane),
1010
            &OrientModifier::new(OrientMode::FaceCameraPosition),
1011
            &OrientModifier::new(OrientMode::AlongVelocity),
1012
        ];
1013
        for &modifier in modifiers.iter() {
1014
            let mut module = base_module.clone();
1015
            let property_layout = PropertyLayout::default();
1016
            let particle_layout = ParticleLayout::default();
1017
            let texture_layout = module.texture_layout();
1018
            let mut context =
1019
                RenderContext::new(&property_layout, &particle_layout, &texture_layout);
1020
            modifier
1021
                .apply_render(&mut module, &mut context)
1022
                .expect("Failed to apply modifier to render context.");
1023
            let vertex_code = context.vertex_code;
1024
            let fragment_code = context.fragment_code;
1025
            let render_extra = context.render_extra;
1026

1027
            let mut particle_layout = ParticleLayout::new();
1028
            for &attr in modifier.attributes() {
1029
                particle_layout = particle_layout.append(attr);
1030
            }
1031
            let particle_layout = particle_layout.build();
1032
            let attributes_code = particle_layout.generate_code();
1033

1034
            let code = format!(
1035
                r##"
1036
struct ColorGrading {{
1037
    balance: mat3x3<f32>,
1038
    saturation: vec3<f32>,
1039
    contrast: vec3<f32>,
1040
    gamma: vec3<f32>,
1041
    gain: vec3<f32>,
1042
    lift: vec3<f32>,
1043
    midtone_range: vec2<f32>,
1044
    exposure: f32,
1045
    hue: f32,
1046
    post_saturation: f32,
1047
}}
1048

1049
struct View {{
1050
    clip_from_world: mat4x4<f32>,
1051
    unjittered_clip_from_world: mat4x4<f32>,
1052
    world_from_clip: mat4x4<f32>,
1053
    world_from_view: mat4x4<f32>,
1054
    view_from_world: mat4x4<f32>,
1055
    clip_from_view: mat4x4<f32>,
1056
    view_from_clip: mat4x4<f32>,
1057
    world_position: vec3<f32>,
1058
    exposure: f32,
1059
    // viewport(x_origin, y_origin, width, height)
1060
    viewport: vec4<f32>,
1061
    frustum: array<vec4<f32>, 6>,
1062
    color_grading: ColorGrading,
1063
    mip_bias: f32,
1064
}}
1065

1066
fn frand() -> f32 {{ return 0.0; }}
1067
fn get_camera_position_effect_space() -> vec3<f32> {{ return vec3<f32>(); }}
1068
fn get_camera_rotation_effect_space() -> mat3x3<f32> {{ return mat3x3<f32>(); }}
1069

1070
const tau: f32 = 6.283185307179586476925286766559;
1071

1072
struct Particle {{
1073
    {attributes_code}
1074
}};
1075

1076
struct VertexOutput {{
1077
    @builtin(position) position: vec4<f32>,
1078
    @location(0) color: vec4<f32>,
1079
}};
1080

1081
@group(0) @binding(0) var<uniform> view: View;
1082

1083
{render_extra}
1084

1085
@compute @workgroup_size(64)
1086
fn main() {{
1087
    var particle = Particle();
1088
    var position = vec3<f32>(0.0, 0.0, 0.0);
1089
    var velocity = vec3<f32>(0.0, 0.0, 0.0);
1090
    var size = vec3<f32>(1.0, 1.0, 1.0);
1091
    var axis_x = vec3<f32>(1.0, 0.0, 0.0);
1092
    var axis_y = vec3<f32>(0.0, 1.0, 0.0);
1093
    var axis_z = vec3<f32>(0.0, 0.0, 1.0);
1094
    var color = vec4<f32>(1.0, 1.0, 1.0, 1.0);
1095
{vertex_code}
1096
    var out: VertexOutput;
1097
    return out;
1098
}}
1099

1100

1101
@fragment
1102
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {{
1103
    var color = vec4<f32>(0.0);
1104
    var uv = vec2<f32>(0.0);
1105
{fragment_code}
1106
    return vec4<f32>(1.0);
1107
}}"##
1108
            );
1109

1110
            let mut frontend = Frontend::new();
1111
            let res = frontend.parse(&code);
1112
            if let Err(err) = &res {
1113
                println!(
1114
                    "Modifier: {:?}",
1115
                    modifier.get_represented_type_info().unwrap().type_path()
1116
                );
1117
                println!("Code: {:?}", code);
1118
                println!("Err: {:?}", err);
1119
            }
1120
            assert!(res.is_ok());
1121
        }
1122
    }
1123

1124
    #[test]
1125
    fn eval_cached() {
1126
        let mut module = Module::default();
1127
        let property_layout = PropertyLayout::default();
1128
        let particle_layout = ParticleLayout::default();
1129
        let x = module.builtin(BuiltInOperator::Rand(ScalarType::Float.into()));
1130
        let texture_layout = module.texture_layout();
1131
        let init: &mut dyn EvalContext =
1132
            &mut ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
1133
        let update: &mut dyn EvalContext =
1134
            &mut ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
1135
        let render: &mut dyn EvalContext =
1136
            &mut RenderContext::new(&property_layout, &particle_layout, &texture_layout);
1137
        for ctx in [init, update, render] {
1138
            // First evaluation is cached inside a local variable 'var0'
1139
            let s = ctx.eval(&module, x).unwrap();
1140
            assert_eq!(s, "var0");
1141
            // Second evaluation return the same variable
1142
            let s2 = ctx.eval(&module, x).unwrap();
1143
            assert_eq!(s2, s);
1144
        }
1145
    }
1146
}
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