From a29a40c5c837939e8800ef56d5278776755f5ed2 Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Wed, 12 Mar 2025 22:11:42 -0400 Subject: [PATCH] Copy Avian2d interpolation example https://github.com/Jondolf/avian/blob/5b8e64c6fdf7aca0854d7d25da1a3a3fd8152df3/crates/avian2d/examples/interpolation.rs --- src/main.rs | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e7a11a9..b7264d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,235 @@ +//! This example showcases how `Transform` interpolation or extrapolation can be used +//! to make movement appear smooth at fixed timesteps. +//! +//! To produce consistent, frame rate independent behavior, physics by default runs +//! in the `FixedPostUpdate` schedule with a fixed timestep, meaning that the time between +//! physics ticks remains constant. On some frames, physics can either not run at all or run +//! more than once to catch up to real time. This can lead to visible stutter for movement. +//! +//! `Transform` interpolation resolves this issue by updating `Transform` at every frame in between +//! physics ticks to smooth out the visual result. The interpolation is done from the previous position +//! to the current physics position, which keeps movement smooth, but has the downside of making movement +//! feel slightly delayed as the rendered result lags slightly behind the true positions. +//! +//! `Transform` extrapolation works similarly, but instead of using the previous positions, it predicts +//! the next positions based on velocity. This makes movement feel much more responsive, but can cause +//! jumpy results when the prediction is wrong, such as when the velocity of an object is suddenly altered. + +use avian2d::{math::*, prelude::*}; +use bevy::{ + color::palettes::{ + css::WHITE, + tailwind::{CYAN_400, LIME_400, RED_400}, + }, + input::common_conditions::input_pressed, + prelude::*, +}; + fn main() { - println!("Hello, world!"); + let mut app = App::new(); + + // Interpolation and extrapolation functionality is enabled by the `PhysicsInterpolationPlugin`. + // It is included in the `PhysicsPlugins` by default. + app.add_plugins(( + DefaultPlugins, + PhysicsPlugins::default().with_length_unit(50.0), + )); + + // By default, interpolation must be enabled for each entity manually + // by adding the `TransformInterpolation` component. + // + // It can also be enabled for all rigid bodies with `PhysicsInterpolationPlugin::interpolate_all()`: + // + // app.add_plugins(PhysicsPlugins::default().set(PhysicsInterpolationPlugin::interpolate_all())); + + // Set gravity. + app.insert_resource(Gravity(Vector::NEG_Y * 900.0)); + + // Set the fixed timestep to just 10 Hz for demonstration purposes. + app.insert_resource(Time::from_hz(10.0)); + + // Setup the scene and UI, and update text in `Update`. + app.add_systems(Startup, (setup_scene, setup_balls, setup_text)) + .add_systems( + Update, + ( + change_timestep, + update_timestep_text, + // Reset the scene when the 'R' key is pressed. + reset_balls.run_if(input_pressed(KeyCode::KeyR)), + ), + ); + + // Run the app. + app.run(); +} + +#[derive(Component)] +struct Ball; + +fn setup_scene( + mut commands: Commands, + mut materials: ResMut>, + mut meshes: ResMut>, +) { + // Spawn a camera. + commands.spawn(Camera2d); + + // Spawn the ground. + commands.spawn(( + Name::new("Ground"), + RigidBody::Static, + Collider::rectangle(500.0, 20.0), + Restitution::new(0.99).with_combine_rule(CoefficientCombine::Max), + Transform::from_xyz(0.0, -300.0, 0.0), + Mesh2d(meshes.add(Rectangle::new(500.0, 20.0))), + MeshMaterial2d(materials.add(Color::from(WHITE))), + )); +} + +fn setup_balls( + mut commands: Commands, + mut materials: ResMut>, + mut meshes: ResMut>, +) { + let circle = Circle::new(30.0); + let mesh = meshes.add(circle); + + // This entity uses transform interpolation. + commands.spawn(( + Name::new("Interpolation"), + Ball, + RigidBody::Dynamic, + Collider::from(circle), + TransformInterpolation, + Transform::from_xyz(-100.0, 300.0, 0.0), + Mesh2d(mesh.clone()), + MeshMaterial2d(materials.add(Color::from(CYAN_400)).clone()), + )); + + // This entity uses transform extrapolation. + commands.spawn(( + Name::new("Extrapolation"), + Ball, + RigidBody::Dynamic, + Collider::from(circle), + TransformExtrapolation, + Transform::from_xyz(0.0, 300.0, 0.0), + Mesh2d(mesh.clone()), + MeshMaterial2d(materials.add(Color::from(LIME_400)).clone()), + )); + + // This entity is simulated in `FixedUpdate` without any smoothing. + commands.spawn(( + Name::new("No Interpolation"), + Ball, + RigidBody::Dynamic, + Collider::from(circle), + Transform::from_xyz(100.0, 300.0, 0.0), + Mesh2d(mesh.clone()), + MeshMaterial2d(materials.add(Color::from(RED_400)).clone()), + )); +} + +/// Despawns all balls and respawns them. +fn reset_balls(mut commands: Commands, query: Query>) { + for entity in &query { + commands.entity(entity).despawn(); + } + + commands.run_system_cached(setup_balls); +} + +#[derive(Component)] +struct TimestepText; + +fn setup_text(mut commands: Commands) { + let font = TextFont { + font_size: 20.0, + ..default() + }; + + commands + .spawn(( + Text::new("Fixed Hz: "), + TextColor::from(WHITE), + font.clone(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + )) + .with_child((TimestepText, TextSpan::default())); + + commands.spawn(( + Text::new("Change Timestep With Up/Down Arrow\nPress R to reset"), + TextColor::from(WHITE), + TextLayout::new_with_justify(JustifyText::Right), + font.clone(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + right: Val::Px(10.0), + ..default() + }, + )); + + commands.spawn(( + Text::new("Interpolation"), + TextColor::from(CYAN_400), + font.clone(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(50.0), + left: Val::Px(10.0), + ..default() + }, + )); + + commands.spawn(( + Text::new("Extrapolation"), + TextColor::from(LIME_400), + font.clone(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(75.0), + left: Val::Px(10.0), + ..default() + }, + )); + + commands.spawn(( + Text::new("No Interpolation"), + TextColor::from(RED_400), + font.clone(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(100.0), + left: Val::Px(10.0), + ..default() + }, + )); +} + +/// Changes the timestep of the simulation when the up or down arrow keys are pressed. +fn change_timestep(mut time: ResMut>, keyboard_input: Res>) { + if keyboard_input.pressed(KeyCode::ArrowUp) { + let new_timestep = (time.delta_secs_f64() * 0.975).max(1.0 / 255.0); + time.set_timestep_seconds(new_timestep); + } + if keyboard_input.pressed(KeyCode::ArrowDown) { + let new_timestep = (time.delta_secs_f64() * 1.025).min(1.0 / 5.0); + time.set_timestep_seconds(new_timestep); + } +} + +/// Updates the text with the current timestep. +fn update_timestep_text( + mut text: Single<&mut TextSpan, With>, + time: Res>, +) { + let timestep = time.timestep().as_secs_f32().recip(); + text.0 = format!("{timestep:.2}"); }