Skip to content

Commit

Permalink
Add alpha masking (djeedai#214)
Browse files Browse the repository at this point in the history
Add an alternative alpha blending mode, alpha masking, based on a cutoff
threshold, and producing opaque particles emitted into the `AlphaMask3d`
render phase (for 3D views).

Add a new `BuiltInOperator::AlphaCutoff` to allow accessing and
modifying that cutoff value from the fragment shader.

Allow the render context to evaluate expressions, and add the missing
access to simulation parameters.

Update the billboard example to use alpha masking with a dynamically
varying alpha cutoff driven by the simulation time.
  • Loading branch information
djeedai authored Aug 8, 2023
1 parent 8854f75 commit 314ae9e
Show file tree
Hide file tree
Showing 15 changed files with 630 additions and 413 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add access to `ModifierContext` and `ParticleLayout` from the `EvalContext` when evaluating modifiers.
- Added `SimulationSpace::eval()` to evaluate a context-specific expression allowing to transform the particles to the proper simulation space.
- Added a few more functions to `Gradient<T>`: `is_empty(` and `len()` which do as implied, `from_keys()` which creates a new gradient from a key point iterator, and `with_key()` and `with_keys()` which append one or more keys to an existing gradient.
- Added `AlphaMode` and the ability to render particles with alpha masking instead of alpha blending. This is controlled by `EffectAsset::alpha_mode` and the new `EffectAsset::with_alpha_mode()` helper.
- Added a new `BuiltInOperator::AlphaCutoff` value and associated expression, which represent the alpha cutoff threshold when rendering an effect with alpha masking. The `billboard` example has been updated to show how to use that value, and even dynamically change it with an expression.

### Changed

Expand All @@ -20,12 +22,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Gradient<T>::new()`, `Gradient<T>::constant()`, and `Gradient<T>::linear()` do not require the `T: Default` trait bound anymore. The bound had been added by mistake, and is not necessary.
- `Gradient<T>::new()` is now a `const fn`.
- `Gradient<T>::constant()` and `Gradient<T>::linear()` do not attempt to perform linear searches anymore; instead they directly create the `Gradient<T>` object from scratch. This should not have any real consequence in practice though.
- Changed `CompiledParticleEffect` to store a `LayoutFlags` instead of individual boolean values, for convenience and consistency with the internal representation.
- Changed `RenderContext` to implement `EvalContext`. This allows render modifiers to use the expression API.

### Removed

- Removed the `BillboardModifier`; this is superseded by the `OrientModifier { mode: OrientMode::ParallelCameraDepthPlane }`.
- Removed the `OrientAlongVelocityModifier`; this is superseded by the `OrientModifier { mode: OrientMode::AlongVelocity }`.

### Fixed

- Render modifiers can now access simulation parameters (time, delta time) like in any other context.

## [0.7.0] 2023-07-17

### Added
Expand Down
Binary file modified assets/cloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions examples/billboard.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
//! An example using the [`OrientModifier`] to force
//! particles to always render facing the camera.
//! An example using the [`OrientModifier`] to force particles to always render
//! facing the camera, even when the view moves. This is particularly beneficial
//! 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
use bevy::{
core_pipeline::tonemapping::Tonemapping,
Expand Down Expand Up @@ -92,9 +97,15 @@ fn setup(
speed: (writer.lit(0.5) + writer.lit(0.2) * writer.rand(ScalarType::Float)).expr(),
};

// 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();

let effect = effects.add(
EffectAsset::new(32768, Spawner::rate(64.0.into()), writer.finish())
.with_name("billboard")
.with_alpha_mode(bevy_hanabi::AlphaMode::Mask(alpha_cutoff))
.init(init_pos)
.init(init_vel)
.init(init_age)
Expand Down
6 changes: 3 additions & 3 deletions examples/circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ fn setup(
let texture_handle: Handle<Image> = asset_server.load("cloud.png");

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

let writer = ExprWriter::new();

Expand Down
4 changes: 2 additions & 2 deletions examples/force_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn setup(
material: materials.add(StandardMaterial {
base_color: Color::rgba(0., 0.7, 0., 0.3),
unlit: true,
alpha_mode: AlphaMode::Blend,
alpha_mode: bevy::pbr::AlphaMode::Blend,
..Default::default()
}),
..Default::default()
Expand All @@ -147,7 +147,7 @@ fn setup(
material: materials.add(StandardMaterial {
base_color: Color::rgba(0.7, 0., 0., 0.3),
unlit: true,
alpha_mode: AlphaMode::Blend,
alpha_mode: bevy::pbr::AlphaMode::Blend,
..Default::default()
}),
transform: Transform::from_translation(Vec3::new(-2., -1., 0.1)),
Expand Down
11 changes: 6 additions & 5 deletions examples/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ fn setup(
let texture_handle: Handle<Image> = asset_server.load("cloud.png");

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

let writer = ExprWriter::new();
Expand Down Expand Up @@ -164,8 +164,9 @@ fn lemniscate(time: f32, radius: f32) -> Vec2 {
let sign = theta.cos().signum();
let theta = theta.sin() * PI_OVER_4;

// Solve the polar equation to build the parametric position
let r2 = radius * radius * (theta * 2.0).cos();
// Solve the polar equation to build the parametric position. Clamp to positive
// values for r2 due to numerical errors infrequently yielding negative values.
let r2 = (radius * radius * (theta * 2.0).cos()).max(0.);
let r = r2.sqrt().copysign(sign);

// Convert to cartesian coordinates
Expand Down
99 changes: 88 additions & 11 deletions src/asset.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use bevy::{
asset::{AssetLoader, LoadContext, LoadedAsset},
reflect::{Reflect, TypeUuid},
utils::{BoxedFuture, HashSet},
utils::{default, BoxedFuture, HashSet},
};
use serde::{Deserialize, Serialize};

use crate::{
graph::Value,
modifier::{InitModifier, RenderModifier, UpdateModifier},
BoxedModifier, Module, ParticleLayout, Property, PropertyLayout, SimulationSpace, Spawner,
BoxedModifier, ExprHandle, Module, ParticleLayout, Property, PropertyLayout, SimulationSpace,
Spawner,
};

/// Type of motion integration applied to the particles of a system.
Expand Down Expand Up @@ -76,6 +77,82 @@ pub enum SimulationCondition {
Always,
}

/// Alpha mode for rendering an effect.
///
/// The alpha mode determines how the alpha value of a particle is used to
/// render it. In general effects use semi-transparent particles. However, there
/// are multiple alpha blending techniques available, producing different
/// results.
///
/// This is very similar to the [`bevy::pbr::AlphaMode`] of the `bevy_pbr`
/// crate, except that a different set of values is supported which reflects
/// what this library currently supports.
///
/// The alpha mode only affects the render phase that particles are rendered
/// into when rendering 3D views. For 2D views, all particle effects are
/// rendered during the [`Transparent2d`] render phase.
///
/// [`Transparent2d`]: bevy::core_pipeline::core_2d::Transparent2d
#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AlphaMode {
/// Render the effect with alpha blending.
///
/// This is the most common mode for handling transparency. It uses the
/// "blend" or "over" formula, where the color of each particle fragment is
/// accumulated into the destination render target after being modulated by
/// its alpha value.
///
/// ```txt
/// dst_color = src_color * (1 - particle_alpha) + particle_color * particle_alpha;
/// dst_alpha = src_alpha * (1 - particle_alpha) + particle_alpha
/// ```
///
/// This is the default blending mode.
///
/// For 3D views, effects with this mode are rendered during the
/// [`Transparent3d`] render phase.
///
/// [`Transparent3d`]: bevy::core_pipeline::core_3d::Transparent3d
#[default]
Blend,

/// Render the effect with alpha masking.
///
/// With this mode, the final alpha value computed per particle fragment is
/// compared against the cutoff value stored in this enum. Any fragment
/// with a value under the cutoff is discarded, while any fragment with
/// a value equal or over the cutoff becomes fully opaque. The end result is
/// an opaque particle with a cutout shape.
///
/// ```txt
/// if src_alpha >= cutoff {
/// dst_color = particle_color;
/// dst_alpha = 1;
/// } else {
/// discard;
/// }
/// ```
///
/// The assigned expression must yield a scalar floating-point value,
/// typically in the \[0:1\] range. This expression is assigned at the
/// beginning of the fragment shader to the special built-in `alpha_cutoff`
/// variable, which can be further accessed and modified by render
/// modifiers.
///
/// The cutoff threshold comparison of the fragment's alpha value against
/// `alpha_cutoff` is performed as the last operation in the fragment
/// shader. This allows modifiers to affect the alpha value of the
/// particle before it's tested against the cutoff value stored in
/// `alpha_cutoff`.
///
/// For 3D views, effects with this mode are rendered during the
/// [`AlphaMask3d`] render phase.
///
/// [`AlphaMask3d`]: bevy::core_pipeline::core_3d::AlphaMask3d
Mask(ExprHandle),
}

/// Asset describing a visual effect.
///
/// The effect can be instanciated with a [`ParticleEffect`] component, or a
Expand Down Expand Up @@ -142,6 +219,8 @@ pub struct EffectAsset {
pub motion_integration: MotionIntegration,
/// Expression module for this effect.
module: Module,
/// Alpha mode.
pub alpha_mode: AlphaMode,
}

impl EffectAsset {
Expand Down Expand Up @@ -196,18 +275,10 @@ impl EffectAsset {
/// [`Expr`]: crate::graph::expr::Expr
pub fn new(capacity: u32, spawner: Spawner, module: Module) -> Self {
Self {
name: String::new(),
capacity,
spawner,
z_layer_2d: 0.,
simulation_space: SimulationSpace::default(),
simulation_condition: SimulationCondition::default(),
init_modifiers: vec![],
update_modifiers: vec![],
render_modifiers: vec![],
properties: vec![],
motion_integration: MotionIntegration::default(),
module,
..default()
}
}

Expand Down Expand Up @@ -257,6 +328,12 @@ impl EffectAsset {
self
}

/// Set the alpha mode.
pub fn with_alpha_mode(mut self, alpha_mode: AlphaMode) -> Self {
self.alpha_mode = alpha_mode;
self
}

/// Add a new property to the asset.
///
/// See [`Property`] for more details on what effect properties are.
Expand Down
32 changes: 32 additions & 0 deletions src/graph/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,17 @@ pub enum BuiltInOperator {
/// [the PCG family]: https://www.pcg-random.org/
/// [`Spawner`]: crate::Spawner
Rand(ValueType),
/// Value of the alpha cutoff for alpha masking.
///
/// This value is only available in the render context. It represents the
/// current threshold, generally in \[0:1\], which the particle's fragment
/// alpha value will be compared against to determine alpha masking.
///
/// The value is initalized at the beginning of the fragment shader to the
/// expression stored in [`AlphaMode::Mask`].
///
/// [`AlphaMode::Mask`]: crate::AlphaMode::Mask
AlphaCutoff,
}

impl BuiltInOperator {
Expand Down Expand Up @@ -820,6 +831,7 @@ impl BuiltInOperator {
}
ValueType::Matrix(_) => panic!("Invalid BuiltInOperator::Rand(ValueType::Matrix)."),
},
BuiltInOperator::AlphaCutoff => "alpha_cutoff",
}
}

Expand All @@ -829,6 +841,7 @@ impl BuiltInOperator {
BuiltInOperator::Time => ValueType::Scalar(ScalarType::Float),
BuiltInOperator::DeltaTime => ValueType::Scalar(ScalarType::Float),
BuiltInOperator::Rand(value_type) => *value_type,
BuiltInOperator::AlphaCutoff => ValueType::Scalar(ScalarType::Float),
}
}

Expand Down Expand Up @@ -1270,6 +1283,25 @@ impl ExprWriter {
))))
}

/// Create a new writer expression representing the alpha cutoff value used
/// for alpha masking.
///
/// This expression is only valid when used in the context of the fragment
/// shader, in the render context.
///
/// # Example
///
/// ```
/// # use bevy_hanabi::*;
/// let mut w = ExprWriter::new();
/// let x = w.alpha_cutoff(); // x = alpha_cutoff;
/// ```
pub fn alpha_cutoff(&self) -> WriterExpr {
self.push(Expr::BuiltIn(BuiltInExpr::new(
BuiltInOperator::AlphaCutoff,
)))
}

/// Finish using the writer, and recover the [`Module`] where all [`Expr`]
/// were written by the writer.
///
Expand Down
Loading

0 comments on commit 314ae9e

Please sign in to comment.