Initial Commit
This commit is contained in:
parent
53eb92e9af
commit
270ab7d11f
15341 changed files with 700234 additions and 0 deletions
|
@ -0,0 +1,306 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace UnityEditor.Timeline
|
||||
{
|
||||
class AnimationTrackRecorder
|
||||
{
|
||||
public static readonly string kRecordClipDefaultName = L10n.Tr("Recorded");
|
||||
|
||||
AnimationClip m_TargetClip;
|
||||
int m_CurveCount = 0;
|
||||
|
||||
double m_ClipTime;
|
||||
bool m_needRebuildRects;
|
||||
|
||||
bool m_TrackHasPreviewComponents;
|
||||
|
||||
public TimelineClip recordClip { get; private set; }
|
||||
|
||||
public void PrepareForRecord(WindowState state)
|
||||
{
|
||||
m_CurveCount = 0;
|
||||
m_TargetClip = null;
|
||||
m_TrackHasPreviewComponents = false;
|
||||
}
|
||||
|
||||
public AnimationClip PrepareTrack(TrackAsset track, WindowState state, GameObject gameObject, out double startTime)
|
||||
{
|
||||
AnimationClip animationClip = null;
|
||||
|
||||
// if we are not in clip mode, we simply use the track clip
|
||||
var animationTrack = (AnimationTrack)track;
|
||||
|
||||
// ignore recording if we are in Legacy auto mode
|
||||
startTime = -1;
|
||||
var parentTrack = TimelineUtility.GetSceneReferenceTrack(track) as AnimationTrack;
|
||||
if (parentTrack != null && parentTrack.trackOffset == TrackOffset.Auto)
|
||||
return null;
|
||||
|
||||
if (!animationTrack.inClipMode)
|
||||
{
|
||||
var trackClip = animationTrack.GetOrCreateClip();
|
||||
startTime = trackClip.frameRate * state.editSequence.time;
|
||||
|
||||
// Make the first key be at time 0 of the clip
|
||||
if (trackClip.empty)
|
||||
{
|
||||
animationTrack.infiniteClipTimeOffset = 0; // state.time;
|
||||
animationTrack.infiniteClipPreExtrapolation = TimelineClip.ClipExtrapolation.Hold;
|
||||
animationTrack.infiniteClipPostExtrapolation = TimelineClip.ClipExtrapolation.Hold;
|
||||
}
|
||||
|
||||
animationClip = trackClip;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimelineClip activeClip = null;
|
||||
|
||||
// if it fails, but returns no clip, we can add one.
|
||||
if (!track.FindRecordingClipAtTime(state.editSequence.time, out activeClip) && activeClip != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (activeClip == null)
|
||||
{
|
||||
activeClip = AddRecordableClip(track, state, state.editSequence.time);
|
||||
}
|
||||
|
||||
var clip = activeClip.animationClip;
|
||||
|
||||
// flags this as the clip being recorded for the track
|
||||
var clipTime = state.editSequence.time - activeClip.start;
|
||||
|
||||
// if we are in the past
|
||||
if (clipTime < 0)
|
||||
{
|
||||
Undo.RegisterCompleteObjectUndo(clip, L10n.Tr("Record Key"));
|
||||
UndoExtensions.RegisterTrack(track, L10n.Tr("Prepend Key"));
|
||||
ShiftAnimationClip(clip, (float)-clipTime);
|
||||
activeClip.start = state.editSequence.time;
|
||||
activeClip.duration += -clipTime;
|
||||
clipTime = 0;
|
||||
}
|
||||
|
||||
m_ClipTime = clipTime;
|
||||
recordClip = activeClip;
|
||||
startTime = recordClip.ToLocalTimeUnbound(state.editSequence.time) * clip.frameRate;
|
||||
m_needRebuildRects = clip.empty;
|
||||
|
||||
animationClip = clip;
|
||||
}
|
||||
|
||||
m_TargetClip = animationClip;
|
||||
m_CurveCount = GetCurveCount(animationClip);
|
||||
m_TrackHasPreviewComponents = animationTrack.hasPreviewComponents;
|
||||
|
||||
return animationClip;
|
||||
}
|
||||
|
||||
static int GetCurveCount(AnimationClip animationClip)
|
||||
{
|
||||
int count = 0;
|
||||
if (animationClip != null)
|
||||
{
|
||||
var clipCache = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip);
|
||||
count = clipCache.curves.Length + clipCache.objectCurves.Count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public void FinializeTrack(TrackAsset track, WindowState state)
|
||||
{
|
||||
// make sure we dirty the clip if we are in non clip mode
|
||||
var animTrack = track as AnimationTrack;
|
||||
if (!animTrack.inClipMode)
|
||||
{
|
||||
EditorUtility.SetDirty(animTrack.GetOrCreateClip());
|
||||
}
|
||||
|
||||
// in clip mode we need to do some extra work
|
||||
if (recordClip != null)
|
||||
{
|
||||
// stretch the clip out to meet the new recording time
|
||||
if (m_ClipTime > recordClip.duration)
|
||||
{
|
||||
UndoExtensions.RegisterTrack(track, L10n.Tr("Add Key"));
|
||||
recordClip.duration = m_ClipTime;
|
||||
}
|
||||
|
||||
track.CalculateExtrapolationTimes();
|
||||
}
|
||||
|
||||
recordClip = null;
|
||||
m_ClipTime = 0;
|
||||
if (m_needRebuildRects)
|
||||
{
|
||||
state.CalculateRowRects();
|
||||
m_needRebuildRects = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void FinalizeRecording(WindowState state)
|
||||
{
|
||||
// rebuild the graph if we add/remove a clip. Rebuild the graph with an evaluation immediately
|
||||
// so previews and scene position is maintained.
|
||||
if (m_CurveCount != GetCurveCount(m_TargetClip))
|
||||
{
|
||||
state.rebuildGraph = true;
|
||||
state.GetWindow().RebuildGraphIfNecessary(true);
|
||||
}
|
||||
else if (m_TrackHasPreviewComponents)
|
||||
{
|
||||
// Track with preview components potentially has modifications impacting other properties that need
|
||||
// to be refreshed before inspector or scene view to not interfere with manipulation.
|
||||
state.EvaluateImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
// For a given track asset get a unique clip name
|
||||
public static string GetUniqueRecordedClipName(Object owner, string name)
|
||||
{
|
||||
// first attempt -- uniquely named in file
|
||||
var path = AssetDatabase.GetAssetPath(owner);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var names = AssetDatabase.LoadAllAssetsAtPath(path).Where(x => x != null).Select(x => x.name);
|
||||
return ObjectNames.GetUniqueName(names.ToArray(), name);
|
||||
}
|
||||
|
||||
TrackAsset asset = owner as TrackAsset;
|
||||
if (asset == null || asset.clips.Length == 0)
|
||||
return name;
|
||||
|
||||
// final attempt - uniquely named in track
|
||||
return ObjectNames.GetUniqueName(asset.clips.Select(x => x.displayName).ToArray(), name);
|
||||
}
|
||||
|
||||
// Given an appropriate parent track, create a recordable clip
|
||||
public static TimelineClip AddRecordableClip(TrackAsset parentTrack, WindowState state, double atTime)
|
||||
{
|
||||
var sequenceAsset = state.editSequence.asset;
|
||||
if (sequenceAsset == null)
|
||||
{
|
||||
Debug.LogError("Parent Track needs to be bound to an asset to add a recordable");
|
||||
return null;
|
||||
}
|
||||
|
||||
var animTrack = parentTrack as AnimationTrack;
|
||||
if (animTrack == null)
|
||||
{
|
||||
Debug.LogError("Recordable clips are only valid on Animation Tracks");
|
||||
return null;
|
||||
}
|
||||
|
||||
var newClip = animTrack.CreateRecordableClip(GetUniqueRecordedClipName(parentTrack, kRecordClipDefaultName));
|
||||
if (newClip == null)
|
||||
{
|
||||
Debug.LogError("Could not create a recordable clip");
|
||||
return null;
|
||||
}
|
||||
|
||||
newClip.mixInCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
newClip.mixOutCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
|
||||
|
||||
newClip.preExtrapolationMode = TimelineClip.ClipExtrapolation.Hold;
|
||||
newClip.postExtrapolationMode = TimelineClip.ClipExtrapolation.Hold;
|
||||
|
||||
double startTime = 0;
|
||||
double endTime = 0;
|
||||
|
||||
GetAddedRecordingClipRange(animTrack, state, atTime, out startTime, out endTime);
|
||||
|
||||
newClip.start = startTime;
|
||||
newClip.duration = endTime - startTime;
|
||||
|
||||
state.Refresh();
|
||||
|
||||
return newClip;
|
||||
}
|
||||
|
||||
// get the start and end times of what an added recording clip at a given time would be
|
||||
internal static void GetAddedRecordingClipRange(TrackAsset track, WindowState state, double atTime, out double start, out double end)
|
||||
{
|
||||
// size to make the clip in pixels. Reasonably big so that both handles are easily manipulated,
|
||||
// and the full title is normally visible
|
||||
double defaultDuration = state.PixelDeltaToDeltaTime(100);
|
||||
|
||||
start = atTime;
|
||||
end = atTime + defaultDuration;
|
||||
|
||||
double gapStart = 0;
|
||||
double gapEnd = 0;
|
||||
|
||||
// no gap, pick are reasonable amount
|
||||
if (!track.GetGapAtTime(atTime, out gapStart, out gapEnd))
|
||||
{
|
||||
start = atTime;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!double.IsInfinity(gapEnd))
|
||||
end = gapEnd;
|
||||
|
||||
start = TimeReferenceUtility.SnapToFrameIfRequired(start);
|
||||
end = TimeReferenceUtility.SnapToFrameIfRequired(end);
|
||||
}
|
||||
|
||||
// Given a clip, shifts the keys in that clip by the given amount.
|
||||
internal static void ShiftAnimationClip(AnimationClip clip, float amount)
|
||||
{
|
||||
if (clip == null)
|
||||
return;
|
||||
|
||||
var curveBindings = AnimationUtility.GetCurveBindings(clip);
|
||||
var objectCurveBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
|
||||
|
||||
foreach (var binding in curveBindings)
|
||||
{
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||||
curve.keys = ShiftKeys(curve.keys, amount);
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||
}
|
||||
|
||||
foreach (var binding in objectCurveBindings)
|
||||
{
|
||||
ObjectReferenceKeyframe[] keyframes = AnimationUtility.GetObjectReferenceCurve(clip, binding);
|
||||
keyframes = ShiftObjectKeys(keyframes, amount);
|
||||
AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(clip);
|
||||
}
|
||||
|
||||
// shift all the keys over by the given time, stretching the time 0 key
|
||||
static Keyframe[] ShiftKeys(Keyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null || keys.Length == 0 || time == 0)
|
||||
return keys;
|
||||
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
keys[i].time += time;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Shift object keys over by the appropriate amount
|
||||
static ObjectReferenceKeyframe[] ShiftObjectKeys(ObjectReferenceKeyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null || keys.Length == 0 || time == 0)
|
||||
return keys;
|
||||
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
keys[i].time += time;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a0a02e768c802b641b6793fa864f1c2c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,603 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Timeline;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace UnityEditor.Timeline
|
||||
{
|
||||
// Handles Undo animated properties on Monobehaviours to create track clips
|
||||
static partial class TimelineRecording
|
||||
{
|
||||
static readonly List<PropertyModification> s_TempPropertyModifications = new List<PropertyModification>(6);
|
||||
|
||||
internal static UndoPropertyModification[] ProcessUndoModification(UndoPropertyModification[] modifications, WindowState state)
|
||||
{
|
||||
if (HasAnyPlayableAssetModifications(modifications))
|
||||
return ProcessPlayableAssetModification(modifications, state, false);
|
||||
return ProcessMonoBehaviourModification(modifications, state);
|
||||
}
|
||||
|
||||
static UnityEngine.Object GetTarget(UndoPropertyModification undo)
|
||||
{
|
||||
if (undo.currentValue != null)
|
||||
return undo.currentValue.target;
|
||||
if (undo.previousValue != null)
|
||||
return undo.previousValue.target;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gets the appropriate track for a given game object
|
||||
static TrackAsset GetTrackForGameObject(GameObject gameObject, WindowState state)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return null;
|
||||
|
||||
var director = state.editSequence.director;
|
||||
if (director == null)
|
||||
return null;
|
||||
|
||||
var level = int.MaxValue;
|
||||
|
||||
TrackAsset result = null;
|
||||
|
||||
// search the output tracks
|
||||
var outputTracks = state.editSequence.asset.flattenedTracks;
|
||||
foreach (var track in outputTracks)
|
||||
{
|
||||
if (track.GetType() != typeof(AnimationTrack))
|
||||
continue;
|
||||
if (!state.IsTrackRecordable(track))
|
||||
continue;
|
||||
|
||||
var obj = TimelineUtility.GetSceneGameObject(director, track);
|
||||
if (obj != null)
|
||||
{
|
||||
// checks if the effected gameobject is our child
|
||||
var childLevel = GetChildLevel(obj, gameObject);
|
||||
if (childLevel != -1 && childLevel < level)
|
||||
{
|
||||
result = track;
|
||||
level = childLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the resulting track is not armed. checking here avoids accidentally recording objects with their own
|
||||
// tracks
|
||||
if (result && !state.IsTrackRecordable(result))
|
||||
{
|
||||
result = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Gets the track this property would record to.
|
||||
// Returns null if there is a track, but it's not currently active for recording
|
||||
public static TrackAsset GetRecordingTrack(SerializedProperty property, WindowState state)
|
||||
{
|
||||
var serializedObject = property.serializedObject;
|
||||
var component = serializedObject.targetObject as Component;
|
||||
if (component == null)
|
||||
return null;
|
||||
|
||||
var gameObject = component.gameObject;
|
||||
return GetTrackForGameObject(gameObject, state);
|
||||
}
|
||||
|
||||
// Given a serialized property, gathers all animatable properties
|
||||
static void GatherModifications(SerializedProperty property, List<PropertyModification> modifications)
|
||||
{
|
||||
// handles child properties (Vector3 is 3 recordable properties)
|
||||
if (property.hasChildren)
|
||||
{
|
||||
var iter = property.Copy();
|
||||
var end = property.GetEndProperty(false);
|
||||
|
||||
// recurse over all children properties
|
||||
while (iter.Next(true) && !SerializedProperty.EqualContents(iter, end))
|
||||
{
|
||||
GatherModifications(iter, modifications);
|
||||
}
|
||||
}
|
||||
|
||||
var isObject = property.propertyType == SerializedPropertyType.ObjectReference;
|
||||
var isFloat = property.propertyType == SerializedPropertyType.Float ||
|
||||
property.propertyType == SerializedPropertyType.Boolean ||
|
||||
property.propertyType == SerializedPropertyType.Integer;
|
||||
|
||||
if (isObject || isFloat)
|
||||
{
|
||||
var serializedObject = property.serializedObject;
|
||||
var modification = new PropertyModification();
|
||||
|
||||
modification.target = serializedObject.targetObject;
|
||||
modification.propertyPath = property.propertyPath;
|
||||
if (isObject)
|
||||
{
|
||||
modification.value = string.Empty;
|
||||
modification.objectReference = property.objectReferenceValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
modification.value = TimelineUtility.PropertyToString(property);
|
||||
}
|
||||
|
||||
// Path for monobehaviour based - better to grab the component to get the curvebinding to allow validation
|
||||
if (serializedObject.targetObject is Component)
|
||||
{
|
||||
EditorCurveBinding temp;
|
||||
var go = ((Component)serializedObject.targetObject).gameObject;
|
||||
if (AnimationUtility.PropertyModificationToEditorCurveBinding(modification, go, out temp) != null)
|
||||
{
|
||||
modifications.Add(modification);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
modifications.Add(modification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddKey(SerializedProperty prop, WindowState state)
|
||||
{
|
||||
s_TempPropertyModifications.Clear();
|
||||
GatherModifications(prop, s_TempPropertyModifications);
|
||||
if (s_TempPropertyModifications.Any())
|
||||
{
|
||||
AddKey(s_TempPropertyModifications, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddKey(IEnumerable<PropertyModification> modifications, WindowState state)
|
||||
{
|
||||
var undos = modifications.Select(PropertyModificationToUndoPropertyModification).ToArray();
|
||||
if (HasAnyPlayableAssetModifications(undos))
|
||||
ProcessPlayableAssetModification(undos, state, true);
|
||||
ProcessMonoBehaviourModification(undos, state);
|
||||
}
|
||||
|
||||
static UndoPropertyModification PropertyModificationToUndoPropertyModification(PropertyModification prop)
|
||||
{
|
||||
return new UndoPropertyModification
|
||||
{
|
||||
previousValue = prop,
|
||||
currentValue = new PropertyModification
|
||||
{
|
||||
objectReference = prop.objectReference,
|
||||
propertyPath = prop.propertyPath,
|
||||
target = prop.target,
|
||||
value = prop.value
|
||||
},
|
||||
keepPrefabOverride = true
|
||||
};
|
||||
}
|
||||
|
||||
// Given an animation track, return the clip that we are currently recording to
|
||||
static AnimationClip GetRecordingClip(TrackAsset asset, WindowState state, out double startTime, out double timeScale)
|
||||
{
|
||||
startTime = 0;
|
||||
timeScale = 1;
|
||||
|
||||
TimelineClip displayBackground = null;
|
||||
asset.FindRecordingClipAtTime(state.editSequence.time, out displayBackground);
|
||||
var animClip = asset.FindRecordingAnimationClipAtTime(state.editSequence.time);
|
||||
|
||||
if (displayBackground != null)
|
||||
{
|
||||
startTime = displayBackground.start;
|
||||
timeScale = displayBackground.timeScale;
|
||||
}
|
||||
|
||||
return animClip;
|
||||
}
|
||||
|
||||
// Helper that finds the animation clip we are recording and the relative time to that clip
|
||||
static bool GetClipAndRelativeTime(UnityEngine.Object target, WindowState state,
|
||||
out AnimationClip outClip, out double keyTime, out bool keyInRange)
|
||||
{
|
||||
const float floatToDoubleError = 0.00001f;
|
||||
outClip = null;
|
||||
keyTime = 0;
|
||||
keyInRange = false;
|
||||
|
||||
double startTime = 0;
|
||||
double timeScale = 1;
|
||||
AnimationClip clip = null;
|
||||
|
||||
IPlayableAsset playableAsset = target as IPlayableAsset;
|
||||
Component component = target as Component;
|
||||
|
||||
// Handle recordable playable assets
|
||||
if (playableAsset != null)
|
||||
{
|
||||
var curvesOwner = AnimatedParameterUtility.ToCurvesOwner(playableAsset, state.editSequence.asset);
|
||||
if (curvesOwner != null)
|
||||
{
|
||||
if (curvesOwner.curves == null)
|
||||
curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName());
|
||||
|
||||
clip = curvesOwner.curves;
|
||||
|
||||
var timelineClip = curvesOwner as TimelineClip;
|
||||
if (timelineClip != null)
|
||||
{
|
||||
startTime = timelineClip.start;
|
||||
timeScale = timelineClip.timeScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle recording components, including infinite clip
|
||||
else if (component != null)
|
||||
{
|
||||
var asset = GetTrackForGameObject(component.gameObject, state);
|
||||
if (asset != null)
|
||||
{
|
||||
clip = GetRecordingClip(asset, state, out startTime, out timeScale);
|
||||
}
|
||||
}
|
||||
|
||||
if (clip == null)
|
||||
return false;
|
||||
|
||||
keyTime = (state.editSequence.time - startTime) * timeScale;
|
||||
outClip = clip;
|
||||
keyInRange = keyTime >= 0 && keyTime <= (clip.length * timeScale + floatToDoubleError);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool HasCurve(IList<PropertyModification> modifications, UnityEngine.Object target,
|
||||
WindowState state)
|
||||
{
|
||||
return GetKeyTimes(modifications, state).Any();
|
||||
}
|
||||
|
||||
public static bool HasKey(IList<PropertyModification> modifications,
|
||||
WindowState state)
|
||||
{
|
||||
AnimationClip clip;
|
||||
double keyTime;
|
||||
bool inRange;
|
||||
|
||||
if (!GetClipAndRelativeTime(modifications[0].target, state, out clip, out keyTime, out inRange))
|
||||
return false;
|
||||
|
||||
return GetKeyTimes(modifications, state).Any(t => (CurveEditUtility.KeyCompare((float)state.editSequence.time, (float)t, clip.frameRate) == 0));
|
||||
}
|
||||
|
||||
// Checks if a key already exists for this property
|
||||
static bool HasBinding(UnityEngine.Object target, PropertyModification modification, AnimationClip clip, out EditorCurveBinding binding)
|
||||
{
|
||||
var component = target as Component;
|
||||
var playableAsset = target as IPlayableAsset;
|
||||
|
||||
if (component != null)
|
||||
{
|
||||
var type = AnimationUtility.PropertyModificationToEditorCurveBinding(modification, component.gameObject, out binding);
|
||||
binding = RotationCurveInterpolation.RemapAnimationBindingForRotationCurves(binding, clip);
|
||||
return type != null;
|
||||
}
|
||||
|
||||
if (playableAsset != null)
|
||||
{
|
||||
binding = EditorCurveBinding.FloatCurve(string.Empty, target.GetType(),
|
||||
AnimatedParameterUtility.GetAnimatedParameterBindingName(target, modification.propertyPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
binding = new EditorCurveBinding();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RemoveKey(UnityEngine.Object target, IEnumerable<PropertyModification> modifications,
|
||||
WindowState state)
|
||||
{
|
||||
AnimationClip clip;
|
||||
double keyTime;
|
||||
bool inRange;
|
||||
if (!GetClipAndRelativeTime(target, state, out clip, out keyTime, out inRange) || !inRange)
|
||||
return;
|
||||
var refreshPreview = false;
|
||||
TimelineUndo.PushUndo(clip, L10n.Tr("Remove Key"));
|
||||
foreach (var mod in modifications)
|
||||
{
|
||||
EditorCurveBinding temp;
|
||||
if (HasBinding(target, mod, clip, out temp))
|
||||
{
|
||||
if (temp.isPPtrCurve)
|
||||
{
|
||||
CurveEditUtility.RemoveObjectKey(clip, temp, keyTime);
|
||||
if (CurveEditUtility.GetObjectKeyCount(clip, temp) == 0)
|
||||
{
|
||||
refreshPreview = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, temp);
|
||||
if (curve != null)
|
||||
{
|
||||
CurveEditUtility.RemoveKeyFrameFromCurve(curve, (float)keyTime, clip.frameRate);
|
||||
AnimationUtility.SetEditorCurve(clip, temp, curve);
|
||||
if (curve.length == 0)
|
||||
{
|
||||
AnimationUtility.SetEditorCurve(clip, temp, null);
|
||||
refreshPreview = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshPreview)
|
||||
{
|
||||
state.ResetPreviewMode();
|
||||
}
|
||||
}
|
||||
|
||||
static HashSet<double> GetKeyTimes(IList<PropertyModification> modifications, WindowState state)
|
||||
{
|
||||
var keyTimes = new HashSet<double>();
|
||||
|
||||
AnimationClip animationClip;
|
||||
double keyTime;
|
||||
bool inRange;
|
||||
|
||||
var component = modifications[0].target as Component;
|
||||
var target = modifications[0].target;
|
||||
if (component != null)
|
||||
{
|
||||
var track = GetTrackForGameObject(component.gameObject, state);
|
||||
var go = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
|
||||
if (go != null)
|
||||
{
|
||||
target = go.transform;
|
||||
}
|
||||
}
|
||||
|
||||
GetClipAndRelativeTime(target, state, out animationClip, out keyTime, out inRange);
|
||||
if (animationClip == null)
|
||||
return keyTimes;
|
||||
|
||||
var playableAsset = target as IPlayableAsset;
|
||||
var info = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip);
|
||||
|
||||
TimelineClip clip = null;
|
||||
if (component != null)
|
||||
{
|
||||
GetTrackForGameObject(component.gameObject, state).FindRecordingClipAtTime(state.editSequence.time, out clip);
|
||||
}
|
||||
else if (playableAsset != null)
|
||||
{
|
||||
clip = FindClipWithAsset(state.editSequence.asset, playableAsset);
|
||||
}
|
||||
|
||||
foreach (var mod in modifications)
|
||||
{
|
||||
EditorCurveBinding temp;
|
||||
if (HasBinding(target, mod, animationClip, out temp))
|
||||
{
|
||||
IEnumerable<double> keys = new HashSet<double>();
|
||||
if (temp.isPPtrCurve)
|
||||
{
|
||||
var curve = info.GetObjectCurveForBinding(temp);
|
||||
if (curve != null)
|
||||
{
|
||||
keys = curve.Select(x => (double)x.time);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var curve = info.GetCurveForBinding(temp);
|
||||
if (curve != null)
|
||||
{
|
||||
keys = curve.keys.Select(x => (double)x.time);
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the times in to 'global' space using the clip
|
||||
if (clip != null)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
{
|
||||
var time = clip.FromLocalTimeUnbound(k);
|
||||
const double eps = 1e-5;
|
||||
if (time >= clip.start - eps && time <= clip.end + eps)
|
||||
{
|
||||
keyTimes.Add(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
// infinite clip mode, global == local space
|
||||
else
|
||||
{
|
||||
keyTimes.UnionWith(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyTimes;
|
||||
}
|
||||
|
||||
public static void NextKey(UnityEngine.Object target, IList<PropertyModification> modifications, WindowState state)
|
||||
{
|
||||
const double eps = 1e-5;
|
||||
var keyTimes = GetKeyTimes(modifications, state);
|
||||
if (keyTimes.Count == 0)
|
||||
return;
|
||||
var nextKeys = keyTimes.Where(x => x > state.editSequence.time + eps);
|
||||
if (nextKeys.Any())
|
||||
{
|
||||
state.editSequence.time = nextKeys.Min();
|
||||
}
|
||||
}
|
||||
|
||||
public static void PrevKey(UnityEngine.Object target, IList<PropertyModification> modifications, WindowState state)
|
||||
{
|
||||
const double eps = 1e-5;
|
||||
var keyTimes = GetKeyTimes(modifications, state);
|
||||
if (keyTimes.Count == 0)
|
||||
return;
|
||||
var prevKeys = keyTimes.Where(x => x < state.editSequence.time - eps);
|
||||
if (prevKeys.Any())
|
||||
{
|
||||
state.editSequence.time = prevKeys.Max();
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveCurve(UnityEngine.Object target, IEnumerable<PropertyModification> modifications, WindowState state)
|
||||
{
|
||||
AnimationClip clip = null;
|
||||
double keyTime = 0;
|
||||
var inRange = false; // not used for curves
|
||||
if (!GetClipAndRelativeTime(target, state, out clip, out keyTime, out inRange))
|
||||
return;
|
||||
|
||||
TimelineUndo.PushUndo(clip, L10n.Tr("Remove Curve"));
|
||||
foreach (var mod in modifications)
|
||||
{
|
||||
EditorCurveBinding temp;
|
||||
if (HasBinding(target, mod, clip, out temp))
|
||||
{
|
||||
if (temp.isPPtrCurve)
|
||||
AnimationUtility.SetObjectReferenceCurve(clip, temp, null);
|
||||
else
|
||||
AnimationUtility.SetEditorCurve(clip, temp, null);
|
||||
}
|
||||
}
|
||||
|
||||
state.ResetPreviewMode();
|
||||
}
|
||||
|
||||
public static void KeyAllProperties(Component target, WindowState state)
|
||||
{
|
||||
var go = target is Component component ? component.gameObject : null;
|
||||
GetClipAndRelativeTime(target, state, out var animationClip, out _, out _);
|
||||
|
||||
var info = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip);
|
||||
if (animationClip != null && info.curves.Length > 0)
|
||||
{
|
||||
KeyProperties(go, state, info.bindings);
|
||||
}
|
||||
}
|
||||
|
||||
public static void KeyProperties(GameObject go, WindowState state, IList<EditorCurveBinding> bindings)
|
||||
{
|
||||
var allKeyedProperties = new List<PropertyModification>();
|
||||
var rotationPaths = new HashSet<string>();
|
||||
for (var i = 0; i < bindings.Count; ++i)
|
||||
{
|
||||
// Skip the euler and key quaternion+hint
|
||||
if (CurveEditUtility.IsRotationKey(bindings[i]))
|
||||
{
|
||||
rotationPaths.Add(bindings[i].path);
|
||||
continue;
|
||||
}
|
||||
|
||||
AnimationUtility.GetFloatValue(go, bindings[i], out var val);
|
||||
var compo = GetTargetFromEditorBinding(go, bindings[i]);
|
||||
allKeyedProperties.Add(new PropertyModification
|
||||
{
|
||||
target = compo, value = val.ToString(EditorGUI.kFloatFieldFormatString),
|
||||
propertyPath = bindings[i].propertyName
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var path in rotationPaths)
|
||||
{
|
||||
foreach (var binding in GetRotationBindings(path))
|
||||
{
|
||||
var compo = GetTargetFromEditorBinding(go, binding);
|
||||
var readBinding = binding;
|
||||
switch (binding.propertyName)
|
||||
{
|
||||
case kLocalEulerHint + ".x":
|
||||
readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".x");
|
||||
break;
|
||||
case kLocalEulerHint + ".y":
|
||||
readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".y");
|
||||
break;
|
||||
case kLocalEulerHint + ".z":
|
||||
readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".z");
|
||||
break;
|
||||
}
|
||||
|
||||
AnimationUtility.GetFloatValue(go, readBinding, out var val);
|
||||
allKeyedProperties.Add(new PropertyModification
|
||||
{
|
||||
target = compo, value = val.ToString(EditorGUI.kFloatFieldFormatString),
|
||||
propertyPath = binding.propertyName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AddKey(allKeyedProperties, state);
|
||||
state.Refresh();
|
||||
}
|
||||
|
||||
static IEnumerable<EditorCurveBinding> GetRotationBindings(string path)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".x"),
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".y"),
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".z"),
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".w"),
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".x"),
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".y"),
|
||||
EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".z"),
|
||||
};
|
||||
}
|
||||
|
||||
static Component GetTargetFromEditorBinding(GameObject root, EditorCurveBinding binding)
|
||||
{
|
||||
GameObject go = null;
|
||||
if (string.IsNullOrEmpty(binding.path))
|
||||
{
|
||||
go = root;
|
||||
}
|
||||
|
||||
var childTransform = root.transform.Find(binding.path);
|
||||
if (childTransform != null)
|
||||
{
|
||||
go = childTransform.gameObject;
|
||||
}
|
||||
|
||||
if (go != null)
|
||||
{
|
||||
return go.GetComponent(binding.type);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IEnumerable<GameObject> GetRecordableGameObjects(WindowState state)
|
||||
{
|
||||
if (state == null || state.editSequence.asset == null || state.editSequence.director == null)
|
||||
yield break;
|
||||
|
||||
var outputTracks = state.editSequence.asset.GetOutputTracks();
|
||||
foreach (var track in outputTracks)
|
||||
{
|
||||
if (track.GetType() != typeof(AnimationTrack))
|
||||
continue;
|
||||
if (!state.IsTrackRecordable(track) && !track.GetChildTracks().Any(state.IsTrackRecordable))
|
||||
continue;
|
||||
|
||||
var obj = TimelineUtility.GetSceneGameObject(state.editSequence.director, track);
|
||||
if (obj != null)
|
||||
{
|
||||
yield return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ef4c81c9368d5a340b14c2fec1cad345
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,142 @@
|
|||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace UnityEditor.Timeline
|
||||
{
|
||||
class TimelineRecordingContextualResponder : IAnimationContextualResponder
|
||||
{
|
||||
public WindowState state { get; internal set; }
|
||||
|
||||
public TimelineRecordingContextualResponder(WindowState _state)
|
||||
{
|
||||
state = _state;
|
||||
}
|
||||
|
||||
//Unsupported stuff
|
||||
public bool HasAnyCandidates() { return false; }
|
||||
|
||||
public bool HasAnyCurves()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AddCandidateKeys() {}
|
||||
|
||||
public void AddAnimatedKeys()
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAnimatable(PropertyModification[] modifications)
|
||||
{
|
||||
// search playable assets
|
||||
for (int i = 0; i < modifications.Length; i++)
|
||||
{
|
||||
var iAsset = modifications[i].target as IPlayableAsset;
|
||||
if (iAsset != null)
|
||||
{
|
||||
var curvesOwner = AnimatedParameterUtility.ToCurvesOwner(iAsset, state.editSequence.asset);
|
||||
if (curvesOwner != null && curvesOwner.HasAnyAnimatableParameters() && curvesOwner.IsParameterAnimatable(modifications[i].propertyPath))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// search recordable game objects
|
||||
foreach (var gameObject in TimelineRecording.GetRecordableGameObjects(state))
|
||||
{
|
||||
for (int i = 0; i < modifications.Length; ++i)
|
||||
{
|
||||
var modification = modifications[i];
|
||||
if (AnimationWindowUtility.PropertyIsAnimatable(modification.target, modification.propertyPath, gameObject))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsEditable(Object targetObject)
|
||||
{
|
||||
return true; // i.e. all animatable properties are editable
|
||||
}
|
||||
|
||||
public bool KeyExists(PropertyModification[] modifications)
|
||||
{
|
||||
if (modifications.Length == 0 || modifications[0].target == null)
|
||||
return false;
|
||||
|
||||
return TimelineRecording.HasKey(modifications, state);
|
||||
}
|
||||
|
||||
public bool CandidateExists(PropertyModification[] modifications)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CurveExists(PropertyModification[] modifications)
|
||||
{
|
||||
if (modifications.Length == 0 || modifications[0].target == null)
|
||||
return false;
|
||||
|
||||
return TimelineRecording.HasCurve(modifications, modifications[0].target, state);
|
||||
}
|
||||
|
||||
public void AddKey(PropertyModification[] modifications)
|
||||
{
|
||||
TimelineRecording.AddKey(modifications, state);
|
||||
state.Refresh();
|
||||
}
|
||||
|
||||
public void RemoveKey(PropertyModification[] modifications)
|
||||
{
|
||||
if (modifications.Length == 0)
|
||||
return;
|
||||
|
||||
var target = modifications[0].target;
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
TimelineRecording.RemoveKey(modifications[0].target, modifications, state);
|
||||
|
||||
var curvesOwner = target as ICurvesOwner;
|
||||
if (curvesOwner != null)
|
||||
curvesOwner.SanitizeCurvesData();
|
||||
|
||||
state.Refresh();
|
||||
}
|
||||
|
||||
public void RemoveCurve(PropertyModification[] modifications)
|
||||
{
|
||||
if (modifications.Length == 0)
|
||||
return;
|
||||
|
||||
var target = modifications[0].target;
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
TimelineRecording.RemoveCurve(target, modifications, state);
|
||||
|
||||
var curvesOwner = target as ICurvesOwner;
|
||||
if (curvesOwner != null)
|
||||
curvesOwner.SanitizeCurvesData();
|
||||
|
||||
state.Refresh();
|
||||
}
|
||||
|
||||
public void GoToNextKeyframe(PropertyModification[] modifications)
|
||||
{
|
||||
if (modifications.Length == 0 || modifications[0].target == null)
|
||||
return;
|
||||
|
||||
TimelineRecording.NextKey(modifications[0].target, modifications, state);
|
||||
state.Refresh();
|
||||
}
|
||||
|
||||
public void GoToPreviousKeyframe(PropertyModification[] modifications)
|
||||
{
|
||||
TimelineRecording.PrevKey(modifications[0].target, modifications, state);
|
||||
state.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a59c2e62fbd97f84f92c3b546e3903cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,718 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Timeline;
|
||||
using System.Globalization;
|
||||
|
||||
namespace UnityEditor.Timeline
|
||||
{
|
||||
// Methods and data for handling recording to monobehaviours
|
||||
static partial class TimelineRecording
|
||||
{
|
||||
internal class RecordingState : IAnimationRecordingState
|
||||
{
|
||||
public GameObject activeGameObject { get; set; }
|
||||
public GameObject activeRootGameObject { get; set; }
|
||||
public AnimationClip activeAnimationClip { get; set; }
|
||||
|
||||
public void SaveCurve(AnimationWindowCurve curve)
|
||||
{
|
||||
Undo.RegisterCompleteObjectUndo(activeAnimationClip, L10n.Tr("Edit Curve"));
|
||||
AnimationWindowUtility.SaveCurve(activeAnimationClip, curve);
|
||||
}
|
||||
|
||||
public void AddPropertyModification(EditorCurveBinding binding, PropertyModification propertyModification, bool keepPrefabOverride)
|
||||
{
|
||||
AnimationMode.AddPropertyModification(binding, propertyModification, keepPrefabOverride);
|
||||
}
|
||||
|
||||
public bool addZeroFrame
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public int currentFrame { get; set; }
|
||||
|
||||
public bool DiscardModification(PropertyModification modification)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static readonly RecordingState s_RecordState = new RecordingState();
|
||||
static readonly AnimationTrackRecorder s_TrackRecorder = new AnimationTrackRecorder();
|
||||
static readonly List<UndoPropertyModification> s_UnprocessedMods = new List<UndoPropertyModification>();
|
||||
static readonly List<UndoPropertyModification> s_ModsToProcess = new List<UndoPropertyModification>();
|
||||
static AnimationTrack s_LastTrackWarning;
|
||||
|
||||
public const string kLocalPosition = "m_LocalPosition";
|
||||
public const string kLocalRotation = "m_LocalRotation";
|
||||
public const string kLocalEulerHint = "m_LocalEulerAnglesHint";
|
||||
const string kRotationWarning = "You are recording with an initial rotation offset. This may result in a misrepresentation of euler angles. When recording transform properties, it is recommended to reset rotation prior to recording";
|
||||
|
||||
|
||||
public static bool IsRecordingAnimationTrack { get; private set; }
|
||||
|
||||
|
||||
internal static UndoPropertyModification[] ProcessMonoBehaviourModification(UndoPropertyModification[] modifications, WindowState state)
|
||||
{
|
||||
if (!state.recording)
|
||||
return modifications;
|
||||
|
||||
if (state == null || state.editSequence.director == null)
|
||||
return modifications;
|
||||
|
||||
s_UnprocessedMods.Clear();
|
||||
|
||||
s_TrackRecorder.PrepareForRecord(state);
|
||||
|
||||
s_ModsToProcess.Clear();
|
||||
s_ModsToProcess.AddRange(modifications.Reverse());
|
||||
|
||||
while (s_ModsToProcess.Count > 0)
|
||||
{
|
||||
var modification = s_ModsToProcess[s_ModsToProcess.Count - 1];
|
||||
s_ModsToProcess.RemoveAt(s_ModsToProcess.Count - 1);
|
||||
|
||||
// grab the clip we need to apply to
|
||||
var modifiedGO = GetGameObjectFromModification(modification);
|
||||
var track = GetTrackForGameObject(modifiedGO, state);
|
||||
if (track != null)
|
||||
{
|
||||
IsRecordingAnimationTrack = true;
|
||||
|
||||
double startTime = 0;
|
||||
var clip = s_TrackRecorder.PrepareTrack(track, state, modifiedGO, out startTime);
|
||||
if (clip == null)
|
||||
{
|
||||
s_ModsToProcess.Reverse();
|
||||
return s_ModsToProcess.ToArray();
|
||||
}
|
||||
s_RecordState.activeAnimationClip = clip;
|
||||
s_RecordState.activeRootGameObject = state.GetSceneReference(track);
|
||||
s_RecordState.activeGameObject = modifiedGO;
|
||||
s_RecordState.currentFrame = Mathf.RoundToInt((float)startTime);
|
||||
|
||||
EditorUtility.SetDirty(clip);
|
||||
var toProcess = GatherRelatedModifications(modification, s_ModsToProcess);
|
||||
|
||||
var animator = s_RecordState.activeRootGameObject.GetComponent<Animator>();
|
||||
var animTrack = track as AnimationTrack;
|
||||
|
||||
// update preview mode before recording so the correct values get placed (in case we modify offsets)
|
||||
// Case 900624
|
||||
UpdatePreviewMode(toProcess, modifiedGO);
|
||||
|
||||
// if this is the first position/rotation recording, copy the current position / rotation to the track offset
|
||||
AddTrackOffset(animTrack, toProcess, clip, animator);
|
||||
|
||||
// same for clip mod clips being created
|
||||
AddClipOffset(animTrack, toProcess, s_TrackRecorder.recordClip, animator);
|
||||
|
||||
// Check if we need to handle position/rotation offsets
|
||||
var handleOffsets = animator != null && modification.currentValue != null &&
|
||||
modification.currentValue.target == s_RecordState.activeRootGameObject.transform &&
|
||||
HasOffsets(animTrack, s_TrackRecorder.recordClip);
|
||||
if (handleOffsets)
|
||||
{
|
||||
toProcess = HandleEulerModifications(animTrack, s_TrackRecorder.recordClip, clip, s_RecordState.currentFrame * clip.frameRate, toProcess);
|
||||
RemoveOffsets(modification, animTrack, s_TrackRecorder.recordClip, toProcess);
|
||||
}
|
||||
|
||||
var remaining = AnimationRecording.Process(s_RecordState, toProcess);
|
||||
if (remaining != null && remaining.Length != 0)
|
||||
{
|
||||
s_UnprocessedMods.AddRange(remaining);
|
||||
}
|
||||
|
||||
if (handleOffsets)
|
||||
{
|
||||
ReapplyOffsets(modification, animTrack, s_TrackRecorder.recordClip, toProcess);
|
||||
}
|
||||
|
||||
s_TrackRecorder.FinializeTrack(track, state);
|
||||
|
||||
IsRecordingAnimationTrack = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_UnprocessedMods.Add(modification);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
s_TrackRecorder.FinalizeRecording(state);
|
||||
|
||||
return s_UnprocessedMods.ToArray();
|
||||
}
|
||||
|
||||
internal static bool IsPosition(UndoPropertyModification modification)
|
||||
{
|
||||
if (modification.currentValue != null)
|
||||
return modification.currentValue.propertyPath.StartsWith(kLocalPosition);
|
||||
else if (modification.previousValue != null)
|
||||
return modification.previousValue.propertyPath.StartsWith(kLocalPosition);
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsRotation(UndoPropertyModification modification)
|
||||
{
|
||||
if (modification.currentValue != null)
|
||||
return modification.currentValue.propertyPath.StartsWith(kLocalRotation) ||
|
||||
modification.currentValue.propertyPath.StartsWith(kLocalEulerHint);
|
||||
if (modification.previousValue != null)
|
||||
return modification.previousValue.propertyPath.StartsWith(kLocalRotation) ||
|
||||
modification.previousValue.propertyPath.StartsWith(kLocalEulerHint);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test if this modification position or rotation
|
||||
internal static bool IsPositionOrRotation(UndoPropertyModification modification)
|
||||
{
|
||||
return IsPosition(modification) || IsRotation(modification);
|
||||
}
|
||||
|
||||
internal static void UpdatePreviewMode(UndoPropertyModification[] mods, GameObject go)
|
||||
{
|
||||
if (mods.Any(x => IsPositionOrRotation(x) && IsRootModification(x)))
|
||||
{
|
||||
bool hasPosition = false;
|
||||
bool hasRotation = false;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
EditorCurveBinding binding = new EditorCurveBinding();
|
||||
if (AnimationUtility.PropertyModificationToEditorCurveBinding(mod.previousValue, go, out binding) != null)
|
||||
{
|
||||
hasPosition |= IsPosition(mod);
|
||||
hasRotation |= IsRotation(mod);
|
||||
AnimationMode.AddPropertyModification(binding, mod.previousValue, true);
|
||||
}
|
||||
}
|
||||
|
||||
// case 931859 - if we are only changing one field, all fields must be registered before
|
||||
// any recording modifications
|
||||
var driver = WindowState.previewDriver;
|
||||
if (driver != null && AnimationMode.InAnimationMode(driver))
|
||||
{
|
||||
if (hasPosition)
|
||||
{
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalPosition + ".x");
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalPosition + ".y");
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalPosition + ".z");
|
||||
}
|
||||
else if (hasRotation)
|
||||
{
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalRotation + ".x");
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalRotation + ".y");
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalRotation + ".z");
|
||||
DrivenPropertyManager.RegisterProperty(driver, go.transform, kLocalRotation + ".w");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsRootModification(UndoPropertyModification modification)
|
||||
{
|
||||
string path = string.Empty;
|
||||
if (modification.currentValue != null)
|
||||
path = modification.currentValue.propertyPath;
|
||||
else if (modification.previousValue != null)
|
||||
path = modification.previousValue.propertyPath;
|
||||
|
||||
return !path.Contains('/') && !path.Contains('\\');
|
||||
}
|
||||
|
||||
// test if the clip has any position or rotation bindings
|
||||
internal static bool ClipHasPositionOrRotation(AnimationClip clip)
|
||||
{
|
||||
if (clip == null || clip.empty)
|
||||
return false;
|
||||
|
||||
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
|
||||
for (var i = 0; i < info.bindings.Length; i++)
|
||||
{
|
||||
bool isPositionOrRotation =
|
||||
info.bindings[i].type != null &&
|
||||
typeof(Transform).IsAssignableFrom(info.bindings[i].type) &&
|
||||
(
|
||||
info.bindings[i].propertyName.StartsWith(kLocalPosition) ||
|
||||
info.bindings[i].propertyName.StartsWith(kLocalRotation) ||
|
||||
info.bindings[i].propertyName.StartsWith("localEuler")
|
||||
);
|
||||
|
||||
if (isPositionOrRotation)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static TimelineAnimationUtilities.RigidTransform ComputeInitialClipOffsets(AnimationTrack track, UndoPropertyModification[] mods, Animator animator)
|
||||
{
|
||||
// take into account the track transform
|
||||
var target = GetInitialTransform(mods, animator);
|
||||
var trackToClip = TimelineAnimationUtilities.RigidTransform.identity;
|
||||
if (track.trackOffset == TrackOffset.ApplyTransformOffsets)
|
||||
trackToClip = TimelineAnimationUtilities.RigidTransform.Compose(track.position, track.rotation);
|
||||
else if (track.trackOffset == TrackOffset.ApplySceneOffsets)
|
||||
trackToClip = TimelineAnimationUtilities.RigidTransform.Compose(track.sceneOffsetPosition, Quaternion.Euler(track.sceneOffsetRotation));
|
||||
|
||||
target = TimelineAnimationUtilities.RigidTransform.Mul(TimelineAnimationUtilities.RigidTransform.Inverse(trackToClip), target);
|
||||
|
||||
// set the previous position in case the animation system adds a default key
|
||||
SetPreviousPositionAndRotation(mods, animator, trackToClip.position, trackToClip.rotation);
|
||||
return target;
|
||||
}
|
||||
|
||||
internal static TimelineAnimationUtilities.RigidTransform GetInitialTransform(UndoPropertyModification[] mods, Animator animator)
|
||||
{
|
||||
var pos = Vector3.zero;
|
||||
var rot = Quaternion.identity;
|
||||
|
||||
// if we are operating on the root, grab the transform from the undo
|
||||
if (mods[0].previousValue.target == animator.transform)
|
||||
{
|
||||
GetPreviousPositionAndRotation(mods, ref pos, ref rot);
|
||||
}
|
||||
// otherwise we need to grab it from the root object, which is the one with the actual animator
|
||||
else
|
||||
{
|
||||
pos = animator.transform.localPosition;
|
||||
rot = animator.transform.localRotation;
|
||||
}
|
||||
|
||||
// take into account the track transform
|
||||
return TimelineAnimationUtilities.RigidTransform.Compose(pos, rot);
|
||||
}
|
||||
|
||||
internal static void SetPreviousPositionAndRotation(UndoPropertyModification[] mods, Animator animator, Vector3 pos, Quaternion rot)
|
||||
{
|
||||
if (mods[0].previousValue.target == animator.transform)
|
||||
{
|
||||
SetPreviousPositionAndRotation(mods, pos, rot);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are adding to an infinite clip, strip the objects position and rotation and set it as the clip offset
|
||||
internal static void AddTrackOffset(AnimationTrack track, UndoPropertyModification[] mods, AnimationClip clip, Animator animator)
|
||||
{
|
||||
var copyTrackOffset = !track.inClipMode &&
|
||||
!ClipHasPositionOrRotation(clip) &&
|
||||
mods.Any(x => IsPositionOrRotation(x) && IsRootModification(x)) &&
|
||||
animator != null;
|
||||
if (copyTrackOffset)
|
||||
{
|
||||
// in scene offset mode, makes sure we have the correct initial transform set
|
||||
if (track.trackOffset == TrackOffset.ApplySceneOffsets)
|
||||
{
|
||||
var rigidTransform = GetInitialTransform(mods, animator);
|
||||
track.sceneOffsetPosition = rigidTransform.position;
|
||||
track.sceneOffsetRotation = rigidTransform.rotation.eulerAngles;
|
||||
SetPreviousPositionAndRotation(mods, animator, rigidTransform.position, rigidTransform.rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rigidTransform = ComputeInitialClipOffsets(track, mods, animator);
|
||||
track.infiniteClipOffsetPosition = rigidTransform.position;
|
||||
track.infiniteClipOffsetEulerAngles = rigidTransform.rotation.eulerAngles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void AddClipOffset(AnimationTrack track, UndoPropertyModification[] mods, TimelineClip clip, Animator animator)
|
||||
{
|
||||
if (clip == null || clip.asset == null)
|
||||
return;
|
||||
|
||||
var clipAsset = clip.asset as AnimationPlayableAsset;
|
||||
var copyClipOffset = track.inClipMode &&
|
||||
clipAsset != null && !ClipHasPositionOrRotation(clipAsset.clip) &&
|
||||
mods.Any(x => IsPositionOrRotation(x) && IsRootModification(x)) &&
|
||||
animator != null;
|
||||
if (copyClipOffset)
|
||||
{
|
||||
var rigidTransform = ComputeInitialClipOffsets(track, mods, animator);
|
||||
|
||||
clipAsset.position = rigidTransform.position;
|
||||
clipAsset.rotation = rigidTransform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
internal static TimelineAnimationUtilities.RigidTransform GetLocalToTrack(AnimationTrack track, TimelineClip clip)
|
||||
{
|
||||
if (track == null)
|
||||
return TimelineAnimationUtilities.RigidTransform.Compose(Vector3.zero, Quaternion.identity);
|
||||
|
||||
var trackPos = track.position;
|
||||
var trackRot = track.rotation;
|
||||
|
||||
if (track.trackOffset == TrackOffset.ApplySceneOffsets)
|
||||
{
|
||||
trackPos = track.sceneOffsetPosition;
|
||||
trackRot = Quaternion.Euler(track.sceneOffsetRotation);
|
||||
}
|
||||
|
||||
var clipWrapper = clip == null ? null : clip.asset as AnimationPlayableAsset;
|
||||
var clipTransform = TimelineAnimationUtilities.RigidTransform.Compose(Vector3.zero, Quaternion.identity);
|
||||
if (clipWrapper != null)
|
||||
{
|
||||
clipTransform = TimelineAnimationUtilities.RigidTransform.Compose(clipWrapper.position, clipWrapper.rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
clipTransform = TimelineAnimationUtilities.RigidTransform.Compose(track.infiniteClipOffsetPosition, track.infiniteClipOffsetRotation);
|
||||
}
|
||||
|
||||
var trackTransform = TimelineAnimationUtilities.RigidTransform.Compose(trackPos, trackRot);
|
||||
|
||||
return TimelineAnimationUtilities.RigidTransform.Mul(trackTransform, clipTransform);
|
||||
}
|
||||
|
||||
// Checks whether there are any offsets applied to a clip
|
||||
internal static bool HasOffsets(AnimationTrack track, TimelineClip clip)
|
||||
{
|
||||
if (track == null)
|
||||
return false;
|
||||
|
||||
bool hasClipOffsets = false;
|
||||
bool hasTrackOffsets = false;
|
||||
|
||||
var clipWrapper = clip == null ? null : clip.asset as AnimationPlayableAsset;
|
||||
if (clipWrapper != null)
|
||||
hasClipOffsets |= clipWrapper.position != Vector3.zero || clipWrapper.rotation != Quaternion.identity;
|
||||
|
||||
if (track.trackOffset == TrackOffset.ApplySceneOffsets)
|
||||
{
|
||||
hasTrackOffsets = track.sceneOffsetPosition != Vector3.zero || track.sceneOffsetRotation != Vector3.zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasTrackOffsets = (track.position != Vector3.zero || track.rotation != Quaternion.identity);
|
||||
if (!track.inClipMode)
|
||||
hasClipOffsets |= track.infiniteClipOffsetPosition != Vector3.zero || track.infiniteClipOffsetRotation != Quaternion.identity;
|
||||
}
|
||||
|
||||
return hasTrackOffsets || hasClipOffsets;
|
||||
}
|
||||
|
||||
internal static void RemoveOffsets(UndoPropertyModification modification, AnimationTrack track, TimelineClip clip, UndoPropertyModification[] mods)
|
||||
{
|
||||
if (IsPositionOrRotation(modification))
|
||||
{
|
||||
var modifiedGO = GetGameObjectFromModification(modification);
|
||||
var target = TimelineAnimationUtilities.RigidTransform.Compose(modifiedGO.transform.localPosition, modifiedGO.transform.localRotation);
|
||||
var localToTrack = GetLocalToTrack(track, clip);
|
||||
var trackToLocal = TimelineAnimationUtilities.RigidTransform.Inverse(localToTrack);
|
||||
var localSpace = TimelineAnimationUtilities.RigidTransform.Mul(trackToLocal, target);
|
||||
|
||||
// Update the undo call values
|
||||
var prevPos = modifiedGO.transform.localPosition;
|
||||
var prevRot = modifiedGO.transform.localRotation;
|
||||
GetPreviousPositionAndRotation(mods, ref prevPos, ref prevRot);
|
||||
var previousRigidTransform = TimelineAnimationUtilities.RigidTransform.Mul(trackToLocal, TimelineAnimationUtilities.RigidTransform.Compose(prevPos, prevRot));
|
||||
SetPreviousPositionAndRotation(mods, previousRigidTransform.position, previousRigidTransform.rotation);
|
||||
|
||||
var currentPos = modifiedGO.transform.localPosition;
|
||||
var currentRot = modifiedGO.transform.localRotation;
|
||||
GetCurrentPositionAndRotation(mods, ref currentPos, ref currentRot);
|
||||
var currentRigidTransform = TimelineAnimationUtilities.RigidTransform.Mul(trackToLocal, TimelineAnimationUtilities.RigidTransform.Compose(currentPos, currentRot));
|
||||
SetCurrentPositionAndRotation(mods, currentRigidTransform.position, currentRigidTransform.rotation);
|
||||
|
||||
modifiedGO.transform.localPosition = localSpace.position;
|
||||
modifiedGO.transform.localRotation = localSpace.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ReapplyOffsets(UndoPropertyModification modification, AnimationTrack track, TimelineClip clip, UndoPropertyModification[] mods)
|
||||
{
|
||||
if (IsPositionOrRotation(modification))
|
||||
{
|
||||
var modifiedGO = GetGameObjectFromModification(modification);
|
||||
var target = TimelineAnimationUtilities.RigidTransform.Compose(modifiedGO.transform.localPosition, modifiedGO.transform.localRotation);
|
||||
var localToTrack = GetLocalToTrack(track, clip);
|
||||
var trackSpace = TimelineAnimationUtilities.RigidTransform.Mul(localToTrack, target);
|
||||
|
||||
// Update the undo call values
|
||||
var prevPos = modifiedGO.transform.localPosition;
|
||||
var prevRot = modifiedGO.transform.localRotation;
|
||||
GetPreviousPositionAndRotation(mods, ref prevPos, ref prevRot);
|
||||
var previousRigidTransform = TimelineAnimationUtilities.RigidTransform.Mul(localToTrack, TimelineAnimationUtilities.RigidTransform.Compose(prevPos, prevRot));
|
||||
SetPreviousPositionAndRotation(mods, previousRigidTransform.position, previousRigidTransform.rotation);
|
||||
|
||||
var currentPos = modifiedGO.transform.localPosition;
|
||||
var currentRot = modifiedGO.transform.localRotation;
|
||||
GetCurrentPositionAndRotation(mods, ref currentPos, ref currentRot);
|
||||
var currentRigidTransform = TimelineAnimationUtilities.RigidTransform.Mul(localToTrack, TimelineAnimationUtilities.RigidTransform.Compose(currentPos, currentRot));
|
||||
SetCurrentPositionAndRotation(mods, currentRigidTransform.position, currentRigidTransform.rotation);
|
||||
|
||||
modifiedGO.transform.localPosition = trackSpace.position;
|
||||
modifiedGO.transform.localRotation = trackSpace.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
// This will gather the modifications that modify the same property on the same object (rgba of a color, xyzw of a vector)
|
||||
// Note: This will modify the list, removing any elements that match
|
||||
static UndoPropertyModification[] GatherRelatedModifications(UndoPropertyModification toMatch, List<UndoPropertyModification> list)
|
||||
{
|
||||
var matching = new List<UndoPropertyModification> {toMatch};
|
||||
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var undo = list[i];
|
||||
if (undo.previousValue.target == toMatch.previousValue.target &&
|
||||
DoesPropertyPathMatch(undo.previousValue.propertyPath, toMatch.previousValue.propertyPath))
|
||||
{
|
||||
matching.Add(undo);
|
||||
list.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return matching.ToArray();
|
||||
}
|
||||
|
||||
// Grab the game object out of the modification object
|
||||
static GameObject GetGameObjectFromModification(UndoPropertyModification mod)
|
||||
{
|
||||
// grab the GO this is modifying
|
||||
GameObject modifiedGO = null;
|
||||
if (mod.previousValue.target is GameObject)
|
||||
modifiedGO = mod.previousValue.target as GameObject;
|
||||
else if (mod.previousValue.target is Component)
|
||||
modifiedGO = (mod.previousValue.target as Component).gameObject;
|
||||
|
||||
return modifiedGO;
|
||||
}
|
||||
|
||||
// returns the level of the child in the hierarchy relative to the parent,
|
||||
// or -1 if the child is not the parent or a descendent of it
|
||||
static int GetChildLevel(GameObject parent, GameObject child)
|
||||
{
|
||||
var level = 0;
|
||||
while (child != null)
|
||||
{
|
||||
if (parent == child)
|
||||
break;
|
||||
if (child.transform.parent == null)
|
||||
return -1;
|
||||
child = child.transform.parent.gameObject;
|
||||
level++;
|
||||
}
|
||||
|
||||
if (child != null)
|
||||
return level;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool IsRotationCurve(string propertyName)
|
||||
{
|
||||
string groupName = AnimationWindowUtility.GetPropertyGroupName(propertyName);
|
||||
return groupName == kLocalRotation || groupName == kLocalEulerHint;
|
||||
}
|
||||
|
||||
public static bool DoesPropertyPathMatch(string a, string b)
|
||||
{
|
||||
return AnimationWindowUtility.GetPropertyGroupName(a).Equals(AnimationWindowUtility.GetPropertyGroupName(b)) || IsRotationCurve(a) && IsRotationCurve(b);
|
||||
}
|
||||
|
||||
internal static void GetPreviousPositionAndRotation(UndoPropertyModification[] mods, ref Vector3 position, ref Quaternion rotation)
|
||||
{
|
||||
var t = mods[0].previousValue.target as Transform;
|
||||
if (t == null)
|
||||
t = (Transform)mods[0].currentValue.target;
|
||||
|
||||
position = t.localPosition;
|
||||
rotation = t.localRotation;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod.previousValue.propertyPath)
|
||||
{
|
||||
case kLocalPosition + ".x":
|
||||
position.x = ParseFloat(mod.previousValue.value, position.x);
|
||||
break;
|
||||
case kLocalPosition + ".y":
|
||||
position.y = ParseFloat(mod.previousValue.value, position.y);
|
||||
break;
|
||||
case kLocalPosition + ".z":
|
||||
position.z = ParseFloat(mod.previousValue.value, position.z);
|
||||
break;
|
||||
case kLocalRotation + ".x":
|
||||
rotation.x = ParseFloat(mod.previousValue.value, rotation.x);
|
||||
break;
|
||||
case kLocalRotation + ".y":
|
||||
rotation.y = ParseFloat(mod.previousValue.value, rotation.y);
|
||||
break;
|
||||
case kLocalRotation + ".z":
|
||||
rotation.z = ParseFloat(mod.previousValue.value, rotation.z);
|
||||
break;
|
||||
case kLocalRotation + ".w":
|
||||
rotation.w = ParseFloat(mod.previousValue.value, rotation.w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void GetCurrentPositionAndRotation(UndoPropertyModification[] mods, ref Vector3 position, ref Quaternion rotation)
|
||||
{
|
||||
var t = (Transform)mods[0].currentValue.target;
|
||||
position = t.localPosition;
|
||||
rotation = t.localRotation;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod.currentValue.propertyPath)
|
||||
{
|
||||
case kLocalPosition + ".x":
|
||||
position.x = ParseFloat(mod.currentValue.value, position.x);
|
||||
break;
|
||||
case kLocalPosition + ".y":
|
||||
position.y = ParseFloat(mod.currentValue.value, position.y);
|
||||
break;
|
||||
case kLocalPosition + ".z":
|
||||
position.z = ParseFloat(mod.currentValue.value, position.z);
|
||||
break;
|
||||
case kLocalRotation + ".x":
|
||||
rotation.x = ParseFloat(mod.currentValue.value, rotation.x);
|
||||
break;
|
||||
case kLocalRotation + ".y":
|
||||
rotation.y = ParseFloat(mod.currentValue.value, rotation.y);
|
||||
break;
|
||||
case kLocalRotation + ".z":
|
||||
rotation.z = ParseFloat(mod.currentValue.value, rotation.z);
|
||||
break;
|
||||
case kLocalRotation + ".w":
|
||||
rotation.w = ParseFloat(mod.currentValue.value, rotation.w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when making the previous position and rotation
|
||||
internal static void SetPreviousPositionAndRotation(UndoPropertyModification[] mods, Vector3 pos, Quaternion rot)
|
||||
{
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod.previousValue.propertyPath)
|
||||
{
|
||||
case kLocalPosition + ".x":
|
||||
mod.previousValue.value = pos.x.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalPosition + ".y":
|
||||
mod.previousValue.value = pos.y.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalPosition + ".z":
|
||||
mod.previousValue.value = pos.z.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".x":
|
||||
mod.previousValue.value = rot.x.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".y":
|
||||
mod.previousValue.value = rot.y.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".z":
|
||||
mod.previousValue.value = rot.z.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".w":
|
||||
mod.previousValue.value = rot.w.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetCurrentPositionAndRotation(UndoPropertyModification[] mods, Vector3 pos, Quaternion rot)
|
||||
{
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod.previousValue.propertyPath)
|
||||
{
|
||||
case kLocalPosition + ".x":
|
||||
mod.currentValue.value = pos.x.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalPosition + ".y":
|
||||
mod.currentValue.value = pos.y.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalPosition + ".z":
|
||||
mod.currentValue.value = pos.z.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".x":
|
||||
mod.currentValue.value = rot.x.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".y":
|
||||
mod.currentValue.value = rot.y.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".z":
|
||||
mod.currentValue.value = rot.z.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
case kLocalRotation + ".w":
|
||||
mod.currentValue.value = rot.w.ToString(EditorGUI.kFloatFieldFormatString);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static float ParseFloat(string str, float defaultVal)
|
||||
{
|
||||
float temp = 0.0f;
|
||||
if (float.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out temp))
|
||||
return temp;
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
internal static UndoPropertyModification[] HandleEulerModifications(AnimationTrack track, TimelineClip clip, AnimationClip animClip, float time, UndoPropertyModification[] mods)
|
||||
{
|
||||
if (mods.Any(x => x.currentValue.propertyPath.StartsWith(kLocalEulerHint) || x.currentValue.propertyPath.StartsWith(kLocalRotation)))
|
||||
{
|
||||
// if there is a rotational offsets, we need to strip the euler hints, since they are used by the animation recording system
|
||||
// over the quaternion.
|
||||
var localToTrack = GetLocalToTrack(track, clip);
|
||||
if (localToTrack.rotation != Quaternion.identity)
|
||||
{
|
||||
if (s_LastTrackWarning != track)
|
||||
{
|
||||
s_LastTrackWarning = track;
|
||||
Debug.LogWarning(kRotationWarning);
|
||||
}
|
||||
|
||||
Transform transform = mods[0].currentValue.target as Transform;
|
||||
if (transform != null)
|
||||
{
|
||||
var trackToLocal = TimelineAnimationUtilities.RigidTransform.Inverse(localToTrack);
|
||||
// since the euler angles are going to be transformed, we do a best guess at a euler that gives the shortest path
|
||||
var quatMods = mods.Where(x => !x.currentValue.propertyPath.StartsWith(kLocalEulerHint));
|
||||
var eulerMods = FindBestEulerHint(trackToLocal.rotation * transform.localRotation, animClip, time, transform);
|
||||
return quatMods.Union(eulerMods).ToArray();
|
||||
}
|
||||
return mods.Where(x => !x.currentValue.propertyPath.StartsWith(kLocalEulerHint)).ToArray();
|
||||
}
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
internal static IEnumerable<UndoPropertyModification> FindBestEulerHint(Quaternion rotation, AnimationClip clip, float time, Transform transform)
|
||||
{
|
||||
Vector3 euler = rotation.eulerAngles;
|
||||
|
||||
var xCurve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(string.Empty, typeof(Transform), "localEulerAnglesRaw.x"));
|
||||
var yCurve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(string.Empty, typeof(Transform), "localEulerAnglesRaw.y"));
|
||||
var zCurve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(string.Empty, typeof(Transform), "localEulerAnglesRaw.z"));
|
||||
|
||||
if (xCurve != null)
|
||||
euler.x = xCurve.Evaluate(time);
|
||||
if (yCurve != null)
|
||||
euler.y = yCurve.Evaluate(time);
|
||||
if (zCurve != null)
|
||||
euler.z = zCurve.Evaluate(time);
|
||||
|
||||
euler = QuaternionCurveTangentCalculation.GetEulerFromQuaternion(rotation, euler);
|
||||
|
||||
return new[]
|
||||
{
|
||||
PropertyModificationToUndoPropertyModification(new PropertyModification {target = transform, propertyPath = kLocalEulerHint + ".x", value = euler.x.ToString() }),
|
||||
PropertyModificationToUndoPropertyModification(new PropertyModification {target = transform, propertyPath = kLocalEulerHint + ".y", value = euler.y.ToString() }),
|
||||
PropertyModificationToUndoPropertyModification(new PropertyModification {target = transform, propertyPath = kLocalEulerHint + ".z", value = euler.z.ToString() })
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9805855c8e379ed4cad77f639aaddb73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,119 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Timeline;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace UnityEditor.Timeline
|
||||
{
|
||||
// Handles Undo animated properties on PlayableAssets from clips to create parameter animation
|
||||
|
||||
static partial class TimelineRecording
|
||||
{
|
||||
internal static bool HasAnyPlayableAssetModifications(UndoPropertyModification[] modifications)
|
||||
{
|
||||
return modifications.Any(x => GetTarget(x) as IPlayableAsset != null);
|
||||
}
|
||||
|
||||
internal static UndoPropertyModification[] ProcessPlayableAssetModification(UndoPropertyModification[] modifications, WindowState state, bool allowAdd)
|
||||
{
|
||||
// can't record without a director since the asset being modified might be a scene instance
|
||||
if (state == null || state.editSequence.director == null)
|
||||
return modifications;
|
||||
|
||||
var remaining = new List<UndoPropertyModification>();
|
||||
foreach (UndoPropertyModification mod in modifications)
|
||||
{
|
||||
if (!ProcessPlayableAssetModification(mod, state, allowAdd))
|
||||
remaining.Add(mod);
|
||||
}
|
||||
|
||||
if (remaining.Count != modifications.Length)
|
||||
{
|
||||
TimelineEditor.Refresh(RefreshReason.ContentsModified);
|
||||
}
|
||||
|
||||
return remaining.ToArray();
|
||||
}
|
||||
|
||||
static bool ProcessPlayableAssetModification(UndoPropertyModification mod, WindowState state, bool allowAdd)
|
||||
{
|
||||
var target = GetTarget(mod) as IPlayableAsset;
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
var curvesOwner = AnimatedParameterUtility.ToCurvesOwner(target, state.editSequence.asset);
|
||||
if (curvesOwner == null || !curvesOwner.HasAnyAnimatableParameters())
|
||||
return false;
|
||||
|
||||
return ProcessPlayableAssetRecording(mod, state, curvesOwner, allowAdd);
|
||||
}
|
||||
|
||||
internal static TimelineClip FindClipWithAsset(TimelineAsset asset, IPlayableAsset target)
|
||||
{
|
||||
if (target == null || asset == null)
|
||||
return null;
|
||||
|
||||
var clips = asset.flattenedTracks.SelectMany(x => x.clips);
|
||||
return clips.FirstOrDefault(x => x != null && x.asset != null && target == x.asset as IPlayableAsset);
|
||||
}
|
||||
|
||||
static bool ProcessPlayableAssetRecording(UndoPropertyModification mod, WindowState state, ICurvesOwner curvesOwner, bool allowAdd)
|
||||
{
|
||||
if (mod.currentValue == null)
|
||||
return false;
|
||||
|
||||
if (!curvesOwner.IsParameterAnimatable(mod.currentValue.propertyPath))
|
||||
return false;
|
||||
|
||||
// only animate items with existing curves
|
||||
if (!allowAdd && !curvesOwner.IsParameterAnimated(mod.currentValue.propertyPath))
|
||||
return false;
|
||||
|
||||
var localTime = state.editSequence.time;
|
||||
var timelineClip = curvesOwner as TimelineClip;
|
||||
if (timelineClip != null)
|
||||
{
|
||||
// don't use time global to local since it will possibly loop.
|
||||
localTime = timelineClip.ToLocalTimeUnbound(state.editSequence.time);
|
||||
}
|
||||
|
||||
if (localTime < 0)
|
||||
return false;
|
||||
|
||||
// grab the value from the current modification
|
||||
float fValue;
|
||||
if (!ExpressionEvaluator.Evaluate(mod.currentValue.value, out fValue))
|
||||
{
|
||||
// case 916913 -- 'Add Key' menu item will passes 'True' or 'False' (instead of 1, 0)
|
||||
// so we need a special case to parse the boolean string
|
||||
bool bValue;
|
||||
if (!bool.TryParse(mod.currentValue.value, out bValue))
|
||||
{
|
||||
Debug.Assert(false, "Invalid type in PlayableAsset recording");
|
||||
return false;
|
||||
}
|
||||
|
||||
fValue = bValue ? 1 : 0;
|
||||
}
|
||||
|
||||
var added = curvesOwner.AddAnimatedParameterValueAt(mod.currentValue.propertyPath, fValue, (float)localTime);
|
||||
if (added && AnimationMode.InAnimationMode())
|
||||
{
|
||||
EditorCurveBinding binding = curvesOwner.GetCurveBinding(mod.previousValue.propertyPath);
|
||||
AnimationMode.AddPropertyModification(binding, mod.previousValue, true);
|
||||
curvesOwner.targetTrack.SetShowInlineCurves(true);
|
||||
if (state.GetWindow() != null && state.GetWindow().treeView != null)
|
||||
state.GetWindow().treeView.CalculateRowRects();
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
static bool IsPlayableAssetProperty(SerializedProperty property)
|
||||
{
|
||||
return property.serializedObject.targetObject is IPlayableAsset;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7341c0cd0aad4994e8fa461cb443aa7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,191 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace UnityEditor.Timeline
|
||||
{
|
||||
static class TrackAssetRecordingExtensions
|
||||
{
|
||||
static readonly Dictionary<TrackAsset, AnimationClip> s_ActiveClips = new Dictionary<TrackAsset, AnimationClip>();
|
||||
|
||||
internal static void OnRecordingArmed(this TrackAsset track, PlayableDirector director)
|
||||
{
|
||||
if (track == null)
|
||||
return;
|
||||
|
||||
var animClip = track.FindRecordingAnimationClipAtTime(director.time);
|
||||
if (animClip == null)
|
||||
return;
|
||||
|
||||
s_ActiveClips[track] = animClip;
|
||||
}
|
||||
|
||||
internal static void OnRecordingTimeChanged(this TrackAsset track, PlayableDirector director)
|
||||
{
|
||||
if (track == null)
|
||||
return;
|
||||
|
||||
var animClip = track.FindRecordingAnimationClipAtTime(director.time);
|
||||
AnimationClip prevClip = track.GetActiveRecordingAnimationClip();
|
||||
if (prevClip != animClip)
|
||||
{
|
||||
s_ActiveClips[track] = animClip;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void OnRecordingUnarmed(this TrackAsset track, PlayableDirector director)
|
||||
{
|
||||
s_ActiveClips.Remove(track);
|
||||
}
|
||||
|
||||
internal static bool CanRecordAtTime(this TrackAsset track, double time)
|
||||
{
|
||||
// Animation Track
|
||||
var animTrack = track as AnimationTrack;
|
||||
if (animTrack != null)
|
||||
{
|
||||
if (!animTrack.inClipMode)
|
||||
return true;
|
||||
|
||||
TimelineClip clip = null;
|
||||
return FindRecordingClipAtTime(track, time, out clip);
|
||||
}
|
||||
|
||||
// Custom track
|
||||
return track.clips.Any(x => x.start < time + TimeUtility.kTimeEpsilon && x.HasAnyAnimatableParameters());
|
||||
}
|
||||
|
||||
internal static AnimationClip GetActiveRecordingAnimationClip(this TrackAsset track)
|
||||
{
|
||||
AnimationClip clip = null;
|
||||
s_ActiveClips.TryGetValue(track, out clip);
|
||||
return clip;
|
||||
}
|
||||
|
||||
internal static bool IsRecordingToClip(this TrackAsset track, TimelineClip clip)
|
||||
{
|
||||
if (track == null || clip == null)
|
||||
return false;
|
||||
var animClip = track.GetActiveRecordingAnimationClip();
|
||||
if (animClip == null)
|
||||
return false;
|
||||
if (animClip == clip.curves)
|
||||
return true;
|
||||
|
||||
var animAsset = clip.asset as AnimationPlayableAsset;
|
||||
return animAsset != null && animClip == animAsset.clip;
|
||||
}
|
||||
|
||||
// Finds the clip at the given time that recording should use
|
||||
// returns whether recording at this particular point is valid
|
||||
// The target clip will be returned, even if recording at that time is invalid
|
||||
// in case of recording in a blend OR recording to a non-timeline clip
|
||||
internal static bool FindRecordingClipAtTime(this TrackAsset track, double time, out TimelineClip target)
|
||||
{
|
||||
target = null;
|
||||
if (track == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var discreteTime = new DiscreteTime(time);
|
||||
|
||||
// only animation tracks require the recordable flag as they are recording
|
||||
// to an animation clip
|
||||
bool requiresRecordable = (track as AnimationTrack) != null;
|
||||
if (requiresRecordable)
|
||||
{
|
||||
track.SortClips();
|
||||
var sortedByStartTime = track.clips;
|
||||
int i = 0;
|
||||
for (i = 0; i < sortedByStartTime.Length; i++)
|
||||
{
|
||||
var clip = sortedByStartTime[i];
|
||||
if (new DiscreteTime(clip.start) <= discreteTime && new DiscreteTime(clip.end) > discreteTime)
|
||||
{
|
||||
target = clip;
|
||||
// not recordable
|
||||
if (!clip.recordable)
|
||||
return false;
|
||||
|
||||
// in a blend
|
||||
if (!Mathf.Approximately(1.0f, clip.EvaluateMixIn(time) * clip.EvaluateMixOut(time)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (new DiscreteTime(clip.start) > discreteTime)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Recordable playable assets -- takes the last clip that matches
|
||||
track.SortClips();
|
||||
for (int i = 0; i < track.clips.Length; i++)
|
||||
{
|
||||
var clip = track.clips[i];
|
||||
if (clip.start <= time && clip.end >= time && clip.HasAnyAnimatableParameters())
|
||||
target = clip;
|
||||
|
||||
if (clip.start > time)
|
||||
break;
|
||||
}
|
||||
|
||||
return target != null;
|
||||
}
|
||||
|
||||
// Given a track, return the animation clip
|
||||
internal static AnimationClip FindRecordingAnimationClipAtTime(this TrackAsset trackAsset, double time)
|
||||
{
|
||||
if (trackAsset == null)
|
||||
return null;
|
||||
|
||||
AnimationTrack animTrack = trackAsset as AnimationTrack;
|
||||
if (animTrack != null && !animTrack.inClipMode)
|
||||
{
|
||||
return animTrack.infiniteClip;
|
||||
}
|
||||
|
||||
TimelineClip displayBackground;
|
||||
trackAsset.FindRecordingClipAtTime(time, out displayBackground);
|
||||
if (displayBackground != null)
|
||||
{
|
||||
if (displayBackground.recordable)
|
||||
{
|
||||
AnimationPlayableAsset asset = displayBackground.asset as AnimationPlayableAsset;
|
||||
if (asset != null)
|
||||
return asset.clip;
|
||||
}
|
||||
else if (animTrack == null)
|
||||
{
|
||||
if (displayBackground.curves == null)
|
||||
displayBackground.CreateCurves(AnimationTrackRecorder.GetUniqueRecordedClipName(displayBackground.GetParentTrack(), TimelineClip.kDefaultCurvesName));
|
||||
|
||||
return displayBackground.curves;
|
||||
}
|
||||
}
|
||||
else if (trackAsset.HasAnyAnimatableParameters())
|
||||
{
|
||||
if (trackAsset.curves == null)
|
||||
trackAsset.CreateCurves(AnimationTrackRecorder.GetUniqueRecordedClipName(trackAsset.timelineAsset, TrackAsset.kDefaultCurvesName));
|
||||
|
||||
return trackAsset.curves;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static void ClearRecordingState()
|
||||
{
|
||||
s_ActiveClips.Clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 49f1d2c7420db4444b011955726d0046
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Add table
Add a link
Reference in a new issue