You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1071 lines
41 KiB
1071 lines
41 KiB
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 |
|
{ |
|
/// <summary> |
|
/// Flags specifying which offset fields to match |
|
/// </summary> |
|
[Flags] |
|
public enum MatchTargetFields |
|
{ |
|
/// <summary> |
|
/// Translation X value |
|
/// </summary> |
|
PositionX = 1 << 0, |
|
/// <summary> |
|
/// Translation Y value |
|
/// </summary> |
|
PositionY = 1 << 1, |
|
/// <summary> |
|
/// Translation Z value |
|
/// </summary> |
|
PositionZ = 1 << 2, |
|
/// <summary> |
|
/// Rotation Euler Angle X value |
|
/// </summary> |
|
RotationX = 1 << 3, |
|
/// <summary> |
|
/// Rotation Euler Angle Y value |
|
/// </summary> |
|
RotationY = 1 << 4, |
|
/// <summary> |
|
/// Rotation Euler Angle Z value |
|
/// </summary> |
|
RotationZ = 1 << 5 |
|
} |
|
|
|
/// <summary> |
|
/// Describes what is used to set the starting position and orientation of each Animation Track. |
|
/// </summary> |
|
/// <remarks> |
|
/// 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. |
|
/// </remarks> |
|
public enum TrackOffset |
|
{ |
|
/// <summary> |
|
/// Use this setting to offset each Animation Track based on a set position and orientation. |
|
/// </summary> |
|
ApplyTransformOffsets, |
|
/// <summary> |
|
/// Use this setting to offset each Animation Track based on the current position and orientation in the scene. |
|
/// </summary> |
|
ApplySceneOffsets, |
|
/// <summary> |
|
/// Use this setting to offset root transforms based on the state of the animator. |
|
/// </summary> |
|
/// <remarks> |
|
/// 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. |
|
/// </remarks> |
|
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; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// A Timeline track used for playing back animations on an Animator. |
|
/// </summary> |
|
[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 |
|
|
|
/// <summary> |
|
/// The translation offset of the entire track. |
|
/// </summary> |
|
public Vector3 position |
|
{ |
|
get { return m_Position; } |
|
set { m_Position = value; } |
|
} |
|
|
|
/// <summary> |
|
/// The rotation offset of the entire track, expressed as a quaternion. |
|
/// </summary> |
|
public Quaternion rotation |
|
{ |
|
get { return Quaternion.Euler(m_EulerAngles); } |
|
set { m_EulerAngles = value.eulerAngles; } |
|
} |
|
|
|
/// <summary> |
|
/// The euler angle representation of the rotation offset of the entire track. |
|
/// </summary> |
|
public Vector3 eulerAngles |
|
{ |
|
get { return m_EulerAngles; } |
|
set { m_EulerAngles = value; } |
|
} |
|
|
|
/// <summary> |
|
/// Specifies whether to apply track offsets to all clips on the track. |
|
/// </summary> |
|
/// <remarks> |
|
/// This can be used to offset all clips on a track, in addition to the clips individual offsets. |
|
/// </remarks> |
|
[Obsolete("applyOffset is deprecated. Use trackOffset instead", true)] |
|
public bool applyOffsets |
|
{ |
|
get { return false; } |
|
set { } |
|
} |
|
|
|
/// <summary> |
|
/// Specifies what is used to set the starting position and orientation of an Animation Track. |
|
/// </summary> |
|
/// <remarks> |
|
/// Track Offset is only applied when the Animation Track contains animation that modifies the root Transform. |
|
/// </remarks> |
|
public TrackOffset trackOffset |
|
{ |
|
get { return m_TrackOffset; } |
|
set { m_TrackOffset = value; } |
|
} |
|
|
|
/// <summary> |
|
/// Specifies which fields to match when aligning offsets of clips. |
|
/// </summary> |
|
public MatchTargetFields matchTargetFields |
|
{ |
|
get { return m_MatchTargetFields; } |
|
set { m_MatchTargetFields = value & MatchTargetFieldConstants.All; } |
|
} |
|
|
|
/// <summary> |
|
/// An AnimationClip storing the data for an infinite track. |
|
/// </summary> |
|
/// <remarks> |
|
/// The value of this property is null when the AnimationTrack is in Clip Mode. |
|
/// </remarks> |
|
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; } |
|
} |
|
|
|
/// <summary> |
|
/// Specifies the AvatarMask to be applied to all clips on the track. |
|
/// </summary> |
|
/// <remarks> |
|
/// Applying an AvatarMask to an animation track will allow discarding portions of the animation being applied on the track. |
|
/// </remarks> |
|
public AvatarMask avatarMask |
|
{ |
|
get { return m_AvatarMask; } |
|
set { m_AvatarMask = value; } |
|
} |
|
|
|
/// <summary> |
|
/// Specifies whether to apply the AvatarMask to the track. |
|
/// </summary> |
|
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)); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public override IEnumerable<PlayableBinding> outputs |
|
{ |
|
get { yield return AnimationPlayableBinding.Create(name, this); } |
|
} |
|
|
|
|
|
/// <summary> |
|
/// Specifies whether the Animation Track has clips, or is in infinite mode. |
|
/// </summary> |
|
public bool inClipMode |
|
{ |
|
get { return clips != null && clips.Length != 0; } |
|
} |
|
|
|
/// <summary> |
|
/// The translation offset of a track in infinite mode. |
|
/// </summary> |
|
public Vector3 infiniteClipOffsetPosition |
|
{ |
|
get { return m_InfiniteClipOffsetPosition; } |
|
set { m_InfiniteClipOffsetPosition = value; } |
|
} |
|
|
|
/// <summary> |
|
/// The rotation offset of a track in infinite mode. |
|
/// </summary> |
|
public Quaternion infiniteClipOffsetRotation |
|
{ |
|
get { return Quaternion.Euler(m_InfiniteClipOffsetEulerAngles); } |
|
set { m_InfiniteClipOffsetEulerAngles = value.eulerAngles; } |
|
} |
|
|
|
/// <summary> |
|
/// The euler angle representation of the rotation offset of the track when in infinite mode. |
|
/// </summary> |
|
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; } |
|
} |
|
|
|
/// <summary> |
|
/// The saved state of pre-extrapolation for clips converted to infinite mode. |
|
/// </summary> |
|
public TimelineClip.ClipExtrapolation infiniteClipPreExtrapolation |
|
{ |
|
get { return m_InfiniteClipPreExtrapolation; } |
|
set { m_InfiniteClipPreExtrapolation = value; } |
|
} |
|
|
|
/// <summary> |
|
/// The saved state of post-extrapolation for clips when converted to infinite mode. |
|
/// </summary> |
|
public TimelineClip.ClipExtrapolation infiniteClipPostExtrapolation |
|
{ |
|
get { return m_InfiniteClipPostExtrapolation; } |
|
set { m_InfiniteClipPostExtrapolation = value; } |
|
} |
|
|
|
/// <summary> |
|
/// The saved state of animation clip loop state when converted to infinite mode |
|
/// </summary> |
|
internal AnimationPlayableAsset.LoopMode infiniteClipLoop |
|
{ |
|
get { return mInfiniteClipLoop; } |
|
set { mInfiniteClipLoop = value; } |
|
} |
|
|
|
[ContextMenu("Reset Offsets")] |
|
void ResetOffsets() |
|
{ |
|
m_Position = Vector3.zero; |
|
m_EulerAngles = Vector3.zero; |
|
UpdateClipOffsets(); |
|
} |
|
|
|
/// <summary> |
|
/// Creates a TimelineClip on this track that uses an AnimationClip. |
|
/// </summary> |
|
/// <param name="clip">Source animation clip of the resulting TimelineClip.</param> |
|
/// <returns>A new TimelineClip which has an AnimationPlayableAsset asset attached.</returns> |
|
public TimelineClip CreateClip(AnimationClip clip) |
|
{ |
|
if (clip == null) |
|
return null; |
|
|
|
var newClip = CreateClip<AnimationPlayableAsset>(); |
|
AssignAnimationClip(newClip, clip); |
|
return newClip; |
|
} |
|
|
|
/// <summary> |
|
/// Creates an AnimationClip that stores the data for an infinite track. |
|
/// </summary> |
|
/// <remarks> |
|
/// If an infiniteClip already exists, this method produces no result, even if you provide a different value |
|
/// for infiniteClipName. |
|
/// </remarks> |
|
/// <remarks> |
|
/// 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. |
|
/// </remarks> |
|
/// <remarks> |
|
/// 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. |
|
/// </remarks> |
|
/// <param name="infiniteClipName"> |
|
/// 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. |
|
/// </param> |
|
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); |
|
} |
|
|
|
/// <summary> |
|
/// Creates a TimelineClip, AnimationPlayableAsset and an AnimationClip. Use this clip to record in a timeline. |
|
/// </summary> |
|
/// <remarks> |
|
/// 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. |
|
/// </remarks> |
|
/// <param name="animClipName"> |
|
/// 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. |
|
/// </param> |
|
/// <returns> |
|
/// Returns a new TimelineClip with an AnimationPlayableAsset asset attached. |
|
/// </returns> |
|
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 |
|
|
|
/// <summary> |
|
/// Used to initialize default values on a newly created clip |
|
/// </summary> |
|
/// <param name="clip">The clip added to the track</param> |
|
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<RuntimeElement> 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); |
|
} |
|
|
|
/// <inheritdoc cref="ILayerable.CreateLayerMixer"/> |
|
/// <returns>Returns <c>Playable.Null</c></returns> |
|
Playable ILayerable.CreateLayerMixer(PlayableGraph graph, GameObject go, int inputCount) |
|
{ |
|
return Playable.Null; |
|
} |
|
|
|
internal override Playable CreateMixerPlayableGraph(PlayableGraph graph, GameObject go, IntervalTree<RuntimeElement> tree) |
|
{ |
|
if (isSubTrack) |
|
throw new InvalidOperationException("Nested animation tracks should never be asked to create a graph directly"); |
|
|
|
List<AnimationTrack> flattenTracks = new List<AnimationTrack>(); |
|
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<PlayableDirector>() : null); |
|
if (animator != null) |
|
{ |
|
GameObject targetGO = animator.gameObject; |
|
IAnimationWindowPreview[] previewComponents = targetGO.GetComponents<IAnimationWindowPreview>(); |
|
|
|
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<AnimationClip>(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<PlayableDirector>() : 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<PlayableDirector>() : 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<Animator>(); |
|
} |
|
|
|
return animator; |
|
} |
|
|
|
static AnimationLayerMixerPlayable CreateGroupMixer(PlayableGraph graph, GameObject go, int inputCount) |
|
{ |
|
return AnimationLayerMixerPlayable.Create(graph, inputCount); |
|
} |
|
|
|
Playable CreateInfiniteTrackPlayable(PlayableGraph graph, GameObject go, IntervalTree<RuntimeElement> 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; |
|
} |
|
|
|
/// <summary> |
|
/// Called by the Timeline Editor to gather properties requiring preview. |
|
/// </summary> |
|
/// <param name="director">The PlayableDirector invoking the preview</param> |
|
/// <param name="driver">PropertyCollector used to gather previewable properties</param> |
|
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<AnimationClip>(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 |
|
} |
|
|
|
/// <summary> |
|
/// Gather all the animation clips for this track |
|
/// </summary> |
|
/// <param name="animClips"></param> |
|
private void GetAnimationClips(List<AnimationClip> 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<PlayableDirector>() : 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<PlayableDirector>() : 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<Transform> s_CachedQueue = new Queue<Transform>(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; |
|
} |
|
} |
|
}
|
|
|