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

Graph API #191

Merged
merged 51 commits into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8108c27
Initial BoxedExpr impl
djeedai Apr 26, 2023
04f7fd0
Split scalar/vector/matrix types and values
djeedai Apr 27, 2023
0926ce2
Format comments
djeedai Apr 28, 2023
e5f4bd3
4-byte bool type + docs
djeedai Apr 28, 2023
6e1e2db
Fix most tests
djeedai Apr 28, 2023
6c1c3de
Add binary arithmetic operator
djeedai Apr 29, 2023
8a7e22f
AttributeExpr
djeedai Apr 30, 2023
190bba1
impl Add for Expr
djeedai May 1, 2023
dc4f3e7
Add other math operators
djeedai May 1, 2023
d53d7a3
Simple Graph API
djeedai May 3, 2023
0aa1ad6
Add a few basic nodes
djeedai May 4, 2023
5ee3149
Add other math nodes
djeedai May 5, 2023
3788e24
Add built-in op/expr and `TimeNode`
djeedai May 5, 2023
c995756
Add normalize node/expr
djeedai May 6, 2023
0c113b6
Wrap math expressions with parenthesis
djeedai May 11, 2023
4bf25b0
Start wiring modifiers and properties to Expr
djeedai May 11, 2023
aad462f
Bug fixex
djeedai May 13, 2023
00a77fd
Unify names in `BuiltInOperator` and `SimParams`
djeedai May 13, 2023
32ba38f
Transition `LinearDragModifier` to `BoxedExpr`
djeedai May 13, 2023
238d107
Start converting init context to BoxedExpr
djeedai May 13, 2023
5a88e73
Fix all examples
djeedai May 13, 2023
ed87590
Delete `InitAgeModifier` and `InitLifetimeModifier`
djeedai May 13, 2023
435a051
Transition `AabbKillModifier`
djeedai May 13, 2023
734aa5c
Add an expression writer helper
djeedai May 13, 2023
897846a
`Expr` as enum
djeedai May 19, 2023
da321b0
Convert modifiers to `ExprHandle`
djeedai Jun 2, 2023
6dc0d00
Migrate a few examples
djeedai Jun 2, 2023
3846074
Fix all examples
djeedai Jun 5, 2023
40ffe6a
Add docs and format code
djeedai Jun 5, 2023
fdfec05
Transition some modifiers and fix tests
djeedai Jun 7, 2023
6240ecb
Fix and add more docs
djeedai Jun 7, 2023
4563566
Add more docs
djeedai Jun 8, 2023
c7674c9
Merge remote-tracking branch 'origin/main' into u/expr-enum-naga
djeedai Jun 10, 2023
8229bd3
Re-enable attribute tests
djeedai Jun 10, 2023
5cc0957
Fix examples
djeedai Jun 11, 2023
7c7290a
Fix tests and clippy
djeedai Jun 11, 2023
858cfa6
Add CHANGELOG
djeedai Jun 11, 2023
72b274a
Add migration guide
djeedai Jun 11, 2023
ec4cdc4
Re-add Node implementations
djeedai Jun 13, 2023
1719bd0
Re-enable `Graph` test
djeedai Jun 13, 2023
349730b
impl Default for most nodes
djeedai Jun 22, 2023
f84a3c8
cargo fmt
djeedai Jun 22, 2023
7a44199
Add more docs
djeedai Jun 22, 2023
6a47648
Merge remote-tracking branch 'origin/main' into u/expr-enum-naga
djeedai Jun 22, 2023
3f08a36
Add rand_uniform to render shader for consistency
djeedai Jun 22, 2023
77db28c
Add more tests, missing vec methods, fix bugs
djeedai Jun 23, 2023
0287edf
clippy fixes
djeedai Jun 23, 2023
a6882fb
Add missing `WriterExpr::normalized`
djeedai Jun 29, 2023
40e3f22
Fix README example, copy from lib.rs one
djeedai Jul 1, 2023
4f33f5d
Add ctor to `EffectAsset`
djeedai Jul 1, 2023
f0b760f
Add more complex expression example
djeedai Jul 1, 2023
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
3 changes: 3 additions & 0 deletions .github/example-run/3d/expr.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
exit_after: Some(60)
)
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Added `Gradient::linear()` helper method to produce a linear gradient between two values at keys `0.` and `1.`.
- `EffectAsset` now owns a `Module` field containing all the `Expr` used by the effect's modifiers.
- Added `ScalarType`, `VectorType`, and `MatrixType` to reprensent a scalar, vector, or matrix type, respectively.
- Added `ValueType::is_numeric()` as well as query methods to determine the kind of value type `is_scalar()` / `is_vector()` / `is_matrix()`.
- Added new Expression API: `Expr`, `ExprHandle`, `Module`, `ExprWriter`, `WriterExpr`.
- Added new `EvalContext` trait representing the evaluation context of an expression, and giving access to the underlying expression `Module`
and the property layout of the efect. The trait is implemented by `InitContext` and `UpdateContext`.
- Added convenience method `PropertyLayout::contains()` to determine if a layout contains a property by name.

### Changed

- `ValueType` is now one of `ScalarType` / `VectorType` / `MatrixType`, allowing to represent a wider range of types, including booleans and matrices.
- `graph::Value` is now one of `ScalarValue` / `VectorValue` / `MatrixValue`, for consistency with `ValueType`.
- `SimParams::dt` was renamed to `SimParams::delta_time` for readability. Inside shaders, `sim_params.dt` was also renamed to `sim_params.delta_time`.
- `InitContext` and `UpdateContext` now hold a mutable reference to the underlying `Module` to allow modifiers to create new `Expr`,
and a read-only reference to the property layout of the effect.
- `InitModifier::apply()` and `UpdateModifier::apply()` now return a `Result<(), ExprError>`.
- The following modifiers changed to leverage the new Expression API:
- `InitAttributeModifier`:
- `value` field is now an `ExprHandle`.
- The modifier is `Copy`-able.
- `AccelModifier`:
- `accel` field is now an `ExprHandle`.
- The modifier is `Copy`-able.
- `AccelModifier::constant()` takes a `&mut Module` argument to create the literal expression assigned to the `accel` field.
- `AccelModifier::via_property()` takes a `&mut Module` argument to create the property expression assigned to the `accel` field.
- `LinearDragModifier`:
- `drag` field is now an `ExprHandle`.
- `AabbKillModifier`:
- `center` and `half_size` fields are now `ExprHandle`.
- `Property::new()` takes a `default_value` argument as `impl Into<Value>` instead of `Value`. This should make it easier to call, without requiring any change to existing code.
- `PropertyLayout::new()` takes an `iter` argument as `impl IntoIterator` instead of `impl Iterator`. This should make it easier to call, without requiring any change to existing code.

### Removed

- The `InitAgeModifier` and `InitLifetimeModifier` were deleted. They're replaced with the more generic `InitAttributeModifier` which can initialize any attribute of the particle.

## [0.6.2] 2023-06-10

### Added
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_hanabi"
version = "0.6.2"
version = "0.7.0-dev"
authors = ["Jerome Humbert <djeedai@gmail.com>"]
edition = "2021"
description = "Hanabi GPU particle system for the Bevy game engine"
Expand Down Expand Up @@ -57,6 +57,10 @@ required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]
name = "portal"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]

[[example]]
name = "expr"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]

[[example]]
name = "spawn"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]
Expand Down
97 changes: 56 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,56 +51,71 @@ Create an `EffectAsset` describing a visual effect:

```rust
fn setup(mut effects: ResMut<Assets<EffectAsset>>) {
// Define a color gradient from red to transparent black
let mut gradient = Gradient::new();
gradient.add_key(0.0, Vec4::new(1., 0., 0., 1.)); // Red
gradient.add_key(1.0, Vec4::ZERO); // Transparent black

// Create the effect asset
let effect = effects.add(EffectAsset {
name: "MyEffect".to_string(),
// Maximum number of particles alive at a time
capacity: 32768,
// Spawn at a rate of 5 particles per second
spawner: Spawner::rate(5.0.into()),
..Default::default()
}
// On spawn, randomly initialize the position of the particle
// to be over the surface of a sphere of radius 2 units.
.init(InitPositionSphereModifier {
center: Vec3::ZERO,
radius: 2.,
dimension: ShapeDimension::Surface,
})
// Also initialize a radial initial velocity to 6 units/sec
// away from the (same) sphere center.
.init(InitVelocitySphereModifier {
center: Vec3::ZERO,
speed: 6.0.into(),
})
// Also initialize the total lifetime of the particle, that is
// the time for which it's simulated and rendered. This modifier
// is mandatory, otherwise the particles won't show up.
.init(InitLifetimeModifier { lifetime: 10_f32.into() })
// Every frame, add a gravity-like acceleration downward
.update(AccelModifier::constant(Vec3::new(0., -3., 0.)))
// Render the particles with a color gradient over their
// lifetime. This maps the gradient key 0 to the particle spawn
// time, and the gradient key 1 to the particle death (here, 10s).
.render(ColorOverLifetimeModifier { gradient })
);
// Define a color gradient from red to transparent black
let mut gradient = Gradient::new();
gradient.add_key(0.0, Vec4::new(1., 0., 0., 1.));
gradient.add_key(1.0, Vec4::splat(0.));

// Create a new expression module
let mut module = Module::default();

// Create a lifetime modifier
let lifetime = module.lit(10.); // literal value "10.0"
let init_lifetime = InitAttributeModifier::new(
Attribute::LIFETIME, lifetime);

// Create an acceleration modifier
let accel = module.lit(Vec3::new(0., -3., 0.));
let update_accel = AccelModifier::new(accel);

// Create the effect asset
let effect = EffectAsset::new(
// Maximum number of particles alive at a time
32768,
// Spawn at a rate of 5 particles per second
Spawner::rate(5.0.into()),
// Move the expression module into the asset
module
)
.with_name("MyEffect")
// On spawn, randomly initialize the position of the particle
// to be over the surface of a sphere of radius 2 units.
.init(InitPositionSphereModifier {
center: Vec3::ZERO,
radius: 2.,
dimension: ShapeDimension::Surface,
})
// Also initialize a radial initial velocity to 6 units/sec
// away from the (same) sphere center.
.init(InitVelocitySphereModifier {
center: Vec3::ZERO,
speed: 6.0.into(),
})
// Also initialize the total lifetime of the particle, that is
// the time for which it's simulated and rendered. This modifier
// is almost always required, otherwise the particles won't show.
.init(init_lifetime)
// Every frame, add a gravity-like acceleration downward
.update(update_accel)
// Render the particles with a color gradient over their
// lifetime. This maps the gradient key 0 to the particle spawn
// time, and the gradient key 1 to the particle death (10s).
.render(ColorOverLifetimeModifier { gradient });

// Insert into the asset system
let effect_handle = effects.add(effect);
}
```

### Add a particle effect

Use a `ParticleEffectBundle` to create an effect instance from an existing asset:
Use a `ParticleEffectBundle` to create an effect instance from an existing asset. The simplest way is to use the [`ParticleEffectBundle`] to ensure all required components are spawned together.

```rust
commands
.spawn(ParticleEffectBundle {
effect: ParticleEffect::new(effect),
transform: Transform::from_translation(Vec3::new(0., 1., 0.)),
effect: ParticleEffect::new(effect_handle),
transform: Transform::from_translation(Vec3::Y),
..Default::default()
});
```
Expand Down
132 changes: 132 additions & 0 deletions docs/migration-v0.6-to-v0.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Migration Guide v0.6 -> v0.7

🎆 Hanabi v0.7 marks an important milestone for the project: the addition of a generic expression-based API to enable complete customizing of the visual effects by the user.

The _Expression API_ departs radically from the previous version where the behavior of a particle was exclusively hard-coded into modifiers without any possibility to customize that behavior. Now, modifiers can leverage the Expression API to allow the user to provide a generic _expressions_ for any of their input value, rendering those modifiers completely under the control of the user.

This guide helps the user migrate from the pre-Expression v0.6 to the new Expression-based v0.7 of 🎆 Hanabi. Users are encouraged to also read the [`CHANGELOG`](../CHANGELOG.md) for an exhaustive list of all changes.

## Understanding Expressions

An _expression_ is an object generating some WGSL shader code to produce a value at runtime when the shader is executed on the GPU. For example, `5.` is a literal (constant) expression, whose value is hard-coded and will never change. `max(x, y)` on the other hand produces a value equal to the maximum of two other values. Those two other values are themselves expressions; expressions combine together to form more complex expressions.

An expression is represented by the `Expr` enum. Expressions are stored into a `Module`, and referenced by an `ExprHandle`, a sort of index into the module's internal storage. Each `EffectAsset` owns a `Module` which stores all the `Expr` used by the modifiers associated with that `EffectAsset`.

Because `Module` and `Expr` are focusing on storage (serialization) and runtime execution, manipulating expressions directly can be verbose. The `ExprWriter` utility simplifies writing expressions by providing a concise syntax, operator overloading (_e.g._ `impl std::ops::Add`), and other helper methods which make writing new expressions shorter and clearer.

The typical workflow for the end user is as follow:

1. Call `ExprWriter::new()` to create a new expression writer. The new writer internally allocates a `Module`, that it'll use to store expressions as they're produced.
1. Build new expressions with the `ExprWriter` methods and the use of the associated `WriterExpr` type. A `WriterExpr` is very similar to an `ExprHandle`, but is more heavyweight at the benefit of allowing a more concise syntax and operator overloading. `WriterExpr` represents an intermediate expression, and should not be stored long term.
1. Finalize any expression with `WriterExpr::expr()`, writing the corresponding `Expr` into the underlying `Module` and recovering the associated `ExprHandle`.
1. Create a new modifier and assign the `ExprHandle` to one of its field.
1. Repeat until all modifiers are ready for a given `EffectAsset`.
1. Finish using the writer with `ExprWriter::finish()`, recovering the finalized `Module` containing all written `Expr`.
1. Create a new effect with `EffectAsset::new()`, passing that `Module` as argument.

That last point is critical; expressions are owned by a `Module`, and assigning an `ExprHandle` from a different module produces undefined behaviors.

## Migrating `EffectAsset`

The `EffectAsset` struct has a `new()` associated function serving as the new entry point to create a new `EffectAsset` instance. This function takes the effect capacity, which is immutable after creation, the spawner, and the `Module` storing the effect expressions.

Commonly, the `Module` passed to `EffectAsset::new()` will be the one retrieved from the effect writer with `EffectWriter::finish()`.

// OLD v0.6

```rust
let asset = EffectAsset {
capacity: 256,
spawner: Spawner::rate(256.0.into()),
..Default::default()
};
```

// NEW v0.7

```rust
let writer = ExprWriter::new();
//[...]
let module = writer.finish();
let asset = EffectAsset::new(256, Spawner::rate(256.0.into()), module);
```

More rarely if no `Expr` is used in any modifier, just a default module can be used. Going forward, most modifiers will use expressions, so passing an empty `Module` should become anecdotal.

```rust
let asset = EffectAsset::new(256, Spawner::rate(256.0.into()), Module::default());
```

## Migrating modifiers

To migrate your modifiers from v0.6 to v0.7, you need to follow the steps above to prepare a `Module` with all the `Expr` describing the modifier inputs, then assign that module to the `EffectAsset` when you create it. Most examples have been migrated this way, and can be referenced for further details.

First, replace any `InitAgeModifier` and `InitLifetimeModifier` with an `InitAttributeModifer` built with the `Attribute::AGE` and `Attribute::LIFETIME`, respectively.

Then, follow the above steps to build expressions for all the modifier fields which were migrated to use `ExprHandle`. For example, an `AccelModifier::accel` field previously initialized with a constant:

// OLD v0.6

```rust
let asset = EffectAsset {
// [...]
..Default::default()
}.update(AccelModifier::constant(Vec3::Y * -3.));
```

now requires building a literal expression instead:

// NEW v0.7

```rust
// Create an ExprWriter for convenience
let w = ExprWriter::new();

// Build the `accel` literal expression
let accel = w.lit(Vec3::Y * -3.);

// Write it down into the Module, and get back the ExprHandle
let expr = accel.expr();

// Repeat for other modifiers...
// [...]

// Finish using the ExprWriter and recover the Module
let module = w.finish();

// Finally, create the EffectAsset with the modifiers
let asset = EffectAsset::new(capacity, spawner, module)
.update(AccelModifier::new(expr));
```

Note that previously a common pattern was to create modifiers inline while building the `EffectAsset`. This is not possible anymore for all modifiers using an `ExprHandle`, because the expression `Module` need to be finalized and assigned to the `EffectAsset` before the modifiers can be added to it. Otherwise the modifiers when they attach to the effect will fail their consistency check and panic.

## Other migration items

- All typed values like `graph::Value::Float3` need to be replaced by their scalar/vector counterpart:
- `graph::Value::Float(f)` becomes `graph::Value::Scalar(ScalarValue::Float(f)))`
- `graph::Value::Float3(v)` becomes `graph::Value::Vector(VectorValue::new_vec3(v))`
- _etc._

Some conversions are provided via `From<>` / `Into<>`, which can make the syntax shorter in some cases.

// OLD v0.6

```rust
let x = Value::Float(3.5);
let v = Value::Float3(Vec3::ONE);
```

// NEW v0.7

```rust
let x = Value::Scalar(3.5.into());
let v = Value::Vector(Vec3::ONE.into());
// -OR-
let x = Value::Scalar(ScalarValue::Float(3.5));
let v = Value::Vector(VectorValue::new_vec3(Vec3::ONE));
```

- Same kind of conversions for `graph::ValueType`.

- Rename `SimParams::dt` into `SimParams::delta_time`, and any shader use of `dt` into `delta_time`.
47 changes: 23 additions & 24 deletions examples/2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,35 +76,34 @@ fn setup(
gradient.add_key(0.0, Vec4::new(0.5, 0.5, 1.0, 1.0));
gradient.add_key(1.0, Vec4::new(0.5, 0.5, 1.0, 0.0));

let writer = ExprWriter::new();

let lifetime = writer.lit(5.).expr();
let init_lifetime = InitAttributeModifier::new(Attribute::LIFETIME, lifetime);

// Create a new effect asset spawning 30 particles per second from a circle
// and slowly fading from blue-ish to transparent over their lifetime.
// By default the asset spawns the particles at Z=0.
let spawner = Spawner::rate(30.0.into());
let effect = effects.add(
EffectAsset {
name: "Effect".into(),
capacity: 4096,
spawner,
..Default::default()
}
.init(InitPositionCircleModifier {
center: Vec3::ZERO,
axis: Vec3::Z,
radius: 0.05,
dimension: ShapeDimension::Surface,
})
.init(InitVelocityCircleModifier {
center: Vec3::ZERO,
axis: Vec3::Z,
speed: 0.1.into(),
})
.init(InitLifetimeModifier {
lifetime: 5_f32.into(),
})
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant(Vec2::splat(0.02)),
})
.render(ColorOverLifetimeModifier { gradient }),
EffectAsset::new(4096, spawner, writer.finish())
.with_name("2d")
.init(InitPositionCircleModifier {
center: Vec3::ZERO,
axis: Vec3::Z,
radius: 0.05,
dimension: ShapeDimension::Surface,
})
.init(InitVelocityCircleModifier {
center: Vec3::ZERO,
axis: Vec3::Z,
speed: 0.1.into(),
})
.init(init_lifetime)
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant(Vec2::splat(0.02)),
})
.render(ColorOverLifetimeModifier { gradient }),
);

// Spawn an instance of the particle effect, and override its Z layer to
Expand Down
Loading
Loading