├── crates ├── bevy_animation_graph_editor │ ├── .gitignore │ ├── src │ │ ├── ui │ │ │ ├── events │ │ │ │ └── mod.rs │ │ │ ├── view_state │ │ │ │ └── mod.rs │ │ │ ├── editor_windows │ │ │ │ ├── mod.rs │ │ │ │ └── ragdoll_editor │ │ │ │ │ ├── settings_panel.rs │ │ │ │ │ ├── body_inspector.rs │ │ │ │ │ ├── body_mapping_inspector.rs │ │ │ │ │ ├── collider_inspector.rs │ │ │ │ │ ├── bone_mapping_inspector.rs │ │ │ │ │ └── top_panel.rs │ │ │ ├── generic_widgets │ │ │ │ ├── mod.rs │ │ │ │ ├── picker.rs │ │ │ │ ├── body_mode.rs │ │ │ │ ├── quat.rs │ │ │ │ ├── animation_node.rs │ │ │ │ ├── option.rs │ │ │ │ ├── uuid.rs │ │ │ │ ├── isometry3d.rs │ │ │ │ ├── asset_picker.rs │ │ │ │ └── vec3.rs │ │ │ ├── mod.rs │ │ │ ├── ecs_utils.rs │ │ │ ├── global_state │ │ │ │ ├── active_graph.rs │ │ │ │ ├── active_fsm_state.rs │ │ │ │ ├── active_fsm.rs │ │ │ │ ├── active_ragdoll.rs │ │ │ │ ├── active_skeleton.rs │ │ │ │ ├── active_graph_node.rs │ │ │ │ ├── active_fsm_transition.rs │ │ │ │ ├── inspector_selection.rs │ │ │ │ ├── scene_picker.rs │ │ │ │ ├── active_graph_context.rs │ │ │ │ └── active_scene.rs │ │ │ ├── reflect_widgets │ │ │ │ ├── checkbox.rs │ │ │ │ ├── entity_path.rs │ │ │ │ ├── plugin.rs │ │ │ │ ├── pattern_mapper.rs │ │ │ │ └── submittable.rs │ │ │ ├── node_editors │ │ │ │ ├── reflect_editor.rs │ │ │ │ ├── ragdoll_config.rs │ │ │ │ └── mod.rs │ │ │ ├── native_windows │ │ │ │ ├── scene_picker.rs │ │ │ │ ├── scene_preview_errors.rs │ │ │ │ ├── fsm_picker.rs │ │ │ │ ├── graph_picker.rs │ │ │ │ ├── event_sender.rs │ │ │ │ └── preview_hierarchy.rs │ │ │ ├── actions │ │ │ │ └── window.rs │ │ │ └── utils │ │ │ │ └── collapsing.rs │ │ ├── egui_fsm │ │ │ └── mod.rs │ │ ├── egui_nodes │ │ │ ├── mod.rs │ │ │ └── pin.rs │ │ ├── tooltips │ │ │ └── override_offset.txt │ │ ├── main.rs │ │ ├── icons.rs │ │ ├── icons │ │ │ ├── box.svg │ │ │ ├── bone.svg │ │ │ └── joint.svg │ │ └── scanner.rs │ └── Cargo.toml ├── bevy_animation_graph │ ├── .gitignore │ ├── src │ │ ├── utils │ │ │ └── mod.rs │ │ ├── core │ │ │ ├── duration_data.rs │ │ │ ├── state_machine │ │ │ │ ├── mod.rs │ │ │ │ └── high_level │ │ │ │ │ └── loader.rs │ │ │ ├── animation_graph │ │ │ │ └── mod.rs │ │ │ ├── skeleton │ │ │ │ ├── mod.rs │ │ │ │ └── serial.rs │ │ │ ├── errors │ │ │ │ ├── mod.rs │ │ │ │ ├── validation_error.rs │ │ │ │ ├── asset_loader_error.rs │ │ │ │ └── graph_error.rs │ │ │ ├── edge_data │ │ │ │ ├── mod.rs │ │ │ │ ├── bone_mask.rs │ │ │ │ └── events.rs │ │ │ ├── context │ │ │ │ ├── spec_context.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── system_resources.rs │ │ │ │ └── graph_context.rs │ │ │ ├── ragdoll │ │ │ │ ├── mod.rs │ │ │ │ ├── definition_loader.rs │ │ │ │ ├── relative_kinematic_body.rs │ │ │ │ ├── configuration.rs │ │ │ │ ├── write_pose.rs │ │ │ │ ├── bone_mapping.rs │ │ │ │ └── bone_mapping_loader.rs │ │ │ ├── mod.rs │ │ │ ├── id.rs │ │ │ └── animated_scene │ │ │ │ └── loader.rs │ │ ├── nodes │ │ │ ├── arithmetic │ │ │ │ ├── bool │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── const_bool.rs │ │ │ │ ├── event_queue │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── fire_event.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── quat │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── inverse.rs │ │ │ │ │ ├── from_euler.rs │ │ │ │ │ ├── into_euler.rs │ │ │ │ │ ├── mul.rs │ │ │ │ │ └── slerp.rs │ │ │ │ ├── vec3 │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── const_vec3.rs │ │ │ │ │ ├── length.rs │ │ │ │ │ ├── normalize.rs │ │ │ │ │ ├── rotation_arc.rs │ │ │ │ │ ├── into_f32.rs │ │ │ │ │ ├── from_f32.rs │ │ │ │ │ └── lerp.rs │ │ │ │ └── f32 │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── const_f32.rs │ │ │ │ │ ├── abs_f32.rs │ │ │ │ │ ├── add_f32.rs │ │ │ │ │ ├── div_f32.rs │ │ │ │ │ ├── sub_f32.rs │ │ │ │ │ ├── mul_f32.rs │ │ │ │ │ ├── clamp_f32.rs │ │ │ │ │ ├── select_f32.rs │ │ │ │ │ └── compare_f32.rs │ │ │ ├── space_conversion │ │ │ │ ├── mod.rs │ │ │ │ ├── extend_skeleton.rs │ │ │ │ ├── into_bone.rs │ │ │ │ ├── into_global.rs │ │ │ │ └── into_character.rs │ │ │ ├── dummy_node.rs │ │ │ ├── mod.rs │ │ │ ├── const_entity_path.rs │ │ │ ├── const_ragdoll_config.rs │ │ │ ├── fsm_node.rs │ │ │ └── speed_node.rs │ │ ├── interpolation │ │ │ └── mod.rs │ │ └── symmetry │ │ │ └── mod.rs │ ├── README.md │ └── Cargo.toml └── bevy_animation_graph_proc_macros │ └── Cargo.toml ├── assets ├── models │ ├── Fox.glb │ ├── character_rigged.glb │ ├── snake_with_anim.glb │ └── snake_without_anim.glb ├── skeletons │ ├── fox.skn.ron │ ├── human.skn.ron │ ├── snake.skn.ron │ └── snake_b.skn.ron ├── animations │ ├── fox_run.anim.ron │ ├── fox_walk.anim.ron │ ├── fox_survey.anim.ron │ ├── human_run.anim.ron │ ├── human_walk.anim.ron │ ├── human_wave.anim.ron │ ├── human_stand.anim.ron │ └── snake_wiggle.anim.ron ├── animated_scenes │ ├── snake_a.animscn.ron │ ├── human_wave.anim.ron │ ├── fox.animscn.ron │ ├── fsm.animscn.ron │ ├── human.animscn.ron │ ├── human_ik.animscn.ron │ ├── blend_space.animscn.ron │ ├── custom_node_example.animscn.ron │ ├── human_colliders.animscn.ron │ ├── human_ragdoll.animscn.ron │ └── snake_b.animscn.ron ├── fsm │ └── locomotion.fsm.ron ├── skeleton_colliders │ └── human.coll.ron └── animation_graphs │ ├── toplevel.animgraph.ron │ ├── custom_node_example.animgraph.ron │ ├── fox.animgraph.ron │ ├── snake.animgraph.ron │ ├── human_ik.animgraph.ron │ ├── walk_to_run.animgraph.ron │ └── human_ragdoll.animgraph.ron ├── locomotion_graph.png ├── .gitignore ├── examples ├── human │ └── Cargo.toml ├── human_fsm │ └── Cargo.toml ├── many_foxes │ ├── examples │ │ └── warning_string.txt │ └── Cargo.toml ├── retargeting │ ├── Cargo.toml │ └── examples │ │ └── retargeting.rs ├── human_ik │ └── Cargo.toml ├── editor_as_a_plugin │ ├── Cargo.toml │ └── README.md └── human_ragdoll │ └── Cargo.toml ├── CREDITS.md ├── Cargo.toml ├── LICENSE-MIT └── .github └── workflows ├── dry_run.yaml └── ci.yaml /crates/bevy_animation_graph_editor/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/events/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | test_dot.dot 3 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delaunay; 2 | pub mod geometry; 3 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/view_state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clip_preview; 2 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/duration_data.rs: -------------------------------------------------------------------------------- 1 | pub type DurationData = Option; 2 | -------------------------------------------------------------------------------- /assets/models/Fox.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrea-c/bevy_animation_graph/HEAD/assets/models/Fox.glb -------------------------------------------------------------------------------- /assets/skeletons/fox.skn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: Gltf(source: "models/Fox.glb", label: "Scene0"), 3 | ) 4 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/state_machine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod high_level; 2 | pub mod low_level; 3 | -------------------------------------------------------------------------------- /locomotion_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrea-c/bevy_animation_graph/HEAD/locomotion_graph.png -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/bool/mod.rs: -------------------------------------------------------------------------------- 1 | mod const_bool; 2 | 3 | pub use const_bool::*; 4 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ragdoll_editor; 2 | pub mod saving; 3 | -------------------------------------------------------------------------------- /assets/skeletons/human.skn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: Gltf(source: "models/character_rigged.glb", label: "Scene0"), 3 | ) 4 | -------------------------------------------------------------------------------- /assets/skeletons/snake.skn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: Gltf(source: "models/snake_with_anim.glb", label: "Scene0"), 3 | ) 4 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/event_queue/mod.rs: -------------------------------------------------------------------------------- 1 | mod fire_event; 2 | 3 | pub use fire_event::*; 4 | -------------------------------------------------------------------------------- /assets/skeletons/snake_b.skn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: Gltf(source: "models/snake_without_anim.glb", label: "Scene0"), 3 | ) 4 | -------------------------------------------------------------------------------- /assets/models/character_rigged.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrea-c/bevy_animation_graph/HEAD/assets/models/character_rigged.glb -------------------------------------------------------------------------------- /assets/models/snake_with_anim.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrea-c/bevy_animation_graph/HEAD/assets/models/snake_with_anim.glb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .direnv 3 | .envrc 4 | post-update.dot 5 | post-update.pdf 6 | fixed-post-update.dot 7 | fixed-post-update.pdf 8 | -------------------------------------------------------------------------------- /assets/models/snake_without_anim.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrea-c/bevy_animation_graph/HEAD/assets/models/snake_without_anim.glb -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/animation_graph/mod.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | pub mod loader; 3 | mod pin; 4 | pub mod serial; 5 | 6 | pub use core::*; 7 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/skeleton/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod loader; 2 | pub mod serial; 3 | #[allow(clippy::module_inception)] 4 | mod skeleton; 5 | 6 | pub use skeleton::*; 7 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/egui_fsm/mod.rs: -------------------------------------------------------------------------------- 1 | /// Adapted from https://github.com/theoparis/egui_nodes/ 2 | pub mod lib; 3 | pub mod link; 4 | pub mod node; 5 | pub mod style; 6 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/interpolation/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod linear; 2 | pub mod step; 3 | 4 | pub mod prelude { 5 | pub use super::linear::*; 6 | pub use super::step::*; 7 | } 8 | -------------------------------------------------------------------------------- /assets/animations/fox_run.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/Fox.glb", 4 | animation_name: "Run", 5 | ), 6 | skeleton: "skeletons/fox.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/README.md: -------------------------------------------------------------------------------- 1 | # Bevy Animation Graph 2 | 3 | This crate contains the library side of this project. This is what loads and 4 | runs the animation graphs in your game. 5 | -------------------------------------------------------------------------------- /assets/animations/fox_walk.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/Fox.glb", 4 | animation_name: "Walk", 5 | ), 6 | skeleton: "skeletons/fox.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /assets/animations/fox_survey.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/Fox.glb", 4 | animation_name: "Survey", 5 | ), 6 | skeleton: "skeletons/fox.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/egui_nodes/mod.rs: -------------------------------------------------------------------------------- 1 | /// Adapted from https://github.com/theoparis/egui_nodes/ 2 | pub mod lib; 3 | pub mod link; 4 | pub mod node; 5 | pub mod pin; 6 | pub mod style; 7 | -------------------------------------------------------------------------------- /assets/animated_scenes/snake_a.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/snake_with_anim.glb#Scene0", 3 | animation_graph: "animation_graphs/snake.animgraph.ron", 4 | skeleton: "skeletons/snake.skn.ron", 5 | ) 6 | -------------------------------------------------------------------------------- /assets/animations/human_run.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/character_rigged.glb", 4 | animation_name: "Run", 5 | ), 6 | skeleton: "skeletons/human.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /assets/animations/human_walk.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/character_rigged.glb", 4 | animation_name: "Walk", 5 | ), 6 | skeleton: "skeletons/human.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /assets/animations/human_wave.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/character_rigged.glb", 4 | animation_name: "Wave", 5 | ), 6 | skeleton: "skeletons/human.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /assets/animated_scenes/human_wave.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/character_rigged.glb", 4 | animation_name: "Wave", 5 | ), 6 | skeleton: "skeletons/human.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /assets/animations/human_stand.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/character_rigged.glb", 4 | animation_name: "Stand", 5 | ), 6 | skeleton: "skeletons/human.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /assets/animations/snake_wiggle.anim.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: GltfNamed( 3 | path: "models/snake_with_anim.glb", 4 | animation_name: "Wiggle", 5 | ), 6 | skeleton: "skeletons/snake.skn.ron", 7 | ) 8 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/errors/mod.rs: -------------------------------------------------------------------------------- 1 | mod asset_loader_error; 2 | mod graph_error; 3 | mod validation_error; 4 | 5 | pub use asset_loader_error::*; 6 | pub use graph_error::*; 7 | pub use validation_error::*; 8 | -------------------------------------------------------------------------------- /assets/animated_scenes/fox.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/Fox.glb#Scene0", 3 | path_to_player: ["root"], 4 | animation_graph: "animation_graphs/fox.animgraph.ron", 5 | skeleton: "skeletons/fox.skn.ron", 6 | ) 7 | -------------------------------------------------------------------------------- /examples/human/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "human" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph" } 9 | -------------------------------------------------------------------------------- /examples/human_fsm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "human_fsm" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph" } 9 | -------------------------------------------------------------------------------- /examples/many_foxes/examples/warning_string.txt: -------------------------------------------------------------------------------- 1 | This is a stress test used to push Bevy to its limit and debug performance issues. It is not representative of an actual game. It must be run in release mode using --release or it will be very slow. 2 | -------------------------------------------------------------------------------- /assets/animated_scenes/fsm.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/toplevel.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | ) 7 | -------------------------------------------------------------------------------- /assets/animated_scenes/human.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/human_new.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | ) 7 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/tooltips/override_offset.txt: -------------------------------------------------------------------------------- 1 | If not overriden, the mapping offset will be computed based on default body and bone positions. 2 | I recommended leaving this unchecked unless you have an issue with the default offset. 3 | -------------------------------------------------------------------------------- /examples/retargeting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "retargeting" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph" } 9 | -------------------------------------------------------------------------------- /assets/animated_scenes/human_ik.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/human_ik.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | ) 7 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/edge_data/mod.rs: -------------------------------------------------------------------------------- 1 | mod bone_mask; 2 | mod core; 3 | mod events; 4 | 5 | pub use bone_mask::BoneMask; 6 | pub use core::DataSpec; 7 | pub use core::DataValue; 8 | pub use core::OptDataSpec; 9 | pub use events::*; 10 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/mod.rs: -------------------------------------------------------------------------------- 1 | mod bool; 2 | mod event_queue; 3 | mod f32; 4 | mod quat; 5 | mod vec3; 6 | 7 | pub use bool::*; 8 | pub use event_queue::*; 9 | pub use f32::*; 10 | pub use quat::*; 11 | pub use vec3::*; 12 | -------------------------------------------------------------------------------- /assets/animated_scenes/blend_space.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/blend_space.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | ) 7 | -------------------------------------------------------------------------------- /assets/animated_scenes/custom_node_example.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/custom_node_example.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | ) 7 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/quat/mod.rs: -------------------------------------------------------------------------------- 1 | mod from_euler; 2 | mod into_euler; 3 | mod inverse; 4 | mod mul; 5 | mod slerp; 6 | 7 | pub use from_euler::*; 8 | pub use into_euler::*; 9 | pub use inverse::*; 10 | pub use mul::*; 11 | pub use slerp::*; 12 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/space_conversion/mod.rs: -------------------------------------------------------------------------------- 1 | mod extend_skeleton; 2 | mod into_bone; 3 | mod into_character; 4 | mod into_global; 5 | 6 | pub use extend_skeleton::*; 7 | pub use into_bone::*; 8 | pub use into_character::*; 9 | pub use into_global::*; 10 | -------------------------------------------------------------------------------- /examples/many_foxes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "many_foxes" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph" } 9 | argh = "0.1.12" 10 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/main.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_animation_graph_editor::AnimationGraphEditorPlugin; 3 | 4 | fn main() { 5 | let mut app = App::new(); 6 | 7 | app.add_plugins(AnimationGraphEditorPlugin); 8 | 9 | app.run(); 10 | } 11 | -------------------------------------------------------------------------------- /examples/human_ik/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "human_ik" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph" } 9 | bevy-inspector-egui = { version = "0.28" } 10 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/icons.rs: -------------------------------------------------------------------------------- 1 | use egui::{ImageSource, include_image}; 2 | 3 | pub const JOINT: ImageSource<'_> = include_image!("./icons/joint.svg"); 4 | pub const BOX: ImageSource<'_> = include_image!("./icons/box.svg"); 5 | pub const BONE: ImageSource<'_> = include_image!("./icons/bone.svg"); 6 | -------------------------------------------------------------------------------- /assets/animated_scenes/human_colliders.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/human_new.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | colliders: Some("skeleton_colliders/human.coll.ron") 7 | ) 8 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/context/spec_context.rs: -------------------------------------------------------------------------------- 1 | use crate::{core::state_machine::high_level::StateMachine, prelude::AnimationGraph}; 2 | use bevy::asset::Assets; 3 | 4 | #[derive(Clone, Copy)] 5 | pub struct SpecContext<'a> { 6 | pub graph_assets: &'a Assets, 7 | pub fsm_assets: &'a Assets, 8 | } 9 | -------------------------------------------------------------------------------- /assets/animated_scenes/human_ragdoll.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/character_rigged.glb#Scene0", 3 | path_to_player: ["metarig"], 4 | animation_graph: "animation_graphs/human_new.animgraph.ron", 5 | skeleton: "skeletons/human.skn.ron", 6 | ragdoll: Some("ragdolls/human.rag.ron"), 7 | ragdoll_bone_map: Some("ragdoll_bone_maps/human.bm.ron"), 8 | ) 9 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/mod.rs: -------------------------------------------------------------------------------- 1 | mod const_vec3; 2 | mod from_f32; 3 | mod into_f32; 4 | mod length; 5 | mod lerp; 6 | mod normalize; 7 | mod rotation_arc; 8 | 9 | pub use const_vec3::*; 10 | pub use from_f32::*; 11 | pub use into_f32::*; 12 | pub use length::*; 13 | pub use lerp::*; 14 | pub use normalize::*; 15 | pub use rotation_arc::*; 16 | -------------------------------------------------------------------------------- /examples/editor_as_a_plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editor_as_a_plugin" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph" } 9 | bevy_animation_graph_editor = { path = "../../crates/bevy_animation_graph_editor" } 10 | rand = "0.8" 11 | -------------------------------------------------------------------------------- /assets/animated_scenes/snake_b.animscn.ron: -------------------------------------------------------------------------------- 1 | ( 2 | source: "models/snake_without_anim.glb#Scene0", 3 | animation_graph: "animation_graphs/snake.animgraph.ron", 4 | retargeting: Some(( 5 | source_skeleton: "skeletons/snake_b.skn.ron", 6 | bone_path_overrides: { "cylinderbone1": "bone1" }, 7 | )), 8 | skeleton: "skeletons/snake.skn.ron", 9 | ) 10 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bone_mapping; 2 | pub mod bone_mapping_loader; 3 | pub mod configuration; 4 | pub mod definition; 5 | pub mod definition_loader; 6 | #[cfg(feature = "physics_avian")] 7 | pub mod read_pose_avian; 8 | #[cfg(feature = "physics_avian")] 9 | pub mod relative_kinematic_body; 10 | pub mod spawning; 11 | pub mod write_pose; 12 | -------------------------------------------------------------------------------- /examples/human_ragdoll/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "human_ragdoll" 3 | publish = false 4 | edition = { workspace = true } 5 | 6 | [dependencies] 7 | bevy = { workspace = true } 8 | bevy_animation_graph = { path = "../../crates/bevy_animation_graph", features = [ 9 | "physics_avian", 10 | ] } 11 | avian3d = { workspace = true } 12 | serde = { workspace = true } 13 | rand = "0.9.2" 14 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/skeleton/serial.rs: -------------------------------------------------------------------------------- 1 | use bevy::reflect::Reflect; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | pub struct SkeletonSerial { 6 | /// Path to animated scene source 7 | pub source: SkeletonSource, 8 | } 9 | 10 | #[derive(Clone, Reflect, Serialize, Deserialize)] 11 | pub enum SkeletonSource { 12 | Gltf { source: String, label: String }, 13 | } 14 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod angle_limit; 2 | pub mod animation_node; 3 | pub mod asset_picker; 4 | pub mod body_id; 5 | pub mod body_mode; 6 | pub mod bone_id; 7 | pub mod hashmap; 8 | pub mod isometry3d; 9 | pub mod list; 10 | pub mod option; 11 | pub mod picker; 12 | pub mod quat; 13 | pub mod ragdoll_config; 14 | pub mod tree; 15 | pub mod u32_flags; 16 | pub mod uuid; 17 | pub mod vec3; 18 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/errors/validation_error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::core::animation_graph::{SourcePin, TargetPin}; 4 | 5 | #[non_exhaustive] 6 | #[derive(Debug, Error)] 7 | pub enum GraphValidationError { 8 | #[error("{0:?} and {1:?} have different types but are connected.")] 9 | InconsistentPinTypes(SourcePin, TargetPin), 10 | #[error("Catchall error: {0}")] 11 | UnknownError(String), 12 | } 13 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/mod.rs: -------------------------------------------------------------------------------- 1 | mod abs_f32; 2 | mod add_f32; 3 | mod clamp_f32; 4 | mod compare_f32; 5 | mod const_f32; 6 | mod div_f32; 7 | mod mul_f32; 8 | mod select_f32; 9 | mod sub_f32; 10 | 11 | pub use abs_f32::*; 12 | pub use add_f32::*; 13 | pub use clamp_f32::*; 14 | pub use compare_f32::*; 15 | pub use const_f32::*; 16 | pub use div_f32::*; 17 | pub use mul_f32::*; 18 | pub use select_f32::*; 19 | pub use sub_f32::*; 20 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/dummy_node.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 2 | use bevy::prelude::*; 3 | 4 | #[derive(Reflect, Clone, Debug, Default)] 5 | #[reflect(Default, NodeLike)] 6 | pub struct DummyNode; 7 | 8 | impl DummyNode { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | } 13 | 14 | impl NodeLike for DummyNode { 15 | fn display_name(&self) -> String { 16 | "Dummy".into() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | ## Assets 2 | 3 | - glTF animated fox from [glTF Sample Models][fox] 4 | - Low poly fox [by PixelMannen] (CC0 1.0 Universal) 5 | - Rigging and animation [by @tomkranis on Sketchfab] ([CC-BY 4.0]) 6 | 7 | [fox]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox 8 | [by PixelMannen]: https://opengameart.org/content/fox-and-shiba 9 | [by @tomkranis on Sketchfab]: https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc 10 | [CC-BY 4.0]: https://creativecommons.org/licenses/by/4.0/ 11 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actions; 2 | pub mod core; 3 | pub mod ecs_utils; 4 | pub mod editor_windows; 5 | pub mod egui_inspector_impls; 6 | pub mod events; 7 | pub mod generic_widgets; 8 | pub mod global_state; 9 | pub mod native_views; 10 | pub mod native_windows; 11 | pub mod node_editors; 12 | pub mod reflect_widgets; 13 | pub mod scenes; 14 | pub mod utils; 15 | pub mod view_state; 16 | pub mod windows; 17 | 18 | pub use core::{UiState, setup_ui, show_ui_system}; 19 | pub use scenes::*; 20 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_proc_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_animation_graph_proc_macros" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | license = { workspace = true } 6 | readme = { workspace = true } 7 | repository = { workspace = true } 8 | description = "Procedural macros for the bevy animation graph crate" 9 | keywords = ["bevy", "animation", "gamedev", "editor", "graph"] 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | quote = "1.0" 16 | syn = { version = "2.0", features = ["full"] } 17 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/ecs_utils.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::{component::Component, entity::Entity, query::With, world::World}; 2 | 3 | use crate::ui::native_views::EditorViewState; 4 | 5 | pub fn get_entity_state(world: &World, entity: Entity) -> Option<&T> { 6 | let mut query_state = world.try_query_filtered::<&T, With>()?; 7 | query_state.get(world, entity).ok() 8 | } 9 | 10 | pub fn get_view_state(world: &World, view: Entity) -> Option<&T> { 11 | get_entity_state::(world, view) 12 | } 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*", "examples/*"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.8.0" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | edition = "2024" 10 | repository = "https://github.com/mbrea-c/bevy_animation_graph" 11 | 12 | [workspace.dependencies] 13 | bevy_animation_graph_proc_macros = { version = "0.8.0", path = "./crates/bevy_animation_graph_proc_macros" } 14 | bevy_animation_graph = { version = "0.8.0", path = "./crates/bevy_animation_graph" } 15 | 16 | bevy = { version = "0.17" } 17 | avian3d = { version = "0.4" } 18 | ron = "0.10.1" 19 | serde = { version = "1.0" } 20 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/picker.rs: -------------------------------------------------------------------------------- 1 | pub struct PickerWidget { 2 | pub id_hash: egui::Id, 3 | } 4 | 5 | impl PickerWidget { 6 | pub fn new_salted(salt: impl std::hash::Hash) -> Self { 7 | Self { 8 | id_hash: egui::Id::new(salt), 9 | } 10 | } 11 | } 12 | 13 | impl PickerWidget { 14 | pub fn ui( 15 | self, 16 | ui: &mut egui::Ui, 17 | selected_text: impl Into, 18 | show: impl FnOnce(&mut egui::Ui) -> R, 19 | ) -> egui::InnerResponse> { 20 | ui.push_id(self.id_hash, |ui| { 21 | ui.menu_button(selected_text, |ui| show(ui)) 22 | }) 23 | .inner 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/context/mod.rs: -------------------------------------------------------------------------------- 1 | mod deferred_gizmos; 2 | pub mod graph_context; 3 | mod graph_context_arena; 4 | pub mod node_caches; 5 | pub mod node_state_box; 6 | pub mod node_states; 7 | mod pass_context; 8 | mod pose_fallback; 9 | mod spec_context; 10 | mod system_resources; 11 | 12 | pub use deferred_gizmos::{ 13 | CustomRelativeDrawCommand, CustomRelativeDrawCommandReference, DeferredGizmos, 14 | DeferredGizmosContext, 15 | }; 16 | pub use graph_context_arena::{GraphContextArena, GraphContextId}; 17 | pub use pass_context::{FsmContext, PassContext, StateRole, StateStack}; 18 | pub use pose_fallback::{PoseFallbackContext, RootOffsetResult}; 19 | pub use spec_context::SpecContext; 20 | pub use system_resources::SystemResources; 21 | -------------------------------------------------------------------------------- /examples/editor_as_a_plugin/README.md: -------------------------------------------------------------------------------- 1 | This example shows how to create your own editor binary using the provided 2 | `AnimationGraphEditorPlugin`. 3 | 4 | While the `bevy_animation_graph_editor` crate already bundles an editor binary, there 5 | are cases where it may be preferable to create your own binary that uses the 6 | editor as a plugin. For example: 7 | * Your crate defines custom `AnimationNode`s, which you would like to register 8 | so that they are available in the editor. 9 | * Your crate depends on a git version or fork of the animation graph workspace, and 10 | you would like to use that particular version of the editor without having to 11 | install it globally. 12 | 13 | For demonstration purposes, we define a custom animation node in this crate, 14 | which will appear in the editor. 15 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_animation_graph" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | license = { workspace = true } 6 | readme = { workspace = true } 7 | repository = { workspace = true } 8 | description = "Animation graph library for the Bevy game engine" 9 | keywords = ["bevy", "animation", "gamedev"] 10 | 11 | [dependencies] 12 | bevy = { workspace = true } 13 | thiserror = "1.0.58" 14 | ron = { workspace = true } 15 | serde = { workspace = true, features = ["derive", "rc"] } 16 | indexmap = { version = "2.2.1", features = ["serde"] } 17 | regex = "1.10.3" 18 | uuid = "1.0" 19 | rmp-serde = "1.3.0" 20 | bevy_animation_graph_proc_macros = { workspace = true } 21 | avian3d = { workspace = true, optional = true } 22 | 23 | [features] 24 | default = [] 25 | physics_avian = ["dep:avian3d"] 26 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/icons/box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/definition_loader.rs: -------------------------------------------------------------------------------- 1 | use bevy::asset::{AssetLoader, LoadContext, io::Reader}; 2 | 3 | use crate::core::{errors::AssetLoaderError, ragdoll::definition::Ragdoll}; 4 | 5 | #[derive(Default)] 6 | pub struct RagdollLoader; 7 | 8 | impl AssetLoader for RagdollLoader { 9 | type Asset = Ragdoll; 10 | type Settings = (); 11 | type Error = AssetLoaderError; 12 | 13 | async fn load( 14 | &self, 15 | reader: &mut dyn Reader, 16 | _settings: &Self::Settings, 17 | _load_context: &mut LoadContext<'_>, 18 | ) -> Result { 19 | let mut bytes = vec![]; 20 | reader.read_to_end(&mut bytes).await?; 21 | let ragdoll: Ragdoll = ron::de::from_bytes(&bytes)?; 22 | 23 | Ok(ragdoll) 24 | } 25 | 26 | fn extensions(&self) -> &[&str] { 27 | &["rag.ron"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arithmetic; 2 | pub mod blend_node; 3 | pub mod blend_space_node; 4 | pub mod chain_node; 5 | pub mod clip_node; 6 | pub mod const_entity_path; 7 | pub mod const_ragdoll_config; 8 | pub mod dummy_node; 9 | pub mod event_markup_node; 10 | pub mod flip_lr_node; 11 | pub mod fsm_node; 12 | pub mod graph_node; 13 | pub mod loop_node; 14 | pub mod padding; 15 | pub mod rotation_node; 16 | pub mod speed_node; 17 | pub mod twoboneik_node; 18 | // pub mod space_conversion; 19 | // 20 | pub use arithmetic::*; 21 | pub use blend_node::*; 22 | pub use chain_node::*; 23 | pub use clip_node::*; 24 | pub use dummy_node::*; 25 | pub use event_markup_node::*; 26 | pub use flip_lr_node::*; 27 | pub use graph_node::*; 28 | pub use loop_node::*; 29 | pub use rotation_node::*; 30 | // pub use space_conversion::*; 31 | pub use const_entity_path::*; 32 | pub use fsm_node::*; 33 | pub use padding::*; 34 | pub use speed_node::*; 35 | pub use twoboneik_node::*; 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/icons/bone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod animated_scene; 2 | pub mod animation_clip; 3 | pub mod animation_graph; 4 | pub mod animation_graph_player; 5 | pub mod animation_node; 6 | pub mod context; 7 | pub mod duration_data; 8 | pub mod edge_data; 9 | pub mod errors; 10 | pub mod event_track; 11 | pub mod id; 12 | #[cfg(feature = "physics_avian")] 13 | pub mod physics_systems_avian; 14 | pub mod pin_map; 15 | pub mod plugin; 16 | pub mod pose; 17 | pub mod ragdoll; 18 | pub mod skeleton; 19 | pub mod space_conversion; 20 | pub mod state_machine; 21 | pub mod systems; 22 | 23 | pub mod prelude { 24 | use super::*; 25 | pub use animated_scene::*; 26 | pub use animation_clip::GraphClip; 27 | pub use animation_graph::AnimationGraph; 28 | pub use animation_graph_player::*; 29 | pub use animation_node::*; 30 | pub use context::*; 31 | pub use edge_data::DataSpec; 32 | pub use edge_data::DataValue; 33 | pub use edge_data::OptDataSpec; 34 | pub use plugin::*; 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_graph.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::prelude::AnimationGraph; 6 | 7 | use crate::ui::global_state::{ 8 | RegisterStateComponent, SetOrInsertEvent, observe_set_or_insert_event, 9 | }; 10 | 11 | #[derive(Debug, Component, Default, Clone)] 12 | pub struct ActiveGraph { 13 | pub handle: Handle, 14 | } 15 | 16 | impl RegisterStateComponent for ActiveGraph { 17 | fn register(world: &mut World, _global_state_entity: Entity) { 18 | world.add_observer(observe_set_or_insert_event::); 19 | } 20 | } 21 | 22 | #[derive(Event)] 23 | pub struct SetActiveGraph { 24 | pub new: ActiveGraph, 25 | } 26 | 27 | impl SetOrInsertEvent for SetActiveGraph { 28 | type Target = ActiveGraph; 29 | 30 | fn get_component(&self) -> Self::Target { 31 | self.new.clone() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/const_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct ConstF32 { 11 | pub constant: f32, 12 | } 13 | 14 | impl ConstF32 { 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new(constant: f32) -> Self { 18 | Self { constant } 19 | } 20 | } 21 | 22 | impl NodeLike for ConstF32 { 23 | fn display_name(&self) -> String { 24 | "F32".into() 25 | } 26 | 27 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 28 | ctx.set_data_fwd(Self::OUTPUT, self.constant); 29 | Ok(()) 30 | } 31 | 32 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 33 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/bool/const_bool.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct ConstBool { 11 | pub constant: bool, 12 | } 13 | 14 | impl ConstBool { 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new(constant: bool) -> Self { 18 | Self { constant } 19 | } 20 | } 21 | 22 | impl NodeLike for ConstBool { 23 | fn display_name(&self) -> String { 24 | "Bool".into() 25 | } 26 | 27 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 28 | ctx.set_data_fwd(Self::OUTPUT, self.constant); 29 | Ok(()) 30 | } 31 | 32 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 33 | [(Self::OUTPUT.into(), DataSpec::Bool)].into() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/const_vec3.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct ConstVec3Node { 11 | pub constant: Vec3, 12 | } 13 | 14 | impl ConstVec3Node { 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new(constant: Vec3) -> Self { 18 | Self { constant } 19 | } 20 | } 21 | 22 | impl NodeLike for ConstVec3Node { 23 | fn display_name(&self) -> String { 24 | "Vec3".into() 25 | } 26 | 27 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 28 | ctx.set_data_fwd(Self::OUTPUT, self.constant); 29 | Ok(()) 30 | } 31 | 32 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 33 | [(Self::OUTPUT.into(), DataSpec::Vec3)].into() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_fsm_state.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::core::state_machine::high_level::{StateId, StateMachine}; 6 | 7 | use crate::ui::global_state::{ 8 | RegisterStateComponent, SetOrInsertEvent, observe_set_or_insert_event, 9 | }; 10 | 11 | #[derive(Debug, Component, Default, Clone)] 12 | pub struct ActiveFsmState { 13 | pub handle: Handle, 14 | pub state: StateId, 15 | } 16 | 17 | impl RegisterStateComponent for ActiveFsmState { 18 | fn register(world: &mut World, _global_state_entity: Entity) { 19 | world.add_observer(observe_set_or_insert_event::); 20 | } 21 | } 22 | 23 | #[derive(Event)] 24 | pub struct SetActiveFsmState { 25 | pub new: ActiveFsmState, 26 | } 27 | 28 | impl SetOrInsertEvent for SetActiveFsmState { 29 | type Target = ActiveFsmState; 30 | 31 | fn get_component(&self) -> Self::Target { 32 | self.new.clone() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_fsm.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::core::state_machine::high_level::StateMachine; 6 | 7 | use crate::ui::global_state::{ 8 | RegisterStateComponent, SetOrInsertEvent, observe_clear_global_state, 9 | observe_set_or_insert_event, 10 | }; 11 | 12 | #[derive(Debug, Component, Default, Clone)] 13 | pub struct ActiveFsm { 14 | pub handle: Handle, 15 | } 16 | 17 | impl RegisterStateComponent for ActiveFsm { 18 | fn register(world: &mut World, _global_state_entity: Entity) { 19 | world.add_observer(observe_set_or_insert_event::); 20 | world.add_observer(observe_clear_global_state::); 21 | } 22 | } 23 | 24 | #[derive(Event)] 25 | pub struct SetActiveFsm { 26 | pub new: ActiveFsm, 27 | } 28 | 29 | impl SetOrInsertEvent for SetActiveFsm { 30 | type Target = ActiveFsm; 31 | 32 | fn get_component(&self) -> Self::Target { 33 | self.new.clone() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/edge_data/bone_mask.rs: -------------------------------------------------------------------------------- 1 | use crate::core::pose::BoneId; 2 | use bevy::{ 3 | platform::collections::HashMap, 4 | reflect::{Reflect, std_traits::ReflectDefault}, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Reflect, Clone, Debug, Serialize, Deserialize)] 9 | #[reflect(Default)] 10 | pub enum BoneMask { 11 | /// If a bone is in the bones map, weight is given. Otherwise, weight is zero 12 | Positive { bones: HashMap }, 13 | /// If a bone is not in bones map, weight is 1. Otherwise, weight is as given 14 | Negative { bones: HashMap }, 15 | } 16 | 17 | impl Default for BoneMask { 18 | fn default() -> Self { 19 | Self::Positive { 20 | bones: Default::default(), 21 | } 22 | } 23 | } 24 | 25 | impl BoneMask { 26 | pub fn bone_weight(&self, bone_id: &BoneId) -> f32 { 27 | match self { 28 | BoneMask::Positive { bones } => bones.get(bone_id).copied().unwrap_or(0.), 29 | BoneMask::Negative { bones } => bones.get(bone_id).copied().unwrap_or(1.), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_ragdoll.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::core::ragdoll::definition::Ragdoll; 6 | 7 | use crate::ui::global_state::{ 8 | RegisterStateComponent, SetOrInsertEvent, observe_clear_global_state, 9 | observe_set_or_insert_event, 10 | }; 11 | 12 | #[derive(Debug, Component, Default, Clone)] 13 | pub struct ActiveRagdoll { 14 | pub handle: Handle, 15 | } 16 | 17 | impl RegisterStateComponent for ActiveRagdoll { 18 | fn register(world: &mut World, _global_state_entity: Entity) { 19 | world.add_observer(observe_set_or_insert_event::); 20 | world.add_observer(observe_clear_global_state::); 21 | } 22 | } 23 | 24 | #[derive(Event)] 25 | pub struct SetActiveRagdoll { 26 | pub new: ActiveRagdoll, 27 | } 28 | 29 | impl SetOrInsertEvent for SetActiveRagdoll { 30 | type Target = ActiveRagdoll; 31 | 32 | fn get_component(&self) -> Self::Target { 33 | self.new.clone() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_skeleton.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::core::skeleton::Skeleton; 6 | 7 | use crate::ui::global_state::{ 8 | RegisterStateComponent, SetOrInsertEvent, observe_clear_global_state, 9 | observe_set_or_insert_event, 10 | }; 11 | 12 | #[derive(Debug, Component, Default, Clone)] 13 | pub struct ActiveSkeleton { 14 | pub handle: Handle, 15 | } 16 | 17 | impl RegisterStateComponent for ActiveSkeleton { 18 | fn register(world: &mut World, _global_state_entity: Entity) { 19 | world.add_observer(observe_set_or_insert_event::); 20 | world.add_observer(observe_clear_global_state::); 21 | } 22 | } 23 | 24 | #[derive(Event)] 25 | pub struct SetActiveSkeleton { 26 | pub new: ActiveSkeleton, 27 | } 28 | 29 | impl SetOrInsertEvent for SetActiveSkeleton { 30 | type Target = ActiveSkeleton; 31 | 32 | fn get_component(&self) -> Self::Target { 33 | self.new.clone() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Manuel Brea Carreras 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/reflect_widgets/checkbox.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use bevy_inspector_egui::reflect_inspector::InspectorUi; 4 | use egui_dock::egui; 5 | 6 | use super::{EguiInspectorExtension, MakeBuffer}; 7 | 8 | #[derive(Default)] 9 | pub struct CheckboxInspector; 10 | 11 | impl EguiInspectorExtension for CheckboxInspector { 12 | type Base = bool; 13 | type Buffer = (); 14 | 15 | fn mutable( 16 | value: &mut Self::Base, 17 | _buffer: &mut Self::Buffer, 18 | ui: &mut egui::Ui, 19 | _options: &dyn Any, 20 | _id: egui::Id, 21 | _env: InspectorUi<'_, '_>, 22 | ) -> bool { 23 | ui.checkbox(value, "").changed() 24 | } 25 | 26 | fn readonly( 27 | value: &Self::Base, 28 | _buffer: &Self::Buffer, 29 | ui: &mut egui::Ui, 30 | _options: &dyn Any, 31 | _id: egui::Id, 32 | _env: InspectorUi<'_, '_>, 33 | ) { 34 | let mut val = *value; 35 | ui.add_enabled_ui(false, |ui| ui.checkbox(&mut val, "")); 36 | } 37 | } 38 | 39 | impl MakeBuffer<()> for bool { 40 | fn make_buffer(&self) {} 41 | } 42 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/const_entity_path.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_clip::EntityPath; 2 | use crate::core::animation_graph::PinMap; 3 | use crate::core::animation_node::NodeLike; 4 | use crate::core::errors::GraphError; 5 | use crate::core::prelude::DataSpec; 6 | use crate::prelude::{DataValue, PassContext, SpecContext}; 7 | use bevy::prelude::*; 8 | 9 | #[derive(Reflect, Clone, Debug, Default)] 10 | #[reflect(Default)] 11 | pub struct ConstEntityPath { 12 | pub path: EntityPath, 13 | } 14 | 15 | impl ConstEntityPath { 16 | pub const OUTPUT: &'static str = "out"; 17 | 18 | pub fn new(path: EntityPath) -> Self { 19 | Self { path } 20 | } 21 | } 22 | 23 | impl NodeLike for ConstEntityPath { 24 | fn display_name(&self) -> String { 25 | "Entity Path".into() 26 | } 27 | 28 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 29 | ctx.set_data_fwd(Self::OUTPUT, DataValue::EntityPath(self.path.clone())); 30 | Ok(()) 31 | } 32 | 33 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 34 | [(Self::OUTPUT.into(), DataSpec::EntityPath)].into() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_graph_node.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::{ 6 | core::animation_graph::{NodeId, PinId}, 7 | prelude::AnimationGraph, 8 | }; 9 | 10 | use crate::ui::global_state::{ 11 | RegisterStateComponent, SetOrInsertEvent, observe_set_or_insert_event, 12 | }; 13 | 14 | #[derive(Debug, Component, Default, Clone)] 15 | pub struct ActiveGraphNode { 16 | pub handle: Handle, 17 | pub node: NodeId, 18 | pub selected_pin: Option, 19 | } 20 | 21 | impl RegisterStateComponent for ActiveGraphNode { 22 | fn register(world: &mut World, _global_state_entity: Entity) { 23 | world.add_observer(observe_set_or_insert_event::); 24 | } 25 | } 26 | 27 | #[derive(Event)] 28 | pub struct SetActiveGraphNode { 29 | pub new: ActiveGraphNode, 30 | } 31 | 32 | impl SetOrInsertEvent for SetActiveGraphNode { 33 | type Target = ActiveGraphNode; 34 | 35 | fn get_component(&self) -> Self::Target { 36 | self.new.clone() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_animation_graph_editor" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | license = { workspace = true } 6 | readme = { workspace = true } 7 | repository = { workspace = true } 8 | description = "Animation graph editor for the Bevy game engine" 9 | keywords = ["bevy", "animation", "gamedev", "editor", "graph"] 10 | 11 | [dependencies] 12 | bevy = { workspace = true, features = ["file_watcher", "wayland"] } 13 | ron = { workspace = true } 14 | derivative = "2.2.0" 15 | bevy_animation_graph = { workspace = true } 16 | clap = { version = "4.4.18", features = ["derive"] } 17 | serde = { workspace = true } 18 | 19 | egui_dock = "0.17" 20 | egui-notify = { version = "0.20" } 21 | egui = { version = "0.32" } 22 | egui_extras = { version = "0.32", features = ["all_loaders"] } 23 | bevy_egui = { version = "0.37" } 24 | bevy-inspector-egui = { version = "0.34" } 25 | avian3d = { workspace = true, optional = true } 26 | 27 | rand = "0.9.0" 28 | uuid = { version = "1.16.0", features = ["v4"] } 29 | 30 | [features] 31 | default = ["physics_avian"] 32 | physics_avian = ["dep:avian3d", "bevy_animation_graph/physics_avian"] 33 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_fsm_transition.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::Handle, 3 | ecs::{component::Component, entity::Entity, event::Event, world::World}, 4 | }; 5 | use bevy_animation_graph::core::state_machine::high_level::{StateMachine, TransitionId}; 6 | 7 | use crate::ui::global_state::{ 8 | RegisterStateComponent, SetOrInsertEvent, observe_set_or_insert_event, 9 | }; 10 | 11 | #[derive(Debug, Component, Default, Clone)] 12 | pub struct ActiveFsmTransition { 13 | pub handle: Handle, 14 | pub transition: TransitionId, 15 | } 16 | 17 | impl RegisterStateComponent for ActiveFsmTransition { 18 | fn register(world: &mut World, _global_state_entity: Entity) { 19 | world.add_observer( 20 | observe_set_or_insert_event::, 21 | ); 22 | } 23 | } 24 | 25 | #[derive(Event)] 26 | pub struct SetActiveFsmTransition { 27 | pub new: ActiveFsmTransition, 28 | } 29 | 30 | impl SetOrInsertEvent for SetActiveFsmTransition { 31 | type Target = ActiveFsmTransition; 32 | 33 | fn get_component(&self) -> Self::Target { 34 | self.new.clone() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/ragdoll_editor/settings_panel.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{Assets, Handle}, 3 | ecs::world::World, 4 | }; 5 | use bevy_animation_graph::core::ragdoll::definition::Ragdoll; 6 | use egui::Widget; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct RagdollEditorSettings { 10 | show_all_colliders: bool, 11 | } 12 | 13 | pub struct SettingsPanel<'a> { 14 | pub target: Handle, 15 | pub world: &'a mut World, 16 | pub settings: &'a mut RagdollEditorSettings, 17 | } 18 | 19 | impl Widget for SettingsPanel<'_> { 20 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 21 | let mut response = ui.heading("Ragdoll settings"); 22 | self.world 23 | .resource_scope::, _>(|_world, ragdoll_assets| { 24 | let Some(_ragdoll) = ragdoll_assets.get(&self.target) else { 25 | return; 26 | }; 27 | }); 28 | response |= ui.separator(); 29 | 30 | response |= ui.heading("Preview settings"); 31 | response |= ui.checkbox(&mut self.settings.show_all_colliders, "Show all colliders"); 32 | 33 | response 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/const_ragdoll_config.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::core::ragdoll::configuration::RagdollConfig; 6 | use crate::prelude::{DataValue, PassContext, SpecContext}; 7 | use bevy::prelude::*; 8 | 9 | #[derive(Reflect, Clone, Debug, Default)] 10 | #[reflect(Default, NodeLike)] 11 | pub struct ConstRagdollConfig { 12 | pub value: RagdollConfig, 13 | } 14 | 15 | impl ConstRagdollConfig { 16 | pub const OUTPUT: &'static str = "out"; 17 | 18 | pub fn new(value: RagdollConfig) -> Self { 19 | Self { value } 20 | } 21 | } 22 | 23 | impl NodeLike for ConstRagdollConfig { 24 | fn display_name(&self) -> String { 25 | "Ragdoll Config".into() 26 | } 27 | 28 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 29 | ctx.set_data_fwd(Self::OUTPUT, DataValue::RagdollConfig(self.value.clone())); 30 | Ok(()) 31 | } 32 | 33 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 34 | [(Self::OUTPUT.into(), DataSpec::RagdollConfig)].into() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/fsm/locomotion.fsm.ron: -------------------------------------------------------------------------------- 1 | ( 2 | states: [ 3 | ( 4 | id: "walk", 5 | graph: "animation_graphs/walk.animgraph.ron", 6 | global_transition: Some(( 7 | duration: 1.0, 8 | graph: "animation_graphs/walk_to_run.animgraph.ron", 9 | )), 10 | ), 11 | ( 12 | id: "run", 13 | graph: "animation_graphs/run.animgraph.ron", 14 | global_transition: None, 15 | ), 16 | ], 17 | transitions: [ 18 | ( 19 | id: Direct("slow_down"), 20 | source: "run", 21 | target: "walk", 22 | duration: 1.0, 23 | graph: "animation_graphs/walk_to_run.animgraph.ron", 24 | ), 25 | ( 26 | id: Direct("speed_up"), 27 | source: "walk", 28 | target: "run", 29 | duration: 1.0, 30 | graph: "animation_graphs/walk_to_run.animgraph.ron", 31 | ), 32 | ], 33 | start_state: "run", 34 | input_data: { 35 | "speed": F32(1.0), 36 | }, 37 | extra: ( 38 | states: { 39 | "walk": (334.10522, 309.52664), 40 | "run": (552.15, 310.6519), 41 | }, 42 | ), 43 | ) 44 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/relative_kinematic_body.rs: -------------------------------------------------------------------------------- 1 | use avian3d::prelude::RigidBody; 2 | use bevy::{ 3 | ecs::{component::Component, entity::Entity}, 4 | math::{Isometry3d, Vec3}, 5 | reflect::Reflect, 6 | }; 7 | 8 | /// A physics-simulated body that has two components of motion: 9 | /// * A component that is relative to a dynamic body 10 | /// * A component that is kinematically-driven 11 | #[derive(Component, Default, Debug, Clone, Reflect)] 12 | #[require(RigidBody = RigidBody::Kinematic)] 13 | pub struct RelativeKinematicBody { 14 | /// This entity should have `LinearVelocity` and `AngularVelocity` 15 | pub relative_to: Option, 16 | pub kinematic_linear_velocity: Vec3, 17 | pub kinematic_angular_velocity: Vec3, 18 | } 19 | 20 | /// A physics-simulated body that has two components of motion: 21 | /// * A component that is relative to a dynamic body 22 | /// * A component that is kinematically-driven 23 | #[derive(Component, Default, Debug, Clone, Reflect)] 24 | #[require(RelativeKinematicBody)] 25 | pub struct RelativeKinematicBodyPositionBased { 26 | /// This entity should have `LinearVelocity` and `AngularVelocity` 27 | pub relative_to: Option, 28 | pub relative_target: Isometry3d, 29 | } 30 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/inspector_selection.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::{component::Component, entity::Entity, event::Event, world::World}; 2 | 3 | use crate::ui::global_state::{ 4 | RegisterStateComponent, SetOrInsertEvent, observe_set_or_insert_event, 5 | }; 6 | 7 | #[derive(Debug, Component, Default, Clone, Hash)] 8 | pub enum InspectorSelection { 9 | ActiveFsm, 10 | ActiveFsmTransition, 11 | ActiveFsmState, 12 | 13 | ActiveGraph, 14 | ActiveNode, 15 | 16 | #[default] 17 | Nothing, 18 | } 19 | 20 | impl RegisterStateComponent for InspectorSelection { 21 | fn register(world: &mut World, global_state_entity: Entity) { 22 | world 23 | .entity_mut(global_state_entity) 24 | .insert(InspectorSelection::default()); 25 | 26 | world 27 | .add_observer(observe_set_or_insert_event::); 28 | } 29 | } 30 | 31 | #[derive(Event)] 32 | pub struct SetInspectorSelection { 33 | pub selection: InspectorSelection, 34 | } 35 | 36 | impl SetOrInsertEvent for SetInspectorSelection { 37 | type Target = InspectorSelection; 38 | 39 | fn get_component(&self) -> Self::Target { 40 | self.selection.clone() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/node_editors/reflect_editor.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::world::World; 2 | use bevy_animation_graph::prelude::{NodeLike, ReflectEditProxy}; 3 | 4 | use crate::ui::{node_editors::DynNodeEditor, utils::using_inspector_env}; 5 | 6 | #[derive(Default)] 7 | pub struct ReflectNodeEditor; 8 | 9 | impl DynNodeEditor for ReflectNodeEditor { 10 | fn show_dyn( 11 | &self, 12 | ui: &mut egui::Ui, 13 | world: &mut World, 14 | node: &mut dyn NodeLike, 15 | ) -> egui::Response { 16 | let mut response = ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()); 17 | let type_id = node.as_any().type_id(); 18 | let changed = using_inspector_env(world, |mut env| { 19 | if let Some(edit_proxy) = env.type_registry.get_type_data::(type_id) { 20 | let mut proxy = (edit_proxy.to_proxy)(node); 21 | env.ui_for_reflect_with_options(proxy.as_partial_reflect_mut(), ui, ui.id(), &()) 22 | } else { 23 | env.ui_for_reflect(node.as_partial_reflect_mut(), ui) 24 | } 25 | }); 26 | 27 | if changed { 28 | response.mark_changed(); 29 | } 30 | 31 | response 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/body_mode.rs: -------------------------------------------------------------------------------- 1 | use bevy_animation_graph::core::ragdoll::definition::BodyMode; 2 | 3 | use crate::ui::generic_widgets::picker::PickerWidget; 4 | 5 | pub struct BodyModeWidget<'a> { 6 | pub body_mode: &'a mut BodyMode, 7 | pub id_hash: egui::Id, 8 | } 9 | 10 | impl<'a> BodyModeWidget<'a> { 11 | pub fn new_salted(body_mode: &'a mut BodyMode, salt: impl std::hash::Hash) -> Self { 12 | Self { 13 | body_mode, 14 | id_hash: egui::Id::new(salt), 15 | } 16 | } 17 | } 18 | 19 | impl<'a> egui::Widget for BodyModeWidget<'a> { 20 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 21 | ui.push_id(self.id_hash, |ui| { 22 | let labeler = |b| format!("{:?}", b); 23 | PickerWidget::new_salted("body mode picker") 24 | .ui(ui, labeler(*self.body_mode), |ui| { 25 | let mut option = |mode| { 26 | ui.selectable_value(self.body_mode, mode, labeler(mode)); 27 | }; 28 | 29 | option(BodyMode::Kinematic); 30 | option(BodyMode::Dynamic); 31 | }) 32 | .response 33 | }) 34 | .inner 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/id.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | animation::AnimationTargetId, 3 | reflect::{Reflect, std_traits::ReflectDefault}, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use std::hash::{Hash, Hasher}; 7 | use uuid::Uuid; 8 | 9 | use super::animation_clip::EntityPath; 10 | 11 | #[derive( 12 | Reflect, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Debug, 13 | )] 14 | #[reflect(Default)] 15 | pub struct BoneId { 16 | id: Uuid, 17 | } 18 | 19 | impl Hash for BoneId { 20 | fn hash(&self, state: &mut H) { 21 | let (hi, lo) = self.id.as_u64_pair(); 22 | state.write_u64(hi ^ lo); 23 | } 24 | } 25 | 26 | impl BoneId { 27 | pub fn from_uuid(uuid: Uuid) -> Self { 28 | Self { id: uuid } 29 | } 30 | 31 | pub fn id(&self) -> Uuid { 32 | self.id 33 | } 34 | 35 | pub fn animation_target_id(&self) -> AnimationTargetId { 36 | AnimationTargetId(self.id) 37 | } 38 | } 39 | 40 | impl From for BoneId { 41 | fn from(value: EntityPath) -> Self { 42 | value.id() 43 | } 44 | } 45 | 46 | impl From for BoneId { 47 | fn from(value: AnimationTargetId) -> Self { 48 | BoneId { id: value.0 } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/abs_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct AbsF32; 11 | 12 | impl AbsF32 { 13 | pub const INPUT: &'static str = "in"; 14 | pub const OUTPUT: &'static str = "out"; 15 | 16 | pub fn new() -> Self { 17 | Self 18 | } 19 | } 20 | 21 | impl NodeLike for AbsF32 { 22 | fn display_name(&self) -> String { 23 | "|_| Absolute val".into() 24 | } 25 | 26 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 27 | let input = ctx.data_back(Self::INPUT)?.as_f32()?; 28 | ctx.set_data_fwd(Self::OUTPUT, input.abs()); 29 | Ok(()) 30 | } 31 | 32 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 33 | [(Self::INPUT.into(), DataSpec::F32)].into() 34 | } 35 | 36 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 37 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/configuration.rs: -------------------------------------------------------------------------------- 1 | use bevy::{platform::collections::HashMap, reflect::Reflect}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::core::{ 5 | id::BoneId, 6 | ragdoll::definition::{BodyId, BodyMode}, 7 | }; 8 | 9 | /// Determines: 10 | /// * Default rigidbody modes for ragdoll bodies, and per-body overrides. 11 | /// * Default readback configuration for skeleton bones (whether bone position is read back from 12 | /// the ragdoll), and per-bone overrides. 13 | #[derive(Reflect, Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 14 | pub struct RagdollConfig { 15 | pub default_mode: Option, 16 | pub mode_overrides: HashMap, 17 | pub default_readback: Option, 18 | pub readback_overrides: HashMap, 19 | } 20 | 21 | impl RagdollConfig { 22 | pub fn body_mode(&self, body: BodyId) -> Option { 23 | self.mode_overrides 24 | .get(&body) 25 | .copied() 26 | .or(self.default_mode) 27 | } 28 | 29 | pub fn should_readback(&self, bone: BoneId) -> Option { 30 | self.readback_overrides 31 | .get(&bone) 32 | .copied() 33 | .or(self.default_readback) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /assets/skeleton_colliders/human.coll.ron: -------------------------------------------------------------------------------- 1 | ( 2 | colliders: [ 3 | ( 4 | id: ("505e7d48-bbdb-440b-ad3f-5886d332b294"), 5 | shape: Capsule(( 6 | radius: 0.1, 7 | half_length: 0.15, 8 | )), 9 | override_layers: false, 10 | layer_membership: 0, 11 | layer_filter: 0, 12 | attached_to: [ 13 | "metarig", 14 | "spine", 15 | "thigh.R", 16 | "shin.R", 17 | "foot.R", 18 | ], 19 | offset: ( 20 | rotation: (0.0, -0.0, 0.0, 1.0), 21 | translation: (0.0, 0.25, -0.025), 22 | ), 23 | offset_mode: Global, 24 | label: "shin", 25 | use_suffixes: true, 26 | ), 27 | ], 28 | skeleton: "skeletons/human.skn.ron", 29 | symmetry: ( 30 | name_mapper: Pattern(( 31 | key_1: "L", 32 | key_2: "R", 33 | pattern_before: "^.*", 34 | pattern_after: "$", 35 | )), 36 | mode: MirrorX, 37 | ), 38 | symmetry_enabled: true, 39 | default_layer_membership: 1, 40 | default_layer_filter: 0, 41 | suffix: ".R", 42 | mirror_suffix: ".L", 43 | ) -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/native_windows/scene_picker.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::Handle, prelude::World}; 2 | use egui_dock::egui; 3 | 4 | use crate::ui::{ 5 | generic_widgets::asset_picker::AssetPicker, 6 | global_state::{ 7 | active_scene::{ActiveScene, SetActiveScene}, 8 | get_global_state, 9 | }, 10 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct ScenePickerWindow; 15 | 16 | impl NativeEditorWindowExtension for ScenePickerWindow { 17 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, ctx: &mut EditorWindowContext) { 18 | let mut active = get_global_state::(world) 19 | .map(|active_scene| active_scene.handle.clone()) 20 | .unwrap_or_default(); 21 | 22 | let response = ui.add(AssetPicker::new_salted( 23 | &mut active, 24 | world, 25 | "active scene asset picker", 26 | )); 27 | 28 | if response.changed() && active != Handle::default() { 29 | ctx.trigger(SetActiveScene { 30 | new: ActiveScene { handle: active }, 31 | }); 32 | } 33 | } 34 | 35 | fn display_name(&self) -> String { 36 | "Select Scene".to_string() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/context/system_resources.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{skeleton::Skeleton, state_machine::high_level::StateMachine}, 3 | prelude::{AnimationGraph, GraphClip}, 4 | }; 5 | use bevy::{ 6 | asset::Assets, 7 | ecs::{prelude::*, system::SystemParam}, 8 | transform::prelude::*, 9 | }; 10 | 11 | /// Contains temprary data such as references to assets, gizmos, etc. 12 | #[derive(SystemParam)] 13 | pub struct SystemResources<'w, 's> { 14 | pub graph_clip_assets: Res<'w, Assets>, 15 | pub animation_graph_assets: Res<'w, Assets>, 16 | pub state_machine_assets: Res<'w, Assets>, 17 | pub skeleton_assets: Res<'w, Assets>, 18 | // HACK: The mutable transform access is needed due to the query being reused by the apply_pose 19 | // function. This is due to bevy's restriction against conflicting system parameters 20 | pub transform_query: Query<'w, 's, (&'static mut Transform, &'static GlobalTransform)>, 21 | pub names_query: Query<'w, 's, &'static Name>, 22 | pub children_query: Query<'w, 's, &'static Children>, 23 | pub parent_query: Query<'w, 's, &'static ChildOf>, 24 | #[cfg(feature = "physics_avian")] 25 | pub rigidbody_query: Query<'w, 's, &'static avian3d::prelude::RigidBody>, 26 | } 27 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/scene_picker.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::Handle, prelude::World}; 2 | use egui_dock::egui; 3 | 4 | use crate::ui::{ 5 | generic_widgets::asset_picker::AssetPicker, 6 | global_state::{ 7 | active_scene::{ActiveScene, SetActiveScene}, 8 | get_global_state, 9 | }, 10 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct ScenePickerWindow; 15 | 16 | impl NativeEditorWindowExtension for ScenePickerWindow { 17 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, ctx: &mut EditorWindowContext) { 18 | let Some(active_scene) = get_global_state::(world) else { 19 | return; 20 | }; 21 | 22 | let mut active = active_scene.handle.clone().unwrap_or_default(); 23 | 24 | let response = ui.add(AssetPicker::new_salted( 25 | &mut active, 26 | world, 27 | "active scene asset picker", 28 | )); 29 | 30 | if response.changed() && active != Handle::default() { 31 | ctx.trigger(SetActiveScene { 32 | handle: Some(active), 33 | }); 34 | } 35 | } 36 | 37 | fn display_name(&self) -> String { 38 | "Select Scene".to_string() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /assets/animation_graphs/toplevel.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "Locomotion FSM", 5 | inner: { 6 | "bevy_animation_graph::nodes::fsm_node::FSMNode": ( 7 | fsm: "fsm/locomotion.fsm.ron", 8 | ), 9 | }, 10 | ), 11 | ], 12 | edges_inverted: { 13 | OutputData("pose"): NodeData("Locomotion FSM", "pose"), 14 | NodeData("Locomotion FSM", "driver events"): InputData("user events"), 15 | NodeData("Locomotion FSM", "speed"): InputData("speed"), 16 | OutputTime: NodeTime("Locomotion FSM"), 17 | }, 18 | default_parameters: { 19 | "user events": EventQueue(( 20 | events: [ 21 | ( 22 | event: StringId(""), 23 | weight: 1.0, 24 | percentage: 1.0, 25 | ), 26 | ], 27 | )), 28 | "speed": F32(2.0), 29 | }, 30 | input_times: {}, 31 | output_parameters: { 32 | "pose": Pose, 33 | }, 34 | output_time: Some(()), 35 | extra: ( 36 | node_positions: { 37 | "Locomotion FSM": (463.6761, 488.58313), 38 | }, 39 | input_position: (237.51926, 497.61542), 40 | output_position: (727.0, 463.0), 41 | ), 42 | ) 43 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/length.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct LengthVec3Node; 11 | 12 | impl LengthVec3Node { 13 | pub const INPUT: &'static str = "in"; 14 | pub const OUTPUT: &'static str = "out"; 15 | 16 | pub fn new() -> Self { 17 | Self 18 | } 19 | } 20 | 21 | impl NodeLike for LengthVec3Node { 22 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 23 | let input: Vec3 = ctx.data_back(Self::INPUT)?.as_vec3()?; 24 | let output = input.length(); 25 | 26 | ctx.set_data_fwd(Self::OUTPUT, output); 27 | 28 | Ok(()) 29 | } 30 | 31 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 32 | [(Self::INPUT.into(), DataSpec::Vec3)].into() 33 | } 34 | 35 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 36 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 37 | } 38 | 39 | fn display_name(&self) -> String { 40 | "Length Vec3".into() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/quat/inverse.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct InvertQuatNode; 11 | 12 | impl InvertQuatNode { 13 | pub const INPUT: &'static str = "quat"; 14 | pub const OUTPUT: &'static str = "inverse"; 15 | 16 | pub fn new() -> Self { 17 | Self 18 | } 19 | } 20 | 21 | impl NodeLike for InvertQuatNode { 22 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 23 | let input: Quat = ctx.data_back(Self::INPUT)?.as_quat()?; 24 | let output: Quat = input.inverse(); 25 | 26 | ctx.set_data_fwd(Self::OUTPUT, output); 27 | 28 | Ok(()) 29 | } 30 | 31 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 32 | [(Self::INPUT.into(), DataSpec::Quat)].into() 33 | } 34 | 35 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 36 | [(Self::OUTPUT.into(), DataSpec::Quat)].into() 37 | } 38 | 39 | fn display_name(&self) -> String { 40 | "Invert Quat".into() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/normalize.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct NormalizeVec3Node; 11 | 12 | impl NormalizeVec3Node { 13 | pub const INPUT: &'static str = "in"; 14 | pub const OUTPUT: &'static str = "out"; 15 | 16 | pub fn new() -> Self { 17 | Self 18 | } 19 | } 20 | 21 | impl NodeLike for NormalizeVec3Node { 22 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 23 | let input: Vec3 = ctx.data_back(Self::INPUT)?.as_vec3()?; 24 | let output = input.normalize_or_zero(); 25 | 26 | ctx.set_data_fwd(Self::OUTPUT, output); 27 | 28 | Ok(()) 29 | } 30 | 31 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 32 | [(Self::INPUT.into(), DataSpec::Vec3)].into() 33 | } 34 | 35 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 36 | [(Self::OUTPUT.into(), DataSpec::Vec3)].into() 37 | } 38 | 39 | fn display_name(&self) -> String { 40 | "Normalize Vec3".into() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/native_windows/scene_preview_errors.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::World; 2 | use bevy_animation_graph::prelude::{AnimatedSceneInstance, AnimationGraphPlayer}; 3 | use egui_dock::egui; 4 | 5 | use crate::ui::{ 6 | PreviewScene, 7 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 8 | }; 9 | 10 | #[derive(Debug)] 11 | pub struct ScenePreviewErrorsWindow; 12 | 13 | impl NativeEditorWindowExtension for ScenePreviewErrorsWindow { 14 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, _ctx: &mut EditorWindowContext) { 15 | let mut query = world.query::<(&AnimatedSceneInstance, &PreviewScene)>(); 16 | let Ok((instance, _)) = query.single(world) else { 17 | return; 18 | }; 19 | let entity = instance.player_entity(); 20 | let mut query = world.query::<&AnimationGraphPlayer>(); 21 | let Ok(player) = query.get(world, entity) else { 22 | return; 23 | }; 24 | if let Some(error) = player.get_error() { 25 | ui.horizontal(|ui| { 26 | ui.label("⚠"); 27 | ui.label(format!("{error}")); 28 | }); 29 | } else { 30 | ui.centered_and_justified(|ui| { 31 | ui.label("No errors to show"); 32 | }); 33 | } 34 | } 35 | 36 | fn display_name(&self) -> String { 37 | "Errors".to_string() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/quat/from_euler.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct FromEulerNode { 11 | pub mode: EulerRot, 12 | } 13 | 14 | impl FromEulerNode { 15 | pub const INPUT: &'static str = "euler"; 16 | pub const OUTPUT: &'static str = "quat"; 17 | 18 | pub fn new(mode: EulerRot) -> Self { 19 | Self { mode } 20 | } 21 | } 22 | 23 | impl NodeLike for FromEulerNode { 24 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 25 | let Vec3 { x, y, z } = ctx.data_back(Self::INPUT)?.as_vec3()?; 26 | 27 | let output = Quat::from_euler(self.mode, x, y, z); 28 | 29 | ctx.set_data_fwd(Self::OUTPUT, output); 30 | 31 | Ok(()) 32 | } 33 | 34 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 35 | [(Self::INPUT.into(), DataSpec::Vec3)].into() 36 | } 37 | 38 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 39 | [(Self::OUTPUT.into(), DataSpec::Quat)].into() 40 | } 41 | 42 | fn display_name(&self) -> String { 43 | "Quat from Euler".into() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /assets/animation_graphs/custom_node_example.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "Custom node", 5 | inner: { 6 | "editor_as_a_plugin::MyCustomNode": (), 7 | }, 8 | ), 9 | ( 10 | name: "Human animation", 11 | inner: { 12 | "bevy_animation_graph::nodes::graph_node::GraphNode": ( 13 | graph: "animation_graphs/human_new.animgraph.ron", 14 | ), 15 | }, 16 | ), 17 | ], 18 | edges_inverted: { 19 | NodeTime("Custom node", "time"): NodeTime("Human animation"), 20 | OutputTime: NodeTime("Custom node"), 21 | NodeData("Custom node", "pose"): NodeData("Human animation", "pose"), 22 | OutputData("pose"): NodeData("Custom node", "pose"), 23 | }, 24 | default_parameters: {}, 25 | input_times: {}, 26 | output_parameters: { 27 | "pose": Pose, 28 | }, 29 | output_time: Some(()), 30 | extra: ( 31 | node_positions: { 32 | "Custom node": (357.23087, 492.0), 33 | "Human animation": (152.3077, 421.84613), 34 | }, 35 | input_position: (-0.92308044, 500.30765), 36 | output_position: (515.07697, 513.2307), 37 | input_param_order: {}, 38 | input_time_order: {}, 39 | output_data_order: { 40 | "pose": 0, 41 | }, 42 | output_pose_order: {}, 43 | ), 44 | ) 45 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/symmetry/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod serial; 3 | 4 | use self::config::SymmetryConfig; 5 | use crate::core::{ 6 | pose::{BonePose, Pose}, 7 | skeleton::Skeleton, 8 | }; 9 | 10 | fn flip_bone_pose(val: &BonePose, config: &SymmetryConfig) -> BonePose { 11 | BonePose { 12 | rotation: val.rotation.map(|v| config.mode.apply_quat(v)), 13 | translation: val.translation.map(|v| config.mode.apply_position(v)), 14 | scale: val.scale, 15 | weights: val.weights.clone(), 16 | } 17 | } 18 | 19 | pub fn flip_pose(val: &Pose, config: &SymmetryConfig, skeleton: &Skeleton) -> Pose { 20 | let mut out = Pose::default(); 21 | for (bone_id, bone_index) in val.paths.iter() { 22 | let channel = flip_bone_pose(&val.bones[*bone_index], config); 23 | // TODO: Make flipped return a Result type, so we can gracefully fail if no match for 24 | // id 25 | let path = skeleton.id_to_path(*bone_id).unwrap(); 26 | let new_path = config.name_mapper.flip(&path); 27 | let new_id = new_path.id(); 28 | 29 | // TODO: Should we assert that the new id is part of the skeleton? Probably yes 30 | // Fix this when we can gracefully fail 31 | if !skeleton.has_id(&new_id) { 32 | panic!("No match for flipped bone id"); 33 | } 34 | 35 | out.add_bone(channel, new_id); 36 | } 37 | out.skeleton = val.skeleton.clone(); 38 | out 39 | } 40 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/actions/window.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use bevy::ecs::{ 4 | system::{In, ResMut}, 5 | world::World, 6 | }; 7 | 8 | use super::{DynamicAction, run_handler}; 9 | use crate::ui::{UiState, actions::ActionContext, windows::WindowId}; 10 | 11 | pub type DynWindowAction = Box; 12 | 13 | /// An editor update event aimed at a particular window. 14 | /// How they're handled is up to the window. 15 | pub struct WindowAction { 16 | pub target: WindowId, 17 | pub action: DynWindowAction, 18 | } 19 | 20 | impl DynamicAction for WindowAction { 21 | fn handle(self: Box, world: &mut World, _: &mut ActionContext) { 22 | run_handler(world, "Failed to handle window action")(Self::system, *self); 23 | } 24 | } 25 | 26 | impl WindowAction { 27 | pub fn system(In(window_action): In, mut ui_state: ResMut) { 28 | ui_state.windows.handle_action(window_action); 29 | } 30 | } 31 | 32 | pub struct CloseWindowAction { 33 | pub id: WindowId, 34 | } 35 | 36 | impl DynamicAction for CloseWindowAction { 37 | fn handle(self: Box, world: &mut World, _: &mut ActionContext) { 38 | run_handler(world, "Failed to close window")(Self::system, *self) 39 | } 40 | } 41 | 42 | impl CloseWindowAction { 43 | pub fn system(In(action): In, mut ui_state: ResMut) { 44 | ui_state.windows.close(action.id); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_graph_context.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::UntypedAssetId, 3 | ecs::{ 4 | component::Component, entity::Entity, event::Event, observer::On, query::With, 5 | system::Single, world::World, 6 | }, 7 | platform::collections::HashMap, 8 | }; 9 | use bevy_animation_graph::prelude::GraphContextId; 10 | 11 | use crate::ui::global_state::{GlobalState, RegisterStateComponent}; 12 | 13 | #[derive(Debug, Component, Clone, Default)] 14 | pub struct ActiveContexts { 15 | pub by_asset: HashMap, 16 | } 17 | 18 | impl RegisterStateComponent for ActiveContexts { 19 | fn register(world: &mut World, global_state_entity: Entity) { 20 | world 21 | .entity_mut(global_state_entity) 22 | .insert(ActiveContexts::default()); 23 | 24 | world.add_observer(SetActiveContext::observe); 25 | } 26 | } 27 | 28 | #[derive(Event)] 29 | pub struct SetActiveContext { 30 | pub asset_id: UntypedAssetId, 31 | pub entity: Entity, 32 | pub id: GraphContextId, 33 | } 34 | 35 | impl SetActiveContext { 36 | pub fn observe( 37 | event: On, 38 | mut global_state: Single<&mut ActiveContexts, With>, 39 | ) { 40 | let event = event.event(); 41 | global_state 42 | .by_asset 43 | .insert(event.asset_id, (event.entity, event.id)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/quat/into_euler.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct IntoEulerNode { 11 | pub mode: EulerRot, 12 | } 13 | 14 | impl IntoEulerNode { 15 | pub const INPUT: &'static str = "quat"; 16 | pub const OUTPUT: &'static str = "euler"; 17 | 18 | pub fn new(mode: EulerRot) -> Self { 19 | Self { mode } 20 | } 21 | } 22 | 23 | impl NodeLike for IntoEulerNode { 24 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 25 | let quat: Quat = ctx.data_back(Self::INPUT)?.as_quat()?; 26 | 27 | let (x, y, z) = quat.to_euler(self.mode); 28 | let output = Vec3::new(x, y, z); 29 | 30 | ctx.set_data_fwd(Self::OUTPUT, output); 31 | 32 | Ok(()) 33 | } 34 | 35 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 36 | [(Self::INPUT.into(), DataSpec::Quat)].into() 37 | } 38 | 39 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 40 | [(Self::OUTPUT.into(), DataSpec::Vec3)].into() 41 | } 42 | 43 | fn display_name(&self) -> String { 44 | "Quat into Euler".into() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/native_windows/fsm_picker.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::Handle, prelude::World}; 2 | use egui_dock::egui; 3 | 4 | use crate::ui::{ 5 | generic_widgets::asset_picker::AssetPicker, 6 | global_state::{ 7 | active_fsm::{ActiveFsm, SetActiveFsm}, 8 | get_global_state, 9 | inspector_selection::{InspectorSelection, SetInspectorSelection}, 10 | }, 11 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct FsmPickerWindow; 16 | 17 | impl NativeEditorWindowExtension for FsmPickerWindow { 18 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, ctx: &mut EditorWindowContext) { 19 | let mut active = get_global_state::(world) 20 | .map(|active_scene| active_scene.handle.clone()) 21 | .unwrap_or_default(); 22 | 23 | let response = ui.add(AssetPicker::new_salted( 24 | &mut active, 25 | world, 26 | "active graph asset picker", 27 | )); 28 | 29 | if response.changed() && active != Handle::default() { 30 | ctx.trigger(SetActiveFsm { 31 | new: ActiveFsm { handle: active }, 32 | }); 33 | ctx.trigger(SetInspectorSelection { 34 | selection: InspectorSelection::ActiveFsm, 35 | }) 36 | } 37 | } 38 | 39 | fn display_name(&self) -> String { 40 | "Select FSM".to_string() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/add_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct AddF32; 11 | 12 | impl AddF32 { 13 | pub const INPUT_1: &'static str = "in_a"; 14 | pub const INPUT_2: &'static str = "in_b"; 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new() -> Self { 18 | Self 19 | } 20 | } 21 | 22 | impl NodeLike for AddF32 { 23 | fn display_name(&self) -> String { 24 | "+ Add".into() 25 | } 26 | 27 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 28 | let input_1 = ctx.data_back(Self::INPUT_1)?.as_f32()?; 29 | let input_2 = ctx.data_back(Self::INPUT_2)?.as_f32()?; 30 | ctx.set_data_fwd(Self::OUTPUT, input_1 + input_2); 31 | Ok(()) 32 | } 33 | 34 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 35 | [ 36 | (Self::INPUT_1.into(), DataSpec::F32), 37 | (Self::INPUT_2.into(), DataSpec::F32), 38 | ] 39 | .into() 40 | } 41 | 42 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 43 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/div_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct DivF32; 11 | 12 | impl DivF32 { 13 | pub const INPUT_1: &'static str = "in_a"; 14 | pub const INPUT_2: &'static str = "in_b"; 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new() -> Self { 18 | Self 19 | } 20 | } 21 | 22 | impl NodeLike for DivF32 { 23 | fn display_name(&self) -> String { 24 | "÷ Divide".into() 25 | } 26 | 27 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 28 | let input_1 = ctx.data_back(Self::INPUT_1)?.as_f32()?; 29 | let input_2 = ctx.data_back(Self::INPUT_2)?.as_f32()?; 30 | 31 | ctx.set_data_fwd(Self::OUTPUT, input_1 / input_2); 32 | Ok(()) 33 | } 34 | 35 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 36 | [ 37 | (Self::INPUT_1.into(), DataSpec::F32), 38 | (Self::INPUT_2.into(), DataSpec::F32), 39 | ] 40 | .into() 41 | } 42 | 43 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 44 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /assets/animation_graphs/fox.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "loop", 5 | inner: { 6 | "bevy_animation_graph::nodes::loop_node::LoopNode": ( 7 | interpolation_period: 0.0, 8 | ), 9 | }, 10 | ), 11 | ( 12 | name: "Walk", 13 | inner: { 14 | "bevy_animation_graph::nodes::clip_node::ClipNode": ( 15 | clip: "animations/fox_walk.anim.ron", 16 | override_duration: None, 17 | override_interpolation: None, 18 | ), 19 | }, 20 | ), 21 | ], 22 | edges_inverted: { 23 | OutputTime: NodeTime("loop"), 24 | NodeTime("loop", "time"): NodeTime("Walk"), 25 | NodeData("loop", "pose"): NodeData("Walk", "pose"), 26 | OutputData("pose"): NodeData("loop", "pose"), 27 | }, 28 | default_parameters: { 29 | "speed": F32(1.0), 30 | }, 31 | input_times: {}, 32 | output_parameters: { 33 | "pose": Pose, 34 | }, 35 | output_time: Some(()), 36 | extra: ( 37 | node_positions: { 38 | "loop": (808.6153, 420.00003), 39 | "Walk": (548.30774, 418.1538), 40 | }, 41 | input_position: (312.0769, 409.0769), 42 | output_position: (1130.0, 408.84613), 43 | input_param_order: {}, 44 | input_time_order: {}, 45 | output_data_order: {}, 46 | output_pose_order: {}, 47 | ), 48 | ) -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/quat/mul.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct MulQuatNode; 11 | 12 | impl MulQuatNode { 13 | pub const INPUT_A: &'static str = "a"; 14 | pub const INPUT_B: &'static str = "b"; 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new() -> Self { 18 | Self 19 | } 20 | } 21 | 22 | impl NodeLike for MulQuatNode { 23 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 24 | let a: Quat = ctx.data_back(Self::INPUT_A)?.as_quat()?; 25 | let b: Quat = ctx.data_back(Self::INPUT_B)?.as_quat()?; 26 | 27 | let output = a * b; 28 | 29 | ctx.set_data_fwd(Self::OUTPUT, output); 30 | 31 | Ok(()) 32 | } 33 | 34 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 35 | [ 36 | (Self::INPUT_A.into(), DataSpec::Quat), 37 | (Self::INPUT_B.into(), DataSpec::Quat), 38 | ] 39 | .into() 40 | } 41 | 42 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 43 | [(Self::OUTPUT.into(), DataSpec::Quat)].into() 44 | } 45 | 46 | fn display_name(&self) -> String { 47 | "× Multiply Quat".into() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /assets/animation_graphs/snake.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "wiggle", 5 | inner: { 6 | "bevy_animation_graph::nodes::clip_node::ClipNode": ( 7 | clip: "animations/snake_wiggle.anim.ron", 8 | override_duration: None, 9 | override_interpolation: None, 10 | ), 11 | }, 12 | ), 13 | ( 14 | name: "loop", 15 | inner: { 16 | "bevy_animation_graph::nodes::loop_node::LoopNode": ( 17 | interpolation_period: 0.0, 18 | ), 19 | }, 20 | ), 21 | ], 22 | edges_inverted: { 23 | NodeData("loop", "pose"): NodeData("wiggle", "pose"), 24 | NodeTime("loop", "time"): NodeTime("wiggle"), 25 | OutputData("pose"): NodeData("loop", "pose"), 26 | OutputTime: NodeTime("loop"), 27 | }, 28 | default_parameters: {}, 29 | input_times: {}, 30 | output_parameters: { 31 | "pose": Pose, 32 | }, 33 | output_time: Some(()), 34 | extra: ( 35 | node_positions: { 36 | "wiggle": (111.69232, 188.30771), 37 | "loop": (279.69232, 191.0769), 38 | }, 39 | input_position: (-80.30768, 187.38464), 40 | output_position: (458.7693, 188.30766), 41 | input_param_order: {}, 42 | input_time_order: {}, 43 | output_data_order: { 44 | "pose": 0, 45 | }, 46 | output_pose_order: {}, 47 | ), 48 | ) -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/rotation_arc.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct RotationArcNode; 11 | 12 | impl RotationArcNode { 13 | pub const INPUT_1: &'static str = "in_a"; 14 | pub const INPUT_2: &'static str = "in_b"; 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new() -> Self { 18 | Self 19 | } 20 | } 21 | 22 | impl NodeLike for RotationArcNode { 23 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 24 | let input_1: Vec3 = ctx.data_back(Self::INPUT_1)?.as_vec3()?; 25 | let input_2: Vec3 = ctx.data_back(Self::INPUT_2)?.as_vec3()?; 26 | 27 | ctx.set_data_fwd(Self::OUTPUT, Quat::from_rotation_arc(input_1, input_2)); 28 | 29 | Ok(()) 30 | } 31 | 32 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 33 | [ 34 | (Self::INPUT_1.into(), DataSpec::Vec3), 35 | (Self::INPUT_2.into(), DataSpec::Vec3), 36 | ] 37 | .into() 38 | } 39 | 40 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 41 | [(Self::OUTPUT.into(), DataSpec::Quat)].into() 42 | } 43 | 44 | fn display_name(&self) -> String { 45 | "Rotation Arc".into() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/quat.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::{EulerRot, Quat, Vec3}; 2 | 3 | use crate::ui::generic_widgets::vec3::Vec3Widget; 4 | 5 | pub struct QuatWidget<'a> { 6 | pub quat: &'a mut Quat, 7 | pub slider_step_size: f32, 8 | pub id_hash: egui::Id, 9 | pub width: f32, 10 | } 11 | 12 | impl<'a> QuatWidget<'a> { 13 | pub fn new_salted(quat: &'a mut Quat, salt: impl std::hash::Hash) -> Self { 14 | Self { 15 | quat, 16 | slider_step_size: 0.1, 17 | id_hash: egui::Id::new(salt), 18 | width: 300., 19 | } 20 | } 21 | 22 | pub fn with_step_size(mut self, step_size: f32) -> Self { 23 | self.slider_step_size = step_size; 24 | self 25 | } 26 | 27 | pub fn with_width(mut self, width: f32) -> Self { 28 | self.width = width; 29 | self 30 | } 31 | } 32 | 33 | impl<'a> egui::Widget for QuatWidget<'a> { 34 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 35 | ui.horizontal(|ui| { 36 | let mut euler_vec: Vec3 = self.quat.to_euler(EulerRot::XYZ).into(); 37 | 38 | let response = ui.add(Vec3Widget { 39 | vec3: &mut euler_vec, 40 | slider_step_size: self.slider_step_size, 41 | id_hash: self.id_hash.with("quat as vec"), 42 | width: self.width, 43 | }); 44 | 45 | *self.quat = Quat::from_euler(EulerRot::XYZ, euler_vec.x, euler_vec.y, euler_vec.z); 46 | 47 | response 48 | }) 49 | .inner 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/sub_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct SubF32; 11 | 12 | impl SubF32 { 13 | pub const INPUT_1: &'static str = "in_a"; 14 | pub const INPUT_2: &'static str = "in_b"; 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new() -> Self { 18 | Self 19 | } 20 | } 21 | 22 | impl NodeLike for SubF32 { 23 | fn display_name(&self) -> String { 24 | "- Subtract".into() 25 | } 26 | 27 | fn duration(&self, _ctx: PassContext) -> Result<(), GraphError> { 28 | Ok(()) 29 | } 30 | 31 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 32 | let input_1 = ctx.data_back(Self::INPUT_1)?.as_f32()?; 33 | let input_2 = ctx.data_back(Self::INPUT_2)?.as_f32()?; 34 | 35 | ctx.set_data_fwd(Self::OUTPUT, input_1 - input_2); 36 | Ok(()) 37 | } 38 | 39 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 40 | [ 41 | (Self::INPUT_1.into(), DataSpec::F32), 42 | (Self::INPUT_2.into(), DataSpec::F32), 43 | ] 44 | .into() 45 | } 46 | 47 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 48 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/mul_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct MulF32; 11 | 12 | impl MulF32 { 13 | pub const INPUT_1: &'static str = "in_a"; 14 | pub const INPUT_2: &'static str = "in_b"; 15 | pub const OUTPUT: &'static str = "out"; 16 | 17 | pub fn new() -> Self { 18 | Self 19 | } 20 | } 21 | 22 | impl NodeLike for MulF32 { 23 | fn display_name(&self) -> String { 24 | "× Multiply F32".into() 25 | } 26 | 27 | fn duration(&self, _ctx: PassContext) -> Result<(), GraphError> { 28 | Ok(()) 29 | } 30 | 31 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 32 | let input_1 = ctx.data_back(Self::INPUT_1)?.as_f32()?; 33 | let input_2 = ctx.data_back(Self::INPUT_2)?.as_f32()?; 34 | 35 | ctx.set_data_fwd(Self::OUTPUT, input_1 * input_2); 36 | Ok(()) 37 | } 38 | 39 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 40 | [ 41 | (Self::INPUT_1.into(), DataSpec::F32), 42 | (Self::INPUT_2.into(), DataSpec::F32), 43 | ] 44 | .into() 45 | } 46 | 47 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 48 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/icons/joint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/reflect_widgets/entity_path.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use bevy_animation_graph::core::animation_clip::EntityPath; 4 | use bevy_inspector_egui::reflect_inspector::InspectorUi; 5 | use egui_dock::egui; 6 | 7 | use super::{EguiInspectorExtension, MakeBuffer}; 8 | 9 | pub struct EntityPathInspector; 10 | 11 | impl EguiInspectorExtension for EntityPathInspector { 12 | type Base = EntityPath; 13 | type Buffer = String; 14 | 15 | fn mutable( 16 | value: &mut Self::Base, 17 | buffer: &mut Self::Buffer, 18 | ui: &mut egui::Ui, 19 | _options: &dyn Any, 20 | _id: egui::Id, 21 | _env: InspectorUi<'_, '_>, 22 | ) -> bool { 23 | let buffered = buffer; 24 | let response = ui.text_edit_singleline(buffered); 25 | 26 | if response.lost_focus() { 27 | *value = EntityPath::from_slashed_string(buffered.clone()); 28 | true 29 | } else if !response.has_focus() { 30 | *buffered = value.to_slashed_string(); 31 | false 32 | } else { 33 | false 34 | } 35 | } 36 | 37 | fn readonly( 38 | value: &Self::Base, 39 | _buffer: &Self::Buffer, 40 | ui: &mut egui::Ui, 41 | _options: &dyn Any, 42 | _id: egui::Id, 43 | _env: InspectorUi<'_, '_>, 44 | ) { 45 | let slashed_path = value.to_slashed_string(); 46 | ui.label(slashed_path); 47 | } 48 | } 49 | 50 | impl MakeBuffer for EntityPath { 51 | fn make_buffer(&self) -> String { 52 | self.to_slashed_string() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/clamp_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct ClampF32; 11 | 12 | impl ClampF32 { 13 | pub const INPUT: &'static str = "in"; 14 | pub const CLAMP_MIN: &'static str = "min"; 15 | pub const CLAMP_MAX: &'static str = "max"; 16 | pub const OUTPUT: &'static str = "out"; 17 | 18 | pub fn new() -> Self { 19 | Self 20 | } 21 | } 22 | 23 | impl NodeLike for ClampF32 { 24 | fn display_name(&self) -> String { 25 | "Clamp".into() 26 | } 27 | 28 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 29 | let input = ctx.data_back(Self::INPUT)?.as_f32()?; 30 | let min = ctx.data_back(Self::CLAMP_MIN)?.as_f32()?; 31 | let max = ctx.data_back(Self::CLAMP_MAX)?.as_f32()?; 32 | ctx.set_data_fwd(Self::OUTPUT, input.clamp(min, max)); 33 | Ok(()) 34 | } 35 | 36 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 37 | [ 38 | (Self::INPUT.into(), DataSpec::F32), 39 | (Self::CLAMP_MIN.into(), DataSpec::F32), 40 | (Self::CLAMP_MAX.into(), DataSpec::F32), 41 | ] 42 | .into() 43 | } 44 | 45 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 46 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/into_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct DecomposeVec3Node; 11 | 12 | impl DecomposeVec3Node { 13 | pub const INPUT: &'static str = "vec"; 14 | pub const OUTPUT_X: &'static str = "x"; 15 | pub const OUTPUT_Y: &'static str = "y"; 16 | pub const OUTPUT_Z: &'static str = "z"; 17 | 18 | pub fn new() -> Self { 19 | Self 20 | } 21 | } 22 | 23 | impl NodeLike for DecomposeVec3Node { 24 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 25 | let Vec3 { x, y, z } = ctx.data_back(Self::INPUT)?.as_vec3()?; 26 | 27 | ctx.set_data_fwd(Self::OUTPUT_X, x); 28 | ctx.set_data_fwd(Self::OUTPUT_Y, y); 29 | ctx.set_data_fwd(Self::OUTPUT_Z, z); 30 | 31 | Ok(()) 32 | } 33 | 34 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 35 | [(Self::INPUT.into(), DataSpec::Vec3)].into() 36 | } 37 | 38 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 39 | [ 40 | (Self::OUTPUT_X.into(), DataSpec::F32), 41 | (Self::OUTPUT_Y.into(), DataSpec::F32), 42 | (Self::OUTPUT_Z.into(), DataSpec::F32), 43 | ] 44 | .into() 45 | } 46 | 47 | fn display_name(&self) -> String { 48 | "Decompose Vec3".into() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/from_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct BuildVec3Node; 11 | 12 | impl BuildVec3Node { 13 | pub const INPUT_X: &'static str = "x"; 14 | pub const INPUT_Y: &'static str = "y"; 15 | pub const INPUT_Z: &'static str = "z"; 16 | pub const OUTPUT: &'static str = "vec"; 17 | 18 | pub fn new() -> Self { 19 | Self 20 | } 21 | } 22 | 23 | impl NodeLike for BuildVec3Node { 24 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 25 | let x = ctx.data_back(Self::INPUT_X)?.as_f32()?; 26 | let y = ctx.data_back(Self::INPUT_Y)?.as_f32()?; 27 | let z = ctx.data_back(Self::INPUT_Z)?.as_f32()?; 28 | 29 | ctx.set_data_fwd(Self::OUTPUT, Vec3::new(x, y, z)); 30 | 31 | Ok(()) 32 | } 33 | 34 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 35 | [ 36 | (Self::INPUT_X.into(), DataSpec::F32), 37 | (Self::INPUT_Y.into(), DataSpec::F32), 38 | (Self::INPUT_Z.into(), DataSpec::F32), 39 | ] 40 | .into() 41 | } 42 | 43 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 44 | [(Self::OUTPUT.into(), DataSpec::Vec3)].into() 45 | } 46 | 47 | fn display_name(&self) -> String { 48 | "Build Vec3".into() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/reflect_widgets/plugin.rs: -------------------------------------------------------------------------------- 1 | use bevy::app::{App, Plugin}; 2 | use bevy_animation_graph::{ 3 | core::{ 4 | event_track::TrackItemValue, 5 | ragdoll::{bone_mapping::RagdollBoneMap, definition::Ragdoll}, 6 | state_machine::high_level::StateMachine, 7 | }, 8 | prelude::{AnimatedScene, AnimationGraph, GraphClip}, 9 | }; 10 | 11 | use super::{ 12 | EguiInspectorExtensionRegistration, asset_picker::AssetPickerInspector, 13 | checkbox::CheckboxInspector, entity_path::EntityPathInspector, 14 | pattern_mapper::PatternMapperInspector, submittable::SubmittableInspector, 15 | target_tracks::TargetTracksInspector, vec2_plane::Vec2PlaneInspector, 16 | }; 17 | pub struct BetterInspectorPlugin; 18 | impl Plugin for BetterInspectorPlugin { 19 | fn build(&self, app: &mut App) { 20 | EntityPathInspector.register(app); 21 | PatternMapperInspector.register(app); 22 | CheckboxInspector.register(app); 23 | AssetPickerInspector::::default().register(app); 24 | AssetPickerInspector::::default().register(app); 25 | AssetPickerInspector::::default().register(app); 26 | AssetPickerInspector::::default().register(app); 27 | AssetPickerInspector::::default().register(app); 28 | AssetPickerInspector::::default().register(app); 29 | TargetTracksInspector.register(app); 30 | SubmittableInspector::::default().register(app); 31 | SubmittableInspector::::default().register(app); 32 | Vec2PlaneInspector.register(app); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/native_windows/graph_picker.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::Handle, prelude::World}; 2 | use egui_dock::egui; 3 | 4 | use crate::ui::{ 5 | actions::graph::CreateGraphAction, 6 | generic_widgets::asset_picker::AssetPicker, 7 | global_state::{ 8 | active_graph::{ActiveGraph, SetActiveGraph}, 9 | get_global_state, 10 | inspector_selection::{InspectorSelection, SetInspectorSelection}, 11 | }, 12 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct GraphPickerWindow; 17 | 18 | impl NativeEditorWindowExtension for GraphPickerWindow { 19 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, ctx: &mut EditorWindowContext) { 20 | let mut active = get_global_state::(world) 21 | .map(|active_scene| active_scene.handle.clone()) 22 | .unwrap_or_default(); 23 | 24 | let response = ui.add(AssetPicker::new_salted( 25 | &mut active, 26 | world, 27 | "active graph asset picker", 28 | )); 29 | 30 | if response.changed() && active != Handle::default() { 31 | ctx.trigger(SetActiveGraph { 32 | new: ActiveGraph { handle: active }, 33 | }); 34 | ctx.trigger(SetInspectorSelection { 35 | selection: InspectorSelection::ActiveGraph, 36 | }) 37 | } 38 | 39 | if ui.button("New Graph").clicked() { 40 | ctx.editor_actions.dynamic(CreateGraphAction); 41 | } 42 | } 43 | 44 | fn display_name(&self) -> String { 45 | "Select Graph".to_string() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/reflect_widgets/pattern_mapper.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use bevy_animation_graph::prelude::{config::PatternMapper, serial::PatternMapperSerial}; 4 | use bevy_inspector_egui::reflect_inspector::InspectorUi; 5 | use egui_dock::egui; 6 | 7 | use super::{EguiInspectorExtension, MakeBuffer}; 8 | 9 | #[derive(Default)] 10 | pub struct PatternMapperInspector; 11 | 12 | impl EguiInspectorExtension for PatternMapperInspector { 13 | type Base = PatternMapper; 14 | type Buffer = PatternMapperSerial; 15 | 16 | fn mutable( 17 | value: &mut Self::Base, 18 | buffer: &mut Self::Buffer, 19 | ui: &mut egui::Ui, 20 | _options: &dyn Any, 21 | id: egui::Id, 22 | mut env: InspectorUi<'_, '_>, 23 | ) -> bool { 24 | match env.ui_for_reflect_with_options(buffer, ui, id, &()) { 25 | true => { 26 | if let Ok(mapper) = buffer.to_value() { 27 | *value = mapper; 28 | true 29 | } else { 30 | false 31 | } 32 | } 33 | false => false, 34 | } 35 | } 36 | 37 | fn readonly( 38 | _value: &Self::Base, 39 | buffer: &Self::Buffer, 40 | ui: &mut egui::Ui, 41 | _options: &dyn Any, 42 | id: egui::Id, 43 | mut env: InspectorUi<'_, '_>, 44 | ) { 45 | env.ui_for_reflect_readonly_with_options(buffer, ui, id, &()); 46 | } 47 | } 48 | 49 | impl MakeBuffer for PatternMapper { 50 | fn make_buffer(&self) -> PatternMapperSerial { 51 | PatternMapperSerial::from_value(self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/animation_node.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::{reflect::AppTypeRegistry, world::World}; 2 | use bevy_animation_graph::prelude::AnimationNode; 3 | 4 | use crate::ui::node_editors::{ReflectEditable, reflect_editor::ReflectNodeEditor}; 5 | 6 | pub struct AnimationNodeWidget<'a> { 7 | pub node: &'a mut AnimationNode, 8 | pub id_hash: egui::Id, 9 | pub world: &'a mut World, 10 | } 11 | 12 | impl<'a> AnimationNodeWidget<'a> { 13 | pub fn new_salted( 14 | node: &'a mut AnimationNode, 15 | world: &'a mut World, 16 | salt: impl std::hash::Hash, 17 | ) -> Self { 18 | Self { 19 | node, 20 | id_hash: egui::Id::new(salt), 21 | world, 22 | } 23 | } 24 | } 25 | 26 | impl<'a> egui::Widget for AnimationNodeWidget<'a> { 27 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 28 | ui.push_id(self.id_hash, |ui| { 29 | let response = ui.text_edit_singleline(&mut self.node.name); 30 | 31 | let editor = if let Some(editable) = self 32 | .world 33 | .resource::() 34 | .0 35 | .clone() 36 | .read() 37 | .get_type_data::(self.node.inner.type_id()) 38 | { 39 | (editable.get_editor)(self.node.inner.as_ref()) 40 | } else { 41 | Box::new(ReflectNodeEditor) 42 | }; 43 | 44 | let inner_edit_response = editor.show_dyn(ui, self.world, self.node.inner.as_mut()); 45 | 46 | response | inner_edit_response 47 | }) 48 | .inner 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/event_queue/fire_event.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{ 2 | animation_graph::PinMap, 3 | animation_node::{NodeLike, ReflectNodeLike}, 4 | context::{PassContext, SpecContext}, 5 | edge_data::{AnimationEvent, DataSpec, EventQueue, SampledEvent}, 6 | errors::GraphError, 7 | }; 8 | use bevy::prelude::*; 9 | 10 | #[derive(Reflect, Clone, Debug, Default)] 11 | #[reflect(Default, NodeLike)] 12 | pub struct FireEventNode { 13 | pub event: AnimationEvent, 14 | } 15 | 16 | impl FireEventNode { 17 | pub const EVENT_OUT: &'static str = "event"; 18 | pub const CONDITION_IN: &'static str = "condition"; 19 | 20 | pub fn new(event: AnimationEvent) -> Self { 21 | Self { event } 22 | } 23 | } 24 | 25 | impl NodeLike for FireEventNode { 26 | fn display_name(&self) -> String { 27 | "FireEvent".into() 28 | } 29 | 30 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 31 | let cond: bool = ctx.data_back(Self::CONDITION_IN)?.into_bool()?; 32 | 33 | if cond { 34 | ctx.set_data_fwd( 35 | Self::EVENT_OUT, 36 | EventQueue::with_events([SampledEvent::instant(self.event.clone())]), 37 | ); 38 | } else { 39 | ctx.set_data_fwd(Self::EVENT_OUT, EventQueue::with_events([])); 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 46 | [(Self::CONDITION_IN.into(), DataSpec::Bool)].into() 47 | } 48 | 49 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 50 | [(Self::EVENT_OUT.into(), DataSpec::EventQueue)].into() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/node_editors/ragdoll_config.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::Assets, ecs::world::World}; 2 | use bevy_animation_graph::{ 3 | core::{ragdoll::definition::Ragdoll, skeleton::Skeleton}, 4 | nodes::const_ragdoll_config::ConstRagdollConfig, 5 | }; 6 | use egui::Widget; 7 | 8 | use crate::ui::{ 9 | generic_widgets::ragdoll_config::RagdollConfigWidget, 10 | global_state::{ 11 | active_ragdoll::ActiveRagdoll, active_skeleton::ActiveSkeleton, get_global_state, 12 | }, 13 | node_editors::{Editable, NodeEditor}, 14 | }; 15 | 16 | pub struct RagdollConfigNodeEditor; 17 | 18 | impl NodeEditor for RagdollConfigNodeEditor { 19 | type Target = ConstRagdollConfig; 20 | 21 | fn show( 22 | &self, 23 | ui: &mut egui::Ui, 24 | world: &mut World, 25 | node: &mut Self::Target, 26 | ) -> egui::Response { 27 | let skeleton_assets = world.resource::>(); 28 | let ragdoll_assets = world.resource::>(); 29 | let active_skeleton = 30 | get_global_state::(world).and_then(|s| skeleton_assets.get(&s.handle)); 31 | let active_ragdoll = 32 | get_global_state::(world).and_then(|s| ragdoll_assets.get(&s.handle)); 33 | RagdollConfigWidget::new_salted(&mut node.value, "Ragdoll config node editor") 34 | .with_skeleton(active_skeleton) 35 | .with_ragdoll(active_ragdoll) 36 | .ui(ui) 37 | } 38 | } 39 | 40 | impl Editable for ConstRagdollConfig { 41 | type Editor = RagdollConfigNodeEditor; 42 | 43 | fn get_editor(&self) -> Self::Editor { 44 | RagdollConfigNodeEditor 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/errors/asset_loader_error.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use thiserror::Error; 3 | 4 | use super::GraphValidationError; 5 | 6 | /// Possible errors that can be produced by a custom asset loader 7 | // TODO: clean this up 8 | // https://rust-lang.github.io/api-guidelines/interoperability.html?highlight=error#examples-of-error-messages 9 | // - lowercase error messages 10 | // - don't print sources exclusively 11 | // - avoid mega error enums 12 | #[non_exhaustive] 13 | #[derive(Debug, Error)] 14 | pub enum AssetLoaderError { 15 | /// An [IO](std::io) Error 16 | #[error("Could load shader: {0}")] 17 | Io(#[from] std::io::Error), 18 | /// A [RON](ron) Error 19 | #[error("Could not parse RON: {0}")] 20 | RonSpannedError(#[from] ron::error::SpannedError), 21 | #[error("Could not load Gltf: {0}")] 22 | GltfError(#[from] bevy::gltf::GltfError), 23 | #[error("Could not find gltf named label: {0}")] 24 | GltfMissingLabel(String), 25 | #[error("Could not complete direct asset load: {0}")] 26 | LoadDirectError(#[from] bevy::asset::LoadDirectError), 27 | #[error("Animated scene path is incorrect: {0}")] 28 | AnimatedSceneMissingName(String), 29 | #[error( 30 | "Animated scene missing a root (an exsiting AnimationPlayer). Does your scene root have a `Name`?" 31 | )] 32 | AnimatedSceneMissingRoot, 33 | #[error("Graph does not satisfy constraints: {0}")] 34 | InconsistentGraphError(#[from] GraphValidationError), 35 | #[error("Failed to load skeleton colliders object")] 36 | SkeletonColliderLoadError, 37 | #[error("Failed to parse a provided regular expression: {0}")] 38 | RegexParsingError(#[from] regex::Error), 39 | } 40 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/vec3/lerp.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct LerpVec3Node; 11 | 12 | impl LerpVec3Node { 13 | pub const INPUT_A: &'static str = "a"; 14 | pub const INPUT_B: &'static str = "b"; 15 | pub const INPUT_FACTOR: &'static str = "factor"; 16 | pub const OUTPUT: &'static str = "out"; 17 | 18 | pub fn new() -> Self { 19 | Self 20 | } 21 | } 22 | 23 | impl NodeLike for LerpVec3Node { 24 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 25 | let a: Vec3 = ctx.data_back(Self::INPUT_A)?.as_vec3()?; 26 | let b: Vec3 = ctx.data_back(Self::INPUT_B)?.as_vec3()?; 27 | let factor: f32 = ctx.data_back(Self::INPUT_FACTOR)?.as_f32()?; 28 | 29 | let output = Vec3::lerp(a, b, factor); 30 | 31 | ctx.set_data_fwd(Self::OUTPUT, output); 32 | 33 | Ok(()) 34 | } 35 | 36 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 37 | [ 38 | (Self::INPUT_A.into(), DataSpec::Vec3), 39 | (Self::INPUT_B.into(), DataSpec::Vec3), 40 | (Self::INPUT_FACTOR.into(), DataSpec::F32), 41 | ] 42 | .into() 43 | } 44 | 45 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 46 | [(Self::OUTPUT.into(), DataSpec::Vec3)].into() 47 | } 48 | 49 | fn display_name(&self) -> String { 50 | "Lerp Vec3".into() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/quat/slerp.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct SlerpQuatNode; 11 | 12 | impl SlerpQuatNode { 13 | pub const INPUT_A: &'static str = "a"; 14 | pub const INPUT_B: &'static str = "b"; 15 | pub const INPUT_FACTOR: &'static str = "factor"; 16 | pub const OUTPUT: &'static str = "out"; 17 | 18 | pub fn new() -> Self { 19 | Self 20 | } 21 | } 22 | 23 | impl NodeLike for SlerpQuatNode { 24 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 25 | let a: Quat = ctx.data_back(Self::INPUT_A)?.as_quat()?; 26 | let b: Quat = ctx.data_back(Self::INPUT_B)?.as_quat()?; 27 | let factor: f32 = ctx.data_back(Self::INPUT_FACTOR)?.as_f32()?; 28 | 29 | let output = Quat::slerp(a, b, factor); 30 | 31 | ctx.set_data_fwd(Self::OUTPUT, output); 32 | 33 | Ok(()) 34 | } 35 | 36 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 37 | [ 38 | (Self::INPUT_A.into(), DataSpec::Quat), 39 | (Self::INPUT_B.into(), DataSpec::Quat), 40 | (Self::INPUT_FACTOR.into(), DataSpec::F32), 41 | ] 42 | .into() 43 | } 44 | 45 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 46 | [(Self::OUTPUT.into(), DataSpec::Quat)].into() 47 | } 48 | 49 | fn display_name(&self) -> String { 50 | "Slerp Quat".into() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/write_pose.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for mapping a skeleton to a desired ragdoll configuration 2 | 3 | use bevy::math::Isometry3d; 4 | 5 | use crate::{ 6 | core::{ 7 | pose::Pose, 8 | ragdoll::{ 9 | bone_mapping::RagdollBoneMap, 10 | definition::{BodyId, Ragdoll}, 11 | }, 12 | skeleton::Skeleton, 13 | space_conversion::SpaceConversionContext, 14 | }, 15 | prelude::PoseFallbackContext, 16 | }; 17 | 18 | pub struct BodyTarget { 19 | pub body_id: BodyId, 20 | /// Isometry relative to the character root transform 21 | pub character_space_isometry: Isometry3d, 22 | } 23 | 24 | pub struct RagdollTargets { 25 | pub bodies: Vec, 26 | } 27 | 28 | pub fn write_pose_to_ragdoll( 29 | pose: &Pose, 30 | skeleton: &Skeleton, 31 | ragdoll: &Ragdoll, 32 | mapping: &RagdollBoneMap, 33 | pose_fallback_ctx: PoseFallbackContext, 34 | ) -> RagdollTargets { 35 | let mut targets = RagdollTargets { bodies: Vec::new() }; 36 | let convert = SpaceConversionContext { 37 | pose_fallback: pose_fallback_ctx, 38 | }; 39 | 40 | for body in ragdoll.bodies.values() { 41 | let Some(body_mapping) = mapping.bodies_from_bones.get(&body.id) else { 42 | continue; 43 | }; 44 | 45 | // TODO: Quaternion interpolation for more than 1 bone weights 46 | let character_transform = 47 | convert.character_transform_of_bone(pose, skeleton, body_mapping.bone.bone.id()); 48 | 49 | targets.bodies.push(BodyTarget { 50 | body_id: body.id, 51 | character_space_isometry: character_transform.to_isometry() * body_mapping.bone.offset, 52 | }) 53 | } 54 | 55 | targets 56 | } 57 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/option.rs: -------------------------------------------------------------------------------- 1 | pub struct CheapOptionWidget<'a, T> { 2 | pub value: &'a mut Option, 3 | pub id_hash: egui::Id, 4 | } 5 | 6 | impl<'a, T> CheapOptionWidget<'a, T> { 7 | pub fn new_salted(value: &'a mut Option, salt: impl std::hash::Hash) -> Self { 8 | Self { 9 | value, 10 | id_hash: egui::Id::new(salt), 11 | } 12 | } 13 | } 14 | 15 | impl<'a, T> CheapOptionWidget<'a, T> 16 | where 17 | T: Clone + Default + Send + Sync + 'static, 18 | { 19 | pub fn ui( 20 | self, 21 | ui: &mut egui::Ui, 22 | show: impl FnOnce(&mut egui::Ui, &mut T) -> egui::Response, 23 | ) -> egui::Response { 24 | ui.push_id(self.id_hash, |ui| { 25 | let buffer_id = ui.id().with("cheap option widget buffer"); 26 | let mut check = self.value.is_some(); 27 | let mut value = self.value.clone().unwrap_or_else(|| { 28 | ui.memory_mut(|mem| mem.data.get_temp::(buffer_id)) 29 | .unwrap_or_default() 30 | }); 31 | 32 | let response = ui 33 | .horizontal(|ui| { 34 | let mut response = ui.add(egui::Checkbox::without_text(&mut check)); 35 | 36 | response |= ui.add_enabled_ui(check, |ui| show(ui, &mut value)).inner; 37 | 38 | response 39 | }) 40 | .inner; 41 | 42 | ui.memory_mut(|mem| mem.data.insert_temp(buffer_id, value.clone())); 43 | 44 | if check { 45 | *self.value = Some(value); 46 | } else { 47 | *self.value = None; 48 | } 49 | 50 | response 51 | }) 52 | .inner 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/select_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | 8 | #[derive(Reflect, Clone, Debug, Default)] 9 | #[reflect(Default, NodeLike)] 10 | pub struct SelectF32; 11 | 12 | impl SelectF32 { 13 | pub const INPUT_BOOL: &'static str = "bool"; 14 | pub const INPUT_FALSE: &'static str = "if_false"; 15 | pub const INPUT_TRUE: &'static str = "if_true"; 16 | pub const OUTPUT: &'static str = "out"; 17 | 18 | pub fn new() -> Self { 19 | Self 20 | } 21 | } 22 | 23 | impl NodeLike for SelectF32 { 24 | fn display_name(&self) -> String { 25 | "Select F32".into() 26 | } 27 | 28 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 29 | let bool: bool = ctx.data_back(Self::INPUT_BOOL)?.as_bool()?; 30 | let if_false: f32 = ctx.data_back(Self::INPUT_FALSE)?.as_f32()?; 31 | let if_true: f32 = ctx.data_back(Self::INPUT_TRUE)?.as_f32()?; 32 | 33 | let output = if bool { if_true } else { if_false }; 34 | 35 | ctx.set_data_fwd(Self::OUTPUT, output); 36 | Ok(()) 37 | } 38 | 39 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 40 | [ 41 | (Self::INPUT_BOOL.into(), DataSpec::Bool), 42 | (Self::INPUT_FALSE.into(), DataSpec::F32), 43 | (Self::INPUT_TRUE.into(), DataSpec::F32), 44 | ] 45 | .into() 46 | } 47 | 48 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 49 | [(Self::OUTPUT.into(), DataSpec::F32)].into() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/context/graph_context.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{animation_graph::TimeUpdate, prelude::AnimationGraph}, 3 | prelude::{ 4 | node_caches::NodeCaches, 5 | node_states::{NodeStates, StateKey}, 6 | }, 7 | }; 8 | use bevy::{asset::AssetId, platform::collections::HashMap, reflect::prelude::*}; 9 | 10 | #[derive(Debug, Reflect)] 11 | pub struct GraphContext { 12 | pub node_states: NodeStates, 13 | pub node_caches: NodeCaches, 14 | pub query_output_time: QueryOutputTime, 15 | graph_id: AssetId, 16 | } 17 | 18 | impl GraphContext { 19 | pub fn new(graph_id: AssetId) -> Self { 20 | Self { 21 | graph_id, 22 | node_states: NodeStates::default(), 23 | node_caches: NodeCaches::default(), 24 | query_output_time: QueryOutputTime::None, 25 | } 26 | } 27 | 28 | pub fn next_frame(&mut self) { 29 | self.node_states.next_frame(); 30 | self.node_caches.next_frame(); 31 | } 32 | 33 | pub fn get_graph_id(&self) -> AssetId { 34 | self.graph_id 35 | } 36 | } 37 | 38 | #[derive(Debug, Reflect)] 39 | pub enum QueryOutputTime { 40 | None, 41 | Forced(TimeUpdate), 42 | ByKey(HashMap), 43 | } 44 | 45 | impl QueryOutputTime { 46 | pub fn from_key(key: StateKey, update: TimeUpdate) -> Self { 47 | Self::ByKey([(key, update)].into()) 48 | } 49 | 50 | pub fn get(&self, key: StateKey) -> Option { 51 | match self { 52 | QueryOutputTime::None => None, 53 | QueryOutputTime::Forced(time_update) => Some(time_update.clone()), 54 | QueryOutputTime::ByKey(hash_map) => hash_map.get(&key).cloned(), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/space_conversion/extend_skeleton.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{ 3 | animation_graph::{PinMap, TimeUpdate}, 4 | animation_node::{AnimationNode, AnimationNodeType, NodeLike}, 5 | duration_data::DurationData, 6 | errors::GraphError, 7 | pose::{Pose, PoseSpec}, 8 | space_conversion::SpaceConversion, 9 | }, 10 | prelude::{PassContext, SpecContext}, 11 | }; 12 | use bevy::reflect::{std_traits::ReflectDefault, Reflect}; 13 | 14 | #[derive(Reflect, Clone, Debug, Default)] 15 | #[reflect(Default)] 16 | pub struct ExtendSkeleton {} 17 | 18 | impl ExtendSkeleton { 19 | pub const POSE_IN: &'static str = "pose_in"; 20 | 21 | pub fn new() -> Self { 22 | Self {} 23 | } 24 | 25 | pub fn wrapped(self, name: impl Into) -> AnimationNode { 26 | AnimationNode::new_from_nodetype(name.into(), AnimationNodeType::ExtendSkeleton(self)) 27 | } 28 | } 29 | 30 | impl NodeLike for ExtendSkeleton { 31 | fn duration(&self, mut ctx: PassContext) -> Result, GraphError> { 32 | Ok(Some(ctx.duration_back(Self::POSE_IN)?)) 33 | } 34 | 35 | fn pose_pass( 36 | &self, 37 | time_update: TimeUpdate, 38 | mut ctx: PassContext, 39 | ) -> Result, GraphError> { 40 | let in_pose = ctx.pose_back(Self::POSE_IN, time_update)?; 41 | 42 | Ok(Some(ctx.extend_skeleton_bone(&in_pose))) 43 | } 44 | 45 | fn pose_input_spec(&self, _ctx: SpecContext) -> PinMap { 46 | [(Self::POSE_IN.into(), PoseSpec::BoneSpace)].into() 47 | } 48 | 49 | fn pose_output_spec(&self, _ctx: SpecContext) -> Option { 50 | Some(PoseSpec::BoneSpace) 51 | } 52 | 53 | fn display_name(&self) -> String { 54 | "Extend Skeleton".into() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/dry_run.yaml: -------------------------------------------------------------------------------- 1 | name: DryRun 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | # Run cargo publish pipeline as a dry run 12 | dry_run_library: 13 | name: Dry Run Publish Library 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 30 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v4 19 | - name: Cache 20 | uses: actions/cache@v3 21 | with: 22 | path: | 23 | ~/.cargo/bin/ 24 | ~/.cargo/registry/index/ 25 | ~/.cargo/registry/cache/ 26 | ~/.cargo/git/db/ 27 | target/ 28 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 29 | - name: Install stable toolchain 30 | uses: dtolnay/rust-toolchain@stable 31 | - name: Install Dependencies 32 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 33 | - name: Run cargo publish dry run for library 34 | run: cargo publish --dry-run -p bevy_animation_graph 35 | dry_run_editor: 36 | name: Dry Run Publish Editor 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 30 39 | steps: 40 | - name: Checkout sources 41 | uses: actions/checkout@v4 42 | - name: Cache 43 | uses: actions/cache@v3 44 | with: 45 | path: | 46 | ~/.cargo/bin/ 47 | ~/.cargo/registry/index/ 48 | ~/.cargo/registry/cache/ 49 | ~/.cargo/git/db/ 50 | target/ 51 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 52 | - name: Install stable toolchain 53 | uses: dtolnay/rust-toolchain@stable 54 | - name: Install Dependencies 55 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 56 | - name: Run cargo publish dry run for editor 57 | run: cargo publish --dry-run -p bevy_animation_graph_editor 58 | 59 | -------------------------------------------------------------------------------- /assets/animation_graphs/human_ik.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "base anim", 5 | inner: { 6 | "bevy_animation_graph::nodes::graph_node::GraphNode": ( 7 | graph: "animation_graphs/human_new.animgraph.ron", 8 | ) 9 | } 10 | ), 11 | ( 12 | name: "arm ik", 13 | inner: { 14 | "bevy_animation_graph::nodes::twoboneik_node::TwoBoneIKNode": () 15 | } 16 | ), 17 | ], 18 | edges_inverted: { 19 | NodeData("arm ik", "target_path"): InputData("right hand path"), 20 | NodeData("base anim", "target_speed"): InputData("target_speed"), 21 | OutputTime: NodeTime("arm ik"), 22 | NodeData("base anim", "target_direction"): InputData("target_direction"), 23 | OutputData("pose"): NodeData("arm ik", "pose"), 24 | NodeData("arm ik", "pose"): NodeData("base anim", "pose"), 25 | NodeData("arm ik", "target_position"): InputData("target hand pos"), 26 | NodeTime("arm ik", "time"): NodeTime("base anim"), 27 | }, 28 | default_parameters: { 29 | "target_speed": F32(1.0), 30 | "target_direction": Vec3((1.0, 0.0, 0.0)), 31 | "right hand path": EntityPath([ 32 | "metarig", 33 | "spine", 34 | "spine.001", 35 | "spine.002", 36 | "spine.003", 37 | "shoulder.R", 38 | "upper_arm.R", 39 | "forearm.R", 40 | "hand.R", 41 | ]), 42 | "target hand pos": Vec3((0.5, 1.5, 0.0)), 43 | }, 44 | input_times: {}, 45 | output_parameters: { 46 | "pose": Pose, 47 | }, 48 | output_time: Some(()), 49 | extra: ( 50 | node_positions: { 51 | "base anim": (366.46158, 444.0), 52 | "arm ik": (563.07697, 420.92303), 53 | }, 54 | input_position: (108.0, 459.69223), 55 | output_position: (777.2307, 446.7692), 56 | ), 57 | ) 58 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/ragdoll_editor/body_inspector.rs: -------------------------------------------------------------------------------- 1 | use bevy_animation_graph::core::ragdoll::definition::{Body, BodyMode}; 2 | use egui::Widget; 3 | 4 | use crate::ui::generic_widgets::vec3::Vec3Widget; 5 | 6 | pub struct BodyInspector<'a> { 7 | pub body: &'a mut Body, 8 | } 9 | 10 | impl Widget for BodyInspector<'_> { 11 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 12 | let response = egui::Grid::new("ragdoll body inspector").show(ui, |ui| { 13 | let mut response = ui.label("ID"); 14 | response |= ui.label(format!("{:?}", self.body.id)); 15 | ui.end_row(); 16 | 17 | response |= ui.label("label:"); 18 | response |= ui.text_edit_singleline(&mut self.body.label); 19 | ui.end_row(); 20 | 21 | response |= ui.label("offset:"); 22 | response |= ui 23 | .add(Vec3Widget::new_salted(&mut self.body.offset, "offset").with_step_size(0.005)); 24 | ui.end_row(); 25 | 26 | response |= ui.label("Body mode:"); 27 | response |= egui::ComboBox::from_id_salt("body mode") 28 | .selected_text(match self.body.default_mode { 29 | BodyMode::Kinematic => "Kinematic", 30 | BodyMode::Dynamic => "Dynamic", 31 | }) 32 | .show_ui(ui, |ui| { 33 | ui.selectable_value( 34 | &mut self.body.default_mode, 35 | BodyMode::Kinematic, 36 | "Kinematic", 37 | ); 38 | ui.selectable_value(&mut self.body.default_mode, BodyMode::Dynamic, "Dynamic"); 39 | }) 40 | .response; 41 | ui.end_row(); 42 | 43 | response |= ui.label("use symmetry:"); 44 | response |= ui.add(egui::Checkbox::without_text(&mut self.body.use_symmetry)); 45 | 46 | response 47 | }); 48 | 49 | response.inner 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/native_windows/event_sender.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::World; 2 | use bevy_animation_graph::core::edge_data::AnimationEvent; 3 | use egui_dock::egui; 4 | 5 | use crate::ui::{ 6 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 7 | utils, 8 | }; 9 | 10 | #[derive(Debug)] 11 | pub struct EventSenderWindow; 12 | 13 | impl NativeEditorWindowExtension for EventSenderWindow { 14 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, ctx: &mut EditorWindowContext) { 15 | let Some(graph_player) = utils::get_animation_graph_player_mut(world) else { 16 | return; 17 | }; 18 | 19 | let buffer = ctx 20 | .buffers 21 | .get_mut_or_default::(ui.id().with("event sender table buffer")); 22 | 23 | ui.horizontal_wrapped(|ui| { 24 | buffer.events.retain(|ev| { 25 | egui::Frame::NONE 26 | .stroke(egui::Stroke::new(1., egui::Color32::WHITE)) 27 | .show(ui, |ui| { 28 | ui.horizontal(|ui| { 29 | if ui.button(format!("{ev:?}")).clicked() { 30 | graph_player.send_event(ev.clone()); 31 | } 32 | !ui.button("×").clicked() 33 | }) 34 | .inner 35 | }) 36 | .inner 37 | }); 38 | }); 39 | 40 | ui.separator(); 41 | 42 | utils::using_inspector_env(world, |mut env| { 43 | env.ui_for_reflect(&mut buffer.event_editor, ui); 44 | }); 45 | 46 | if ui.button("Add").clicked() { 47 | buffer.events.push(buffer.event_editor.clone()); 48 | } 49 | } 50 | 51 | fn display_name(&self) -> String { 52 | "Send events".to_string() 53 | } 54 | } 55 | 56 | #[derive(Default)] 57 | struct EventSenderBuffer { 58 | events: Vec, 59 | event_editor: AnimationEvent, 60 | } 61 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/global_state/active_scene.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{Assets, Handle}, 3 | ecs::{ 4 | component::Component, 5 | entity::Entity, 6 | event::Event, 7 | observer::On, 8 | system::{Commands, Res}, 9 | world::World, 10 | }, 11 | }; 12 | use bevy_animation_graph::prelude::AnimatedScene; 13 | 14 | use crate::ui::global_state::{ 15 | RegisterStateComponent, SetOrInsertEvent, 16 | active_ragdoll::{ActiveRagdoll, SetActiveRagdoll}, 17 | active_skeleton::{ActiveSkeleton, SetActiveSkeleton}, 18 | observe_clear_global_state, observe_set_or_insert_event, 19 | }; 20 | 21 | #[derive(Debug, Component, Default, Clone)] 22 | pub struct ActiveScene { 23 | pub handle: Handle, 24 | } 25 | 26 | impl RegisterStateComponent for ActiveScene { 27 | fn register(world: &mut World, _global_state_entity: Entity) { 28 | world.add_observer(observe_set_or_insert_event::); 29 | world.add_observer(observe_clear_global_state::); 30 | world.add_observer(set_skeleton_from_scene); 31 | } 32 | } 33 | 34 | #[derive(Event)] 35 | pub struct SetActiveScene { 36 | pub new: ActiveScene, 37 | } 38 | 39 | impl SetOrInsertEvent for SetActiveScene { 40 | type Target = ActiveScene; 41 | 42 | fn get_component(&self) -> Self::Target { 43 | self.new.clone() 44 | } 45 | } 46 | 47 | fn set_skeleton_from_scene( 48 | event: On, 49 | mut commands: Commands, 50 | animated_scene_asset: Res>, 51 | ) { 52 | if let Some(scn) = animated_scene_asset.get(&event.new.handle) { 53 | commands.trigger(SetActiveSkeleton { 54 | new: ActiveSkeleton { 55 | handle: scn.skeleton.clone(), 56 | }, 57 | }); 58 | 59 | if let Some(ragdoll) = scn.ragdoll.clone() { 60 | commands.trigger(SetActiveRagdoll { 61 | new: ActiveRagdoll { handle: ragdoll }, 62 | }); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/bone_mapping.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{Asset, Handle}, 3 | math::Isometry3d, 4 | platform::collections::HashMap, 5 | reflect::Reflect, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{ 10 | core::{ 11 | animation_clip::EntityPath, 12 | ragdoll::definition::{BodyId, Ragdoll}, 13 | skeleton::Skeleton, 14 | }, 15 | prelude::config::SymmetryConfig, 16 | }; 17 | 18 | // TODO: Replace EntityPath with bone ids, and add a serial proxy that allows the loader to map 19 | 20 | #[derive(Asset, Debug, Clone, Reflect)] 21 | pub struct RagdollBoneMap { 22 | /// We assume the invariant that the sum of all body weights for a bone is always 1. 23 | pub bones_from_bodies: HashMap, 24 | /// We assume the invariant that the sum of all body weights for a bone is always 1. 25 | pub bodies_from_bones: HashMap, 26 | pub skeleton: Handle, 27 | pub ragdoll: Handle, 28 | 29 | pub skeleton_symmetry: SymmetryConfig, 30 | } 31 | 32 | /// How to get a bone's position from rigidbodies 33 | #[derive(Debug, Default, Clone, Reflect, Serialize, Deserialize)] 34 | pub struct BoneMapping { 35 | pub bone_id: EntityPath, 36 | pub bodies: Vec, 37 | pub created_from: Option, 38 | } 39 | 40 | /// How to get a rigibody's position from bones 41 | #[derive(Debug, Default, Clone, Reflect, Serialize, Deserialize)] 42 | pub struct BodyMapping { 43 | pub body_id: BodyId, 44 | pub bone: BoneWeight, 45 | pub created_from: Option, 46 | } 47 | 48 | #[derive(Debug, Clone, Default, Reflect, Serialize, Deserialize)] 49 | pub struct BodyWeight { 50 | pub body: BodyId, 51 | pub weight: f32, 52 | pub offset: Isometry3d, 53 | #[serde(default)] 54 | pub override_offset: bool, 55 | } 56 | 57 | #[derive(Debug, Clone, Default, Reflect, Serialize, Deserialize)] 58 | pub struct BoneWeight { 59 | pub bone: EntityPath, 60 | pub offset: Isometry3d, 61 | #[serde(default)] 62 | pub override_offset: bool, 63 | } 64 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/ragdoll_editor/body_mapping_inspector.rs: -------------------------------------------------------------------------------- 1 | use bevy_animation_graph::core::{ragdoll::bone_mapping::BodyMapping, skeleton::Skeleton}; 2 | use egui::Widget; 3 | 4 | use crate::ui::generic_widgets::{bone_id::BoneIdWidget, isometry3d::Isometry3dWidget}; 5 | 6 | pub struct BodyMappingInspector<'a> { 7 | pub body_mapping: &'a mut BodyMapping, 8 | pub skeleton: &'a Skeleton, 9 | } 10 | 11 | impl Widget for BodyMappingInspector<'_> { 12 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 13 | let BodyMapping { body_id, bone, .. } = self.body_mapping; 14 | 15 | egui::Grid::new("body weight grid") 16 | .show(ui, |ui| { 17 | let mut response = ui.label("ID:"); 18 | response |= ui.label(format!("{}", body_id.uuid().hyphenated())); 19 | ui.end_row(); 20 | 21 | response |= ui.label("target bone:"); 22 | let mut bone_id = bone.bone.id(); 23 | response |= ui.add( 24 | BoneIdWidget::new_salted(&mut bone_id, "bone id picker") 25 | .with_skeleton(Some(self.skeleton)), 26 | ); 27 | if let Some(path) = self.skeleton.id_to_path(bone_id) { 28 | bone.bone = path; 29 | } 30 | 31 | ui.end_row(); 32 | 33 | response |= ui 34 | .label("override offset?:") 35 | .on_hover_text(include_str!("../../../tooltips/override_offset.txt")); 36 | response |= ui.add(egui::Checkbox::without_text(&mut bone.override_offset)); 37 | ui.end_row(); 38 | 39 | if bone.override_offset { 40 | response |= ui.add( 41 | Isometry3dWidget::new_salted(&mut bone.offset, "offset from bone") 42 | .with_flatten_grid(true), 43 | ); 44 | 45 | ui.end_row(); 46 | } 47 | 48 | response 49 | }) 50 | .inner 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/node_editors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ragdoll_config; 2 | pub mod reflect_editor; 3 | 4 | use std::any::Any; 5 | 6 | use bevy::{app::App, ecs::world::World, reflect::FromType}; 7 | use bevy_animation_graph::{nodes::const_ragdoll_config::ConstRagdollConfig, prelude::NodeLike}; 8 | 9 | pub trait NodeEditor: 'static { 10 | type Target: 'static; 11 | fn show(&self, ui: &mut egui::Ui, world: &mut World, node: &mut Self::Target) 12 | -> egui::Response; 13 | } 14 | 15 | pub trait DynNodeEditor: 'static { 16 | fn show_dyn( 17 | &self, 18 | ui: &mut egui::Ui, 19 | world: &mut World, 20 | node: &mut dyn NodeLike, 21 | ) -> egui::Response; 22 | } 23 | 24 | impl DynNodeEditor for E 25 | where 26 | E: NodeEditor, 27 | { 28 | fn show_dyn( 29 | &self, 30 | ui: &mut egui::Ui, 31 | world: &mut World, 32 | node: &mut dyn NodeLike, 33 | ) -> egui::Response { 34 | self.show( 35 | ui, 36 | world, 37 | node.as_any_mut() 38 | .downcast_mut() 39 | .expect("Mismatched node editor used"), 40 | ) 41 | } 42 | } 43 | 44 | pub trait Editable: 'static { 45 | type Editor: DynNodeEditor; 46 | 47 | fn get_editor(&self) -> Self::Editor; 48 | } 49 | 50 | #[derive(Clone)] 51 | pub struct ReflectEditable { 52 | // If needed, we can also store the type ID of the editor in this struct 53 | pub get_editor: fn(&dyn Any) -> Box, 54 | } 55 | 56 | impl FromType for ReflectEditable 57 | where 58 | T: Editable, 59 | { 60 | fn from_type() -> Self { 61 | Self { 62 | get_editor: reflect_get_editor::, 63 | } 64 | } 65 | } 66 | 67 | fn reflect_get_editor(value: &dyn Any) -> Box { 68 | let static_value = value.downcast_ref::().expect("Reflection type mismatch"); 69 | Box::new(static_value.get_editor()) 70 | } 71 | 72 | pub fn register_node_editables(app: &mut App) { 73 | app.register_type_data::(); 74 | } 75 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/reflect_widgets/submittable.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, marker::PhantomData}; 2 | 3 | use bevy::reflect::{FromReflect, Reflect, Reflectable}; 4 | use bevy_inspector_egui::reflect_inspector::InspectorUi; 5 | use egui_dock::egui; 6 | 7 | use super::{EguiInspectorExtension, MakeBuffer, WidgetHash}; 8 | 9 | #[derive(Default)] 10 | pub struct SubmittableInspector { 11 | _phantom_data: PhantomData, 12 | } 13 | 14 | /// Wrapping a type in submittable is used to create a reflect-based editor that 15 | /// 1. Uses the editor widget for the inner type 16 | /// 2. Has a "submit" method and does not update the original value until submission 17 | #[derive(Clone, Reflect, Debug, Default)] 18 | pub struct Submittable { 19 | pub value: T, 20 | } 21 | 22 | impl EguiInspectorExtension 23 | for SubmittableInspector 24 | { 25 | type Base = Submittable; 26 | type Buffer = T; 27 | 28 | fn mutable( 29 | value: &mut Self::Base, 30 | buffer: &mut Self::Buffer, 31 | ui: &mut egui::Ui, 32 | options: &dyn Any, 33 | id: egui::Id, 34 | mut env: InspectorUi<'_, '_>, 35 | ) -> bool { 36 | env.ui_for_reflect_with_options(buffer, ui, id, options); 37 | if ui.button("Submit").clicked() { 38 | *value = Submittable { 39 | value: buffer.clone(), 40 | }; 41 | true 42 | } else { 43 | false 44 | } 45 | } 46 | 47 | fn readonly( 48 | value: &Self::Base, 49 | _buffer: &Self::Buffer, 50 | ui: &mut egui::Ui, 51 | options: &dyn Any, 52 | id: egui::Id, 53 | mut env: InspectorUi<'_, '_>, 54 | ) { 55 | env.ui_for_reflect_readonly_with_options(&value.value, ui, id, options); 56 | } 57 | } 58 | 59 | impl MakeBuffer for Submittable { 60 | fn make_buffer(&self) -> T { 61 | self.value.clone() 62 | } 63 | } 64 | 65 | impl WidgetHash for Submittable { 66 | fn widget_hash(&self) -> u64 { 67 | self.value.widget_hash() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/uuid.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | pub struct UuidWidget<'a> { 4 | pub uuid: &'a mut Uuid, 5 | pub id_hash: egui::Id, 6 | } 7 | 8 | impl<'a> UuidWidget<'a> { 9 | pub fn new_salted(uuid: &'a mut Uuid, salt: impl std::hash::Hash) -> Self { 10 | Self { 11 | uuid, 12 | id_hash: egui::Id::new(salt), 13 | } 14 | } 15 | } 16 | 17 | #[derive(Clone)] 18 | struct UuidData { 19 | original: Uuid, 20 | buffer: String, 21 | } 22 | 23 | impl UuidData { 24 | pub fn from_uuid(uuid: Uuid) -> Self { 25 | Self { 26 | original: uuid, 27 | buffer: format!("{}", uuid.hyphenated()), 28 | } 29 | } 30 | } 31 | 32 | impl<'a> egui::Widget for UuidWidget<'a> { 33 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 34 | ui.push_id(self.id_hash, |ui| { 35 | let buffer_id = ui.id().with("buffer"); 36 | let mut data = ui.memory_mut(|mem| { 37 | let prev_data = mem.data.get_temp::(buffer_id); 38 | if prev_data.is_none_or(|d| d.original != *self.uuid) { 39 | mem.data 40 | .insert_temp(buffer_id, UuidData::from_uuid(*self.uuid)); 41 | } 42 | 43 | mem.data 44 | .get_temp_mut_or_insert_with(buffer_id, || UuidData::from_uuid(*self.uuid)) 45 | .clone() 46 | }); 47 | 48 | let mut response = ui.add( 49 | egui::TextEdit::singleline(&mut data.buffer).min_size(egui::Vec2::new(250., 0.)), 50 | ); 51 | 52 | // Mark non-changed; we only consider the response changed if the uuid string is valid 53 | response.flags &= !egui::response::Flags::CHANGED; 54 | 55 | if let Ok(new_uuid) = Uuid::parse_str(&data.buffer) { 56 | *self.uuid = new_uuid; 57 | response.mark_changed(); 58 | } 59 | 60 | ui.memory_mut(|mem| mem.data.insert_temp(buffer_id, data)); 61 | 62 | response 63 | }) 64 | .inner 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/space_conversion/into_bone.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{ 3 | animation_graph::{PinMap, TimeUpdate}, 4 | animation_node::{AnimationNode, AnimationNodeType, NodeLike}, 5 | duration_data::DurationData, 6 | errors::GraphError, 7 | pose::{Pose, PoseSpec}, 8 | }, 9 | prelude::{PassContext, SpecContext}, 10 | }; 11 | use bevy::reflect::{std_traits::ReflectDefault, Reflect}; 12 | 13 | #[derive(Reflect, Clone, Debug, Default)] 14 | #[reflect(Default, NodeLike)] 15 | pub struct IntoBoneSpaceNode {} 16 | 17 | impl IntoBoneSpaceNode { 18 | pub const POSE_IN: &'static str = "pose_in"; 19 | 20 | pub fn new() -> Self { 21 | Self {} 22 | } 23 | 24 | pub fn wrapped(self, name: impl Into) -> AnimationNode { 25 | AnimationNode::new_from_nodetype(name.into(), AnimationNodeType::IntoBoneSpace(self)) 26 | } 27 | } 28 | 29 | impl NodeLike for IntoBoneSpaceNode { 30 | fn duration(&self, mut ctx: PassContext) -> Result, GraphError> { 31 | Ok(Some(ctx.duration_back(Self::POSE_IN)?)) 32 | } 33 | 34 | fn pose_pass( 35 | &self, 36 | _time_update: TimeUpdate, 37 | mut _ctx: PassContext, 38 | ) -> Result, GraphError> { 39 | // let in_pose = ctx.pose_back(Self::POSE_IN, time_update)?; 40 | // Ok(Some(PoseFrame { 41 | // timestamp: in_pose.timestamp, 42 | // data: PoseFrameData::BoneSpace(match &in_pose.data { 43 | // PoseFrameData::BoneSpace(data) => data.clone(), 44 | // PoseFrameData::CharacterSpace(data) => ctx.character_to_bone(data), 45 | // PoseFrameData::GlobalSpace(data) => ctx.global_to_bone(data), 46 | // }), 47 | // })) 48 | todo!() 49 | } 50 | 51 | fn pose_input_spec(&self, _ctx: SpecContext) -> PinMap { 52 | [(Self::POSE_IN.into(), PoseSpec::Any)].into() 53 | } 54 | 55 | fn pose_output_spec(&self, _ctx: SpecContext) -> Option { 56 | Some(PoseSpec::BoneSpace) 57 | } 58 | 59 | fn display_name(&self) -> String { 60 | "* → Bone".into() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/native_windows/preview_hierarchy.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::World; 2 | use bevy_animation_graph::{core::animation_clip::EntityPath, prelude::AnimatedSceneInstance}; 3 | use egui_dock::egui; 4 | 5 | use crate::{ 6 | tree::{Tree, TreeResult}, 7 | ui::{ 8 | PreviewScene, 9 | native_windows::{EditorWindowContext, NativeEditorWindowExtension}, 10 | utils, 11 | }, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct PreviewHierarchyWindow; 16 | 17 | impl NativeEditorWindowExtension for PreviewHierarchyWindow { 18 | fn ui(&self, ui: &mut egui::Ui, world: &mut World, ctx: &mut EditorWindowContext) { 19 | let mut query = world.query::<(&AnimatedSceneInstance, &PreviewScene)>(); 20 | let Ok((instance, _)) = query.single(world) else { 21 | return; 22 | }; 23 | let entity = instance.player_entity(); 24 | let tree = Tree::entity_tree(world, entity); 25 | match utils::select_from_branches(ui, tree.0) { 26 | TreeResult::Leaf((_, path), _) => { 27 | let path = EntityPath { parts: path }; 28 | ui.output_mut(|o| { 29 | o.commands 30 | .push(egui::OutputCommand::CopyText(path.to_slashed_string())) 31 | }); 32 | ctx.notifications 33 | .info(format!("{} copied to clipboard", path.to_slashed_string())); 34 | // ctx.global_state.entity_path = Some(path); 35 | } 36 | TreeResult::Node((_, path), _) => { 37 | let path = EntityPath { parts: path }; 38 | ui.output_mut(|o| { 39 | o.commands 40 | .push(egui::OutputCommand::CopyText(path.to_slashed_string())) 41 | }); 42 | ctx.notifications 43 | .info(format!("{} copied to clipboard", path.to_slashed_string())); 44 | // ctx.global_state.entity_path = Some(path); 45 | } 46 | TreeResult::None => (), 47 | } 48 | } 49 | 50 | fn display_name(&self) -> String { 51 | "Preview Hierarchy".to_string() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/fsm_node.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{ 2 | animation_graph::PinMap, 3 | animation_node::{NodeLike, ReflectNodeLike}, 4 | context::{PassContext, SpecContext}, 5 | edge_data::DataSpec, 6 | errors::GraphError, 7 | state_machine::{high_level::StateMachine, low_level::LowLevelStateMachine}, 8 | }; 9 | use bevy::prelude::*; 10 | 11 | #[derive(Reflect, Clone, Debug, Default)] 12 | #[reflect(Default, NodeLike)] 13 | pub struct FSMNode { 14 | pub fsm: Handle, 15 | } 16 | 17 | impl FSMNode { 18 | pub const OUT_POSE: &'static str = "pose"; 19 | 20 | pub fn new(fsm: Handle) -> Self { 21 | Self { fsm } 22 | } 23 | } 24 | 25 | impl NodeLike for FSMNode { 26 | fn duration(&self, _ctx: PassContext) -> Result<(), GraphError> { 27 | todo!() 28 | } 29 | 30 | fn update(&self, ctx: PassContext) -> Result<(), GraphError> { 31 | let fsm = ctx.resources.state_machine_assets.get(&self.fsm).unwrap(); 32 | fsm.get_low_level_fsm().update(ctx)?; 33 | 34 | Ok(()) 35 | } 36 | 37 | fn data_input_spec(&self, ctx: SpecContext) -> PinMap { 38 | let fsm = ctx 39 | .fsm_assets 40 | .get(&self.fsm) 41 | .unwrap_or_else(|| panic!("no FSM asset `{:?}`", self.fsm)); 42 | let fsm_args = fsm 43 | .input_data 44 | .iter() 45 | .map(|(pin_id, default_val)| (pin_id.clone(), DataSpec::from(default_val))); 46 | 47 | let mut input_map = PinMap::from([( 48 | LowLevelStateMachine::DRIVER_EVENT_QUEUE.into(), 49 | DataSpec::EventQueue, 50 | )]); 51 | input_map.extend(fsm_args); 52 | 53 | input_map 54 | } 55 | 56 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 57 | [(Self::OUT_POSE.into(), DataSpec::Pose)].into() 58 | } 59 | 60 | fn time_input_spec(&self, _ctx: SpecContext) -> PinMap<()> { 61 | [].into() 62 | } 63 | 64 | fn time_output_spec(&self, _ctx: SpecContext) -> Option<()> { 65 | Some(()) 66 | } 67 | 68 | fn display_name(&self) -> String { 69 | "⌘ FSM".into() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/arithmetic/f32/compare_f32.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::PinMap; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::prelude::*; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Reflect, Clone, Copy, Default, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] 10 | pub enum CompareOp { 11 | Less, 12 | LessEqual, 13 | More, 14 | MoreEqual, 15 | #[default] 16 | Equal, 17 | } 18 | 19 | #[derive(Reflect, Clone, Debug, Default)] 20 | #[reflect(Default, NodeLike)] 21 | pub struct CompareF32 { 22 | pub op: CompareOp, 23 | } 24 | 25 | impl CompareF32 { 26 | pub const INPUT_1: &'static str = "in_a"; 27 | pub const INPUT_2: &'static str = "in_b"; 28 | pub const OUTPUT: &'static str = "out"; 29 | 30 | pub fn new(op: CompareOp) -> Self { 31 | Self { op } 32 | } 33 | } 34 | 35 | impl NodeLike for CompareF32 { 36 | fn display_name(&self) -> String { 37 | "== Compare".into() 38 | } 39 | 40 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 41 | let input_1 = ctx.data_back(Self::INPUT_1)?.as_f32()?; 42 | let input_2 = ctx.data_back(Self::INPUT_2)?.as_f32()?; 43 | ctx.set_data_fwd( 44 | Self::OUTPUT, 45 | match self.op { 46 | CompareOp::Less => input_1 < input_2, 47 | CompareOp::LessEqual => input_1 <= input_2, 48 | CompareOp::More => input_1 > input_2, 49 | CompareOp::MoreEqual => input_1 >= input_2, 50 | CompareOp::Equal => input_1 == input_2, 51 | }, 52 | ); 53 | Ok(()) 54 | } 55 | 56 | fn data_input_spec(&self, _ctx: SpecContext) -> PinMap { 57 | [ 58 | (Self::INPUT_1.into(), DataSpec::F32), 59 | (Self::INPUT_2.into(), DataSpec::F32), 60 | ] 61 | .into() 62 | } 63 | 64 | fn data_output_spec(&self, _ctx: SpecContext) -> PinMap { 65 | [(Self::OUTPUT.into(), DataSpec::Bool)].into() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/space_conversion/into_global.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{ 3 | animation_graph::{PinMap, TimeUpdate}, 4 | animation_node::{AnimationNode, AnimationNodeType, NodeLike}, 5 | duration_data::DurationData, 6 | errors::GraphError, 7 | pose::{Pose, PoseSpec}, 8 | }, 9 | prelude::{PassContext, SpecContext}, 10 | }; 11 | use bevy::reflect::{std_traits::ReflectDefault, Reflect}; 12 | 13 | #[derive(Reflect, Clone, Debug, Default)] 14 | #[reflect(Default, NodeLike)] 15 | pub struct IntoGlobalSpaceNode {} 16 | 17 | impl IntoGlobalSpaceNode { 18 | pub const POSE_IN: &'static str = "pose_in"; 19 | 20 | pub fn new() -> Self { 21 | Self {} 22 | } 23 | 24 | pub fn wrapped(self, name: impl Into) -> AnimationNode { 25 | AnimationNode::new_from_nodetype(name.into(), AnimationNodeType::IntoGlobalSpace(self)) 26 | } 27 | } 28 | 29 | impl NodeLike for IntoGlobalSpaceNode { 30 | fn duration(&self, mut ctx: PassContext) -> Result, GraphError> { 31 | Ok(Some(ctx.duration_back(Self::POSE_IN)?)) 32 | } 33 | 34 | fn pose_pass( 35 | &self, 36 | _time_update: TimeUpdate, 37 | mut _ctx: PassContext, 38 | ) -> Result, GraphError> { 39 | // let in_pose = ctx.pose_back(Self::POSE_IN, time_update)?; 40 | // Ok(Some(PoseFrame { 41 | // timestamp: in_pose.timestamp, 42 | // data: PoseFrameData::GlobalSpace(match &in_pose.data { 43 | // PoseFrameData::BoneSpace(data) => ctx.bone_to_global(data), 44 | // PoseFrameData::CharacterSpace(data) => ctx.character_to_global(data), 45 | // PoseFrameData::GlobalSpace(data) => data.clone(), 46 | // }), 47 | // })) 48 | todo!() 49 | } 50 | 51 | fn pose_input_spec(&self, _ctx: SpecContext) -> PinMap { 52 | [(Self::POSE_IN.into(), PoseSpec::Any)].into() 53 | } 54 | 55 | fn pose_output_spec(&self, _ctx: SpecContext) -> Option { 56 | Some(PoseSpec::GlobalSpace) 57 | } 58 | 59 | fn display_name(&self) -> String { 60 | "* → Global".into() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/state_machine/high_level/loader.rs: -------------------------------------------------------------------------------- 1 | use super::{GlobalTransition, State, StateMachine, Transition, serial::StateMachineSerial}; 2 | use crate::core::errors::AssetLoaderError; 3 | use bevy::asset::{AssetLoader, LoadContext, io::Reader}; 4 | 5 | #[derive(Default)] 6 | pub struct StateMachineLoader; 7 | 8 | impl AssetLoader for StateMachineLoader { 9 | type Asset = StateMachine; 10 | type Settings = (); 11 | type Error = AssetLoaderError; 12 | 13 | async fn load( 14 | &self, 15 | reader: &mut dyn Reader, 16 | _settings: &Self::Settings, 17 | load_context: &mut LoadContext<'_>, 18 | ) -> Result { 19 | let mut bytes = vec![]; 20 | reader.read_to_end(&mut bytes).await?; 21 | let serial: StateMachineSerial = ron::de::from_bytes(&bytes)?; 22 | let mut fsm = StateMachine { 23 | extra: serial.extra, 24 | input_data: serial.input_data, 25 | ..Default::default() 26 | }; 27 | 28 | for state_serial in serial.states { 29 | let global_transition_data = 30 | state_serial.global_transition.map(|gt| GlobalTransition { 31 | duration: gt.duration, 32 | graph: load_context.load(gt.graph), 33 | }); 34 | fsm.add_state(State { 35 | id: state_serial.id, 36 | graph: load_context.load(state_serial.graph), 37 | global_transition: global_transition_data, 38 | }); 39 | } 40 | 41 | for transition_serial in serial.transitions { 42 | fsm.add_transition(Transition { 43 | id: transition_serial.id, 44 | source: transition_serial.source, 45 | target: transition_serial.target, 46 | duration: transition_serial.duration, 47 | graph: load_context.load(transition_serial.graph), 48 | }); 49 | } 50 | 51 | fsm.set_start_state(serial.start_state); 52 | 53 | fsm.update_low_level_fsm(); 54 | 55 | Ok(fsm) 56 | } 57 | 58 | fn extensions(&self) -> &[&str] { 59 | &["fsm.ron"] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/space_conversion/into_character.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{ 3 | animation_graph::{PinMap, TimeUpdate}, 4 | animation_node::{AnimationNode, AnimationNodeType, NodeLike}, 5 | duration_data::DurationData, 6 | errors::GraphError, 7 | pose::{Pose, PoseSpec}, 8 | }, 9 | prelude::{PassContext, SpecContext}, 10 | }; 11 | use bevy::reflect::{std_traits::ReflectDefault, Reflect}; 12 | 13 | #[derive(Reflect, Clone, Debug, Default)] 14 | #[reflect(Default, NodeLike)] 15 | pub struct IntoCharacterSpaceNode {} 16 | 17 | impl IntoCharacterSpaceNode { 18 | pub const POSE_IN: &'static str = "pose_in"; 19 | 20 | pub fn new() -> Self { 21 | Self {} 22 | } 23 | 24 | pub fn wrapped(self, name: impl Into) -> AnimationNode { 25 | AnimationNode::new_from_nodetype(name.into(), AnimationNodeType::IntoCharacterSpace(self)) 26 | } 27 | } 28 | 29 | impl NodeLike for IntoCharacterSpaceNode { 30 | fn duration(&self, mut ctx: PassContext) -> Result, GraphError> { 31 | Ok(Some(ctx.duration_back(Self::POSE_IN)?)) 32 | } 33 | 34 | fn pose_pass( 35 | &self, 36 | _time_update: TimeUpdate, 37 | mut _ctx: PassContext, 38 | ) -> Result, GraphError> { 39 | // let in_pose = ctx.pose_back(Self::POSE_IN, time_update)?; 40 | // Ok(Some(PoseFrame { 41 | // timestamp: in_pose.timestamp, 42 | // data: PoseFrameData::CharacterSpace(match &in_pose.data { 43 | // PoseFrameData::BoneSpace(data) => ctx.bone_to_character(data), 44 | // PoseFrameData::CharacterSpace(data) => data.clone(), 45 | // PoseFrameData::GlobalSpace(data) => ctx.global_to_character(data), 46 | // }), 47 | // })) 48 | todo!() 49 | } 50 | 51 | fn pose_input_spec(&self, _ctx: SpecContext) -> PinMap { 52 | [(Self::POSE_IN.into(), PoseSpec::Any)].into() 53 | } 54 | 55 | fn pose_output_spec(&self, _ctx: SpecContext) -> Option { 56 | Some(PoseSpec::CharacterSpace) 57 | } 58 | 59 | fn display_name(&self) -> String { 60 | "* → Character".into() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/errors/graph_error.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::{NodeId, SourcePin, TargetPin}; 2 | use bevy::prelude::*; 3 | use thiserror::Error; 4 | 5 | /// Possible errors that can be produced by graph evaluation 6 | #[non_exhaustive] 7 | #[derive(Debug, Error, Reflect, Clone)] 8 | pub enum GraphError { 9 | #[error("Expected an edge connected to the target {0:?}")] 10 | MissingEdgeToTarget(TargetPin), 11 | #[error("Expected an edge connected to the source {0:?}")] 12 | MissingEdgeToSource(SourcePin), 13 | #[error("Node update did not produce output for {0:?}")] 14 | OutputMissing(SourcePin), 15 | #[error("Node update did not produce output for {0:?}")] 16 | DurationMissing(SourcePin), 17 | #[error("Time update requested is not cached: {0:?}")] 18 | TimeUpdateMissingBack(TargetPin), 19 | #[error("Time update requested is not cached: {0:?}")] 20 | TimeUpdateMissingFwd(SourcePin), 21 | #[error("A parent graph was requested but none is present")] 22 | MissingParentGraph, 23 | #[error("Graph requested data from FSM transition, but it is assigned to a state")] 24 | FSMExpectedTransitionFoundState, 25 | #[error("FSM sub-graph requested data that isn't available")] 26 | FSMRequestedMissingData, 27 | #[error( 28 | "The current state id does not match any known states, perhaps you deleted a state while the state machine was running?" 29 | )] 30 | FSMCurrentStateMissing, 31 | #[error("The asset for a state's graph is missing.")] 32 | FSMGraphAssetMissing, 33 | #[error("We're missing a skeleton asset at: {0:?}")] 34 | SkeletonMissing(NodeId), 35 | #[error("The requested animation clip is not found.")] 36 | ClipMissing, 37 | #[error( 38 | "Failed to update time. Possibly the requested event track does not exist in a given clip." 39 | )] 40 | TimeUpdateFailed, 41 | #[error("Tried to convert to incorrect data type: expected {0}, got {1}")] 42 | MismatchedDataType(String, String), 43 | #[error("Tried to get node state of the wrong type")] 44 | MismatchedStateType, 45 | #[error("State value not found for the given parameters")] 46 | MissingStateValue, 47 | } 48 | 49 | pub type GraphResult = Result; 50 | -------------------------------------------------------------------------------- /examples/retargeting/examples/retargeting.rs: -------------------------------------------------------------------------------- 1 | extern crate bevy; 2 | extern crate bevy_animation_graph; 3 | 4 | use bevy::{light::CascadeShadowConfigBuilder, prelude::*}; 5 | use bevy_animation_graph::prelude::*; 6 | use std::f32::consts::PI; 7 | 8 | fn main() { 9 | App::new() 10 | .add_plugins(DefaultPlugins.set(AssetPlugin { 11 | file_path: "../../assets".to_string(), 12 | ..default() 13 | })) 14 | .add_plugins(AnimationGraphPlugin::default()) 15 | .insert_resource(AmbientLight { 16 | color: Color::WHITE, 17 | brightness: 0.1, 18 | ..default() 19 | }) 20 | .add_systems(Startup, setup) 21 | .run(); 22 | } 23 | 24 | fn setup( 25 | mut commands: Commands, 26 | asset_server: Res, 27 | mut meshes: ResMut>, 28 | mut materials: ResMut>, 29 | ) { 30 | // Camera 31 | commands.spawn(( 32 | Camera3d::default(), 33 | Transform::from_xyz(9., 9., 9.).looking_at(Vec3::new(0.0, 3., 0.0), Vec3::Y), 34 | )); 35 | 36 | // Plane 37 | commands.spawn(( 38 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::new(5., 5.)))), 39 | MeshMaterial3d(materials.add(Color::from(LinearRgba::rgb(0.3, 0.5, 0.3)))), 40 | )); 41 | 42 | // Light 43 | commands.spawn(( 44 | Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), 45 | DirectionalLight { 46 | shadows_enabled: true, 47 | ..default() 48 | }, 49 | CascadeShadowConfigBuilder { 50 | first_cascade_far_bound: 10.0, 51 | num_cascades: 3, 52 | minimum_distance: 0.3, 53 | maximum_distance: 100.0, 54 | ..default() 55 | } 56 | .build(), 57 | )); 58 | 59 | // Animated scene without retargeting required 60 | commands.spawn(( 61 | AnimatedSceneHandle::new(asset_server.load("animated_scenes/snake_a.animscn.ron")), 62 | Transform::from_xyz(-2., 0., 0.), 63 | )); 64 | 65 | // Animated scene with retargeting applied 66 | // The retargeting is configured in the scene asset file 67 | commands.spawn(( 68 | AnimatedSceneHandle::new(asset_server.load("animated_scenes/snake_b.animscn.ron")), 69 | Transform::from_xyz(2., 0., 0.), 70 | )); 71 | } 72 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/isometry3d.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::{Isometry3d, Vec3}; 2 | 3 | use crate::ui::generic_widgets::{quat::QuatWidget, vec3::Vec3Widget}; 4 | 5 | pub struct Isometry3dWidget<'a> { 6 | pub isometry: &'a mut Isometry3d, 7 | pub slider_step_size: f32, 8 | pub id_hash: egui::Id, 9 | pub flatten_grid: bool, 10 | } 11 | 12 | impl<'a> Isometry3dWidget<'a> { 13 | pub fn new_salted(isometry: &'a mut Isometry3d, salt: impl std::hash::Hash) -> Self { 14 | Self { 15 | isometry, 16 | slider_step_size: 0.1, 17 | id_hash: egui::Id::new(salt), 18 | flatten_grid: false, 19 | } 20 | } 21 | 22 | pub fn with_step_size(mut self, step_size: f32) -> Self { 23 | self.slider_step_size = step_size; 24 | self 25 | } 26 | 27 | pub fn with_flatten_grid(mut self, flatten_grid: bool) -> Self { 28 | self.flatten_grid = flatten_grid; 29 | self 30 | } 31 | } 32 | 33 | impl<'a> egui::Widget for Isometry3dWidget<'a> { 34 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 35 | let mut draw = |ui: &mut egui::Ui| { 36 | let translation_label_response = ui.label("translation:"); 37 | let mut translation: Vec3 = self.isometry.translation.into(); 38 | let translation_response = ui.add( 39 | Vec3Widget::new_salted(&mut translation, self.id_hash.with("translation")) 40 | .with_step_size(self.slider_step_size) 41 | .with_width(200.), 42 | ); 43 | self.isometry.translation = translation.into(); 44 | ui.end_row(); 45 | 46 | let rotation_label_response = ui.label("rotation:"); 47 | let rotation_response = ui.add( 48 | QuatWidget::new_salted(&mut self.isometry.rotation, self.id_hash.with("rotation")) 49 | .with_step_size(self.slider_step_size) 50 | .with_width(200.), 51 | ); 52 | ui.end_row(); 53 | 54 | translation_label_response 55 | | translation_response 56 | | rotation_label_response 57 | | rotation_response 58 | }; 59 | 60 | if self.flatten_grid { 61 | draw(ui) 62 | } else { 63 | egui::Grid::new(self.id_hash).show(ui, draw).inner 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/ragdoll_editor/collider_inspector.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::world::World; 2 | use bevy_animation_graph::core::ragdoll::definition::Collider; 3 | use egui::Widget; 4 | 5 | use crate::ui::{ 6 | generic_widgets::{isometry3d::Isometry3dWidget, u32_flags::U32Flags}, 7 | utils::using_inspector_env, 8 | }; 9 | 10 | pub struct ColliderInspector<'a> { 11 | pub world: &'a mut World, 12 | pub collider: &'a mut Collider, 13 | } 14 | 15 | impl Widget for ColliderInspector<'_> { 16 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 17 | let response = egui::Grid::new("ragdoll body inspector").show(ui, |ui| { 18 | let mut response = ui.label("ID:"); 19 | response |= ui.label(format!("{:?}", self.collider.id)); 20 | ui.end_row(); 21 | 22 | response |= ui.label("label:"); 23 | response |= ui.text_edit_singleline(&mut self.collider.label); 24 | ui.end_row(); 25 | 26 | response |= ui.add( 27 | Isometry3dWidget::new_salted(&mut self.collider.local_offset, "isometry") 28 | .with_step_size(0.01) 29 | .with_flatten_grid(true), 30 | ); 31 | 32 | response |= ui.label("layer memberships:"); 33 | response |= ui.add(U32Flags::new_salted( 34 | &mut self.collider.layer_membership, 35 | "layer memberships", 36 | )); 37 | ui.end_row(); 38 | 39 | response |= ui.label("layer filters:"); 40 | response |= ui.add(U32Flags::new_salted( 41 | &mut self.collider.layer_filter, 42 | "layer filters", 43 | )); 44 | ui.end_row(); 45 | 46 | response |= ui.label("override layers:"); 47 | response |= ui.add(egui::Checkbox::without_text( 48 | &mut self.collider.override_layers, 49 | )); 50 | ui.end_row(); 51 | 52 | // TODO: Figure out a better way to handle enum UIs 53 | response |= ui.label("shape:"); 54 | let shape_changed = using_inspector_env(self.world, |mut env| { 55 | env.ui_for_reflect(&mut self.collider.shape, ui) 56 | }); 57 | if shape_changed { 58 | response.mark_changed(); 59 | } 60 | 61 | response 62 | }); 63 | 64 | response.inner 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/asset_picker.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{Asset, AssetId, AssetServer, Assets, Handle}, 3 | ecs::world::World, 4 | }; 5 | 6 | use crate::ui::{ 7 | generic_widgets::tree::{Tree, TreeWidget, paths::PathTreeRenderer}, 8 | utils::{asset_sort_key, handle_path}, 9 | }; 10 | 11 | pub struct AssetPicker<'a, A: Asset> { 12 | pub handle: &'a mut Handle, 13 | pub id_hash: egui::Id, 14 | pub world: &'a mut World, 15 | } 16 | 17 | impl<'a, A: Asset> AssetPicker<'a, A> { 18 | pub fn new_salted( 19 | handle: &'a mut Handle, 20 | world: &'a mut World, 21 | salt: impl std::hash::Hash, 22 | ) -> Self { 23 | Self { 24 | handle, 25 | id_hash: egui::Id::new(salt), 26 | world, 27 | } 28 | } 29 | } 30 | 31 | impl<'a, A: Asset> egui::Widget for AssetPicker<'a, A> { 32 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 33 | ui.push_id(self.id_hash, |ui| { 34 | self.world 35 | .resource_scope::, _>(|world, mut assets| { 36 | let asset_server = world.resource::(); 37 | let mut asset_ids: Vec<_> = assets.ids().collect(); 38 | asset_ids.sort_by_key(|id| asset_sort_key(*id, asset_server)); 39 | let paths = asset_ids 40 | .into_iter() 41 | .map(|id| (handle_path(id.untyped(), asset_server), id)) 42 | .collect(); 43 | let path_tree = Tree::from_paths(paths); 44 | TreeWidget::new_salted( 45 | path_tree, 46 | PathTreeRenderer { 47 | is_selected: { 48 | let selected_id = self.handle.id(); 49 | Box::new(move |_, v: &AssetId| *v == selected_id) 50 | }, 51 | }, 52 | "asset picker tree", 53 | ) 54 | .show(ui, |result| { 55 | if let Some(id) = result.clicked 56 | && let Some(handle) = assets.get_strong_handle(id) 57 | { 58 | *self.handle = handle; 59 | } 60 | }) 61 | }) 62 | }) 63 | .inner 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /assets/animation_graphs/walk_to_run.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "Done", 5 | inner: { 6 | "bevy_animation_graph::nodes::arithmetic::event_queue::fire_event::FireEventNode": ( 7 | event: EndTransition, 8 | ), 9 | }, 10 | ), 11 | ( 12 | name: "Blend", 13 | inner: { 14 | "bevy_animation_graph::nodes::blend_node::BlendNode": (), 15 | }, 16 | ), 17 | ( 18 | name: "Is it done?", 19 | inner: { 20 | "bevy_animation_graph::nodes::arithmetic::f32::compare_f32::CompareF32": ( 21 | op: MoreEqual, 22 | ), 23 | }, 24 | ), 25 | ], 26 | edges_inverted: { 27 | NodeData("Is it done?", "in_b"): InputData("1"), 28 | NodeTime("Blend", "time_a"): InputTime("source time"), 29 | OutputData("pose"): NodeData("Blend", "pose"), 30 | NodeData("Done", "condition"): NodeData("Is it done?", "out"), 31 | NodeData("Is it done?", "in_a"): InputData("elapsed percent"), 32 | NodeTime("Blend", "time_b"): InputTime("target time"), 33 | OutputTime: NodeTime("Blend"), 34 | NodeData("Blend", "pose_b"): InputData("target pose"), 35 | NodeData("Blend", "factor"): InputData("elapsed percent"), 36 | NodeData("Blend", "pose_a"): InputData("source pose"), 37 | OutputData("driver events"): NodeData("Done", "event"), 38 | }, 39 | default_parameters: { 40 | "blend": F32(0.2), 41 | "speed": F32(5.0), 42 | "elapsed percent": F32(0.0), 43 | "1": F32(1.0), 44 | "source pose": Pose(( 45 | bones: [], 46 | paths: {}, 47 | timestamp: 0.0, 48 | )), 49 | "target pose": Pose(( 50 | bones: [], 51 | paths: {}, 52 | timestamp: 0.0, 53 | )), 54 | }, 55 | input_times: { 56 | "source time": (), 57 | "target time": (), 58 | }, 59 | output_parameters: { 60 | "driver events": EventQueue, 61 | "pose": Pose, 62 | }, 63 | output_time: Some(()), 64 | extra: ( 65 | node_positions: { 66 | "Blend": (553.0, 308.0), 67 | "Done": (592.0, 167.0), 68 | "Is it done?": (382.0, 156.0), 69 | }, 70 | input_position: (167.0, 156.0), 71 | output_position: (877.0, 329.0), 72 | ), 73 | ) 74 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/edge_data/events.rs: -------------------------------------------------------------------------------- 1 | use bevy::reflect::{Reflect, std_traits::ReflectDefault}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::core::state_machine::high_level::{StateId, TransitionId}; 5 | 6 | /// Event data 7 | #[derive(Clone, Debug, Reflect, Serialize, Deserialize, PartialEq, Hash)] 8 | #[reflect(Default)] 9 | pub enum AnimationEvent { 10 | /// Trigger the most specific transition from transitioning into the provided state. That 11 | /// will be: 12 | /// * A direct transition, if present, or 13 | /// * A global transition, if present 14 | TransitionToState(StateId), 15 | /// Trigger a specific transition (if possible) 16 | Transition(TransitionId), 17 | EndTransition, 18 | StringId(String), 19 | } 20 | 21 | impl Default for AnimationEvent { 22 | fn default() -> Self { 23 | Self::StringId("".to_string()) 24 | } 25 | } 26 | 27 | /// Structure containing a sampled event and relevant metadata 28 | #[derive(Clone, Debug, Reflect, Serialize, Deserialize)] 29 | #[reflect(Default)] 30 | pub struct SampledEvent { 31 | /// Event that was sampled 32 | pub event: AnimationEvent, 33 | /// Weight of event (is reduced by blending, for example), 0.0 to 1.0 34 | pub weight: f32, 35 | /// Percentage of total event duration at sampling time, 0.0 to 1.0 36 | pub percentage: f32, 37 | /// If the event comes from a markup track, contains the track id 38 | pub track: Option, 39 | } 40 | 41 | impl Default for SampledEvent { 42 | fn default() -> Self { 43 | Self { 44 | event: AnimationEvent::default(), 45 | weight: 1., 46 | percentage: 1., 47 | track: None, 48 | } 49 | } 50 | } 51 | 52 | impl SampledEvent { 53 | pub fn instant(event: AnimationEvent) -> Self { 54 | Self { 55 | event, 56 | weight: 1., 57 | percentage: 1., 58 | track: None, 59 | } 60 | } 61 | } 62 | 63 | /// Sequence of events 64 | #[derive(Clone, Debug, Reflect, Serialize, Deserialize, Default)] 65 | #[reflect(Default)] 66 | pub struct EventQueue { 67 | pub events: Vec, 68 | } 69 | 70 | impl EventQueue { 71 | pub fn with_events(events: impl Into>) -> Self { 72 | Self { 73 | events: events.into(), 74 | } 75 | } 76 | 77 | pub fn concat(mut self, other: EventQueue) -> Self { 78 | self.events.extend(other.events); 79 | self 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | # Run cargo test 14 | test: 15 | name: Test Suite 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v4 21 | - name: Cache 22 | uses: actions/cache@v3 23 | with: 24 | path: | 25 | ~/.cargo/bin/ 26 | ~/.cargo/registry/index/ 27 | ~/.cargo/registry/cache/ 28 | ~/.cargo/git/db/ 29 | target/ 30 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 31 | - name: Install stable toolchain 32 | uses: dtolnay/rust-toolchain@stable 33 | - name: Install Dependencies 34 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev 35 | - name: Run cargo test 36 | # set parallel jobs to 1 to reduce chance of github killing it 37 | run: cargo test -j 1 38 | 39 | # Run cargo clippy -- -D warnings 40 | clippy_check: 41 | name: Clippy 42 | runs-on: ubuntu-latest 43 | timeout-minutes: 30 44 | steps: 45 | - name: Checkout sources 46 | uses: actions/checkout@v4 47 | - name: Cache 48 | uses: actions/cache@v3 49 | with: 50 | path: | 51 | ~/.cargo/bin/ 52 | ~/.cargo/registry/index/ 53 | ~/.cargo/registry/cache/ 54 | ~/.cargo/git/db/ 55 | target/ 56 | key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.toml') }} 57 | - name: Install stable toolchain 58 | uses: dtolnay/rust-toolchain@stable 59 | with: 60 | components: clippy 61 | - name: Install Dependencies 62 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev 63 | - name: Run clippy 64 | run: cargo clippy -- -D warnings 65 | 66 | # Run cargo fmt --all -- --check 67 | format: 68 | name: Format 69 | runs-on: ubuntu-latest 70 | timeout-minutes: 30 71 | steps: 72 | - name: Checkout sources 73 | uses: actions/checkout@v4 74 | - name: Install stable toolchain 75 | uses: dtolnay/rust-toolchain@stable 76 | with: 77 | components: rustfmt 78 | - name: Run cargo fmt 79 | run: cargo fmt --all -- --check 80 | -------------------------------------------------------------------------------- /assets/animation_graphs/human_ragdoll.animgraph.ron: -------------------------------------------------------------------------------- 1 | ( 2 | nodes: [ 3 | ( 4 | name: "ragdoll config", 5 | inner: { 6 | "bevy_animation_graph::nodes::const_ragdoll_config::ConstRagdollConfig": ( 7 | value: ( 8 | default_mode: None, 9 | mode_overrides: {}, 10 | default_readback: None, 11 | readback_overrides: {}, 12 | ), 13 | ), 14 | }, 15 | ), 16 | ( 17 | name: "locomotion", 18 | inner: { 19 | "bevy_animation_graph::nodes::graph_node::GraphNode": ( 20 | graph: "animation_graphs/human_new.animgraph.ron", 21 | ), 22 | }, 23 | ), 24 | ], 25 | edges_inverted: { 26 | OutputData("pose"): NodeData("locomotion", "pose"), 27 | NodeData("locomotion", "target_speed"): InputData("target_speed"), 28 | OutputData("ragdoll_config"): NodeData("ragdoll config", "out"), 29 | OutputTime: NodeTime("locomotion"), 30 | NodeData("locomotion", "rotation_bone"): InputData("rotation_bone"), 31 | NodeData("locomotion", "Z"): InputData("Z"), 32 | NodeData("locomotion", "1"): InputData("1"), 33 | NodeData("locomotion", "target_direction"): InputData("target_direction"), 34 | }, 35 | default_parameters: { 36 | "rotation_bone": EntityPath([ 37 | "metarig", 38 | "spine", 39 | ]), 40 | "Z": Vec3((0.0, 0.0, 1.0)), 41 | "target_direction": Vec3((1.0, 0.0, 0.0)), 42 | "target_speed": F32(0.0), 43 | "1": F32(1.0), 44 | }, 45 | input_times: {}, 46 | output_parameters: { 47 | "ragdoll_config": RagdollConfig, 48 | "pose": Pose, 49 | }, 50 | output_time: Some(()), 51 | extra: ( 52 | node_positions: { 53 | "ragdoll config": (44.128906, 327.39453), 54 | "locomotion": (43.117188, -5.1875), 55 | }, 56 | input_position: (-280.8789, -4.265625), 57 | output_position: (368.76953, 156.24219), 58 | input_param_order: { 59 | "rotation_bone": 2, 60 | "Z": 1, 61 | "target_direction": 0, 62 | "target_speed": 3, 63 | "1": 4, 64 | }, 65 | input_time_order: {}, 66 | output_data_order: { 67 | "ragdoll_config": 1, 68 | "pose": 0, 69 | }, 70 | output_pose_order: {}, 71 | ), 72 | ) -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/ragdoll_editor/bone_mapping_inspector.rs: -------------------------------------------------------------------------------- 1 | use bevy_animation_graph::core::ragdoll::{bone_mapping::BoneMapping, definition::Ragdoll}; 2 | use egui::Widget; 3 | 4 | use crate::ui::generic_widgets::{ 5 | body_id::BodyIdWidget, isometry3d::Isometry3dWidget, list::ListWidget, 6 | }; 7 | 8 | pub struct BoneMappingInspector<'a> { 9 | pub bone: &'a mut BoneMapping, 10 | pub ragdoll: &'a Ragdoll, 11 | } 12 | 13 | impl Widget for BoneMappingInspector<'_> { 14 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 15 | ui.label(format!("ID: {:?}", self.bone.bone_id.id())); 16 | ui.label(format!("Path: {}", self.bone.bone_id.to_slashed_string())); 17 | ListWidget::new_salted(&mut self.bone.bodies, "body weight list").ui( 18 | ui, 19 | |ui, body_weight| { 20 | egui::Grid::new("body weight grid") 21 | .show(ui, |ui| { 22 | let mut response = ui.label("body ID:"); 23 | response |= ui.add( 24 | BodyIdWidget::new_salted(&mut body_weight.body, "body id picker") 25 | .with_ragdoll(Some(self.ragdoll)), 26 | ); 27 | ui.end_row(); 28 | 29 | response |= ui.label("weight:"); 30 | response |= ui.add(egui::DragValue::new(&mut body_weight.weight)); 31 | ui.end_row(); 32 | 33 | response |= ui 34 | .label("override offset?:") 35 | .on_hover_text(include_str!("../../../tooltips/override_offset.txt")); 36 | response |= ui.add(egui::Checkbox::without_text( 37 | &mut body_weight.override_offset, 38 | )); 39 | ui.end_row(); 40 | 41 | if body_weight.override_offset { 42 | response |= ui.add( 43 | Isometry3dWidget::new_salted( 44 | &mut body_weight.offset, 45 | "offset from body", 46 | ) 47 | .with_flatten_grid(true), 48 | ); 49 | 50 | ui.end_row(); 51 | } 52 | 53 | response 54 | }) 55 | .inner 56 | }, 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/scanner.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use bevy::{asset::LoadedUntypedAsset, platform::collections::HashSet, prelude::*}; 3 | use std::{ 4 | fs, io, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | pub struct ScannerPlugin; 9 | impl Plugin for ScannerPlugin { 10 | fn build(&self, app: &mut bevy::prelude::App) { 11 | app.insert_resource(PersistedAssetHandles { 12 | loaded_paths: HashSet::default(), 13 | }) 14 | .add_message::() 15 | .add_systems(Startup, core_setup) 16 | .add_systems(Update, asset_reload); 17 | } 18 | } 19 | 20 | /// Keeps a handle to the folder so that it does not get unloaded 21 | #[derive(Resource)] 22 | pub struct PersistedAssetHandles { 23 | #[allow(dead_code)] 24 | pub loaded_paths: HashSet>, 25 | } 26 | 27 | #[derive(Message)] 28 | pub struct RescanAssets; 29 | 30 | pub fn core_setup( 31 | mut evw_rescan_events: MessageWriter, 32 | mut gizmo_config: ResMut, 33 | ) { 34 | evw_rescan_events.write(RescanAssets); 35 | 36 | let config = gizmo_config.config_mut::().0; 37 | config.depth_bias = -1.; 38 | } 39 | 40 | pub fn asset_reload( 41 | mut reload_events: MessageReader, 42 | asset_server: Res, 43 | mut persisted_asset_handles: ResMut, 44 | cli: Res, 45 | ) { 46 | if reload_events.read().next().is_some() { 47 | visit_dirs(&cli.asset_source, &mut |path| { 48 | let relative_path = path.strip_prefix(&cli.asset_source).unwrap().to_owned(); 49 | let loaded = asset_server.load_untyped(relative_path); 50 | persisted_asset_handles.loaded_paths.insert(loaded); 51 | }) 52 | .unwrap_or_else(|err| { 53 | panic!( 54 | "Failed to load asset path {:?}: {:?}", 55 | cli.asset_source, err 56 | ) 57 | }); 58 | } 59 | } 60 | 61 | // one possible implementation of walking a directory only visiting files 62 | // taken from https://doc.rust-lang.org/nightly/std/fs/fn.read_dir.html#examples 63 | fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(PathBuf)) -> io::Result<()> { 64 | if dir.is_dir() { 65 | for entry in fs::read_dir(dir)? { 66 | let entry = entry?; 67 | let path = entry.path(); 68 | if path.is_dir() { 69 | visit_dirs(&path, cb)?; 70 | } else { 71 | info!("Loading {path:?}"); 72 | cb(path); 73 | } 74 | } 75 | } 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/utils/collapsing.rs: -------------------------------------------------------------------------------- 1 | pub struct Collapser { 2 | pub default_open: bool, 3 | pub id_salt: egui::Id, 4 | } 5 | 6 | pub struct CollapserResponse { 7 | pub head: H, 8 | pub body: Option, 9 | } 10 | 11 | impl Collapser { 12 | pub fn new() -> Self { 13 | Self { 14 | default_open: false, 15 | id_salt: egui::Id::new(0), 16 | } 17 | } 18 | 19 | pub fn with_default_open(mut self, default_open: bool) -> Self { 20 | self.default_open = default_open; 21 | self 22 | } 23 | 24 | pub fn with_id_salt(mut self, salt: impl std::hash::Hash) -> Self { 25 | self.id_salt = egui::Id::new(salt); 26 | self 27 | } 28 | 29 | pub fn show( 30 | self, 31 | ui: &mut egui::Ui, 32 | show_header: impl FnOnce(&mut egui::Ui) -> H, 33 | show_body: impl FnOnce(&mut egui::Ui) -> B, 34 | ) -> CollapserResponse { 35 | let id = ui.id().with("Collapser").with(self.id_salt); 36 | let mut is_open = ui.memory_mut(|mem| *mem.data.get_temp_mut_or(id, self.default_open)); 37 | 38 | let response = ui 39 | .vertical(|ui| { 40 | let header_response = ui 41 | .horizontal(|ui| { 42 | let label = if is_open { "⏷" } else { "⏵" }; 43 | if ui.add(egui::Button::new(label).frame(false)).clicked() { 44 | is_open = !is_open; 45 | } 46 | show_header(ui) 47 | }) 48 | .inner; 49 | let body_response = if is_open { 50 | let response = ui.horizontal(|ui| { 51 | ui.add_space(12.); 52 | ui.vertical(|ui| show_body(ui)).inner 53 | }); 54 | let rect = response.response.rect; 55 | let offset = egui::Vec2::new(5., 0.); 56 | ui.painter().line_segment( 57 | [rect.left_top() + offset, rect.left_bottom() + offset], 58 | ui.visuals().widgets.noninteractive.bg_stroke, 59 | ); 60 | Some(response.inner) 61 | } else { 62 | None 63 | }; 64 | CollapserResponse { 65 | head: header_response, 66 | body: body_response, 67 | } 68 | }) 69 | .inner; 70 | ui.memory_mut(|mem| mem.data.insert_temp(id, is_open)); 71 | response 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/generic_widgets/vec3.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::Vec3; 2 | 3 | pub struct Vec3Widget<'a> { 4 | pub vec3: &'a mut Vec3, 5 | pub slider_step_size: f32, 6 | pub id_hash: egui::Id, 7 | pub width: f32, 8 | } 9 | 10 | impl<'a> Vec3Widget<'a> { 11 | pub fn new_salted(vec3: &'a mut Vec3, salt: impl std::hash::Hash) -> Self { 12 | Self { 13 | vec3, 14 | slider_step_size: 0.01, 15 | id_hash: egui::Id::new(salt), 16 | width: 300., 17 | } 18 | } 19 | 20 | pub fn with_step_size(mut self, step_size: f32) -> Self { 21 | self.slider_step_size = step_size; 22 | self 23 | } 24 | 25 | pub fn with_width(mut self, width: f32) -> Self { 26 | self.width = width; 27 | self 28 | } 29 | } 30 | 31 | impl<'a> egui::Widget for Vec3Widget<'a> { 32 | fn ui(self, ui: &mut egui::Ui) -> egui::Response { 33 | ui.push_id(self.id_hash, |ui| { 34 | let mut total_size = ui.available_size(); 35 | total_size.x = self.width; 36 | ui.horizontal(|ui| { 37 | let x_id = ui.id().with(self.id_hash).with("vec3 x"); 38 | let y_id = ui.id().with(self.id_hash).with("vec3 y"); 39 | let z_id = ui.id().with(self.id_hash).with("vec3 z"); 40 | 41 | let x_response = ui 42 | .push_id(x_id, |ui| { 43 | ui.add_sized( 44 | egui::Vec2::new(total_size.x / 3.1, total_size.y), 45 | egui::DragValue::new(&mut self.vec3.x).speed(self.slider_step_size), 46 | ) 47 | }) 48 | .inner; 49 | let y_response = ui 50 | .push_id(y_id, |ui| { 51 | ui.add_sized( 52 | egui::Vec2::new(total_size.x / 3.1, total_size.y), 53 | egui::DragValue::new(&mut self.vec3.y).speed(self.slider_step_size), 54 | ) 55 | }) 56 | .inner; 57 | let z_response = ui 58 | .push_id(z_id, |ui| { 59 | ui.add_sized( 60 | egui::Vec2::new(total_size.x / 3.1, total_size.y), 61 | egui::DragValue::new(&mut self.vec3.z).speed(self.slider_step_size), 62 | ) 63 | }) 64 | .inner; 65 | x_response | y_response | z_response 66 | }) 67 | .inner 68 | }) 69 | .inner 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/animated_scene/loader.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{AssetLoader, AssetPath, Handle, LoadContext, io::Reader}, 3 | platform::collections::HashMap, 4 | scene::Scene, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{ 9 | core::{errors::AssetLoaderError, skeleton::Skeleton}, 10 | prelude::AnimationGraph, 11 | }; 12 | 13 | use super::{AnimatedScene, Retargeting}; 14 | 15 | #[derive(Serialize, Deserialize, Clone)] 16 | struct AnimatedSceneSerial { 17 | source: AssetPath<'static>, 18 | animation_graph: AssetPath<'static>, 19 | skeleton: AssetPath<'static>, 20 | #[serde(default)] 21 | retargeting: Option, 22 | 23 | #[serde(default)] 24 | colliders: Option>, 25 | #[serde(default)] 26 | ragdoll: Option>, 27 | #[serde(default)] 28 | ragdoll_bone_map: Option>, 29 | } 30 | 31 | #[derive(Clone, Serialize, Deserialize)] 32 | pub struct RetargetingSerial { 33 | source_skeleton: AssetPath<'static>, 34 | bone_path_overrides: HashMap, 35 | } 36 | 37 | #[derive(Default)] 38 | pub struct AnimatedSceneLoader; 39 | 40 | impl AssetLoader for AnimatedSceneLoader { 41 | type Asset = AnimatedScene; 42 | type Settings = (); 43 | type Error = AssetLoaderError; 44 | 45 | async fn load( 46 | &self, 47 | reader: &mut dyn Reader, 48 | _settings: &Self::Settings, 49 | load_context: &mut LoadContext<'_>, 50 | ) -> Result { 51 | let mut bytes = vec![]; 52 | reader.read_to_end(&mut bytes).await?; 53 | let serial: AnimatedSceneSerial = ron::de::from_bytes(&bytes)?; 54 | 55 | let animation_graph: Handle = load_context.load(serial.animation_graph); 56 | let skeleton: Handle = load_context.load(serial.skeleton); 57 | let source: Handle = load_context.load(serial.source); 58 | let retargeting: Option = serial.retargeting.map(|r| Retargeting { 59 | source_skeleton: load_context.load(r.source_skeleton), 60 | bone_path_overrides: r.bone_path_overrides, 61 | }); 62 | 63 | Ok(AnimatedScene { 64 | source, 65 | processed_scene: None, 66 | animation_graph, 67 | skeleton, 68 | retargeting, 69 | ragdoll: serial.ragdoll.map(|c| load_context.load(c)), 70 | ragdoll_bone_map: serial.ragdoll_bone_map.map(|c| load_context.load(c)), 71 | }) 72 | } 73 | 74 | fn extensions(&self) -> &[&str] { 75 | &["animscn.ron"] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/nodes/speed_node.rs: -------------------------------------------------------------------------------- 1 | use crate::core::animation_graph::{PinMap, TimeUpdate}; 2 | use crate::core::animation_node::{NodeLike, ReflectNodeLike}; 3 | use crate::core::errors::GraphError; 4 | use crate::core::prelude::DataSpec; 5 | use crate::prelude::{PassContext, SpecContext}; 6 | use bevy::reflect::Reflect; 7 | use bevy::reflect::std_traits::ReflectDefault; 8 | 9 | #[derive(Reflect, Clone, Debug, Default)] 10 | #[reflect(Default, NodeLike)] 11 | pub struct SpeedNode; 12 | 13 | impl SpeedNode { 14 | pub const IN_POSE: &'static str = "pose"; 15 | pub const IN_TIME: &'static str = "time"; 16 | pub const OUT_POSE: &'static str = "pose"; 17 | pub const SPEED: &'static str = "speed"; 18 | 19 | pub fn new() -> Self { 20 | Self 21 | } 22 | } 23 | 24 | impl NodeLike for SpeedNode { 25 | fn duration(&self, mut ctx: PassContext) -> Result<(), GraphError> { 26 | let speed = ctx.data_back(Self::SPEED)?.as_f32()?; 27 | let out_duration = if speed == 0. { 28 | None 29 | } else { 30 | let duration = ctx.duration_back(Self::IN_TIME)?; 31 | duration.as_ref().map(|duration| duration / speed) 32 | }; 33 | ctx.set_duration_fwd(out_duration); 34 | Ok(()) 35 | } 36 | 37 | fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> { 38 | let speed = ctx.data_back(Self::SPEED)?.as_f32()?; 39 | let input = ctx.time_update_fwd()?; 40 | 41 | let fw_upd = match input { 42 | TimeUpdate::Delta(dt) => TimeUpdate::Delta(dt * speed), 43 | // TODO: add warnings if input is not delta 44 | other => other, 45 | }; 46 | 47 | ctx.set_time_update_back(Self::IN_TIME, fw_upd); 48 | let mut in_pose = ctx.data_back(Self::IN_POSE)?.into_pose()?; 49 | 50 | if speed != 0. { 51 | in_pose.timestamp /= speed.abs(); 52 | } 53 | 54 | ctx.set_time(in_pose.timestamp); 55 | ctx.set_data_fwd(Self::OUT_POSE, in_pose); 56 | 57 | Ok(()) 58 | } 59 | 60 | fn time_input_spec(&self, _: SpecContext) -> PinMap<()> { 61 | [(Self::IN_TIME.into(), ())].into() 62 | } 63 | 64 | fn time_output_spec(&self, _ctx: SpecContext) -> Option<()> { 65 | Some(()) 66 | } 67 | 68 | fn data_input_spec(&self, _: SpecContext) -> PinMap { 69 | [ 70 | (Self::SPEED.into(), DataSpec::F32), 71 | (Self::IN_POSE.into(), DataSpec::Pose), 72 | ] 73 | .into() 74 | } 75 | 76 | fn data_output_spec(&self, _: SpecContext) -> PinMap { 77 | [(Self::OUT_POSE.into(), DataSpec::Pose)].into() 78 | } 79 | 80 | fn display_name(&self) -> String { 81 | "⌚ Speed".into() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/egui_nodes/pin.rs: -------------------------------------------------------------------------------- 1 | use bevy_egui::egui; 2 | use bevy_inspector_egui::bevy_egui; 3 | use derivative::Derivative; 4 | 5 | #[derive(Default, Debug, Clone)] 6 | /// The Visual Style of a Link. 7 | /// If feilds are None then the Context style is used. 8 | /// shape defualts to CircleFilled 9 | pub struct PinStyleArgs { 10 | pub background: Option, 11 | pub hovered: Option, 12 | pub shape: Option, 13 | } 14 | 15 | #[derive(PartialEq, Clone, Copy, Debug, Default)] 16 | pub(crate) enum PinType { 17 | #[default] 18 | None, 19 | Input, 20 | Output, 21 | } 22 | 23 | /// Controls the shape of an attribut pin. 24 | /// Triangle and TriangleFilled are not currently implemented and will not be drawn 25 | #[derive(Clone, Copy, Debug, Default)] 26 | #[allow(dead_code)] 27 | pub enum PinShape { 28 | Circle, 29 | #[default] 30 | CircleFilled, 31 | Triangle, 32 | TriangleFilled, 33 | Quad, 34 | QuadFilled, 35 | } 36 | 37 | /// Controls the way that attribute pins behave 38 | #[derive(Debug)] 39 | pub enum AttributeFlags { 40 | None = 0, 41 | 42 | /// If there is a link on the node then it will detatch instead of creating a new one. 43 | /// Requires handling of deleted links via Context::link_destroyed 44 | EnableLinkDetachWithDragClick = 1 << 0, 45 | 46 | /// Visual snapping will trigger link creation / destruction 47 | EnableLinkCreationOnSnap = 1 << 1, 48 | } 49 | 50 | #[derive(Default, Debug, Clone)] 51 | pub(crate) struct PinStyle { 52 | pub background: egui::Color32, 53 | pub hovered: egui::Color32, 54 | pub shape: PinShape, 55 | } 56 | 57 | #[derive(Derivative, Clone)] 58 | #[derivative(Debug, Default)] 59 | pub struct PinSpec { 60 | pub id: usize, 61 | pub kind: PinType, 62 | pub name: String, 63 | pub style_args: PinStyleArgs, 64 | pub flags: usize, 65 | } 66 | 67 | #[derive(Derivative, Clone)] 68 | #[derivative(Debug)] 69 | pub(crate) struct PinState { 70 | pub parent_node_idx: usize, 71 | pub attribute_rect: egui::Rect, 72 | pub pos: egui::Pos2, 73 | #[derivative(Debug = "ignore")] 74 | pub color_style: PinStyle, 75 | #[derivative(Debug = "ignore")] 76 | pub shape_gui: Option, 77 | } 78 | 79 | impl Default for PinState { 80 | fn default() -> Self { 81 | Self { 82 | parent_node_idx: Default::default(), 83 | attribute_rect: egui::Rect::ZERO, 84 | pos: Default::default(), 85 | color_style: Default::default(), 86 | shape_gui: Default::default(), 87 | } 88 | } 89 | } 90 | 91 | #[derive(Derivative)] 92 | #[derivative(Debug)] 93 | pub(crate) struct Pin { 94 | pub spec: PinSpec, 95 | pub state: PinState, 96 | } 97 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph_editor/src/ui/editor_windows/ragdoll_editor/top_panel.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::Handle, ecs::world::World}; 2 | use bevy_animation_graph::{ 3 | core::ragdoll::{bone_mapping::RagdollBoneMap, definition::Ragdoll}, 4 | prelude::AnimatedScene, 5 | }; 6 | 7 | use crate::ui::{ 8 | core::LegacyEditorWindowContext, editor_windows::ragdoll_editor::RagdollEditorAction, 9 | reflect_widgets::wrap_ui::using_wrap_ui, 10 | }; 11 | 12 | pub struct TopPanel<'a, 'b> { 13 | pub ragdoll: Option>, 14 | pub ragdoll_bone_map: Option>, 15 | pub scene: Option>, 16 | pub world: &'a mut World, 17 | pub ctx: &'a mut LegacyEditorWindowContext<'b>, 18 | } 19 | 20 | impl TopPanel<'_, '_> { 21 | pub fn draw(self, ui: &mut egui::Ui) { 22 | ui.horizontal(|ui| { 23 | ui.label("Scene:"); 24 | using_wrap_ui(self.world, |mut env| { 25 | if let Some(new_handle) = env.mutable_buffered( 26 | &self.scene.clone().unwrap_or_default(), 27 | ui, 28 | ui.id().with("ragdoll base scene selector"), 29 | &(), 30 | ) { 31 | self.ctx 32 | .window_action(RagdollEditorAction::SelectBaseScene(new_handle)); 33 | } 34 | }); 35 | 36 | ui.label("Ragdoll:"); 37 | using_wrap_ui(self.world, |mut env| { 38 | if let Some(new_handle) = env.mutable_buffered( 39 | &self.ragdoll.clone().unwrap_or_default(), 40 | ui, 41 | ui.id().with("ragdoll selectors"), 42 | &(), 43 | ) { 44 | self.ctx 45 | .window_action(RagdollEditorAction::SelectRagdoll(new_handle)); 46 | } 47 | }); 48 | 49 | ui.label("Ragdoll bone map:"); 50 | using_wrap_ui(self.world, |mut env| { 51 | if let Some(new_handle) = env.mutable_buffered( 52 | &self.ragdoll_bone_map.clone().unwrap_or_default(), 53 | ui, 54 | ui.id().with("ragdoll bone map selectors"), 55 | &(), 56 | ) { 57 | self.ctx 58 | .window_action(RagdollEditorAction::SelectRagdollBoneMap(new_handle)); 59 | } 60 | }); 61 | 62 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 63 | if ui.button("⚙").clicked() { 64 | self.ctx 65 | .window_action(RagdollEditorAction::ToggleSettingsWindow); 66 | } 67 | }); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/bevy_animation_graph/src/core/ragdoll/bone_mapping_loader.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{AssetLoader, AssetPath, LoadContext, io::Reader}, 3 | platform::collections::HashMap, 4 | reflect::Reflect, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{ 9 | core::{ 10 | animation_clip::EntityPath, 11 | errors::AssetLoaderError, 12 | ragdoll::{ 13 | bone_mapping::{BodyMapping, BoneMapping, RagdollBoneMap}, 14 | definition::BodyId, 15 | }, 16 | }, 17 | prelude::serial::SymmetryConfigSerial, 18 | }; 19 | 20 | #[derive(Default)] 21 | pub struct RagdollBoneMapLoader; 22 | 23 | impl AssetLoader for RagdollBoneMapLoader { 24 | type Asset = RagdollBoneMap; 25 | type Settings = (); 26 | type Error = AssetLoaderError; 27 | 28 | async fn load( 29 | &self, 30 | reader: &mut dyn Reader, 31 | _settings: &Self::Settings, 32 | load_context: &mut LoadContext<'_>, 33 | ) -> Result { 34 | let mut bytes = vec![]; 35 | reader.read_to_end(&mut bytes).await?; 36 | let RagdollBoneMapSerial { 37 | bones_from_bodies, 38 | bodies_from_bones, 39 | skeleton, 40 | ragdoll, 41 | skeleton_symmetry, 42 | } = ron::de::from_bytes(&bytes)?; 43 | 44 | Ok(RagdollBoneMap { 45 | bones_from_bodies, 46 | bodies_from_bones, 47 | skeleton: load_context.load(skeleton), 48 | ragdoll: load_context.load(ragdoll), 49 | skeleton_symmetry: skeleton_symmetry.to_value()?, 50 | }) 51 | } 52 | 53 | fn extensions(&self) -> &[&str] { 54 | &["bm.ron"] 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone, Reflect, Serialize, Deserialize)] 59 | pub struct RagdollBoneMapSerial { 60 | pub bones_from_bodies: HashMap, 61 | pub bodies_from_bones: HashMap, 62 | pub skeleton: AssetPath<'static>, 63 | pub ragdoll: AssetPath<'static>, 64 | #[serde(default)] 65 | pub skeleton_symmetry: SymmetryConfigSerial, 66 | } 67 | 68 | impl RagdollBoneMapSerial { 69 | pub fn from_value(ragdoll_bone_map: &RagdollBoneMap) -> Option { 70 | let RagdollBoneMap { 71 | bones_from_bodies, 72 | bodies_from_bones, 73 | skeleton, 74 | ragdoll, 75 | skeleton_symmetry, 76 | } = ragdoll_bone_map; 77 | 78 | Some(Self { 79 | bones_from_bodies: bones_from_bodies.clone(), 80 | bodies_from_bones: bodies_from_bones.clone(), 81 | skeleton: skeleton.path()?.to_owned(), 82 | ragdoll: ragdoll.path()?.to_owned(), 83 | skeleton_symmetry: SymmetryConfigSerial::from_value(skeleton_symmetry), 84 | }) 85 | } 86 | } 87 | --------------------------------------------------------------------------------