animation_graph.rs:
#[cfg(not(target_arch = "wasm32"))]
use std::{fs::File, path::Path};
use bevy::{
animation::animate_targets,
color::palettes::{
basic::WHITE,
css::{ANTIQUE_WHITE, DARK_GREEN},
},
prelude::*,
ui::RelativeCursorPosition,
};
use argh::FromArgs;
#[cfg(not(target_arch = "wasm32"))]
use bevy::asset::io::file::FileAssetReader;
#[cfg(not(target_arch = "wasm32"))]
use bevy::tasks::IoTaskPool;
#[cfg(not(target_arch = "wasm32"))]
use ron::ser::PrettyConfig;
static ANIMATION_GRAPH_PATH: &str = "animation_graphs/Fox.animgraph.ron";
static CLIP_NODE_INDICES: [u32; 3] = [2, 3, 4];
static HELP_TEXT: &str = "Click and drag an animation clip node to change its weight";
static NODE_TYPES: [NodeType; 5] = [
NodeType::Clip(ClipNode::new("Idle", 0)),
NodeType::Clip(ClipNode::new("Walk", 1)),
NodeType::Blend("Root"),
NodeType::Blend("Blend\n0.5"),
NodeType::Clip(ClipNode::new("Run", 2)),
];
static NODE_RECTS: [NodeRect; 5] = [
NodeRect::new(10.00, 10.00, 97.64, 48.41),
NodeRect::new(10.00, 78.41, 97.64, 48.41),
NodeRect::new(286.08, 78.41, 97.64, 48.41),
NodeRect::new(148.04, 44.20, 97.64, 48.41),
NodeRect::new(10.00, 146.82, 97.64, 48.41),
];
static HORIZONTAL_LINES: [Line; 6] = [
Line::new(107.64, 34.21, 20.20),
Line::new(107.64, 102.61, 20.20),
Line::new(107.64, 171.02, 158.24),
Line::new(127.84, 68.41, 20.20),
Line::new(245.68, 68.41, 20.20),
Line::new(265.88, 102.61, 20.20),
];
static VERTICAL_LINES: [Line; 2] = [
Line::new(127.83, 34.21, 68.40),
Line::new(265.88, 68.41, 102.61),
];
fn main() {
#[cfg(not(target_arch = "wasm32"))]
let args: Args = argh::from_env();
#[cfg(target_arch = "wasm32")]
let args = Args::from_args(&[], &[]).unwrap();
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Animation Graph Example".into(),
..default()
}),
..default()
}))
.add_systems(Startup, (setup_assets, setup_scene, setup_ui))
.add_systems(Update, init_animations.before(animate_targets))
.add_systems(
Update,
(handle_weight_drag, update_ui, sync_weights).chain(),
)
.insert_resource(args)
.insert_resource(AmbientLight {
color: WHITE.into(),
brightness: 100.0,
})
.run();
}
#[derive(FromArgs, Resource)]
struct Args {
#[argh(switch)]
no_load: bool,
#[argh(switch)]
save: bool,
}
#[derive(Clone, Resource)]
struct ExampleAnimationGraph(Handle<AnimationGraph>);
#[derive(Component)]
struct ExampleAnimationWeights {
weights: [f32; 3],
}
fn setup_assets(
mut commands: Commands,
mut asset_server: ResMut<AssetServer>,
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
args: Res<Args>,
) {
if args.no_load || args.save {
setup_assets_programmatically(
&mut commands,
&mut asset_server,
&mut animation_graphs,
args.save,
);
} else {
setup_assets_via_serialized_animation_graph(&mut commands, &mut asset_server);
}
}
fn setup_ui(mut commands: Commands) {
setup_help_text(&mut commands);
setup_node_rects(&mut commands);
setup_node_lines(&mut commands);
}
fn setup_assets_programmatically(
commands: &mut Commands,
asset_server: &mut AssetServer,
animation_graphs: &mut Assets<AnimationGraph>,
_save: bool,
) {
let mut animation_graph = AnimationGraph::new();
let blend_node = animation_graph.add_blend(0.5, animation_graph.root);
animation_graph.add_clip(
asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
1.0,
animation_graph.root,
);
animation_graph.add_clip(
asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
1.0,
blend_node,
);
animation_graph.add_clip(
asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
1.0,
blend_node,
);
#[cfg(not(target_arch = "wasm32"))]
if _save {
let animation_graph = animation_graph.clone();
IoTaskPool::get()
.spawn(async move {
let mut animation_graph_writer = File::create(Path::join(
&FileAssetReader::get_base_path(),
Path::join(Path::new("assets"), Path::new(ANIMATION_GRAPH_PATH)),
))
.expect("Failed to open the animation graph asset");
ron::ser::to_writer_pretty(
&mut animation_graph_writer,
&animation_graph,
PrettyConfig::default(),
)
.expect("Failed to serialize the animation graph");
})
.detach();
}
let handle = animation_graphs.add(animation_graph);
commands.insert_resource(ExampleAnimationGraph(handle));
}
fn setup_assets_via_serialized_animation_graph(
commands: &mut Commands,
asset_server: &mut AssetServer,
) {
commands.insert_resource(ExampleAnimationGraph(
asset_server.load(ANIMATION_GRAPH_PATH),
));
}
fn setup_scene(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-10.0, 5.0, 13.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
..default()
});
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 10_000_000.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(-4.0, 8.0, 13.0),
..default()
});
commands.spawn(SceneBundle {
scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
transform: Transform::from_scale(Vec3::splat(0.07)),
..default()
});
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(7.0)),
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
..default()
});
}
fn setup_help_text(commands: &mut Commands) {
commands.spawn(TextBundle {
text: Text::from_section(HELP_TEXT, TextStyle::default()),
style: Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
..default()
});
}
fn setup_node_rects(commands: &mut Commands) {
for (node_rect, node_type) in NODE_RECTS.iter().zip(NODE_TYPES.iter()) {
let node_string = match *node_type {
NodeType::Clip(ref clip) => clip.text,
NodeType::Blend(text) => text,
};
let text = commands
.spawn(TextBundle {
text: Text::from_section(
node_string,
TextStyle {
font_size: 16.0,
color: ANTIQUE_WHITE.into(),
..default()
},
)
.with_justify(JustifyText::Center),
..default()
})
.id();
let container = {
let mut container = commands.spawn((
NodeBundle {
style: Style {
position_type: PositionType::Absolute,
bottom: Val::Px(node_rect.bottom),
left: Val::Px(node_rect.left),
height: Val::Px(node_rect.height),
width: Val::Px(node_rect.width),
align_items: AlignItems::Center,
justify_items: JustifyItems::Center,
align_content: AlignContent::Center,
justify_content: JustifyContent::Center,
..default()
},
border_color: WHITE.into(),
..default()
},
Outline::new(Val::Px(1.), Val::ZERO, Color::WHITE),
));
if let NodeType::Clip(ref clip) = node_type {
container.insert((
Interaction::None,
RelativeCursorPosition::default(),
(*clip).clone(),
));
}
container.id()
};
if let NodeType::Clip(_) = node_type {
let background = commands
.spawn(NodeBundle {
style: Style {
position_type: PositionType::Absolute,
top: Val::Px(0.),
left: Val::Px(0.),
height: Val::Px(node_rect.height),
width: Val::Px(node_rect.width),
..default()
},
background_color: DARK_GREEN.into(),
..default()
})
.id();
commands.entity(container).add_child(background);
}
commands.entity(container).add_child(text);
}
}
fn setup_node_lines(commands: &mut Commands) {
for line in &HORIZONTAL_LINES {
commands.spawn(NodeBundle {
style: Style {
position_type: PositionType::Absolute,
bottom: Val::Px(line.bottom),
left: Val::Px(line.left),
height: Val::Px(0.0),
width: Val::Px(line.length),
border: UiRect::bottom(Val::Px(1.0)),
..default()
},
border_color: WHITE.into(),
..default()
});
}
for line in &VERTICAL_LINES {
commands.spawn(NodeBundle {
style: Style {
position_type: PositionType::Absolute,
bottom: Val::Px(line.bottom),
left: Val::Px(line.left),
height: Val::Px(line.length),
width: Val::Px(0.0),
border: UiRect::left(Val::Px(1.0)),
..default()
},
border_color: WHITE.into(),
..default()
});
}
}
fn init_animations(
mut commands: Commands,
mut query: Query<(Entity, &mut AnimationPlayer)>,
animation_graph: Res<ExampleAnimationGraph>,
mut done: Local<bool>,
) {
if *done {
return;
}
for (entity, mut player) in query.iter_mut() {
commands.entity(entity).insert((
animation_graph.0.clone(),
ExampleAnimationWeights::default(),
));
for &node_index in &CLIP_NODE_INDICES {
player.play(node_index.into()).repeat();
}
*done = true;
}
}
fn handle_weight_drag(
mut interaction_query: Query<(&Interaction, &RelativeCursorPosition, &ClipNode)>,
mut animation_weights_query: Query<&mut ExampleAnimationWeights>,
) {
for (interaction, relative_cursor, clip_node) in &mut interaction_query {
if !matches!(*interaction, Interaction::Pressed) {
continue;
}
let Some(pos) = relative_cursor.normalized else {
continue;
};
for mut animation_weights in animation_weights_query.iter_mut() {
animation_weights.weights[clip_node.index] = pos.x.clamp(0., 1.);
}
}
}
fn update_ui(
mut text_query: Query<&mut Text>,
mut background_query: Query<&mut Style, Without<Text>>,
container_query: Query<(&Children, &ClipNode)>,
animation_weights_query: Query<&ExampleAnimationWeights, Changed<ExampleAnimationWeights>>,
) {
for animation_weights in animation_weights_query.iter() {
for (children, clip_node) in &container_query {
let mut bg_iter = background_query.iter_many_mut(children);
if let Some(mut style) = bg_iter.fetch_next() {
style.width =
Val::Px(NODE_RECTS[0].width * animation_weights.weights[clip_node.index]);
}
let mut text_iter = text_query.iter_many_mut(children);
if let Some(mut text) = text_iter.fetch_next() {
text.sections[0].value = format!(
"{}\n{:.2}",
clip_node.text, animation_weights.weights[clip_node.index]
);
}
}
}
}
fn sync_weights(mut query: Query<(&mut AnimationPlayer, &ExampleAnimationWeights)>) {
for (mut animation_player, animation_weights) in query.iter_mut() {
for (&animation_node_index, &animation_weight) in CLIP_NODE_INDICES
.iter()
.zip(animation_weights.weights.iter())
{
if !animation_player.animation_is_playing(animation_node_index.into()) {
animation_player.play(animation_node_index.into());
}
if let Some(active_animation) =
animation_player.animation_mut(animation_node_index.into())
{
active_animation.set_weight(animation_weight);
}
}
}
}
#[derive(Debug)]
struct NodeRect {
left: f32,
bottom: f32,
width: f32,
height: f32,
}
struct Line {
left: f32,
bottom: f32,
length: f32,
}
enum NodeType {
Clip(ClipNode),
Blend(&'static str),
}
#[derive(Clone, Component)]
struct ClipNode {
text: &'static str,
index: usize,
}
impl Default for ExampleAnimationWeights {
fn default() -> Self {
Self { weights: [1.0; 3] }
}
}
impl ClipNode {
const fn new(text: &'static str, index: usize) -> Self {
Self { text, index }
}
}
impl NodeRect {
const fn new(left: f32, bottom: f32, width: f32, height: f32) -> NodeRect {
NodeRect {
left,
bottom,
width,
height,
}
}
}
impl Line {
const fn new(left: f32, bottom: f32, length: f32) -> Self {
Self {
left,
bottom,
length,
}
}
}