diff --git a/CHANGELOG.md b/CHANGELOG.md index 509033c8..d5fd12ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,37 @@ ## Unreleased +## v0.28.0 (09 December 2024) + +### Modified + +- `RapierContext` has been split in multiple `Component`s: + - `RapierContextColliders` + - `RapierContextJoints` + - `RapierContextSimulation` + - `RapierRigidBodySet` +- Renamed `DefaultReadRapierContext` and `DefaultWriteRapierContext`. + - Use `ReadRapierContext` and its associated `single()` method. + +## v0.28.0 (09 December 2024) + ### Modified - Update from rapier `0.21` to rapier `0.22`, see [rapier's changelog](https://github.com/dimforge/rapier/blob/master/CHANGELOG.md). +- Update bevy to 0.15. +- `RapierContext`, `RapierConfiguration` and `SimulationToRenderTime` are now a `Component` instead of resources. + - Rapier now supports multiple independent physics worlds, see example `multi_world3` for usage details. + - Migration guide: + - `ResMut` -> `WriteDefaultRapierContext` + - `Res` -> `ReadDefaultRapierContext` + - Access to `RapierConfiguration` and `SimulationToRenderTime` should query for it +on the responsible entity owning the `RenderContext`. + - If you are building a library on top of `bevy_rapier` and would want to support multiple independent physics worlds too, +you can check out the details of [#545](https://github.com/dimforge/bevy_rapier/pull/545) +to get more context and information. +- `colliders_with_aabb_intersecting_aabb` now takes `bevy::math::bounding::Aabb3d` (or `[..]::Aabb2d` in 2D) as parameter. + - it is now accessible with `headless` feature enabled. ### Fix @@ -21,21 +48,6 @@ which was its hardcoded behaviour. `RapierDebugColliderPlugin` and `DebugRenderContext`, as well as individual collider setup via a `ColliderDebug` component. -### Modified - -- `RapierContext`, `RapierConfiguration` and `RenderToSimulationTime` are now a `Component` instead of resources. - - Rapier now supports multiple independent physics worlds, see example `multi_world3` for usage details. - - Migration guide: - - `ResMut` -> `WriteDefaultRapierContext` - - `Res` -> `ReadDefaultRapierContext` - - Access to `RapierConfiguration` and `RenderToSimulationTime` should query for it -on the responsible entity owning the `RenderContext`. - - If you are building a library on top of `bevy_rapier` and would want to support multiple independent physics worlds too, -you can check out the details of [#545](https://github.com/dimforge/bevy_rapier/pull/545) -to get more context and information. -- `colliders_with_aabb_intersecting_aabb` now takes `bevy::math::bounding::Aabb3d` (or `[..]::Aabb2d` in 2D) as parameter. - - it is now accessible with `headless` feature enabled. - ## v0.27.0 (07 July 2024) **This is an update from rapier 0.19 to Rapier 0.21 which includes several stability improvements diff --git a/Cargo.toml b/Cargo.toml index cd1b8bc9..abab0339 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,18 +2,17 @@ members = ["bevy_rapier2d", "bevy_rapier3d", "bevy_rapier_benches3d"] resolver = "2" -[workspace.lints] -rust.unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(feature, values("dim2", "dim3"))', -] } - [profile.dev] # Use slightly better optimization by default, as examples otherwise seem laggy. -opt-level = 1 +# opt-level = 1 [profile.release] codegen-units = 1 +[profile.tracing] +inherits = "release" +debug = 1 + [patch.crates-io] #nalgebra = { path = "../nalgebra" } #parry2d = { path = "../parry/crates/parry2d" } @@ -25,5 +24,3 @@ codegen-units = 1 #parry3d = { git = "https://github.com/dimforge/parry", branch = "master" } #rapier2d = { git = "https://github.com/dimforge/rapier", branch = "character-controller" } #rapier3d = { git = "https://github.com/dimforge/rapier", branch = "character-controller" } -bevy_egui = { git = "https://github.com/Vrixyz/bevy_egui", branch = "bevy_main" } -bevy-inspector-egui = { git = "https://github.com/Vrixyz/bevy-inspector-egui", branch = "bevy_0.15" } diff --git a/bevy_rapier2d/Cargo.toml b/bevy_rapier2d/Cargo.toml index 983e9b33..4c86e072 100644 --- a/bevy_rapier2d/Cargo.toml +++ b/bevy_rapier2d/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_rapier2d" -version = "0.27.0" +version = "0.28.0" authors = ["Sébastien Crozet "] description = "2-dimensional physics engine in Rust, official Bevy plugin." documentation = "http://docs.rs/bevy_rapier2d" @@ -18,7 +18,10 @@ path = "../src/lib.rs" required-features = ["dim2"] [lints] -workspace = true +rust.unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(feature, values("dim3"))', +] } +clippy = { needless_lifetimes = "allow" } [features] default = ["dim2", "async-collider", "debug-render-2d"] @@ -47,30 +50,44 @@ serde-serialize = ["rapier2d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier2d/enhanced-determinism"] headless = [] async-collider = ["bevy/bevy_asset", "bevy/bevy_scene"] +background_simulation = ["bevy/multi_threaded"] [dependencies] -bevy = { version = "0.15.0-rc.2", default-features = false } +bevy = { version = "0.15", default-features = false } nalgebra = { version = "0.33", features = ["convert-glam029"] } +#rapier2d = { git = "http://github.com/dimforge/rapier", branch = "master" } rapier2d = "0.22" +profiling = "1.0" + bitflags = "2.4" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +crossbeam-channel = "0.5" +async-std = "1.13" [dev-dependencies] -bevy = { version = "0.15.0-rc.2", default-features = false, features = [ +bevy = { version = "0.15", default-features = false, features = [ "x11", "bevy_state", + "bevy_window", "bevy_debug_stepping", + "bevy_text", + "bevy_ui", + "bevy_asset", + "default_font", ] } oorandom = "11" approx = "0.5.1" glam = { version = "0.29", features = ["approx"] } bevy-inspector-egui = "0.28.0" -bevy_egui = "0.30.0" -# bevy_mod_debugdump = "0.11" -bevy_mod_debugdump = { git = "https://github.com/andriyDev/bevy_mod_debugdump.git", branch = "bevy-0.15" } +bevy_egui = "0.31" +bevy_mod_debugdump = "0.12" bevy_transform_interpolation = { git = "https://github.com/Jondolf/bevy_transform_interpolation.git" } [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs features = ["debug-render-2d", "serde-serialize"] + +[[example]] +name = "background2" +# required-features = ["background_simulation", "bevy/multi_threaded"] diff --git a/bevy_rapier2d/examples/background2.rs b/bevy_rapier2d/examples/background2.rs new file mode 100644 index 00000000..c351d7c4 --- /dev/null +++ b/bevy_rapier2d/examples/background2.rs @@ -0,0 +1,288 @@ +//! This example should be run with features bevy/multi_threaded and bevy_rapier2d/background_simulation + +use std::{fs::File, io::Write}; + +use bevy::color::palettes::css::GOLD; +use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}; +use bevy::prelude::*; +use bevy::{color::palettes, prelude::*}; +use bevy_mod_debugdump::{schedule_graph, schedule_graph_dot}; +use bevy_rapier2d::plugin::systems::task::SimulationTask; +use bevy_rapier2d::prelude::*; +use bevy_transform_interpolation::prelude::{ + RotationInterpolation, TransformInterpolationPlugin, TranslationInterpolation, +}; +use configuration::FixedSubsteps; + +fn main() { + let mut app = App::new(); + app.insert_resource(ClearColor(Color::srgb( + 0xF9 as f32 / 255.0, + 0xF9 as f32 / 255.0, + 0xFF as f32 / 255.0, + ))) + .insert_resource(TimestepMode::FixedSubsteps(FixedSubsteps { + // target 1 steps per frame if 60 rendering frames per second. + // For determinism, substep_dt should never be changed. + substep_dt: (1.0 / 60.0) / 1.0, + substeps: 1, + })) + .insert_resource(Time::::from_hz(60.0)) + .add_plugins(( + DefaultPlugins, + FrameTimeDiagnosticsPlugin, + TransformInterpolationPlugin::default(), + RapierPhysicsPlugin::::pixels_per_meter(100.0).in_fixed_schedule(), + RapierDebugRenderPlugin::default(), + )) + .add_systems(Startup, (setup_graphics, setup_physics)) + .add_systems(Update, fps_text_update_system); + app.add_systems( + PostUpdate, + debug_with_transform_info.after(TransformSystem::TransformPropagate), + ); + app.add_systems( + FixedUpdate, + ( + sim_to_render_text_update_system, + react_before_starting_simulation, + ) + .chain() + .after(PhysicsSet::StepSimulation) + .before(PhysicsSet::StartBackgroundSimulation), + ); + + let mut debugdump_settings = schedule_graph::Settings::default(); + // Filter out some less relevant systems. + debugdump_settings.include_system = + Some(Box::new(|system: &(dyn System)| { + if system.name().starts_with("bevy_pbr") + || system.name().starts_with("bevy_render") + || system.name().starts_with("bevy_gizmos") + || system.name().starts_with("bevy_winit") + || system.name().starts_with("bevy_sprite") + { + return false; + } + true + })); + let dot = schedule_graph_dot(&mut app, PostUpdate, &debugdump_settings); + + let mut file = File::create("interpolation2.dot").expect("could not create file."); + file.set_len(0).unwrap(); + file.write_all(&dot.as_bytes()) + .expect("Could not write to file"); + + app.run(); +} + +#[derive(Component, Clone)] +pub struct VisualBallDebug; + +#[derive(Component)] +struct SimToRenderTimeText; +#[derive(Component)] +struct FPSText; + +pub fn setup_graphics(mut commands: Commands) { + commands.spawn(( + Camera2d::default(), + OrthographicProjection { + scale: 15.0, + ..OrthographicProjection::default_2d() + }, + Transform::from_xyz(0.0, 50.0, 0.0), + )); + commands + .spawn(Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + justify_content: JustifyContent::SpaceBetween, + ..default() + }) + .insert(PickingBehavior::IGNORE) + .with_children(|parent| { + parent.spawn(( + Text::new(""), + TextFont { + font_size: 30.0, + ..default() + }, + TextColor(GOLD.into()), + SimToRenderTimeText, + )); + parent.spawn(( + // Create a Text with multiple child spans. + Text::new("FPS: "), + TextFont { + font_size: 42.0, + ..default() + }, + FPSText, + )); + }); +} + +fn sim_to_render_text_update_system( + mut time_step_mode: ResMut, + sim_to_render: Query<(&SimulationToRenderTime, &RapierContextColliders)>, + mut query: Query<(&mut TextColor, &mut Text), With>, +) { + let TimestepMode::FixedSubsteps(update_strat) = *time_step_mode else { + return; + }; + let (sim_to_render_time, colliders) = sim_to_render.get_single().unwrap(); + for (mut color, mut text) in &mut query { + text.0 = format!( + "real lag to render: {:.2} +lag taken into account: {:.2} +nb of frames to simulate physics: {} +nb of substeps for last physics simulation: {} +substep deltatime: {:.2} +amount of entities: {}", + sim_to_render_time.diff, + sim_to_render_time.accumulated_diff, + sim_to_render_time.last_simulation_frame_count, + update_strat.substeps, + update_strat.substep_dt, + colliders.colliders.len() + ); + *color = if sim_to_render_time.diff < 0.0 { + // simulation is ahead! + palettes::basic::GREEN.into() + } else { + // simulation is behind! + palettes::basic::BLUE.into() + }; + } +} +fn fps_text_update_system( + diagnostics: Res, + mut query: Query<(&mut TextColor, &mut Text), With>, +) { + for (mut color, mut text) in &mut query { + if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) { + if let Some(value) = fps.smoothed() { + text.0 = format!("FPS: {value:.0}"); + *color = if value > 50.0 { + palettes::basic::GREEN.into() + } else { + palettes::basic::RED.into() + }; + } + } + } +} + +pub fn setup_physics(mut commands: Commands) { + /* + * Ground + */ + let ground_size = 5000.0; + let ground_height = 100.0; + + commands.spawn(( + Transform::from_xyz(0.0, 0.0 * -ground_height, 0.0), + Collider::cuboid(ground_size, ground_height), + )); + + let ball = ( + Transform::from_xyz(0.0, 200.0, 0.0), + RigidBody::Dynamic, + Collider::ball(20.0), + Restitution { + coefficient: 0.99, + combine_rule: CoefficientCombineRule::Max, + }, + VisualBallDebug, + ); + let ball_column_height = 1000; + for i in 0..ball_column_height { + let y_offset = i as f32 * 41f32; + let x_noise_offset = i as f32 / ball_column_height as f32 - 0.5f32; + commands.spawn(ball.clone()).insert(Transform::from_xyz( + 80.0 + x_noise_offset, + 200.0 + y_offset, + 0.0, + )); + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(0.0 + x_noise_offset, 200.0 + y_offset, 0.0), + //TranslationInterpolation, + )); + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(-80.0 + x_noise_offset, 200.0 + y_offset, 0.0), + //TranslationInterpolation, + ColliderDebug::NeverRender, + )); + + for i in 0..4 { + let x_offset = 80.0 * i as f32; + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(-x_offset + x_noise_offset, 200.0 + y_offset, 0.0), + //TranslationInterpolation, + ColliderDebug::NeverRender, + )); + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(x_offset + x_noise_offset, 200.0 + y_offset, 0.0), + //TranslationInterpolation, + ColliderDebug::NeverRender, + )); + } + } +} + +pub fn debug_with_transform_info( + mut gizmos: Gizmos, + entities: Query<(&Transform, &Collider), With>, +) { + for (transform, collider) in entities.iter() { + gizmos.circle( + transform.translation, + collider.as_ball().unwrap().radius(), + palettes::basic::RED, + ); + } +} + +pub fn react_before_starting_simulation( + mut time_step_mode: ResMut, + mut q_context: Query<&mut SimulationToRenderTime, Without>, +) { + for mut sim_to_render_time in q_context.iter_mut() { + profiling::scope!("react_before_starting_simulation"); + dbg!("before starting simulation"); + if let TimestepMode::FixedSubsteps(sync) = &mut *time_step_mode { + if sim_to_render_time.diff > 0.5f32 { + // The simulation is behind the render time. The simulation slows down, + // the strategy to handle this could be to : + // - run more steps per simulation frame ( avoiding bevy overhead, synchronization, etc) + // - reduce the quality of the simulation (reduce substeps, ccd...) + // - allow a time drift: accept the simulation has slowed down, and don't attempt to catch up. + // For now, we just reset the diff to 0, effectively allowing a time drift, + // but ideally, we should report that to the user. + //dbg!(sim_to_render_time.diff = 0f32); + + let half_time_to_catch_back = sim_to_render_time.diff / 2f32; + if sim_to_render_time.diff > 1f32 { + // Allow a time drift, the simulation is too far behind. + sim_to_render_time.diff = 0f32; + } + let clamped_substeps_runs = + ((half_time_to_catch_back / sync.substep_dt as f32) as usize).clamp(10, 40); + sync.substeps = clamped_substeps_runs; + dbg!(sync.substeps); + } else { + // the physics simulation is ahead of the real time, it should either: + // - do nothing: wait for the render to catch up + // - run again, then wait for the render to catch up before reading. + // - allow user to customize ? + // This could be a target_sim_to_render ; where we should starting new when sim_to_render.diff > target_overtime + + // TODO: estimate how many frames it will take to catch up. + // If we're not behind, we can expect it to take 1 frame. + let time_to_advance = (1.0 / 60.0 + sim_to_render_time.diff).max(0f32); + sync.substeps = (time_to_advance / sync.substep_dt) as usize; + } + } + } +} diff --git a/bevy_rapier2d/examples/debugdump2.rs b/bevy_rapier2d/examples/debugdump2.rs index 88b302d0..da7c49f7 100644 --- a/bevy_rapier2d/examples/debugdump2.rs +++ b/bevy_rapier2d/examples/debugdump2.rs @@ -1,6 +1,6 @@ //! Example using bevy_mod_debugdump to output a graph of systems execution order. //! run with: -//! `cargo run --example debugdump2 > dump.dot && dot -Tsvg dump.dot > dump.svg` +//! `cargo run --example debugdump2 > dump.dot && dot -Tsvg dump.dot > dump.svg` use bevy::prelude::*; use bevy_mod_debugdump::{schedule_graph, schedule_graph_dot}; diff --git a/bevy_rapier2d/examples/testbed2.rs b/bevy_rapier2d/examples/testbed2.rs index debfc0f0..c1bcc9d0 100644 --- a/bevy_rapier2d/examples/testbed2.rs +++ b/bevy_rapier2d/examples/testbed2.rs @@ -13,7 +13,7 @@ mod player_movement2; mod rope_joint2; use bevy::prelude::*; -use bevy_egui::EguiPlugin; +use bevy_egui::{egui, EguiContexts, EguiPlugin}; use bevy_inspector_egui::quick::WorldInspectorPlugin; use bevy_rapier2d::prelude::*; @@ -108,6 +108,7 @@ fn main() { ) .run_if(in_state(Examples::DebugToggle2)), ) + .add_systems(OnExit(Examples::DebugToggle2), cleanup) // // rope joint .add_systems( @@ -204,11 +205,12 @@ fn main() { OnExit(Examples::PlayerMovement2), ( cleanup, - |mut rapier_config: Query<&mut RapierConfiguration>, - ctxt: ReadDefaultRapierContext| { + |mut rapier_config: Query<&mut RapierConfiguration>, ctxt: ReadRapierContext| { let mut rapier_config = rapier_config.single_mut(); - rapier_config.gravity = - RapierConfiguration::new(ctxt.integration_parameters.length_unit).gravity; + rapier_config.gravity = RapierConfiguration::new( + ctxt.single().simulation.integration_parameters.length_unit, + ) + .gravity; }, ), ) @@ -224,7 +226,7 @@ fn main() { .add_systems( Update, ( - //ui_example_system, + ui_example_system, change_example.run_if(resource_changed::), ) .chain(), @@ -260,7 +262,7 @@ fn change_example( ) { next_state.set(examples_available.0[example_selected.0].state); } -/* + fn ui_example_system( mut contexts: EguiContexts, mut current_example: ResMut, @@ -284,4 +286,3 @@ fn ui_example_system( } }); } -*/ diff --git a/bevy_rapier3d/Cargo.toml b/bevy_rapier3d/Cargo.toml index c17f1ccf..46c92a6c 100644 --- a/bevy_rapier3d/Cargo.toml +++ b/bevy_rapier3d/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_rapier3d" -version = "0.27.0" +version = "0.28.0" authors = ["Sébastien Crozet "] description = "3-dimensional physics engine in Rust, official Bevy plugin." documentation = "http://docs.rs/bevy_rapier3d" @@ -18,7 +18,10 @@ path = "../src/lib.rs" required-features = ["dim3"] [lints] -workspace = true +rust.unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(feature, values("dim2"))', +] } +clippy = { needless_lifetimes = "allow" } [features] default = ["dim3", "async-collider", "debug-render-3d"] @@ -47,38 +50,37 @@ wasm-bindgen = ["rapier3d/wasm-bindgen"] serde-serialize = ["rapier3d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier3d/enhanced-determinism"] headless = [] -async-collider = ["bevy/bevy_asset", "bevy/bevy_scene"] +async-collider = ["bevy/bevy_asset", "bevy/bevy_scene", "bevy/bevy_render"] +background_simulation = ["bevy/multi_threaded"] [dependencies] -bevy = { version = "0.15.0-rc.2", default-features = false } +bevy = { version = "0.15", default-features = false } nalgebra = { version = "0.33", features = ["convert-glam029"] } rapier3d = "0.22" bitflags = "2.4" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +crossbeam-channel = "0.5" +async-std = "1.13" +profiling = "1.0" [dev-dependencies] -bevy = { version = "0.15.0-rc.3", default-features = false, features = [ +bevy = { version = "0.15", default-features = false, features = [ + "bevy_window", "x11", "tonemapping_luts", "bevy_state", "bevy_debug_stepping", + "bevy_text", + "bevy_ui", + "bevy_asset", + "default_font", ] } approx = "0.5.1" glam = { version = "0.29", features = ["approx"] } bevy-inspector-egui = "0.28" -bevy_egui = "0.30.0" -divan = "0.1" -bevy_rapier_benches3d = { version = "0.1", path = "../bevy_rapier_benches3d" } +bevy_egui = "0.31" [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs features = ["debug-render-3d", "serde-serialize"] - -[[bench]] -name = "cubes" -harness = false - -[[bench]] -name = "many_pyramids3" -harness = false diff --git a/bevy_rapier3d/examples/joints3.rs b/bevy_rapier3d/examples/joints3.rs index 3edecc74..3ded4ade 100644 --- a/bevy_rapier3d/examples/joints3.rs +++ b/bevy_rapier3d/examples/joints3.rs @@ -279,7 +279,7 @@ pub fn setup_physics(mut commands: Commands) { } pub fn print_impulse_revolute_joints( - context: ReadDefaultRapierContext, + context: ReadRapierContext, joints: Query<(Entity, &ImpulseJoint)>, ) { for (entity, impulse_joint) in joints.iter() { @@ -288,7 +288,7 @@ pub fn print_impulse_revolute_joints( println!( "angle for {}: {:?}", entity, - context.impulse_revolute_joint_angle(entity), + context.single().impulse_revolute_joint_angle(entity), ); } _ => {} diff --git a/bevy_rapier3d/examples/multi_world3.rs b/bevy_rapier3d/examples/multi_contexts3.rs similarity index 80% rename from bevy_rapier3d/examples/multi_world3.rs rename to bevy_rapier3d/examples/multi_contexts3.rs index 54a0ad93..8e74acbb 100644 --- a/bevy_rapier3d/examples/multi_world3.rs +++ b/bevy_rapier3d/examples/multi_contexts3.rs @@ -1,7 +1,7 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*}; use bevy_rapier3d::prelude::*; -const N_WORLDS: usize = 2; +const N_CONTEXTS: usize = 2; fn main() { App::new() @@ -18,21 +18,21 @@ fn main() { )) .add_systems( Startup, - ((create_worlds, setup_physics).chain(), setup_graphics), + ((create_contexts, setup_physics).chain(), setup_graphics), ) .add_systems(Update, move_platforms) .add_systems( Update, - change_world.run_if(input_just_pressed(KeyCode::KeyC)), + change_context.run_if(input_just_pressed(KeyCode::KeyC)), ) .run(); } -fn create_worlds(mut commands: Commands) { - for i in 0..N_WORLDS { - let mut world = commands.spawn((RapierContext::default(), WorldId(i))); +fn create_contexts(mut commands: Commands) { + for i in 0..N_CONTEXTS { + let mut context = commands.spawn((RapierContextSimulation::default(), ContextId(i))); if i == 0 { - world.insert(DefaultRapierContext); + context.insert((DefaultRapierContext, RapierContextSimulation::default())); } } } @@ -45,7 +45,7 @@ fn setup_graphics(mut commands: Commands) { } #[derive(Component)] -pub struct WorldId(pub usize); +pub struct ContextId(pub usize); #[derive(Component)] struct Platform { @@ -58,8 +58,8 @@ fn move_platforms(time: Res