You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

847 lines
30 KiB

using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.PostProcessing
{
public sealed class CurveEditor
{
#region Enums
enum EditMode
{
None,
Moving,
TangentEdit
}
enum Tangent
{
In,
Out
}
#endregion
#region Structs
public struct Settings
{
public Rect bounds;
public RectOffset padding;
public Color selectionColor;
public float curvePickingDistance;
public float keyTimeClampingDistance;
public static Settings defaultSettings
{
get
{
return new Settings
{
bounds = new Rect(0f, 0f, 1f, 1f),
padding = new RectOffset(10, 10, 10, 10),
selectionColor = Color.yellow,
curvePickingDistance = 6f,
keyTimeClampingDistance = 1e-4f
};
}
}
}
public struct CurveState
{
public bool visible;
public bool editable;
public uint minPointCount;
public float zeroKeyConstantValue;
public Color color;
public float width;
public float handleWidth;
public bool showNonEditableHandles;
public bool onlyShowHandlesOnSelection;
public bool loopInBounds;
public static CurveState defaultState
{
get
{
return new CurveState
{
visible = true,
editable = true,
minPointCount = 2,
zeroKeyConstantValue = 0f,
color = Color.white,
width = 2f,
handleWidth = 2f,
showNonEditableHandles = true,
onlyShowHandlesOnSelection = false,
loopInBounds = false
};
}
}
}
public struct Selection
{
public SerializedProperty curve;
public int keyframeIndex;
public Keyframe? keyframe;
public Selection(SerializedProperty curve, int keyframeIndex, Keyframe? keyframe)
{
this.curve = curve;
this.keyframeIndex = keyframeIndex;
this.keyframe = keyframe;
}
}
internal struct MenuAction
{
internal SerializedProperty curve;
internal int index;
internal Vector3 position;
internal MenuAction(SerializedProperty curve)
{
this.curve = curve;
this.index = -1;
this.position = Vector3.zero;
}
internal MenuAction(SerializedProperty curve, int index)
{
this.curve = curve;
this.index = index;
this.position = Vector3.zero;
}
internal MenuAction(SerializedProperty curve, Vector3 position)
{
this.curve = curve;
this.index = -1;
this.position = position;
}
}
#endregion
#region Fields & properties
public Settings settings { get; private set; }
Dictionary<SerializedProperty, CurveState> m_Curves;
Rect m_CurveArea;
SerializedProperty m_SelectedCurve;
int m_SelectedKeyframeIndex = -1;
EditMode m_EditMode = EditMode.None;
Tangent m_TangentEditMode;
bool m_Dirty;
#endregion
#region Constructors & destructors
public CurveEditor()
: this(Settings.defaultSettings)
{}
public CurveEditor(Settings settings)
{
this.settings = settings;
m_Curves = new Dictionary<SerializedProperty, CurveState>();
}
#endregion
#region Public API
public void Add(params SerializedProperty[] curves)
{
foreach (var curve in curves)
Add(curve, CurveState.defaultState);
}
public void Add(SerializedProperty curve)
{
Add(curve, CurveState.defaultState);
}
public void Add(SerializedProperty curve, CurveState state)
{
// Make sure the property is in fact an AnimationCurve
var animCurve = curve.animationCurveValue;
if (animCurve == null)
throw new ArgumentException("curve");
if (m_Curves.ContainsKey(curve))
Debug.LogWarning("Curve has already been added to the editor");
m_Curves.Add(curve, state);
}
public void Remove(SerializedProperty curve)
{
m_Curves.Remove(curve);
}
public void RemoveAll()
{
m_Curves.Clear();
}
public CurveState GetCurveState(SerializedProperty curve)
{
CurveState state;
if (!m_Curves.TryGetValue(curve, out state))
throw new KeyNotFoundException("curve");
return state;
}
public void SetCurveState(SerializedProperty curve, CurveState state)
{
if (!m_Curves.ContainsKey(curve))
throw new KeyNotFoundException("curve");
m_Curves[curve] = state;
}
public Selection GetSelection()
{
Keyframe? key = null;
if (m_SelectedKeyframeIndex > -1)
{
var curve = m_SelectedCurve.animationCurveValue;
if (m_SelectedKeyframeIndex >= curve.length)
m_SelectedKeyframeIndex = -1;
else
key = curve[m_SelectedKeyframeIndex];
}
return new Selection(m_SelectedCurve, m_SelectedKeyframeIndex, key);
}
public void SetKeyframe(SerializedProperty curve, int keyframeIndex, Keyframe keyframe)
{
var animCurve = curve.animationCurveValue;
SetKeyframe(animCurve, keyframeIndex, keyframe);
SaveCurve(curve, animCurve);
}
public bool OnGUI(Rect rect)
{
if (Event.current.type == EventType.Repaint)
m_Dirty = false;
GUI.BeginClip(rect);
{
var area = new Rect(Vector2.zero, rect.size);
m_CurveArea = settings.padding.Remove(area);
foreach (var curve in m_Curves)
OnCurveGUI(area, curve.Key, curve.Value);
OnGeneralUI(area);
}
GUI.EndClip();
return m_Dirty;
}
#endregion
#region UI & events
void OnCurveGUI(Rect rect, SerializedProperty curve, CurveState state)
{
// Discard invisible curves
if (!state.visible)
return;
var animCurve = curve.animationCurveValue;
var keys = animCurve.keys;
var length = keys.Length;
// Curve drawing
// Slightly dim non-editable curves
var color = state.color;
if (!state.editable)
color.a *= 0.5f;
Handles.color = color;
var bounds = settings.bounds;
if (length == 0)
{
var p1 = CurveToCanvas(new Vector3(bounds.xMin, state.zeroKeyConstantValue));
var p2 = CurveToCanvas(new Vector3(bounds.xMax, state.zeroKeyConstantValue));
Handles.DrawAAPolyLine(state.width, p1, p2);
}
else if (length == 1)
{
var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value));
var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[0].value));
Handles.DrawAAPolyLine(state.width, p1, p2);
}
else
{
var prevKey = keys[0];
for (int k = 1; k < length; k++)
{
var key = keys[k];
var pts = BezierSegment(prevKey, key);
if (float.IsInfinity(prevKey.outTangent) || float.IsInfinity(key.inTangent))
{
var s = HardSegment(prevKey, key);
Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
}
else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
prevKey = key;
}
// Curve extents & loops
if (keys[0].time > bounds.xMin)
{
if (state.loopInBounds)
{
var p1 = keys[length - 1];
p1.time -= settings.bounds.width;
var p2 = keys[0];
var pts = BezierSegment(p1, p2);
if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent))
{
var s = HardSegment(p1, p2);
Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
}
else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
}
else
{
var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value));
var p2 = CurveToCanvas(keys[0]);
Handles.DrawAAPolyLine(state.width, p1, p2);
}
}
if (keys[length - 1].time < bounds.xMax)
{
if (state.loopInBounds)
{
var p1 = keys[length - 1];
var p2 = keys[0];
p2.time += settings.bounds.width;
var pts = BezierSegment(p1, p2);
if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent))
{
var s = HardSegment(p1, p2);
Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
}
else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
}
else
{
var p1 = CurveToCanvas(keys[length - 1]);
var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[length - 1].value));
Handles.DrawAAPolyLine(state.width, p1, p2);
}
}
}
// Make sure selection is correct (undo can break it)
bool isCurrentlySelectedCurve = curve == m_SelectedCurve;
if (isCurrentlySelectedCurve && m_SelectedKeyframeIndex >= length)
m_SelectedKeyframeIndex = -1;
// Handles & keys
for (int k = 0; k < length; k++)
{
bool isCurrentlySelectedKeyframe = k == m_SelectedKeyframeIndex;
var e = Event.current;
var pos = CurveToCanvas(keys[k]);
var hitRect = new Rect(pos.x - 8f, pos.y - 8f, 16f, 16f);
var offset = isCurrentlySelectedCurve
? new RectOffset(5, 5, 5, 5)
: new RectOffset(6, 6, 6, 6);
var outTangent = pos + CurveTangentToCanvas(keys[k].outTangent).normalized * 40f;
var inTangent = pos - CurveTangentToCanvas(keys[k].inTangent).normalized * 40f;
var inTangentHitRect = new Rect(inTangent.x - 7f, inTangent.y - 7f, 14f, 14f);
var outTangentHitrect = new Rect(outTangent.x - 7f, outTangent.y - 7f, 14f, 14f);
// Draw
if (state.showNonEditableHandles)
{
if (e.type == EventType.Repaint)
{
var selectedColor = (isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
? settings.selectionColor
: state.color;
// Keyframe
EditorGUI.DrawRect(offset.Remove(hitRect), selectedColor);
// Tangents
if (isCurrentlySelectedCurve && (!state.onlyShowHandlesOnSelection || (state.onlyShowHandlesOnSelection && isCurrentlySelectedKeyframe)))
{
Handles.color = selectedColor;
if (k > 0 || state.loopInBounds)
{
Handles.DrawAAPolyLine(state.handleWidth, pos, inTangent);
EditorGUI.DrawRect(offset.Remove(inTangentHitRect), selectedColor);
}
if (k < length - 1 || state.loopInBounds)
{
Handles.DrawAAPolyLine(state.handleWidth, pos, outTangent);
EditorGUI.DrawRect(offset.Remove(outTangentHitrect), selectedColor);
}
}
}
}
// Events
if (state.editable)
{
// Keyframe move
if (m_EditMode == EditMode.Moving && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
{
EditMoveKeyframe(animCurve, keys, k);
}
// Tangent editing
if (m_EditMode == EditMode.TangentEdit && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
{
bool alreadyBroken = !(Mathf.Approximately(keys[k].inTangent, keys[k].outTangent) || (float.IsInfinity(keys[k].inTangent) && float.IsInfinity(keys[k].outTangent)));
EditMoveTangent(animCurve, keys, k, m_TangentEditMode, e.shift || !(alreadyBroken || e.control));
}
// Keyframe selection & context menu
if (e.type == EventType.MouseDown && rect.Contains(e.mousePosition))
{
if (hitRect.Contains(e.mousePosition))
{
if (e.button == 0)
{
SelectKeyframe(curve, k);
m_EditMode = EditMode.Moving;
e.Use();
}
else if (e.button == 1)
{
// Keyframe context menu
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Delete Key"), false, (x) =>
{
var action = (MenuAction)x;
var curveValue = action.curve.animationCurveValue;
action.curve.serializedObject.Update();
RemoveKeyframe(curveValue, action.index);
m_SelectedKeyframeIndex = -1;
SaveCurve(action.curve, curveValue);
action.curve.serializedObject.ApplyModifiedProperties();
}, new MenuAction(curve, k));
menu.ShowAsContext();
e.Use();
}
}
}
// Tangent selection & edit mode
if (e.type == EventType.MouseDown && rect.Contains(e.mousePosition))
{
if (inTangentHitRect.Contains(e.mousePosition) && (k > 0 || state.loopInBounds))
{
SelectKeyframe(curve, k);
m_EditMode = EditMode.TangentEdit;
m_TangentEditMode = Tangent.In;
e.Use();
}
else if (outTangentHitrect.Contains(e.mousePosition) && (k < length - 1 || state.loopInBounds))
{
SelectKeyframe(curve, k);
m_EditMode = EditMode.TangentEdit;
m_TangentEditMode = Tangent.Out;
e.Use();
}
}
// Mouse up - clean up states
if (e.rawType == EventType.MouseUp && m_EditMode != EditMode.None)
{
m_EditMode = EditMode.None;
}
// Set cursors
{
EditorGUIUtility.AddCursorRect(hitRect, MouseCursor.MoveArrow);
if (k > 0 || state.loopInBounds)
EditorGUIUtility.AddCursorRect(inTangentHitRect, MouseCursor.RotateArrow);
if (k < length - 1 || state.loopInBounds)
EditorGUIUtility.AddCursorRect(outTangentHitrect, MouseCursor.RotateArrow);
}
}
}
Handles.color = Color.white;
SaveCurve(curve, animCurve);
}
void OnGeneralUI(Rect rect)
{
var e = Event.current;
// Selection
if (e.type == EventType.MouseDown)
{
GUI.FocusControl(null);
m_SelectedCurve = null;
m_SelectedKeyframeIndex = -1;
bool used = false;
var hit = CanvasToCurve(e.mousePosition);
float curvePickValue = CurveToCanvas(hit).y;
// Try and select a curve
foreach (var curve in m_Curves)
{
if (!curve.Value.editable || !curve.Value.visible)
continue;
var prop = curve.Key;
var state = curve.Value;
var animCurve = prop.animationCurveValue;
float hitY = animCurve.length == 0
? state.zeroKeyConstantValue
: animCurve.Evaluate(hit.x);
var curvePos = CurveToCanvas(new Vector3(hit.x, hitY));
if (Mathf.Abs(curvePos.y - curvePickValue) < settings.curvePickingDistance)
{
m_SelectedCurve = prop;
if (e.clickCount == 2 && e.button == 0)
{
// Create a keyframe on double-click on this curve
EditCreateKeyframe(animCurve, hit, true, state.zeroKeyConstantValue);
SaveCurve(prop, animCurve);
}
else if (e.button == 1)
{
// Curve context menu
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Add Key"), false, (x) =>
{
var action = (MenuAction)x;
var curveValue = action.curve.animationCurveValue;
action.curve.serializedObject.Update();
EditCreateKeyframe(curveValue, hit, true, 0f);
SaveCurve(action.curve, curveValue);
action.curve.serializedObject.ApplyModifiedProperties();
}, new MenuAction(prop, hit));
menu.ShowAsContext();
e.Use();
used = true;
}
}
}
if (e.clickCount == 2 && e.button == 0 && m_SelectedCurve == null)
{
// Create a keyframe on every curve on double-click
foreach (var curve in m_Curves)
{
if (!curve.Value.editable || !curve.Value.visible)
continue;
var prop = curve.Key;
var state = curve.Value;
var animCurve = prop.animationCurveValue;
EditCreateKeyframe(animCurve, hit, e.alt, state.zeroKeyConstantValue);
SaveCurve(prop, animCurve);
}
}
else if (!used && e.button == 1)
{
// Global context menu
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Add Key At Position"), false, () => ContextMenuAddKey(hit, false));
menu.AddItem(new GUIContent("Add Key On Curves"), false, () => ContextMenuAddKey(hit, true));
menu.ShowAsContext();
}
e.Use();
}
// Delete selected key(s)
if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Delete || e.keyCode == KeyCode.Backspace))
{
if (m_SelectedKeyframeIndex != -1 && m_SelectedCurve != null)
{
var animCurve = m_SelectedCurve.animationCurveValue;
var length = animCurve.length;
if (m_Curves[m_SelectedCurve].minPointCount < length && length >= 0)
{
EditDeleteKeyframe(animCurve, m_SelectedKeyframeIndex);
m_SelectedKeyframeIndex = -1;
SaveCurve(m_SelectedCurve, animCurve);
}
e.Use();
}
}
}
void SaveCurve(SerializedProperty prop, AnimationCurve curve)
{
prop.animationCurveValue = curve;
}
void Invalidate()
{
m_Dirty = true;
}
#endregion
#region Keyframe manipulations
void SelectKeyframe(SerializedProperty curve, int keyframeIndex)
{
m_SelectedKeyframeIndex = keyframeIndex;
m_SelectedCurve = curve;
Invalidate();
}
void ContextMenuAddKey(Vector3 hit, bool createOnCurve)
{
SerializedObject serializedObject = null;
foreach (var curve in m_Curves)
{
if (!curve.Value.editable || !curve.Value.visible)
continue;
var prop = curve.Key;
var state = curve.Value;
if (serializedObject == null)
{
serializedObject = prop.serializedObject;
serializedObject.Update();
}
var animCurve = prop.animationCurveValue;
EditCreateKeyframe(animCurve, hit, createOnCurve, state.zeroKeyConstantValue);
SaveCurve(prop, animCurve);
}
if (serializedObject != null)
serializedObject.ApplyModifiedProperties();
Invalidate();
}
void EditCreateKeyframe(AnimationCurve curve, Vector3 position, bool createOnCurve, float zeroKeyConstantValue)
{
float tangent = EvaluateTangent(curve, position.x);
if (createOnCurve)
{
position.y = curve.length == 0
? zeroKeyConstantValue
: curve.Evaluate(position.x);
}
AddKeyframe(curve, new Keyframe(position.x, position.y, tangent, tangent));
}
void EditDeleteKeyframe(AnimationCurve curve, int keyframeIndex)
{
RemoveKeyframe(curve, keyframeIndex);
}
void AddKeyframe(AnimationCurve curve, Keyframe newValue)
{
curve.AddKey(newValue);
Invalidate();
}
void RemoveKeyframe(AnimationCurve curve, int keyframeIndex)
{
curve.RemoveKey(keyframeIndex);
Invalidate();
}
void SetKeyframe(AnimationCurve curve, int keyframeIndex, Keyframe newValue)
{
var keys = curve.keys;
if (keyframeIndex > 0)
newValue.time = Mathf.Max(keys[keyframeIndex - 1].time + settings.keyTimeClampingDistance, newValue.time);
if (keyframeIndex < keys.Length - 1)
newValue.time = Mathf.Min(keys[keyframeIndex + 1].time - settings.keyTimeClampingDistance, newValue.time);
curve.MoveKey(keyframeIndex, newValue);
Invalidate();
}
void EditMoveKeyframe(AnimationCurve curve, Keyframe[] keys, int keyframeIndex)
{
var key = CanvasToCurve(Event.current.mousePosition);
float inTgt = keys[keyframeIndex].inTangent;
float outTgt = keys[keyframeIndex].outTangent;
SetKeyframe(curve, keyframeIndex, new Keyframe(key.x, key.y, inTgt, outTgt));
}
void EditMoveTangent(AnimationCurve curve, Keyframe[] keys, int keyframeIndex, Tangent targetTangent, bool linkTangents)
{
var pos = CanvasToCurve(Event.current.mousePosition);
float time = keys[keyframeIndex].time;
float value = keys[keyframeIndex].value;
pos -= new Vector3(time, value);
if (targetTangent == Tangent.In && pos.x > 0f)
pos.x = 0f;
if (targetTangent == Tangent.Out && pos.x < 0f)
pos.x = 0f;
float tangent;
if (Mathf.Approximately(pos.x, 0f))
tangent = pos.y < 0f ? float.PositiveInfinity : float.NegativeInfinity;
else
tangent = pos.y / pos.x;
float inTangent = keys[keyframeIndex].inTangent;
float outTangent = keys[keyframeIndex].outTangent;
if (targetTangent == Tangent.In || linkTangents)
inTangent = tangent;
if (targetTangent == Tangent.Out || linkTangents)
outTangent = tangent;
SetKeyframe(curve, keyframeIndex, new Keyframe(time, value, inTangent, outTangent));
}
#endregion
#region Maths utilities
Vector3 CurveToCanvas(Keyframe keyframe)
{
return CurveToCanvas(new Vector3(keyframe.time, keyframe.value));
}
Vector3 CurveToCanvas(Vector3 position)
{
var bounds = settings.bounds;
var output = new Vector3((position.x - bounds.x) / (bounds.xMax - bounds.x), (position.y - bounds.y) / (bounds.yMax - bounds.y));
output.x = output.x * (m_CurveArea.xMax - m_CurveArea.xMin) + m_CurveArea.xMin;
output.y = (1f - output.y) * (m_CurveArea.yMax - m_CurveArea.yMin) + m_CurveArea.yMin;
return output;
}
Vector3 CanvasToCurve(Vector3 position)
{
var bounds = settings.bounds;
var output = position;
output.x = (output.x - m_CurveArea.xMin) / (m_CurveArea.xMax - m_CurveArea.xMin);
output.y = (output.y - m_CurveArea.yMin) / (m_CurveArea.yMax - m_CurveArea.yMin);
output.x = Mathf.Lerp(bounds.x, bounds.xMax, output.x);
output.y = Mathf.Lerp(bounds.yMax, bounds.y, output.y);
return output;
}
Vector3 CurveTangentToCanvas(float tangent)
{
if (!float.IsInfinity(tangent))
{
var bounds = settings.bounds;
float ratio = (m_CurveArea.width / m_CurveArea.height) / ((bounds.xMax - bounds.x) / (bounds.yMax - bounds.y));
return new Vector3(1f, -tangent / ratio).normalized;
}
return float.IsPositiveInfinity(tangent) ? Vector3.up : Vector3.down;
}
Vector3[] BezierSegment(Keyframe start, Keyframe end)
{
var segment = new Vector3[4];
segment[0] = CurveToCanvas(new Vector3(start.time, start.value));
segment[3] = CurveToCanvas(new Vector3(end.time, end.value));
float middle = start.time + ((end.time - start.time) * 0.333333f);
float middle2 = start.time + ((end.time - start.time) * 0.666666f);
segment[1] = CurveToCanvas(new Vector3(middle, ProjectTangent(start.time, start.value, start.outTangent, middle)));
segment[2] = CurveToCanvas(new Vector3(middle2, ProjectTangent(end.time, end.value, end.inTangent, middle2)));
return segment;
}
Vector3[] HardSegment(Keyframe start, Keyframe end)
{
var segment = new Vector3[3];
segment[0] = CurveToCanvas(start);
segment[1] = CurveToCanvas(new Vector3(end.time, start.value));
segment[2] = CurveToCanvas(end);
return segment;
}
float ProjectTangent(float inPosition, float inValue, float inTangent, float projPosition)
{
return inValue + ((projPosition - inPosition) * inTangent);
}
float EvaluateTangent(AnimationCurve curve, float time)
{
int prev = -1, next = 0;
for (int i = 0; i < curve.keys.Length; i++)
{
if (time > curve.keys[i].time)
{
prev = i;
next = i + 1;
}
else break;
}
if (next == 0)
return 0f;
if (prev == curve.keys.Length - 1)
return 0f;
const float kD = 1e-3f;
float tp = Mathf.Max(time - kD, curve.keys[prev].time);
float tn = Mathf.Min(time + kD, curve.keys[next].time);
float vp = curve.Evaluate(tp);
float vn = curve.Evaluate(tn);
if (Mathf.Approximately(tn, tp))
return (vn - vp > 0f) ? float.PositiveInfinity : float.NegativeInfinity;
return (vn - vp) / (tn - tp);
}
#endregion
}
}