Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,276 changes: 691 additions & 585 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["crates/*", ]
members = ["crates/*"]

[workspace.package]
version = "0.1.0"
Expand All @@ -9,9 +9,10 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/nixon-voxell/bevy_motion_matching"

[workspace.dependencies]
bevy = { version = "0.15", features = ["asset_processor"] }
bevy_gltf = "0.15"
bevy_egui = "0.33"
bevy = { version = "0.16", features = ["asset_processor"] }
bevy_gltf = "0.16"
bevy_egui = "0.34"
bevy_platform = "0.16"
serde_json = "1.0"
serde = "1.0"
bvh_anim = "0.4"
Expand Down Expand Up @@ -43,23 +44,24 @@ bevy_bvh_anim = { path = "crates/bevy_bvh_anim" }
bevy = { workspace = true }
bevy_gltf = { workspace = true }
bevy_egui = { workspace = true }
bevy_platform = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }

egui_extras = "0.31.1"
egui_plot = "0.31.0"
leafwing-input-manager = "0.16.0"
egui_plot = "0.32.1"
leafwing-input-manager = "0.17"

# Debug editor (run with debug feature to enable it)
bevy-inspector-egui = { version = "0.29", optional = true }
bevy-inspector-egui = { version = "0.31", optional = true }
kdtree = "0.7.0"
peak_alloc = "0.2.1"
peak_alloc = { git = "https://github.com/Steveplays28/peak_alloc", branch = "fix-usize-overflow" }
clustering = "0.2.1"
csv = "1.3.0"

[dev-dependencies]
bevy-inspector-egui = { version = "0.29" }
bevy-inspector-egui = { version = "0.31" }

[lints]
workspace = true
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
Motion matching enables characters to smoothly transition between animations by finding the best matching pose and trajectory from an extensive database, without the need to create state machines.
Gameplay logic can be embedded side by side with motion matching by querying animations with the desired attributes.

## Showcase

### Configuration

![config](./.github/assets/config.png)
Expand All @@ -16,6 +18,29 @@ Gameplay logic can be embedded side by side with motion matching by querying ani

![play-mode](./.github/assets/play-mode.png)

## Development

### Prerequisites

- Rust
- MSRV: v1.85.0
- Cargo
- Linux: [Bevy dependencies](https://github.com/bevyengine/bevy/blob/main/docs/linux_dependencies.md)
- Optional, for the Visual Studio Code `start` task: `cargo-watch`
- `cargo install cargo-watch` or `cargo binstall cargo-watch`

### Building

```bash
cargo build
```

### Running

```bash
cargo run --features bevy/dynamic_linking
```

## Reference

- [Learned Motion Matching](https://static-wordpress.ubisoft.com/montreal.ubisoft.com/wp-content/uploads/2020/07/09154101/Learned_Motion_Matching.pdf)
Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_bvh_anim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ pub mod joint_matrices;
pub mod joint_traits;

pub trait FrameExt {
#[must_use]
fn get_pos_rot(&self, joint_data: &JointData) -> (Vec3, Quat);

#[must_use]
fn get_pos(&self, joint_data: &JointData) -> Vec3;

#[must_use]
fn get_rot(&self, joint_data: &JointData) -> Quat;
}

impl FrameExt for Frame {
#[must_use]
fn get_pos_rot(&self, joint_data: &JointData) -> (Vec3, Quat) {
let mut pos = Vec3::ZERO;
let mut euler = Vec3::ZERO;
Expand All @@ -52,7 +54,6 @@ impl FrameExt for Frame {
)
}

#[must_use]
fn get_pos(&self, joint_data: &JointData) -> Vec3 {
let mut pos = Vec3::ZERO;

Expand All @@ -72,7 +73,6 @@ impl FrameExt for Frame {
pos
}

#[must_use]
fn get_rot(&self, joint_data: &JointData) -> Quat {
let mut euler = Vec3::ZERO;

Expand Down
4 changes: 2 additions & 2 deletions src/bvh_manager/bvh_gizmos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn armature_gizmos(
}

if let Ok(children) = q_children.get(parent) {
for &child in children.iter() {
for child in children.iter() {
if let Ok(transform) = q_transforms.get(child) {
let child_translation = transform.translation();

Expand All @@ -99,7 +99,7 @@ fn armature_gizmos(
}
}

if let Ok((entity, transform)) = q_character.get_single() {
if let Ok((entity, transform)) = q_character.single() {
recursive_draw(
0,
entity,
Expand Down
3 changes: 2 additions & 1 deletion src/bvh_manager/bvh_library.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::{fs, path::PathBuf, str::FromStr};

use bevy::{prelude::*, utils::HashSet};
use bevy::prelude::*;
use bevy_bvh_anim::prelude::*;
use bevy_platform::collections::HashSet;

pub const BVH_FOLDER: &str = "bvh";
pub const BVH_MAP_FOLDER: &str = "bvh_map";
Expand Down
6 changes: 3 additions & 3 deletions src/bvh_manager/bvh_player.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use bevy::{
asset::{DependencyLoadState, LoadState, RecursiveDependencyLoadState},
platform::collections::HashMap,
prelude::*,
utils::hashbrown::HashMap,
};
use bevy_bvh_anim::{bvh_anim::ChannelType, prelude::*};

Expand Down Expand Up @@ -35,7 +35,7 @@ fn generate_bone_map(
server: Res<AssetServer>,
mut asset_loaded: Local<bool>,
) {
let Ok((entity, scene_root)) = q_character.get_single() else {
let Ok((entity, scene_root)) = q_character.single() else {
return;
};

Expand Down Expand Up @@ -64,7 +64,7 @@ fn generate_bone_map(
q_transforms: &Query<&Transform>,
) {
if let Ok(children) = q_children.get(parent) {
for &child in children.iter() {
for child in children.iter() {
for _ in 0..indent {
print!("| ");
}
Expand Down
2 changes: 1 addition & 1 deletion src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ fn pan_orbit_camera(
{
if is_focus {
camera_focus.clear();
} else if let Ok(entity) = q_main_scene.get_single() {
} else if let Ok(entity) = q_main_scene.single() {
camera_focus.set(entity);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/motion/motion_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fn _test(

// if input.just_pressed(KeyCode::Space) {
// for entity in q_entities.iter() {
// jump_evw.send(JumpToPose {
// jump_evw.write(JumpToPose {
// motion_pose: MotionPose {
// chunk_index: 0,
// time: 3.0,
Expand Down
14 changes: 7 additions & 7 deletions src/motion_matching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn flow(
let index = motion_player.target_pair_index();
let Some(traj_pose) = &traj_pose_pair[index] else {
// Find a new animation to play.
traj_match_evw.send(TrajectoryMatch(entity));
traj_match_evw.write(TrajectoryMatch(entity));
continue;
};

Expand All @@ -101,7 +101,7 @@ fn flow(
continue;
}
false => {
pred_match_evw.send(PredictionMatch {
pred_match_evw.write(PredictionMatch {
motion_pose: *traj_pose.motion_pose(),
entity,
});
Expand Down Expand Up @@ -154,7 +154,7 @@ fn prediction_match(
trajectory_data.get_chunk(pred_match.chunk_index),
pose_data.is_chunk_loopable(pred_match.chunk_index),
) else {
traj_match_evw.send(TrajectoryMatch(pred_match.entity));
traj_match_evw.write(TrajectoryMatch(pred_match.entity));
continue;
};

Expand All @@ -163,7 +163,7 @@ fn prediction_match(
match loopable {
true => chunk_offset = 0,
false => {
traj_match_evw.send(TrajectoryMatch(pred_match.entity));
traj_match_evw.write(TrajectoryMatch(pred_match.entity));
continue;
}
}
Expand All @@ -186,7 +186,7 @@ fn prediction_match(
.collect::<Vec<_>>();

if traj.distance(&data_traj) > match_config.pred_match_threshold {
traj_match_evw.send(TrajectoryMatch(pred_match.entity));
traj_match_evw.write(TrajectoryMatch(pred_match.entity));
}
}
}
Expand Down Expand Up @@ -301,7 +301,7 @@ fn trajectory_match(
/ runs as f64;
motion_matching_result.matching_result.runs = runs;

nearest_trajectories_evw.send(NearestTrajectories {
nearest_trajectories_evw.write(NearestTrajectories {
trajectories: nearest_trajs,
entity,
});
Expand Down Expand Up @@ -378,7 +378,7 @@ fn pose_match(
motion_matching_result.selected_trajectory = best_traj_index;

let best_traj = &trajs[best_traj_index];
jump_evw.send(JumpToPose {
jump_evw.write(JumpToPose {
motion_pose: MotionPose {
chunk_index: best_traj.chunk_index,
time: motion_asset
Expand Down
2 changes: 1 addition & 1 deletion src/motion_matching/kdtree_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ fn trajectory_match_with_kdtree(
/ runs as f64;
motion_matching_result.matching_result.runs = runs;

nearest_trajectories_evw.send(NearestTrajectories {
nearest_trajectories_evw.write(NearestTrajectories {
trajectories: nearest_trajs,
entity,
});
Expand Down
2 changes: 1 addition & 1 deletion src/motion_matching/kmeans_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ fn trajectory_match_with_kmeans(
motion_matching_result.matching_result.runs = runs;
nearest_trajs.sort_by(|t0, t1| t0.distance.total_cmp(&t1.distance));

nearest_trajectories_evw.send(NearestTrajectories {
nearest_trajectories_evw.write(NearestTrajectories {
trajectories: nearest_trajs,
entity,
});
Expand Down
5 changes: 4 additions & 1 deletion src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ fn movement_direction(
if **run_preset_direction {
return;
}
let camera_transform = q_camera.single();
let Ok(camera_transform) = q_camera.single() else {
return;
};

let mut action_axis = action
.clamped_axis_pair(&PlayerAction::Walk)
.normalize_or_zero();
Expand Down
20 changes: 12 additions & 8 deletions src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use bevy::{ecs::system::SystemState, prelude::*};
use bevy_egui::{
egui::{self, Color32},
EguiContexts,
EguiContextPass, EguiContexts,
};

#[cfg(not(feature = "debug"))]
Expand All @@ -24,7 +24,9 @@ impl Plugin for UiPlugin {
#[cfg(feature = "debug")]
app.add_plugins(WorldInspectorPlugin::new());
#[cfg(not(feature = "debug"))]
app.add_plugins(EguiPlugin);
app.add_plugins(EguiPlugin {
enable_multipass_for_primary_context: true,
});

app.init_resource::<MouseInUi>()
.init_resource::<config::BvhTrailConfig>()
Expand All @@ -36,7 +38,7 @@ impl Plugin for UiPlugin {
.init_resource::<builder::BuildConfigs>()
.init_resource::<play_mode::MotionMatchingResult>()
.add_systems(PreUpdate, reset_mouse_in_ui)
.add_systems(Update, right_panel.in_set(UiSystemSet));
.add_systems(EguiContextPass, right_panel.in_set(UiSystemSet));
}
}

Expand Down Expand Up @@ -74,12 +76,14 @@ fn right_panel(
reset_player: &mut SystemState<EventWriter<ResetPlayer>>,
) {
let (mut contexts, mut page) = params.get_mut(world);

let ctx = contexts.ctx_mut().clone();
let Some(context) = contexts.try_ctx_mut() else {
return;
};
let context = context.clone();

egui::SidePanel::left("left_panel")
.resizable(true)
.show(&ctx, |ui| {
.show(&context, |ui| {
ui.add_space(10.0);
ui.horizontal(|ui| {
if ui.button("Config").clicked() {
Expand All @@ -95,7 +99,7 @@ fn right_panel(

if ui.button("Reset Player").clicked() {
let mut evw_reset_player = reset_player.get_mut(world);
evw_reset_player.send(ResetPlayer);
evw_reset_player.write(ResetPlayer);
}

egui::ScrollArea::vertical().show(ui, |ui| match *page {
Expand All @@ -108,7 +112,7 @@ fn right_panel(
});
});

if ctx.is_pointer_over_area() {
if context.is_pointer_over_area() {
let mut mouse_in_ui = world.resource_mut::<MouseInUi>();
mouse_in_ui.set_is_inside();
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use std::io::Write;

use bevy::ecs::system::SystemState;
use bevy::prelude::*;
use bevy::utils::{HashMap, HashSet};
use bevy_bvh_anim::prelude::*;
use bevy_egui::egui;
use bevy_platform::collections::{HashMap, HashSet};

use crate::bvh_manager::bvh_library::BvhLibrary;
use crate::motion::motion_asset::MotionAsset;
Expand Down
4 changes: 2 additions & 2 deletions src/ui/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ fn bvh_trail_config(ui: &mut egui::Ui, world: &mut World) {

fn show_character_checkbox(ui: &mut egui::Ui, world: &mut World) {
let mut q_main_scene = world.query_filtered::<&mut Visibility, With<MainScene>>();
let Ok(mut main_scene_vis) = q_main_scene.get_single_mut(world) else {
let Ok(mut main_scene_vis) = q_main_scene.single_mut(world) else {
return;
};

Expand All @@ -144,7 +144,7 @@ fn draw_trajectory_checkbox(ui: &mut egui::Ui, world: &mut World) {

fn show_ground_checkbox(ui: &mut egui::Ui, world: &mut World) {
let mut q_ground = world.query_filtered::<&mut Visibility, With<GroundPlane>>();
let Ok(mut vis) = q_ground.get_single_mut(world) else {
let Ok(mut vis) = q_ground.single_mut(world) else {
return;
};

Expand Down
Loading