#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
#![doc = include_str!("../README.md")]
use std::{
any::Any,
fmt::Debug,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use bevy::{
app::{Plugin, PostUpdate, Update},
asset::Assets,
color::{ColorToComponents, Srgba},
ecs::query::QueryItem,
math::{Quat, Vec3},
prelude::{Commands, Component, Entity, IntoSystemConfigs, Query, Res},
render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_resource::Shader,
Render, RenderApp, RenderSet,
},
time::Time,
transform::{
components::{GlobalTransform, Transform},
systems::{propagate_transforms, sync_simple_transforms},
},
};
use noop::NoopParticleSystem;
mod material;
pub use material::*;
mod pipeline;
use pipeline::InstanceBuffer;
pub use pipeline::ParticleMaterialPlugin;
pub mod shader;
mod sub;
pub use sub::*;
mod buffer;
pub mod trail;
pub mod util;
pub use buffer::*;
use trail::{trail_rendering, TrailParticleSystem};
mod noop;
mod ring_buffer;
pub use ring_buffer::RingBuffer;
mod billboard;
pub use billboard::*;
pub struct ParticlePlugin;
impl Plugin for ParticlePlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.world_mut().resource_mut::<Assets<Shader>>().insert(
&shader::PARTICLE_VERTEX,
Shader::from_wgsl(shader::SHADER_VERTEX, "berdicle/particle_vertex.wgsl"),
);
app.world_mut().resource_mut::<Assets<Shader>>().insert(
&shader::PARTICLE_FRAGMENT,
Shader::from_wgsl(shader::SHADER_FRAGMENT, "berdicle/particle_fragment.wgsl"),
);
app.world_mut().resource_mut::<Assets<Shader>>().insert(
&shader::PARTICLE_DBG_FRAGMENT,
Shader::from_wgsl(shader::SHADER_DBG, "berdicle/particle_dbg_fragment.wgsl"),
);
app.add_plugins(ExtractComponentPlugin::<ExtractedParticleBuffer>::default());
app.add_plugins(ExtractComponentPlugin::<ParticleRef>::default());
app.add_plugins(ParticleMaterialPlugin::<StandardParticle>::default());
app.add_plugins(ParticleMaterialPlugin::<DebugParticle>::default());
app.add_systems(Update, particle_system);
app.add_systems(Update, trail_rendering.after(particle_system));
app.add_systems(
PostUpdate,
billboard_system
.after(propagate_transforms)
.after(sync_simple_transforms),
);
app.sub_app_mut(RenderApp).add_systems(
Render,
particle_ref_system.in_set(RenderSet::PrepareResourcesFlush),
);
}
}
pub fn particle_system(
time: Res<Time>,
mut particles: Query<(
Entity,
&mut ParticleInstance,
&mut ParticleBuffer,
Option<&mut ParticleEventBuffer>,
Option<&ParticleParent>,
)>,
) {
let dt = time.delta_seconds();
particles
.par_iter_mut()
.for_each(|(_, mut system, mut buffer, events, _)| {
if buffer.is_uninit() {
*buffer = system.spawn_particle_buffer();
}
if let Some(mut events) = events {
events.clear();
system.update_with_event_buffer(dt, &mut buffer, &mut events);
} else {
system.update(dt, &mut buffer);
}
});
for (entity, mut system, mut buffer, _, parent) in unsafe { particles.iter_unsafe() } {
let Some(ParticleParent(parent)) = parent else {
continue;
};
if entity == *parent {
panic!("ParticleSystem's parent cannot be itself.")
}
if let Some(sub) = system.as_sub_particle_system() {
let Ok((_, _, mut parent, _, _)) = (unsafe { particles.get_unchecked(*parent) }) else {
continue;
};
sub.spawn_from_parent(dt, &mut buffer, &mut parent);
}
if let Some(sub) = system.as_event_particle_system() {
let Ok((_, _, _, Some(parent), _)) = particles.get(*parent) else {
continue;
};
sub.spawn_on_event(&mut buffer, parent);
}
}
}
fn sort_unstable<T>(buf: &mut [T], mut key: impl FnMut(&T) -> bool) {
if buf.len() < 2 {
return;
}
let mut start = 0;
let mut end = buf.len() - 1;
while start < end {
if key(&buf[start]) {
while key(&buf[end]) && end > 0 {
end -= 1;
}
if start < end {
buf.swap(start, end)
}
}
start += 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpirationState {
None,
Fizzle,
Explode,
}
impl ExpirationState {
pub const fn is_expired(&self) -> bool {
!matches!(self, Self::None)
}
pub const fn fizzle_if(&self, cond: bool) -> Self {
if cond {
Self::Fizzle
} else {
Self::None
}
}
pub const fn explode_if(&self, cond: bool) -> Self {
if cond {
Self::Explode
} else {
Self::None
}
}
}
pub trait Particle: Copy + 'static {
fn get_seed(&self) -> f32;
fn get_index(&self) -> u32 {
0
}
fn get_lifetime(&self) -> f32;
fn get_fac(&self) -> f32 {
self.get_lifetime()
}
fn get_transform(&self) -> Transform;
fn get_position(&self) -> Vec3 {
self.get_transform().translation
}
fn get_tangent(&self) -> Vec3 {
self.get_transform().forward().as_vec3()
}
fn get_color(&self) -> Srgba {
Srgba::WHITE
}
fn update(&mut self, dt: f32);
fn update_with_event_buffer(&mut self, dt: f32, buffer: &mut ParticleEventBuffer) {
if self.is_expired() {
return;
}
self.update(dt);
let expr = self.expiration_state();
if expr.is_expired() {
buffer.push(ParticleEvent {
event: expr.into(),
seed: self.get_seed(),
lifetime: self.get_lifetime(),
position: self.get_position(),
tangent: self.get_tangent(),
})
}
}
fn expiration_state(&self) -> ExpirationState;
fn is_expired(&self) -> bool {
self.expiration_state().is_expired()
}
}
pub trait ParticleSystem {
const WORLD_SPACE: bool = false;
const STRATEGY: ParticleBufferStrategy = ParticleBufferStrategy::Retain;
type Particle: Particle;
fn as_debug(&self) -> &dyn Debug {
#[derive(Debug)]
pub struct ErasedParticleSystem;
&ErasedParticleSystem
}
fn capacity(&self) -> usize;
fn rng(&mut self) -> f32 {
fastrand::f32()
}
fn spawn_step(&mut self, time: f32) -> usize;
fn build_particle(&self, seed: f32) -> Self::Particle;
#[allow(unused_variables)]
fn on_update(&mut self, dt: f32, buffer: &mut ParticleBuffer) {}
#[allow(unused_variables)]
fn detach_slice(&mut self, detached: Range<usize>, buffer: &mut ParticleBuffer) {}
#[allow(unused_variables)]
fn apply_meta(&mut self, command: &dyn Any) {}
fn as_sub_particle_system(&mut self) -> Option<&mut dyn ErasedSubParticleSystem> {
None
}
fn as_event_particle_system(&mut self) -> Option<&mut dyn ErasedEventParticleSystem> {
None
}
fn as_trail_particle_system(&mut self) -> Option<&mut dyn TrailParticleSystem> {
None
}
}
pub trait ErasedParticleSystem: Send + Sync {
fn as_debug(&self) -> &dyn Debug;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn is_world_space(&self) -> bool;
fn update(&mut self, dt: f32, buffer: &mut ParticleBuffer);
fn update_with_event_buffer(
&mut self,
dt: f32,
buffer: &mut ParticleBuffer,
events: &mut ParticleEventBuffer,
);
fn spawn_particle_buffer(&self) -> ParticleBuffer;
fn apply_meta(&mut self, command: &dyn Any);
fn extract(
&self,
buffer: &ParticleBuffer,
transform: &GlobalTransform,
billboard: Option<Quat>,
vec: &mut Vec<ExtractedParticle>,
);
fn as_sub_particle_system(&mut self) -> Option<&mut dyn ErasedSubParticleSystem>;
fn as_event_particle_system(&mut self) -> Option<&mut dyn ErasedEventParticleSystem>;
fn as_trail_particle_system(&mut self) -> Option<&mut dyn TrailParticleSystem>;
}
#[derive(Debug, Component)]
pub struct ParticleInstance(Box<dyn ErasedParticleSystem>);
impl Default for ParticleInstance {
fn default() -> Self {
ParticleInstance::new(NoopParticleSystem)
}
}
impl ParticleInstance {
pub fn new<P: ParticleSystem + Send + Sync + 'static>(particles: P) -> Self {
Self(Box::new(particles))
}
pub fn downcast_ref<P: ParticleSystem + Send + Sync + 'static>(&self) -> Option<&P> {
self.0.as_any().downcast_ref()
}
pub fn downcast_mut<P: ParticleSystem + Send + Sync + 'static>(&mut self) -> Option<&mut P> {
self.0.as_any_mut().downcast_mut()
}
}
impl Deref for ParticleInstance {
type Target = dyn ErasedParticleSystem;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl DerefMut for ParticleInstance {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut()
}
}
fn spawn_particle<T: ParticleSystem>(particles: &mut T) -> T::Particle {
let seed = particles.rng();
particles.build_particle(seed)
}
impl<T> ErasedParticleSystem for T
where
T: ParticleSystem + Send + Sync + 'static,
{
fn as_debug(&self) -> &dyn Debug {
ParticleSystem::as_debug(self)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn is_world_space(&self) -> bool {
T::WORLD_SPACE
}
fn update(&mut self, dt: f32, buffer: &mut ParticleBuffer) {
match Self::STRATEGY {
ParticleBufferStrategy::Retain => {
let original_len = buffer.len;
let buf = buffer.get_mut::<T::Particle>();
let mut len = 0;
for item in buf.iter_mut() {
item.update(dt);
len += (!item.is_expired()) as usize
}
if len != original_len {
sort_unstable(buf, |x| x.is_expired());
self.detach_slice(len..original_len, buffer)
}
buffer.len = len;
buffer.extend((0..self.spawn_step(dt)).map(|_| spawn_particle(self)))
}
ParticleBufferStrategy::RingBuffer => {
let buf = buffer.get_mut::<T::Particle>();
let mut len = 0;
for item in buf {
item.update(dt);
len += (!item.is_expired()) as usize
}
buffer.len = len;
buffer.extend((0..self.spawn_step(dt)).map(|_| spawn_particle(self)))
}
}
self.on_update(dt, buffer)
}
fn update_with_event_buffer(
&mut self,
dt: f32,
buffer: &mut ParticleBuffer,
events: &mut ParticleEventBuffer,
) {
match Self::STRATEGY {
ParticleBufferStrategy::Retain => {
let original_len = buffer.len;
let buf = buffer.get_mut::<T::Particle>();
let mut len = 0;
for item in buf.iter_mut() {
item.update_with_event_buffer(dt, events);
len += (!item.is_expired()) as usize
}
if len != original_len {
sort_unstable(buf, |x| x.is_expired());
self.detach_slice(len..original_len, buffer)
}
buffer.len = len;
buffer.extend((0..self.spawn_step(dt)).map(|_| spawn_particle(self)))
}
ParticleBufferStrategy::RingBuffer => {
let buf = buffer.get_mut::<T::Particle>();
let mut len = 0;
for item in buf {
item.update_with_event_buffer(dt, events);
len += (!item.is_expired()) as usize
}
buffer.len = len;
buffer.extend((0..self.spawn_step(dt)).map(|_| spawn_particle(self)))
}
}
self.on_update(dt, buffer)
}
fn spawn_particle_buffer(&self) -> ParticleBuffer {
match Self::STRATEGY {
ParticleBufferStrategy::Retain => {
ParticleBuffer::new_retain::<T::Particle>(self.capacity())
}
ParticleBufferStrategy::RingBuffer => {
ParticleBuffer::new_ring::<T::Particle>(self.capacity())
}
}
}
fn apply_meta(&mut self, command: &dyn Any) {
ParticleSystem::apply_meta(self, command)
}
#[allow(clippy::collapsible_else_if)]
fn extract(
&self,
buffer: &ParticleBuffer,
transform: &GlobalTransform,
billboard: Option<Quat>,
vec: &mut Vec<ExtractedParticle>,
) {
vec.clear();
vec.extend(
buffer
.get::<T::Particle>()
.iter()
.filter(|x| !x.is_expired())
.map(|x| {
let transform = if let Some(bb) = billboard {
if T::WORLD_SPACE {
x.get_transform().with_rotation(bb).compute_matrix()
} else {
let (scale, _, translation) = transform
.mul_transform(x.get_transform())
.to_scale_rotation_translation();
Transform {
translation,
rotation: bb,
scale,
}
.compute_matrix()
}
} else {
if T::WORLD_SPACE {
x.get_transform().compute_matrix()
} else {
transform.mul_transform(x.get_transform()).compute_matrix()
}
};
ExtractedParticle {
index: x.get_index(),
lifetime: x.get_lifetime(),
seed: x.get_seed(),
fac: x.get_fac(),
color: x.get_color().to_vec4(),
transform_x: transform.row(0),
transform_y: transform.row(1),
transform_z: transform.row(2),
}
}),
)
}
fn as_sub_particle_system(&mut self) -> Option<&mut dyn ErasedSubParticleSystem> {
ParticleSystem::as_sub_particle_system(self)
}
fn as_event_particle_system(&mut self) -> Option<&mut dyn ErasedEventParticleSystem> {
ParticleSystem::as_event_particle_system(self)
}
fn as_trail_particle_system(&mut self) -> Option<&mut dyn TrailParticleSystem> {
ParticleSystem::as_trail_particle_system(self)
}
}
impl Debug for dyn ErasedParticleSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_debug().fmt(f)
}
}
impl ExtractComponent for ExtractedParticleBuffer {
type QueryData = (
&'static ParticleInstance,
&'static ParticleBuffer,
&'static GlobalTransform,
Option<&'static BillboardParticle>,
);
type QueryFilter = ();
type Out = ExtractedParticleBuffer;
fn extract_component(
(system, buffer, transform, billboard): QueryItem<'_, Self::QueryData>,
) -> Option<Self::Out> {
let mut lock = buffer.extracted_allocation.lock().unwrap();
if let Some(vec) = Arc::get_mut(&mut lock) {
system.extract(buffer, transform, billboard.map(|x| x.0), vec);
Some(ExtractedParticleBuffer(lock.clone()))
} else {
None
}
}
}
impl ExtractComponent for ParticleRef {
type QueryData = &'static ParticleRef;
type QueryFilter = ();
type Out = ParticleRef;
fn extract_component(r: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
Some(*r)
}
}
#[derive(Debug, Component, Clone, Copy)]
pub struct ParticleRef(pub Entity);
impl Default for ParticleRef {
fn default() -> Self {
ParticleRef(Entity::PLACEHOLDER)
}
}
fn particle_ref_system(
mut commands: Commands,
particles: Query<&InstanceBuffer>,
query: Query<(Entity, &ParticleRef)>,
) {
for (entity, ParticleRef(parent)) in &query {
if let Ok(buffer) = particles.get(*parent) {
commands.entity(entity).insert(buffer.clone());
}
}
}