using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor; using UnityEngineInternal; using UnityEngine.Timeline; using UnityEngine.Playables; using Object = UnityEngine.Object; namespace UnityEditor.Timeline { class TimelineAnimationUtilities { public enum OffsetEditMode { None = -1, Translation = 0, Rotation = 1 } public static bool ValidateOffsetAvailabitity(PlayableDirector director, Animator animator) { if (director == null || animator == null) return false; return true; } public static TimelineClip GetPreviousClip(TimelineClip clip) { TimelineClip previousClip = null; foreach (var c in clip.GetParentTrack().clips) { if (c.start < clip.start && (previousClip == null || c.start >= previousClip.start)) previousClip = c; } return previousClip; } public static TimelineClip GetNextClip(TimelineClip clip) { return clip.GetParentTrack().clips.Where(c => c.start > clip.start).OrderBy(c => c.start).FirstOrDefault(); } public struct RigidTransform { public Vector3 position; public Quaternion rotation; public static RigidTransform Compose(Vector3 pos, Quaternion rot) { RigidTransform ret; ret.position = pos; ret.rotation = rot; return ret; } public static RigidTransform Mul(RigidTransform a, RigidTransform b) { RigidTransform ret; ret.rotation = a.rotation * b.rotation; ret.position = a.position + a.rotation * b.position; return ret; } public static RigidTransform Inverse(RigidTransform a) { RigidTransform ret; ret.rotation = Quaternion.Inverse(a.rotation); ret.position = ret.rotation * (-a.position); return ret; } public static RigidTransform identity { get { return Compose(Vector3.zero, Quaternion.identity); } } } private static Matrix4x4 GetTrackMatrix(Transform transform, AnimationTrack track) { Matrix4x4 trackMatrix = Matrix4x4.TRS(track.position, track.rotation, Vector3.one); // in scene off mode, the track offsets are set to the preview position which is stored in the track if (track.trackOffset == TrackOffset.ApplySceneOffsets) { trackMatrix = Matrix4x4.TRS(track.sceneOffsetPosition, Quaternion.Euler(track.sceneOffsetRotation), Vector3.one); } // put the parent transform on to the track matrix if (transform.parent != null) { trackMatrix = transform.parent.localToWorldMatrix * trackMatrix; } return trackMatrix; } // Given a world space position and rotation, updates the clip offsets to match that public static RigidTransform UpdateClipOffsets(AnimationPlayableAsset asset, AnimationTrack track, Transform transform, Vector3 globalPosition, Quaternion globalRotation) { Matrix4x4 worldToLocal = transform.worldToLocalMatrix; Matrix4x4 clipMatrix = Matrix4x4.TRS(asset.position, asset.rotation, Vector3.one); Matrix4x4 trackMatrix = GetTrackMatrix(transform, track); // Use the transform to find the proper goal matrix with scale taken into account var oldPos = transform.position; var oldRot = transform.rotation; transform.position = globalPosition; transform.rotation = globalRotation; Matrix4x4 goal = transform.localToWorldMatrix; transform.position = oldPos; transform.rotation = oldRot; // compute the new clip matrix. Matrix4x4 newClip = trackMatrix.inverse * goal * worldToLocal * trackMatrix * clipMatrix; return RigidTransform.Compose(newClip.GetColumn(3), MathUtils.QuaternionFromMatrix(newClip)); } public static RigidTransform GetTrackOffsets(AnimationTrack track, Transform transform) { Vector3 position = track.position; Quaternion rotation = track.rotation; if (transform != null && transform.parent != null) { position = transform.parent.TransformPoint(position); rotation = transform.parent.rotation * rotation; MathUtils.QuaternionNormalize(ref rotation); } return RigidTransform.Compose(position, rotation); } public static void UpdateTrackOffset(AnimationTrack track, Transform transform, RigidTransform offsets) { if (transform != null && transform.parent != null) { offsets.position = transform.parent.InverseTransformPoint(offsets.position); offsets.rotation = Quaternion.Inverse(transform.parent.rotation) * offsets.rotation; MathUtils.QuaternionNormalize(ref offsets.rotation); } track.position = offsets.position; track.eulerAngles = AnimationUtility.GetClosestEuler(offsets.rotation, track.eulerAngles, RotationOrder.OrderZXY); track.UpdateClipOffsets(); } static MatchTargetFields GetMatchFields(TimelineClip clip) { var track = clip.GetParentTrack() as AnimationTrack; if (track == null) return MatchTargetFieldConstants.None; var asset = clip.asset as AnimationPlayableAsset; var fields = track.matchTargetFields; if (asset != null && !asset.useTrackMatchFields) fields = asset.matchTargetFields; return fields; } static void WriteMatchFields(AnimationPlayableAsset asset, RigidTransform result, MatchTargetFields fields) { Vector3 position = asset.position; position.x = fields.HasAny(MatchTargetFields.PositionX) ? result.position.x : position.x; position.y = fields.HasAny(MatchTargetFields.PositionY) ? result.position.y : position.y; position.z = fields.HasAny(MatchTargetFields.PositionZ) ? result.position.z : position.z; asset.position = position; // check first to avoid unnecessary conversion errors if (fields.HasAny(MatchTargetFieldConstants.Rotation)) { Vector3 eulers = asset.eulerAngles; Vector3 resultEulers = result.rotation.eulerAngles; eulers.x = fields.HasAny(MatchTargetFields.RotationX) ? resultEulers.x : eulers.x; eulers.y = fields.HasAny(MatchTargetFields.RotationY) ? resultEulers.y : eulers.y; eulers.z = fields.HasAny(MatchTargetFields.RotationZ) ? resultEulers.z : eulers.z; asset.eulerAngles = AnimationUtility.GetClosestEuler(Quaternion.Euler(eulers), asset.eulerAngles, RotationOrder.OrderZXY); } } public static void MatchPrevious(TimelineClip currentClip, Transform matchPoint, PlayableDirector director) { const double timeEpsilon = 0.00001; MatchTargetFields matchFields = GetMatchFields(currentClip); if (matchFields == MatchTargetFieldConstants.None || matchPoint == null) return; double cachedTime = director.time; // finds previous clip TimelineClip previousClip = GetPreviousClip(currentClip); if (previousClip == null || currentClip == previousClip) return; // make sure the transform is properly updated before modifying the graph director.Evaluate(); var parentTrack = currentClip.GetParentTrack() as AnimationTrack; var blendIn = currentClip.blendInDuration; currentClip.blendInDuration = 0; var blendOut = previousClip.blendOutDuration; previousClip.blendOutDuration = 0; //evaluate previous without current parentTrack.RemoveClip(currentClip); director.RebuildGraph(); double previousEndTime = currentClip.start > previousClip.end ? previousClip.end : currentClip.start; director.time = previousEndTime - timeEpsilon; director.Evaluate(); // add port to evaluate only track var targetPosition = matchPoint.position; var targetRotation = matchPoint.rotation; // evaluate current without previous parentTrack.AddClip(currentClip); parentTrack.RemoveClip(previousClip); director.RebuildGraph(); director.time = currentClip.start + timeEpsilon; director.Evaluate(); ////////////////////////////////////////////////////////////////////// //compute offsets var animationPlayable = currentClip.asset as AnimationPlayableAsset; var match = UpdateClipOffsets(animationPlayable, parentTrack, matchPoint, targetPosition, targetRotation); WriteMatchFields(animationPlayable, match, matchFields); ////////////////////////////////////////////////////////////////////// currentClip.blendInDuration = blendIn; previousClip.blendOutDuration = blendOut; parentTrack.AddClip(previousClip); director.RebuildGraph(); director.time = cachedTime; director.Evaluate(); } public static void MatchNext(TimelineClip currentClip, Transform matchPoint, PlayableDirector director) { const double timeEpsilon = 0.00001; MatchTargetFields matchFields = GetMatchFields(currentClip); if (matchFields == MatchTargetFieldConstants.None || matchPoint == null) return; double cachedTime = director.time; // finds next clip TimelineClip nextClip = GetNextClip(currentClip); if (nextClip == null || currentClip == nextClip) return; // make sure the transform is properly updated before modifying the graph director.Evaluate(); var parentTrack = currentClip.GetParentTrack() as AnimationTrack; var blendOut = currentClip.blendOutDuration; var blendIn = nextClip.blendInDuration; currentClip.blendOutDuration = 0; nextClip.blendInDuration = 0; //evaluate previous without current parentTrack.RemoveClip(currentClip); director.RebuildGraph(); director.time = nextClip.start + timeEpsilon; director.Evaluate(); // add port to evaluate only track var targetPosition = matchPoint.position; var targetRotation = matchPoint.rotation; // evaluate current without next parentTrack.AddClip(currentClip); parentTrack.RemoveClip(nextClip); director.RebuildGraph(); director.time = Math.Min(nextClip.start, currentClip.end - timeEpsilon); director.Evaluate(); ////////////////////////////////////////////////////////////////////// //compute offsets var animationPlayable = currentClip.asset as AnimationPlayableAsset; var match = UpdateClipOffsets(animationPlayable, parentTrack, matchPoint, targetPosition, targetRotation); WriteMatchFields(animationPlayable, match, matchFields); ////////////////////////////////////////////////////////////////////// currentClip.blendOutDuration = blendOut; nextClip.blendInDuration = blendIn; parentTrack.AddClip(nextClip); director.RebuildGraph(); director.time = cachedTime; director.Evaluate(); } public static TimelineWindowTimeControl CreateTimeController(TimelineClip clip) { var animationWindow = EditorWindow.GetWindow(); var timeController = ScriptableObject.CreateInstance(); timeController.Init(animationWindow.state, clip); return timeController; } public static TimelineWindowTimeControl CreateTimeController(TimelineWindowTimeControl.ClipData clipData) { var animationWindow = EditorWindow.GetWindow(); var timeController = ScriptableObject.CreateInstance(); timeController.Init(animationWindow.state, clipData); return timeController; } public static void EditAnimationClipWithTimeController(AnimationClip animationClip, TimelineWindowTimeControl timeController, Object sourceObject) { var animationWindow = EditorWindow.GetWindow(); animationWindow.EditSequencerClip(animationClip, sourceObject, timeController); } public static void UnlinkAnimationWindowFromTracks(IEnumerable tracks) { var clips = new List(); foreach (var track in tracks) { var animationTrack = track as AnimationTrack; if (animationTrack != null && animationTrack.infiniteClip != null) clips.Add(animationTrack.infiniteClip); GetAnimationClips(track.GetClips(), clips); } UnlinkAnimationWindowFromAnimationClips(clips); } public static void UnlinkAnimationWindowFromClips(IEnumerable timelineClips) { var clips = new List(); GetAnimationClips(timelineClips, clips); UnlinkAnimationWindowFromAnimationClips(clips); } public static void UnlinkAnimationWindowFromAnimationClips(ICollection clips) { if (clips.Count == 0) return; UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(typeof(AnimationWindow)); foreach (var animWindow in windows.OfType()) { if (animWindow != null && animWindow.state != null && animWindow.state.linkedWithSequencer && clips.Contains(animWindow.state.activeAnimationClip)) animWindow.UnlinkSequencer(); } } public static void UnlinkAnimationWindow() { UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(typeof(AnimationWindow)); foreach (var animWindow in windows.OfType()) { if (animWindow != null && animWindow.state != null && animWindow.state.linkedWithSequencer) animWindow.UnlinkSequencer(); } } private static void GetAnimationClips(IEnumerable timelineClips, List clips) { foreach (var timelineClip in timelineClips) { if (timelineClip.curves != null) clips.Add(timelineClip.curves); AnimationPlayableAsset apa = timelineClip.asset as AnimationPlayableAsset; if (apa != null && apa.clip != null) clips.Add(apa.clip); } } public static int GetAnimationWindowCurrentFrame() { var animationWindow = EditorWindow.GetWindow(); if (animationWindow) return animationWindow.state.currentFrame; return -1; } public static void SetAnimationWindowCurrentFrame(int frame) { var animationWindow = EditorWindow.GetWindow(); if (animationWindow) animationWindow.state.currentFrame = frame; } public static void ConstrainCurveToBooleanValues(AnimationCurve curve) { // Clamp the values first var keys = curve.keys; for (var i = 0; i < keys.Length; i++) { var key = keys[i]; key.value = key.value < 0.5f ? 0.0f : 1.0f; keys[i] = key; } curve.keys = keys; // Update the tangents once all the values are clamped for (var i = 0; i < curve.length; i++) { AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant); AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant); } } public static void ConstrainCurveToRange(AnimationCurve curve, float minValue, float maxValue) { var keys = curve.keys; for (var i = 0; i < keys.Length; i++) { var key = keys[i]; key.value = Mathf.Clamp(key.value, minValue, maxValue); keys[i] = key; } curve.keys = keys; } public static bool IsAnimationClip(TimelineClip clip) { return clip != null && (clip.asset as AnimationPlayableAsset) != null; } } }