diff --git a/Cargo.toml b/Cargo.toml index 86499943..f8cc964f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,10 @@ required-features = [ "bevy/bevy_winit", "bevy/bevy_sprite", "2d" ] name = "lifetime" required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ] +[[example]] +name = "instancing" +required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ] + [workspace] resolver = "2" members = ["."] diff --git a/examples/instancing.rs b/examples/instancing.rs new file mode 100644 index 00000000..3c5e4395 --- /dev/null +++ b/examples/instancing.rs @@ -0,0 +1,91 @@ +use bevy::{prelude::*, render::mesh::shape::Cube}; +use bevy_inspector_egui::WorldInspectorPlugin; + +use bevy_hanabi::prelude::*; + +fn main() { + App::default() + .insert_resource(bevy::log::LogSettings { + level: bevy::log::Level::WARN, + filter: "bevy_hanabi=trace,instancing=trace".to_string(), + }) + .add_plugins(DefaultPlugins) + .add_system(bevy::window::close_on_esc) + .add_plugin(HanabiPlugin) + .add_plugin(WorldInspectorPlugin::new()) + .add_startup_system(setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut effects: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let mut camera = Camera3dBundle::default(); + camera.transform.translation = Vec3::new(0.0, 0.0, 180.0); + commands.spawn_bundle(camera); + + commands.spawn_bundle(DirectionalLightBundle { + directional_light: DirectionalLight { + color: Color::WHITE, + // Crank the illuminance way (too) high to make the reference cube clearly visible + illuminance: 100000., + shadows_enabled: false, + ..Default::default() + }, + ..Default::default() + }); + + let cube = meshes.add(Mesh::from(Cube { size: 1.0 })); + let mat = materials.add(Color::PURPLE.into()); + + let mut gradient = Gradient::new(); + gradient.add_key(0.0, Vec4::new(0.0, 0.0, 1.0, 1.0)); + gradient.add_key(1.0, Vec4::splat(0.0)); + + let effect = effects.add( + EffectAsset { + name: "effect".to_string(), + capacity: 512, + spawner: Spawner::rate(50.0.into()), + ..Default::default() + } + .init(PositionSphereModifier { + center: Vec3::ZERO, + radius: 1., + dimension: ShapeDimension::Volume, + speed: 2.0.into(), + }) + .init(ParticleLifetimeModifier { lifetime: 12.0 }) + .render(ColorOverLifetimeModifier { gradient }), + ); + + for j in -4..=4 { + for i in -5..=5 { + commands + .spawn() + .insert(Name::new(format!("({},{})", i, j))) + .insert_bundle(ParticleEffectBundle { + effect: ParticleEffect::new(effect.clone()), + transform: Transform::from_translation(Vec3::new( + i as f32 * 10., + j as f32 * 10., + 0., + )), + ..Default::default() + }) + .with_children(|p| { + // Reference cube to visualize the emit origin + p.spawn() + .insert_bundle(PbrBundle { + mesh: cube.clone(), + material: mat.clone(), + ..Default::default() + }) + .insert(Name::new("source")); + }); + } + } +} diff --git a/run_examples.bat b/run_examples.bat index b510da13..aaf3dd5c 100644 --- a/run_examples.bat +++ b/run_examples.bat @@ -8,4 +8,5 @@ cargo r --example spawn_on_command --no-default-features --features="bevy/bevy_w cargo r --example activate --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d" cargo r --example force_field --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d" cargo r --example 2d --no-default-features --features="bevy/bevy_winit bevy/bevy_sprite 2d" -cargo r --example lifetime --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d" \ No newline at end of file +cargo r --example lifetime --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d" +cargo r --example instancing --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d" \ No newline at end of file diff --git a/src/render/effect_cache.rs b/src/render/effect_cache.rs index 1985a79a..fc091e66 100644 --- a/src/render/effect_cache.rs +++ b/src/render/effect_cache.rs @@ -117,7 +117,11 @@ pub enum BufferState { impl EffectBuffer { /// Minimum buffer capacity to allocate, in number of particles. - pub const MIN_CAPACITY: u32 = 65536; // at least 64k particles + // FIXME - Batching is broken due to binding a single GpuSpawnerParam instead of N, + // and inability for a particle index to tell which Spawner it should use. Setting + // this to 1 effectively ensures that all new buffers just fit the effect, so batching + // never occurs. + pub const MIN_CAPACITY: u32 = 1; //65536; // at least 64k particles /// Create a new group and a GPU buffer to back it up. /// diff --git a/src/render/mod.rs b/src/render/mod.rs index 63144720..1d15e5e7 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1396,6 +1396,7 @@ pub(crate) fn queue_effects( resource: BindingResource::Buffer(BufferBinding { buffer: effects_meta.spawner_buffer.buffer().unwrap(), offset: 0, + // TODO - should bind N consecutive structs for batching size: Some(GpuSpawnerParams::min_size()), }), }], @@ -1980,14 +1981,16 @@ impl Node for ParticleUpdateNode { assert!( spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize ); + let spawner_offset = spawner_base * spawner_buffer_aligned as u32; trace!( - "record commands for pipeline of effect {:?} ({} items / {}B/item = {} workgroups) spawner_base={} buffer_offset={}...", + "record commands for pipeline of effect {:?} ({} items / {}B/item = {} workgroups) spawner_base={} spawner_offset={} buffer_offset={}...", batch.handle, item_count, item_size, workgroup_count, spawner_base, + spawner_offset, buffer_offset, ); @@ -2007,7 +2010,7 @@ impl Node for ParticleUpdateNode { compute_pass.set_bind_group( 2, effects_meta.spawner_bind_group.as_ref().unwrap(), - &[spawner_base * spawner_buffer_aligned as u32], + &[spawner_offset], ); compute_pass.dispatch_workgroups(workgroup_count, 1, 1); trace!("compute dispatched");