using System; using System.Collections.Generic; using UnityEngine.Animations; #if !UNITY_2020_2_OR_NEWER using UnityEngine.Experimental.Animations; #endif using UnityEngine.Playables; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityEngine.Timeline { /// /// Flags specifying which offset fields to match /// [Flags] public enum MatchTargetFields { /// /// Translation X value /// PositionX = 1 << 0, /// /// Translation Y value /// PositionY = 1 << 1, /// /// Translation Z value /// PositionZ = 1 << 2, /// /// Rotation Euler Angle X value /// RotationX = 1 << 3, /// /// Rotation Euler Angle Y value /// RotationY = 1 << 4, /// /// Rotation Euler Angle Z value /// RotationZ = 1 << 5 } /// /// Describes what is used to set the starting position and orientation of each Animation Track. /// /// /// By default, each Animation Track uses ApplyTransformOffsets to start from a set position and orientation. /// To offset each Animation Track based on the current position and orientation in the scene, use ApplySceneOffsets. /// public enum TrackOffset { /// /// Use this setting to offset each Animation Track based on a set position and orientation. /// ApplyTransformOffsets, /// /// Use this setting to offset each Animation Track based on the current position and orientation in the scene. /// ApplySceneOffsets, /// /// Use this setting to offset root transforms based on the state of the animator. /// /// /// Only use this setting to support legacy Animation Tracks. This mode may be deprecated in a future release. /// /// In Auto mode, when the animator bound to the animation track contains an AnimatorController, it offsets all animations similar to ApplySceneOffsets. /// If no controller is assigned, then all offsets are set to start from a fixed position and orientation, similar to ApplyTransformOffsets. /// In Auto mode, in most cases, root transforms are not affected by local scale or Animator.humanScale, unless the animator has an AnimatorController and Animator.applyRootMotion is set to true. /// Auto } // offset mode enum AppliedOffsetMode { NoRootTransform, TransformOffset, SceneOffset, TransformOffsetLegacy, SceneOffsetLegacy, SceneOffsetEditor, // scene offset mode in editor SceneOffsetLegacyEditor, } // separate from the enum to hide them from UI elements static class MatchTargetFieldConstants { public static MatchTargetFields All = MatchTargetFields.PositionX | MatchTargetFields.PositionY | MatchTargetFields.PositionZ | MatchTargetFields.RotationX | MatchTargetFields.RotationY | MatchTargetFields.RotationZ; public static MatchTargetFields None = 0; public static MatchTargetFields Position = MatchTargetFields.PositionX | MatchTargetFields.PositionY | MatchTargetFields.PositionZ; public static MatchTargetFields Rotation = MatchTargetFields.RotationX | MatchTargetFields.RotationY | MatchTargetFields.RotationZ; public static bool HasAny(this MatchTargetFields me, MatchTargetFields fields) { return (me & fields) != None; } public static MatchTargetFields Toggle(this MatchTargetFields me, MatchTargetFields flag) { return me ^ flag; } } /// /// A Timeline track used for playing back animations on an Animator. /// [Serializable] [TrackClipType(typeof(AnimationPlayableAsset), false)] [TrackBindingType(typeof(Animator))] [ExcludeFromPreset] [TimelineHelpURL(typeof(AnimationTrack))] public partial class AnimationTrack : TrackAsset, ILayerable { const string k_DefaultInfiniteClipName = "Recorded"; const string k_DefaultRecordableClipName = "Recorded"; [SerializeField, FormerlySerializedAs("m_OpenClipPreExtrapolation")] TimelineClip.ClipExtrapolation m_InfiniteClipPreExtrapolation = TimelineClip.ClipExtrapolation.None; [SerializeField, FormerlySerializedAs("m_OpenClipPostExtrapolation")] TimelineClip.ClipExtrapolation m_InfiniteClipPostExtrapolation = TimelineClip.ClipExtrapolation.None; [SerializeField, FormerlySerializedAs("m_OpenClipOffsetPosition")] Vector3 m_InfiniteClipOffsetPosition = Vector3.zero; [SerializeField, FormerlySerializedAs("m_OpenClipOffsetEulerAngles")] Vector3 m_InfiniteClipOffsetEulerAngles = Vector3.zero; [SerializeField, FormerlySerializedAs("m_OpenClipTimeOffset")] double m_InfiniteClipTimeOffset; [SerializeField, FormerlySerializedAs("m_OpenClipRemoveOffset")] bool m_InfiniteClipRemoveOffset; // cached value for remove offset [SerializeField] bool m_InfiniteClipApplyFootIK = true; [SerializeField, HideInInspector] AnimationPlayableAsset.LoopMode mInfiniteClipLoop = AnimationPlayableAsset.LoopMode.UseSourceAsset; [SerializeField] MatchTargetFields m_MatchTargetFields = MatchTargetFieldConstants.All; [SerializeField] Vector3 m_Position = Vector3.zero; [SerializeField] Vector3 m_EulerAngles = Vector3.zero; [SerializeField] AvatarMask m_AvatarMask; [SerializeField] bool m_ApplyAvatarMask = true; [SerializeField] TrackOffset m_TrackOffset = TrackOffset.ApplyTransformOffsets; [SerializeField, HideInInspector] AnimationClip m_InfiniteClip; #if UNITY_EDITOR private AnimationClip m_DefaultPoseClip; private AnimationClip m_CachedPropertiesClip; private int m_CachedHash; private EditorCurveBinding[] m_CachedBindings; AnimationOffsetPlayable m_ClipOffset; private Vector3 m_SceneOffsetPosition = Vector3.zero; private Vector3 m_SceneOffsetRotation = Vector3.zero; private bool m_HasPreviewComponents = false; #endif /// /// The translation offset of the entire track. /// public Vector3 position { get { return m_Position; } set { m_Position = value; } } /// /// The rotation offset of the entire track, expressed as a quaternion. /// public Quaternion rotation { get { return Quaternion.Euler(m_EulerAngles); } set { m_EulerAngles = value.eulerAngles; } } /// /// The euler angle representation of the rotation offset of the entire track. /// public Vector3 eulerAngles { get { return m_EulerAngles; } set { m_EulerAngles = value; } } /// /// Specifies whether to apply track offsets to all clips on the track. /// /// /// This can be used to offset all clips on a track, in addition to the clips individual offsets. /// [Obsolete("applyOffset is deprecated. Use trackOffset instead", true)] public bool applyOffsets { get { return false; } set { } } /// /// Specifies what is used to set the starting position and orientation of an Animation Track. /// /// /// Track Offset is only applied when the Animation Track contains animation that modifies the root Transform. /// public TrackOffset trackOffset { get { return m_TrackOffset; } set { m_TrackOffset = value; } } /// /// Specifies which fields to match when aligning offsets of clips. /// public MatchTargetFields matchTargetFields { get { return m_MatchTargetFields; } set { m_MatchTargetFields = value & MatchTargetFieldConstants.All; } } /// /// An AnimationClip storing the data for an infinite track. /// /// /// The value of this property is null when the AnimationTrack is in Clip Mode. /// public AnimationClip infiniteClip { get { return m_InfiniteClip; } internal set { m_InfiniteClip = value; } } // saved value for converting to/from infinite mode internal bool infiniteClipRemoveOffset { get { return m_InfiniteClipRemoveOffset; } set { m_InfiniteClipRemoveOffset = value; } } /// /// Specifies the AvatarMask to be applied to all clips on the track. /// /// /// Applying an AvatarMask to an animation track will allow discarding portions of the animation being applied on the track. /// public AvatarMask avatarMask { get { return m_AvatarMask; } set { m_AvatarMask = value; } } /// /// Specifies whether to apply the AvatarMask to the track. /// public bool applyAvatarMask { get { return m_ApplyAvatarMask; } set { m_ApplyAvatarMask = value; } } // is this track compilable internal override bool CanCompileClips() { return !muted && (m_Clips.Count > 0 || (m_InfiniteClip != null && !m_InfiniteClip.empty)); } /// public override IEnumerable outputs { get { yield return AnimationPlayableBinding.Create(name, this); } } /// /// Specifies whether the Animation Track has clips, or is in infinite mode. /// public bool inClipMode { get { return clips != null && clips.Length != 0; } } /// /// The translation offset of a track in infinite mode. /// public Vector3 infiniteClipOffsetPosition { get { return m_InfiniteClipOffsetPosition; } set { m_InfiniteClipOffsetPosition = value; } } /// /// The rotation offset of a track in infinite mode. /// public Quaternion infiniteClipOffsetRotation { get { return Quaternion.Euler(m_InfiniteClipOffsetEulerAngles); } set { m_InfiniteClipOffsetEulerAngles = value.eulerAngles; } } /// /// The euler angle representation of the rotation offset of the track when in infinite mode. /// public Vector3 infiniteClipOffsetEulerAngles { get { return m_InfiniteClipOffsetEulerAngles; } set { m_InfiniteClipOffsetEulerAngles = value; } } internal bool infiniteClipApplyFootIK { get { return m_InfiniteClipApplyFootIK; } set { m_InfiniteClipApplyFootIK = value; } } internal double infiniteClipTimeOffset { get { return m_InfiniteClipTimeOffset; } set { m_InfiniteClipTimeOffset = value; } } /// /// The saved state of pre-extrapolation for clips converted to infinite mode. /// public TimelineClip.ClipExtrapolation infiniteClipPreExtrapolation { get { return m_InfiniteClipPreExtrapolation; } set { m_InfiniteClipPreExtrapolation = value; } } /// /// The saved state of post-extrapolation for clips when converted to infinite mode. /// public TimelineClip.ClipExtrapolation infiniteClipPostExtrapolation { get { return m_InfiniteClipPostExtrapolation; } set { m_InfiniteClipPostExtrapolation = value; } } /// /// The saved state of animation clip loop state when converted to infinite mode /// internal AnimationPlayableAsset.LoopMode infiniteClipLoop { get { return mInfiniteClipLoop; } set { mInfiniteClipLoop = value; } } [ContextMenu("Reset Offsets")] void ResetOffsets() { m_Position = Vector3.zero; m_EulerAngles = Vector3.zero; UpdateClipOffsets(); } /// /// Creates a TimelineClip on this track that uses an AnimationClip. /// /// Source animation clip of the resulting TimelineClip. /// A new TimelineClip which has an AnimationPlayableAsset asset attached. public TimelineClip CreateClip(AnimationClip clip) { if (clip == null) return null; var newClip = CreateClip(); AssignAnimationClip(newClip, clip); return newClip; } /// /// Creates an AnimationClip that stores the data for an infinite track. /// /// /// If an infiniteClip already exists, this method produces no result, even if you provide a different value /// for infiniteClipName. /// /// /// This method can't create an infinite clip for an AnimationTrack that contains one or more Timeline clips. /// Use AnimationTrack.inClipMode to determine whether it is possible to create an infinite clip on an AnimationTrack. /// /// /// When used from the editor, this method attempts to save the created infinite clip to the TimelineAsset. /// The TimelineAsset must already exist in the AssetDatabase to save the infinite clip. If the TimelineAsset /// does not exist, the infinite clip is still created but it is not saved. /// /// /// The name of the AnimationClip to create. /// This method does not ensure unique names. If you want a unique clip name, you must provide one. /// See ObjectNames.GetUniqueName for information on a method that creates unique names. /// public void CreateInfiniteClip(string infiniteClipName) { if (inClipMode) { Debug.LogWarning("CreateInfiniteClip cannot create an infinite clip for an AnimationTrack that contains one or more Timeline Clips."); return; } if (m_InfiniteClip != null) return; m_InfiniteClip = TimelineCreateUtilities.CreateAnimationClipForTrack(string.IsNullOrEmpty(infiniteClipName) ? k_DefaultInfiniteClipName : infiniteClipName, this, false); } /// /// Creates a TimelineClip, AnimationPlayableAsset and an AnimationClip. Use this clip to record in a timeline. /// /// /// When used from the editor, this method attempts to save the created recordable clip to the TimelineAsset. /// The TimelineAsset must already exist in the AssetDatabase to save the recordable clip. If the TimelineAsset /// does not exist, the recordable clip is still created but it is not saved. /// /// /// The name of the AnimationClip to create. /// This method does not ensure unique names. If you want a unique clip name, you must provide one. /// See ObjectNames.GetUniqueName for information on a method that creates unique names. /// /// /// Returns a new TimelineClip with an AnimationPlayableAsset asset attached. /// public TimelineClip CreateRecordableClip(string animClipName) { var clip = TimelineCreateUtilities.CreateAnimationClipForTrack(string.IsNullOrEmpty(animClipName) ? k_DefaultRecordableClipName : animClipName, this, false); var timelineClip = CreateClip(clip); timelineClip.displayName = animClipName; timelineClip.recordable = true; timelineClip.start = 0; timelineClip.duration = 1; var apa = timelineClip.asset as AnimationPlayableAsset; if (apa != null) apa.removeStartOffset = false; return timelineClip; } #if UNITY_EDITOR internal Vector3 sceneOffsetPosition { get { return m_SceneOffsetPosition; } set { m_SceneOffsetPosition = value; } } internal Vector3 sceneOffsetRotation { get { return m_SceneOffsetRotation; } set { m_SceneOffsetRotation = value; } } internal bool hasPreviewComponents { get { if (m_HasPreviewComponents) return true; var parentTrack = parent as AnimationTrack; if (parentTrack != null) { return parentTrack.hasPreviewComponents; } return false; } } #endif /// /// Used to initialize default values on a newly created clip /// /// The clip added to the track protected override void OnCreateClip(TimelineClip clip) { var extrapolation = TimelineClip.ClipExtrapolation.None; if (!isSubTrack) extrapolation = TimelineClip.ClipExtrapolation.Hold; clip.preExtrapolationMode = extrapolation; clip.postExtrapolationMode = extrapolation; } protected internal override int CalculateItemsHash() { return GetAnimationClipHash(m_InfiniteClip).CombineHash(base.CalculateItemsHash()); } internal void UpdateClipOffsets() { #if UNITY_EDITOR if (m_ClipOffset.IsValid()) { m_ClipOffset.SetPosition(position); m_ClipOffset.SetRotation(rotation); } #endif } Playable CompileTrackPlayable(PlayableGraph graph, AnimationTrack track, GameObject go, IntervalTree tree, AppliedOffsetMode mode) { var mixer = AnimationMixerPlayable.Create(graph, track.clips.Length); for (int i = 0; i < track.clips.Length; i++) { var c = track.clips[i]; var asset = c.asset as PlayableAsset; if (asset == null) continue; var animationAsset = asset as AnimationPlayableAsset; if (animationAsset != null) animationAsset.appliedOffsetMode = mode; var source = asset.CreatePlayable(graph, go); if (source.IsValid()) { var clip = new RuntimeClip(c, source, mixer); tree.Add(clip); graph.Connect(source, 0, mixer, i); mixer.SetInputWeight(i, 0.0f); } } if (!track.AnimatesRootTransform()) return mixer; return ApplyTrackOffset(graph, mixer, go, mode); } /// /// Returns Playable.Null Playable ILayerable.CreateLayerMixer(PlayableGraph graph, GameObject go, int inputCount) { return Playable.Null; } internal override Playable CreateMixerPlayableGraph(PlayableGraph graph, GameObject go, IntervalTree tree) { if (isSubTrack) throw new InvalidOperationException("Nested animation tracks should never be asked to create a graph directly"); List flattenTracks = new List(); if (CanCompileClips()) flattenTracks.Add(this); var genericRoot = GetGenericRootNode(go); var animatesRootTransformNoMask = AnimatesRootTransform(); var animatesRootTransform = animatesRootTransformNoMask && !IsRootTransformDisabledByMask(go, genericRoot); foreach (var subTrack in GetChildTracks()) { var child = subTrack as AnimationTrack; if (child != null && child.CanCompileClips()) { var childAnimatesRoot = child.AnimatesRootTransform(); animatesRootTransformNoMask |= child.AnimatesRootTransform(); animatesRootTransform |= (childAnimatesRoot && !child.IsRootTransformDisabledByMask(go, genericRoot)); flattenTracks.Add(child); } } // figure out which mode to apply AppliedOffsetMode mode = GetOffsetMode(go, animatesRootTransform); int defaultBlendCount = GetDefaultBlendCount(); var layerMixer = CreateGroupMixer(graph, go, flattenTracks.Count + defaultBlendCount); for (int c = 0; c < flattenTracks.Count; c++) { int blendIndex = c + defaultBlendCount; // if the child is masking the root transform, compile it as if we are non-root mode var childMode = mode; if (mode != AppliedOffsetMode.NoRootTransform && flattenTracks[c].IsRootTransformDisabledByMask(go, genericRoot)) childMode = AppliedOffsetMode.NoRootTransform; var compiledTrackPlayable = flattenTracks[c].inClipMode ? CompileTrackPlayable(graph, flattenTracks[c], go, tree, childMode) : flattenTracks[c].CreateInfiniteTrackPlayable(graph, go, tree, childMode); graph.Connect(compiledTrackPlayable, 0, layerMixer, blendIndex); layerMixer.SetInputWeight(blendIndex, flattenTracks[c].inClipMode ? 0 : 1); if (flattenTracks[c].applyAvatarMask && flattenTracks[c].avatarMask != null) { layerMixer.SetLayerMaskFromAvatarMask((uint)blendIndex, flattenTracks[c].avatarMask); } } var requiresMotionXPlayable = RequiresMotionXPlayable(mode, go); // In the editor, we may require the motion X playable if we are animating the root transform but it is masked out, because the default poses // need to properly update root motion requiresMotionXPlayable |= (defaultBlendCount > 0 && RequiresMotionXPlayable(GetOffsetMode(go, animatesRootTransformNoMask), go)); // Attach the default poses AttachDefaultBlend(graph, layerMixer, requiresMotionXPlayable); // motionX playable not required in scene offset mode, or root transform mode Playable mixer = layerMixer; if (requiresMotionXPlayable) { // If we are animating a root transform, add the motionX to delta playable as the root node var motionXToDelta = AnimationMotionXToDeltaPlayable.Create(graph); graph.Connect(mixer, 0, motionXToDelta, 0); motionXToDelta.SetInputWeight(0, 1.0f); motionXToDelta.SetAbsoluteMotion(UsesAbsoluteMotion(mode)); mixer = (Playable)motionXToDelta; } #if UNITY_EDITOR if (!Application.isPlaying) { var animator = GetBinding(go != null ? go.GetComponent() : null); if (animator != null) { GameObject targetGO = animator.gameObject; IAnimationWindowPreview[] previewComponents = targetGO.GetComponents(); m_HasPreviewComponents = previewComponents.Length > 0; if (m_HasPreviewComponents) { foreach (var component in previewComponents) { mixer = component.BuildPreviewGraph(graph, mixer); } } } } #endif return mixer; } private int GetDefaultBlendCount() { #if UNITY_EDITOR if (Application.isPlaying) return 0; return ((m_CachedPropertiesClip != null) ? 1 : 0) + ((m_DefaultPoseClip != null) ? 1 : 0); #else return 0; #endif } // Attaches the default blends to the layer mixer // the base layer is a default clip of all driven properties // the next layer is optionally the desired default pose (in the case of humanoid, the TPose) private void AttachDefaultBlend(PlayableGraph graph, AnimationLayerMixerPlayable mixer, bool requireOffset) { #if UNITY_EDITOR if (Application.isPlaying) return; int mixerInput = 0; if (m_CachedPropertiesClip) { var cachedPropertiesClip = AnimationClipPlayable.Create(graph, m_CachedPropertiesClip); cachedPropertiesClip.SetApplyFootIK(false); var defaults = (Playable)cachedPropertiesClip; if (requireOffset) defaults = AttachOffsetPlayable(graph, defaults, m_SceneOffsetPosition, Quaternion.Euler(m_SceneOffsetRotation)); graph.Connect(defaults, 0, mixer, mixerInput); mixer.SetInputWeight(mixerInput, 1.0f); mixerInput++; } if (m_DefaultPoseClip) { var defaultPose = AnimationClipPlayable.Create(graph, m_DefaultPoseClip); defaultPose.SetApplyFootIK(false); var blendDefault = (Playable)defaultPose; if (requireOffset) blendDefault = AttachOffsetPlayable(graph, blendDefault, m_SceneOffsetPosition, Quaternion.Euler(m_SceneOffsetRotation)); graph.Connect(blendDefault, 0, mixer, mixerInput); mixer.SetInputWeight(mixerInput, 1.0f); } #endif } private Playable AttachOffsetPlayable(PlayableGraph graph, Playable playable, Vector3 pos, Quaternion rot) { var offsetPlayable = AnimationOffsetPlayable.Create(graph, pos, rot, 1); offsetPlayable.SetInputWeight(0, 1.0f); graph.Connect(playable, 0, offsetPlayable, 0); return offsetPlayable; } #if UNITY_EDITOR private static string k_DefaultHumanoidClipPath = "Packages/com.unity.timeline/Editor/StyleSheets/res/HumanoidDefault.anim"; private static AnimationClip s_DefaultHumanoidClip = null; AnimationClip GetDefaultHumanoidClip() { if (s_DefaultHumanoidClip == null) { s_DefaultHumanoidClip = AssetDatabase.LoadAssetAtPath(k_DefaultHumanoidClipPath); if (s_DefaultHumanoidClip == null) Debug.LogError("Could not load default humanoid animation clip for Timeline"); } return s_DefaultHumanoidClip; } #endif bool RequiresMotionXPlayable(AppliedOffsetMode mode, GameObject gameObject) { if (mode == AppliedOffsetMode.NoRootTransform) return false; if (mode == AppliedOffsetMode.SceneOffsetLegacy) { var animator = GetBinding(gameObject != null ? gameObject.GetComponent() : null); return animator != null && animator.hasRootMotion; } return true; } static bool UsesAbsoluteMotion(AppliedOffsetMode mode) { #if UNITY_EDITOR // in editor, previewing is always done in absolute motion if (!Application.isPlaying) return true; #endif return mode != AppliedOffsetMode.SceneOffset && mode != AppliedOffsetMode.SceneOffsetLegacy; } bool HasController(GameObject gameObject) { var animator = GetBinding(gameObject != null ? gameObject.GetComponent() : null); return animator != null && animator.runtimeAnimatorController != null; } internal Animator GetBinding(PlayableDirector director) { if (director == null) return null; UnityEngine.Object key = this; if (isSubTrack) key = parent; UnityEngine.Object binding = null; if (director != null) binding = director.GetGenericBinding(key); Animator animator = null; if (binding != null) // the binding can be an animator or game object { animator = binding as Animator; var gameObject = binding as GameObject; if (animator == null && gameObject != null) animator = gameObject.GetComponent(); } return animator; } static AnimationLayerMixerPlayable CreateGroupMixer(PlayableGraph graph, GameObject go, int inputCount) { #if UNITY_2022_2_OR_NEWER return AnimationLayerMixerPlayable.Create(graph, inputCount, false); #else return AnimationLayerMixerPlayable.Create(graph, inputCount); #endif } Playable CreateInfiniteTrackPlayable(PlayableGraph graph, GameObject go, IntervalTree tree, AppliedOffsetMode mode) { if (m_InfiniteClip == null) return Playable.Null; var mixer = AnimationMixerPlayable.Create(graph, 1); // In infinite mode, we always force the loop mode of the clip off because the clip keys are offset in infinite mode // which causes loop to behave different. // The inline curve editor never shows loops in infinite mode. var playable = AnimationPlayableAsset.CreatePlayable(graph, m_InfiniteClip, m_InfiniteClipOffsetPosition, m_InfiniteClipOffsetEulerAngles, false, mode, infiniteClipApplyFootIK, AnimationPlayableAsset.LoopMode.Off); if (playable.IsValid()) { tree.Add(new InfiniteRuntimeClip(playable)); graph.Connect(playable, 0, mixer, 0); mixer.SetInputWeight(0, 1.0f); } if (!AnimatesRootTransform()) return mixer; var rootTrack = isSubTrack ? (AnimationTrack)parent : this; return rootTrack.ApplyTrackOffset(graph, mixer, go, mode); } Playable ApplyTrackOffset(PlayableGraph graph, Playable root, GameObject go, AppliedOffsetMode mode) { #if UNITY_EDITOR m_ClipOffset = AnimationOffsetPlayable.Null; #endif // offsets don't apply in scene offset, or if there is no root transform (globally or on this track) if (mode == AppliedOffsetMode.SceneOffsetLegacy || mode == AppliedOffsetMode.SceneOffset || mode == AppliedOffsetMode.NoRootTransform ) return root; var pos = position; var rot = rotation; #if UNITY_EDITOR // in the editor use the preview position to playback from if available if (mode == AppliedOffsetMode.SceneOffsetEditor) { pos = m_SceneOffsetPosition; rot = Quaternion.Euler(m_SceneOffsetRotation); } #endif var offsetPlayable = AnimationOffsetPlayable.Create(graph, pos, rot, 1); #if UNITY_EDITOR m_ClipOffset = offsetPlayable; #endif graph.Connect(root, 0, offsetPlayable, 0); offsetPlayable.SetInputWeight(0, 1); return offsetPlayable; } // the evaluation time is large so that the properties always get evaluated internal override void GetEvaluationTime(out double outStart, out double outDuration) { if (inClipMode) { base.GetEvaluationTime(out outStart, out outDuration); } else { outStart = 0; outDuration = TimelineClip.kMaxTimeValue; } } internal override void GetSequenceTime(out double outStart, out double outDuration) { if (inClipMode) { base.GetSequenceTime(out outStart, out outDuration); } else { outStart = 0; outDuration = Math.Max(GetNotificationDuration(), TimeUtility.GetAnimationClipLength(m_InfiniteClip)); } } void AssignAnimationClip(TimelineClip clip, AnimationClip animClip) { if (clip == null || animClip == null) return; if (animClip.legacy) throw new InvalidOperationException("Legacy Animation Clips are not supported"); AnimationPlayableAsset asset = clip.asset as AnimationPlayableAsset; if (asset != null) { asset.clip = animClip; asset.name = animClip.name; var duration = asset.duration; if (!double.IsInfinity(duration) && duration >= TimelineClip.kMinDuration && duration < TimelineClip.kMaxTimeValue) clip.duration = duration; } clip.displayName = animClip.name; } /// /// Called by the Timeline Editor to gather properties requiring preview. /// /// The PlayableDirector invoking the preview /// PropertyCollector used to gather previewable properties public override void GatherProperties(PlayableDirector director, IPropertyCollector driver) { #if UNITY_EDITOR m_SceneOffsetPosition = Vector3.zero; m_SceneOffsetRotation = Vector3.zero; var animator = GetBinding(director); if (animator == null) return; var animClips = new List(this.clips.Length + 2); GetAnimationClips(animClips); var hasHumanMotion = animClips.Exists(clip => clip.humanMotion); // case 1174752 - recording root transform on humanoid clips clips cause invalid pose. This will apply the default T-Pose, only if it not already driven by another track if (!hasHumanMotion && animator.isHuman && AnimatesRootTransform() && !DrivenPropertyManagerInternal.IsDriven(animator.transform, "m_LocalPosition.x") && !DrivenPropertyManagerInternal.IsDriven(animator.transform, "m_LocalRotation.x")) hasHumanMotion = true; m_SceneOffsetPosition = animator.transform.localPosition; m_SceneOffsetRotation = animator.transform.localEulerAngles; // Create default pose clip from collected properties if (hasHumanMotion) animClips.Add(GetDefaultHumanoidClip()); m_DefaultPoseClip = hasHumanMotion ? GetDefaultHumanoidClip() : null; var hash = AnimationPreviewUtilities.GetClipHash(animClips); if (m_CachedBindings == null || m_CachedHash != hash) { m_CachedBindings = AnimationPreviewUtilities.GetBindings(animator.gameObject, animClips); m_CachedPropertiesClip = AnimationPreviewUtilities.CreateDefaultClip(animator.gameObject, m_CachedBindings); m_CachedHash = hash; } AnimationPreviewUtilities.PreviewFromCurves(animator.gameObject, m_CachedBindings); // faster to preview from curves then an animation clip #endif } /// /// Gather all the animation clips for this track /// /// private void GetAnimationClips(List animClips) { foreach (var c in clips) { var a = c.asset as AnimationPlayableAsset; if (a != null && a.clip != null) animClips.Add(a.clip); } if (m_InfiniteClip != null) animClips.Add(m_InfiniteClip); foreach (var childTrack in GetChildTracks()) { var animChildTrack = childTrack as AnimationTrack; if (animChildTrack != null) animChildTrack.GetAnimationClips(animClips); } } // calculate which offset mode to apply AppliedOffsetMode GetOffsetMode(GameObject go, bool animatesRootTransform) { if (!animatesRootTransform) return AppliedOffsetMode.NoRootTransform; if (m_TrackOffset == TrackOffset.ApplyTransformOffsets) return AppliedOffsetMode.TransformOffset; if (m_TrackOffset == TrackOffset.ApplySceneOffsets) return (Application.isPlaying) ? AppliedOffsetMode.SceneOffset : AppliedOffsetMode.SceneOffsetEditor; if (HasController(go)) { if (!Application.isPlaying) return AppliedOffsetMode.SceneOffsetLegacyEditor; return AppliedOffsetMode.SceneOffsetLegacy; } return AppliedOffsetMode.TransformOffsetLegacy; } private bool IsRootTransformDisabledByMask(GameObject gameObject, Transform genericRootNode) { if (avatarMask == null || !applyAvatarMask) return false; var animator = GetBinding(gameObject != null ? gameObject.GetComponent() : null); if (animator == null) return false; if (animator.isHuman) return !avatarMask.GetHumanoidBodyPartActive(AvatarMaskBodyPart.Root); if (avatarMask.transformCount == 0) return false; // no special root supplied if (genericRootNode == null) return string.IsNullOrEmpty(avatarMask.GetTransformPath(0)) && !avatarMask.GetTransformActive(0); // walk the avatar list to find the matching transform for (int i = 0; i < avatarMask.transformCount; i++) { if (genericRootNode == animator.transform.Find(avatarMask.GetTransformPath(i))) return !avatarMask.GetTransformActive(i); } return false; } // Returns the generic root transform node. Returns null if it is the root node, OR if it not a generic node private Transform GetGenericRootNode(GameObject gameObject) { var animator = GetBinding(gameObject != null ? gameObject.GetComponent() : null); if (animator == null) return null; if (animator.isHuman) return null; if (animator.avatar == null) return null; // this returns the bone name, but not the full path var rootName = animator.avatar.humanDescription.m_RootMotionBoneName; if (rootName == animator.name || string.IsNullOrEmpty(rootName)) return null; // walk the hierarchy to find the first bone with this name return FindInHierarchyBreadthFirst(animator.transform, rootName); } internal bool AnimatesRootTransform() { // infinite mode if (AnimationPlayableAsset.HasRootTransforms(m_InfiniteClip)) return true; // clip mode foreach (var c in GetClips()) { var apa = c.asset as AnimationPlayableAsset; if (apa != null && apa.hasRootTransforms) return true; } return false; } private static readonly Queue s_CachedQueue = new Queue(100); private static Transform FindInHierarchyBreadthFirst(Transform t, string name) { s_CachedQueue.Clear(); s_CachedQueue.Enqueue(t); while (s_CachedQueue.Count > 0) { var r = s_CachedQueue.Dequeue(); if (r.name == name) return r; for (int i = 0; i < r.childCount; i++) s_CachedQueue.Enqueue(r.GetChild(i)); } return null; } } }