Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPU Particle Systems #3504

Closed
wants to merge 11 commits into from
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dynamic = ["bevy_dylib"]
# Rendering support
render = [
"bevy_internal/bevy_core_pipeline",
"bevy_internal/bevy_particles",
"bevy_internal/bevy_pbr",
"bevy_internal/bevy_gltf",
"bevy_internal/bevy_render",
Expand All @@ -45,6 +46,7 @@ render = [
# Optional bevy crates
bevy_audio = ["bevy_internal/bevy_audio"]
bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"]
bevy_particles = ["bevy_internal/bevy_particles"]
bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"]
bevy_gilrs = ["bevy_internal/bevy_gilrs"]
bevy_gltf = ["bevy_internal/bevy_gltf"]
Expand Down Expand Up @@ -244,6 +246,11 @@ path = "examples/app/thread_pool_resources.rs"
name = "without_winit"
path = "examples/app/without_winit.rs"

# Particle Systems
[[example]]
name = "particles-fireball"
path = "examples/particles/fireball.rs"

# Assets
[[example]]
name = "asset_loading"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.5.0" }
bevy_render = { path = "../bevy_render", optional = true, version = "0.5.0" }
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.5.0" }
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.5.0" }
bevy_particles = { path = "../bevy_particles", optional = true, version = "0.5.0" }
bevy_text = { path = "../bevy_text", optional = true, version = "0.5.0" }
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.5.0" }
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.5.0" }
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_internal/src/default_plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ impl PluginGroup for DefaultPlugins {

#[cfg(feature = "bevy_gilrs")]
group.add(bevy_gilrs::GilrsPlugin::default());

#[cfg(feature = "bevy_particles")]
group.add(bevy_particles::ParticlePlugin::default());
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_internal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ pub mod gltf {
pub use bevy_gltf::*;
}

#[cfg(feature = "bevy_particles")]
pub mod particles {
//! Particle systems.
pub use bevy_particles::*;
}

#[cfg(feature = "bevy_pbr")]
pub mod pbr {
//! Physically based rendering.
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_internal/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ pub use crate::dynamic_plugin::*;
#[doc(hidden)]
#[cfg(feature = "bevy_gilrs")]
pub use crate::gilrs::*;

#[doc(hidden)]
#[cfg(feature = "bevy_particles")]
pub use crate::particles::prelude::*;
30 changes: 30 additions & 0 deletions crates/bevy_particles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "bevy_particles"
version = "0.5.0"
edition = "2021"
description = "Adds particle systems to Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[dependencies]
rand = { version = "0.8", features = ["small_rng"] }
wgpu = "0.12"
bytemuck = { version = "1.7.0", features = ["derive"] }
bitflags = "1.2"

bevy_app = { path = "../bevy_app", version = "0.5.0" }
bevy_asset = { path = "../bevy_asset", version = "0.5.0" }
bevy_core = { path = "../bevy_core", version = "0.5.0" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.5.0" }
bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" }
bevy_math = { path = "../bevy_math", version = "0.5.0" }
bevy_reflect = { path = "../bevy_reflect", version = "0.5.0" }
bevy_render = { path = "../bevy_render", version = "0.5.0" }
bevy_tasks = { path = "../bevy_tasks", version = "0.5.0" }
bevy_transform = { path = "../bevy_transform", version = "0.5.0" }

[features]
default = []
webgl2 = []
204 changes: 204 additions & 0 deletions crates/bevy_particles/src/emitter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use crate::particles::{ParticleParams, Particles};
use bevy_core::Time;
use bevy_ecs::prelude::*;
use bevy_math::*;
use bevy_render::color::Color;
use bevy_tasks::ComputeTaskPool;
use bevy_transform::prelude::*;
use rand::Rng;
use std::{ops::Range, time::Duration};

#[derive(Debug, Clone)]
pub struct EmitterBurst {
pub count: Range<usize>,
pub wait: Duration,
}

pub trait EmitterModifier: Send + Sync + 'static {
fn modify(&mut self, particle: &mut ParticleParams);
}

#[derive(Component)]
pub struct ParticleEmitter {
next_burst: Duration,
burst_idx: usize,
default_params: ParticleParams,
default_speed: f32,
bursts: Vec<EmitterBurst>,
shape: EmitterShape,
modifiers: Vec<Box<dyn EmitterModifier>>,
}

impl ParticleEmitter {
pub fn sphere(center: Vec3, radius: f32) -> ParticleEmitterBuilder {
ParticleEmitterBuilder::new(EmitterShape::Sphere { center, radius })
}

pub fn hemisphere(center: Vec3, radius: f32) -> ParticleEmitterBuilder {
ParticleEmitterBuilder::new(EmitterShape::Hemisphere { center, radius })
}
}

pub struct ParticleEmitterBuilder {
default_params: ParticleParams,
default_speed: f32,
bursts: Vec<EmitterBurst>,
shape: EmitterShape,
modifiers: Vec<Box<dyn EmitterModifier>>,
}

impl ParticleEmitterBuilder {
fn new(shape: EmitterShape) -> Self {
Self {
default_params: ParticleParams {
size: 1.0,
color: Color::WHITE,
lifetime: 5.0,
..Default::default()
},
default_speed: 0.0,
bursts: Vec::new(),
shape,
modifiers: Vec::new(),
}
}

pub fn add_burst(mut self, burst: EmitterBurst) -> Self {
self.bursts.push(burst);
self
}

pub fn add_modifier(mut self, modifier: impl EmitterModifier) -> Self {
self.modifiers.push(Box::new(modifier));
self
}

pub fn with_default_speed(mut self, speed: f32) -> Self {
self.default_speed = speed;
self
}

pub fn with_default_color(mut self, color: Color) -> Self {
self.default_params.color = color;
self
}

pub fn with_default_lifetime(mut self, lifetime: f32) -> Self {
self.default_params.lifetime = lifetime;
self
}

pub fn with_default_size(mut self, size: f32) -> Self {
self.default_params.size = size;
self
}

pub fn build(self) -> ParticleEmitter {
ParticleEmitter {
next_burst: Duration::from_millis(0),
burst_idx: 0,
default_params: self.default_params,
default_speed: self.default_speed,
bursts: self.bursts,
shape: self.shape,
modifiers: self.modifiers,
}
}
}

pub enum EmitterShape {
Sphere { center: Vec3, radius: f32 },
Hemisphere { center: Vec3, radius: f32 },
}

impl EmitterShape {
pub fn sample(&self, rng: &mut impl Rng, params: &mut ParticleParams) {
match self {
Self::Sphere { radius, center } => Self::sample_sphere(*center, *radius, rng, params),
Self::Hemisphere { radius, center } => {
Self::sample_hemisphere(*center, *radius, rng, params)
}
}
}

fn sample_sphere(center: Vec3, radius: f32, rng: &mut impl Rng, params: &mut ParticleParams) {
let position = sample_sphere(rng);
let r = rng.gen_range(0.0..1.0);
params.position = position * r * radius + center;
params.velocity = position;
}

fn sample_hemisphere(
center: Vec3,
radius: f32,
rng: &mut impl Rng,
params: &mut ParticleParams,
) {
let mut position = sample_sphere(rng);
position.y = f32::abs(position.y);
let r = rng.gen_range(0.0..1.0);
params.position = position * r * radius + center;
params.velocity = position;
}
}

pub fn emit_particles(
time: Res<Time>,
compute_task_pool: Res<ComputeTaskPool>,
mut particles: Query<(&mut ParticleEmitter, &mut Particles, &GlobalTransform)>,
) {
let delta_time = time.delta();
particles.par_for_each_mut(
&compute_task_pool,
8,
|(mut emitter, mut particles, transform)| {
if !particles.state().is_playing() {
return;
}

let mut remaining = delta_time;
let mut rng = rand::thread_rng();
let mut total = 0;
while remaining > emitter.next_burst {
let EmitterBurst { count, wait } = emitter.bursts[emitter.burst_idx].clone();
let exact_count = rng.gen_range(count);
total += exact_count;

remaining -= emitter.next_burst;

emitter.next_burst = wait;
emitter.burst_idx = (emitter.burst_idx + 1) % emitter.bursts.len();
}

emitter.next_burst -= remaining;

if total > 0 {
let local_to_world = transform.compute_matrix();
let target_capacity = particles.len() + total;
particles.reserve(target_capacity);
for _ in 0..total {
let mut params = emitter.default_params.clone();
emitter.shape.sample(&mut rng, &mut params);
params.velocity *= emitter.default_speed;
params.position = local_to_world.transform_point3(params.position);
params.velocity = local_to_world.transform_vector3(params.velocity);
for modifier in emitter.modifiers.iter_mut() {
modifier.modify(&mut params);
}
particles.spawn(params);
}
}
},
);
}

/// Select one point at random on the unit sphere.
fn sample_sphere(rng: &mut impl Rng) -> Vec3 {
const TWO_PI: f32 = std::f32::consts::PI * 2.0;
let theta = rng.gen_range(0.0..TWO_PI);
let z = rng.gen_range(-1.0..1.0);
let x = f32::sqrt(1.0 - z * z) * f32::cos(theta);
let y = f32::sqrt(1.0 - z * z) * f32::sin(theta);

Vec3::from((x, y, z))
}
66 changes: 66 additions & 0 deletions crates/bevy_particles/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_render::{
prelude::{ComputedVisibility, Visibility},
primitives::Aabb,
view::VisibilitySystems,
};
use bevy_transform::prelude::{GlobalTransform, Transform};

pub mod emitter;
pub mod material;
pub mod modifiers;
mod particles;
pub mod prelude;
mod render;

pub use emitter::*;
pub use material::*;
use modifiers::*;
pub use particles::*;
use render::ParticleRenderPlugin;

#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemLabel)]
pub struct ParticleUpdate;

#[derive(Default)]
pub struct ParticlePlugin;

impl Plugin for ParticlePlugin {
fn build(&self, app: &mut App) {
app.add_plugin(ParticleMaterialPlugin)
.add_plugin(ParticleRenderPlugin)
.add_system(particles::update_particles.label(ParticleUpdate))
.add_system(emitter::emit_particles.after(ParticleUpdate))
.add_system_to_stage(
CoreStage::PostUpdate,
particles::compute_particles_aabb.label(VisibilitySystems::CalculateBounds),
)
.add_system_to_stage(
CoreStage::PostUpdate,
particles::cull_particles.after(VisibilitySystems::CheckVisibility),
)
.register_particle_modifier::<ConstantForce>();
}
}

pub trait ParticleModifierAppExt {
fn register_particle_modifier<T: ParticleModifier>(&mut self) -> &mut Self;
}

impl ParticleModifierAppExt for App {
fn register_particle_modifier<T: ParticleModifier>(&mut self) -> &mut Self {
self.add_system(modifiers::apply_particle_modifier::<T>.before(ParticleUpdate));
self
}
}

#[derive(Bundle, Default)]
pub struct ParticleBundle {
pub particles: Particles,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub aabb: Aabb,
pub visibility: Visibility,
pub computed_visibility: ComputedVisibility,
}
Loading