I have a generic animation which is 30 FPS, 1.7 seconds long and which contains 51 frames.
This is what it looks like in Unity:
And these are the frames in Blender:
I want to use this generic animation on a different model.
I can of course do this using a Humanoid Avatar and Unity's humanoid retargetting system. It works, but I don't really want my model to have a Humanoid Avatar. I have many generic animations perfectly built for just this model, so now switching to a Humanoid Avatar is against my initial decision for a generic avatar.
So what I do is to duplicate my generic model. I create a Humanoid Avatar for the duplicate.
Then I play the animation on the Humanoid model. At each LateUpdate, I copy the bone positions and rotations from the Humanoid model to the Generic model. And I use a GameObjectRecorder to save the bone rotation and position changes to a new animation file.
When the animation has finished playing once, I save the results in an animation file.
So what I have then is the animation file for my generic model.
Now I can delete the Humanoid Avatar because I used it just as a middle man.
However, the resulting animation plays much slower than the original animation.
Unity states that the animation contains 83 frames and is 2:23 seconds long.
I don't see what the problem is.
The GameObjectRecorder that I'm using to capture the animation provides the methode TakeSnapshot(dt).
"dt" means the milliseconds that it should forward the new animation.
How do I calculate "dt"?
I know that the original clip is 1.7 seconds long and plays at 30 fps.
So the duration between 2 frames should (in my opinion) be: 1.7 / 30 = 0,0567.
However, when I call _Recorder.TakeSnapshot(0,056f) at each call, it results in this weird duration and frame count.
What I did next was to take note of the frame written by the recorder.
I use a simple counter:
_iCountOfHowManyFramesWhereWrittenToAnimationFile += 1;
_Recorder.TakeSnapshot(_fDurationOf1Frame);
When the animation has played once, I output the number of frames and the duration:
Debug.Log("Actually written to animation file: " +
_iCountOfHowManyFramesWhereWrittenToAnimationFile + " duration: " +
_Recorder.currentTime);// how many frames were written to the animation?
Result: Actually written to animation file: 51 duration: 2,833332
This confuses me: Why is my file with 51 frames longer than the other animation which also contains 51 frames? It must be because I have miscalculated the duration, but I don't see what the duration should be in order.
I thought that the duration between 2 frames was
fDurationBetween2Frames = AnimationClip.length / AnimationClip.frameRate;
But obviously that is wrong.
Here is the code that I'm using to transfer the animation from Humanoid to Generic, if anybody wants to know.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
public class TransferAnimation : MonoBehaviour
{
public Transform HumanoidModelWhoseAnimationWillBeCopiedToThisModel;
private Animator _animator;
private int _iLastFrameIDInAnimation = -1;
private int _iCount = 0;
private int _iFrameCount = 0;
private int _iCountSequentFramesWhichWouldBeFatal = 0;
private int _iLastCountWithNewAnimationID = 0;
public AnimationClip ClipInWhichTransferredAnimationWillBeSavedAsGeneric;
private float _iNumberOfFramesInClip;
private GameObjectRecorder _Recorder;
private bool _bReturn = false;
private float _fFPS = -1;
private int _iCountOfHowManyFramesWhereWrittenToAnimationFile = 0;
private float _fDurationOf1Frame;
void Start()
{
_Recorder = new GameObjectRecorder(gameObject);
_animator = HumanoidModelWhoseAnimationWillBeCopiedToThisModel.gameObject.GetComponent<Animator>();
AnimationClip nClip = _animator.GetCurrentAnimatorClipInfo(0)[0].clip;
_iNumberOfFramesInClip = (int)(nClip.length * nClip.frameRate);
_fFPS = nClip.frameRate;
_fDurationOf1Frame = nClip.length / nClip.frameRate;
_animator.enabled = true;
pRestart();
}
private void pRestart()
{
_iCountOfHowManyFramesWhereWrittenToAnimationFile = 0;
_iCount = 0;
_bReturn = false;
_iLastFrameIDInAnimation = -1;
_iCountSequentFramesWhichWouldBeFatal = 0;
_iFrameCount = 0;
_animator.Rebind();
_animator.Update(0f);
ClipInWhichTransferredAnimationWillBeSavedAsGeneric.ClearCurves();
_Recorder = new GameObjectRecorder(gameObject);
// Bind all the Transforms on the GameObject and all its children.
_Recorder.BindComponentsOfType<Transform>(gameObject, true);
}
private void LateUpdate()
{
if (_bReturn)
{
Debug.Break();//should not happen. Script should be deactivated
}
if (!_animator.enabled)
{
return;
}
List<Transform> nMyTransforms = new List<Transform>();
GetAllChildren(this.transform, ref nMyTransforms);
List<Transform> nHumanoidTransforms = new List<Transform>();
GetAllChildren(HumanoidModelWhoseAnimationWillBeCopiedToThisModel, ref nHumanoidTransforms);
//we also need to process the main transform, not only its bones
transform.localRotation = HumanoidModelWhoseAnimationWillBeCopiedToThisModel.localRotation;
transform.position = HumanoidModelWhoseAnimationWillBeCopiedToThisModel.position;//auch die position ist wichtig!!! z. B. wenn sich jemand duckt beim zielen!!
Undo.RecordObject(HumanoidModelWhoseAnimationWillBeCopiedToThisModel, "Inspector"); //Bone rotations done by a script are not recorded. Is there a trick to record these bone rotations via script anyways? For example using something like (pseudo-code) this? -> Undo.RecordObject(nTargetTransform, "Inspector");
float f = _animator.GetCurrentAnimatorClipInfo(0)[0].clip.length * (_animator.GetCurrentAnimatorStateInfo(0).normalizedTime % 1) * _animator.GetCurrentAnimatorClipInfo(0)[0].clip.frameRate;
int iCurrentFrameIDInAnimation = (int)f;
bool bIsNewFrame = (iCurrentFrameIDInAnimation != _iLastFrameIDInAnimation);
_iCount += 1;
if (!bIsNewFrame)
{
return;
}
else if (bIsNewFrame)
{
if (_iCount == (_iLastCountWithNewAnimationID - 1))
{
_iCountSequentFramesWhichWouldBeFatal += 1;// das wäre schlecht!!!!
}
_iFrameCount += 1;
_iLastCountWithNewAnimationID = _iCount;
}
_iLastFrameIDInAnimation = iCurrentFrameIDInAnimation;
foreach (Transform nHumanoidTransform in nHumanoidTransforms)
{
foreach (Transform nMyGenericTransform in nMyTransforms)
{
if (nHumanoidTransform.name == nMyGenericTransform.name)
{
nMyGenericTransform.localRotation = nHumanoidTransform.localRotation;
nMyGenericTransform.position = nHumanoidTransform.position;//auch die position ist wichtig!!! z. B. wenn sich jemand duckt beim zielen!!
Undo.RecordObject(nHumanoidTransform, "Inspector"); //Bone rotations done by a script are not recorded. Is there a trick to record these bone rotations via script anyways? For example using something like (pseudo-code) this? -> Undo.RecordObject(nTargetTransform, "Inspector");
break;
}
}
}
_iCountOfHowManyFramesWhereWrittenToAnimationFile += 1;
_Recorder.TakeSnapshot(_fDurationOf1Frame);
float fCurTime = _Recorder.currentTime;
float fExpecteded = ((_iCountOfHowManyFramesWhereWrittenToAnimationFile - 1) * _fDurationOf1Frame);
Debug.Log("Frames: " + _iFrameCount + ", recorder cur time: " + fCurTime.ToString() + " vs " + fExpecteded.ToString() + "\n");
if (_animator.GetCurrentAnimatorStateInfo(0).normalizedTime > 1 && !_animator.IsInTransition(0))
{
//animation has finished playing
if ((_iFrameCount - 1) == _iNumberOfFramesInClip)//wir müssen -1 machen, weil wir ja oben noch eines draufgeben
{
//alles ok :-)
if (_Recorder.isRecording)
{
// Save the recorded session to the clip.
_Recorder.SaveToClip(ClipInWhichTransferredAnimationWillBeSavedAsGeneric, _fFPS);
Debug.Log("Actually written to animation file: " + _iCountOfHowManyFramesWhereWrittenToAnimationFile + " duration: " + _Recorder.currentTime);// how many frames were written to the animation?
AssetDatabase.SaveAssets();//save out!!!!!!!!!!! Else, Unity only writes the animation to file when it quits
gameObject.SetActive(false);//disable this script
UnityEditor.EditorApplication.isPlaying = false;
}
}
else
{
Debug.Log("Frames: " + _iFrameCount + "\n");
//fatal error: unity didnt play all frames. Try again
_Recorder.ResetRecording();
pRestart();
}
}
}
public static void GetAllChildren(Transform parent, ref List<Transform> transforms)
{
foreach (Transform t in parent)
{
transforms.Add(t);
GetAllChildren(t, ref transforms);
}
}
}