Skip to content

Commit

Permalink
Add a modifier to make particles round.
Browse files Browse the repository at this point in the history
This commit adds a `RoundModifier`, which allows particles to be made
into [squircle]s without having to use a texture. Squircles are like
rounded rectangles but are cheaper to evaluate. A configurable
`roundness` parameter allows the roundness to vary smoothly from a sharp
rectangle to a perfect ellipse.

Given x and y from (-1, 1), the equation of the shape of the particle is
|x|ⁿ + |y|ⁿ = 1, where n = 2 / `roundness`.

The `firework` and `portal` examples have been updated to use the
roundness modifier, as rounded particles generally look better than
square ones in these scenarios.

[squircle]: https://en.wikipedia.org/wiki/Squircle
  • Loading branch information
pcwalton committed Feb 27, 2024
1 parent 5bf400f commit 518063c
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 24 deletions.
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 a new `RoundModifier` that allows round particles to be constructed without having to create a texture.

### Changed

Expand Down
38 changes: 19 additions & 19 deletions examples/firework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,25 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
speed: (writer.rand(ScalarType::Float) * writer.lit(20.) + writer.lit(60.)).expr(),
};

let effect = EffectAsset::new(
32768,
Spawner::burst(2500.0.into(), 2.0.into()),
writer.finish(),
)
.with_name("firework")
.init(init_pos)
.init(init_vel)
.init(init_age)
.init(init_lifetime)
.update(update_drag)
.update(update_accel)
.render(ColorOverLifetimeModifier {
gradient: color_gradient1,
})
.render(SizeOverLifetimeModifier {
gradient: size_gradient1,
screen_space_size: false,
});
let mut module = writer.finish();
let round = RoundModifier::ellipse(&mut module);

let effect = EffectAsset::new(32768, Spawner::burst(2500.0.into(), 2.0.into()), module)
.with_name("firework")
.init(init_pos)
.init(init_vel)
.init(init_age)
.init(init_lifetime)
.update(update_drag)
.update(update_accel)
.render(ColorOverLifetimeModifier {
gradient: color_gradient1,
})
.render(SizeOverLifetimeModifier {
gradient: size_gradient1,
screen_space_size: false,
})
.render(round);

let effect1 = effects.add(effect);

Expand Down
4 changes: 3 additions & 1 deletion examples/portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
let mut module = writer.finish();

let tangent_accel = TangentAccelModifier::constant(&mut module, Vec3::ZERO, Vec3::Z, 30.);
let round = RoundModifier::constant(&mut module, 1.0 / 3.0);

let effect1 = effects.add(
EffectAsset::new(32768, Spawner::rate(5000.0.into()), module)
Expand All @@ -113,7 +114,8 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
gradient: size_gradient1,
screen_space_size: false,
})
.render(OrientModifier::new(OrientMode::AlongVelocity)),
.render(OrientModifier::new(OrientMode::AlongVelocity))
.render(round),
);

commands.spawn((
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,9 @@ impl EffectShaderSource {
if let AlphaMode::Mask(_) = &asset.alpha_mode {
layout_flags |= LayoutFlags::USE_ALPHA_MASK;
}
if render_context.needs_uv {
layout_flags |= LayoutFlags::NEEDS_UV;
}

let (flipbook_scale_code, flipbook_row_count_code) = if let Some(grid_size) =
render_context.sprite_grid_size
Expand Down Expand Up @@ -1745,6 +1748,7 @@ else { return c1; }
let mut shader_defs = std::collections::HashMap::<String, ShaderDefValue>::new();
shader_defs.insert("LOCAL_SPACE_SIMULATION".into(), ShaderDefValue::Bool(true));
shader_defs.insert("PARTICLE_TEXTURE".into(), ShaderDefValue::Bool(true));
shader_defs.insert("NEEDS_UV".into(), ShaderDefValue::Bool(true));
shader_defs.insert("RENDER_NEEDS_SPAWNER".into(), ShaderDefValue::Bool(true));
shader_defs.insert(
"PARTICLE_SCREEN_SPACE_SIZE".into(),
Expand Down
11 changes: 11 additions & 0 deletions src/modifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ pub struct RenderContext<'a> {
pub gradients: HashMap<u64, Gradient<Vec4>>,
/// Size gradients.
pub size_gradients: HashMap<u64, Gradient<Vec2>>,
/// Needs uv
pub needs_uv: bool,
/// Counter for unique variable names.
var_counter: u32,
/// Cache of evaluated expressions.
Expand All @@ -442,15 +444,24 @@ impl<'a> RenderContext<'a> {
sprite_grid_size: None,
gradients: HashMap::new(),
size_gradients: HashMap::new(),
needs_uv: false,
var_counter: 0,
expr_cache: Default::default(),
is_attribute_pointer: false,
}
}

/// Set the main texture used to color particles.
///
/// This implicitly sets `needs_uv`.
fn set_particle_texture(&mut self, handle: Handle<Image>) {
self.particle_texture = Some(handle);
self.needs_uv = true;
}

/// Mark the rendering shader as needing UVs.
fn set_needs_uv(&mut self) {
self.needs_uv = true;
}

/// Add a color gradient.
Expand Down
65 changes: 65 additions & 0 deletions src/modifier/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,71 @@ impl RenderModifier for ScreenSpaceSizeModifier {
}
}

/// Makes particles round.
///
/// The shape of each particle is a [squircle] (like a rounded rectangle, but
/// faster to evaluate). The `roundness` parameter specifies how round the shape
/// is. At 0.0, the particle is a rectangle; at 1.0, the particle is an
/// ellipse.
///
/// Given x and y from (-1, 1), the equation of the shape of the particle is
/// |x|ⁿ + |y|ⁿ = 1, where n = 2 / `roundness``.
///
/// Note that this modifier is presently incompatible with the
/// [`FlipbookModifier`]. Attempts to use them together will produce unexpected
/// results.
///
/// [squircle]: https://en.wikipedia.org/wiki/Squircle
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
pub struct RoundModifier {
/// How round the particle is.
///
/// This ranges from 0.0 for a perfect rectangle to 1.0 for a perfect
/// ellipse. 1/3 produces a nice rounded rectangle shape.
///
/// n in the squircle formula is calculated as (2 / roundness).
pub roundness: ExprHandle,
}

impl_mod_render!(RoundModifier, &[]);

#[typetag::serde]
impl RenderModifier for RoundModifier {
fn apply_render(&self, module: &mut Module, context: &mut RenderContext) {
context.set_needs_uv();

let roundness = context.eval(module, self.roundness).unwrap();
context.fragment_code += &format!(
"let roundness = {};
if (roundness > 0.0f) {{
let n = 2.0f / roundness;
if (pow(abs(1.0f - 2.0f * in.uv.x), n) +
pow(abs(1.0f - 2.0f * in.uv.y), n) > 1.0f) {{
discard;
}}
}}",
roundness
);
}
}

impl RoundModifier {
/// Creates a new [`RoundModifier`] with the given roundness.
///
/// The `roundness` parameter varies from 0.0 to 1.0.
pub fn constant(module: &mut Module, roundness: f32) -> RoundModifier {
RoundModifier {
roundness: module.lit(roundness),
}
}

/// Creates a new [`RoundModifier`] that describes an ellipse.
#[doc(alias = "circle")]
pub fn ellipse(module: &mut Module) -> RoundModifier {
RoundModifier::constant(module, 1.0)
}
}

#[cfg(test)]
mod tests {
use crate::*;
Expand Down
12 changes: 12 additions & 0 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,9 @@ pub(crate) struct ParticleRenderPipelineKey {
/// The effect is rendered with flipbook texture animation based on the
/// sprite index of each particle.
flipbook: bool,
/// Key: NEEDS_UV
/// The effect needs UVs.
needs_uv: bool,
/// For dual-mode configurations only, the actual mode of the current render
/// pipeline. Otherwise the mode is implicitly determined by the active
/// feature.
Expand All @@ -900,6 +903,7 @@ impl Default for ParticleRenderPipelineKey {
local_space_simulation: false,
use_alpha_mask: false,
flipbook: false,
needs_uv: false,
#[cfg(all(feature = "2d", feature = "3d"))]
pipeline_mode: PipelineMode::Camera3d,
msaa_samples: Msaa::default().samples(),
Expand Down Expand Up @@ -1041,6 +1045,10 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline {
shader_defs.push("FLIPBOOK".into());
}

if key.needs_uv {
shader_defs.push("NEEDS_UV".into());
}

#[cfg(all(feature = "2d", feature = "3d"))]
let depth_stencil = match key.pipeline_mode {
// Bevy's Transparent2d render phase doesn't support a depth-stencil buffer.
Expand Down Expand Up @@ -1720,6 +1728,8 @@ bitflags! {
const USE_ALPHA_MASK = (1 << 3);
/// The effect is rendered with flipbook texture animation based on the [`Attribute::SPRITE_INDEX`] of each particle.
const FLIPBOOK = (1 << 4);
/// The effect needs UVs.
const NEEDS_UV = (1 << 5);
}
}

Expand Down Expand Up @@ -2188,6 +2198,7 @@ fn emit_draw<T, F>(
.contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
let use_alpha_mask = batch.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK);
let flipbook = batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
let needs_uv = batch.layout_flags.contains(LayoutFlags::NEEDS_UV);

// Specialize the render pipeline based on the effect batch
trace!(
Expand All @@ -2208,6 +2219,7 @@ fn emit_draw<T, F>(
local_space_simulation,
use_alpha_mask,
flipbook,
needs_uv,
#[cfg(all(feature = "2d", feature = "3d"))]
pipeline_mode,
msaa_samples,
Expand Down
8 changes: 4 additions & 4 deletions src/render/vfx_render.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct ParticleBuffer {
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
#ifdef PARTICLE_TEXTURE
#ifdef NEEDS_UV
@location(1) uv: vec2<f32>,
#endif
}
Expand Down Expand Up @@ -111,7 +111,7 @@ fn transform_position_simulation_to_clip(sim_position: vec3<f32>) -> vec4<f32> {
fn vertex(
@builtin(instance_index) instance_index: u32,
@location(0) vertex_position: vec3<f32>,
#ifdef PARTICLE_TEXTURE
#ifdef NEEDS_UV
@location(1) vertex_uv: vec2<f32>,
#endif
// @location(1) vertex_color: u32,
Expand All @@ -121,15 +121,15 @@ fn vertex(
let index = indirect_buffer.indices[3u * instance_index + pong];
var particle = particle_buffer.particles[index];
var out: VertexOutput;
#ifdef PARTICLE_TEXTURE
#ifdef NEEDS_UV
var uv = vertex_uv;
#ifdef FLIPBOOK
let row_count = {{FLIPBOOK_ROW_COUNT}};
let ij = vec2<f32>(f32(particle.sprite_index % row_count), f32(particle.sprite_index / row_count));
uv = (ij + uv) * {{FLIPBOOK_SCALE}};
#endif
out.uv = uv;
#endif
#endif // NEEDS_UV

{{INPUTS}}

Expand Down

0 comments on commit 518063c

Please sign in to comment.