Skip to content

Commit

Permalink
Allow particles to be arbitrary meshes.
Browse files Browse the repository at this point in the history
Currently, Hanabi requires that particles be 2D quads. This is
sufficient for a good deal of VFX, but in many cases 3D objects are
required: smoke puffs, bullet casings, etc. This commit fixes this
deficiency by allowing particles to take on arbitrary meshes. To set the
mesh of a particle, use the new `EffectAsset::mesh` builder method. By
default, the mesh is a 2D quad.

The implementation is straightforward. The previously-hard-wired quad
vertices have been replaced with a `Handle<Mesh>`. The patch uses the
existing `bevy_render` infrastructure to upload the mesh to the GPU and
retrieve the vertices. Perhaps the most significant change is the
generalization of rendering to allow for indexed drawing in addition to
non-indexed. Because indexed drawing has a different on-GPU format for
indirect draw commands from that of non-indirect draw commands, some
additional bookkeeping is required.

This patch also adds support for a few features useful for 3D rendering:

* A `size3` attribute has been added, to allow the size to be controlled
  in 3D.

* The `SetSizeModifier` now takes a 3D size gradient instead of a 2D
  one.

* Vertex normals are available to modifiers via the `normal` shader
  variable, as long as they call the new
  `RenderContext::set_needs_normal` method.

A new example, `puffs`, has been added to demonstrate the use of 3D
meshes. It depicts the Bevy test fox running with cartoony smoke puffs
emitted at a constant rate behind it. Each puff consists of multiple
spherical mesh particles offset with some random jitter. A custom
Lambertian lighting modifier is supplied with the example, in order to
make the smoke puffs not appear solid white. (This modifier dramatically
improves the look of this example, but it's very limited, so I didn't
upstream it to Hanabi proper. A proper PBR lighting modifier would be
useful, but would be a significant amount of work, so I chose to defer
that to a follow-up.)
  • Loading branch information
pcwalton committed Oct 16, 2024
1 parent 1ad6298 commit b7a10d9
Show file tree
Hide file tree
Showing 24 changed files with 801 additions and 182 deletions.
17 changes: 17 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ version = "0.14"
default-features = false
features = [ "bevy_core_pipeline", "bevy_render", "bevy_asset", "x11" ]

[dependencies.bevy_gltf]
version = "0.14"
optional = true
features = [ "bevy_animation" ]

[package.metadata.docs.rs]
all-features = true

Expand Down Expand Up @@ -162,6 +167,18 @@ required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]
name = "ordering"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]

[[example]]
name = "puffs"
required-features = [
"bevy/bevy_winit",
"bevy/bevy_pbr",
"bevy/bevy_scene",
"bevy/bevy_gltf",
"bevy/bevy_animation",
"bevy_gltf/bevy_animation",
"3d",
]

[[test]]
name = "empty_effect"
path = "gpu_tests/empty_effect.rs"
Expand Down
Binary file added assets/Fox.glb
Binary file not shown.
2 changes: 1 addition & 1 deletion examples/2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fn setup(
.init(init_age)
.init(init_lifetime)
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant(Vec2::splat(0.02)),
gradient: Gradient::constant(Vec3::splat(0.02)),
screen_space_size: false,
})
.render(ColorOverLifetimeModifier { gradient })
Expand Down
2 changes: 1 addition & 1 deletion examples/billboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ fn setup(
rotation: Some(rotation_attr),
})
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant([0.2; 2].into()),
gradient: Gradient::constant([0.2; 3].into()),
screen_space_size: false,
}),
);
Expand Down
2 changes: 1 addition & 1 deletion examples/circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ fn setup(
.render(FlipbookModifier { sprite_grid_size })
.render(ColorOverLifetimeModifier { gradient })
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant([0.5; 2].into()),
gradient: Gradient::constant([0.5; 3].into()),
screen_space_size: false,
}),
);
Expand Down
4 changes: 2 additions & 2 deletions examples/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
color_gradient.add_key(1.0, Vec4::new(0.0, 0.0, 0.0, 0.0));

let mut size_gradient = Gradient::new();
size_gradient.add_key(0.3, Vec2::new(0.2, 0.02));
size_gradient.add_key(1.0, Vec2::ZERO);
size_gradient.add_key(0.3, Vec3::new(0.2, 0.02, 1.0));
size_gradient.add_key(1.0, Vec3::ZERO);

let writer = ExprWriter::new();

Expand Down
6 changes: 3 additions & 3 deletions examples/firework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
color_gradient1.add_key(1.0, Vec4::new(4.0, 0.0, 0.0, 0.0));

let mut size_gradient1 = Gradient::new();
size_gradient1.add_key(0.0, Vec2::splat(0.05));
size_gradient1.add_key(0.3, Vec2::splat(0.05));
size_gradient1.add_key(1.0, Vec2::splat(0.0));
size_gradient1.add_key(0.0, Vec3::splat(0.05));
size_gradient1.add_key(0.3, Vec3::splat(0.05));
size_gradient1.add_key(1.0, Vec3::splat(0.0));

let writer = ExprWriter::new();

Expand Down
2 changes: 1 addition & 1 deletion examples/force_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ fn setup(
.update(allow_zone)
.update(deny_zone)
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant(Vec2::splat(0.05)),
gradient: Gradient::constant(Vec3::splat(0.05)),
screen_space_size: false,
})
.render(ColorOverLifetimeModifier { gradient }),
Expand Down
8 changes: 4 additions & 4 deletions examples/multicam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ struct SplitCamera {

fn make_effect(color: Color) -> EffectAsset {
let mut size_gradient = Gradient::new();
size_gradient.add_key(0.0, Vec2::splat(1.0));
size_gradient.add_key(0.5, Vec2::splat(5.0));
size_gradient.add_key(0.8, Vec2::splat(0.8));
size_gradient.add_key(1.0, Vec2::splat(0.0));
size_gradient.add_key(0.0, Vec3::splat(1.0));
size_gradient.add_key(0.5, Vec3::splat(5.0));
size_gradient.add_key(0.8, Vec3::splat(0.8));
size_gradient.add_key(1.0, Vec3::splat(0.0));

let mut color_gradient = Gradient::new();
color_gradient.add_key(0.0, Vec4::splat(1.0));
Expand Down
6 changes: 3 additions & 3 deletions examples/ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ fn make_firework() -> EffectAsset {
// Keep the size large so we can more visibly see the particles for longer, and
// see the effect of alpha blending.
let mut size_gradient1 = Gradient::new();
size_gradient1.add_key(0.0, Vec2::ONE);
size_gradient1.add_key(0.1, Vec2::ONE);
size_gradient1.add_key(1.0, Vec2::ZERO);
size_gradient1.add_key(0.0, Vec3::ONE);
size_gradient1.add_key(0.1, Vec3::ONE);
size_gradient1.add_key(1.0, Vec3::ZERO);

let writer = ExprWriter::new();

Expand Down
4 changes: 2 additions & 2 deletions examples/portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
color_gradient1.add_key(1.0, Vec4::new(4.0, 0.0, 0.0, 0.0));

let mut size_gradient1 = Gradient::new();
size_gradient1.add_key(0.3, Vec2::new(0.2, 0.02));
size_gradient1.add_key(1.0, Vec2::splat(0.0));
size_gradient1.add_key(0.3, Vec3::new(0.2, 0.02, 1.0));
size_gradient1.add_key(1.0, Vec3::splat(0.0));

let writer = ExprWriter::new();

Expand Down
262 changes: 262 additions & 0 deletions examples/puffs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//! Puffs
//!
//! This example creates cartoony smoke puffs out of spherical meshes.

use crate::utils::*;
use bevy::{
color::palettes::css::FOREST_GREEN,
core_pipeline::tonemapping::Tonemapping,
math::vec3,
prelude::*,
render::mesh::{SphereKind, SphereMeshBuilder},
};
use bevy_hanabi::prelude::*;
use serde::{Deserialize, Serialize};
use std::{error::Error, f32::consts::FRAC_PI_2};

mod utils;

// A simple custom modifier that lights the meshes with Lambertian lighting.
// Other lighting models are possible, up to and including PBR.
#[derive(Clone, Copy, Reflect, Serialize, Deserialize)]
struct LambertianLightingModifier {
// The direction that light is coming from, in particle system space.
light_direction: Vec3,
// The brightness of the ambient light (which is assumed to be white in
// this example).
ambient: f32,
}

// The position of the light in the scene.
static LIGHT_POSITION: Vec3 = vec3(-20.0, 40.0, 5.0);

fn main() -> Result<(), Box<dyn Error>> {
let app_exit = make_test_app("puffs")
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 500.0,
})
.add_systems(Startup, setup)
.add_systems(Update, setup_scene_once_loaded)
.run();
app_exit.into_result()
}

// Performs initialization of the scene.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut effects: ResMut<Assets<EffectAsset>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawn the camera.
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(25.0, 15.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y),
camera: Camera {
hdr: true,
clear_color: Color::BLACK.into(),
..default()
},
tonemapping: Tonemapping::None,
..default()
});

// Spawn the fox.
commands.spawn(SceneBundle {
scene: asset_server.load("Fox.glb#Scene0"),
transform: Transform::from_scale(Vec3::splat(0.1)),
..default()
});

// Spawn the circular base.
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(15.0)),
material: materials.add(StandardMaterial {
base_color: FOREST_GREEN.into(),
..default()
}),
transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)),
..default()
});

// Spawn a light.
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
color: Color::WHITE,
illuminance: 2000.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_translation(LIGHT_POSITION).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});

// Create the mesh.
let mesh = meshes.add(SphereMeshBuilder::new(0.5, SphereKind::Ico { subdivisions: 4 }).build());

// Create the effect asset.
let effect = create_effect(mesh, &mut effects);

// Spawn the effect.
commands.spawn((
Name::new("cartoon explosion"),
ParticleEffectBundle {
effect: ParticleEffect::new(effect),
..default()
},
));
}

// Builds the smoke puffs.
fn create_effect(mesh: Handle<Mesh>, effects: &mut Assets<EffectAsset>) -> Handle<EffectAsset> {
let writer = ExprWriter::new();

// Position the particle laterally within a small radius.
let init_xz_pos = SetPositionCircleModifier {
center: writer.lit(Vec3::ZERO).expr(),
axis: writer.lit(Vec3::Z).expr(),
radius: writer.lit(1.0).expr(),
dimension: ShapeDimension::Volume,
};

// Position the particle vertically. Jiggle it a little bit for variety's
// sake.
let init_y_pos = SetAttributeModifier::new(
Attribute::POSITION,
writer
.attr(Attribute::POSITION)
.add(writer.rand(VectorType::VEC3F) * writer.lit(vec3(0.0, 1.0, 0.0)))
.expr(),
);

// Set up the age and lifetime.
let init_age = SetAttributeModifier::new(Attribute::AGE, writer.lit(0.0).expr());
let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, writer.lit(3.0).expr());

// Vary the size a bit.
let init_size = SetAttributeModifier::new(
Attribute::F32_0,
(writer.rand(ScalarType::Float) * writer.lit(2.0) + writer.lit(0.5)).expr(),
);

// Make the particles move backwards at a constant speed.
let init_velocity = SetAttributeModifier::new(
Attribute::VELOCITY,
writer.lit(vec3(0.0, 0.0, -20.0)).expr(),
);

// Make the particles shrink over time.
let update_size = SetAttributeModifier::new(
Attribute::SIZE,
writer
.attr(Attribute::F32_0)
.mul(
writer
.lit(1.0)
.sub((writer.attr(Attribute::AGE)).mul(writer.lit(0.75)))
.max(writer.lit(0.0)),
)
.expr(),
);

// Add some nice shading to the particles.
let render_lambertian =
LambertianLightingModifier::new(LIGHT_POSITION.normalize_or_zero(), 0.7);

let module = writer.finish();

// Add the effect.
effects.add(
EffectAsset::new(vec![256], Spawner::burst(16.0.into(), 0.45.into()), module)
.with_name("cartoon explosion")
.init(init_xz_pos)
.init(init_y_pos)
.init(init_age)
.init(init_lifetime)
.init(init_size)
.init(init_velocity)
.update(update_size)
.render(render_lambertian)
.mesh(mesh),
)
}

// A system that plays the running animation once the fox loads.
fn setup_scene_once_loaded(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
) {
for (entity, mut animation_player) in players.iter_mut() {
let (animation_graph, animation_graph_node) =
AnimationGraph::from_clip(asset_server.load("Fox.glb#Animation2"));
let animation_graph = animation_graphs.add(animation_graph);
animation_player.play(animation_graph_node).repeat();
commands.entity(entity).insert(animation_graph.clone());
}
}

impl LambertianLightingModifier {
fn new(light_direction: Vec3, ambient: f32) -> LambertianLightingModifier {
LambertianLightingModifier {
light_direction,
ambient,
}
}
}

// Boilerplate implementation of `Modifier` for our lighting modifier.
#[cfg_attr(feature = "serde", typetag::serde)]
impl Modifier for LambertianLightingModifier {
fn context(&self) -> ModifierContext {
ModifierContext::Render
}

fn as_render(&self) -> Option<&dyn RenderModifier> {
Some(self)
}

fn as_render_mut(&mut self) -> Option<&mut dyn RenderModifier> {
Some(self)
}

fn attributes(&self) -> &[Attribute] {
&[]
}

fn boxed_clone(&self) -> BoxedModifier {
Box::new(*self)
}

fn apply(&self, _: &mut Module, _: &mut ShaderWriter) -> Result<(), ExprError> {
Err(ExprError::TypeError("Wrong modifier context".to_string()))
}
}

// The implementation of Lambertian lighting.
#[cfg_attr(feature = "serde", typetag::serde)]
impl RenderModifier for LambertianLightingModifier {
fn apply_render(&self, _: &mut Module, context: &mut RenderContext) -> Result<(), ExprError> {
// We need the vertex normals to light the mesh.
context.set_needs_normal();

// Shade each fragment.
context.fragment_code += &format!(
"color = vec4(color.rgb * mix({}, 1.0, dot(normal, {})), color.a);",
self.ambient.to_wgsl_string(),
self.light_direction.to_wgsl_string()
);

Ok(())
}

fn boxed_render_clone(&self) -> Box<dyn RenderModifier> {
Box::new(*self)
}

fn as_modifier(&self) -> &dyn Modifier {
self
}
}
Loading

0 comments on commit b7a10d9

Please sign in to comment.