#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
#![doc = include_str!("../README.md")]
use std::{
any::Any,
fmt::Debug,
ops::{Deref, DerefMut},
};
use bevy::{
app::{Plugin, Update},
asset::Assets,
color::Srgba,
math::Vec3,
pbr::MaterialPlugin,
prelude::{Component, DetectChanges, Entity, IntoSystemConfigs, Query, Ref, Res, Visibility},
render::{render_resource::Shader, ExtractSchedule, Render, RenderApp, RenderSet},
time::{Time, Virtual},
transform::components::{GlobalTransform, Transform},
};
use despawn::despawn_projectiles;
use noop::NoopParticleSystem;
mod extract;
pub(crate) use extract::*;
pub use extract::{HairParticles, ProjectileRef};
mod material;
pub use material::*;
mod pipeline;
pub use pipeline::InstancedMaterialPlugin;
use pipeline::{prepare_instance_buffers, prepare_transforms};
pub mod shader;
mod sub;
pub use sub::*;
mod buffer;
pub mod trail;
pub mod util;
pub use buffer::*;
use trail::{trail_rendering, TrailMaterial, TrailMeshBuilder};
mod despawn;
mod noop;
pub use despawn::DespawnProjectileCluster;
pub mod templates;
pub struct ProjectilePlugin;
impl Plugin for ProjectilePlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.world_mut().resource_mut::<Assets<Shader>>().insert(
&shader::PARTICLE_VERTEX,
Shader::from_wgsl(
include_str!("./shader.wgsl"),
"berdicle/particle_vertex.wgsl",
),
);
app.world_mut().resource_mut::<Assets<Shader>>().insert(
&shader::PARTICLE_FRAGMENT,
Shader::from_wgsl(
include_str!("./shader.wgsl"),
"berdicle/particle_fragment.wgsl",
),
);
app.world_mut().resource_mut::<Assets<Shader>>().insert(
&shader::TRAIL_VERTEX,
Shader::from_wgsl(
include_str!("./trail_vertex.wgsl"),
"berdicle/trail_vertex.wgsl",
),
);
app.add_plugins(MaterialPlugin::<TrailMaterial>::default());
app.add_plugins(InstancedMaterialPlugin::<StandardParticle>::default());
app.add_systems(Update, projectile_simulation_system);
app.add_systems(Update, trail_rendering.after(projectile_simulation_system));
app.add_systems(
Update,
despawn_projectiles.after(projectile_simulation_system),
);
app.sub_app_mut(RenderApp)
.add_systems(ExtractSchedule, (extract_clean, extract_buffers).chain())
.add_systems(
Render,
(prepare_transforms, prepare_instance_buffers).in_set(RenderSet::PrepareResources),
);
}
}
pub fn projectile_simulation_system(
time: Res<Time<Virtual>>,
mut particles: Query<(
Entity,
&mut ProjectileCluster,
&mut ProjectileBuffer,
Ref<GlobalTransform>,
Option<&mut ProjectileEventBuffer>,
Option<&ProjectileParent>,
)>,
) {
let dt = time.delta_secs();
particles
.par_iter_mut()
.for_each(|(_, mut system, mut buffer, transform, events, _)| {
if buffer.is_uninit() {
*buffer = system.spawn_particle_buffer();
}
if transform.is_changed() && system.is_world_space() {
system.update_position(&transform)
}
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(ProjectileParent(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,
FadeOut,
Explode,
}
impl ExpirationState {
pub const fn is_expired(&self) -> bool {
!matches!(self, Self::None)
}
pub const fn fizzle_if(cond: bool) -> Self {
if cond {
Self::FadeOut
} else {
Self::None
}
}
pub const fn explode_if(cond: bool) -> Self {
if cond {
Self::Explode
} else {
Self::None
}
}
}
pub trait Projectile: Copy + 'static {
fn get_seed(&self) -> f32 {
0.
}
fn get_index(&self) -> u32 {
0
}
fn get_lifetime(&self) -> f32 {
0.
}
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 ProjectileEventBuffer) {
let is_expired = self.is_expired();
self.update(dt);
if is_expired {
return;
}
let expr = self.expiration_state();
if expr.is_expired() {
buffer.push(ProjectileEvent {
event: expr.into(),
seed: self.get_seed(),
index: self.get_index(),
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()
}
fn should_despawn(&self) -> bool {
self.expiration_state().is_expired()
}
fn trail(&self) -> &[(Vec3, f32)] {
&[]
}
fn extract(&self) -> impl ProjectileInstanceBuffer {
DefaultInstanceBuffer::from(self)
}
}
#[allow(unused_variables)]
pub trait ProjectileSystem {
const WORLD_SPACE: bool = false;
const STRATEGY: ParticleBufferStrategy = ParticleBufferStrategy::Retain;
type Projectile: Projectile;
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::Projectile;
fn on_update(&mut self, dt: f32, buffer: &mut ProjectileBuffer) {}
#[allow(unused_variables)]
fn apply_meta(&mut self, command: &dyn Any, buffer: &mut ProjectileBuffer) {}
#[allow(unused_variables)]
fn update_position(&mut self, transform: &GlobalTransform) {}
fn as_sub_particle_system(&mut self) -> Option<&mut dyn ErasedSubParticleSystem> {
None
}
fn as_event_particle_system(&mut self) -> Option<&mut dyn ErasedEventParticleSystem> {
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 ProjectileBuffer);
fn update_with_event_buffer(
&mut self,
dt: f32,
buffer: &mut ProjectileBuffer,
events: &mut ProjectileEventBuffer,
);
fn spawn_particle_buffer(&self) -> ProjectileBuffer;
#[allow(unused_variables)]
fn update_position(&mut self, transform: &GlobalTransform);
fn render_trail(&self, buffer: &ProjectileBuffer, trail: &mut TrailMeshBuilder);
fn apply_meta(&mut self, command: &dyn Any, buffer: &mut ProjectileBuffer);
fn extract(&self, buffer: &ProjectileBuffer, vec: &mut ErasedExtractBuffer);
fn as_sub_particle_system(&mut self) -> Option<&mut dyn ErasedSubParticleSystem>;
fn as_event_particle_system(&mut self) -> Option<&mut dyn ErasedEventParticleSystem>;
fn should_despawn(&self, buffer: &ProjectileBuffer) -> bool;
}
#[derive(Debug, Component)]
#[require(ProjectileBuffer, Transform, Visibility)]
pub struct ProjectileCluster(Box<dyn ErasedParticleSystem>);
impl Default for ProjectileCluster {
fn default() -> Self {
ProjectileCluster::new(NoopParticleSystem)
}
}
impl ProjectileCluster {
pub fn new<P: ProjectileSystem + Send + Sync + 'static>(particles: P) -> Self {
Self(Box::new(particles))
}
pub fn downcast_ref<P: ProjectileSystem + Send + Sync + 'static>(&self) -> Option<&P> {
self.0.as_any().downcast_ref()
}
pub fn downcast_mut<P: ProjectileSystem + Send + Sync + 'static>(&mut self) -> Option<&mut P> {
self.0.as_any_mut().downcast_mut()
}
}
impl Deref for ProjectileCluster {
type Target = dyn ErasedParticleSystem;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl DerefMut for ProjectileCluster {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut()
}
}
fn spawn_particle<T: ProjectileSystem>(particles: &mut T) -> T::Projectile {
let seed = particles.rng();
particles.build_particle(seed)
}
impl<T> ErasedParticleSystem for T
where
T: ProjectileSystem + Send + Sync + 'static,
{
fn as_debug(&self) -> &dyn Debug {
ProjectileSystem::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 ProjectileBuffer) {
match Self::STRATEGY {
ParticleBufferStrategy::Retain => {
let original_len = buffer.len;
let buf = buffer.get_mut::<T::Projectile>();
let mut len = 0;
for item in buf.iter_mut() {
item.update(dt);
len += (!item.should_despawn()) as usize
}
if len != original_len {
sort_unstable(buf, |x| x.should_despawn());
}
buffer.len = len;
buffer.extend((0..self.spawn_step(dt)).map(|_| spawn_particle(self)))
}
ParticleBufferStrategy::RingBuffer => {
let buf = buffer.get_mut::<T::Projectile>();
let mut len = 0;
for item in buf {
item.update(dt);
len += (!item.should_despawn()) 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 ProjectileBuffer,
events: &mut ProjectileEventBuffer,
) {
match Self::STRATEGY {
ParticleBufferStrategy::Retain => {
let original_len = buffer.len;
let buf = buffer.get_mut::<T::Projectile>();
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());
}
buffer.len = len;
buffer.extend((0..self.spawn_step(dt)).map(|_| spawn_particle(self)))
}
ParticleBufferStrategy::RingBuffer => {
let buf = buffer.get_mut::<T::Projectile>();
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) -> ProjectileBuffer {
match Self::STRATEGY {
ParticleBufferStrategy::Retain => {
ProjectileBuffer::new_retain::<T::Projectile>(self.capacity())
}
ParticleBufferStrategy::RingBuffer => {
ProjectileBuffer::new_ring::<T::Projectile>(self.capacity())
}
}
}
fn update_position(&mut self, transform: &GlobalTransform) {
ProjectileSystem::update_position(self, transform)
}
fn apply_meta(&mut self, command: &dyn Any, buffer: &mut ProjectileBuffer) {
ProjectileSystem::apply_meta(self, command, buffer)
}
fn extract(&self, buffer: &ProjectileBuffer, extract: &mut ErasedExtractBuffer) {
let mut count = 0;
extract.bytes.clear();
buffer
.get::<T::Projectile>()
.iter()
.filter(|x| !x.is_expired())
.for_each(|x| {
count += 1;
extract.bytes.extend(bytemuck::bytes_of(&x.extract()));
});
extract.len = count;
}
fn as_sub_particle_system(&mut self) -> Option<&mut dyn ErasedSubParticleSystem> {
ProjectileSystem::as_sub_particle_system(self)
}
fn as_event_particle_system(&mut self) -> Option<&mut dyn ErasedEventParticleSystem> {
ProjectileSystem::as_event_particle_system(self)
}
fn render_trail(&self, buffer: &ProjectileBuffer, trail: &mut TrailMeshBuilder) {
buffer
.get::<T::Projectile>()
.iter()
.for_each(|x| trail.build_plane(x.trail().iter().copied(), 0.0..1.0))
}
fn should_despawn(&self, buffer: &ProjectileBuffer) -> bool {
buffer.len == 0
}
}
impl Debug for dyn ErasedParticleSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_debug().fmt(f)
}
}