Maze / Library /PackageCache /com.unity.timeline@1.6.5 /Editor /inspectors /AnimationTrackInspector.cs
| //#define PERF_PROFILE | |
| using System; | |
| using System.ComponentModel; | |
| using System.Linq; | |
| using UnityEngine; | |
| using UnityEngine.Timeline; | |
| using UnityEngine.Playables; | |
| namespace UnityEditor.Timeline | |
| { | |
| [] | |
| class AnimationTrackInspector : TrackAssetInspector | |
| { | |
| static class Styles | |
| { | |
| public static GUIContent MatchTargetFieldsTitle = L10n.TextContent("Default Offset Match Fields", "Fields to apply when matching offsets on clips. These are the defaults, and can be overridden for each clip."); | |
| public static readonly GUIContent PositionIcon = EditorGUIUtility.IconContent("MoveTool"); | |
| public static readonly GUIContent RotationIcon = EditorGUIUtility.IconContent("RotateTool"); | |
| public static GUIContent XTitle = EditorGUIUtility.TextContent("X"); | |
| public static GUIContent YTitle = EditorGUIUtility.TextContent("Y"); | |
| public static GUIContent ZTitle = EditorGUIUtility.TextContent("Z"); | |
| public static GUIContent PositionTitle = L10n.TextContent("Position"); | |
| public static GUIContent RotationTitle = L10n.TextContent("Rotation"); | |
| public static readonly GUIContent OffsetModeTitle = L10n.TextContent("Track Offsets"); | |
| public static readonly string TransformOffsetInfo = L10n.Tr("Transform offsets are applied to the entire track. Use this mode to play the animation track at a fixed position and rotation."); | |
| public static readonly string SceneOffsetInfo = L10n.Tr("Scene offsets will use the existing transform as initial offsets. Use this to play the track from the gameObjects current position and rotation."); | |
| public static readonly string AutoOffsetInfo = L10n.Tr("Auto will apply scene offsets if there is a controller attached to the animator and transform offsets otherwise."); | |
| public static readonly string AutoOffsetWarning = L10n.Tr("This mode is deprecated may be removed in a future release."); | |
| public static readonly string InheritedFromParent = L10n.Tr("Inherited"); | |
| public static readonly string InheritedToolTip = L10n.Tr("This value is inherited from it's parent track."); | |
| public static readonly string AvatarMaskWarning = L10n.Tr("Applying an Avatar Mask to the base track may not properly mask Root Motion or Humanoid bones from an Animator Controller or other Timeline track."); | |
| public static readonly GUIContent RecordingOffsets = L10n.TextContent("Recorded Offsets", "Offsets applied to recorded position and rotation keys"); | |
| public static readonly GUIContent RecordingIkApplied = L10n.TextContent("Apply Foot IK", "Applies Foot IK to recorded Animation."); | |
| public static readonly GUIContent[] OffsetContents; | |
| public static readonly GUIContent[] OffsetInheritContents; | |
| static Styles() | |
| { | |
| var values = Enum.GetValues(typeof(TrackOffset)); | |
| OffsetContents = new GUIContent[values.Length]; | |
| OffsetInheritContents = new GUIContent[values.Length]; | |
| for (var index = 0; index < values.Length; index++) | |
| { | |
| var offset = (TrackOffset)index; | |
| var name = ObjectNames.NicifyVariableName(L10n.Tr(offset.ToString())); | |
| var memInfo = typeof(TrackOffset).GetMember(offset.ToString()); | |
| var attributes = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false); | |
| if (attributes.Length > 0) | |
| { | |
| name = ((DescriptionAttribute)attributes[0]).Description; | |
| } | |
| OffsetContents[index] = new GUIContent(name); | |
| OffsetInheritContents[index] = new GUIContent(string.Format("{0} ({1})", InheritedFromParent, name)); | |
| } | |
| } | |
| } | |
| TimelineAnimationUtilities.OffsetEditMode m_OffsetEditMode = TimelineAnimationUtilities.OffsetEditMode.None; | |
| SerializedProperty m_MatchFieldsProperty; | |
| SerializedProperty m_TrackPositionProperty; | |
| SerializedProperty m_TrackRotationProperty; | |
| SerializedProperty m_AvatarMaskProperty; | |
| SerializedProperty m_ApplyAvatarMaskProperty; | |
| SerializedProperty m_TrackOffsetProperty; | |
| SerializedProperty m_RecordedOffsetPositionProperty; | |
| SerializedProperty m_RecordedOffsetEulerProperty; | |
| SerializedProperty m_RecordedApplyFootIK; | |
| Vector3 m_lastPosition; | |
| Vector3 m_lastRotation; | |
| GUIContent m_TempContent = new GUIContent(); | |
| void Evaluate() | |
| { | |
| if (timelineWindow.state != null && timelineWindow.state.editSequence.director != null) | |
| { | |
| // force the update immediately, the deferred doesn't always work with the inspector | |
| timelineWindow.state.editSequence.director.Evaluate(); | |
| } | |
| } | |
| void RebuildGraph() | |
| { | |
| TimelineEditor.Refresh(RefreshReason.ContentsModified); | |
| } | |
| public override void OnInspectorGUI() | |
| { | |
| using (new EditorGUI.DisabledScope(IsTrackLocked())) | |
| { | |
| serializedObject.Update(); | |
| DrawRootTransformOffset(); | |
| EditorGUI.BeginChangeCheck(); | |
| DrawRecordedProperties(); | |
| DrawAvatarProperties(); | |
| if (EditorGUI.EndChangeCheck()) | |
| RebuildGraph(); | |
| DrawMatchFieldsGUI(); | |
| serializedObject.ApplyModifiedProperties(); | |
| } | |
| } | |
| bool AnimatesRootTransform() | |
| { | |
| return targets.OfType<AnimationTrack>().All(t => t.AnimatesRootTransform()); | |
| } | |
| bool ShouldDrawOffsets() | |
| { | |
| bool hasMultiple; | |
| var offsetMode = GetOffsetMode(out hasMultiple); | |
| if (hasMultiple) | |
| return false; | |
| if (offsetMode == TrackOffset.ApplySceneOffsets) | |
| return false; | |
| if (offsetMode == TrackOffset.ApplyTransformOffsets) | |
| return true; | |
| // Auto mode. | |
| PlayableDirector director = this.m_Context as PlayableDirector; | |
| if (director == null) | |
| return false; | |
| // If any bound animators have controllers don't show | |
| foreach (var track in targets.OfType<AnimationTrack>()) | |
| { | |
| var animator = track.GetBinding(director); | |
| if (animator != null && animator.runtimeAnimatorController != null) | |
| return false; | |
| } | |
| return true; | |
| } | |
| void DrawRootTransformOffset() | |
| { | |
| if (!AnimatesRootTransform()) | |
| return; | |
| bool showWarning = SetupOffsetTooltip(); | |
| DrawRootTransformDropDown(); | |
| if (ShouldDrawOffsets()) | |
| { | |
| EditorGUI.indentLevel++; | |
| DrawRootMotionToolBar(); | |
| DrawRootMotionOffsetFields(); | |
| EditorGUI.indentLevel--; | |
| } | |
| if (showWarning) | |
| { | |
| EditorGUI.indentLevel++; | |
| EditorGUILayout.HelpBox(Styles.AutoOffsetWarning, MessageType.Warning, true); | |
| EditorGUI.indentLevel--; | |
| } | |
| } | |
| bool SetupOffsetTooltip() | |
| { | |
| Styles.OffsetModeTitle.tooltip = string.Empty; | |
| bool hasMultiple; | |
| var offsetMode = GetOffsetMode(out hasMultiple); | |
| bool showWarning = false; | |
| if (!hasMultiple) | |
| { | |
| if (offsetMode == TrackOffset.ApplyTransformOffsets) | |
| Styles.OffsetModeTitle.tooltip = Styles.TransformOffsetInfo; | |
| else if (offsetMode == TrackOffset.ApplySceneOffsets) | |
| Styles.OffsetModeTitle.tooltip = Styles.SceneOffsetInfo; | |
| else if (offsetMode == TrackOffset.Auto) | |
| { | |
| Styles.OffsetModeTitle.tooltip = Styles.AutoOffsetInfo; | |
| showWarning = true; | |
| } | |
| } | |
| return showWarning; | |
| } | |
| void DrawRootTransformDropDown() | |
| { | |
| bool anySubTracks = targets.OfType<AnimationTrack>().Any(t => t.isSubTrack); | |
| bool allSubTracks = targets.OfType<AnimationTrack>().All(t => t.isSubTrack); | |
| bool mixed; | |
| var rootOffsetMode = GetOffsetMode(out mixed); | |
| // if we are showing subtracks, we need to show the current mode from the parent | |
| // BUT keep it disabled | |
| if (anySubTracks) | |
| { | |
| m_TempContent.tooltip = string.Empty; | |
| if (mixed) | |
| m_TempContent.text = EditorGUI.mixedValueContent.text; | |
| else if (!allSubTracks) | |
| m_TempContent.text = Styles.OffsetContents[(int)rootOffsetMode].text; | |
| else | |
| { | |
| m_TempContent.text = Styles.OffsetInheritContents[(int)rootOffsetMode].text; | |
| m_TempContent.tooltip = Styles.InheritedToolTip; | |
| } | |
| using (new EditorGUI.DisabledScope(true)) | |
| EditorGUILayout.LabelField(Styles.OffsetModeTitle, m_TempContent, EditorStyles.popup); | |
| } | |
| else | |
| { | |
| // We use an enum popup explicitly because it will handle the description attribute on the enum | |
| using (new GUIMixedValueScope(mixed)) | |
| { | |
| var rect = EditorGUILayout.GetControlRect(true, EditorGUI.kSingleLineHeight); | |
| EditorGUI.BeginProperty(rect, Styles.OffsetModeTitle, m_TrackOffsetProperty); | |
| EditorGUI.BeginChangeCheck(); | |
| var result = (TrackOffset)EditorGUI.EnumPopup(rect, Styles.OffsetModeTitle, (TrackOffset)m_TrackOffsetProperty.intValue); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| m_TrackOffsetProperty.enumValueIndex = (int)result; | |
| // this property changes the recordable state of the objects, so auto disable recording | |
| if (TimelineWindow.instance != null) | |
| { | |
| if (TimelineWindow.instance.state != null) | |
| TimelineWindow.instance.state.recording = false; | |
| RebuildGraph(); | |
| } | |
| } | |
| EditorGUI.EndProperty(); | |
| } | |
| } | |
| } | |
| void DrawMatchFieldsGUI() | |
| { | |
| if (!AnimatesRootTransform()) | |
| return; | |
| m_MatchFieldsProperty.isExpanded = EditorGUILayout.Foldout(m_MatchFieldsProperty.isExpanded, Styles.MatchTargetFieldsTitle, true); | |
| if (m_MatchFieldsProperty.isExpanded) | |
| { | |
| EditorGUI.indentLevel++; | |
| MatchTargetsFieldGUI(m_MatchFieldsProperty); | |
| EditorGUI.indentLevel--; | |
| } | |
| } | |
| void DrawRootMotionOffsetFields() | |
| { | |
| EditorGUI.BeginChangeCheck(); | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.PropertyField(m_TrackPositionProperty); | |
| EditorGUILayout.EndHorizontal(); | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.PropertyField(m_TrackRotationProperty, Styles.RotationTitle); | |
| EditorGUILayout.EndHorizontal(); | |
| EditorGUILayout.Space(); | |
| EditorGUILayout.Space(); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| UpdateOffsets(); | |
| } | |
| } | |
| void DrawRootMotionToolBar() | |
| { | |
| bool disable = targets.Length > 1; | |
| bool changed = false; | |
| if (!disable) | |
| { | |
| // detects external changes | |
| changed |= m_lastPosition != m_TrackPositionProperty.vector3Value || m_lastRotation != m_TrackRotationProperty.vector3Value; | |
| m_lastPosition = m_TrackPositionProperty.vector3Value; | |
| m_lastRotation = m_TrackRotationProperty.vector3Value; | |
| SceneView.RepaintAll(); | |
| } | |
| EditorGUI.BeginChangeCheck(); | |
| using (new EditorGUI.DisabledScope(disable)) | |
| ShowMotionOffsetEditModeToolbar(ref m_OffsetEditMode); | |
| changed |= EditorGUI.EndChangeCheck(); | |
| if (changed) | |
| { | |
| UpdateOffsets(); | |
| } | |
| } | |
| void UpdateOffsets() | |
| { | |
| foreach (var track in targets.OfType<AnimationTrack>()) | |
| track.UpdateClipOffsets(); | |
| Evaluate(); | |
| } | |
| void DrawAvatarProperties() | |
| { | |
| EditorGUILayout.PropertyField(m_ApplyAvatarMaskProperty); | |
| if (m_ApplyAvatarMaskProperty.hasMultipleDifferentValues || m_ApplyAvatarMaskProperty.boolValue) | |
| { | |
| EditorGUI.indentLevel++; | |
| EditorGUILayout.PropertyField(m_AvatarMaskProperty); | |
| EditorGUI.indentLevel--; | |
| } | |
| if (targets.OfType<AnimationTrack>().Any(x => !x.isSubTrack)) | |
| EditorGUILayout.HelpBox(Styles.AvatarMaskWarning, MessageType.Warning); | |
| EditorGUILayout.Space(); | |
| } | |
| public static void ShowMotionOffsetEditModeToolbar(ref TimelineAnimationUtilities.OffsetEditMode motionOffset) | |
| { | |
| GUILayout.BeginHorizontal(); | |
| GUILayout.FlexibleSpace(); | |
| GUILayout.FlexibleSpace(); | |
| int newMotionOffsetMode = GUILayout.Toolbar((int)motionOffset, new[] { Styles.PositionIcon, Styles.RotationIcon }); | |
| if (GUI.changed) | |
| { | |
| if ((int)motionOffset == newMotionOffsetMode) //untoggle the button | |
| motionOffset = TimelineAnimationUtilities.OffsetEditMode.None; | |
| else | |
| motionOffset = (TimelineAnimationUtilities.OffsetEditMode)newMotionOffsetMode; | |
| } | |
| GUILayout.FlexibleSpace(); | |
| GUILayout.EndHorizontal(); | |
| GUILayout.Space(3); | |
| } | |
| public override void OnEnable() | |
| { | |
| base.OnEnable(); | |
| SceneView.duringSceneGui += OnSceneGUI; | |
| m_MatchFieldsProperty = serializedObject.FindProperty("m_MatchTargetFields"); | |
| m_TrackPositionProperty = serializedObject.FindProperty("m_Position"); | |
| m_TrackRotationProperty = serializedObject.FindProperty("m_EulerAngles"); | |
| m_TrackOffsetProperty = serializedObject.FindProperty("m_TrackOffset"); | |
| m_AvatarMaskProperty = serializedObject.FindProperty("m_AvatarMask"); | |
| m_ApplyAvatarMaskProperty = serializedObject.FindProperty("m_ApplyAvatarMask"); | |
| m_RecordedOffsetPositionProperty = serializedObject.FindProperty("m_InfiniteClipOffsetPosition"); | |
| m_RecordedOffsetEulerProperty = serializedObject.FindProperty("m_InfiniteClipOffsetEulerAngles"); | |
| m_RecordedApplyFootIK = serializedObject.FindProperty("m_InfiniteClipApplyFootIK"); | |
| m_lastPosition = m_TrackPositionProperty.vector3Value; | |
| m_lastRotation = m_TrackRotationProperty.vector3Value; | |
| } | |
| public void OnDestroy() | |
| { | |
| SceneView.duringSceneGui -= OnSceneGUI; | |
| } | |
| void OnSceneGUI(SceneView sceneView) | |
| { | |
| DoOffsetManipulator(); | |
| } | |
| void DoOffsetManipulator() | |
| { | |
| if (targets.Length > 1) //do not edit the track offset on a multiple selection | |
| return; | |
| if (timelineWindow == null || timelineWindow.state == null || timelineWindow.state.editSequence.director == null) | |
| return; | |
| AnimationTrack animationTrack = target as AnimationTrack; | |
| if (animationTrack != null && (animationTrack.trackOffset == TrackOffset.ApplyTransformOffsets) && m_OffsetEditMode != TimelineAnimationUtilities.OffsetEditMode.None) | |
| { | |
| var boundObject = TimelineUtility.GetSceneGameObject(timelineWindow.state.editSequence.director, animationTrack); | |
| var boundObjectTransform = boundObject != null ? boundObject.transform : null; | |
| var offsets = TimelineAnimationUtilities.GetTrackOffsets(animationTrack, boundObjectTransform); | |
| EditorGUI.BeginChangeCheck(); | |
| switch (m_OffsetEditMode) | |
| { | |
| case TimelineAnimationUtilities.OffsetEditMode.Translation: | |
| offsets.position = Handles.PositionHandle(offsets.position, (Tools.pivotRotation == PivotRotation.Global) | |
| ? Quaternion.identity | |
| : offsets.rotation); | |
| break; | |
| case TimelineAnimationUtilities.OffsetEditMode.Rotation: | |
| offsets.rotation = Handles.RotationHandle(offsets.rotation, offsets.position); | |
| break; | |
| } | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| UndoExtensions.RegisterTrack(animationTrack, L10n.Tr("Inspector")); | |
| TimelineAnimationUtilities.UpdateTrackOffset(animationTrack, boundObjectTransform, offsets); | |
| Evaluate(); | |
| Repaint(); | |
| } | |
| } | |
| } | |
| public void DrawRecordedProperties() | |
| { | |
| // only show if this applies to all targets | |
| foreach (var track in targets) | |
| { | |
| var animationTrack = track as AnimationTrack; | |
| if (animationTrack == null || animationTrack.inClipMode || animationTrack.infiniteClip == null || animationTrack.infiniteClip.empty) | |
| return; | |
| } | |
| GUILayout.Label(Styles.RecordingOffsets); | |
| EditorGUI.indentLevel++; | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.PropertyField(m_RecordedOffsetPositionProperty, Styles.PositionTitle); | |
| EditorGUILayout.EndHorizontal(); | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.PropertyField(m_RecordedOffsetEulerProperty, Styles.RotationTitle); | |
| EditorGUILayout.EndHorizontal(); | |
| EditorGUI.indentLevel--; | |
| EditorGUILayout.Space(); | |
| EditorGUILayout.PropertyField(m_RecordedApplyFootIK, Styles.RecordingIkApplied); | |
| EditorGUILayout.Space(); | |
| } | |
| public static void MatchTargetsFieldGUI(SerializedProperty property) | |
| { | |
| const float ToggleWidth = 20; | |
| int value = 0; | |
| MatchTargetFields enumValue = (MatchTargetFields)property.intValue; | |
| EditorGUI.BeginChangeCheck(); | |
| Rect rect = EditorGUILayout.GetControlRect(false, kLineHeight * 2); | |
| Rect itemRect = new Rect(rect.x, rect.y, rect.width, kLineHeight); | |
| EditorGUI.BeginProperty(rect, Styles.MatchTargetFieldsTitle, property); | |
| float minWidth = 0, maxWidth = 0; | |
| EditorStyles.label.CalcMinMaxWidth(Styles.XTitle, out minWidth, out maxWidth); | |
| float width = minWidth + ToggleWidth; | |
| GUILayout.BeginHorizontal(); | |
| Rect r = EditorGUI.PrefixLabel(itemRect, Styles.PositionTitle); | |
| int oldIndent = EditorGUI.indentLevel; | |
| EditorGUI.indentLevel = 0; | |
| r.width = width; | |
| value |= EditorGUI.ToggleLeft(r, Styles.XTitle, enumValue.HasAny(MatchTargetFields.PositionX)) ? (int)MatchTargetFields.PositionX : 0; | |
| r.x += width; | |
| value |= EditorGUI.ToggleLeft(r, Styles.YTitle, enumValue.HasAny(MatchTargetFields.PositionY)) ? (int)MatchTargetFields.PositionY : 0; | |
| r.x += width; | |
| value |= EditorGUI.ToggleLeft(r, Styles.ZTitle, enumValue.HasAny(MatchTargetFields.PositionZ)) ? (int)MatchTargetFields.PositionZ : 0; | |
| EditorGUI.indentLevel = oldIndent; | |
| GUILayout.EndHorizontal(); | |
| GUILayout.BeginHorizontal(); | |
| itemRect.y += kLineHeight; | |
| r = EditorGUI.PrefixLabel(itemRect, Styles.RotationTitle); | |
| EditorGUI.indentLevel = 0; | |
| r.width = width; | |
| value |= EditorGUI.ToggleLeft(r, Styles.XTitle, enumValue.HasAny(MatchTargetFields.RotationX)) ? (int)MatchTargetFields.RotationX : 0; | |
| r.x += width; | |
| value |= EditorGUI.ToggleLeft(r, Styles.YTitle, enumValue.HasAny(MatchTargetFields.RotationY)) ? (int)MatchTargetFields.RotationY : 0; | |
| r.x += width; | |
| value |= EditorGUI.ToggleLeft(r, Styles.ZTitle, enumValue.HasAny(MatchTargetFields.RotationZ)) ? (int)MatchTargetFields.RotationZ : 0; | |
| EditorGUI.indentLevel = oldIndent; | |
| GUILayout.EndHorizontal(); | |
| EditorGUI.EndProperty(); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| property.intValue = value; | |
| } | |
| } | |
| static TrackOffset GetOffsetMode(AnimationTrack track) | |
| { | |
| if (track.isSubTrack) | |
| { | |
| var parent = track.parent as AnimationTrack; | |
| if (parent != null) // fallback to the current track if there is an error | |
| track = parent; | |
| } | |
| return track.trackOffset; | |
| } | |
| // gets the current mode, | |
| TrackOffset GetOffsetMode(out bool hasMultiple) | |
| { | |
| var rootOffsetMode = GetOffsetMode(target as AnimationTrack); | |
| hasMultiple = targets.OfType<AnimationTrack>().Any(t => GetOffsetMode(t) != rootOffsetMode); | |
| return rootOffsetMode; | |
| } | |
| } | |
| } | |