Skip to content

Commit

Permalink
add logic
Browse files Browse the repository at this point in the history
  • Loading branch information
kayhhh committed Sep 22, 2024
1 parent 5ad6b24 commit 2727cd5
Show file tree
Hide file tree
Showing 15 changed files with 5,652 additions and 4 deletions.
4,448 changes: 4,448 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@ edition = "2021"
repository = "https://github.com/unavi-xyz/bevy_vr_controller"
license = "MIT OR Apache-2.0"

[profile.release]
lto = "thin"
[dependencies]
avian3d = "0.1.2"
bevy = { version = "0.14.2", default-features = false, features = [
"bevy_gltf",
] }
bevy-tnua = "0.19.0"
bevy-tnua-avian3d = "0.1.1"
bevy_vrm = "0.0.12"
paste = "1.0.15"
150 changes: 150 additions & 0 deletions src/animation/load.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use bevy::{gltf::GltfNode, prelude::*, utils::HashMap};
use bevy_vrm::{animations::vrm::VRM_ANIMATION_TARGETS, BoneName};

use super::{
mixamo::{MIXAMO_ANIMATION_TARGETS, MIXAMO_BONE_NAMES},
AnimationName,
};

#[derive(Component, Clone)]
pub struct AvatarAnimationClips(pub HashMap<AnimationName, AvatarAnimation>);

#[derive(Clone)]
pub struct AvatarAnimation {
pub clip: Handle<AnimationClip>,
pub gltf: Handle<Gltf>,
}

#[derive(Component, Clone)]
pub struct AvatarAnimationNodes(pub HashMap<AnimationName, AnimationNodeIndex>);

pub(crate) fn load_animation_nodes(
avatars: Query<(Entity, &AvatarAnimationClips), Without<Handle<AnimationGraph>>>,
mut clips: ResMut<Assets<AnimationClip>>,
mut commands: Commands,
mut gltfs: ResMut<Assets<Gltf>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
nodes: Res<Assets<GltfNode>>,
) {
for (entity, animations) in avatars.iter() {
let mut graph = AnimationGraph::default();
let mut animation_nodes = HashMap::default();

let mut failed = false;

for (name, animation) in animations.0.iter() {
let clip = match clips.get_mut(&animation.clip) {
Some(c) => c,
None => {
failed = true;
break;
}
};

let gltf = match gltfs.get_mut(&animation.gltf) {
Some(c) => c,
None => {
failed = true;
break;
}
};

let clip_curves = clip.curves_mut();

for (name, target) in MIXAMO_ANIMATION_TARGETS.iter() {
// Head transform is set by user's camera.
if *name == BoneName::Head {
continue;
}

if let Some(mut curves) = clip_curves.remove(target) {
let mut to_remove = Vec::default();

for (i, curve) in curves.iter_mut().enumerate() {
if let Keyframes::Translation(translations) = &mut curve.keyframes {
// TODO: Fix translation animations.
// For some reason, all bones get rotated strangely when a translation
// is applied.
to_remove.push(i);

for item in translations {
item.x /= 100.0;
item.y /= 100.0;
item.z /= 100.0;

item.x = -item.x;
}
}
if let Keyframes::Rotation(rotations) = &mut curve.keyframes {
// Find target node.
let mixamo_name = MIXAMO_BONE_NAMES[name];
let node = match gltf.named_nodes.get(mixamo_name) {
Some(n) => n,
None => {
error!("No animation gltf node for {}", mixamo_name);
continue;
}
};

// Get all parents, up to root.
let node = nodes.get(node).unwrap();
let mut parents = Vec::default();
create_parent_chain(gltf, &nodes, &mut parents, node);

// Retarget rotation to be in VRM rig space.
let parent_rot = parents
.iter()
.rev()
.fold(Quat::default(), |rot, n| rot * n.transform.rotation);

let inverse_rot = (parent_rot * node.transform.rotation).inverse();

for item in rotations {
*item = parent_rot * *item * inverse_rot;

item.y = -item.y;
item.w = -item.w;
}
}
}

for index in to_remove.into_iter().rev() {
curves.remove(index);
}

let vrm_target = VRM_ANIMATION_TARGETS[name];
clip_curves.insert(vrm_target, curves);
}
}

let node = graph.add_clip(animation.clip.clone(), 1.0, graph.root);
animation_nodes.insert(name.clone(), node);
}

if failed {
continue;
}

let graph = graphs.add(graph);

commands
.entity(entity)
.insert((graph, AvatarAnimationNodes(animation_nodes)));
}
}

fn create_parent_chain(
gltf: &Gltf,
nodes: &Res<Assets<GltfNode>>,
parents: &mut Vec<GltfNode>,
target: &GltfNode,
) {
for handle in gltf.nodes.iter() {
let node = nodes.get(handle).unwrap();
if node.children.iter().any(|c| c.name == target.name) {
parents.push(node.clone());
create_parent_chain(gltf, nodes, parents, node);
break;
};
}
}
184 changes: 184 additions & 0 deletions src/animation/mixamo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use std::{cell::RefCell, rc::Rc, sync::LazyLock};

use bevy::{animation::AnimationTargetId, utils::HashMap};
use bevy_vrm::{animations::target_chain::TargetChain, BoneName};

macro_rules! finger {
($chain:ident, $side:ident, $finger_vrm:ident, $finger_chain:ident) => {
paste::paste! {
{
let mut chain = $chain.clone();

chain.push_bone(
BoneName::[<$side $finger_vrm Proximal>],
concat!("mixamorig:", stringify!($side), "Hand", stringify!($finger_chain), "1"),
);
chain.push_bone(
BoneName::[<$side $finger_vrm Intermediate>],
concat!("mixamorig:", stringify!($side), "Hand", stringify!($finger_chain), "2"),
);
chain.push_bone(
BoneName::[<$side $finger_vrm Distal>],
concat!("mixamorig:", stringify!($side), "Hand", stringify!($finger_chain), "3"),
);
}
}
};
}

macro_rules! arm {
($chain:ident, $side:ident) => {
paste::paste! {
{
let mut chain = $chain.clone();

chain.push_bone(BoneName::[<$side Shoulder>], concat!("mixamorig:", stringify!($side), "Shoulder"));
chain.push_bone(BoneName::[<$side UpperArm>], concat!("mixamorig:", stringify!($side), "Arm"));
chain.push_bone(BoneName::[<$side LowerArm>], concat!("mixamorig:", stringify!($side), "ForeArm"));
chain.push_bone(BoneName::[<$side Hand>], concat!("mixamorig:", stringify!($side), "Hand"));

finger!(chain, $side, Thumb, Thumb);
finger!(chain, $side, Index, Index);
finger!(chain, $side, Middle, Middle);
finger!(chain, $side, Ring, Ring);
finger!(chain, $side, Little, Pinky);
}
}
};
}

macro_rules! leg {
($chain:ident, $side:ident) => {
paste::paste! {
{
let mut chain = $chain.clone();

chain.push_bone(BoneName::[<$side UpperLeg>], concat!("mixamorig:", stringify!($side), "UpLeg"));
chain.push_bone(BoneName::[<$side LowerLeg>], concat!("mixamorig:", stringify!($side), "Leg"));
chain.push_bone(BoneName::[<$side Foot>], concat!("mixamorig:", stringify!($side), "Foot"));
chain.push_bone(BoneName::[<$side Toes>], concat!("mixamorig:", stringify!($side), "ToeBase"));
}
}
};
}

/// Wrapper around [TargetChain].
/// Allows us to re-use code for both [MIXAMO_ANIMATION_TARGETS] and [MIXAMO_BONE_NAMES].
#[derive(Clone)]
struct ChainWrapper<'a> {
chain: TargetChain,
names: Rc<RefCell<HashMap<BoneName, &'a str>>>,
targets: Rc<RefCell<HashMap<BoneName, AnimationTargetId>>>,
}

impl<'a> ChainWrapper<'a> {
fn new(chain: TargetChain) -> Self {
Self {
chain,
names: Default::default(),
targets: Default::default(),
}
}

fn push_bone(&mut self, bone: BoneName, name: &'a str) {
self.names.borrow_mut().insert(bone, name);
let target = self.chain.push_target(name.to_string());
self.targets.borrow_mut().insert(bone, target);
}

fn into_maps(
self,
) -> (
HashMap<BoneName, &'a str>,
HashMap<BoneName, AnimationTargetId>,
) {
(self.names.take(), self.targets.take())
}
}

fn create_chain() -> ChainWrapper<'static> {
let mut chain = TargetChain::default();
chain.push_target("Armature".to_string());

let mut chain = ChainWrapper::new(chain);

chain.push_bone(BoneName::Hips, "mixamorig:Hips");

leg!(chain, Left);
leg!(chain, Right);

chain.push_bone(BoneName::Spine, "mixamorig:Spine");
chain.push_bone(BoneName::Chest, "mixamorig:Spine1");
chain.push_bone(BoneName::UpperChest, "mixamorig:Spine2");

arm!(chain, Left);
arm!(chain, Right);

chain.push_bone(BoneName::Neck, "mixamorig:Neck");
chain.push_bone(BoneName::Head, "mixamorig:Head");

chain
}

pub static MIXAMO_ANIMATION_TARGETS: LazyLock<HashMap<BoneName, AnimationTargetId>> =
LazyLock::new(|| {
let chain = create_chain();
let (_, targets) = chain.into_maps();
targets
});

pub static MIXAMO_BONE_NAMES: LazyLock<HashMap<BoneName, &'static str>> = LazyLock::new(|| {
let chain = create_chain();
let (names, _) = chain.into_maps();
names
});

#[cfg(test)]
mod tests {
// TODO: This test sometimes fails from a panic while running the main bevy schedule
// use bevy::{gltf::GltfPlugin, prelude::*, render::mesh::skinning::SkinnedMeshInverseBindposes};
//
// use super::*;
//
// #[test]
// fn test_mixamo_targets() {
// let mut app = App::new();
//
// app.add_plugins((
// MinimalPlugins,
// AssetPlugin {
// file_path: "../unavi-app/assets".to_string(),
// ..default()
// },
// AnimationPlugin,
// GltfPlugin::default(),
// ));
//
// app.init_asset::<Scene>();
// app.init_asset::<SkinnedMeshInverseBindposes>();
//
// let asset_server = app.world().get_resource::<AssetServer>().unwrap();
// let _ = asset_server.load::<AnimationClip>("character-animations.glb#Animation0");
//
// app.add_systems(
// Update,
// |assets: Res<Assets<AnimationClip>>,
// time: Res<Time>,
// mut exit: EventWriter<AppExit>| {
// if time.elapsed_seconds() > 15.0 {
// panic!("Took too long");
// }
//
// if let Some((_, clip)) = assets.iter().next() {
// for (name, target) in MIXAMO_ANIMATION_TARGETS.iter() {
// assert!(clip.curves_for_target(*target).is_some(), "name={:?}", name);
// }
//
// exit.send_default();
// }
// },
// );
//
// app.run();
// }
}
37 changes: 37 additions & 0 deletions src/animation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use bevy::prelude::*;

pub(crate) mod load;
mod mixamo;
pub(crate) mod weights;

pub use load::AvatarAnimationNodes;
use weights::{AnimationWeights, TargetAnimationWeights};

#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum AnimationName {
Falling,
#[default]
Idle,
Walk,
WalkLeft,
WalkRight,
Other(String),
}

pub(crate) fn init_animations(
animation_nodes: Query<&Handle<AnimationGraph>, With<AvatarAnimationNodes>>,
mut animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>,
mut commands: Commands,
) {
for (entity, parent) in animation_players.iter_mut() {
let graph = animation_nodes
.get(parent.get())
.expect("Failed to initialize animation, animation nodes not found");

commands.entity(entity).insert((
AnimationWeights::default(),
TargetAnimationWeights::default(),
graph.clone(),
));
}
}
Loading

0 comments on commit 2727cd5

Please sign in to comment.