Skip to content

Commit

Permalink
Add OrientModifier::rotation (#260)
Browse files Browse the repository at this point in the history
Add an optional in-plane rotation to the `OrientModifier` to allow
rotating the particles within their oriented plane.

Add 4 new scalar float attributes `F32_0` to `F32_3` which have no
specific meaning, and can be used for anything. Use one in the
`billboard.rs` example to store a per-particle initial rotation, which
is later read back and used to set `OrientModifier::rotation` and give
some visual variations to the effect.

Change `Attribute::ALL` to private; replace with `Attribute::all()`.
This makes it a non-breaking change to add more attributes or reorder
them in the future.

Fixes #258
  • Loading branch information
djeedai authored Nov 29, 2023
1 parent 6898032 commit 730cec8
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 63 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added a new `EffectProperties` component holding the runtime values for all properties of a single `ParticleEffect` instance. This component can be added manually to the same `Entity` holding the `ParticleEffect` if you want to set initial values different from the default ones declared in the `EffectAsset`. Otherwise Hanabi will add the component automatically.
- Added a new `EffectSystems::UpdatePropertiesFromAsset` set running in the `PostUpdate` schedule. During this set, Hanabi automatically updates all `EffectProperties` if the properties declared in the underlying `EffectAsset` changed.
- Added `OrientModifier::rotation`, an optional expression which allows rotating the particle within its oriented plane. The actual meaning depends on the `OrientMode` used. (#258)
- Added 4 new scalar float attributes `F32_0` to `F32_3`, which have no specified meaning but instead can be used to store any per-particle value.

### Changed

- Properties of an effect have been moved from `CompiledParticleEffect` to a new `EffectProperties` component. This splits the semantic of the `CompiledParticleEffect`, which is purely an internal optimization, from the list of properties stored in `EffectProperties`, which is commonly accessed by the user to assign new values to properties.
- Thanks to the split of properties into `EffectProperties`, change detection now works on properties, and uploads to GPU will only occur when change detection triggered on the component. Previously properties were re-uploaded each frame to the GPU even if unchanged.
- Effect properties are now reflected (via the new `EffectProperties` component).
- `Attribute::ALL` is now private; use `Attribute::all()` instead.

## [0.8.0] 2023-11-08

Expand Down
14 changes: 13 additions & 1 deletion examples/billboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,21 @@ fn setup(
speed: (writer.lit(0.5) + writer.lit(0.2) * writer.rand(ScalarType::Float)).expr(),
};

// Use the F32_0 attribute as a per-particle rotation value, initialized on
// spawn and constant after. The rotation angle is in radians, here randomly
// selected in [0:2*PI].
let init_rotation = (writer.rand(ScalarType::Float) * writer.lit(std::f32::consts::TAU)).expr();
let init_rotation = SetAttributeModifier::new(Attribute::F32_0, init_rotation);

// Bounce the alpha cutoff value between 0 and 1, to show its effect on the
// alpha masking
let alpha_cutoff =
((writer.time() * writer.lit(2.)).sin() * writer.lit(0.5) + writer.lit(0.5)).expr();

// The rotation of the OrientModifier is read from the F32_0 attribute (our
// per-particle rotation)
let rotation = writer.attr(Attribute::F32_0).expr();

let effect = effects.add(
EffectAsset::new(32768, Spawner::rate(64.0.into()), writer.finish())
.with_name("billboard")
Expand All @@ -112,12 +122,14 @@ fn setup(
.init(init_vel)
.init(init_age)
.init(init_lifetime)
.init(init_rotation)
.render(ParticleTextureModifier {
texture: texture_handle,
sample_mapping: ImageSampleMapping::ModulateOpacityFromR,
})
.render(OrientModifier {
mode: OrientMode::ParallelCameraDepthPlane,
mode: OrientMode::FaceCameraPosition,
rotation: Some(rotation),
})
.render(ColorOverLifetimeModifier { gradient })
.render(SizeOverLifetimeModifier {
Expand Down
4 changes: 1 addition & 3 deletions examples/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
gradient: size_gradient,
screen_space_size: false,
})
.render(OrientModifier {
mode: OrientMode::AlongVelocity,
}),
.render(OrientModifier::new(OrientMode::AlongVelocity)),
);

commands.spawn((
Expand Down
4 changes: 1 addition & 3 deletions examples/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ where
.with_name(name)
.with_simulation_space(SimulationSpace::Local)
.init(init)
.render(OrientModifier {
mode: OrientMode::FaceCameraPosition,
})
.render(OrientModifier::new(OrientMode::FaceCameraPosition))
.render(SetColorModifier {
color: COLOR.into(),
})
Expand Down
4 changes: 1 addition & 3 deletions examples/multicam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ fn make_effect(color: Color) -> EffectAsset {
gradient: size_gradient.clone(),
screen_space_size: false,
})
.render(OrientModifier {
mode: OrientMode::FaceCameraPosition,
})
.render(OrientModifier::new(OrientMode::FaceCameraPosition))
}

fn setup(
Expand Down
4 changes: 1 addition & 3 deletions examples/portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,7 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
gradient: size_gradient1,
screen_space_size: false,
})
.render(OrientModifier {
mode: OrientMode::AlongVelocity,
}),
.render(OrientModifier::new(OrientMode::AlongVelocity)),
);

commands.spawn((
Expand Down
30 changes: 9 additions & 21 deletions src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,15 +592,9 @@ mod tests {
.render(ParticleTextureModifier::default())
.render(ColorOverLifetimeModifier::default())
.render(SizeOverLifetimeModifier::default())
.render(OrientModifier {
mode: OrientMode::ParallelCameraDepthPlane,
})
.render(OrientModifier {
mode: OrientMode::FaceCameraPosition,
})
.render(OrientModifier {
mode: OrientMode::AlongVelocity,
});
.render(OrientModifier::new(OrientMode::ParallelCameraDepthPlane))
.render(OrientModifier::new(OrientMode::FaceCameraPosition))
.render(OrientModifier::new(OrientMode::AlongVelocity));

assert_eq!(effect.capacity, 4096);

Expand Down Expand Up @@ -643,18 +637,12 @@ mod tests {
ParticleTextureModifier::default().apply_render(&mut module, &mut render_context);
ColorOverLifetimeModifier::default().apply_render(&mut module, &mut render_context);
SizeOverLifetimeModifier::default().apply_render(&mut module, &mut render_context);
OrientModifier {
mode: OrientMode::ParallelCameraDepthPlane,
}
.apply_render(&mut module, &mut render_context);
OrientModifier {
mode: OrientMode::FaceCameraPosition,
}
.apply_render(&mut module, &mut render_context);
OrientModifier {
mode: OrientMode::AlongVelocity,
}
.apply_render(&mut module, &mut render_context);
OrientModifier::new(OrientMode::ParallelCameraDepthPlane)
.apply_render(&mut module, &mut render_context);
OrientModifier::new(OrientMode::FaceCameraPosition)
.apply_render(&mut module, &mut render_context);
OrientModifier::new(OrientMode::AlongVelocity)
.apply_render(&mut module, &mut render_context);
// assert_eq!(effect.render_layout, render_layout);
}

Expand Down
110 changes: 104 additions & 6 deletions src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,26 @@ impl AttributeInner {
Value::Scalar(ScalarValue::Int(0)),
);

pub const F32_0: &'static AttributeInner = &AttributeInner::new(
Cow::Borrowed("f32_0"),
Value::Scalar(ScalarValue::Float(0.)),
);

pub const F32_1: &'static AttributeInner = &AttributeInner::new(
Cow::Borrowed("f32_1"),
Value::Scalar(ScalarValue::Float(0.)),
);

pub const F32_2: &'static AttributeInner = &AttributeInner::new(
Cow::Borrowed("f32_2"),
Value::Scalar(ScalarValue::Float(0.)),
);

pub const F32_3: &'static AttributeInner = &AttributeInner::new(
Cow::Borrowed("f32_3"),
Value::Scalar(ScalarValue::Float(0.)),
);

#[inline]
pub(crate) const fn new(name: Cow<'static, str>, default_value: Value) -> Self {
Self {
Expand All @@ -438,8 +458,8 @@ impl AttributeInner {
/// Effects are composed of many simulated particles. Each particle is in turn
/// composed of a set of attributes, which are used to simulate and render it.
/// Common attributes include the particle's position, its age, or its color.
/// See [`Attribute::ALL`] for a list of supported attributes. Custom attributes
/// are not supported.
/// See [`Attribute::all()`] for a list of supported attributes. Custom
/// attributes are not supported.
///
/// Attributes are indirectly added to an effect by adding [modifiers] requiring
/// them. Each modifier documents its required attributes. You can force a
Expand Down Expand Up @@ -852,8 +872,68 @@ impl Attribute {
/// [`FlipbookModifier`]: crate::modifier::output::FlipbookModifier
pub const SPRITE_INDEX: Attribute = Attribute(AttributeInner::SPRITE_INDEX);

/// A generic scalar float attribute.
///
/// This attribute can be used for anything. It has no specific meaning. You
/// can store whatever per-particle value you want in it (for example, at
/// spawn time) and read it back later.
///
/// # Name
///
/// `f32_0`
///
/// # Type
///
/// [`ScalarType::Float`]
pub const F32_0: Attribute = Attribute(AttributeInner::F32_0);

/// A generic scalar float attribute.
///
/// This attribute can be used for anything. It has no specific meaning. You
/// can store whatever per-particle value you want in it (for example, at
/// spawn time) and read it back later.
///
/// # Name
///
/// `f32_1`
///
/// # Type
///
/// [`ScalarType::Float`]
pub const F32_1: Attribute = Attribute(AttributeInner::F32_1);

/// A generic scalar float attribute.
///
/// This attribute can be used for anything. It has no specific meaning. You
/// can store whatever per-particle value you want in it (for example, at
/// spawn time) and read it back later.
///
/// # Name
///
/// `f32_2`
///
/// # Type
///
/// [`ScalarType::Float`]
pub const F32_2: Attribute = Attribute(AttributeInner::F32_2);

/// A generic scalar float attribute.
///
/// This attribute can be used for anything. It has no specific meaning. You
/// can store whatever per-particle value you want in it (for example, at
/// spawn time) and read it back later.
///
/// # Name
///
/// `f32_3`
///
/// # Type
///
/// [`ScalarType::Float`]
pub const F32_3: Attribute = Attribute(AttributeInner::F32_3);

/// Collection of all the existing particle attributes.
pub const ALL: [Attribute; 13] = [
const ALL: [Attribute; 17] = [
Attribute::POSITION,
Attribute::VELOCITY,
Attribute::AGE,
Expand All @@ -867,11 +947,15 @@ impl Attribute {
Attribute::AXIS_Y,
Attribute::AXIS_Z,
Attribute::SPRITE_INDEX,
Attribute::F32_0,
Attribute::F32_1,
Attribute::F32_2,
Attribute::F32_3,
];

/// Retrieve an attribute by its name.
///
/// See [`Attribute::ALL`] for the list of attributes, and the
/// See [`Attribute::all()`] for the list of attributes, and the
/// [`Attribute::name()`] method of each attribute for their name.
///
/// # Example
Expand All @@ -888,6 +972,20 @@ impl Attribute {
.copied()
}

/// Get the list of all existing attributes.
///
/// # Example
///
/// ```
/// # use bevy_hanabi::*;
/// for attr in Attribute::all() {
/// println!("{}", attr.name());
/// }
/// ```
pub fn all() -> &'static [Attribute] {
&Self::ALL
}

/// The attribute's name.
///
/// The name of an attribute is unique, and corresponds to the name of the
Expand Down Expand Up @@ -1419,8 +1517,8 @@ mod tests {

#[test]
fn attr_from_name() {
for attr in Attribute::ALL {
assert_eq!(Attribute::from_name(attr.name()), Some(attr));
for attr in Attribute::all() {
assert_eq!(Attribute::from_name(attr.name()), Some(*attr));
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1973,8 +1973,10 @@ else { return c1; }

// Import bevy_render::view for the render shader
{
// It's reasonably hard to retrieve the source code for view.wgsl in bevy_render. We use a few tricks to get a Shader
// that we can then convert into a composable module (which is how imports work in Bevy itself).
// It's reasonably hard to retrieve the source code for view.wgsl in
// bevy_render. We use a few tricks to get a Shader that we can
// then convert into a composable module (which is how imports work in Bevy
// itself).
let mut dummy_app = App::new();
dummy_app.init_resource::<Assets<Shader>>();
dummy_app.add_plugins(bevy::render::view::ViewPlugin);
Expand Down
12 changes: 3 additions & 9 deletions src/modifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,15 +1028,9 @@ fn main() {{
&ParticleTextureModifier::default(),
&ColorOverLifetimeModifier::default(),
&SizeOverLifetimeModifier::default(),
&OrientModifier {
mode: OrientMode::ParallelCameraDepthPlane,
},
&OrientModifier {
mode: OrientMode::FaceCameraPosition,
},
&OrientModifier {
mode: OrientMode::AlongVelocity,
},
&OrientModifier::new(OrientMode::ParallelCameraDepthPlane),
&OrientModifier::new(OrientMode::FaceCameraPosition),
&OrientModifier::new(OrientMode::AlongVelocity),
];
for &modifier in modifiers.iter() {
let mut module = Module::default();
Expand Down
Loading

0 comments on commit 730cec8

Please sign in to comment.