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

using System;
using System.Collections.Generic;
using UnityEngine.Animations;
using UnityEngine.Experimental.Animations;
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]
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 OnCreateClipPlayableGraph(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;
}
}
}