Skip to content

Commit

Permalink
Bevy 0.14 migration (#54)
Browse files Browse the repository at this point in the history
Migrates to Bevy 0.14. Bevy 0.14 has significant changes to the
animation system, but the most significant for our purposes is that
usage of `EntityPath` throughout the system is replaced by an opaque
`AnimationTargetId`, which is essentially a wrapper around `Uuid`.

Since we relied on the transparent structure of `EntityPath` to find the
parent of a bone in certain nodes (rotation node, inverse kinematics
node), this change forced us to add a `Skeleton` asset type, that upon
load will store the entity hierarchy of an animated scene and map
`AnimationTargetId`s to entity paths. The hierarchy will be created
automatically, but you need to manually create the skeleton asset and
point it to a Gltf scene to use as reference (see updated examples).
  • Loading branch information
mbrea-c authored Jul 21, 2024
1 parent c4b4866 commit 1f58272
Show file tree
Hide file tree
Showing 51 changed files with 2,868 additions and 1,894 deletions.
2,882 changes: 1,807 additions & 1,075 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ edition = "2021"
repository = "https://github.com/mbrea-c/bevy_animation_graph"

[workspace.dependencies]
bevy = { version = "0.13" }
bevy = { version = "0.14" }
ron = "0.8.1"
1 change: 1 addition & 0 deletions assets/animated_scenes/fsm.animscn.ron
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
source: "models/character_rigged.glb#Scene0",
path_to_player: ["metarig"],
animation_graph: "animation_graphs/toplevel.animgraph.ron",
skeleton: "skeletons/human.skn.ron",
)
1 change: 1 addition & 0 deletions assets/animated_scenes/human.animscn.ron
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
source: "models/character_rigged.glb#Scene0",
path_to_player: ["metarig"],
animation_graph: "animation_graphs/human_new.animgraph.ron",
skeleton: "skeletons/human.skn.ron",
)
1 change: 1 addition & 0 deletions assets/animated_scenes/human_ik.animscn.ron
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
source: "models/character_rigged.glb#Scene0",
path_to_player: ["metarig"],
animation_graph: "animation_graphs/human_ik.animgraph.ron",
skeleton: "skeletons/human.skn.ron",
)
1 change: 1 addition & 0 deletions assets/animations/human_run.anim.ron
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
path: "models/character_rigged.glb",
animation_name: "RunBake",
),
skeleton: "skeletons/human.skn.ron",
)
1 change: 1 addition & 0 deletions assets/animations/human_walk.anim.ron
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
path: "models/character_rigged.glb",
animation_name: "WalkBake",
),
skeleton: "skeletons/human.skn.ron",
)
3 changes: 3 additions & 0 deletions assets/skeletons/human.skn.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
source: Gltf(source: "models/character_rigged.glb", label: "Scene0"),
)
3 changes: 2 additions & 1 deletion crates/bevy_animation_graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ keywords = ["bevy", "animation", "gamedev"]
bevy = { workspace = true }
thiserror = "1.0.58"
ron = { workspace = true }
serde = { version = "1.0.193", features = ["derive"] }
serde = { version = "1.0.193", features = ["derive", "rc"] }
indexmap = { version = "2.2.1", features = ["serde"] }
regex = "1.10.3"
uuid = "1.0"
50 changes: 27 additions & 23 deletions crates/bevy_animation_graph/src/core/animated_scene.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::errors::AssetLoaderError;
use super::{errors::AssetLoaderError, skeleton::Skeleton};
use crate::prelude::{AnimationGraph, AnimationGraphPlayer};
use bevy::{
asset::{io::Reader, Asset, AssetLoader, AsyncReadExt, Handle, LoadContext},
Expand All @@ -10,7 +10,6 @@ use bevy::{
render::view::{InheritedVisibility, ViewVisibility, Visibility},
scene::{Scene, SceneInstance},
transform::components::{GlobalTransform, Transform},
utils::BoxedFuture,
};
use serde::{Deserialize, Serialize};

Expand All @@ -19,13 +18,15 @@ struct AnimatedSceneSerial {
source: String,
path_to_player: Vec<String>,
animation_graph: String,
skeleton: String,
}

#[derive(Clone, Asset, Reflect)]
pub struct AnimatedScene {
source: Handle<Scene>,
path_to_player: Vec<String>,
animation_graph: Handle<AnimationGraph>,
pub(crate) source: Handle<Scene>,
pub(crate) path_to_player: Vec<String>,
pub(crate) animation_graph: Handle<AnimationGraph>,
pub(crate) skeleton: Handle<Skeleton>,
}

#[derive(Component)]
Expand Down Expand Up @@ -59,25 +60,25 @@ impl AssetLoader for AnimatedSceneLoader {
type Settings = ();
type Error = AssetLoaderError;

fn load<'a>(
async fn load<'a>(
&'a self,
reader: &'a mut Reader,
reader: &'a mut Reader<'_>,
_settings: &'a Self::Settings,
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
Box::pin(async move {
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
let serial: AnimatedSceneSerial = ron::de::from_bytes(&bytes)?;

let animation_graph: Handle<AnimationGraph> = load_context.load(serial.animation_graph);
let source: Handle<Scene> = load_context.load(serial.source);

Ok(AnimatedScene {
source,
path_to_player: serial.path_to_player,
animation_graph,
})
load_context: &'a mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
let serial: AnimatedSceneSerial = ron::de::from_bytes(&bytes)?;

let animation_graph: Handle<AnimationGraph> = load_context.load(serial.animation_graph);
let source: Handle<Scene> = load_context.load(serial.source);
let skeleton: Handle<Skeleton> = load_context.load(serial.skeleton);

Ok(AnimatedScene {
source,
path_to_player: serial.path_to_player,
animation_graph,
skeleton,
})
}

Expand Down Expand Up @@ -183,7 +184,10 @@ pub(crate) fn process_animated_scenes(
commands
.entity(next_entity)
.remove::<AnimationPlayer>()
.insert(AnimationGraphPlayer::new().with_graph(animscn.animation_graph.clone()));
.insert(
AnimationGraphPlayer::new(animscn.skeleton.clone())
.with_graph(animscn.animation_graph.clone()),
);
commands
.entity(animscn_entity)
.insert(AnimatedSceneInstance {
Expand Down
109 changes: 77 additions & 32 deletions crates/bevy_animation_graph/src/core/animation_clip.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use bevy::{
asset::prelude::*, core::prelude::*, math::prelude::*, reflect::prelude::*, utils::HashMap,
animation::AnimationTargetId,
asset::prelude::*,
core::prelude::*,
math::prelude::*,
reflect::prelude::*,
utils::{hashbrown::HashMap, NoOpHash},
};
use serde::{Deserialize, Serialize};

use super::{id, skeleton::Skeleton};

/// List of keyframes for one of the attribute of a [`Transform`].
///
/// [`Transform`]: bevy::transform::prelude::Transform
Expand Down Expand Up @@ -86,6 +93,10 @@ impl EntityPath {
}
}

pub fn last(&self) -> Option<Name> {
self.parts.last().cloned()
}

/// Returns a string representation of the path, with '/' as the separator. If any path parts
/// themselves contain '/', they will be escaped
pub fn to_slashed_string(&self) -> String {
Expand All @@ -104,6 +115,10 @@ impl EntityPath {
.collect(),
}
}

pub fn id(&self) -> id::BoneId {
AnimationTargetId::from_names(self.parts.iter()).into()
}
}

struct InterpretEscapedString<'a> {
Expand Down Expand Up @@ -173,66 +188,96 @@ impl<'de> Deserialize<'de> for EntityPath {
/// A list of [`VariableCurve`], and the [`EntityPath`] to which they apply.
#[derive(Asset, Reflect, Clone, Debug, Default)]
pub struct GraphClip {
pub(crate) curves: Vec<Vec<VariableCurve>>,
pub(crate) paths: HashMap<EntityPath, usize>,
pub(crate) curves: AnimationCurves,
pub(crate) duration: f32,
pub(crate) skeleton: Handle<Skeleton>,
}

/// This is a helper type to "steal" the data from a `bevy_animation::AnimationClip` into our
/// `GraphClip`, since the internal fields of `bevy_animation::AnimationClip` are not public and we
/// need to do a hackery.
struct TempGraphClip {
curves: AnimationCurves,
duration: f32,
}

/// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the
/// animation curves.
pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, NoOpHash>;

impl GraphClip {
#[inline]
/// [`VariableCurve`]s for each bone. Indexed by the bone ID.
pub fn curves(&self) -> &Vec<Vec<VariableCurve>> {
/// [`VariableCurve`]s for each animation target. Indexed by the [`AnimationTargetId`].
pub fn curves(&self) -> &AnimationCurves {
&self.curves
}

/// Gets the curves for a bone.
#[inline]
/// Get mutable references of [`VariableCurve`]s for each animation target. Indexed by the [`AnimationTargetId`].
pub fn curves_mut(&mut self) -> &mut AnimationCurves {
&mut self.curves
}

/// Gets the curves for a single animation target.
///
/// Returns `None` if the bone is invalid.
/// Returns `None` if this clip doesn't animate the target.
#[inline]
pub fn get_curves(&self, bone_id: usize) -> Option<&'_ Vec<VariableCurve>> {
self.curves.get(bone_id)
pub fn curves_for_target(
&self,
target_id: AnimationTargetId,
) -> Option<&'_ Vec<VariableCurve>> {
self.curves.get(&target_id)
}

/// Gets the curves by it's [`EntityPath`].
/// Gets mutable references of the curves for a single animation target.
///
/// Returns `None` if the bone is invalid.
/// Returns `None` if this clip doesn't animate the target.
#[inline]
pub fn get_curves_by_path(&self, path: &EntityPath) -> Option<&'_ Vec<VariableCurve>> {
self.paths.get(path).and_then(|id| self.curves.get(*id))
pub fn curves_for_target_mut(
&mut self,
target_id: AnimationTargetId,
) -> Option<&'_ mut Vec<VariableCurve>> {
self.curves.get_mut(&target_id)
}

/// Duration of the clip, represented in seconds
/// Duration of the clip, represented in seconds.
#[inline]
pub fn duration(&self) -> f32 {
self.duration
}

/// Add a [`VariableCurve`] to an [`EntityPath`].
pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) {
/// Set the duration of the clip in seconds.
#[inline]
pub fn set_duration(&mut self, duration_sec: f32) {
self.duration = duration_sec;
}

/// Adds a [`VariableCurve`] to an [`AnimationTarget`] named by an
/// [`AnimationTargetId`].
///
/// If the curve extends beyond the current duration of this clip, this
/// method lengthens this clip to include the entire time span that the
/// curve covers.
pub fn add_curve_to_target(&mut self, target_id: AnimationTargetId, curve: VariableCurve) {
// Update the duration of the animation by this curve duration if it's longer
self.duration = self
.duration
.max(*curve.keyframe_timestamps.last().unwrap_or(&0.0));
if let Some(bone_id) = self.paths.get(&path) {
self.curves[*bone_id].push(curve);
} else {
let idx = self.curves.len();
self.curves.push(vec![curve]);
self.paths.insert(path, idx);
}
self.curves.entry(target_id).or_default().push(curve);
}

/// Whether this animation clip can run on entity with given [`Name`].
pub fn compatible_with(&self, name: &Name) -> bool {
self.paths.keys().any(|path| &path.parts[0] == name)
}
}

impl From<bevy::animation::AnimationClip> for GraphClip {
fn from(value: bevy::animation::AnimationClip) -> Self {
pub fn from_bevy_clip(
bevy_clip: bevy::animation::AnimationClip,
skelington: Handle<Skeleton>,
) -> Self {
// HACK: to get the corret type, since bevy's AnimationClip
// does not expose its internals
unsafe { std::mem::transmute(value) }
let tmp_clip: TempGraphClip = unsafe { std::mem::transmute(bevy_clip) };
Self {
curves: tmp_clip.curves,
duration: tmp_clip.duration,
skeleton: skelington,
}
}
}

Expand Down
Loading

0 comments on commit 1f58272

Please sign in to comment.