diff --git a/Cargo.toml b/Cargo.toml index 3eb08be0..57a3c6c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 @@ -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" diff --git a/assets/Fox.glb b/assets/Fox.glb new file mode 100644 index 00000000..2bb946e2 Binary files /dev/null and b/assets/Fox.glb differ diff --git a/examples/2d.rs b/examples/2d.rs index 4f4d67d9..17f3f3a5 100644 --- a/examples/2d.rs +++ b/examples/2d.rs @@ -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 }) diff --git a/examples/billboard.rs b/examples/billboard.rs index 977c5fc5..feace31a 100644 --- a/examples/billboard.rs +++ b/examples/billboard.rs @@ -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, }), ); diff --git a/examples/circle.rs b/examples/circle.rs index 3fa04494..665a04e5 100644 --- a/examples/circle.rs +++ b/examples/circle.rs @@ -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, }), ); diff --git a/examples/expr.rs b/examples/expr.rs index 402f6814..178deb8a 100644 --- a/examples/expr.rs +++ b/examples/expr.rs @@ -45,8 +45,8 @@ fn setup(mut commands: Commands, mut effects: ResMut>) { 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(); diff --git a/examples/firework.rs b/examples/firework.rs index 14887161..008c0ea7 100644 --- a/examples/firework.rs +++ b/examples/firework.rs @@ -59,9 +59,9 @@ fn setup(mut commands: Commands, mut effects: ResMut>) { 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(); diff --git a/examples/force_field.rs b/examples/force_field.rs index 055c05be..a7ed2cd0 100644 --- a/examples/force_field.rs +++ b/examples/force_field.rs @@ -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 }), diff --git a/examples/multicam.rs b/examples/multicam.rs index 15d401e7..b6959006 100644 --- a/examples/multicam.rs +++ b/examples/multicam.rs @@ -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)); diff --git a/examples/ordering.rs b/examples/ordering.rs index 9d2fa433..4c899d1a 100644 --- a/examples/ordering.rs +++ b/examples/ordering.rs @@ -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(); diff --git a/examples/portal.rs b/examples/portal.rs index 7042fa96..55649277 100644 --- a/examples/portal.rs +++ b/examples/portal.rs @@ -49,8 +49,8 @@ fn setup(mut commands: Commands, mut effects: ResMut>) { 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(); diff --git a/examples/puffs.rs b/examples/puffs.rs new file mode 100644 index 00000000..33d5a588 --- /dev/null +++ b/examples/puffs.rs @@ -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> { + 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, + mut effects: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // 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, effects: &mut Assets) -> Handle { + 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, + mut animation_graphs: ResMut>, + mut players: Query<(Entity, &mut AnimationPlayer), Added>, +) { + 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 { + Box::new(*self) + } + + fn as_modifier(&self) -> &dyn Modifier { + self + } +} diff --git a/examples/spawn.rs b/examples/spawn.rs index b4b99d29..ec4e9f70 100644 --- a/examples/spawn.rs +++ b/examples/spawn.rs @@ -75,10 +75,10 @@ fn setup( color_gradient1.add_key(1.0, Vec4::splat(0.0)); let mut size_gradient1 = Gradient::new(); - size_gradient1.add_key(0.0, Vec2::splat(0.1)); - size_gradient1.add_key(0.5, Vec2::splat(0.5)); - size_gradient1.add_key(0.8, Vec2::splat(0.08)); - size_gradient1.add_key(1.0, Vec2::splat(0.0)); + size_gradient1.add_key(0.0, Vec3::splat(0.1)); + size_gradient1.add_key(0.5, Vec3::splat(0.5)); + size_gradient1.add_key(0.8, Vec3::splat(0.08)); + size_gradient1.add_key(1.0, Vec3::splat(0.0)); let writer1 = ExprWriter::new(); diff --git a/src/asset.rs b/src/asset.rs index f02f3ca8..30bcc750 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,7 +1,8 @@ use std::ops::Deref; use bevy::{ - asset::Asset, + asset::{Asset, Handle}, + prelude::Mesh, reflect::Reflect, utils::{default, HashSet}, }; @@ -198,6 +199,12 @@ pub enum AlphaMode { /// /// [`AlphaMask3d`]: bevy::core_pipeline::core_3d::AlphaMask3d Mask(ExprHandle), + + /// Render the effect with no alpha, and update the depth buffer. + /// + /// Use this mode when every pixel covered by the particle's mesh is fully + /// opaque. + Opaque, } /// Asset describing a visual effect. @@ -258,6 +265,11 @@ pub struct EffectAsset { module: Module, /// Alpha mode. pub alpha_mode: AlphaMode, + /// The mesh that each particle renders. + /// + /// This defaults to a quad facing the Z axis. + #[cfg_attr(feature = "serde", serde(skip))] + pub mesh: Option>, } impl EffectAsset { @@ -715,6 +727,12 @@ impl EffectAsset { pub fn texture_layout(&self) -> TextureLayout { self.module.texture_layout() } + + /// Sets the mesh that each particle will render. + pub fn mesh(mut self, mesh: Handle) -> Self { + self.mesh = Some(mesh); + self + } } /// Asset loader for [`EffectAsset`]. diff --git a/src/attributes.rs b/src/attributes.rs index 043c5c66..dbec5ed7 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -85,7 +85,8 @@ //! | [`Attribute::HDR_COLOR`] | The particle's HDR color as `vec4`. | //! | [`Attribute::ALPHA`] | The particle's opacity. | //! | [`Attribute::SIZE`] | The particle's uniform size. | -//! | [`Attribute::SIZE2`] | The particle's non-uniform size. | +//! | [`Attribute::SIZE2`] | The particle's non-uniform 2D size. | +//! | [`Attribute::SIZE3`] | The particle's non-uniform 3D size. | //! | [`Attribute::AXIS_X`] | X axis of the particle frame. | //! | [`Attribute::AXIS_Y`] | Y axis of the particle frame. | //! | [`Attribute::AXIS_Z`] | Z axis of the particle frame. | @@ -537,6 +538,11 @@ impl AttributeInner { Value::Vector(VectorValue::new_vec2(Vec2::ONE)), ); + pub const SIZE3: &'static AttributeInner = &AttributeInner::new( + Cow::Borrowed("size3"), + Value::Vector(VectorValue::new_vec3(Vec3::ONE)), + ); + pub const PREV: &'static AttributeInner = &AttributeInner::new( Cow::Borrowed("prev"), Value::Scalar(ScalarValue::Uint(!0u32)), @@ -958,10 +964,10 @@ impl Attribute { /// [`ScalarType::Float`] pub const SIZE: Attribute = Attribute(AttributeInner::SIZE); - /// The particle's 2D size, for quad rendering. + /// The particle's 2D size. /// - /// The particle, when drawn as a quad, is scaled along its local X and Y - /// axes by these values. + /// The particle is scaled along its local X and Y axes by these values. The + /// Z axis is unaffected. /// /// # Name /// @@ -972,6 +978,19 @@ impl Attribute { /// [`VectorType::VEC2F`] representing the XY sizes of the particle. pub const SIZE2: Attribute = Attribute(AttributeInner::SIZE2); + /// The particle's 3D size. + /// + /// The particle is scaled along its local X, Y, and Z axes by these values. + /// + /// # Name + /// + /// `size3` + /// + /// # Type + /// + /// [`VectorType::VEC3F`] representing the XYZ sizes of the particle. + pub const SIZE3: Attribute = Attribute(AttributeInner::SIZE3); + /// The previous particle in the ribbon chain. /// /// # Name @@ -1143,7 +1162,7 @@ impl Attribute { declare_custom_attr_pub!(F32X4_3, "f32x4_3", 4, VEC4F); /// Collection of all the existing particle attributes. - const ALL: [Attribute; 31] = [ + const ALL: [Attribute; 32] = [ Attribute::POSITION, Attribute::VELOCITY, Attribute::AGE, @@ -1153,6 +1172,7 @@ impl Attribute { Attribute::ALPHA, Attribute::SIZE, Attribute::SIZE2, + Attribute::SIZE3, Attribute::PREV, Attribute::NEXT, Attribute::AXIS_X, diff --git a/src/lib.rs b/src/lib.rs index 1796342d..7c7c347f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -779,7 +779,7 @@ impl EffectShaderSource { if attr == Attribute::SIZE { if !has_size { inputs_code += &format!( - "var size = vec2(particle.{0}, particle.{0});\n", + "var size = vec3(particle.{0}, particle.{0}, particle.{0});\n", Attribute::SIZE.name() ); has_size = true; @@ -788,11 +788,21 @@ impl EffectShaderSource { } } else if attr == Attribute::SIZE2 { if !has_size { - inputs_code += &format!("var size = particle.{0};\n", Attribute::SIZE2.name()); + inputs_code += &format!( + "var size = vec3(particle.{0}, 1.0);\n", + Attribute::SIZE2.name() + ); has_size = true; } else { warn!("Attribute SIZE2 conflicts with another size attribute; ignored."); } + } else if attr == Attribute::SIZE3 { + if !has_size { + inputs_code += &format!("var size = particle.{0};\n", Attribute::SIZE3.name()); + has_size = true; + } else { + warn!("Attribute SIZE3 conflicts with another size attribute; ignored."); + } } else if attr == Attribute::HDR_COLOR { if !has_color { inputs_code += @@ -821,7 +831,7 @@ impl EffectShaderSource { if !has_size { inputs_code += &format!( "var size = {0};\n", - Attribute::SIZE2.default_value().to_wgsl_string() // TODO - or SIZE? + Attribute::SIZE3.default_value().to_wgsl_string() // TODO - or SIZE? ); } if !has_color { @@ -977,6 +987,9 @@ impl EffectShaderSource { if render_context.needs_uv { layout_flags |= LayoutFlags::NEEDS_UV; } + if render_context.needs_normal { + layout_flags |= LayoutFlags::NEEDS_NORMAL; + } let alpha_cutoff_code = if let AlphaMode::Mask(cutoff) = &asset.alpha_mode { render_context.eval(&module, *cutoff).unwrap_or_else(|err| { @@ -1135,6 +1148,7 @@ pub struct CompiledParticleEffect { simulation_condition: SimulationCondition, /// Handle to the effect shader for his effect instance, if configured. effect_shader: Option, + mesh: Option>, /// Textures used by the effect, if any. textures: Vec>, /// 2D layer for the effect instance. @@ -1152,6 +1166,7 @@ impl Default for CompiledParticleEffect { asset: default(), simulation_condition: SimulationCondition::default(), effect_shader: None, + mesh: None, textures: vec![], #[cfg(feature = "2d")] z_layer_2d: FloatOrd(0.0), @@ -1269,6 +1284,8 @@ impl CompiledParticleEffect { render: render_shaders, }); + self.mesh = asset.mesh.clone(); + self.textures = material.map(|mat| &mat.images).cloned().unwrap_or_default(); } @@ -1329,6 +1346,47 @@ impl ShaderCode for Gradient { } } +impl ShaderCode for Gradient { + fn to_shader_code(&self, input: &str) -> String { + if self.keys().is_empty() { + return String::new(); + } + let mut s: String = self + .keys() + .iter() + .enumerate() + .map(|(index, key)| { + format!( + "let t{0} = {1};\nlet v{0} = {2};", + index, + key.ratio().to_wgsl_string(), + key.value.to_wgsl_string() + ) + }) + .fold("// Gradient\n".into(), |s, key| s + &key + "\n"); + if self.keys().len() == 1 { + s + "return v0;\n" + } else { + s += &format!("if ({input} <= t0) {{ return v0; }}\n"); + let mut s = self + .keys() + .iter() + .skip(1) + .enumerate() + .map(|(index, _key)| { + format!( + "else if ({input} <= t{1}) {{ return mix(v{0}, v{1}, ({input} - t{0}) / (t{1} - t{0})); }}\n", + index, + index + 1 + ) + }) + .fold(s, |s, key| s + &key); + let _ = writeln!(s, "else {{ return v{}; }}", self.keys().len() - 1); + s + } + } +} + impl ShaderCode for Gradient { fn to_shader_code(&self, input: &str) -> String { if self.keys().is_empty() { @@ -1833,6 +1891,7 @@ else { return c1; } let mut shader_defs = std::collections::HashMap::::new(); shader_defs.insert("LOCAL_SPACE_SIMULATION".into(), ShaderDefValue::Bool(true)); shader_defs.insert("NEEDS_UV".into(), ShaderDefValue::Bool(true)); + shader_defs.insert("NEEDS_NORMAL".into(), ShaderDefValue::Bool(false)); shader_defs.insert("RENDER_NEEDS_SPAWNER".into(), ShaderDefValue::Bool(true)); shader_defs.insert( "PARTICLE_SCREEN_SPACE_SIZE".into(), diff --git a/src/modifier/mod.rs b/src/modifier/mod.rs index 9706fb63..97c50d5d 100644 --- a/src/modifier/mod.rs +++ b/src/modifier/mod.rs @@ -27,7 +27,7 @@ use std::{ use bevy::{ asset::Handle, - math::{UVec2, Vec2, Vec4}, + math::{UVec2, Vec3, Vec4}, reflect::Reflect, render::texture::Image, utils::HashMap, @@ -306,9 +306,8 @@ impl<'a> EvalContext for ShaderWriter<'a> { if let Some(s) = self.expr_cache.get(&handle) { Ok(s.clone()) } else { - module.try_get(handle)?.eval(module, self).map(|s| { + module.try_get(handle)?.eval(module, self).inspect(|s| { self.expr_cache.insert(handle, s.clone()); - s }) } } @@ -384,9 +383,11 @@ pub struct RenderContext<'a> { /// Color gradients. pub gradients: HashMap>, /// Size gradients. - pub size_gradients: HashMap>, + pub size_gradients: HashMap>, /// The particle needs UV coordinates to sample one or more texture(s). pub needs_uv: bool, + /// The particle needs normals for lighting effects. + pub needs_normal: bool, /// Counter for unique variable names. var_counter: u32, /// Cache of evaluated expressions. @@ -414,6 +415,7 @@ impl<'a> RenderContext<'a> { gradients: HashMap::new(), size_gradients: HashMap::new(), needs_uv: false, + needs_normal: false, var_counter: 0, expr_cache: Default::default(), is_attribute_pointer: false, @@ -421,10 +423,15 @@ impl<'a> RenderContext<'a> { } /// Mark the rendering shader as needing UVs. - fn set_needs_uv(&mut self) { + pub fn set_needs_uv(&mut self) { self.needs_uv = true; } + /// Mark the rendering shader as needing normals. + pub fn set_needs_normal(&mut self) { + self.needs_normal = true; + } + /// Add a color gradient. /// /// # Returns @@ -444,7 +451,7 @@ impl<'a> RenderContext<'a> { /// /// Returns the unique name of the gradient, to be used as function name in /// the shader code. - fn add_size_gradient(&mut self, gradient: Gradient) -> String { + fn add_size_gradient(&mut self, gradient: Gradient) -> String { let func_id = calc_func_id(&gradient); self.size_gradients.insert(func_id, gradient); let func_name = format!("size_gradient_{0:016X}", func_id); @@ -477,9 +484,8 @@ impl<'a> EvalContext for RenderContext<'a> { if let Some(s) = self.expr_cache.get(&handle) { Ok(s.clone()) } else { - module.try_get(handle)?.eval(module, self).map(|s| { + module.try_get(handle)?.eval(module, self).inspect(|s| { self.expr_cache.insert(handle, s.clone()); - s }) } } @@ -990,7 +996,7 @@ fn main() {{ var particle = Particle(); var position = vec3(0.0, 0.0, 0.0); var velocity = vec3(0.0, 0.0, 0.0); - var size = vec2(1.0, 1.0); + var size = vec3(1.0, 1.0, 1.0); var axis_x = vec3(1.0, 0.0, 0.0); var axis_y = vec3(0.0, 1.0, 0.0); var axis_z = vec3(0.0, 0.0, 1.0); diff --git a/src/modifier/output.rs b/src/modifier/output.rs index badebc62..9c429f8c 100644 --- a/src/modifier/output.rs +++ b/src/modifier/output.rs @@ -291,7 +291,7 @@ impl RenderModifier for SetSizeModifier { #[derive(Debug, Default, Clone, PartialEq, Hash, Reflect, Serialize, Deserialize)] pub struct SizeOverLifetimeModifier { /// The size gradient defining the particle size based on its lifetime. - pub gradient: Gradient, + pub gradient: Gradient, /// Is the particle size in screen-space logical pixel? If `true`, the size /// is in screen-space logical pixels, and not affected by the camera /// projection. If `false`, the particle size is in world units. @@ -312,7 +312,7 @@ impl RenderModifier for SizeOverLifetimeModifier { ) -> Result<(), ExprError> { let func_name = context.add_size_gradient(self.gradient.clone()); context.render_extra += &format!( - r#"fn {0}(key: f32) -> vec2 {{ + r#"fn {0}(key: f32) -> vec3 {{ {1} }} @@ -680,9 +680,9 @@ impl RenderModifier for FlipbookModifier { /// This modifier requires the following particle attributes: /// - [`Attribute::POSITION`] /// -/// If the [`Attribute::SIZE`] or [`Attribute::SIZE2`] are present, they're used -/// to initialize the particle's size. Otherwise the default size is used. So -/// this modifier doesn't require any size attribute. +/// If the [`Attribute::SIZE`], [`Attribute::SIZE2`], or [`Attribute::SIZE3`] +/// are present, they're used to initialize the particle's size. Otherwise the +/// default size is used. So this modifier doesn't require any size attribute. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] pub struct ScreenSpaceSizeModifier; @@ -869,8 +869,8 @@ mod tests { #[test] fn mod_size_over_lifetime() { - let x = Vec2::new(1., 0.); - let y = Vec2::new(0., 1.); + let x = Vec3::new(1., 0., 1.); + let y = Vec3::new(0., 1., 1.); let mut gradient = Gradient::new(); gradient.add_key(0.5, x); gradient.add_key(0.8, y); diff --git a/src/modifier/ribbon.rs b/src/modifier/ribbon.rs index 12f52725..0e07cdc4 100644 --- a/src/modifier/ribbon.rs +++ b/src/modifier/ribbon.rs @@ -36,7 +36,7 @@ impl RenderModifier for RibbonModifier { axis_z = cross(axis_x, axis_y); position = mix(next_particle.position, particle.position, 0.5); - size = vec2(length(delta), size.y); + size = vec3(length(delta), size.y, 1.0); "##; Ok(()) diff --git a/src/plugin.rs b/src/plugin.rs index f87ccda3..b9cfc149 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,7 +1,7 @@ #[cfg(feature = "2d")] use bevy::core_pipeline::core_2d::Transparent2d; #[cfg(feature = "3d")] -use bevy::core_pipeline::core_3d::{AlphaMask3d, Transparent3d}; +use bevy::core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d}; use bevy::{ prelude::*, render::{ @@ -260,7 +260,11 @@ impl Plugin for HanabiPlugin { assets.insert(&HANABI_COMMON_TEMPLATE_HANDLE, common_shader); } - let effects_meta = EffectsMeta::new(render_device.clone()); + let effects_meta = { + let mut assets = app.world_mut().resource_mut::>(); + EffectsMeta::new(render_device.clone(), &mut assets) + }; + let effect_cache = EffectCache::new(render_device); // Register the custom render pipeline @@ -338,6 +342,14 @@ impl Plugin for HanabiPlugin { .unwrap() .write() .add(draw_particles); + + let draw_particles = DrawEffects::new(render_app.world_mut()); + render_app + .world() + .get_resource::>() + .unwrap() + .write() + .add(draw_particles); } // Add the simulation sub-graph. This render graph runs once per frame no matter diff --git a/src/render/batch.rs b/src/render/batch.rs index 78625803..4e6554ed 100644 --- a/src/render/batch.rs +++ b/src/render/batch.rs @@ -1,4 +1,7 @@ -use std::ops::{Index, Range}; +use std::{ + fmt::Debug, + ops::{Index, Range}, +}; #[cfg(feature = "2d")] use bevy::math::FloatOrd; @@ -39,6 +42,8 @@ pub(crate) struct EffectBatches { pub particle_layout: ParticleLayout, /// Flags describing the render layout. pub layout_flags: LayoutFlags, + /// The mesh to draw. + pub mesh: Handle, /// Texture layout. pub texture_layout: TextureLayout, /// Textures. @@ -126,6 +131,7 @@ impl EffectBatches { .collect(), handle: input.handle, layout_flags: input.layout_flags, + mesh: input.mesh.clone(), texture_layout: input.texture_layout, textures: input.textures, alpha_mode: input.alpha_mode, @@ -138,7 +144,7 @@ impl EffectBatches { } /// Effect batching input, obtained from extracted effects. -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct BatchesInput { /// Handle of the underlying effect asset describing the effect. pub handle: Handle, @@ -153,6 +159,7 @@ pub(crate) struct BatchesInput { pub effect_shader: EffectShader, /// Various flags related to the effect. pub layout_flags: LayoutFlags, + pub mesh: Handle, /// Texture layout. pub texture_layout: TextureLayout, /// Textures. diff --git a/src/render/mod.rs b/src/render/mod.rs index e6e3a49d..1db5b8bb 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -11,7 +11,7 @@ use bevy::math::FloatOrd; #[cfg(feature = "3d")] use bevy::{ core_pipeline::{ - core_3d::{AlphaMask3d, Transparent3d}, + core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, prepass::OpaqueNoLightmap3dBinKey, }, render::render_phase::{BinnedPhaseItem, ViewBinnedRenderPhases}, @@ -24,6 +24,7 @@ use bevy::{ log::trace, prelude::*, render::{ + mesh::{GpuBufferInfo, GpuMesh, MeshVertexBufferLayoutRef}, render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo}, render_phase::{ @@ -304,12 +305,25 @@ pub struct GpuRenderEffectMetadata { pub ping: u32, } +/// Indirect draw parameters, with some data of our own tacked on to the end. +/// +/// A few fields of this differ depending on whether the mesh is indexed or +/// non-indexed. #[repr(C)] #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)] pub struct GpuRenderGroupIndirect { + /// The number of vertices in the mesh, if non-indexed; if indexed, the + /// number of indices in the mesh. pub vertex_count: u32, + /// The number of instances to render. pub instance_count: u32, - pub vertex_offset: i32, + /// The first index to render, if the mesh is indexed; the offset of the + /// first vertex, if the mesh is non-indexed. + pub first_index_or_vertex_offset: u32, + /// The offset of the first vertex, if the mesh is indexed; the first + /// instance to render, if the mesh is non-indexed. + pub vertex_offset_or_base_instance: i32, + /// The first instance to render, if indexed; unused if non-indexed. pub base_instance: u32, // pub alive_count: u32, @@ -1030,15 +1044,16 @@ pub(crate) struct ParticleRenderPipelineKey { shader: Handle, /// Particle layout. particle_layout: ParticleLayout, + mesh_layout: Option, /// Texture layout. texture_layout: TextureLayout, /// Key: LOCAL_SPACE_SIMULATION /// The effect is simulated in local space, and during rendering all /// particles are transformed by the effect's [`GlobalTransform`]. local_space_simulation: bool, - /// Key: USE_ALPHA_MASK - /// The effect is rendered with alpha masking. - use_alpha_mask: bool, + /// Key: USE_ALPHA_MASK, OPAQUE + /// The particle's alpha masking behavior. + alpha_mask: ParticleRenderAlphaMaskPipelineKey, /// The effect needs Alpha blend. alpha_mode: AlphaMode, /// Key: FLIPBOOK @@ -1048,6 +1063,9 @@ pub(crate) struct ParticleRenderPipelineKey { /// Key: NEEDS_UV /// The effect needs UVs. needs_uv: bool, + /// Key: NEEDS_NORMAL + /// The effect needs normals. + needs_normal: bool, /// For dual-mode configurations only, the actual mode of the current render /// pipeline. Otherwise the mode is implicitly determined by the active /// feature. @@ -1059,17 +1077,31 @@ pub(crate) struct ParticleRenderPipelineKey { hdr: bool, } +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)] +pub(crate) enum ParticleRenderAlphaMaskPipelineKey { + #[default] + Blend, + /// Key: USE_ALPHA_MASK + /// The effect is rendered with alpha masking. + AlphaMask, + /// Key: OPAQUE + /// The effect is rendered fully-opaquely. + Opaque, +} + impl Default for ParticleRenderPipelineKey { fn default() -> Self { Self { shader: Handle::default(), particle_layout: ParticleLayout::empty(), + mesh_layout: None, texture_layout: default(), local_space_simulation: false, - use_alpha_mask: false, + alpha_mask: default(), alpha_mode: AlphaMode::Blend, flipbook: false, needs_uv: false, + needs_normal: false, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode::Camera3d, msaa_samples: Msaa::default().samples(), @@ -1084,44 +1116,6 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { trace!("Specializing render pipeline for key: {:?}", key); - // Base mandatory part of vertex buffer layout - let vertex_buffer_layout = VertexBufferLayout { - array_stride: 20, - step_mode: VertexStepMode::Vertex, - attributes: vec![ - // @location(0) vertex_position: vec3 - VertexAttribute { - format: VertexFormat::Float32x3, - offset: 0, - shader_location: 0, - }, - // @location(1) vertex_uv: vec2 - VertexAttribute { - format: VertexFormat::Float32x2, - offset: 12, - shader_location: 1, - }, - // @location(1) vertex_color: u32 - // VertexAttribute { - // format: VertexFormat::Uint32, - // offset: 12, - // shader_location: 1, - // }, - // @location(2) vertex_velocity: vec3 - // VertexAttribute { - // format: VertexFormat::Float32x3, - // offset: 12, - // shader_location: 1, - // }, - // @location(3) vertex_uv: vec2 - // VertexAttribute { - // format: VertexFormat::Float32x2, - // offset: 28, - // shader_location: 3, - // }, - ], - }; - let dispatch_indirect_size = GpuDispatchIndirect::aligned_size( self.render_device .limits() @@ -1187,6 +1181,17 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { let mut layout = vec![self.view_layout.clone(), particles_buffer_layout]; let mut shader_defs = vec!["SPAWNER_READONLY".into()]; + let vertex_buffer_layout = key.mesh_layout.and_then(|mesh_layout| { + mesh_layout + .0 + .get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_UV_0.at_shader_location(1), + Mesh::ATTRIBUTE_NORMAL.at_shader_location(2), + ]) + .ok() + }); + if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) { layout.push(material_bind_group_layout.clone()); // // @location(1) vertex_uv: vec2 @@ -1204,9 +1209,16 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { shader_defs.push("RENDER_NEEDS_SPAWNER".into()); } - // Key: USE_ALPHA_MASK - if key.use_alpha_mask { - shader_defs.push("USE_ALPHA_MASK".into()); + match key.alpha_mask { + ParticleRenderAlphaMaskPipelineKey::Blend => {} + ParticleRenderAlphaMaskPipelineKey::AlphaMask => { + // Key: USE_ALPHA_MASK + shader_defs.push("USE_ALPHA_MASK".into()) + } + ParticleRenderAlphaMaskPipelineKey::Opaque => { + // Key: OPAQUE + shader_defs.push("OPAQUE".into()) + } } // Key: FLIPBOOK @@ -1218,14 +1230,23 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { shader_defs.push("NEEDS_UV".into()); } + if key.needs_normal { + shader_defs.push("NEEDS_NORMAL".into()); + } + #[cfg(all(feature = "2d", feature = "3d"))] let depth_stencil = match key.pipeline_mode { // Bevy's Transparent2d render phase doesn't support a depth-stencil buffer. PipelineMode::Camera2d => None, PipelineMode::Camera3d => Some(DepthStencilState { format: TextureFormat::Depth32Float, - // Use depth buffer with alpha-masked particles, not with transparent ones - depth_write_enabled: key.use_alpha_mask, + // Use depth buffer with alpha-masked or opaque particles, not + // with transparent ones + depth_write_enabled: matches!( + key.alpha_mask, + ParticleRenderAlphaMaskPipelineKey::AlphaMask + | ParticleRenderAlphaMaskPipelineKey::Opaque + ), // Bevy uses reverse-Z, so Greater really means closer depth_compare: CompareFunction::Greater, stencil: StencilState::default(), @@ -1240,7 +1261,11 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { let depth_stencil = Some(DepthStencilState { format: TextureFormat::Depth32Float, // Use depth buffer with alpha-masked particles, not with transparent ones - depth_write_enabled: key.use_alpha_mask, + depth_write_enabled: matches!( + key.alpha_mask, + ParticleRenderAlphaMaskPipelineKey::AlphaMask + | ParticleRenderAlphaMaskPipelineKey::Opaque + ), // Bevy uses reverse-Z, so Greater really means closer depth_compare: CompareFunction::Greater, stencil: StencilState::default(), @@ -1284,7 +1309,7 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline { shader: key.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), - buffers: vec![vertex_buffer_layout], + buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")], }, fragment: Some(FragmentState { shader: key.shader, @@ -1356,6 +1381,7 @@ pub(crate) struct ExtractedEffect { pub inverse_transform: Mat4, /// Layout flags. pub layout_flags: LayoutFlags, + pub mesh: Handle, /// Texture layout. pub texture_layout: TextureLayout, /// Textures. @@ -1390,6 +1416,21 @@ pub struct AddedEffect { pub layout_flags: LayoutFlags, /// Handle of the effect asset. pub handle: Handle, + pub gpu_mesh_info: AddedEffectGpuMeshInfo, +} + +/// Mesh information needed to build newly-added effects. +pub enum AddedEffectGpuMeshInfo { + /// The mesh has vertex indices. + Indexed { + /// The number of indices that make up the mesh. + index_count: u32, + }, + /// The mesh doesn't have vertex indices. + NonIndexed { + /// The number of vertices in the mesh. + vertex_count: u32, + }, } /// Collection of all extracted effects for this frame, inserted into the @@ -1445,6 +1486,7 @@ pub(crate) fn extract_effects( time: Extract>>, effects: Extract>>, _images: Extract>>, + meshes: Extract>>, mut query: Extract< ParamSet<( // All existing ParticleEffect components @@ -1467,6 +1509,7 @@ pub(crate) fn extract_effects( mut removed_effects_event_reader: Extract>, mut sim_params: ResMut, mut extracted_effects: ResMut, + effects_meta: Res, ) { trace!("extract_effects"); @@ -1501,6 +1544,10 @@ pub(crate) fn extract_effects( let handle = effect.asset.clone_weak(); let asset = effects.get(&effect.asset)?; let particle_layout = asset.particle_layout(); + let mesh = meshes.get(match effect.mesh { + Some(ref mesh) => mesh.id(), + None => effects_meta.default_mesh.id() + })?; assert!( particle_layout.size() > 0, "Invalid empty particle layout for effect '{}' on entity {:?}. Did you forget to add some modifier to the asset?", @@ -1517,6 +1564,10 @@ pub(crate) fn extract_effects( property_layout, layout_flags: effect.layout_flags, handle, + gpu_mesh_info: match mesh.indices() { + Some(indices) => AddedEffectGpuMeshInfo::Indexed { index_count: indices.len() as u32 }, + None => AddedEffectGpuMeshInfo::NonIndexed { vertex_count: mesh.count_vertices() as u32 }, + }, }) }) .collect(); @@ -1579,6 +1630,10 @@ pub(crate) fn extract_effects( }; let layout_flags = effect.layout_flags; + let mesh = match effect.mesh { + None => effects_meta.default_mesh.clone(), + Some(ref mesh) => (*mesh).clone(), + }; let alpha_mode = effect.alpha_mode; trace!( @@ -1602,6 +1657,7 @@ pub(crate) fn extract_effects( // TODO - more efficient/correct way than inverse()? inverse_transform: transform.compute_matrix().inverse(), layout_flags, + mesh, texture_layout, textures: effect.textures.clone(), alpha_mode, @@ -1613,17 +1669,6 @@ pub(crate) fn extract_effects( } } -/// GPU representation of a single vertex of a particle mesh stored in a GPU -/// buffer. -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable, ShaderType)] -struct GpuParticleVertex { - /// Vertex position. - pub position: [f32; 3], - /// UV coordinates of vertex. - pub uv: [f32; 2], -} - /// Various GPU limits and aligned sizes computed once and cached. struct GpuLimits { /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`]. @@ -1772,11 +1817,9 @@ pub struct EffectsMeta { /// each particle group that's populated by the CPU and read (only read) by /// the GPU. particle_group_buffer: AlignedBufferVec, - /// Unscaled vertices of the mesh of a single particle, generally a quad. + /// Unscaled vertices of the mesh of a single particle, by default a quad. /// The mesh is later scaled during rendering by the "particle size". - // FIXME - This is a per-effect thing, unless we merge all meshes into a single buffer (makes - // sense) but in that case we need a vertex slice too to know which mesh to draw per effect. - vertices: BufferVec, + default_mesh: Handle, /// The pipeline for the indirect dispatch shader, which populates the /// indirect compute dispatch buffers. indirect_dispatch_pipeline: Option, @@ -1786,16 +1829,8 @@ pub struct EffectsMeta { } impl EffectsMeta { - pub fn new(device: RenderDevice) -> Self { - let mut vertices = BufferVec::new(BufferUsages::VERTEX); - for v in QUAD_VERTEX_POSITIONS { - let uv = v.truncate() + 0.5; - let v = *v * Vec3::new(1.0, 1.0, 1.0); - vertices.push(GpuParticleVertex { - position: v.into(), - uv: uv.into(), - }); - } + pub fn new(device: RenderDevice, mesh_assets: &mut Assets) -> Self { + let default_mesh = mesh_assets.add(Plane3d::new(Vec3::Z, Vec2::splat(0.5))); let gpu_limits = GpuLimits::from_device(&device); @@ -1844,7 +1879,7 @@ impl EffectsMeta { NonZeroU64::new(item_align), Some("hanabi:buffer:particle_group".to_string()), ), - vertices, + default_mesh, indirect_dispatch_pipeline: None, gpu_limits, } @@ -1941,12 +1976,22 @@ impl EffectsMeta { &mut self.render_group_dispatch_buffer, added_effect.capacities.iter().map(|&capacity| { let indirect_dispatch = GpuRenderGroupIndirect { - vertex_count: 6, // TODO - Flexible vertex count and mesh particles + vertex_count: match added_effect.gpu_mesh_info { + AddedEffectGpuMeshInfo::Indexed { index_count, .. } => index_count, + AddedEffectGpuMeshInfo::NonIndexed { vertex_count } => vertex_count, + }, + first_index_or_vertex_offset: 0, + vertex_offset_or_base_instance: match added_effect.gpu_mesh_info { + AddedEffectGpuMeshInfo::Indexed { .. } => 0, + AddedEffectGpuMeshInfo::NonIndexed { .. } => current_base_instance, + }, dead_count: capacity, - base_instance: current_base_instance, - ..default() + base_instance: current_base_instance as u32, + instance_count: 0, + alive_count: 0, + max_update: 0, }; - current_base_instance += capacity; + current_base_instance += capacity as i32; indirect_dispatch }), ); @@ -2020,15 +2065,6 @@ impl EffectsMeta { } } -const QUAD_VERTEX_POSITIONS: &[Vec3] = &[ - Vec3::from_array([-0.5, -0.5, 0.0]), - Vec3::from_array([0.5, 0.5, 0.0]), - Vec3::from_array([-0.5, 0.5, 0.0]), - Vec3::from_array([-0.5, -0.5, 0.0]), - Vec3::from_array([0.5, -0.5, 0.0]), - Vec3::from_array([0.5, 0.5, 0.0]), -]; - bitflags! { /// Effect flags. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -2045,6 +2081,10 @@ bitflags! { const FLIPBOOK = (1 << 4); /// The effect needs UVs. const NEEDS_UV = (1 << 5); + /// The effects needs normals. + const NEEDS_NORMAL = (1 << 6); + /// The effect is fully-opaque. + const OPAQUE = (1 << 7); } } @@ -2079,11 +2119,6 @@ pub(crate) fn prepare_effects( // effects_meta.spawner_buffer.push(GpuSpawnerParams::default()); //} - // Write vertices (TODO - lazily once only) - effects_meta - .vertices - .write_buffer(&render_device, &render_queue); - effects_meta.indirect_dispatch_pipeline = Some(dispatch_indirect_pipeline.pipeline.clone()); // Clear last frame's buffer resizes which may have occured during last frame, @@ -2138,6 +2173,7 @@ pub(crate) fn prepare_effects( property_layout: extracted_effect.property_layout.clone(), effect_shader: extracted_effect.effect_shader.clone(), layout_flags: extracted_effect.layout_flags, + mesh: extracted_effect.mesh, texture_layout: extracted_effect.texture_layout.clone(), textures: extracted_effect.textures.clone(), alpha_mode: extracted_effect.alpha_mode, @@ -2460,6 +2496,8 @@ pub struct QueueEffectsReadOnlyParams<'w, 's> { draw_functions_3d: Res<'w, DrawFunctions>, #[cfg(feature = "3d")] draw_functions_alpha_mask: Res<'w, DrawFunctions>, + #[cfg(feature = "3d")] + draw_functions_opaque: Res<'w, DrawFunctions>, #[system_param(ignore)] marker: PhantomData<&'s usize>, } @@ -2472,6 +2510,7 @@ fn emit_sorted_draw( effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>, render_pipeline: &mut ParticlesRenderPipeline, mut specialized_render_pipelines: Mut>, + render_meshes: &RenderAssets, pipeline_cache: &PipelineCache, msaa_samples: u32, make_phase_item: F, @@ -2530,7 +2569,10 @@ fn emit_sorted_draw( ); // AlphaMask is a binned draw, so no sorted draw can possibly use it - if batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) { + if batches + .layout_flags + .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE) + { continue; } @@ -2562,17 +2604,20 @@ fn emit_sorted_draw( let local_space_simulation = batches .layout_flags .contains(LayoutFlags::LOCAL_SPACE_SIMULATION); - let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK); + let alpha_mask = + ParticleRenderAlphaMaskPipelineKey::from_layout_flags(batches.layout_flags); let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK); let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV); + let needs_normal = batches.layout_flags.contains(LayoutFlags::NEEDS_NORMAL); let image_count = batches.texture_layout.layout.len() as u8; + let gpu_mesh = render_meshes.get(&batches.mesh); // Specialize the render pipeline based on the effect batch trace!( - "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}", + "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}", batches.render_shaders, image_count, - use_alpha_mask, + alpha_mask, flipbook, view.hdr ); @@ -2584,6 +2629,10 @@ fn emit_sorted_draw( let alpha_mode = batches.alpha_mode; + let Some(mesh_layout) = gpu_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else { + continue; + }; + #[cfg(feature = "trace")] let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered(); let render_pipeline_id = specialized_render_pipelines.specialize( @@ -2591,13 +2640,15 @@ fn emit_sorted_draw( render_pipeline, ParticleRenderPipelineKey { shader: render_shader_source.clone(), + mesh_layout: Some(mesh_layout), particle_layout: batches.particle_layout.clone(), texture_layout: batches.texture_layout.clone(), local_space_simulation, - use_alpha_mask, + alpha_mask, alpha_mode, flipbook, needs_uv, + needs_normal, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode, msaa_samples, @@ -2642,10 +2693,11 @@ fn emit_binned_draw( render_pipeline: &mut ParticlesRenderPipeline, mut specialized_render_pipelines: Mut>, pipeline_cache: &PipelineCache, + render_meshes: &RenderAssets, msaa_samples: u32, make_bin_key: F, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode, - use_alpha_mask: bool, + alpha_mask: ParticleRenderAlphaMaskPipelineKey, ) where T: BinnedPhaseItem, F: Fn(CachedRenderPipelineId, &EffectDrawBatch, u32, &ExtractedView) -> T::BinKey, @@ -2655,10 +2707,7 @@ fn emit_binned_draw( trace!("emit_binned_draw() {} views", views.iter().len()); for (view_entity, visible_entities, view) in views.iter() { - trace!( - "Process new binned view (use_alpha_mask={})", - use_alpha_mask - ); + trace!("Process new binned view (alpha_mask={:?})", alpha_mask); let Some(render_phase) = render_phases.get_mut(&view_entity) else { continue; @@ -2704,7 +2753,9 @@ fn emit_binned_draw( batches.layout_flags, ); - if use_alpha_mask != batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) { + if ParticleRenderAlphaMaskPipelineKey::from_layout_flags(batches.layout_flags) + != alpha_mask + { continue; } @@ -2736,17 +2787,20 @@ fn emit_binned_draw( let local_space_simulation = batches .layout_flags .contains(LayoutFlags::LOCAL_SPACE_SIMULATION); - let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK); + let alpha_mask = + ParticleRenderAlphaMaskPipelineKey::from_layout_flags(batches.layout_flags); let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK); let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV); + let needs_normal = batches.layout_flags.contains(LayoutFlags::NEEDS_NORMAL); let image_count = batches.texture_layout.layout.len() as u8; + let gpu_mesh = render_meshes.get(&batches.mesh); // Specialize the render pipeline based on the effect batch trace!( - "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}", + "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}", batches.render_shaders, image_count, - use_alpha_mask, + alpha_mask, flipbook, view.hdr ); @@ -2758,6 +2812,10 @@ fn emit_binned_draw( let alpha_mode = batches.alpha_mode; + let Some(mesh_layout) = gpu_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else { + continue; + }; + #[cfg(feature = "trace")] let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered(); let render_pipeline_id = specialized_render_pipelines.specialize( @@ -2765,13 +2823,15 @@ fn emit_binned_draw( render_pipeline, ParticleRenderPipelineKey { shader: render_shader_source.clone(), + mesh_layout: Some(mesh_layout), particle_layout: batches.particle_layout.clone(), texture_layout: batches.texture_layout.clone(), local_space_simulation, - use_alpha_mask, + alpha_mask, alpha_mode, flipbook, needs_uv, + needs_normal, #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode, msaa_samples, @@ -2815,6 +2875,7 @@ pub(crate) fn queue_effects( effect_batches: Query<(Entity, &mut EffectBatches)>, effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>, events: Res, + render_meshes: Res>, read_params: QueueEffectsReadOnlyParams, msaa: Res, mut view_entities: Local, @@ -2878,6 +2939,7 @@ pub(crate) fn queue_effects( &effect_draw_batches, &mut render_pipeline, specialized_render_pipelines.reborrow(), + &render_meshes, &pipeline_cache, msaa.samples(), |id, entity, draw_batch, _group, _view| Transparent2d { @@ -2918,6 +2980,7 @@ pub(crate) fn queue_effects( &effect_draw_batches, &mut render_pipeline, specialized_render_pipelines.reborrow(), + &render_meshes, &pipeline_cache, msaa.samples(), |id, entity, batch, _group, view| Transparent3d { @@ -2957,6 +3020,7 @@ pub(crate) fn queue_effects( &mut render_pipeline, specialized_render_pipelines.reborrow(), &pipeline_cache, + &render_meshes, msaa.samples(), |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey { pipeline: id, @@ -2972,7 +3036,49 @@ pub(crate) fn queue_effects( }, #[cfg(feature = "2d")] PipelineMode::Camera3d, - true, + ParticleRenderAlphaMaskPipelineKey::AlphaMask, + ); + } + + // Opaque particles + if !views.is_empty() { + #[cfg(feature = "trace")] + let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered(); + + trace!("Emit effect draw calls for opaque 3D views..."); + + let draw_effects_function_opaque = read_params + .draw_functions_opaque + .read() + .get_id::() + .unwrap(); + + emit_binned_draw( + &views, + &mut alpha_mask_3d_render_phases, + &mut view_entities, + &effect_batches, + &effect_draw_batches, + &mut render_pipeline, + specialized_render_pipelines.reborrow(), + &pipeline_cache, + &render_meshes, + msaa.samples(), + |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey { + pipeline: id, + draw_function: draw_effects_function_opaque, + asset_id: AssetId::::default().untyped(), + material_bind_group_id: None, + // }, + // distance: view + // .rangefinder3d() + // .distance_translation(&batch.translation_3d), + // batch_range: 0..1, + // extra_index: PhaseItemExtraIndex::NONE, + }, + #[cfg(feature = "2d")] + PipelineMode::Camera3d, + ParticleRenderAlphaMaskPipelineKey::Opaque, ); } } @@ -3362,6 +3468,7 @@ type DrawEffectsSystemState = SystemState<( SRes, SRes, SRes, + SRes>, SQuery>, SQuery>, SQuery>, @@ -3394,11 +3501,19 @@ fn draw<'w>( pipeline_id: CachedRenderPipelineId, params: &mut DrawEffectsSystemState, ) { - let (effects_meta, effect_bind_groups, pipeline_cache, views, effects, effect_draw_batches) = - params.get(world); + let ( + effects_meta, + effect_bind_groups, + pipeline_cache, + meshes, + views, + effects, + effect_draw_batches, + ) = params.get(world); let view_uniform = views.get(view).unwrap(); let effects_meta = effects_meta.into_inner(); let effect_bind_groups = effect_bind_groups.into_inner(); + let meshes = meshes.into_inner(); let effect_draw_batch = effect_draw_batches.get(entity).unwrap(); let effect_batches = effects.get(effect_draw_batch.batches_entity).unwrap(); @@ -3412,8 +3527,23 @@ fn draw<'w>( pass.set_render_pipeline(pipeline); + let Some(gpu_mesh): Option<&GpuMesh> = meshes.get(&effect_batches.mesh) else { + return; + }; + // Vertex buffer containing the particle model to draw. Generally a quad. - pass.set_vertex_buffer(0, effects_meta.vertices.buffer().unwrap().slice(..)); + pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + + match gpu_mesh.buffer_info { + GpuBufferInfo::Indexed { + ref buffer, + count: _, + index_format, + } => { + pass.set_index_buffer(buffer.slice(..), 0, index_format); + } + GpuBufferInfo::NonIndexed => {} + } // View properties (camera matrix, etc.) pass.set_bind_group( @@ -3482,17 +3612,34 @@ fn draw<'w>( "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \ (render_group_dispatch_indirect_index={:?}, group_index={}).", effect_batch.slice.len(), - effects_meta.vertices.len(), + gpu_mesh.vertex_count, effect_batches.buffer_index, render_group_dispatch_indirect_index, group_index, ); - pass.draw_indirect( - render_indirect_buffer, - render_group_dispatch_indirect_index as u64 - * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64, - ); + match gpu_mesh.buffer_info { + GpuBufferInfo::Indexed { + ref buffer, + count: _, + index_format, + } => { + pass.set_index_buffer(buffer.slice(..), 0, index_format); + + pass.draw_indexed_indirect( + render_indirect_buffer, + render_group_dispatch_indirect_index as u64 + * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64, + ); + } + GpuBufferInfo::NonIndexed => { + pass.draw_indirect( + render_indirect_buffer, + render_group_dispatch_indirect_index as u64 + * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64, + ); + } + } } #[cfg(feature = "2d")] @@ -3558,6 +3705,27 @@ impl Draw for DrawEffects { } } +#[cfg(feature = "3d")] +impl Draw for DrawEffects { + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &Opaque3d, + ) { + trace!("Draw: view={:?}", view); + draw( + world, + pass, + view, + item.representative_entity, + item.key.pipeline, + &mut self.params, + ); + } +} + /// Render node to run the simulation sub-graph once per frame. /// /// This node doesn't simulate anything by itself, but instead schedules the @@ -3995,6 +4163,18 @@ where first_buffer.expect("No buffers allocated") } +impl ParticleRenderAlphaMaskPipelineKey { + fn from_layout_flags(layout_flags: LayoutFlags) -> Self { + if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) { + ParticleRenderAlphaMaskPipelineKey::AlphaMask + } else if layout_flags.contains(LayoutFlags::OPAQUE) { + ParticleRenderAlphaMaskPipelineKey::Opaque + } else { + ParticleRenderAlphaMaskPipelineKey::Blend + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/render/vfx_common.wgsl b/src/render/vfx_common.wgsl index b1f32657..754aa5c4 100644 --- a/src/render/vfx_common.wgsl +++ b/src/render/vfx_common.wgsl @@ -88,11 +88,12 @@ const REM_OFFSET_PING: u32 = 1u; const RGI_OFFSET_VERTEX_COUNT: u32 = 0u; const RGI_OFFSET_INSTANCE_COUNT: u32 = 1u; -const RGI_OFFSET_VERTEX_OFFSET: u32 = 2u; -const RGI_OFFSET_BASE_INSTANCE: u32 = 3u; -const RGI_OFFSET_ALIVE_COUNT: u32 = 4u; -const RGI_OFFSET_MAX_UPDATE: u32 = 5u; -const RGI_OFFSET_DEAD_COUNT: u32 = 6u; +const RGI_OFFSET_FIRST_INDEX_OR_VERTEX_OFFSET: u32 = 2u; +const RGI_OFFSET_VERTEX_OFFSET_OR_BASE_INSTANCE: u32 = 3u; +const RGI_OFFSET_BASE_INSTANCE: u32 = 4u; +const RGI_OFFSET_ALIVE_COUNT: u32 = 5u; +const RGI_OFFSET_MAX_UPDATE: u32 = 6u; +const RGI_OFFSET_DEAD_COUNT: u32 = 7u; struct RenderEffectMetadata { /// Maxmimum number of init threads to run on next frame. This is cached from @@ -117,9 +118,11 @@ struct RenderGroupIndirect { vertex_count: u32, /// Number of mesh instances, equal to the number of particles. instance_count: atomic, - /// Vertex offset (always zero). - vertex_offset: i32, - /// Base instance. + /// First index (if indexed) or vertex offset (if non-indexed). + first_index_or_vertex_offset: u32, + /// Vertex offset (if indexed) or base instance (if non-indexed). + vertex_offset_or_base_instance: i32, + /// Base instance (if indexed). base_instance: u32, /// Number of particles alive after the init pass, used to calculate the number /// of compute threads to spawn for the update pass and to cap those threads diff --git a/src/render/vfx_render.wgsl b/src/render/vfx_render.wgsl index ef1dd778..b9e9a76b 100644 --- a/src/render/vfx_render.wgsl +++ b/src/render/vfx_render.wgsl @@ -20,6 +20,9 @@ struct VertexOutput { #ifdef NEEDS_UV @location(1) uv: vec2, #endif +#ifdef NEEDS_NORMAL + @location(2) normal: vec3, +#endif } @group(0) @binding(0) var view: View; @@ -76,6 +79,15 @@ fn unpack_compressed_transform(compressed_transform: mat3x4) -> mat4x4 ); } +// Unpacks a compressed transform and transposes is. +fn unpack_compressed_transform_3x3_transpose(compressed_transform: mat3x4) -> mat3x3 { + return mat3x3( + compressed_transform[0].xyz, + compressed_transform[1].xyz, + compressed_transform[2].xyz, + ); +} + /// Transform a simulation space position into a world space position. /// /// The simulation space depends on the effect's SimulationSpace value, and is either @@ -89,6 +101,18 @@ fn transform_position_simulation_to_world(sim_position: vec3) -> vec4 #endif } +fn transform_normal_simulation_to_world(sim_normal: vec3) -> vec3 { +#ifdef LOCAL_SPACE_SIMULATION + // We use the inverse transpose transform to transform normals. + // The inverse transpose is the same as the transposed inverse, so we can + // safely use the inverse transform. + let transform = unpack_compressed_transform_3x3_transpose(spawner.inverse_transform); + return transform * sim_normal; +#else + return sim_normal; +#endif +} + /// Transform a simulation space position into a clip space position. /// /// The simulation space depends on the effect's SimulationSpace value, and is either @@ -107,6 +131,9 @@ fn vertex( @location(0) vertex_position: vec3, #ifdef NEEDS_UV @location(1) vertex_uv: vec2, +#endif +#ifdef NEEDS_NORMAL + @location(2) vertex_normal: vec3, #endif // @location(1) vertex_color: u32, // @location(1) vertex_velocity: vec3, @@ -131,12 +158,17 @@ fn vertex( // Expand particle mesh vertex based on particle position ("origin"), and local // orientation and size of the particle mesh (currently: only quad). - let vpos = vertex_position * vec3(size.x, size.y, 1.0); - let sim_position = position + axis_x * vpos.x + axis_y * vpos.y; + let vpos = vertex_position * size; + let sim_position = position + axis_x * vpos.x + axis_y * vpos.y + axis_z * vpos.z; out.position = transform_position_simulation_to_clip(sim_position); out.color = color; +#ifdef NEEDS_NORMAL + let normal = mat3x3(axis_x, axis_y, axis_z) * vertex_normal; + out.normal = transform_normal_simulation_to_world(normal); +#endif // NEEDS_NORMAL + return out; } @@ -150,6 +182,9 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { #ifdef NEEDS_UV var uv = in.uv; #endif +#ifdef NEEDS_NORMAL + var normal = in.normal; +#endif {{FRAGMENT_MODIFIERS}}