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

Add basic support for particle trails. #288

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added a new `ScreenSpaceSizeModifier` which negates the effect of perspective projection, and makes the particle's size a pixel size in screen space, instead of a Bevy world unit size. This replaces the hard-coded behavior previously available on the `SetSizeModifier`.
- Added a new `ConformToSphereModifier` acting as an attractor applying a force toward a point (sphere center) to all particles in range, and making particles conform ("stick") to the sphere surface.
- Added `vec2` and `vec3` functions that allow construction of vectors from dynamic parts.
- Added basic support for particle trails. To use them, replace calls to `EffectAsset::new()` with `EffectAsset::with_trails()`, and call `with_trail_length()` and optionally `with_trail_period()` on the `Spawner`.

### Changed

Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "bevy/png", "3d" ]
name = "2d"
required-features = [ "bevy/bevy_winit", "bevy/bevy_sprite", "2d" ]

[[example]]
name = "worms"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs bevy/png too for loading the circle asset.


[workspace]
resolver = "2"
members = ["."]
Binary file added assets/circle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions examples/worms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! Worms
//!
//! Demonstrates simple use of particle trails.

use std::f32::consts::{FRAC_PI_2, PI};

use bevy::{
core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping},
log::LogPlugin,
math::{vec3, vec4},
prelude::*,
};
#[cfg(feature = "examples_world_inspector")]
use bevy_inspector_egui::quick::WorldInspectorPlugin;

use bevy_hanabi::prelude::*;

fn main() {
let mut app = App::default();
app.add_plugins(
DefaultPlugins
.set(LogPlugin {
level: bevy::log::Level::WARN,
filter: "bevy_hanabi=warn,worms=trace".to_string(),
update_subscriber: None,
})
.set(WindowPlugin {
primary_window: Some(Window {
title: "🎆 Hanabi — worms".to_string(),
..default()
}),
..default()
}),
)
.add_systems(Update, bevy::window::close_on_esc)
.add_plugins(HanabiPlugin);

#[cfg(feature = "examples_world_inspector")]
app.add_plugins(WorldInspectorPlugin::default());

app.add_systems(Startup, setup).run();
}

fn setup(
mut commands: Commands,
asset_server: ResMut<AssetServer>,
mut effects: ResMut<Assets<EffectAsset>>,
) {
commands.spawn((
Camera3dBundle {
transform: Transform::from_translation(Vec3::new(0., 0., 25.)),
camera: Camera {
hdr: true,
clear_color: Color::BLACK.into(),
..default()
},
tonemapping: Tonemapping::None,
..default()
},
BloomSettings::default(),
));

let circle: Handle<Image> = asset_server.load("circle.png");

let writer = ExprWriter::new();

// Init modifiers

// Spawn the particles within a reasonably large box.
let set_initial_position_modifier = SetAttributeModifier::new(
Attribute::POSITION,
((writer.rand(ValueType::Vector(VectorType::VEC3F)) + writer.lit(vec3(-0.5, -0.5, 0.0)))
* writer.lit(vec3(16.0, 16.0, 0.0)))
.expr(),
);

// Randomize the initial angle of the particle, storing it in the `F32_0`
// scratch attribute.`
let set_initial_angle_modifier = SetAttributeModifier::new(
Attribute::F32_0,
writer.lit(0.0).uniform(writer.lit(PI * 2.0)).expr(),
);

// Give each particle a random opaque color.
let set_color_modifier = SetAttributeModifier::new(
Attribute::COLOR,
(writer.rand(ValueType::Vector(VectorType::VEC4F)) * writer.lit(vec4(1.0, 1.0, 1.0, 0.0))
+ writer.lit(Vec4::W))
.pack4x8unorm()
.expr(),
);

// Give the particles a long lifetime.
let set_lifetime_modifier =
SetAttributeModifier::new(Attribute::LIFETIME, writer.lit(10.0).expr());

// Update modifiers

// Make the particle wiggle, following a sine wave.
let set_velocity_modifier = SetAttributeModifier::new(
Attribute::VELOCITY,
WriterExpr::sin(
writer.lit(vec3(1.0, 1.0, 0.0))
* (writer.attr(Attribute::F32_0)
+ (writer.time() * writer.lit(5.0)).sin() * writer.lit(1.0))
+ writer.lit(vec3(0.0, FRAC_PI_2, 0.0)),
)
.mul(writer.lit(5.0))
.expr(),
);

// Render modifiers

// Set the particle size.
let set_size_modifier = SetSizeModifier {
size: Vec2::splat(0.4).into(),
};

// Make each particle round.
let particle_texture_modifier = ParticleTextureModifier {
texture: circle,
sample_mapping: ImageSampleMapping::Modulate,
};

let module = writer.finish();

// Allocate room for 32,768 trail particles. Give each particle a 5-particle
// trail, and spawn a new trail particle every ⅛ of a second.
let effect = effects.add(
EffectAsset::with_trails(
32768,
32768,
Spawner::rate(4.0.into())
.with_trail_length(5)
.with_trail_period(0.125.into()),
module,
)
.with_name("worms")
.init(set_initial_position_modifier)
.init(set_initial_angle_modifier)
.init(set_lifetime_modifier)
.init(set_color_modifier)
.update(set_velocity_modifier)
.render(set_size_modifier)
.render(particle_texture_modifier),
);

commands.spawn((
Name::new("worms"),
ParticleEffectBundle {
effect: ParticleEffect::new(effect),
transform: Transform::IDENTITY,
..default()
},
));
}
37 changes: 36 additions & 1 deletion src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ pub struct EffectAsset {
/// should keep this quantity as close as possible to the maximum number of
/// particles they expect to render.
capacity: u32,
/// Maximum number of concurrent trail particles.
///
/// The same caveats as [`capacity`] apply. This value can't be changed
/// after the effect is created.
trail_capacity: u32,
/// Spawner.
pub spawner: Spawner,
/// For 2D rendering, the Z coordinate used as the sort key.
Expand Down Expand Up @@ -250,6 +255,9 @@ impl EffectAsset {
/// which should be passed to this method. If expressions are not used, just
/// pass an empty module [`Module::default()`].
///
/// This function doesn't allocate space for any trails. If you need
/// particle trails, use [`with_trails`] instead.
///
/// # Examples
///
/// Create a new effect asset without any modifier. This effect doesn't
Expand Down Expand Up @@ -290,12 +298,30 @@ impl EffectAsset {
}
}

/// As [`new`], but reserves space for trails.
///
/// Use this method when you want to enable particle trails.
pub fn with_trails(
capacity: u32,
trail_capacity: u32,
spawner: Spawner,
module: Module,
) -> Self {
Self {
capacity,
trail_capacity,
spawner,
module,
..default()
}
}

/// Get the capacity of the effect, in number of particles.
///
/// This represents the number of particles stored in GPU memory at all
/// time, even if unused, so you should try to minimize this value. However,
/// the [`Spawner`] cannot emit more particles than this capacity. Whatever
/// the spanwer settings, if the number of particles reaches the capacity,
/// the spawner settings, if the number of particles reaches the capacity,
/// no new particle can be emitted. Setting an appropriate capacity for an
/// effect is therefore a compromise between more particles available for
/// visuals and more GPU memory usage.
Expand All @@ -310,6 +336,15 @@ impl EffectAsset {
self.capacity
}

/// Get the trail capacity of the effect, in number of trail particles.
///
/// The same caveats as [`capacity`] apply here: the GPU always allocates
/// space for this many trail particles, regardless of the number actually
/// used.
pub fn trail_capacity(&self) -> u32 {
self.trail_capacity
}

/// Get the expression module storing all expressions in use by modifiers of
/// this effect.
pub fn module(&self) -> &Module {
Expand Down
5 changes: 1 addition & 4 deletions src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ use bevy::{
utils::FloatOrd,
};
use serde::{Deserialize, Serialize};
use std::{
hash::{Hash, Hasher},
vec::Vec,
};
use std::hash::{Hash, Hasher};

/// Describes a type that can be linearly interpolated between two keys.
///
Expand Down
26 changes: 23 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,6 @@ mod spawn;
#[cfg(test)]
mod test_utils;

use properties::PropertyInstance;

pub use asset::{AlphaMode, EffectAsset, MotionIntegration, SimulationCondition};
pub use attributes::*;
pub use bundle::ParticleEffectBundle;
Expand Down Expand Up @@ -840,6 +838,19 @@ impl EffectShaderSource {
"@group(1) @binding(2) var<storage, read> properties : Properties;".to_string()
};

let (trail_binding_code, trail_render_indirect_binding_code) = if asset.trail_capacity()
== 0
{
("// (no trails)".to_string(), "// (no trails)".to_string())
} else {
(
"@group(1) @binding(3) var<storage, read_write> trail_buffer : ParticleBuffer;"
.to_string(),
"@group(3) @binding(1) var<storage, read_write> trail_render_indirect : TrailRenderIndirect;"
.to_string(),
)
};

// Start from the base module containing the expressions actually serialized in
// the asset. We will add the ones created on-the-fly by applying the
// modifiers to the contexts.
Expand Down Expand Up @@ -968,6 +979,10 @@ impl EffectShaderSource {
(String::new(), String::new())
};

if asset.trail_capacity() > 0 {
layout_flags |= LayoutFlags::TRAILS_BUFFER_PRESENT;
}

(
render_context.vertex_code,
render_context.fragment_code,
Expand Down Expand Up @@ -1040,7 +1055,12 @@ impl EffectShaderSource {
.replace("{{UPDATE_CODE}}", &update_code)
.replace("{{UPDATE_EXTRA}}", &update_extra)
.replace("{{PROPERTIES}}", &properties_code)
.replace("{{PROPERTIES_BINDING}}", &properties_binding_code);
.replace("{{PROPERTIES_BINDING}}", &properties_binding_code)
.replace("{{TRAIL_BINDING}}", &trail_binding_code)
.replace(
"{{TRAIL_RENDER_INDIRECT_BINDING}}",
&trail_render_indirect_binding_code,
);
trace!("Configured update shader:\n{}", update_shader_source);

// Configure the render shader template, and make sure a corresponding shader
Expand Down
16 changes: 14 additions & 2 deletions src/render/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub(crate) struct EffectBatch {
///
/// [`ParticleEffect`]: crate::ParticleEffect
pub entities: Vec<u32>,
/// Whether trails are active for this effect (present with a nonzero
/// length).
pub trails_active: bool,
}

impl EffectBatch {
Expand All @@ -74,6 +77,7 @@ impl EffectBatch {
#[cfg(feature = "2d")]
z_sort_key_2d: input.z_sort_key_2d,
entities: vec![input.entity_index],
trails_active: input.trail_capacity > 0 && input.trail_length > 0,
}
}
}
Expand All @@ -98,6 +102,11 @@ pub(crate) struct BatchInput {
pub image_handle: Handle<Image>,
/// Number of particles to spawn for this effect.
pub spawn_count: u32,
pub spawn_trail_particle: bool,
pub trail_length: u32,
pub trail_capacity: u32,
pub trail_head_chunk: u32,
pub trail_tail_chunk: u32,
/// Emitter transform.
pub transform: GpuCompressedTransform,
/// Emitter inverse transform.
Expand Down Expand Up @@ -295,8 +304,6 @@ impl<'a, S, B, I: Batchable<S, B>> Batcher<'a, S, B, I> {

#[cfg(test)]
mod tests {
use crate::EffectShader;

use super::*;

// Test item to batch
Expand Down Expand Up @@ -550,6 +557,11 @@ mod tests {
layout_flags: LayoutFlags::NONE,
image_handle,
spawn_count: 32,
spawn_trail_particle: false,
trail_length: 0,
trail_capacity: 0,
trail_head_chunk: 0,
trail_tail_chunk: 0,
transform: GpuCompressedTransform::default(),
inverse_transform: GpuCompressedTransform::default(),
property_buffer: None,
Expand Down
Loading
Loading