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

djeedai / bevy_hanabi / 11543837292

27 Oct 2024 09:10PM UTC coverage: 57.849% (-1.2%) from 59.035%
11543837292

Pull #387

github

web-flow
Merge a72c10537 into 75f07d778
Pull Request #387: Unify the clone modifier and spawners, and fix races.

114 of 621 new or added lines in 7 files covered. (18.36%)

23 existing lines in 5 files now uncovered.

3534 of 6109 relevant lines covered (57.85%)

23.02 hits per line

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

63.82
/src/modifier/mod.rs
1
//! Building blocks to create a visual effect.
2
//!
3
//! A **modifier** is a building block used to create effects. Particles effects
4
//! are composed of multiple modifiers, which put together and configured
5
//! produce the desired visual effect. Each modifier changes a specific part of
6
//! the behavior of an effect. Modifiers are grouped in three categories:
7
//!
8
//! - **Init modifiers** influence the initializing of particles when they
9
//!   spawn. They typically configure the initial position and/or velocity of
10
//!   particles. Init modifiers implement the [`Modifier`] trait.
11
//! - **Update modifiers** influence the particle update loop each frame. For
12
//!   example, an update modifier can apply a gravity force to all particles.
13
//!   Update modifiers implement the [`Modifier`] trait.
14
//! - **Render modifiers** influence the rendering of each particle. They can
15
//!   change the particle's color, or orient it to face the camera. Render
16
//!   modifiers implement the [`RenderModifier`] trait.
17
//!
18
//! A single modifier can be part of multiple categories. For example, the
19
//! [`SetAttributeModifier`] can be used either to initialize a particle's
20
//! attribute on spawning, or to assign a value to that attribute each frame
21
//! during simulation (update).
22

23
use std::{
24
    collections::hash_map::DefaultHasher,
25
    hash::{Hash, Hasher},
26
};
27

28
use bevy::{
29
    asset::Handle,
30
    math::{UVec2, Vec2, Vec4},
31
    reflect::Reflect,
32
    render::texture::Image,
33
    utils::HashMap,
34
};
35
use bitflags::bitflags;
36
use serde::{Deserialize, Serialize};
37

38
pub mod accel;
39
pub mod attr;
40
pub mod force;
41
pub mod kill;
42
pub mod output;
43
pub mod position;
44
pub mod velocity;
45

46
pub use accel::*;
47
pub use attr::*;
48
pub use force::*;
49
pub use kill::*;
50
pub use output::*;
51
pub use position::*;
52
pub use velocity::*;
53

54
use crate::{
55
    Attribute, EvalContext, ExprError, ExprHandle, Gradient, Module, ParticleLayout,
56
    PropertyLayout, TextureLayout,
57
};
58

59
/// The dimension of a shape to consider.
60
///
61
/// The exact meaning depends on the context where this enum is used.
62
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
63
pub enum ShapeDimension {
64
    /// Consider the surface of the shape only.
65
    #[default]
66
    Surface,
67
    /// Consider the entire shape volume.
68
    Volume,
69
}
70

71
/// Calculate a function ID by hashing the given value representative of the
72
/// function.
73
pub(crate) fn calc_func_id<T: Hash>(value: &T) -> u64 {
24✔
74
    let mut hasher = DefaultHasher::default();
24✔
75
    value.hash(&mut hasher);
24✔
76
    hasher.finish()
24✔
77
}
78

79
bitflags! {
80
    /// Context a modifier applies to.
81
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
82
    pub struct ModifierContext : u8 {
83
        /// Particle initializing on spawning.
84
        ///
85
        /// Modifiers in the init context are executed for each newly spawned
86
        /// particle, to initialize that particle.
87
        const Init = 0b001;
88
        /// Particle simulation (update).
89
        ///
90
        /// Modifiers in the update context are executed each frame to simulate
91
        /// the particle behavior.
92
        const Update = 0b010;
93
        /// Particle rendering.
94
        ///
95
        /// Modifiers in the render context are executed for each view (camera)
96
        /// where a particle is visible, each frame.
97
        const Render = 0b100;
98
    }
99
}
100

101
impl std::fmt::Display for ModifierContext {
102
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8✔
103
        let mut s = if self.contains(ModifierContext::Init) {
16✔
104
            "Init".to_string()
4✔
105
        } else {
106
            String::new()
4✔
107
        };
108
        if self.contains(ModifierContext::Update) {
8✔
109
            if s.is_empty() {
6✔
110
                s = "Update".to_string();
2✔
111
            } else {
112
                s += " | Update";
2✔
113
            }
114
        }
115
        if self.contains(ModifierContext::Render) {
8✔
116
            if s.is_empty() {
5✔
117
                s = "Render".to_string();
1✔
118
            } else {
119
                s += " | Render";
3✔
120
            }
121
        }
122
        if s.is_empty() {
9✔
123
            s = "None".to_string();
1✔
124
        }
125
        write!(f, "{}", s)
8✔
126
    }
127
}
128

129
/// Trait describing a modifier customizing an effect pipeline.
130
#[cfg_attr(feature = "serde", typetag::serde)]
131
pub trait Modifier: Reflect + Send + Sync + 'static {
132
    /// Get the context this modifier applies to.
133
    fn context(&self) -> ModifierContext;
134

135
    /// Try to cast this modifier to a [`RenderModifier`].
136
    fn as_render(&self) -> Option<&dyn RenderModifier> {
×
137
        None
×
138
    }
139

140
    /// Try to cast this modifier to a [`RenderModifier`].
141
    fn as_render_mut(&mut self) -> Option<&mut dyn RenderModifier> {
×
142
        None
×
143
    }
144

145
    /// Get the list of dependent attributes required for this modifier to be
146
    /// used.
147
    fn attributes(&self) -> &[Attribute];
148

149
    /// Clone self.
150
    fn boxed_clone(&self) -> BoxedModifier;
151

152
    /// Apply the modifier to generate code.
153
    fn apply(&self, module: &mut Module, context: &mut ShaderWriter) -> Result<(), ExprError>;
154
}
155

156
/// Boxed version of [`Modifier`].
157
pub type BoxedModifier = Box<dyn Modifier>;
158

159
impl Clone for BoxedModifier {
160
    fn clone(&self) -> Self {
×
161
        self.boxed_clone()
×
162
    }
163
}
164

165
/// A bitfield that describes which particle groups a modifier affects.
166
///
167
/// Bit N will be set if the modifier in question affects particle group N.
168
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
169
#[repr(transparent)]
170
pub struct ParticleGroupSet(pub u32);
171

172
impl ParticleGroupSet {
173
    /// Returns a new [`ParticleGroupSet`] that affects all particle groups.
174
    #[inline]
175
    pub fn all() -> ParticleGroupSet {
20✔
176
        ParticleGroupSet(!0)
20✔
177
    }
178

179
    /// Returns a new [`ParticleGroupSet`] that affects no particle groups.
180
    ///
181
    /// Typically you'll want to add some groups to the resulting set with the
182
    /// [`ParticleGroupSet::with_group`] method.
183
    #[inline]
UNCOV
184
    pub fn none() -> ParticleGroupSet {
×
UNCOV
185
        ParticleGroupSet(0)
×
186
    }
187

188
    /// Returns a new set with the given particle group added.
189
    #[inline]
UNCOV
190
    pub fn with_group(mut self, group_index: u32) -> ParticleGroupSet {
×
UNCOV
191
        self.0 |= 1 << group_index;
×
UNCOV
192
        self
×
193
    }
194

195
    /// Returns a new [`ParticleGroupSet`] affecting a single group.
196
    #[inline]
UNCOV
197
    pub fn single(group_index: u32) -> ParticleGroupSet {
×
UNCOV
198
        ParticleGroupSet::none().with_group(group_index)
×
199
    }
200

201
    /// Returns true if this set contains the group with the given index.
202
    #[inline]
203
    pub fn contains(&self, group_index: u32) -> bool {
6✔
204
        (self.0 & (1 << group_index)) != 0
6✔
205
    }
206
}
207

208
/// A [`Modifier`] that affects to one or more groups.
209
#[derive(Clone)]
210
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
211
pub struct GroupedModifier {
212
    /// The modifier.
213
    pub modifier: BoxedModifier,
214
    /// The set of groups that this modifier affects.
215
    pub groups: ParticleGroupSet,
216
}
217

218
impl GroupedModifier {
219
    /// If this modifier describes a [`RenderModifier`], returns an immutable
220
    /// reference to it.
221
    #[inline]
222
    pub fn as_render(&self) -> Option<&dyn RenderModifier> {
×
223
        self.modifier.as_render()
×
224
    }
225
}
226

227
/// Shader code writer.
228
///
229
/// Writer utility to generate shader code. The writer works in a defined
230
/// context, for a given [`ModifierContext`] and a particular effect setup
231
/// ([`ParticleLayout`] and [`PropertyLayout`]).
232
#[derive(Debug, PartialEq)]
233
pub struct ShaderWriter<'a> {
234
    /// Main shader compute code emitted.
235
    ///
236
    /// This is the WGSL code emitted into the target [`ModifierContext`]. The
237
    /// context dictates what variables are available (this is currently
238
    /// implicit and requires knownledge of the target context; there's little
239
    /// validation that the emitted code is valid).
240
    pub main_code: String,
241
    /// Extra functions emitted at shader top level.
242
    ///
243
    /// This contains optional WGSL code emitted at shader top level. This
244
    /// generally contains functions called from `main_code`.
245
    pub extra_code: String,
246
    /// Layout of properties for the current effect.
247
    pub property_layout: &'a PropertyLayout,
248
    /// Layout of attributes of a particle for the current effect.
249
    pub particle_layout: &'a ParticleLayout,
250
    /// Modifier context the writer is being used from.
251
    modifier_context: ModifierContext,
252
    /// Counter for unique variable names.
253
    var_counter: u32,
254
    /// Cache of evaluated expressions.
255
    expr_cache: HashMap<ExprHandle, String>,
256
    /// Is the attribute struct a pointer?
257
    is_attribute_pointer: bool,
258
}
259

260
impl<'a> ShaderWriter<'a> {
261
    /// Create a new init context.
262
    pub fn new(
104✔
263
        modifier_context: ModifierContext,
264
        property_layout: &'a PropertyLayout,
265
        particle_layout: &'a ParticleLayout,
266
    ) -> Self {
267
        Self {
268
            main_code: String::new(),
104✔
269
            extra_code: String::new(),
104✔
270
            property_layout,
271
            particle_layout,
272
            modifier_context,
273
            var_counter: 0,
274
            expr_cache: Default::default(),
104✔
275
            is_attribute_pointer: false,
276
        }
277
    }
278

279
    /// Mark the attribute struct as being available through a pointer.
280
    pub fn with_attribute_pointer(mut self) -> Self {
18✔
281
        self.is_attribute_pointer = true;
18✔
282
        self
18✔
283
    }
284
}
285

286
impl<'a> EvalContext for ShaderWriter<'a> {
287
    fn modifier_context(&self) -> ModifierContext {
14✔
288
        self.modifier_context
14✔
289
    }
290

291
    fn property_layout(&self) -> &PropertyLayout {
2✔
292
        self.property_layout
2✔
293
    }
294

295
    fn particle_layout(&self) -> &ParticleLayout {
6✔
296
        self.particle_layout
6✔
297
    }
298

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

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

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

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

338
        // Evaluate the function content
339
        let body = f(module, &mut ctx)?;
34✔
340

341
        // Append any extra
342
        self.extra_code += &ctx.extra_code;
×
343

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

351
        Ok(())
×
352
    }
353

354
    fn is_attribute_pointer(&self) -> bool {
26✔
355
        self.is_attribute_pointer
26✔
356
    }
357
}
358

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

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

418
    /// Mark the rendering shader as needing UVs.
419
    fn set_needs_uv(&mut self) {
3✔
420
        self.needs_uv = true;
3✔
421
    }
422

423
    /// Add a color gradient.
424
    ///
425
    /// # Returns
426
    ///
427
    /// Returns the unique name of the gradient, to be used as function name in
428
    /// the shader code.
429
    fn add_color_gradient(&mut self, gradient: Gradient<Vec4>) -> String {
3✔
430
        let func_id = calc_func_id(&gradient);
3✔
431
        self.gradients.insert(func_id, gradient);
3✔
432
        let func_name = format!("color_gradient_{0:016X}", func_id);
3✔
433
        func_name
3✔
434
    }
435

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

449
    /// Mark the attribute struct as being available through a pointer.
450
    pub fn with_attribute_pointer(mut self) -> Self {
×
451
        self.is_attribute_pointer = true;
×
452
        self
×
453
    }
454
}
455

456
impl<'a> EvalContext for RenderContext<'a> {
457
    fn modifier_context(&self) -> ModifierContext {
2✔
458
        ModifierContext::Render
2✔
459
    }
460

461
    fn property_layout(&self) -> &PropertyLayout {
×
462
        self.property_layout
×
463
    }
464

465
    fn particle_layout(&self) -> &ParticleLayout {
×
466
        self.particle_layout
×
467
    }
468

469
    fn eval(&mut self, module: &Module, handle: ExprHandle) -> Result<String, ExprError> {
4✔
470
        // On cache hit, don't re-evaluate the expression to prevent any duplicate
471
        // side-effect.
472
        if let Some(s) = self.expr_cache.get(&handle) {
5✔
473
            Ok(s.clone())
×
474
        } else {
475
            module.try_get(handle)?.eval(module, self).inspect(|s| {
9✔
476
                self.expr_cache.insert(handle, s.clone());
3✔
477
            })
478
        }
479
    }
480

481
    fn make_local_var(&mut self) -> String {
1✔
482
        let index = self.var_counter;
1✔
483
        self.var_counter += 1;
1✔
484
        format!("var{}", index)
1✔
485
    }
486

487
    fn push_stmt(&mut self, stmt: &str) {
1✔
488
        // FIXME - vertex vs. fragment code, can't differentiate here currently
489
        self.vertex_code += stmt;
1✔
490
        self.vertex_code += "\n";
1✔
491
    }
492

493
    fn make_fn(
×
494
        &mut self,
495
        func_name: &str,
496
        args: &str,
497
        module: &mut Module,
498
        f: &mut dyn FnMut(&mut Module, &mut dyn EvalContext) -> Result<String, ExprError>,
499
    ) -> Result<(), ExprError> {
500
        // Generate a temporary context for the function content itself
501
        // FIXME - Dynamic with_attribute_pointer()!
502
        let texture_layout = module.texture_layout();
×
503
        let mut ctx =
×
504
            RenderContext::new(self.property_layout, self.particle_layout, &texture_layout)
×
505
                .with_attribute_pointer();
506

507
        // Evaluate the function content
508
        let body = f(module, &mut ctx)?;
×
509

510
        // Append any extra
511
        self.render_extra += &ctx.render_extra;
×
512

513
        // Append the function itself
514
        self.render_extra += &format!(
×
515
            r##"fn {0}({1}) {{
×
516
            {2};
×
517
        }}
518
        "##,
×
519
            func_name, args, body
×
520
        );
521

522
        Ok(())
×
523
    }
524

525
    fn is_attribute_pointer(&self) -> bool {
×
526
        self.is_attribute_pointer
×
527
    }
528
}
529

530
/// Trait to customize the rendering of alive particles each frame.
531
#[cfg_attr(feature = "serde", typetag::serde)]
532
pub trait RenderModifier: Modifier {
533
    /// Apply the rendering code.
534
    fn apply_render(
535
        &self,
536
        module: &mut Module,
537
        context: &mut RenderContext,
538
    ) -> Result<(), ExprError>;
539

540
    /// Clone into boxed self.
541
    fn boxed_render_clone(&self) -> Box<dyn RenderModifier>;
542

543
    /// Upcast to [`Modifier`] trait.
544
    fn as_modifier(&self) -> &dyn Modifier;
545
}
546

547
impl Clone for Box<dyn RenderModifier> {
548
    fn clone(&self) -> Self {
×
549
        self.boxed_render_clone()
×
550
    }
551
}
552

553
/// Macro to implement the [`Modifier`] trait for a render modifier.
554
macro_rules! impl_mod_render {
555
    ($t:ty, $attrs:expr) => {
556
        #[cfg_attr(feature = "serde", typetag::serde)]
×
557
        impl $crate::Modifier for $t {
558
            fn context(&self) -> $crate::ModifierContext {
9✔
559
                $crate::ModifierContext::Render
9✔
560
            }
561

562
            fn as_render(&self) -> Option<&dyn $crate::RenderModifier> {
2✔
563
                Some(self)
2✔
564
            }
565

566
            fn as_render_mut(&mut self) -> Option<&mut dyn $crate::RenderModifier> {
2✔
567
                Some(self)
2✔
568
            }
569

570
            fn attributes(&self) -> &[$crate::Attribute] {
3✔
571
                $attrs
3✔
572
            }
573

574
            fn boxed_clone(&self) -> $crate::BoxedModifier {
3✔
575
                Box::new(self.clone())
3✔
576
            }
577

578
            fn apply(
×
579
                &self,
×
580
                _module: &mut Module,
×
581
                context: &mut ShaderWriter,
×
582
            ) -> Result<(), ExprError> {
×
583
                Err(ExprError::InvalidModifierContext(
×
584
                    context.modifier_context(),
×
585
                    ModifierContext::Render,
×
586
                ))
587
            }
588
        }
589
    };
590
}
591

592
pub(crate) use impl_mod_render;
593

594
#[cfg(test)]
595
mod tests {
596
    use bevy::prelude::*;
597
    use naga::front::wgsl::Frontend;
598

599
    use super::*;
600
    use crate::{BuiltInOperator, ExprWriter, ScalarType};
601

602
    fn make_test_modifier() -> SetPositionSphereModifier {
603
        // We use a dummy module here because we don't care about the values and won't
604
        // evaluate the modifier.
605
        let mut m = Module::default();
606
        SetPositionSphereModifier {
607
            center: m.lit(Vec3::ZERO),
608
            radius: m.lit(1.),
609
            dimension: ShapeDimension::Surface,
610
        }
611
    }
612

613
    #[test]
614
    fn modifier_context_display() {
615
        assert_eq!("None", format!("{}", ModifierContext::empty()));
616
        assert_eq!("Init", format!("{}", ModifierContext::Init));
617
        assert_eq!("Update", format!("{}", ModifierContext::Update));
618
        assert_eq!("Render", format!("{}", ModifierContext::Render));
619
        assert_eq!(
620
            "Init | Update",
621
            format!("{}", ModifierContext::Init | ModifierContext::Update)
622
        );
623
        assert_eq!(
624
            "Update | Render",
625
            format!("{}", ModifierContext::Update | ModifierContext::Render)
626
        );
627
        assert_eq!(
628
            "Init | Render",
629
            format!("{}", ModifierContext::Init | ModifierContext::Render)
630
        );
631
        assert_eq!(
632
            "Init | Update | Render",
633
            format!("{}", ModifierContext::all())
634
        );
635
    }
636

637
    #[test]
638
    fn reflect() {
639
        let m = make_test_modifier();
640

641
        // Reflect
642
        let reflect: &dyn Reflect = m.as_reflect();
643
        assert!(reflect.is::<SetPositionSphereModifier>());
644
        let m_reflect = reflect.downcast_ref::<SetPositionSphereModifier>().unwrap();
645
        assert_eq!(*m_reflect, m);
646
    }
647

648
    #[cfg(feature = "serde")]
649
    #[test]
650
    fn serde() {
651
        let m = make_test_modifier();
652
        let bm: BoxedModifier = Box::new(m);
653

654
        // Ser
655
        let s = ron::to_string(&bm).unwrap();
656
        println!("modifier: {:?}", s);
657

658
        // De
659
        let m_serde: BoxedModifier = ron::from_str(&s).unwrap();
660

661
        let rm: &dyn Reflect = m.as_reflect();
662
        let rm_serde: &dyn Reflect = m_serde.as_reflect();
663
        assert_eq!(
664
            rm.get_represented_type_info().unwrap().type_id(),
665
            rm_serde.get_represented_type_info().unwrap().type_id()
666
        );
667

668
        assert!(rm_serde.is::<SetPositionSphereModifier>());
669
        let rm_reflect = rm_serde
670
            .downcast_ref::<SetPositionSphereModifier>()
671
            .unwrap();
672
        assert_eq!(*rm_reflect, m);
673
    }
674

675
    #[test]
676
    fn validate_init() {
677
        let mut module = Module::default();
678
        let center = module.lit(Vec3::ZERO);
679
        let axis = module.lit(Vec3::Y);
680
        let radius = module.lit(1.);
681
        let modifiers: &[&dyn Modifier] = &[
682
            &SetPositionCircleModifier {
683
                center,
684
                axis,
685
                radius,
686
                dimension: ShapeDimension::Volume,
687
            },
688
            &SetPositionSphereModifier {
689
                center,
690
                radius,
691
                dimension: ShapeDimension::Volume,
692
            },
693
            &SetPositionCone3dModifier {
694
                base_radius: radius,
695
                top_radius: radius,
696
                height: radius,
697
                dimension: ShapeDimension::Volume,
698
            },
699
            &SetVelocityCircleModifier {
700
                center,
701
                axis,
702
                speed: radius,
703
            },
704
            &SetVelocitySphereModifier {
705
                center,
706
                speed: radius,
707
            },
708
            &SetVelocityTangentModifier {
709
                origin: center,
710
                axis,
711
                speed: radius,
712
            },
713
        ];
714
        for &modifier in modifiers.iter() {
715
            assert!(modifier.context().contains(ModifierContext::Init));
716
            let property_layout = PropertyLayout::default();
717
            let particle_layout = ParticleLayout::default();
718
            let mut context =
719
                ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
720
            assert!(modifier.apply(&mut module, &mut context).is_ok());
721
            let main_code = context.main_code;
722
            let extra_code = context.extra_code;
723

724
            let mut particle_layout = ParticleLayout::new();
725
            for &attr in modifier.attributes() {
726
                particle_layout = particle_layout.append(attr);
727
            }
728
            let particle_layout = particle_layout.build();
729
            let attributes_code = particle_layout.generate_code();
730

731
            let code = format!(
732
                r##"fn frand() -> f32 {{
733
    return 0.0;
734
}}
735

736
const tau: f32 = 6.283185307179586476925286766559;
737

738
struct Particle {{
739
    {attributes_code}
740
}};
741

742
{extra_code}
743

744
@compute @workgroup_size(64)
745
fn main() {{
746
    var particle = Particle();
747
    var transform: mat4x4<f32> = mat4x4<f32>();
748
{main_code}
749
}}"##
750
            );
751
            // println!("code: {:?}", code);
752

753
            let mut frontend = Frontend::new();
754
            let res = frontend.parse(&code);
755
            if let Err(err) = &res {
756
                println!(
757
                    "Modifier: {:?}",
758
                    modifier.get_represented_type_info().unwrap().type_path()
759
                );
760
                println!("Code: {:?}", code);
761
                println!("Err: {:?}", err);
762
            }
763
            assert!(res.is_ok());
764
        }
765
    }
766

767
    #[test]
768
    fn validate_update() {
769
        let writer = ExprWriter::new();
770
        let origin = writer.lit(Vec3::ZERO).expr();
771
        let center = origin;
772
        let axis = origin;
773
        let y_axis = writer.lit(Vec3::Y).expr();
774
        let one = writer.lit(1.).expr();
775
        let radius = one;
776
        let modifiers: &[&dyn Modifier] = &[
777
            &AccelModifier::new(origin),
778
            &RadialAccelModifier::new(origin, one),
779
            &TangentAccelModifier::new(origin, y_axis, one),
780
            &ConformToSphereModifier::new(origin, one, one, one, one),
781
            &LinearDragModifier::new(writer.lit(3.5).expr()),
782
            &KillAabbModifier::new(writer.lit(Vec3::ZERO).expr(), writer.lit(Vec3::ONE).expr()),
783
            &SetPositionCircleModifier {
784
                center,
785
                axis,
786
                radius,
787
                dimension: ShapeDimension::Volume,
788
            },
789
            &SetPositionSphereModifier {
790
                center,
791
                radius,
792
                dimension: ShapeDimension::Volume,
793
            },
794
            &SetPositionCone3dModifier {
795
                base_radius: radius,
796
                top_radius: radius,
797
                height: radius,
798
                dimension: ShapeDimension::Volume,
799
            },
800
            &SetVelocityCircleModifier {
801
                center,
802
                axis,
803
                speed: radius,
804
            },
805
            &SetVelocitySphereModifier {
806
                center,
807
                speed: radius,
808
            },
809
            &SetVelocityTangentModifier {
810
                origin: center,
811
                axis,
812
                speed: radius,
813
            },
814
        ];
815
        let mut module = writer.finish();
816
        for &modifier in modifiers.iter() {
817
            assert!(modifier.context().contains(ModifierContext::Update));
818
            let property_layout = PropertyLayout::default();
819
            let particle_layout = ParticleLayout::default();
820
            let mut context =
821
                ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
822
            assert!(modifier.apply(&mut module, &mut context).is_ok());
823
            let update_code = context.main_code;
824
            let update_extra = context.extra_code;
825

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

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

838
const tau: f32 = 6.283185307179586476925286766559;
839

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

844
struct ParticleBuffer {{
845
    particles: array<Particle>,
846
}};
847

848
struct SimParams {{
849
    delta_time: f32,
850
    time: f32,
851
    virtual_delta_time: f32,
852
    virtual_time: f32,
853
    real_delta_time: f32,
854
    real_time: f32,
855
}};
856

857
struct Spawner {{
858
    transform: mat3x4<f32>, // transposed (row-major)
859
    spawn: atomic<i32>,
860
    seed: u32,
861
    count_unused: u32,
862
    effect_index: u32,
863
}};
864

865
fn proj(u: vec3<f32>, v: vec3<f32>) -> vec3<f32> {{
866
    return dot(v, u) / dot(u,u) * u;
867
}}
868

869
{update_extra}
870

871
@group(0) @binding(0) var<uniform> sim_params : SimParams;
872
@group(1) @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer;
873
@group(2) @binding(0) var<storage, read_write> spawner : Spawner; // NOTE - same group as init
874

875
@compute @workgroup_size(64)
876
fn main() {{
877
    var particle: Particle = particle_buffer.particles[0];
878
    var transform: mat4x4<f32> = mat4x4<f32>();
879
    var is_alive = true;
880
{update_code}
881
}}"##
882
            );
883

884
            let mut frontend = Frontend::new();
885
            let res = frontend.parse(&code);
886
            if let Err(err) = &res {
887
                println!(
888
                    "Modifier: {:?}",
889
                    modifier.get_represented_type_info().unwrap().type_path()
890
                );
891
                println!("Code: {:?}", code);
892
                println!("Err: {:?}", err);
893
            }
894
            assert!(res.is_ok());
895
        }
896
    }
897

898
    #[test]
899
    fn validate_render() {
900
        let mut base_module = Module::default();
901
        let slot_zero = base_module.lit(0u32);
902
        let modifiers: &[&dyn RenderModifier] = &[
903
            &ParticleTextureModifier::new(slot_zero),
904
            &ColorOverLifetimeModifier::default(),
905
            &SizeOverLifetimeModifier::default(),
906
            &OrientModifier::new(OrientMode::ParallelCameraDepthPlane),
907
            &OrientModifier::new(OrientMode::FaceCameraPosition),
908
            &OrientModifier::new(OrientMode::AlongVelocity),
909
        ];
910
        for &modifier in modifiers.iter() {
911
            let mut module = base_module.clone();
912
            let property_layout = PropertyLayout::default();
913
            let particle_layout = ParticleLayout::default();
914
            let texture_layout = module.texture_layout();
915
            let mut context =
916
                RenderContext::new(&property_layout, &particle_layout, &texture_layout);
917
            modifier
918
                .apply_render(&mut module, &mut context)
919
                .expect("Failed to apply modifier to render context.");
920
            let vertex_code = context.vertex_code;
921
            let fragment_code = context.fragment_code;
922
            let render_extra = context.render_extra;
923

924
            let mut particle_layout = ParticleLayout::new();
925
            for &attr in modifier.attributes() {
926
                particle_layout = particle_layout.append(attr);
927
            }
928
            let particle_layout = particle_layout.build();
929
            let attributes_code = particle_layout.generate_code();
930

931
            let code = format!(
932
                r##"
933
struct ColorGrading {{
934
    balance: mat3x3<f32>,
935
    saturation: vec3<f32>,
936
    contrast: vec3<f32>,
937
    gamma: vec3<f32>,
938
    gain: vec3<f32>,
939
    lift: vec3<f32>,
940
    midtone_range: vec2<f32>,
941
    exposure: f32,
942
    hue: f32,
943
    post_saturation: f32,
944
}}
945

946
struct View {{
947
    clip_from_world: mat4x4<f32>,
948
    unjittered_clip_from_world: mat4x4<f32>,
949
    world_from_clip: mat4x4<f32>,
950
    world_from_view: mat4x4<f32>,
951
    view_from_world: mat4x4<f32>,
952
    clip_from_view: mat4x4<f32>,
953
    view_from_clip: mat4x4<f32>,
954
    world_position: vec3<f32>,
955
    exposure: f32,
956
    // viewport(x_origin, y_origin, width, height)
957
    viewport: vec4<f32>,
958
    frustum: array<vec4<f32>, 6>,
959
    color_grading: ColorGrading,
960
    mip_bias: f32,
961
}}
962

963
fn frand() -> f32 {{ return 0.0; }}
964
fn get_camera_position_effect_space() -> vec3<f32> {{ return vec3<f32>(); }}
965
fn get_camera_rotation_effect_space() -> mat3x3<f32> {{ return mat3x3<f32>(); }}
966

967
const tau: f32 = 6.283185307179586476925286766559;
968

969
struct Particle {{
970
    {attributes_code}
971
}};
972

973
struct VertexOutput {{
974
    @builtin(position) position: vec4<f32>,
975
    @location(0) color: vec4<f32>,
976
}};
977

978
@group(0) @binding(0) var<uniform> view: View;
979

980
{render_extra}
981

982
@compute @workgroup_size(64)
983
fn main() {{
984
    var particle = Particle();
985
    var position = vec3<f32>(0.0, 0.0, 0.0);
986
    var velocity = vec3<f32>(0.0, 0.0, 0.0);
987
    var size = vec2<f32>(1.0, 1.0);
988
    var axis_x = vec3<f32>(1.0, 0.0, 0.0);
989
    var axis_y = vec3<f32>(0.0, 1.0, 0.0);
990
    var axis_z = vec3<f32>(0.0, 0.0, 1.0);
991
    var color = vec4<f32>(1.0, 1.0, 1.0, 1.0);
992
{vertex_code}
993
    var out: VertexOutput;
994
    return out;
995
}}
996

997

998
@fragment
999
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {{
1000
    var color = vec4<f32>(0.0);
1001
    var uv = vec2<f32>(0.0);
1002
{fragment_code}
1003
    return vec4<f32>(1.0);
1004
}}"##
1005
            );
1006

1007
            let mut frontend = Frontend::new();
1008
            let res = frontend.parse(&code);
1009
            if let Err(err) = &res {
1010
                println!(
1011
                    "Modifier: {:?}",
1012
                    modifier.get_represented_type_info().unwrap().type_path()
1013
                );
1014
                println!("Code: {:?}", code);
1015
                println!("Err: {:?}", err);
1016
            }
1017
            assert!(res.is_ok());
1018
        }
1019
    }
1020

1021
    #[test]
1022
    fn eval_cached() {
1023
        let mut module = Module::default();
1024
        let property_layout = PropertyLayout::default();
1025
        let particle_layout = ParticleLayout::default();
1026
        let x = module.builtin(BuiltInOperator::Rand(ScalarType::Float.into()));
1027
        let texture_layout = module.texture_layout();
1028
        let init: &mut dyn EvalContext =
1029
            &mut ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
1030
        let update: &mut dyn EvalContext =
1031
            &mut ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
1032
        let render: &mut dyn EvalContext =
1033
            &mut RenderContext::new(&property_layout, &particle_layout, &texture_layout);
1034
        for ctx in [init, update, render] {
1035
            // First evaluation is cached inside a local variable 'var0'
1036
            let s = ctx.eval(&module, x).unwrap();
1037
            assert_eq!(s, "var0");
1038
            // Second evaluation return the same variable
1039
            let s2 = ctx.eval(&module, x).unwrap();
1040
            assert_eq!(s2, s);
1041
        }
1042
    }
1043
}
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