private void SwitchToDefaultState() { currentTime = (state == defaultState) ? currentTime : 0; state = defaultState; if (state == AnimationState.Idle) { currentClip = AnimationIdle; currentEvaluator = animEvaluatorIdle; } else { currentClip = AnimationWalk; currentEvaluator = animEvaluatorWalk; } }
public override void Start() { base.Start(); if (AnimationComponent == null) { throw new InvalidOperationException("The animation component is not set"); } if (AnimationIdle == null) { throw new InvalidOperationException("Idle animation is not set"); } if (AnimationWalk == null) { throw new InvalidOperationException("Walking animation is not set"); } if (AnimationRun == null) { throw new InvalidOperationException("Running animation is not set"); } if (AnimationPunch == null) { throw new InvalidOperationException("Punching animation is not set"); } // By setting a custom blend tree builder we can override the default behavior of the animation system // Instead, BuildBlendTree(FastList<AnimationOperation> blendStack) will be called each frame AnimationComponent.BlendTreeBuilder = this; animEvaluatorIdle = AnimationComponent.Blender.CreateEvaluator(AnimationIdle); animEvaluatorWalk = AnimationComponent.Blender.CreateEvaluator(AnimationWalk); animEvaluatorRun = AnimationComponent.Blender.CreateEvaluator(AnimationRun); animEvaluatorPunch = AnimationComponent.Blender.CreateEvaluator(AnimationPunch); // Initial walk lerp walkLerpFactor = 0; animEvaluatorWalkLerp1 = animEvaluatorIdle; animEvaluatorWalkLerp2 = animEvaluatorWalk; animationClipWalkLerp1 = AnimationIdle; animationClipWalkLerp2 = AnimationWalk; }
public override void Start() { base.Start(); if (AnimationComponent == null) { throw new InvalidOperationException("The animation component is not set"); } if (AnimationIdle == null) { throw new InvalidOperationException("Idle animation is not set"); } if (AnimationWalk == null) { throw new InvalidOperationException("Walking animation is not set"); } if (AnimationShoot == null) { throw new InvalidOperationException("Shooting animation is not set"); } if (AnimationReload == null) { throw new InvalidOperationException("Reloading animation is not set"); } // By setting a custom blend tree builder we can override the default behavior of the animation system // Instead, BuildBlendTree(FastList<AnimationOperation> blendStack) will be called each frame AnimationComponent.BlendTreeBuilder = this; animEvaluatorIdle = AnimationComponent.Blender.CreateEvaluator(AnimationIdle); animEvaluatorWalk = AnimationComponent.Blender.CreateEvaluator(AnimationWalk); animEvaluatorShoot = AnimationComponent.Blender.CreateEvaluator(AnimationShoot); animEvaluatorReload = AnimationComponent.Blender.CreateEvaluator(AnimationReload); currentEvaluator = animEvaluatorIdle; currentClip = AnimationIdle; }
private void UpdateWalking() { if (runSpeed < WalkThreshold) { walkLerpFactor = runSpeed / WalkThreshold; walkLerpFactor = (float)Math.Sqrt(walkLerpFactor); // Idle-Walk blend looks really werid, so skew the factor towards walking animEvaluatorWalkLerp1 = animEvaluatorIdle; animEvaluatorWalkLerp2 = animEvaluatorWalk; animationClipWalkLerp1 = AnimationIdle; animationClipWalkLerp2 = AnimationWalk; } else { walkLerpFactor = (runSpeed - WalkThreshold) / (1.0f - WalkThreshold); animEvaluatorWalkLerp1 = animEvaluatorWalk; animEvaluatorWalkLerp2 = animEvaluatorRun; animationClipWalkLerp1 = AnimationWalk; animationClipWalkLerp2 = AnimationRun; } // Use DrawTime rather than UpdateTime var time = Game.DrawTime; // This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration long blendedMaxDuration = 0; blendedMaxDuration = (long)MathUtil.Lerp(animationClipWalkLerp1.Duration.Ticks, animationClipWalkLerp2.Duration.Ticks, walkLerpFactor); var currentTicks = TimeSpan.FromTicks((long)(currentTime * blendedMaxDuration)); currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) % blendedMaxDuration); currentTime = ((double)currentTicks.Ticks / (double)blendedMaxDuration); }
public override void Start() { base.Start(); if (AnimationComponent == null) { throw new InvalidOperationException("The animation component is not set"); } if (Animation1 == null) { throw new InvalidOperationException("Animation 1 is not set"); } if (Animation2 == null) { throw new InvalidOperationException("Animation 2 is not set"); } AnimationComponent.BlendTreeBuilder = this; anim1Evaluator = AnimationComponent.Blender.CreateEvaluator(Animation1); anim2Evaluator = AnimationComponent.Blender.CreateEvaluator(Animation2); }
private unsafe object ExportAnimation(ICommandContext commandContext, ContentManager contentManager, bool failOnEmptyAnimation) { // Read from model file var modelSkeleton = LoadSkeleton(commandContext, contentManager); // we get model skeleton to compare it to real skeleton we need to map to AdjustSkeleton(modelSkeleton); TimeSpan duration; var animationClips = LoadAnimation(commandContext, contentManager, out duration); // Fix the animation frames double startFrameSeconds = StartFrame.TotalSeconds; double endFrameSeconds = EndFrame.TotalSeconds; var startTime = CompressedTimeSpan.FromSeconds(-startFrameSeconds); foreach (var clip in animationClips) { foreach (var animationCurve in clip.Value.Curves) { animationCurve.ShiftKeys(startTime); } } var durationTimeSpan = TimeSpan.FromSeconds((endFrameSeconds - startFrameSeconds)); if (duration > durationTimeSpan) { duration = durationTimeSpan; } var animationClip = new AnimationClip { Duration = duration }; if (animationClips.Count > 0) { AnimationClip rootMotionAnimationClip = null; // If root motion is explicitely enabled, or if there is no skeleton, try to find root node and apply animation directly on TransformComponent if ((AnimationRootMotion || SkeletonUrl == null) && modelSkeleton.Nodes.Length >= 1) { // No skeleton, map root node only // TODO: For now, it seems to be located on node 1 in FBX files. Need to check if always the case, and what happens with Assimp var rootNode0 = modelSkeleton.Nodes.Length >= 1 ? modelSkeleton.Nodes[0].Name : null; var rootNode1 = modelSkeleton.Nodes.Length >= 2 ? modelSkeleton.Nodes[1].Name : null; if ((rootNode0 != null && animationClips.TryGetValue(rootNode0, out rootMotionAnimationClip)) || (rootNode1 != null && animationClips.TryGetValue(rootNode1, out rootMotionAnimationClip))) { foreach (var channel in rootMotionAnimationClip.Channels) { var curve = rootMotionAnimationClip.Curves[channel.Value.CurveIndex]; // Root motion var channelName = channel.Key; if (channelName.StartsWith("Transform.")) { animationClip.AddCurve($"[TransformComponent.Key]." + channelName.Replace("Transform.", string.Empty), curve); } // Also apply Camera curves // TODO: Add some other curves? if (channelName.StartsWith("Camera.")) { animationClip.AddCurve($"[CameraComponent.Key]." + channelName.Replace("Camera.", string.Empty), curve); } } } } // Load asset reference skeleton if (SkeletonUrl != null) { var skeleton = contentManager.Load <Skeleton>(SkeletonUrl); var skeletonMapping = new SkeletonMapping(skeleton, modelSkeleton); // Process missing nodes foreach (var nodeAnimationClipEntry in animationClips) { var nodeName = nodeAnimationClipEntry.Key; var nodeAnimationClip = nodeAnimationClipEntry.Value; var nodeIndex = modelSkeleton.Nodes.IndexOf(x => x.Name == nodeName); // Node doesn't exist in skeleton? skip it if (nodeIndex == -1 || skeletonMapping.SourceToSource[nodeIndex] != nodeIndex) { continue; } // Skip root motion node (if any) if (nodeAnimationClip == rootMotionAnimationClip) { continue; } // Find parent node var parentNodeIndex = modelSkeleton.Nodes[nodeIndex].ParentIndex; if (parentNodeIndex != -1 && skeletonMapping.SourceToSource[parentNodeIndex] != parentNodeIndex) { // Some nodes were removed, we need to concat the anim curves var currentNodeIndex = nodeIndex; var nodesToMerge = new List <Tuple <ModelNodeDefinition, AnimationBlender, AnimationClipEvaluator> >(); while (currentNodeIndex != -1 && currentNodeIndex != skeletonMapping.SourceToSource[parentNodeIndex]) { AnimationClip animationClipToMerge; AnimationClipEvaluator animationClipEvaluator = null; AnimationBlender animationBlender = null; if (animationClips.TryGetValue(modelSkeleton.Nodes[currentNodeIndex].Name, out animationClipToMerge)) { animationBlender = new AnimationBlender(); animationClipEvaluator = animationBlender.CreateEvaluator(animationClipToMerge); } nodesToMerge.Add(Tuple.Create(modelSkeleton.Nodes[currentNodeIndex], animationBlender, animationClipEvaluator)); currentNodeIndex = modelSkeleton.Nodes[currentNodeIndex].ParentIndex; } // Put them in proper parent to children order nodesToMerge.Reverse(); // Find all key times // TODO: We should detect discontinuities and keep them var animationKeysSet = new HashSet <CompressedTimeSpan>(); foreach (var node in nodesToMerge) { if (node.Item3 != null) { foreach (var curve in node.Item3.Clip.Curves) { foreach (CompressedTimeSpan time in curve.Keys) { animationKeysSet.Add(time); } } } } // Sort key times var animationKeys = animationKeysSet.ToList(); animationKeys.Sort(); var animationOperations = new FastList <AnimationOperation>(); var combinedAnimationClip = new AnimationClip(); var translationCurve = new AnimationCurve <Vector3>(); var rotationCurve = new AnimationCurve <Quaternion>(); var scaleCurve = new AnimationCurve <Vector3>(); // Evaluate at every key frame foreach (var animationKey in animationKeys) { var matrix = Matrix.Identity; // Evaluate node foreach (var node in nodesToMerge) { // Needs to be an array in order for it to be modified by the UpdateEngine, otherwise it would get passed by value var modelNodeDefinitions = new ModelNodeDefinition[1] { node.Item1 }; if (node.Item2 != null && node.Item3 != null) { // Compute AnimationClipResult animationClipResult = null; animationOperations.Clear(); animationOperations.Add(AnimationOperation.NewPush(node.Item3, animationKey)); node.Item2.Compute(animationOperations, ref animationClipResult); var updateMemberInfos = new List <UpdateMemberInfo>(); foreach (var channel in animationClipResult.Channels) { if (channel.IsUserCustomProperty) { continue; } updateMemberInfos.Add(new UpdateMemberInfo { Name = "[0]." + channel.PropertyName, DataOffset = channel.Offset }); } // TODO: Cache this var compiledUpdate = UpdateEngine.Compile(typeof(ModelNodeDefinition[]), updateMemberInfos); fixed(byte *data = animationClipResult.Data) { UpdateEngine.Run(modelNodeDefinitions, compiledUpdate, (IntPtr)data, null); } } Matrix localMatrix; var transformTRS = modelNodeDefinitions[0].Transform; Matrix.Transformation(ref transformTRS.Scale, ref transformTRS.Rotation, ref transformTRS.Position, out localMatrix); matrix = Matrix.Multiply(localMatrix, matrix); } // Done evaluating, let's decompose matrix TransformTRS transform; matrix.Decompose(out transform.Scale, out transform.Rotation, out transform.Position); // Create a key translationCurve.KeyFrames.Add(new KeyFrameData <Vector3>(animationKey, transform.Position)); rotationCurve.KeyFrames.Add(new KeyFrameData <Quaternion>(animationKey, transform.Rotation)); scaleCurve.KeyFrames.Add(new KeyFrameData <Vector3>(animationKey, transform.Scale)); } combinedAnimationClip.AddCurve($"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Position)}", translationCurve); combinedAnimationClip.AddCurve($"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Rotation)}", rotationCurve); combinedAnimationClip.AddCurve($"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Scale)}", scaleCurve); nodeAnimationClip = combinedAnimationClip; } var transformStart = $"{nameof(ModelNodeTransformation.Transform)}."; var transformPosition = $"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Position)}"; foreach (var channel in nodeAnimationClip.Channels) { var curve = nodeAnimationClip.Curves[channel.Value.CurveIndex]; // TODO: Root motion var channelName = channel.Key; if (channelName.StartsWith(transformStart)) { if (channelName == transformPosition) { // Translate node with parent 0 using PivotPosition var keyFrames = ((AnimationCurve <Vector3>)curve).KeyFrames; for (int i = 0; i < keyFrames.Count; ++i) { if (parentNodeIndex == 0) { keyFrames.Items[i].Value -= PivotPosition; } keyFrames.Items[i].Value *= ScaleImport; } } animationClip.AddCurve($"[ModelComponent.Key].Skeleton.NodeTransformations[{skeletonMapping.SourceToTarget[nodeIndex]}]." + channelName, curve); } } } } if (ImportCustomAttributes) { // Add clips clips animating other properties than node transformations foreach (var nodeAnimationClipPair in animationClips) { var nodeName = nodeAnimationClipPair.Key; var nodeAnimationClip = nodeAnimationClipPair.Value; foreach (var channel in nodeAnimationClip.Channels) { var channelName = channel.Key; var channelValue = channel.Value; if (channelValue.IsUserCustomProperty) { animationClip.AddCurve(nodeName + "_" + channelName, nodeAnimationClip.Curves[channel.Value.CurveIndex], true); } } } } } if (animationClip.Channels.Count == 0) { var logString = $"File {SourcePath} doesn't have any animation information."; if (failOnEmptyAnimation) { commandContext.Logger.Error(logString); return(null); } commandContext.Logger.Info(logString); } else { if (animationClip.Duration.Ticks == 0) { commandContext.Logger.Verbose($"File {SourcePath} has a 0 tick long animation."); } // Optimize and set common parameters animationClip.RepeatMode = AnimationRepeatMode; animationClip.Optimize(); } return(animationClip); }
/// <summary> /// Creates a new animation pop operation. /// </summary> /// <param name="evaluator">The evaluator.</param> /// <param name="time">The time.</param> /// <returns></returns> public static AnimationOperation NewPop(AnimationClipEvaluator evaluator, TimeSpan time) { return(new AnimationOperation { Type = AnimationOperationType.Pop, Evaluator = evaluator, Time = time }); }
/// <summary> /// Creates a new animation push operation. /// </summary> /// <param name="evaluator">The evaluator.</param> /// <returns></returns> public static AnimationOperation NewPush(AnimationClipEvaluator evaluator) { return(new AnimationOperation { Type = AnimationOperationType.Push, Evaluator = evaluator, Time = TimeSpan.Zero }); }
private ClientPredictionSnapshotsComponent _clientPredictionSnapshotsComponent; // Optional component public override void Start() { base.Start(); var parentEntity = Entity.GetParent(); Debug.Assert(parentEntity != null); var networkEntityViewComp = parentEntity.Get <NetworkEntityViewComponent>(); var networkedEntity = networkEntityViewComp.NetworkedEntity; _networkEntityComponent = networkedEntity.Get <NetworkEntityComponent>(); Debug.Assert(_networkEntityComponent != null); _movementSnapshotsComponent = networkedEntity.Get <MovementSnapshotsComponent>(); Debug.Assert(_movementSnapshotsComponent != null); _clientPredictionSnapshotsComponent = networkedEntity.Get <ClientPredictionSnapshotsComponent>(); _gameClockManager = Services.GetSafeServiceAs <GameClockManager>(); _networkService = Services.GetService <IGameNetworkService>(); if (AnimationComponent == null) { throw new InvalidOperationException("The animation component is not set"); } if (AnimationIdle == null) { throw new InvalidOperationException("Idle animation is not set"); } if (AnimationWalk == null) { throw new InvalidOperationException("Walking animation is not set"); } if (AnimationRun == null) { throw new InvalidOperationException("Running animation is not set"); } if (AnimationJumpStart == null) { throw new InvalidOperationException("Jumping animation is not set"); } if (AnimationJumpMid == null) { throw new InvalidOperationException("Airborne animation is not set"); } if (AnimationJumpEnd == null) { throw new InvalidOperationException("Landing animation is not set"); } // By setting a custom blend tree builder we can override the default behavior of the animation system // Instead, BuildBlendTree(FastList<AnimationOperation> blendStack) will be called each frame AnimationComponent.BlendTreeBuilder = this; animEvaluatorIdle = AnimationComponent.Blender.CreateEvaluator(AnimationIdle); animEvaluatorWalk = AnimationComponent.Blender.CreateEvaluator(AnimationWalk); animEvaluatorRun = AnimationComponent.Blender.CreateEvaluator(AnimationRun); animEvaluatorJumpStart = AnimationComponent.Blender.CreateEvaluator(AnimationJumpStart); animEvaluatorJumpMid = AnimationComponent.Blender.CreateEvaluator(AnimationJumpMid); animEvaluatorJumpEnd = AnimationComponent.Blender.CreateEvaluator(AnimationJumpEnd); // Initial walk lerp walkLerpFactor = 0; animEvaluatorWalkLerp1 = animEvaluatorIdle; animEvaluatorWalkLerp2 = animEvaluatorWalk; animationClipWalkLerp1 = AnimationIdle; animationClipWalkLerp2 = AnimationWalk; }
private unsafe object ExportAnimation(ICommandContext commandContext, ContentManager contentManager) { // Read from model file var modelSkeleton = LoadSkeleton(commandContext, contentManager); // we get model skeleton to compare it to real skeleton we need to map to var animationClips = LoadAnimation(commandContext, contentManager); AnimationClip animationClip = null; if (animationClips.Count > 0) { animationClip = new AnimationClip(); AnimationClip rootMotionAnimationClip = null; // If root motion is explicitely enabled, or if there is no skeleton, try to find root node and apply animation directly on TransformComponent if ((AnimationRootMotion || SkeletonUrl == null) && modelSkeleton.Nodes.Length >= 1) { // No skeleton, map root node only // TODO: For now, it seems to be located on node 1 in FBX files. Need to check if always the case, and what happens with Assimp var rootNode0 = modelSkeleton.Nodes.Length >= 1 ? modelSkeleton.Nodes[0].Name : null; var rootNode1 = modelSkeleton.Nodes.Length >= 2 ? modelSkeleton.Nodes[1].Name : null; if ((rootNode0 != null && animationClips.TryGetValue(rootNode0, out rootMotionAnimationClip)) || (rootNode1 != null && animationClips.TryGetValue(rootNode1, out rootMotionAnimationClip))) { foreach (var channel in rootMotionAnimationClip.Channels) { var curve = rootMotionAnimationClip.Curves[channel.Value.CurveIndex]; // Root motion var channelName = channel.Key; if (channelName.StartsWith("Transform.")) { animationClip.AddCurve($"[TransformComponent.Key]." + channelName.Replace("Transform.", string.Empty), curve); } // Also apply Camera curves // TODO: Add some other curves? if (channelName.StartsWith("Camera.")) { animationClip.AddCurve($"[CameraComponent.Key]." + channelName.Replace("Camera.", string.Empty), curve); } } // Take max of durations if (animationClip.Duration < rootMotionAnimationClip.Duration) { animationClip.Duration = rootMotionAnimationClip.Duration; } } } // Load asset reference skeleton if (SkeletonUrl != null) { var skeleton = contentManager.Load <Skeleton>(SkeletonUrl); var skeletonMapping = new SkeletonMapping(skeleton, modelSkeleton); // Process missing nodes foreach (var nodeAnimationClipEntry in animationClips) { var nodeName = nodeAnimationClipEntry.Key; var nodeAnimationClip = nodeAnimationClipEntry.Value; var nodeIndex = modelSkeleton.Nodes.IndexOf(x => x.Name == nodeName); // Node doesn't exist in skeleton? skip it if (nodeIndex == -1 || skeletonMapping.SourceToSource[nodeIndex] != nodeIndex) { continue; } // Skip root motion node (if any) if (nodeAnimationClip == rootMotionAnimationClip) { continue; } // Find parent node var parentNodeIndex = modelSkeleton.Nodes[nodeIndex].ParentIndex; if (parentNodeIndex != -1 && skeletonMapping.SourceToSource[parentNodeIndex] != parentNodeIndex) { // Some nodes were removed, we need to concat the anim curves var currentNodeIndex = nodeIndex; var nodesToMerge = new List <Tuple <ModelNodeDefinition, AnimationBlender, AnimationClipEvaluator> >(); while (currentNodeIndex != -1 && currentNodeIndex != skeletonMapping.SourceToSource[parentNodeIndex]) { AnimationClip animationClipToMerge; AnimationClipEvaluator animationClipEvaluator = null; AnimationBlender animationBlender = null; if (animationClips.TryGetValue(modelSkeleton.Nodes[currentNodeIndex].Name, out animationClipToMerge)) { animationBlender = new AnimationBlender(); animationClipEvaluator = animationBlender.CreateEvaluator(animationClipToMerge); } nodesToMerge.Add(Tuple.Create(modelSkeleton.Nodes[currentNodeIndex], animationBlender, animationClipEvaluator)); currentNodeIndex = modelSkeleton.Nodes[currentNodeIndex].ParentIndex; } // Put them in proper parent to children order nodesToMerge.Reverse(); // Find all key times // TODO: We should detect discontinuities and keep them var animationKeysSet = new HashSet <CompressedTimeSpan>(); foreach (var node in nodesToMerge) { foreach (var curve in node.Item3.Clip.Curves) { foreach (CompressedTimeSpan time in curve.Keys) { animationKeysSet.Add(time); } } } // Sort key times var animationKeys = animationKeysSet.ToList(); animationKeys.Sort(); var animationOperations = new FastList <AnimationOperation>(); var combinedAnimationClip = new AnimationClip(); var translationCurve = new AnimationCurve <Vector3>(); var rotationCurve = new AnimationCurve <Quaternion>(); var scaleCurve = new AnimationCurve <Vector3>(); // Evaluate at every key frame foreach (var animationKey in animationKeys) { var matrix = Matrix.Identity; // Evaluate node foreach (var node in nodesToMerge) { // Get default position var modelNodeDefinition = node.Item1; // Compute AnimationClipResult animationClipResult = null; animationOperations.Clear(); animationOperations.Add(AnimationOperation.NewPush(node.Item3, animationKey)); node.Item2.Compute(animationOperations, ref animationClipResult); var updateMemberInfos = new List <UpdateMemberInfo>(); foreach (var channel in animationClipResult.Channels) { updateMemberInfos.Add(new UpdateMemberInfo { Name = channel.PropertyName, DataOffset = channel.Offset }); } // TODO: Cache this var compiledUpdate = UpdateEngine.Compile(typeof(ModelNodeDefinition), updateMemberInfos); unsafe { fixed(byte *data = animationClipResult.Data) UpdateEngine.Run(modelNodeDefinition, compiledUpdate, (IntPtr)data, null); } Matrix localMatrix; TransformComponent.CreateMatrixTRS(ref modelNodeDefinition.Transform.Position, ref modelNodeDefinition.Transform.Rotation, ref modelNodeDefinition.Transform.Scale, out localMatrix); matrix = Matrix.Multiply(localMatrix, matrix); } // Done evaluating, let's decompose matrix TransformTRS transform; matrix.Decompose(out transform.Scale, out transform.Rotation, out transform.Position); // Create a key translationCurve.KeyFrames.Add(new KeyFrameData <Vector3>(animationKey, transform.Position)); rotationCurve.KeyFrames.Add(new KeyFrameData <Quaternion>(animationKey, transform.Rotation)); scaleCurve.KeyFrames.Add(new KeyFrameData <Vector3>(animationKey, transform.Scale)); } combinedAnimationClip.AddCurve($"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Position)}", translationCurve); combinedAnimationClip.AddCurve($"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Rotation)}", rotationCurve); combinedAnimationClip.AddCurve($"{nameof(ModelNodeTransformation.Transform)}.{nameof(TransformTRS.Scale)}", scaleCurve); nodeAnimationClip = combinedAnimationClip; } foreach (var channel in nodeAnimationClip.Channels) { var curve = nodeAnimationClip.Curves[channel.Value.CurveIndex]; // TODO: Root motion var channelName = channel.Key; if (channelName.StartsWith("Transform.")) { animationClip.AddCurve($"[ModelComponent.Key].Skeleton.NodeTransformations[{skeletonMapping.SourceToTarget[nodeIndex]}]." + channelName, curve); } } // Take max of durations if (animationClip.Duration < nodeAnimationClip.Duration) { animationClip.Duration = nodeAnimationClip.Duration; } } } } if (animationClip == null) { commandContext.Logger.Info("File {0} has an empty animation.", SourcePath); } else { if (animationClip.Duration.Ticks == 0) { commandContext.Logger.Warning("File {0} has a 0 tick long animation.", SourcePath); } // Optimize and set common parameters animationClip.RepeatMode = AnimationRepeatMode; animationClip.Optimize(); } return(animationClip); }