1use crate::scene::node::constructor::NodeConstructor;
25use crate::scene::particle_system::emitter::base::BaseEmitterBuilder;
26use crate::scene::particle_system::emitter::sphere::SphereEmitterBuilder;
27use crate::{
28 core::{
29 algebra::{Point3, Vector2, Vector3},
30 color::Color,
31 color_gradient::ColorGradient,
32 math::{aabb::AxisAlignedBoundingBox, TriangleDefinition},
33 pool::Handle,
34 reflect::prelude::*,
35 type_traits::prelude::*,
36 uuid::{uuid, Uuid},
37 value_as_u8_slice,
38 variable::InheritableVariable,
39 visitor::prelude::*,
40 },
41 material::{self, Material, MaterialResource},
42 rand::{prelude::StdRng, Error, RngCore, SeedableRng},
43 renderer::{self, bundle::RenderContext},
44 scene::{
45 base::{Base, BaseBuilder},
46 graph::Graph,
47 mesh::{buffer::VertexTrait, RenderPath},
48 node::{Node, NodeTrait, RdcControlFlow, UpdateContext},
49 particle_system::{
50 draw::Vertex,
51 emitter::{Emit, Emitter},
52 particle::Particle,
53 },
54 },
55};
56use fyrox_graph::constructor::ConstructorProvider;
57use fyrox_graph::BaseSceneGraph;
58use std::{
59 cmp::Ordering,
60 fmt::Debug,
61 ops::{Deref, DerefMut},
62};
63use strum_macros::{AsRefStr, EnumString, VariantNames};
64
65pub(crate) mod draw;
66pub mod emitter;
67pub mod particle;
68
69#[derive(Debug, Clone, Reflect)]
71pub struct ParticleSystemRng {
72 rng_seed: u64,
73
74 #[reflect(hidden)]
75 rng: StdRng,
76}
77
78impl Default for ParticleSystemRng {
79 fn default() -> Self {
80 Self::new(0xDEADBEEF)
81 }
82}
83
84impl ParticleSystemRng {
85 pub fn new(seed: u64) -> Self {
88 Self {
89 rng_seed: seed,
90 rng: StdRng::seed_from_u64(seed),
91 }
92 }
93
94 #[inline]
96 pub fn reset(&mut self) {
97 self.rng = StdRng::seed_from_u64(self.rng_seed);
98 }
99}
100
101impl RngCore for ParticleSystemRng {
102 #[inline]
103 fn next_u32(&mut self) -> u32 {
104 self.rng.next_u32()
105 }
106
107 #[inline]
108 fn next_u64(&mut self) -> u64 {
109 self.rng.next_u64()
110 }
111
112 #[inline]
113 fn fill_bytes(&mut self, dest: &mut [u8]) {
114 self.rng.fill_bytes(dest)
115 }
116
117 #[inline]
118 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
119 self.rng.try_fill_bytes(dest)
120 }
121}
122
123impl Visit for ParticleSystemRng {
124 fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
125 let mut guard = visitor.enter_region(name)?;
126
127 self.rng_seed.visit("Seed", &mut guard)?;
128
129 if guard.is_reading() {
131 self.rng = StdRng::seed_from_u64(self.rng_seed);
132 }
133
134 Ok(())
135 }
136}
137
138#[derive(Debug, Clone, Reflect, ComponentProvider)]
225pub struct ParticleSystem {
226 base: Base,
227
228 pub emitters: InheritableVariable<Vec<Emitter>>,
230
231 #[reflect(setter = "set_material")]
232 material: InheritableVariable<MaterialResource>,
233
234 #[reflect(setter = "set_acceleration")]
235 acceleration: InheritableVariable<Vector3<f32>>,
236
237 #[reflect(setter = "set_color_over_lifetime_gradient")]
238 color_over_lifetime: InheritableVariable<ColorGradient>,
239
240 #[reflect(setter = "play")]
241 is_playing: InheritableVariable<bool>,
242
243 #[reflect(hidden)]
244 particles: Vec<Particle>,
245
246 #[reflect(hidden)]
247 free_particles: Vec<u32>,
248
249 #[reflect(
250 description = "The maximum distance (in meters) from an observer to the particle system at \
251 which the particle system remains visible. If the distance is larger, then the particle \
252 system will fade out and eventually will be excluded from the rendering. Use this value to \
253 tweak performance. Default is 30.0"
254 )]
255 visible_distance: InheritableVariable<f32>,
256
257 #[reflect(
258 description = "Defines a coordinate system for particles. Local coordinate space could \
259 be used for particles that must move with the particle system (sparks), world space - for \
260 particles that must be detached from the particle system (smoke trails)"
261 )]
262 coordinate_system: InheritableVariable<CoordinateSystem>,
263
264 rng: ParticleSystemRng,
265}
266
267#[derive(
269 Default,
270 Copy,
271 Clone,
272 PartialOrd,
273 PartialEq,
274 Eq,
275 Ord,
276 Hash,
277 Debug,
278 Visit,
279 Reflect,
280 AsRefStr,
281 EnumString,
282 VariantNames,
283 TypeUuidProvider,
284)]
285#[type_uuid(id = "d19e13ec-03d5-4c88-b0b2-d161d1912632")]
286pub enum CoordinateSystem {
287 #[default]
293 Local,
294 World,
300}
301
302impl Visit for ParticleSystem {
303 fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
304 let mut region = visitor.enter_region(name)?;
305
306 self.base.visit("Base", &mut region)?;
307 self.emitters.visit("Emitters", &mut region)?;
308 self.acceleration.visit("Acceleration", &mut region)?;
309 self.color_over_lifetime
310 .visit("ColorGradient", &mut region)?;
311 self.is_playing.visit("Enabled", &mut region)?;
312 self.particles.visit("Particles", &mut region)?;
313 self.free_particles.visit("FreeParticles", &mut region)?;
314 let _ = self.rng.visit("Rng", &mut region);
315 let _ = self.visible_distance.visit("VisibleDistance", &mut region);
316 let _ = self
317 .coordinate_system
318 .visit("CoordinateSystem", &mut region);
319
320 if region.is_reading() {
322 if let Some(material) = material::visit_old_texture_as_material(
323 &mut region,
324 Material::standard_particle_system,
325 ) {
326 self.material = material.into();
327 } else {
328 self.material.visit("Material", &mut region)?;
329 }
330 } else {
331 self.material.visit("Material", &mut region)?;
332 }
333
334 let mut soft_boundary_sharpness_factor = 100.0;
335 if soft_boundary_sharpness_factor
336 .visit("SoftBoundarySharpnessFactor", &mut region)
337 .is_ok()
338 {
339 self.material.data_ref().set_property(
340 "softBoundarySharpnessFactor",
341 soft_boundary_sharpness_factor,
342 );
343 }
344
345 Ok(())
346 }
347}
348
349impl Deref for ParticleSystem {
350 type Target = Base;
351
352 fn deref(&self) -> &Self::Target {
353 &self.base
354 }
355}
356
357impl DerefMut for ParticleSystem {
358 fn deref_mut(&mut self) -> &mut Self::Target {
359 &mut self.base
360 }
361}
362
363impl TypeUuidProvider for ParticleSystem {
364 fn type_uuid() -> Uuid {
365 uuid!("8b210eff-97a4-494f-ba7a-a581d3f4a442")
366 }
367}
368
369impl ParticleSystem {
370 const FADEOUT_MARGIN: f32 = 1.5;
371
372 pub fn acceleration(&self) -> Vector3<f32> {
374 *self.acceleration
375 }
376
377 pub fn set_acceleration(&mut self, accel: Vector3<f32>) -> Vector3<f32> {
380 self.acceleration.set_value_and_mark_modified(accel)
381 }
382
383 pub fn set_color_over_lifetime_gradient(&mut self, gradient: ColorGradient) -> ColorGradient {
385 self.color_over_lifetime
386 .set_value_and_mark_modified(gradient)
387 }
388
389 pub fn play(&mut self, is_playing: bool) -> bool {
392 self.is_playing.set_value_and_mark_modified(is_playing)
393 }
394
395 pub fn is_playing(&self) -> bool {
397 *self.is_playing
398 }
399
400 pub fn set_particles(&mut self, particles: Vec<Particle>) {
403 self.free_particles.clear();
404 self.particles = particles;
405 }
406
407 pub fn particles(&self) -> &[Particle] {
409 &self.particles
410 }
411
412 pub fn clear_particles(&mut self) {
414 self.particles.clear();
415 self.free_particles.clear();
416 for emitter in self.emitters.get_value_mut_silent().iter_mut() {
417 emitter.alive_particles = 0;
418 emitter.spawned_particles = 0;
419 }
420 }
421
422 pub fn set_material(&mut self, material: MaterialResource) -> MaterialResource {
424 self.material.set_value_and_mark_modified(material)
425 }
426
427 pub fn texture(&self) -> MaterialResource {
429 (*self.material).clone()
430 }
431
432 pub fn texture_ref(&self) -> &MaterialResource {
434 &self.material
435 }
436
437 fn tick(&mut self, dt: f32) {
438 for emitter in self.emitters.get_value_mut_silent().iter_mut() {
439 emitter.tick(dt);
440 }
441
442 let global_transform = self.global_transform();
443
444 for (i, emitter) in self.emitters.get_value_mut_silent().iter_mut().enumerate() {
445 for _ in 0..emitter.particles_to_spawn {
446 let mut particle = Particle {
447 emitter_index: i as u32,
448 ..Particle::default()
449 };
450 emitter.alive_particles += 1;
451 emitter.emit(&mut particle, &mut self.rng);
452 if *self.coordinate_system == CoordinateSystem::World {
453 particle.position = global_transform
454 .transform_point(&particle.position.into())
455 .coords;
456 }
457 if let Some(free_index) = self.free_particles.pop() {
458 self.particles[free_index as usize] = particle;
459 } else {
460 self.particles.push(particle);
461 }
462 }
463 }
464
465 let acceleration_offset = self.acceleration.scale(dt * dt);
466
467 for (i, particle) in self.particles.iter_mut().enumerate() {
468 if particle.alive {
469 particle.lifetime += dt;
470 if particle.lifetime >= particle.initial_lifetime {
471 self.free_particles.push(i as u32);
472 if let Some(emitter) = self
473 .emitters
474 .get_value_mut_and_mark_modified()
475 .get_mut(particle.emitter_index as usize)
476 {
477 emitter.alive_particles = emitter.alive_particles.saturating_sub(1);
478 }
479 particle.alive = false;
480 particle.lifetime = particle.initial_lifetime;
481 } else {
482 particle.velocity += acceleration_offset;
483 particle.position += particle.velocity;
484 particle.size += particle.size_modifier * dt;
485 if particle.size < 0.0 {
486 particle.size = 0.0;
487 }
488 particle.rotation += particle.rotation_speed * dt;
489
490 let k = particle.lifetime / particle.initial_lifetime;
491 particle.color = self.color_over_lifetime.get_color(k);
492 }
493 }
494 }
495 }
496
497 pub fn rewind(&mut self, dt: f32, time: f32) {
499 assert!(dt > 0.0);
500
501 self.rng.reset();
502 self.clear_particles();
503
504 let mut t = 0.0;
505 while t < time {
506 self.tick(dt);
507 t += dt;
508 }
509 }
510
511 pub fn set_visible_distance(&mut self, distance: f32) {
516 self.visible_distance.set_value_and_mark_modified(distance);
517 }
518
519 pub fn visible_distance(&self) -> f32 {
522 *self.visible_distance
523 }
524
525 pub fn set_coordinate_system(&mut self, coordinate_system: CoordinateSystem) {
528 self.coordinate_system
529 .set_value_and_mark_modified(coordinate_system);
530 }
531
532 pub fn coordinate_system(&self) -> CoordinateSystem {
534 *self.coordinate_system
535 }
536
537 fn is_distance_clipped(&self, point: &Vector3<f32>) -> bool {
538 point.metric_distance(&self.global_position())
539 > (*self.visible_distance + Self::FADEOUT_MARGIN)
540 }
541}
542
543impl Default for ParticleSystem {
544 fn default() -> Self {
545 ParticleSystemBuilder::new(BaseBuilder::new()).build_particle_system()
546 }
547}
548
549impl ConstructorProvider<Node, Graph> for ParticleSystem {
550 fn constructor() -> NodeConstructor {
551 NodeConstructor::new::<Self>().with_variant("Particle System", |_| {
552 ParticleSystemBuilder::new(BaseBuilder::new().with_name("ParticleSystem"))
553 .with_emitters(vec![SphereEmitterBuilder::new(
554 BaseEmitterBuilder::new()
555 .with_max_particles(100)
556 .resurrect_particles(true),
557 )
558 .with_radius(1.0)
559 .build()])
560 .build_node()
561 .into()
562 })
563 }
564}
565
566impl NodeTrait for ParticleSystem {
567 fn local_bounding_box(&self) -> AxisAlignedBoundingBox {
568 AxisAlignedBoundingBox::unit()
569 }
570
571 fn world_bounding_box(&self) -> AxisAlignedBoundingBox {
572 self.local_bounding_box()
573 .transform(&self.global_transform())
574 }
575
576 fn id(&self) -> Uuid {
577 Self::type_uuid()
578 }
579
580 fn update(&mut self, context: &mut UpdateContext) {
581 let dt = context.dt;
582
583 if *self.is_playing {
584 self.tick(dt);
585 }
586 }
587
588 fn collect_render_data(&self, ctx: &mut RenderContext) -> RdcControlFlow {
589 if !self.should_be_rendered(ctx.frustum)
590 || self.is_distance_clipped(&ctx.observer_info.observer_position)
591 {
592 return RdcControlFlow::Continue;
593 }
594
595 if renderer::is_shadow_pass(ctx.render_pass_name) && !self.cast_shadows() {
596 return RdcControlFlow::Continue;
597 }
598
599 let distance_to_observer = ctx
600 .observer_info
601 .observer_position
602 .metric_distance(&self.global_position());
603
604 let particle_alpha_factor = if distance_to_observer >= self.visible_distance() {
605 1.0 - (distance_to_observer - self.visible_distance()) / Self::FADEOUT_MARGIN
606 } else {
607 1.0
608 };
609
610 let mut sorted_particles = Vec::new();
611 for (i, particle) in self.particles.iter().enumerate() {
612 if particle.alive {
613 let actual_position = particle.position + self.base.global_position();
614 particle
615 .sqr_distance_to_camera
616 .set((ctx.observer_info.observer_position - actual_position).norm_squared());
617 sorted_particles.push(i as u32);
618 }
619 }
620
621 let particles = &self.particles;
622
623 sorted_particles.sort_by(|a, b| {
624 let particle_a = particles.get(*a as usize).unwrap();
625 let particle_b = particles.get(*b as usize).unwrap();
626
627 if particle_a.sqr_distance_to_camera < particle_b.sqr_distance_to_camera {
629 Ordering::Greater
630 } else if particle_a.sqr_distance_to_camera > particle_b.sqr_distance_to_camera {
631 Ordering::Less
632 } else {
633 Ordering::Equal
634 }
635 });
636
637 let global_transform = self.global_transform();
638 let sort_index = ctx.calculate_sorting_index(self.global_position());
639
640 ctx.storage.push_triangles(
641 Vertex::layout(),
642 &self.material,
643 RenderPath::Forward,
644 sort_index,
645 self.handle(),
646 &mut move |mut vertex_buffer, mut triangle_buffer| {
647 let vertices = sorted_particles.iter().flat_map(move |particle_index| {
648 let particle = self.particles.get(*particle_index as usize).unwrap();
649
650 let position = if *self.coordinate_system == CoordinateSystem::Local {
651 global_transform
652 .transform_point(&Point3::from(particle.position))
653 .coords
654 } else {
655 particle.position
656 };
657
658 let alpha = (particle.color.a as f32 * particle_alpha_factor) as u8;
659 let color = Color::from_rgba(
660 particle.color.r,
661 particle.color.g,
662 particle.color.b,
663 alpha,
664 );
665
666 [
667 Vertex {
668 position,
669 tex_coord: Vector2::default(),
670 size: particle.size,
671 rotation: particle.rotation,
672 color,
673 },
674 Vertex {
675 position,
676 tex_coord: Vector2::new(1.0, 0.0),
677 size: particle.size,
678 rotation: particle.rotation,
679 color,
680 },
681 Vertex {
682 position,
683 tex_coord: Vector2::new(1.0, 1.0),
684 size: particle.size,
685 rotation: particle.rotation,
686 color,
687 },
688 Vertex {
689 position,
690 tex_coord: Vector2::new(0.0, 1.0),
691 size: particle.size,
692 rotation: particle.rotation,
693 color,
694 },
695 ]
696 });
697
698 let triangles = (0..sorted_particles.len()).flat_map(|i| {
699 let base_index = (i * 4) as u32;
700
701 [
702 TriangleDefinition([base_index, base_index + 1, base_index + 2]),
703 TriangleDefinition([base_index, base_index + 2, base_index + 3]),
704 ]
705 });
706
707 let start_vertex_index = vertex_buffer.vertex_count();
708
709 for vertex in vertices {
710 vertex_buffer
711 .push_vertex_raw(value_as_u8_slice(&vertex))
712 .unwrap();
713 }
714
715 triangle_buffer.push_triangles_iter_with_offset(start_vertex_index, triangles)
716 },
717 );
718
719 RdcControlFlow::Continue
720 }
721}
722
723pub struct ParticleSystemBuilder {
726 base_builder: BaseBuilder,
727 emitters: Vec<Emitter>,
728 material: MaterialResource,
729 acceleration: Vector3<f32>,
730 particles: Vec<Particle>,
731 color_over_lifetime: ColorGradient,
732 is_playing: bool,
733 rng: ParticleSystemRng,
734 visible_distance: f32,
735 coordinate_system: CoordinateSystem,
736}
737
738impl ParticleSystemBuilder {
739 pub fn new(base_builder: BaseBuilder) -> Self {
741 Self {
742 base_builder,
743 emitters: Default::default(),
744 material: MaterialResource::new_ok(
745 Default::default(),
746 Material::standard_particle_system(),
747 ),
748 particles: Default::default(),
749 acceleration: Vector3::new(0.0, -9.81, 0.0),
750 color_over_lifetime: Default::default(),
751 is_playing: true,
752 rng: ParticleSystemRng::default(),
753 visible_distance: 30.0,
754 coordinate_system: Default::default(),
755 }
756 }
757
758 pub fn with_emitters(mut self, emitters: Vec<Emitter>) -> Self {
760 self.emitters = emitters;
761 self
762 }
763
764 pub fn with_material(mut self, material: MaterialResource) -> Self {
766 self.material = material;
767 self
768 }
769
770 pub fn with_acceleration(mut self, acceleration: Vector3<f32>) -> Self {
772 self.acceleration = acceleration;
773 self
774 }
775
776 pub fn with_visible_distance(mut self, distance: f32) -> Self {
779 self.visible_distance = distance;
780 self
781 }
782
783 pub fn with_color_over_lifetime_gradient(mut self, color_over_lifetime: ColorGradient) -> Self {
785 self.color_over_lifetime = color_over_lifetime;
786 self
787 }
788
789 pub fn with_particles(mut self, particles: Vec<Particle>) -> Self {
792 self.particles = particles;
793 self
794 }
795
796 pub fn with_playing(mut self, enabled: bool) -> Self {
798 self.is_playing = enabled;
799 self
800 }
801
802 pub fn with_rng(mut self, rng: ParticleSystemRng) -> Self {
804 self.rng = rng;
805 self
806 }
807
808 pub fn with_coordinate_system(mut self, coordinate_system: CoordinateSystem) -> Self {
810 self.coordinate_system = coordinate_system;
811 self
812 }
813
814 fn build_particle_system(self) -> ParticleSystem {
815 ParticleSystem {
816 base: self.base_builder.build_base(),
817 particles: self.particles,
818 free_particles: Vec::new(),
819 emitters: self.emitters.into(),
820 material: self.material.into(),
821 acceleration: self.acceleration.into(),
822 color_over_lifetime: self.color_over_lifetime.into(),
823 is_playing: self.is_playing.into(),
824 rng: self.rng,
825 visible_distance: self.visible_distance.into(),
826 coordinate_system: self.coordinate_system.into(),
827 }
828 }
829
830 pub fn build_node(self) -> Node {
832 Node::new(self.build_particle_system())
833 }
834
835 pub fn build(self, graph: &mut Graph) -> Handle<Node> {
837 graph.add_node(self.build_node())
838 }
839}