ambient_animation/
retargeting.rs

1use std::sync::Arc;
2
3use ambient_core::transform::{rotation, translation};
4use ambient_model::{Model, ModelFromUrl};
5use ambient_std::{
6    asset_cache::{AssetCache, AssetKeepalive, AsyncAssetKey, AsyncAssetKeyExt},
7    asset_url::{AnimationAssetType, ModelAssetType, TypedAssetUrl},
8    download_asset::AssetError,
9};
10use anyhow::Context;
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13
14use super::{AnimationClip, AnimationClipFromUrl, AnimationOutputs, AnimationTrack};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum AnimationRetargeting {
18    /// Bone Translation comes from the animation data, unchanged.
19    None,
20    /// Bone Translation comes from the Target Skeleton's bind pose.
21    Skeleton,
22    /// Bone translation comes from the animation data, but is scaled by the Skeleton's proportions.
23    /// This is the ratio between the bone length of the Target Skeleton (the skeleton the animation
24    /// is being played on), and the Source Skeleton (the skeleton the animation was authored for).
25    AnimationScaled {
26        /// Rotates the Hips bone based on the difference between the rotation the animation models root and the retarget animations root
27        normalize_hip: bool,
28    },
29}
30impl Default for AnimationRetargeting {
31    fn default() -> Self {
32        Self::None
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct AnimationClipRetargetedFromModel {
38    pub clip: TypedAssetUrl<AnimationAssetType>,
39    pub translation_retargeting: AnimationRetargeting,
40    pub retarget_model: Option<TypedAssetUrl<ModelAssetType>>,
41}
42#[async_trait]
43impl AsyncAssetKey<Result<Arc<AnimationClip>, AssetError>> for AnimationClipRetargetedFromModel {
44    fn keepalive(&self) -> AssetKeepalive {
45        // TODO(fred): We _could_ have a timeout here, but it looks weird when animations are loading
46        // so just keeping them forever for now, and since they are only peeked, the keepalive timeout
47        // wouldn't refresh, so once per hour we'd always have that "bug"
48        AssetKeepalive::Forever
49    }
50    async fn load(self, assets: AssetCache) -> Result<Arc<AnimationClip>, AssetError> {
51        let clip_url: TypedAssetUrl<AnimationAssetType> = self
52            .clip
53            .abs()
54            .context(format!("Expected absolute url, got: {}", self.clip))?
55            .into();
56        let anim_model = ModelFromUrl(clip_url.model_crate().context("Invalid clip url")?.model())
57            .get(&assets)
58            .await
59            .context("Failed to load model")?;
60        let clip = AnimationClipFromUrl::new(clip_url.unwrap_abs(), true)
61            .get(&assets)
62            .await
63            .context("No such clip")?;
64        match self.translation_retargeting {
65            AnimationRetargeting::None => Ok(clip),
66            AnimationRetargeting::Skeleton => {
67                let mut clip = (*clip).clone();
68                clip.tracks
69                    .retain(|track| track.outputs.component() != translation());
70                Ok(Arc::new(clip))
71            }
72            AnimationRetargeting::AnimationScaled { normalize_hip } => {
73                let retarget_model_url = self
74                    .retarget_model
75                    .context("No retarget_model specified")?
76                    .abs()
77                    .context("Failed to resolve retarget url")?;
78                let retarget_model = ModelFromUrl(retarget_model_url.into())
79                    .get(&assets)
80                    .await
81                    .context("Failed to load retarget model")?;
82                let mut clip = (*clip).clone();
83                let anim_root = anim_model.roots()[0];
84                let _retarget_root = retarget_model.roots()[0];
85                let anim_root_rot = anim_model.0.get(anim_root, rotation()).unwrap_or_default();
86                let retarget_root_rot = retarget_model
87                    .0
88                    .get(anim_root, rotation())
89                    .unwrap_or_default();
90                clip.tracks.retain_mut(|track| {
91                    if normalize_hip && track.target.bind_id() == Some("Hips") {
92                        let zup = retarget_root_rot.inverse() * anim_root_rot;
93
94                        if track.outputs.component() == rotation() {
95                            if let AnimationOutputs::Quat { data, .. } = &mut track.outputs {
96                                for v in data {
97                                    *v = zup * *v;
98                                }
99                            }
100                        } else if track.outputs.component() == translation() {
101                            if let AnimationOutputs::Vec3 { data, .. } = &mut track.outputs {
102                                for v in data {
103                                    *v = zup * *v;
104                                }
105                            }
106                        }
107                    }
108                    if track.outputs.component() == translation() {
109                        retarget_track(track, &anim_model, &retarget_model).is_some()
110                    } else {
111                        true
112                    }
113                });
114                Ok(Arc::new(clip))
115            }
116        }
117    }
118}
119fn retarget_track(
120    track: &mut AnimationTrack,
121    anim_model: &Model,
122    retarget_model: &Model,
123) -> Option<()> {
124    let bind_id = track.target.bind_id().unwrap();
125    let original = anim_model.get_entity_id_by_bind_id(bind_id).unwrap();
126    let target = retarget_model.get_entity_id_by_bind_id(bind_id)?;
127    let original = anim_model.0.get(original, translation()).unwrap().length();
128    let target = anim_model.0.get(target, translation()).ok()?.length();
129    if target == 0. {
130        return Some(());
131    }
132    let scale = target / original;
133    match &mut track.outputs {
134        AnimationOutputs::Vec3 { data, .. } => {
135            for v in data.iter_mut() {
136                *v *= scale;
137            }
138        }
139        AnimationOutputs::Quat { .. } => unreachable!(),
140        AnimationOutputs::Vec3Field { data, .. } => {
141            for v in data.iter_mut() {
142                *v *= scale;
143            }
144        }
145    }
146    Some(())
147}