Skip to content

Commit

Permalink
Add packing expression functions (#261)
Browse files Browse the repository at this point in the history
Add some new packing and unpacking expression functions corresponding to
the WGSL functions `pack4x8snorm`, `pack4x8unorm`, and their unpack
equivalent. Those are very commonly used to convert between low
definition `u32` color 0xAABBGGRR and high-definition `vec4<f32>` color.

Update the `billboard.rs` example to use those new expressions to store
a per-particle color initialized randomly on spawn.

Bug: #259
  • Loading branch information
djeedai committed Dec 2, 2023
1 parent 730cec8 commit 15d8a35
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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.
- Added 4 new expressions for packing and unpacking a `vec4<f32>` into a `u32`: `pack4x8snorm`, `pack4x8unorm`, `unpack4x8snorm`, `unpack4x8unorm`. This is particularly useful to convert between `COLOR` (`u32`) and `HDR_COLOR` (`vec4<f32>`). See the `billboard.rs` example for a use case. (#259)

### Changed

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,18 +287,18 @@ cargo run --example lifetime --features="bevy/bevy_winit bevy/bevy_pbr 3d"

### Billboard

This example demonstrates particles with the billboard render modifier, making them always face the camera. It also demonstrates the use of alpha cutoff to filter out texture samples below a certain threshold, varying this threshold back and forth between 0 and 1.
This example demonstrates particles with the billboard render modifier, making them always face the camera. It also demonstrates the use of alpha cutoff to filter out texture samples below a certain threshold, varying this threshold back and forth between 0 and 1. Finally, the example uses attributes to store per-particle data like a color or in-plane rotation.

```shell
cargo run --example billboard --features="bevy/bevy_winit bevy/bevy_pbr bevy/png 3d"
```

The image on the left has the `BillboardModifier` enabled.

![billboard](https://github.com/djeedai/bevy_hanabi/0a04904e589ddbc6b8dd34614850fc850d99a3a5/examples/billboard.png)
![billboard](./examples/billboard.gif)

## Feature List

This list contains the major fixed features provided by 🎆 Hanabi. Beyond that, with the power of the [Expressions API](https://docs.rs/bevy_hanabi/latest/bevy_hanabi/graph/expr/index.html), visual effect authors can further customize their effects by assigning individual particle attributes (position, color, _etc._).

- Spawn
- [x] Constant rate
- [x] One-time burst
Expand Down
Binary file added examples/billboard.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed examples/billboard.png
Binary file not shown.
53 changes: 40 additions & 13 deletions examples/billboard.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
//! An example using the [`OrientModifier`] to force particles to always render
//! facing the camera, even when the view moves. This is particularly beneficial
//! facing the camera, even when the view moves. This is particularly useful
//! with flat particles, to prevent stretching and maintain the illusion of 3D.
//!
//! This example also demonstrates the use of [`AlphaMode::Mask`] to render
//! particles with an alpha mask threshold. This feature is generally useful to
//! obtain non-square "cutout" opaque shapes, or to produce some
//! obtain non-square "cutout" opaque shapes. The alpha cutoff value is animated
//! over time with an expression, to show how the value affects the shape of the
//! particle.
//!
//! To obtain some visual diversity, each particle is spawned with its own
//! random color and random in-plane rotation.
//! - The color is stored into the per-particle [`Attribute::COLOR`], and
//! automatically used in the render pass as the base color of the particle.
//! The random value is a 4-component floating-point vector `vec4<f32>`
//! obtained with the `rand()` expression, and is converted to a
//! low-resolution `u32` RGBA color with the `pack4x8unorm()` expression.
//! - There's no built-in attribute to store the rotation angle, so the example
//! makes use of the [`Attribute::F32_0`] attribute, one of the generic
//! attibutes which you can use to store any per-particle floating-point
//! value. The attribute is used here to storethe in-plane rotation angle, in
//! radians, which will be passed to the [`OrientModifier`] to rotate the
//! particles around their normal.
//!
//! Note: Particles can sometimes flicker. This is a current limitation of
//! Hanabi, which doesn't yet have any particle sorting feature, so the
//! rendering order may vary frame to frame. This is tracked on GitHub as issue
//! #183.

use bevy::{
core_pipeline::tonemapping::Tonemapping,
Expand Down Expand Up @@ -73,11 +94,6 @@ fn setup(

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

let mut gradient = Gradient::new();
gradient.add_key(0.0, Vec4::ONE);
gradient.add_key(0.5, Vec4::ONE);
gradient.add_key(1.0, Vec4::new(1.0, 1.0, 1.0, 0.0));

let writer = ExprWriter::new();

let age = writer.lit(0.).expr();
Expand All @@ -99,20 +115,31 @@ fn setup(
speed: (writer.lit(0.5) + writer.lit(0.2) * writer.rand(ScalarType::Float)).expr(),
};

// To give some visual diversity, we initialize each spawned particle with a
// random per-particle color. The COLOR attribute is read back in the vertex
// shader to initialize the particle's base color, which is later modulated
// in this example with the texture of the ParticleTextureModifier.
// Note that the ParticleTextureModifier uses
// ImageSampleMapping::ModulateOpacityFromR so it will override
// the alpha component of the color. Therefore we don't need to care about
// rand() assigning a transparent value and making the particle invisible.
let color = writer.rand(VectorType::VEC4F).pack4x8unorm();
let init_color = SetAttributeModifier::new(Attribute::COLOR, color.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);
let rotation = (writer.rand(ScalarType::Float) * writer.lit(std::f32::consts::TAU)).expr();
let init_rotation = SetAttributeModifier::new(Attribute::F32_0, 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();
((writer.time() * writer.lit(2.)).sin() * writer.lit(0.3) + writer.lit(0.4)).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 rotation_attr = writer.attr(Attribute::F32_0).expr();

let effect = effects.add(
EffectAsset::new(32768, Spawner::rate(64.0.into()), writer.finish())
Expand All @@ -123,15 +150,15 @@ fn setup(
.init(init_age)
.init(init_lifetime)
.init(init_rotation)
.init(init_color)
.render(ParticleTextureModifier {
texture: texture_handle,
sample_mapping: ImageSampleMapping::ModulateOpacityFromR,
})
.render(OrientModifier {
mode: OrientMode::FaceCameraPosition,
rotation: Some(rotation),
rotation: Some(rotation_attr),
})
.render(ColorOverLifetimeModifier { gradient })
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant([0.2; 2].into()),
screen_space_size: false,
Expand Down
5 changes: 5 additions & 0 deletions examples/circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ fn setup(
let anim_img = make_anim_img(sprite_size, sprite_grid_size, Vec3::new(0.1, 0.1, 0.1));
let texture_handle = images.add(anim_img);

// The sprites form a grid, with a total animation frame count equal to the
// number of sprites.
let frame_count = sprite_grid_size.x * sprite_grid_size.y;

let mut gradient = Gradient::new();
Expand All @@ -86,6 +88,9 @@ fn setup(
let age = writer.rand(ScalarType::Float).expr();
let init_age = SetAttributeModifier::new(Attribute::AGE, age);

// All particles stay alive until their AGE is 5 seconds. Note that this doesn't
// mean they live for 5 seconds; if the AGE is initialized to a non-zero value
// at spawn, the total particle lifetime is (LIFETIME - AGE).
let lifetime = writer.lit(5.).expr();
let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, lifetime);

Expand Down
142 changes: 142 additions & 0 deletions src/graph/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,14 @@ impl Module {
impl_module_unary!(log, Log);
impl_module_unary!(log2, Log2);
impl_module_unary!(normalize, Normalize);
impl_module_unary!(pack4x8snorm, Pack4x8snorm);
impl_module_unary!(pack4x8unorm, Pack4x8unorm);
impl_module_unary!(saturate, Saturate);
impl_module_unary!(sign, Sign);
impl_module_unary!(sin, Sin);
impl_module_unary!(tan, Tan);
impl_module_unary!(unpack4x8snorm, Unpack4x8snorm);
impl_module_unary!(unpack4x8unorm, Unpack4x8unorm);
impl_module_unary!(w, W);
impl_module_unary!(x, X);
impl_module_unary!(y, Y);
Expand Down Expand Up @@ -1287,6 +1291,22 @@ pub enum UnaryOperator {
/// operands.
Normalize,

/// Packing operator from `vec4<f32>` to `u32` (signed normalized).
///
/// Convert the four components of a signed normalized floating point vector
/// into a signed integral `i8` value in `[-128:127]`, then pack those
/// four values into a single `u32`. Each vector component should be in
/// `[-1:1]` before packing; values outside this range are clamped.
Pack4x8snorm,

/// Packing operator from `vec4<f32>` to `u32` (unsigned normalized).
///
/// Convert the four components of an unsigned normalized floating point
/// vector into an unsigned integral `u8` value in `[0:255]`, then pack
/// those four values into a single `u32`. Each vector component should
/// be in `[0:1]` before packing; values outside this range are clamped.
Pack4x8unorm,

/// Saturate operator.
///
/// Clamp the value of the operand to the \[0:1\] range, component-wise for
Expand All @@ -1310,6 +1330,19 @@ pub enum UnaryOperator {
/// Tangent operator.
Tan,

/// Unpacking operator from `u32` to `vec4<f32>` (signed normalized).
///
/// Unpack the `u32` into four signed integral `i8` value in `[-128:127]`,
/// then convert each value to a signed normalized `f32` value in `[-1:1]`.
Unpack4x8snorm,

/// Unpacking operator from `u32` to `vec4<f32>` (unsigned normalized).
///
/// Unpack the `u32` into four unsigned integral `u8` value in `[0:255]`,
/// then convert each value to an unsigned normalized `f32` value in
/// `[0:1]`.
Unpack4x8unorm,

/// Get the fourth component of a vector.
///
/// This is only valid for vectors of rank 4.
Expand Down Expand Up @@ -1361,10 +1394,14 @@ impl ToWgslString for UnaryOperator {
UnaryOperator::Log => "log".to_string(),
UnaryOperator::Log2 => "log2".to_string(),
UnaryOperator::Normalize => "normalize".to_string(),
UnaryOperator::Pack4x8snorm => "pack4x8snorm".to_string(),
UnaryOperator::Pack4x8unorm => "pack4x8unorm".to_string(),
UnaryOperator::Saturate => "saturate".to_string(),
UnaryOperator::Sign => "sign".to_string(),
UnaryOperator::Sin => "sin".to_string(),
UnaryOperator::Tan => "tan".to_string(),
UnaryOperator::Unpack4x8snorm => "unpack4x8snorm".to_string(),
UnaryOperator::Unpack4x8unorm => "unpack4x8unorm".to_string(),
UnaryOperator::W => "w".to_string(),
UnaryOperator::X => "x".to_string(),
UnaryOperator::Y => "y".to_string(),
Expand Down Expand Up @@ -2164,6 +2201,52 @@ impl WriterExpr {
self.unary_op(UnaryOperator::Normalize)
}

/// Apply the "pack4x8snorm" operator to the current 4-component float
/// vector expression.
///
/// This is a unary operator, which applies to 4-component float vector
/// operand expressions to produce a single `u32` scalar expression.
///
/// # Example
///
/// ```
/// # use bevy_hanabi::*;
/// # use bevy::math::Vec4;
/// # let mut w = ExprWriter::new();
/// // A literal expression `x = vec4<f32>(-1., 1., 0., 7.2);`.
/// let x = w.lit(Vec4::new(-1., 1., 0., 7.2));
///
/// // Pack: `y = pack4x8snorm(x);`
/// let y = x.pack4x8snorm(); // 0x7F007FFFu32
/// ```
#[inline]
pub fn pack4x8snorm(self) -> Self {
self.unary_op(UnaryOperator::Pack4x8snorm)
}

/// Apply the "pack4x8unorm" operator to the current 4-component float
/// vector expression.
///
/// This is a unary operator, which applies to 4-component float vector
/// operand expressions to produce a single `u32` scalar expression.
///
/// # Example
///
/// ```
/// # use bevy_hanabi::*;
/// # use bevy::math::Vec4;
/// # let mut w = ExprWriter::new();
/// // A literal expression `x = vec4<f32>(-1., 1., 0., 7.2);`.
/// let x = w.lit(Vec4::new(-1., 1., 0., 7.2));
///
/// // Pack: `y = pack4x8unorm(x);`
/// let y = x.pack4x8unorm(); // 0xFF00FF00u32
/// ```
#[inline]
pub fn pack4x8unorm(self) -> Self {
self.unary_op(UnaryOperator::Pack4x8unorm)
}

/// Apply the "sign" operator to the current float scalar or vector
/// expression.
///
Expand Down Expand Up @@ -2236,6 +2319,54 @@ impl WriterExpr {
self.unary_op(UnaryOperator::Tan)
}

/// Apply the "unpack4x8snorm" operator to the current `u32` scalar
/// expression.
///
/// This is a unary operator, which applies to `u32` scalar operand
/// expressions to produce a 4-component floating point vector of signed
/// normalized components in `[-1:1]`.
///
/// # Example
///
/// ```
/// # use bevy_hanabi::*;
/// # use bevy::math::Vec3;
/// # let mut w = ExprWriter::new();
/// // A literal expression `y = 0x7F007FFFu32;`.
/// let y = w.lit(0x7F007FFFu32);
///
/// // Unpack: `x = unpack4x8snorm(y);`
/// let x = y.unpack4x8snorm(); // vec4<f32>(-1., 1., 0., 7.2)
/// ```
#[inline]
pub fn unpack4x8snorm(self) -> Self {
self.unary_op(UnaryOperator::Unpack4x8snorm)
}

/// Apply the "unpack4x8unorm" operator to the current `u32` scalar
/// expression.
///
/// This is a unary operator, which applies to `u32` scalar operand
/// expressions to produce a 4-component floating point vector of unsigned
/// normalized components in `[0:1]`.
///
/// # Example
///
/// ```
/// # use bevy_hanabi::*;
/// # use bevy::math::Vec3;
/// # let mut w = ExprWriter::new();
/// // A literal expression `y = 0xFF00FF00u32;`.
/// let y = w.lit(0xFF00FF00u32);
///
/// // Unpack: `x = unpack4x8unorm(y);`
/// let x = y.unpack4x8unorm(); // vec4<f32>(-1., 1., 0., 7.2)
/// ```
#[inline]
pub fn unpack4x8unorm(self) -> Self {
self.unary_op(UnaryOperator::Unpack4x8unorm)
}

/// Apply the "saturate" operator to the current float scalar or vector
/// expression.
///
Expand Down Expand Up @@ -3174,6 +3305,9 @@ mod tests {
let y = m.lit(Vec3::new(1., -3.1, 6.99));
let z = m.lit(BVec3::new(false, true, false));
let w = m.lit(Vec4::W);
let v = m.lit(Vec4::new(-1., 1., 0., 7.2));
let us = m.lit(0x0u32);
let uu = m.lit(0x0u32);

let abs = m.abs(x);
let all = m.all(z);
Expand All @@ -3188,10 +3322,14 @@ mod tests {
let log = m.log(y);
let log2 = m.log2(y);
let norm = m.normalize(y);
let pack4x8snorm = m.pack4x8snorm(v);
let pack4x8unorm = m.pack4x8unorm(v);
let saturate = m.saturate(y);
let sign = m.sign(y);
let sin = m.sin(y);
let tan = m.tan(y);
let unpack4x8snorm = m.unpack4x8snorm(us);
let unpack4x8unorm = m.unpack4x8unorm(uu);
let comp_x = m.x(w);
let comp_y = m.y(w);
let comp_z = m.z(w);
Expand Down Expand Up @@ -3219,10 +3357,14 @@ mod tests {
(log, "log", "vec3<f32>(1.,-3.1,6.99)"),
(log2, "log2", "vec3<f32>(1.,-3.1,6.99)"),
(norm, "normalize", "vec3<f32>(1.,-3.1,6.99)"),
(pack4x8snorm, "pack4x8snorm", "vec4<f32>(-1.,1.,0.,7.2)"),
(pack4x8unorm, "pack4x8unorm", "vec4<f32>(-1.,1.,0.,7.2)"),
(saturate, "saturate", "vec3<f32>(1.,-3.1,6.99)"),
(sign, "sign", "vec3<f32>(1.,-3.1,6.99)"),
(sin, "sin", "vec3<f32>(1.,-3.1,6.99)"),
(tan, "tan", "vec3<f32>(1.,-3.1,6.99)"),
(unpack4x8snorm, "unpack4x8snorm", "0"),
(unpack4x8unorm, "unpack4x8unorm", "0"),
] {
let expr = ctx.eval(&m, expr);
assert!(expr.is_ok());
Expand Down

0 comments on commit 15d8a35

Please sign in to comment.