Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Physics Interpolation and Extrapolation #566

Merged
merged 12 commits into from
Dec 7, 2024
11 changes: 6 additions & 5 deletions crates/avian2d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ bevy_picking = ["bevy/bevy_picking"]
serialize = [
"dep:serde",
"bevy/serialize",
"bevy_transform_interpolation/serialize",
"parry2d?/serde-serialize",
"parry2d-f64?/serde-serialize",
"bitflags/serde",
Expand All @@ -63,6 +64,7 @@ avian_derive = { path = "../avian_derive", version = "0.1" }
bevy = { version = "0.15", default-features = false }
bevy_math = { version = "0.15" }
bevy_heavy = { version = "0.1" }
bevy_transform_interpolation = { version = "0.1" }
libm = { version = "0.2", optional = true }
parry2d = { version = "0.17", optional = true }
parry2d-f64 = { version = "0.17", optional = true }
Expand Down Expand Up @@ -117,6 +119,10 @@ required-features = ["2d", "default-collider", "enhanced-determinism"]
name = "fixed_joint_2d"
required-features = ["2d", "default-collider"]

[[example]]
name = "interpolation"
required-features = ["2d"]

[[example]]
name = "move_marbles"
required-features = ["2d", "default-collider"]
Expand All @@ -136,8 +142,3 @@ required-features = ["2d", "default-collider"]
[[example]]
name = "debugdump_2d"
required-features = ["2d"]

[[bench]]
name = "pyramid"
required-features = ["2d", "default-collider"]
harness = false
231 changes: 231 additions & 0 deletions crates/avian2d/examples/interpolation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//! 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() {
let mut app = App::new();

// Add the `PhysicsInterpolationPlugin` to enable interpolation and extrapolation functionality.
//
// By default, interpolation and extrapolation must be enabled for each entity manually.
// Use `PhysicsInterpolationPlugin::interpolate_all()` to enable interpolation for all rigid bodies.
app.add_plugins((
DefaultPlugins,
PhysicsPlugins::default().with_length_unit(50.0),
PhysicsInterpolationPlugin::default(),
));

// 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<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
// 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<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
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<Entity, With<Ball>>) {
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<Time<Fixed>>, keyboard_input: Res<ButtonInput<KeyCode>>) {
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<TimestepText>>,
time: Res<Time<Fixed>>,
) {
let timestep = time.timestep().as_secs_f32().recip();
text.0 = format!("{timestep:.2}");
}
2 changes: 2 additions & 0 deletions crates/avian3d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ serialize = [
"dep:serde",
"bevy/serialize",
"bevy_heavy/serialize",
"bevy_transform_interpolation/serialize",
"parry3d?/serde-serialize",
"parry3d-f64?/serde-serialize",
"bitflags/serde",
Expand All @@ -66,6 +67,7 @@ avian_derive = { path = "../avian_derive", version = "0.1" }
bevy = { version = "0.15", default-features = false }
bevy_math = { version = "0.15" }
bevy_heavy = { version = "0.1" }
bevy_transform_interpolation = { version = "0.1" }
libm = { version = "0.2", optional = true }
parry3d = { version = "0.17", optional = true }
parry3d-f64 = { version = "0.17", optional = true }
Expand Down
Loading
Loading