bevy_hanabi/asset.rs
1#[cfg(feature = "serde")]
2use bevy::asset::{io::Reader, AssetLoader, LoadContext};
3#[cfg(feature = "serde")]
4use bevy::reflect::TypePath;
5use bevy::{
6 asset::{Asset, Assets, Handle},
7 log::trace,
8 math::{Vec2, Vec3},
9 platform::collections::HashSet,
10 prelude::{Component, Entity, FromWorld, Mesh, Plane3d, Resource, World},
11 reflect::Reflect,
12 utils::default,
13};
14use serde::{Deserialize, Serialize};
15#[cfg(feature = "serde")]
16use thiserror::Error;
17use wgpu::{BlendComponent, BlendFactor, BlendOperation, BlendState};
18
19use crate::{
20 modifier::{Modifier, RenderModifier},
21 BoxedModifier, ExprHandle, ModifierContext, Module, ParticleLayout, Property, PropertyLayout,
22 SimulationSpace, SpawnerSettings, TextureLayout,
23};
24
25/// Type of motion integration applied to the particles of a system.
26#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
27pub enum MotionIntegration {
28 /// No motion integration. The [`Attribute::POSITION`] of the particles
29 /// needs to be explicitly assigned by a modifier for the particles to move.
30 ///
31 /// [`Attribute::POSITION`]: crate::Attribute::POSITION
32 None,
33
34 /// Apply Euler motion integration each simulation update before all
35 /// modifiers are applied.
36 ///
37 /// Not to be confused with Bevy's `PreUpdate` phase. Here "update" refers
38 /// to the particle update on the GPU via a compute shader.
39 PreUpdate,
40
41 /// Apply Euler motion integration each simulation update after all
42 /// modifiers are applied. This is the default.
43 ///
44 /// Not to be confused with Bevy's `PostUpdate` phase. Here "update" refers
45 /// to the particle update on the GPU via a compute shader.
46 #[default]
47 PostUpdate,
48}
49
50/// Simulation condition for an effect.
51#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
52pub enum SimulationCondition {
53 /// Simulate the effect only when visible.
54 ///
55 /// The visibility is determined by the [`InheritedVisibility`] and the
56 /// [`ViewVisibility`] components, if present. The effect is assumed to be
57 /// visible if those components are absent.
58 ///
59 /// This is the default for all assets, and is the most performant option,
60 /// allowing to have many effects in the scene without the need to simulate
61 /// all of them if they're not visible.
62 ///
63 /// Note that any [`ParticleEffect`] spawned is always compiled into a
64 /// [`CompiledParticleEffect`], even when it's not visible and even when
65 /// that variant is selected. That means it consumes GPU resources (memory,
66 /// in particular).
67 ///
68 /// Note also that AABB culling is not currently available. Only boolean
69 /// ON/OFF visibility is used.
70 ///
71 /// [`Visibility`]: bevy::camera::visibility::Visibility
72 /// [`InheritedVisibility`]: bevy::camera::visibility::InheritedVisibility
73 /// [`ViewVisibility`]: bevy::camera::visibility::ViewVisibility
74 /// [`ParticleEffect`]: crate::ParticleEffect
75 /// [`CompiledParticleEffect`]: crate::CompiledParticleEffect
76 #[default]
77 WhenVisible,
78
79 /// Always simulate the effect, whether visible or not.
80 ///
81 /// For performance reasons, it's recommended to only simulate visible
82 /// particle effects (that is, use [`SimulationCondition::WhenVisible`]).
83 /// However occasionally it may be needed to continue the simulation
84 /// when the effect is not visible, to ensure some temporal continuity when
85 /// the effect is made visible again. This is an uncommon case, and you
86 /// should be aware of the performance implications of using this
87 /// condition, and only use it when strictly necessary.
88 ///
89 /// Any [`InheritedVisibility`] or [`ViewVisibility`] component is ignored.
90 ///
91 /// [`Visibility`]: bevy::camera::visibility::Visibility
92 /// [`InheritedVisibility`]: bevy::camera::visibility::InheritedVisibility
93 /// [`ViewVisibility`]: bevy::camera::visibility::ViewVisibility
94 Always,
95}
96
97/// Alpha mode for rendering an effect.
98///
99/// The alpha mode determines how the alpha value of a particle is used to
100/// render it. In general effects use semi-transparent particles. However, there
101/// are multiple alpha blending techniques available, producing different
102/// results.
103///
104/// This is very similar to the `bevy::prelude::AlphaMode` of the `bevy_pbr`
105/// crate, except that a different set of values is supported which reflects
106/// what this library currently supports.
107///
108/// The alpha mode only affects the render phase that particles are rendered
109/// into when rendering 3D views. For 2D views, all particle effects are
110/// rendered during the [`Transparent2d`] render phase.
111///
112/// [`Transparent2d`]: bevy::core_pipeline::core_2d::Transparent2d
113#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
114#[non_exhaustive]
115pub enum AlphaMode {
116 /// Render the effect with alpha blending.
117 ///
118 /// This is the most common mode for handling transparency. It uses the
119 /// "blend" or "over" formula, where the color of each particle fragment is
120 /// accumulated into the destination render target after being modulated by
121 /// its alpha value.
122 ///
123 /// ```txt
124 /// dst_color = src_color * (1 - particle_alpha) + particle_color * particle_alpha;
125 /// dst_alpha = src_alpha * (1 - particle_alpha) + particle_alpha
126 /// ```
127 ///
128 /// This is the default blending mode.
129 ///
130 /// For 3D views, effects with this mode are rendered during the
131 /// [`Transparent3d`] render phase.
132 ///
133 /// [`Transparent3d`]: bevy::core_pipeline::core_3d::Transparent3d
134 #[default]
135 Blend,
136
137 /// Similar to [`AlphaMode::Blend`], however assumes RGB channel values are
138 /// [premultiplied](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied).
139 ///
140 /// For otherwise constant RGB values, behaves more like
141 /// [`AlphaMode::Blend`] for alpha values closer to 1.0, and more like
142 /// [`AlphaMode::Add`] for alpha values closer to 0.0.
143 ///
144 /// Can be used to avoid “border” or “outline” artifacts that can occur
145 /// when using plain alpha-blended textures.
146 Premultiply,
147
148 /// Combines the color of the fragments with the colors behind them in an
149 /// additive process, (i.e. like light) producing lighter results.
150 ///
151 /// Black produces no effect. Alpha values can be used to modulate the
152 /// result.
153 ///
154 /// Useful for effects like holograms, ghosts, lasers and other energy
155 /// beams.
156 Add,
157
158 /// Combines the color of the fragments with the colors behind them in a
159 /// multiplicative process, (i.e. like pigments) producing darker results.
160 ///
161 /// White produces no effect. Alpha values can be used to modulate the
162 /// result.
163 ///
164 /// Useful for effects like stained glass, window tint film and some colored
165 /// liquids.
166 Multiply,
167
168 /// Render the effect with alpha masking.
169 ///
170 /// With this mode, the final alpha value computed per particle fragment is
171 /// compared against the cutoff value stored in this enum. Any fragment
172 /// with a value under the cutoff is discarded, while any fragment with
173 /// a value equal or over the cutoff becomes fully opaque. The end result is
174 /// an opaque particle with a cutout shape.
175 ///
176 /// ```txt
177 /// if src_alpha >= cutoff {
178 /// dst_color = particle_color;
179 /// dst_alpha = 1;
180 /// } else {
181 /// discard;
182 /// }
183 /// ```
184 ///
185 /// The assigned expression must yield a scalar floating-point value,
186 /// typically in the \[0:1\] range. This expression is assigned at the
187 /// beginning of the fragment shader to the special built-in `alpha_cutoff`
188 /// variable, which can be further accessed and modified by render
189 /// modifiers.
190 ///
191 /// The cutoff threshold comparison of the fragment's alpha value against
192 /// `alpha_cutoff` is performed as the last operation in the fragment
193 /// shader. This allows modifiers to affect the alpha value of the
194 /// particle before it's tested against the cutoff value stored in
195 /// `alpha_cutoff`.
196 ///
197 /// For 3D views, effects with this mode are rendered during the
198 /// [`AlphaMask3d`] render phase.
199 ///
200 /// [`AlphaMask3d`]: bevy::core_pipeline::core_3d::AlphaMask3d
201 Mask(ExprHandle),
202
203 /// Render the effect with no alpha, and update the depth buffer.
204 ///
205 /// Use this mode when every pixel covered by the particle's mesh is fully
206 /// opaque.
207 Opaque,
208}
209
210impl From<AlphaMode> for BlendState {
211 fn from(value: AlphaMode) -> Self {
212 match value {
213 AlphaMode::Blend => BlendState::ALPHA_BLENDING,
214 AlphaMode::Premultiply => BlendState::PREMULTIPLIED_ALPHA_BLENDING,
215 AlphaMode::Add => BlendState {
216 color: BlendComponent {
217 src_factor: BlendFactor::SrcAlpha,
218 dst_factor: BlendFactor::One,
219 operation: BlendOperation::Add,
220 },
221 alpha: BlendComponent {
222 src_factor: BlendFactor::Zero,
223 dst_factor: BlendFactor::One,
224 operation: BlendOperation::Add,
225 },
226 },
227 AlphaMode::Multiply => BlendState {
228 color: BlendComponent {
229 src_factor: BlendFactor::Dst,
230 dst_factor: BlendFactor::OneMinusSrcAlpha,
231 operation: BlendOperation::Add,
232 },
233 alpha: BlendComponent::OVER,
234 },
235 _ => BlendState::ALPHA_BLENDING,
236 }
237 }
238}
239
240/// Default particle mesh, if not otherwise specified in [`EffectAsset::mesh`].
241///
242/// This defaults to a unit quad facing the Z axis.
243///
244/// [`EffectAsset`]: crate::EffectAsset
245#[derive(Debug, Clone, Resource)]
246pub struct DefaultMesh(pub Handle<Mesh>);
247
248impl FromWorld for DefaultMesh {
249 fn from_world(world: &mut World) -> Self {
250 let mut meshes = world.resource_mut::<Assets<Mesh>>();
251 let handle = meshes.add(Plane3d::new(Vec3::Z, Vec2::splat(0.5)));
252 trace!("Created DefaultMesh(Plane3d/Z): handle={handle:?}");
253 Self(handle)
254 }
255}
256
257/// Asset describing a visual effect.
258///
259/// An effect asset represents the description of an effect, intended to be
260/// authored during development and instantiated once or more during the
261/// application execution.
262///
263/// An actual effect instance can be spanwed with a [`ParticleEffect`]
264/// component which references the [`EffectAsset`].
265///
266/// [`ParticleEffect`]: crate::ParticleEffect
267/// [`EffectAsset`]: crate::EffectAsset
268#[derive(Asset, Default, Clone, Reflect)]
269#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
270#[reflect(from_reflect = false)]
271pub struct EffectAsset {
272 /// Display name of the effect.
273 ///
274 /// This has no internal use, and is mostly for the user to identify an
275 /// effect or for display in some tool UI. It's however used in serializing
276 /// the asset.
277 pub name: String,
278 /// Maximum number of concurrent particles.
279 ///
280 /// The capacity is the maximum number of particles that can be alive at the
281 /// same time. It determines the size of various GPU resources, most notably
282 /// the particle buffer itself. To prevent wasting GPU resources, users
283 /// should keep this quantity as close as possible to the maximum number of
284 /// particles they expect to render.
285 capacity: u32,
286 /// The CPU spawner for this effect.
287 pub spawner: SpawnerSettings,
288 /// For 2D rendering, the Z coordinate used as the sort key.
289 ///
290 /// This value is passed to the render pipeline and used when sorting
291 /// transparent items to render, to order them. As a result, effects
292 /// with different Z values cannot be batched together, which may
293 /// negatively affect performance.
294 ///
295 /// Ignored for 3D rendering.
296 pub z_layer_2d: f32,
297 /// Particle simulation space.
298 pub simulation_space: SimulationSpace,
299 /// Condition under which the effect is simulated.
300 pub simulation_condition: SimulationCondition,
301 /// Seed for the pseudo-random number generator.
302 ///
303 /// This value is used as the default value for all [`ParticleEffect`]
304 /// instances based on this asset. You can override this on a per-instance
305 /// basis by setting [`ParticleEffect::prng_seed`]. The resulting value
306 /// is uploaded to GPU and used for the various random expressions and
307 /// quantities computed in shaders.
308 ///
309 /// [`ParticleEffect`]: crate::ParticleEffect
310 /// [`ParticleEffect::prng_seed`]: crate::ParticleEffect::prng_seed
311 pub prng_seed: u32,
312 /// Init modifier defining the effect.
313 #[reflect(ignore)]
314 // TODO - Can't manage to implement FromReflect for BoxedModifier in a nice way yet
315 init_modifiers: Vec<BoxedModifier>,
316 /// update modifiers defining the effect.
317 #[reflect(ignore)]
318 // TODO - Can't manage to implement FromReflect for BoxedModifier in a nice way yet
319 update_modifiers: Vec<BoxedModifier>,
320 /// Render modifiers defining the effect.
321 #[reflect(ignore)]
322 // TODO - Can't manage to implement FromReflect for BoxedModifier in a nice way yet
323 render_modifiers: Vec<BoxedModifier>,
324 /// Type of motion integration applied to the particles of a system.
325 pub motion_integration: MotionIntegration,
326 /// Expression module for this effect.
327 module: Module,
328 /// Alpha mode.
329 pub alpha_mode: AlphaMode,
330 /// The mesh that each particle renders.
331 ///
332 /// If `None`, the effect uses the [`DefaultMesh`].
333 #[cfg_attr(feature = "serde", serde(skip))]
334 pub mesh: Option<Handle<Mesh>>,
335}
336
337impl EffectAsset {
338 /// Create a new effect asset.
339 ///
340 /// The effect assets requires 2 essential pieces:
341 /// - The capacity of the effect, which represents the maximum number of
342 /// particles which can be stored and simulated at the same time for each
343 /// group. Each group has its own capacity, in number of particles. All
344 /// capacities must be non-zero and should be the smallest possible values
345 /// which allow you to author the effect. These values directly impact the
346 /// GPU memory consumption of the effect, which will allocate some buffers
347 /// to store that many particles for as long as the effect exists. The
348 /// capacities of an effect are immutable. See also [`capacity()`] for
349 /// more details.
350 /// - The [`SpawnerSettings`], which defines when particles are emitted.
351 ///
352 /// Additionally, if any modifier added to this effect uses some [`Expr`] to
353 /// customize its behavior, then those [`Expr`] are stored into a [`Module`]
354 /// which should be passed to this method. If expressions are not used, just
355 /// pass an empty module [`Module::default()`].
356 ///
357 /// # Examples
358 ///
359 /// Create a new effect asset without any modifier. This effect doesn't
360 /// really do anything because _e.g._ the particles have a zero lifetime.
361 ///
362 /// ```
363 /// # use bevy_hanabi::*;
364 /// let spawner = SpawnerSettings::rate(5_f32.into()); // 5 particles per second
365 /// let module = Module::default();
366 /// let capacity = 1024; // max 1024 particles alive at any time
367 /// let effect = EffectAsset::new(capacity, spawner, module);
368 /// ```
369 ///
370 /// Create a new effect asset with a modifier holding an expression. The
371 /// expression is stored inside the [`Module`] transfered to the
372 /// [`EffectAsset`], which owns the module once created.
373 ///
374 /// ```
375 /// # use bevy_hanabi::*;
376 /// let spawner = SpawnerSettings::rate(5_f32.into()); // 5 particles per second
377 ///
378 /// let mut module = Module::default();
379 ///
380 /// // Create a modifier that initialized the particle lifetime to 10 seconds.
381 /// let lifetime = module.lit(10.); // literal value "10.0"
382 /// let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, lifetime);
383 ///
384 /// let capacity = 1024; // max 1024 particles alive at any time
385 /// let effect = EffectAsset::new(capacity, spawner, module);
386 /// ```
387 ///
388 /// [`capacity()`]: self::EffectAsset::capacity
389 /// [`Expr`]: crate::graph::expr::Expr
390 pub fn new(capacity: u32, spawner: SpawnerSettings, module: Module) -> Self {
391 Self {
392 capacity,
393 spawner,
394 module,
395 ..default()
396 }
397 }
398
399 /// Get the capacity of the effect, in number of particles.
400 ///
401 /// This value represents the number of particles stored in GPU memory at
402 /// all times, even if unused, so you should try to minimize it.
403 /// However, the library cannot emit more particles than the effect
404 /// capacity. Whatever the spawner settings, if the number of particles
405 /// reaches the capacity, no new particle can be emitted. Choosing an
406 /// appropriate capacity for an effect is therefore a compromise between
407 /// more particles available for visuals and more GPU memory usage.
408 ///
409 /// Common values range from 256 or less for smaller effects, to several
410 /// hundreds of thousands for unique effects consuming a large portion of
411 /// the GPU memory budget. Hanabi has been tested with over a million
412 /// particles, however the performance will largely depend on the actual GPU
413 /// hardware and available memory, so authors are encouraged not to go too
414 /// crazy with the capacity.
415 ///
416 /// [`EffectSpawner`]: crate::EffectSpawner
417 pub fn capacity(&self) -> u32 {
418 self.capacity
419 }
420
421 /// Get the expression module storing all expressions in use by modifiers of
422 /// this effect.
423 pub fn module(&self) -> &Module {
424 &self.module
425 }
426
427 /// Set the effect name.
428 ///
429 /// The effect name is used when serializing the effect.
430 pub fn with_name(mut self, name: impl Into<String>) -> Self {
431 self.name = name.into();
432 self
433 }
434
435 /// Set the effect's simulation condition.
436 pub fn with_simulation_condition(mut self, simulation_condition: SimulationCondition) -> Self {
437 self.simulation_condition = simulation_condition;
438 self
439 }
440
441 /// Set the effect's simulation space.
442 pub fn with_simulation_space(mut self, simulation_space: SimulationSpace) -> Self {
443 self.simulation_space = simulation_space;
444 self
445 }
446
447 /// Set the alpha mode.
448 pub fn with_alpha_mode(mut self, alpha_mode: AlphaMode) -> Self {
449 self.alpha_mode = alpha_mode;
450 self
451 }
452
453 /// Set the effect's motion integration.
454 pub fn with_motion_integration(mut self, motion_integration: MotionIntegration) -> Self {
455 self.motion_integration = motion_integration;
456 self
457 }
458
459 /// Get the list of existing properties.
460 ///
461 /// This is a shortcut for `self.module().properties()`.
462 pub fn properties(&self) -> &[Property] {
463 self.module.properties()
464 }
465
466 /// Add an initialization modifier to the effect.
467 ///
468 /// Initialization modifiers apply to all particles that are spawned or
469 /// cloned.
470 ///
471 /// # Panics
472 ///
473 /// Panics if the modifier doesn't support the init context (that is,
474 /// `modifier.context()` returns a flag which doesn't include
475 /// [`ModifierContext::Init`]).
476 #[inline]
477 pub fn init<M>(mut self, modifier: M) -> Self
478 where
479 M: Modifier + Send + Sync,
480 {
481 assert!(modifier.context().contains(ModifierContext::Init));
482 self.init_modifiers.push(Box::new(modifier));
483 self
484 }
485
486 /// Add an update modifier to the effect.
487 ///
488 /// # Panics
489 ///
490 /// Panics if the modifier doesn't support the update context (that is,
491 /// `modifier.context()` returns a flag which doesn't include
492 /// [`ModifierContext::Update`]).
493 #[inline]
494 pub fn update<M>(mut self, modifier: M) -> Self
495 where
496 M: Modifier + Send + Sync,
497 {
498 assert!(modifier.context().contains(ModifierContext::Update));
499 self.update_modifiers.push(Box::new(modifier));
500 self
501 }
502
503 /// Add a [`BoxedModifier`] to the specific context.
504 ///
505 /// # Panics
506 ///
507 /// Panics if the context is [`ModifierContext::Render`]; use
508 /// [`add_render_modifier()`] instead.
509 ///
510 /// Panics if the input `context` contains more than one context (the
511 /// bitfield contains more than 1 bit set) or no context at all (zero bit
512 /// set).
513 ///
514 /// Panics if the modifier doesn't support the context specified (that is,
515 /// `modifier.context()` returns a flag which doesn't include `context`).
516 ///
517 /// [`BoxedModifier`]: crate::BoxedModifier
518 /// [`add_render_modifier()`]: crate::EffectAsset::add_render_modifier
519 pub fn add_modifier(mut self, context: ModifierContext, modifier: Box<dyn Modifier>) -> Self {
520 assert!(context == ModifierContext::Init || context == ModifierContext::Update);
521 assert!(modifier.context().contains(context));
522 if context == ModifierContext::Init {
523 self.init_modifiers.push(modifier);
524 } else {
525 self.update_modifiers.push(modifier);
526 }
527 self
528 }
529
530 /// Add a render modifier to the effect.
531 ///
532 /// # Panics
533 ///
534 /// Panics if the modifier doesn't support the render context (that is,
535 /// `modifier.context()` returns a flag which doesn't include
536 /// [`ModifierContext::Render`]).
537 #[inline]
538 pub fn render<M>(mut self, modifier: M) -> Self
539 where
540 M: RenderModifier + Send + Sync,
541 {
542 assert!(modifier.context().contains(ModifierContext::Render));
543 self.render_modifiers.push(Box::new(modifier));
544 self
545 }
546
547 /// Add a [`RenderModifier`] to the render context.
548 ///
549 /// # Panics
550 ///
551 /// Panics if the modifier doesn't support the render context (that is,
552 /// `modifier.context()` returns a flag which doesn't include
553 /// [`ModifierContext::Render`]).
554 pub fn add_render_modifier(mut self, modifier: Box<dyn RenderModifier>) -> Self {
555 assert!(modifier.context().contains(ModifierContext::Render));
556 self.render_modifiers.push(modifier.boxed_clone());
557 self
558 }
559
560 /// Get a list of all the modifiers of this effect.
561 pub fn modifiers(&self) -> impl Iterator<Item = &dyn Modifier> {
562 self.init_modifiers
563 .iter()
564 .map(|bm| &**bm)
565 .chain(self.update_modifiers.iter().map(|bm| &**bm))
566 .chain(self.render_modifiers.iter().map(|bm| &**bm))
567 }
568
569 /// Get a list of all the init modifiers of this effect.
570 ///
571 /// This is a filtered list of all modifiers, retaining only modifiers
572 /// executing in the [`ModifierContext::Init`] context.
573 ///
574 /// [`ModifierContext::Init`]: crate::ModifierContext::Init
575 pub fn init_modifiers(&self) -> impl Iterator<Item = &dyn Modifier> {
576 self.init_modifiers.iter().map(|bm| &**bm)
577 }
578
579 /// Get a list of all the update modifiers of this effect.
580 ///
581 /// This is a filtered list of all modifiers, retaining only modifiers
582 /// executing in the [`ModifierContext::Update`] context.
583 ///
584 /// [`ModifierContext::Update`]: crate::ModifierContext::Update
585 pub fn update_modifiers(&self) -> impl Iterator<Item = &dyn Modifier> {
586 self.update_modifiers.iter().map(|bm| &**bm)
587 }
588
589 /// Get a list of all the render modifiers of this effect.
590 ///
591 /// This is a filtered list of all modifiers, retaining only modifiers
592 /// executing in the [`ModifierContext::Render`] context.
593 ///
594 /// [`ModifierContext::Render`]: crate::ModifierContext::Render
595 pub fn render_modifiers(&self) -> impl Iterator<Item = &dyn RenderModifier> {
596 self.render_modifiers.iter().filter_map(|m| m.as_render())
597 }
598
599 /// Build the particle layout of the asset based on its modifiers.
600 ///
601 /// This method calculates the particle layout of the effect based on the
602 /// currently existing modifiers, and return it as a newly allocated
603 /// [`ParticleLayout`] object.
604 pub fn particle_layout(&self) -> ParticleLayout {
605 // Build the set of unique attributes required for all modifiers
606 let mut set = HashSet::new();
607 for modifier in self.modifiers() {
608 for &attr in modifier.attributes() {
609 set.insert(attr);
610 }
611 }
612
613 // Add all attributes used by expressions. Those are indirectly used by
614 // modifiers, but may not have been added directly yet.
615 self.module.gather_attributes(&mut set);
616
617 // Build the layout
618 let mut layout = ParticleLayout::new();
619 for attr in set {
620 layout = layout.append(attr);
621 }
622 layout.build()
623 }
624
625 /// Build the property layout of the asset based on its properties.
626 ///
627 /// This method calculates the property layout of the effect based on the
628 /// currently existing properties, and return it as a newly allocated
629 /// [`PropertyLayout`] object.
630 pub fn property_layout(&self) -> PropertyLayout {
631 PropertyLayout::new(self.properties().iter())
632 }
633
634 /// Get the texture layout of the module of this effect.
635 pub fn texture_layout(&self) -> TextureLayout {
636 self.module.texture_layout()
637 }
638
639 /// Sets the mesh that each particle will render.
640 pub fn mesh(mut self, mesh: Handle<Mesh>) -> Self {
641 self.mesh = Some(mesh);
642 self
643 }
644}
645
646/// Asset loader for [`EffectAsset`].
647///
648/// Effet assets take the `.effect` extension.
649#[cfg(feature = "serde")]
650#[derive(Default, TypePath)]
651pub struct EffectAssetLoader;
652
653/// Error for the [`EffectAssetLoader`] loading an [`EffectAsset`].
654#[cfg(feature = "serde")]
655#[derive(Error, Debug)]
656pub enum EffectAssetLoaderError {
657 /// I/O error reading the asset source.
658 #[error("An IO error occurred during loading of a particle effect")]
659 Io(#[from] std::io::Error),
660
661 /// Error during RON format parsing.
662 #[error("A RON format error occurred during loading of a particle effect")]
663 Ron(#[from] ron::error::SpannedError),
664}
665
666#[cfg(feature = "serde")]
667impl AssetLoader for EffectAssetLoader {
668 type Asset = EffectAsset;
669
670 type Settings = ();
671
672 type Error = EffectAssetLoaderError;
673
674 async fn load(
675 &self,
676 reader: &mut dyn Reader,
677 _settings: &Self::Settings,
678 _load_context: &mut LoadContext<'_>,
679 ) -> Result<Self::Asset, Self::Error> {
680 let mut bytes = Vec::new();
681 reader.read_to_end(&mut bytes).await?;
682 let custom_asset = ron::de::from_bytes::<EffectAsset>(&bytes)?;
683 Ok(custom_asset)
684 }
685
686 fn extensions(&self) -> &[&str] {
687 &["effect"]
688 }
689}
690
691/// Component defining the parent effect of the current effect.
692///
693/// This component is optional. When present, on the same entity as the
694/// [`ParticleEffect`], it defines the "parent effect" of that effect. The
695/// particles of the parent effect are accessible from the init pass of this
696/// effect, to allow the particles from the current effect to inherit some
697/// attributes (position, velocity, ...) from the parent particle which
698/// triggered its spawning via GPU spawn events.
699///
700/// Adding this component automatically makes the current particle effect
701/// instance use GPU spawn events emitted by its parent, and automatically makes
702/// the parent effect instance emits such events.
703///
704/// An effect has at most one parent, defined by this component, but a parent
705/// effect can have multiple children. For example, a parent effect can emit GPU
706/// spawn events continuously ([`EventEmitCondition::Always`]) to generate some
707/// kind of trail, and also emit GPU spawn events when its particles die
708/// ([`EventEmitCondition::OnDie`]) for any explosion-like effect.
709///
710/// [`ParticleEffect`]: crate::ParticleEffect
711/// [`EventEmitCondition::Always`]: crate::EventEmitCondition::Always
712/// [`EventEmitCondition::OnDie`]: crate::EventEmitCondition::OnDie
713#[derive(Debug, Clone, Copy, Component, Reflect)]
714pub struct EffectParent {
715 /// Entity of the parent effect.
716 pub entity: Entity,
717}
718
719impl EffectParent {
720 /// Create a new component with the given entity as parent.
721 pub fn new(parent: Entity) -> Self {
722 Self { entity: parent }
723 }
724}
725
726#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
727#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
728pub struct ParticleTrails {
729 pub spawn_period: f32,
730}
731
732#[cfg(test)]
733mod tests {
734 #[cfg(feature = "serde")]
735 use ron::ser::PrettyConfig;
736
737 use super::*;
738 use crate::*;
739
740 #[test]
741 fn add_modifiers() {
742 let mut m = Module::default();
743 let expr = m.lit(3.);
744
745 for modifier_context in [ModifierContext::Init, ModifierContext::Update] {
746 let effect = EffectAsset::default().add_modifier(
747 modifier_context,
748 Box::new(SetAttributeModifier::new(Attribute::POSITION, expr)),
749 );
750 assert_eq!(effect.modifiers().count(), 1);
751 let m = effect.modifiers().next().unwrap();
752 assert!(m.context().contains(modifier_context));
753 }
754
755 {
756 let effect = EffectAsset::default().add_render_modifier(Box::new(SetColorModifier {
757 color: CpuValue::Single(Vec4::ONE),
758 blend: ColorBlendMode::Overwrite,
759 mask: ColorBlendMask::RGBA,
760 }));
761 assert_eq!(effect.modifiers().count(), 1);
762 let m = effect.modifiers().next().unwrap();
763 assert!(m.context().contains(ModifierContext::Render));
764 }
765 }
766
767 #[test]
768 fn test_apply_modifiers() {
769 let mut module = Module::default();
770 let origin = module.lit(Vec3::ZERO);
771 let one = module.lit(1.);
772 let slot_zero = module.lit(0u32);
773 let init_age = SetAttributeModifier::new(Attribute::AGE, one);
774 let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, one);
775 let init_pos_sphere = SetPositionSphereModifier {
776 center: module.lit(Vec3::ZERO),
777 radius: module.lit(1.),
778 dimension: ShapeDimension::Volume,
779 };
780 let init_vel_sphere = SetVelocitySphereModifier {
781 center: module.lit(Vec3::ZERO),
782 speed: module.lit(1.),
783 };
784
785 let mut effect = EffectAsset::new(4096, SpawnerSettings::rate(30.0.into()), module)
786 .init(init_pos_sphere)
787 .init(init_vel_sphere)
788 //.update(AccelModifier::default())
789 .update(LinearDragModifier::new(one))
790 .update(ConformToSphereModifier::new(origin, one, one, one, one))
791 .render(ParticleTextureModifier::new(slot_zero))
792 .render(ColorOverLifetimeModifier::default())
793 .render(SizeOverLifetimeModifier::default())
794 .render(OrientModifier::new(OrientMode::ParallelCameraDepthPlane))
795 .render(OrientModifier::new(OrientMode::FaceCameraPosition))
796 .render(OrientModifier::new(OrientMode::AlongVelocity));
797
798 assert_eq!(effect.capacity, 4096);
799
800 let module = &mut effect.module;
801 let property_layout = PropertyLayout::default();
802 let particle_layout = ParticleLayout::default();
803 let mut init_context =
804 ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
805 assert!(init_pos_sphere.apply(module, &mut init_context).is_ok());
806 assert!(init_vel_sphere.apply(module, &mut init_context).is_ok());
807 assert!(init_age.apply(module, &mut init_context).is_ok());
808 assert!(init_lifetime.apply(module, &mut init_context).is_ok());
809 // assert_eq!(effect., init_context.init_code);
810
811 let accel_mod = AccelModifier::constant(module, Vec3::ONE);
812 let drag_mod = LinearDragModifier::constant(module, 3.5);
813 let property_layout = PropertyLayout::default();
814 let particle_layout = ParticleLayout::default();
815 let mut update_context =
816 ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
817 assert!(accel_mod.apply(module, &mut update_context).is_ok());
818 assert!(drag_mod.apply(module, &mut update_context).is_ok());
819 assert!(ConformToSphereModifier::new(origin, one, one, one, one)
820 .apply(module, &mut update_context)
821 .is_ok());
822 // assert_eq!(effect.update_layout, update_layout);
823
824 let property_layout = PropertyLayout::default();
825 let particle_layout = ParticleLayout::default();
826 let texture_layout = TextureLayout::default();
827 let mut render_context =
828 RenderContext::new(&property_layout, &particle_layout, &texture_layout);
829 ParticleTextureModifier::new(slot_zero)
830 .apply_render(module, &mut render_context)
831 .unwrap();
832 ColorOverLifetimeModifier::default()
833 .apply_render(module, &mut render_context)
834 .unwrap();
835 SizeOverLifetimeModifier::default()
836 .apply_render(module, &mut render_context)
837 .unwrap();
838 OrientModifier::new(OrientMode::ParallelCameraDepthPlane)
839 .apply_render(module, &mut render_context)
840 .unwrap();
841 OrientModifier::new(OrientMode::FaceCameraPosition)
842 .apply_render(module, &mut render_context)
843 .unwrap();
844 OrientModifier::new(OrientMode::AlongVelocity)
845 .apply_render(module, &mut render_context)
846 .unwrap();
847 // assert_eq!(effect.render_layout, render_layout);
848 }
849
850 #[cfg(feature = "serde")]
851 #[test]
852 fn test_serde_ron() {
853 let w = ExprWriter::new();
854
855 let pos = w.lit(Vec3::new(1.2, -3.45, 87.54485));
856 let x = w.lit(BVec2::new(false, true));
857 let _ = x + pos.clone();
858 let mod_pos = SetAttributeModifier::new(Attribute::POSITION, pos.expr());
859
860 let mut module = w.finish();
861 let prop = module.add_property("my_prop", Vec3::new(1.2, -2.3, 55.32).into());
862 let prop = module.prop(prop);
863 let _ = module.abs(prop);
864
865 let effect = EffectAsset {
866 name: "Effect".into(),
867 capacity: 4096,
868 spawner: SpawnerSettings::rate(30.0.into()),
869 module,
870 ..Default::default()
871 }
872 .init(mod_pos);
873
874 let s = ron::ser::to_string_pretty(&effect, PrettyConfig::new().new_line("\n".to_string()))
875 .unwrap();
876 eprintln!("{}", s);
877 assert_eq!(
878 s,
879 r#"(
880 name: "Effect",
881 capacity: 4096,
882 spawner: (
883 count: Single(30.0),
884 spawn_duration: Single(1.0),
885 period: Single(1.0),
886 cycle_count: 0,
887 starts_active: true,
888 emit_on_start: true,
889 ),
890 z_layer_2d: 0.0,
891 simulation_space: Global,
892 simulation_condition: WhenVisible,
893 prng_seed: 0,
894 init_modifiers: [
895 {
896 "SetAttributeModifier": (
897 attribute: "position",
898 value: 1,
899 ),
900 },
901 ],
902 update_modifiers: [],
903 render_modifiers: [],
904 motion_integration: PostUpdate,
905 module: (
906 expressions: [
907 Literal(Vector(Vec3((1.2, -3.45, 87.54485)))),
908 Literal(Vector(BVec2((false, true)))),
909 Binary(
910 op: Add,
911 left: 2,
912 right: 1,
913 ),
914 Property(1),
915 Unary(
916 op: Abs,
917 expr: 4,
918 ),
919 ],
920 properties: [
921 (
922 name: "my_prop",
923 default_value: Vector(Vec3((1.2, -2.3, 55.32))),
924 ),
925 ],
926 texture_layout: (
927 layout: [],
928 ),
929 ),
930 alpha_mode: Blend,
931)"#
932 );
933 let effect_serde: EffectAsset = ron::from_str(&s).unwrap();
934 assert_eq!(effect.name, effect_serde.name);
935 assert_eq!(effect.capacity, effect_serde.capacity);
936 assert_eq!(effect.spawner, effect_serde.spawner);
937 assert_eq!(effect.z_layer_2d, effect_serde.z_layer_2d);
938 assert_eq!(effect.simulation_space, effect_serde.simulation_space);
939 assert_eq!(
940 effect.simulation_condition,
941 effect_serde.simulation_condition
942 );
943 assert_eq!(effect.motion_integration, effect_serde.motion_integration);
944 assert_eq!(effect.module, effect_serde.module);
945 assert_eq!(effect.alpha_mode, effect_serde.alpha_mode);
946 assert_eq!(
947 effect.init_modifiers().count(),
948 effect_serde.init_modifiers().count()
949 );
950 assert_eq!(
951 effect.update_modifiers().count(),
952 effect_serde.update_modifiers().count()
953 );
954 assert_eq!(
955 effect.render_modifiers().count(),
956 effect_serde.render_modifiers().count()
957 );
958 }
959
960 #[test]
961 fn alpha_mode_blend_state() {
962 assert_eq!(BlendState::ALPHA_BLENDING, AlphaMode::Blend.into());
963 assert_eq!(
964 BlendState::PREMULTIPLIED_ALPHA_BLENDING,
965 AlphaMode::Premultiply.into()
966 );
967
968 let blend_state = BlendState {
969 color: BlendComponent {
970 src_factor: BlendFactor::SrcAlpha,
971 dst_factor: BlendFactor::One,
972 operation: BlendOperation::Add,
973 },
974 alpha: BlendComponent {
975 src_factor: BlendFactor::Zero,
976 dst_factor: BlendFactor::One,
977 operation: BlendOperation::Add,
978 },
979 };
980 assert_eq!(blend_state, AlphaMode::Add.into());
981
982 let blend_state = BlendState {
983 color: BlendComponent {
984 src_factor: BlendFactor::Dst,
985 dst_factor: BlendFactor::OneMinusSrcAlpha,
986 operation: BlendOperation::Add,
987 },
988 alpha: BlendComponent::OVER,
989 };
990 assert_eq!(blend_state, AlphaMode::Multiply.into());
991
992 let expr = Module::default().lit(0.5);
993 assert_eq!(BlendState::ALPHA_BLENDING, AlphaMode::Mask(expr).into());
994 }
995
996 // Regression test for #440
997 #[test]
998 fn transitive_attr() {
999 let mut m = Module::default();
1000 let age = m.attr(Attribute::F32_0);
1001 let modifier = SetAttributeModifier::new(Attribute::AGE, age);
1002 let asset = EffectAsset::new(32, SpawnerSettings::once(3.0.into()), m).init(modifier);
1003 let particle_layout = asset.particle_layout();
1004 assert!(particle_layout.contains(Attribute::AGE)); // direct
1005 assert!(particle_layout.contains(Attribute::F32_0)); // transitive
1006 }
1007}