486 lines
19 KiB
C#
486 lines
19 KiB
C#
|
using UnityEditor;
|
||
|
using UnityEngine;
|
||
|
using System.Collections;
|
||
|
using System;
|
||
|
|
||
|
namespace RootMotion.FinalIK
|
||
|
{
|
||
|
|
||
|
/*
|
||
|
* Custom inspector for RotationLimitSpline
|
||
|
* */
|
||
|
[CustomEditor(typeof(RotationLimitSpline))]
|
||
|
[CanEditMultipleObjects]
|
||
|
public class RotationLimitSplineInspector : RotationLimitInspector
|
||
|
{
|
||
|
|
||
|
// Determines if we are dragging the handle's limits or angle
|
||
|
public enum ScaleMode
|
||
|
{
|
||
|
Limit,
|
||
|
Angle
|
||
|
}
|
||
|
|
||
|
// In Smooth TangentMode, in and out tangents will always be the same, Independent allowes for difference
|
||
|
public enum TangentMode
|
||
|
{
|
||
|
Smooth,
|
||
|
Independent
|
||
|
}
|
||
|
|
||
|
private RotationLimitSpline script { get { return target as RotationLimitSpline; } }
|
||
|
private RotationLimitSpline clone;
|
||
|
private ScaleMode scaleMode;
|
||
|
private TangentMode tangentMode;
|
||
|
private int selectedHandle = -1, deleteHandle = -1, addHandle = -1;
|
||
|
|
||
|
#region Inspector
|
||
|
|
||
|
public void OnEnable()
|
||
|
{
|
||
|
// Check if RotationLimitspline is properly set up, if not, reset to defaults
|
||
|
if (script.spline == null)
|
||
|
{
|
||
|
script.spline = new AnimationCurve(defaultKeys);
|
||
|
EditorUtility.SetDirty(script);
|
||
|
}
|
||
|
else if (script.spline.keys.Length < 4)
|
||
|
{
|
||
|
script.spline.keys = defaultKeys;
|
||
|
EditorUtility.SetDirty(script);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Returns the default keyframes for the RotationLimitspline
|
||
|
* */
|
||
|
private static Keyframe[] defaultKeys
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
Keyframe[] k = new Keyframe[5];
|
||
|
// Values for a simple elliptic spline
|
||
|
k[0].time = 0.01f;
|
||
|
k[0].value = 30;
|
||
|
k[0].inTangent = 0.01f;
|
||
|
k[0].outTangent = 0.01f;
|
||
|
k[1].time = 90;
|
||
|
k[1].value = 45;
|
||
|
k[1].inTangent = 0.01f;
|
||
|
k[1].outTangent = 0.01f;
|
||
|
k[2].time = 180;
|
||
|
k[2].value = 30;
|
||
|
k[2].inTangent = 0.01f;
|
||
|
k[2].outTangent = 0.01f;
|
||
|
k[3].time = 270;
|
||
|
k[3].value = 45;
|
||
|
k[3].inTangent = 0.01f;
|
||
|
k[3].outTangent = 0.01f;
|
||
|
k[4].time = 360;
|
||
|
k[4].value = 30;
|
||
|
k[4].inTangent = 0.01f;
|
||
|
k[4].outTangent = 0.01f;
|
||
|
return k;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public override void OnInspectorGUI()
|
||
|
{
|
||
|
GUI.changed = false;
|
||
|
|
||
|
DrawDefaultInspector();
|
||
|
|
||
|
script.twistLimit = Mathf.Clamp(script.twistLimit, 0, 180);
|
||
|
|
||
|
if (GUI.changed) EditorUtility.SetDirty(script);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Make sure the keyframes and tangents are valid
|
||
|
* */
|
||
|
private void ValidateKeyframes(Keyframe[] keys)
|
||
|
{
|
||
|
keys[keys.Length - 1].value = keys[0].value;
|
||
|
keys[keys.Length - 1].time = keys[0].time + 360;
|
||
|
keys[keys.Length - 1].inTangent = keys[0].inTangent;
|
||
|
}
|
||
|
|
||
|
#endregion Inspector
|
||
|
|
||
|
#region Scene
|
||
|
|
||
|
void OnSceneGUI()
|
||
|
{
|
||
|
GUI.changed = false;
|
||
|
// Get the keyframes of the AnimationCurve to be manipulated
|
||
|
Keyframe[] keys = script.spline.keys;
|
||
|
|
||
|
// Set defaultLocalRotation so that the initial local rotation will be the zero point for the rotation limit
|
||
|
if (!Application.isPlaying && !script.defaultLocalRotationOverride) script.defaultLocalRotation = script.transform.localRotation;
|
||
|
if (script.axis == Vector3.zero) return;
|
||
|
|
||
|
// Make the curve loop
|
||
|
script.spline.postWrapMode = WrapMode.Loop;
|
||
|
script.spline.preWrapMode = WrapMode.Loop;
|
||
|
|
||
|
DrawRotationSphere(script.transform.position);
|
||
|
|
||
|
// Display the main axis
|
||
|
DrawArrow(script.transform.position, Direction(script.axis), colorDefault, "Axis", 0.02f);
|
||
|
|
||
|
Vector3 swing = script.axis.normalized;
|
||
|
|
||
|
// Editing tools GUI
|
||
|
Handles.BeginGUI();
|
||
|
GUILayout.BeginArea(new Rect(10, 10, 440, 100), "Rotation Limit Spline", "Window");
|
||
|
|
||
|
// Scale Mode and Tangent Mode
|
||
|
GUILayout.BeginHorizontal();
|
||
|
scaleMode = (ScaleMode)EditorGUILayout.EnumPopup("Drag Handle", scaleMode);
|
||
|
tangentMode = (TangentMode)EditorGUILayout.EnumPopup("Drag Tangents", tangentMode);
|
||
|
GUILayout.EndHorizontal();
|
||
|
|
||
|
EditorGUILayout.Space();
|
||
|
|
||
|
if (Inspector.Button("Rotate 90 degrees", "Rotate rotation limit around axis.", script, GUILayout.Width(220)))
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle Value");
|
||
|
for (int i = 0; i < keys.Length; i++) keys[i].time += 90;
|
||
|
}
|
||
|
|
||
|
// Cloning values from another RotationLimitSpline
|
||
|
EditorGUILayout.BeginHorizontal();
|
||
|
if (Inspector.Button("Clone From", "Make this rotation limit identical to another", script, GUILayout.Width(220)))
|
||
|
{
|
||
|
CloneLimit();
|
||
|
keys = script.spline.keys;
|
||
|
}
|
||
|
clone = (RotationLimitSpline)EditorGUILayout.ObjectField("", clone, typeof(RotationLimitSpline), true);
|
||
|
EditorGUILayout.EndHorizontal();
|
||
|
|
||
|
GUILayout.EndArea();
|
||
|
Handles.EndGUI();
|
||
|
|
||
|
// Draw keyframes
|
||
|
for (int i = 0; i < keys.Length - 1; i++)
|
||
|
{
|
||
|
float angle = keys[i].time;
|
||
|
|
||
|
// Start drawing handles
|
||
|
Quaternion offset = Quaternion.AngleAxis(angle, swing);
|
||
|
Quaternion rotation = Quaternion.AngleAxis(keys[i].value, offset * script.crossAxis);
|
||
|
Vector3 position = script.transform.position + Direction(rotation * swing);
|
||
|
Handles.Label(position, " " + i.ToString());
|
||
|
|
||
|
// Dragging Values
|
||
|
if (selectedHandle == i)
|
||
|
{
|
||
|
Handles.color = colorHandles;
|
||
|
switch (scaleMode)
|
||
|
{
|
||
|
case ScaleMode.Limit:
|
||
|
float inputValue = keys[i].value;
|
||
|
inputValue = Mathf.Clamp(Inspector.ScaleValueHandleSphere(inputValue, position, Quaternion.identity, 0.5f, 0), 0.01f, 180);
|
||
|
if (keys[i].value != inputValue)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle Value");
|
||
|
keys[i].value = inputValue;
|
||
|
}
|
||
|
break;
|
||
|
case ScaleMode.Angle:
|
||
|
float inputTime = keys[i].time;
|
||
|
inputTime = Inspector.ScaleValueHandleSphere(inputTime, position, Quaternion.identity, 0.5f, 0);
|
||
|
if (keys[i].time != inputTime)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle Angle");
|
||
|
keys[i].time = inputTime;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Handle select button
|
||
|
if (selectedHandle != i)
|
||
|
{
|
||
|
Handles.color = Color.blue;
|
||
|
if (Inspector.SphereButton(position, script.transform.rotation, 0.05f, 0.05f))
|
||
|
{
|
||
|
selectedHandle = i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Tangents
|
||
|
if (selectedHandle == i)
|
||
|
{
|
||
|
// Evaluate positions before and after the key to get the tangent positions
|
||
|
Vector3 prevPosition = GetAnglePosition(keys[i].time - 1);
|
||
|
Vector3 nextPosition = GetAnglePosition(keys[i].time + 1);
|
||
|
|
||
|
// Draw handles for the tangents
|
||
|
Handles.color = Color.white;
|
||
|
Vector3 toNext = (nextPosition - position).normalized * 0.3f;
|
||
|
float outTangent = keys[i].outTangent;
|
||
|
outTangent = Inspector.ScaleValueHandleSphere(outTangent, position + toNext, Quaternion.identity, 0.2f, 0);
|
||
|
|
||
|
Vector3 toPrev = (prevPosition - position).normalized * 0.3f;
|
||
|
float inTangent = keys[i].inTangent;
|
||
|
inTangent = Inspector.ScaleValueHandleSphere(inTangent, position + toPrev, Quaternion.identity, 0.2f, 0);
|
||
|
|
||
|
if (outTangent != keys[i].outTangent || inTangent != keys[i].inTangent) selectedHandle = i;
|
||
|
|
||
|
// Make the other tangent match the dragged tangent (if in "Smooth" TangentMode)
|
||
|
switch (tangentMode)
|
||
|
{
|
||
|
case TangentMode.Smooth:
|
||
|
if (outTangent != keys[i].outTangent)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Tangents");
|
||
|
keys[i].outTangent = outTangent;
|
||
|
keys[i].inTangent = outTangent;
|
||
|
}
|
||
|
else if (inTangent != keys[i].inTangent)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Tangents");
|
||
|
keys[i].outTangent = inTangent;
|
||
|
keys[i].inTangent = inTangent;
|
||
|
}
|
||
|
break;
|
||
|
case TangentMode.Independent:
|
||
|
if (outTangent != keys[i].outTangent)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Tangents");
|
||
|
keys[i].outTangent = outTangent;
|
||
|
}
|
||
|
else if (inTangent != keys[i].inTangent)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Tangents");
|
||
|
keys[i].inTangent = inTangent;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Draw lines and labels to tangent handles
|
||
|
Handles.color = Color.white;
|
||
|
GUI.color = Color.white;
|
||
|
|
||
|
Handles.DrawLine(position, position + toNext);
|
||
|
Handles.Label(position + toNext, " Out");
|
||
|
|
||
|
Handles.DrawLine(position, position + toPrev);
|
||
|
Handles.Label(position + toPrev, " In");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Selected Point GUI
|
||
|
if (selectedHandle != -1)
|
||
|
{
|
||
|
Handles.BeginGUI();
|
||
|
GUILayout.BeginArea(new Rect(Screen.width - 240, Screen.height - 200, 230, 150), "Handle " + selectedHandle.ToString(), "Window");
|
||
|
|
||
|
if (Inspector.Button("Delete", "Delete this handle", script))
|
||
|
{
|
||
|
if (keys.Length > 4)
|
||
|
{
|
||
|
deleteHandle = selectedHandle;
|
||
|
}
|
||
|
else if (!Warning.logged) script.LogWarning("Spline Rotation Limit should have at least 3 handles");
|
||
|
}
|
||
|
if (Inspector.Button("Add Handle", "Add a new handle next to this one", script))
|
||
|
{
|
||
|
addHandle = selectedHandle;
|
||
|
}
|
||
|
|
||
|
// Clamp the key angles to previous and next handle angles
|
||
|
float prevTime = 0, nextTime = 0;
|
||
|
if (selectedHandle < keys.Length - 2) nextTime = keys[selectedHandle + 1].time;
|
||
|
else nextTime = keys[0].time + 360;
|
||
|
|
||
|
if (selectedHandle == 0) prevTime = keys[keys.Length - 2].time - 360;
|
||
|
else prevTime = keys[selectedHandle - 1].time;
|
||
|
|
||
|
// Angles
|
||
|
float inputTime = keys[selectedHandle].time;
|
||
|
inputTime = Mathf.Clamp(EditorGUILayout.FloatField(new GUIContent("Angle", "Angle of the point (0-360)."), inputTime), prevTime, nextTime);
|
||
|
|
||
|
if (keys[selectedHandle].time != inputTime)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle Angle");
|
||
|
keys[selectedHandle].time = inputTime;
|
||
|
}
|
||
|
|
||
|
// Limits
|
||
|
float inputValue = keys[selectedHandle].value;
|
||
|
inputValue = Mathf.Clamp(EditorGUILayout.FloatField(new GUIContent("Limit", "Max angular limit from Axis at this angle"), inputValue), 0, 180);
|
||
|
if (keys[selectedHandle].value != inputValue)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle Limit");
|
||
|
keys[selectedHandle].value = inputValue;
|
||
|
}
|
||
|
|
||
|
// In Tangents
|
||
|
float inputInTangent = keys[selectedHandle].inTangent;
|
||
|
inputInTangent = EditorGUILayout.FloatField(new GUIContent("In Tangent", "In tangent of the handle point on the spline"), inputInTangent);
|
||
|
if (keys[selectedHandle].inTangent != inputInTangent)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle In Tangent");
|
||
|
keys[selectedHandle].inTangent = inputInTangent;
|
||
|
}
|
||
|
|
||
|
// Out tangents
|
||
|
float inputOutTangent = keys[selectedHandle].outTangent;
|
||
|
inputOutTangent = EditorGUILayout.FloatField(new GUIContent("Out Tangent", "Out tangent of the handle point on the spline"), inputOutTangent);
|
||
|
if (keys[selectedHandle].outTangent != inputOutTangent)
|
||
|
{
|
||
|
if (!Application.isPlaying) Undo.RecordObject(script, "Handle Out Tangent");
|
||
|
keys[selectedHandle].outTangent = inputOutTangent;
|
||
|
}
|
||
|
|
||
|
GUILayout.EndArea();
|
||
|
Handles.EndGUI();
|
||
|
}
|
||
|
|
||
|
// Make sure the keyframes are valid;
|
||
|
ValidateKeyframes(keys);
|
||
|
|
||
|
// Replace the AnimationCurve keyframes with the manipulated keyframes
|
||
|
script.spline.keys = keys;
|
||
|
|
||
|
// Display limits
|
||
|
for (int i = 0; i < 360; i += 2)
|
||
|
{
|
||
|
float evaluatedLimit = script.spline.Evaluate((float)i);
|
||
|
Quaternion offset = Quaternion.AngleAxis(i, swing);
|
||
|
Quaternion evaluatedRotation = Quaternion.AngleAxis(evaluatedLimit, offset * script.crossAxis);
|
||
|
Quaternion testRotation = Quaternion.AngleAxis(179.9f, offset * script.crossAxis);
|
||
|
|
||
|
Quaternion limitedRotation = script.LimitSwing(testRotation);
|
||
|
|
||
|
Vector3 evaluatedDirection = evaluatedRotation * swing;
|
||
|
Vector3 limitedDirection = limitedRotation * swing;
|
||
|
|
||
|
// Display the limit points in red if they are out of range
|
||
|
bool isValid = Vector3.Distance(evaluatedDirection, limitedDirection) < 0.01f && evaluatedLimit >= 0;
|
||
|
Color color = isValid ? colorDefaultTransparent : colorInvalid;
|
||
|
|
||
|
Vector3 limitPoint = script.transform.position + Direction(evaluatedDirection);
|
||
|
|
||
|
Handles.color = color;
|
||
|
if (i == 0) zeroPoint = limitPoint;
|
||
|
|
||
|
Handles.DrawLine(script.transform.position, limitPoint);
|
||
|
|
||
|
if (i > 0)
|
||
|
{
|
||
|
Handles.color = isValid ? colorDefault : colorInvalid;
|
||
|
Handles.DrawLine(limitPoint, lastPoint);
|
||
|
if (i == 358) Handles.DrawLine(limitPoint, zeroPoint);
|
||
|
}
|
||
|
|
||
|
lastPoint = limitPoint;
|
||
|
}
|
||
|
|
||
|
// Deleting points
|
||
|
if (deleteHandle != -1)
|
||
|
{
|
||
|
DeleteHandle(deleteHandle);
|
||
|
selectedHandle = -1;
|
||
|
deleteHandle = -1;
|
||
|
}
|
||
|
|
||
|
// Adding points
|
||
|
if (addHandle != -1)
|
||
|
{
|
||
|
AddHandle(addHandle);
|
||
|
addHandle = -1;
|
||
|
}
|
||
|
|
||
|
Handles.color = Color.white;
|
||
|
if (GUI.changed) EditorUtility.SetDirty(script);
|
||
|
}
|
||
|
|
||
|
private Vector3 lastPoint, zeroPoint;
|
||
|
|
||
|
/*
|
||
|
* Return the evaluated position for the specified angle
|
||
|
* */
|
||
|
private Vector3 GetAnglePosition(float angle)
|
||
|
{
|
||
|
Vector3 swing = script.axis.normalized;
|
||
|
Quaternion offset = Quaternion.AngleAxis(angle, swing);
|
||
|
Quaternion rotation = Quaternion.AngleAxis(script.spline.Evaluate(angle), offset * script.crossAxis);
|
||
|
return script.transform.position + Direction(rotation * swing);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Converting directions from local space to world space
|
||
|
* */
|
||
|
private Vector3 Direction(Vector3 v)
|
||
|
{
|
||
|
if (script.transform.parent == null) return script.defaultLocalRotation * v;
|
||
|
return script.transform.parent.rotation * (script.defaultLocalRotation * v);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Removing Handles
|
||
|
* */
|
||
|
private void DeleteHandle(int p)
|
||
|
{
|
||
|
Keyframe[] keys = script.spline.keys;
|
||
|
Keyframe[] newKeys = new Keyframe[0];
|
||
|
|
||
|
for (int i = 0; i < keys.Length; i++)
|
||
|
{
|
||
|
if (i != p)
|
||
|
{
|
||
|
Array.Resize(ref newKeys, newKeys.Length + 1);
|
||
|
newKeys[newKeys.Length - 1] = keys[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
script.spline.keys = newKeys;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Creating new Handles
|
||
|
* */
|
||
|
private void AddHandle(int p)
|
||
|
{
|
||
|
Keyframe[] keys = script.spline.keys;
|
||
|
Keyframe[] newKeys = new Keyframe[keys.Length + 1];
|
||
|
|
||
|
for (int i = 0; i < p + 1; i++) newKeys[i] = keys[i];
|
||
|
|
||
|
float nextTime = 0;
|
||
|
if (p < keys.Length - 1) nextTime = keys[p + 1].time;
|
||
|
else nextTime = keys[0].time;
|
||
|
|
||
|
float newTime = Mathf.Lerp(keys[p].time, nextTime, 0.5f);
|
||
|
float newValue = script.spline.Evaluate(newTime);
|
||
|
|
||
|
newKeys[p + 1] = new Keyframe(newTime, newValue);
|
||
|
|
||
|
for (int i = p + 2; i < newKeys.Length; i++) newKeys[i] = keys[i - 1];
|
||
|
|
||
|
script.spline.keys = newKeys;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Clone properties from another RotationLimitSpline
|
||
|
* */
|
||
|
private void CloneLimit()
|
||
|
{
|
||
|
if (clone == null) return;
|
||
|
if (clone == script)
|
||
|
{
|
||
|
script.LogWarning("Can't clone from self.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
script.axis = clone.axis;
|
||
|
script.twistLimit = clone.twistLimit;
|
||
|
script.spline.keys = clone.spline.keys;
|
||
|
}
|
||
|
|
||
|
#endregion Scene
|
||
|
}
|
||
|
}
|