//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using System.Text;
using bs;
namespace bs.Editor
{
/** @addtogroup AnimationEditor
* @{
*/
///
/// A set of animation curves for a field of a certain type.
///
internal struct FieldAnimCurves
{
public SerializableProperty.FieldType type;
public EdCurveDrawInfo[] curveInfos;
public bool isPropertyCurve;
}
///
/// Stores tangent modes for an 3D vector animation curve (one mode for each keyframe).
///
[SerializeObject]
internal class EditorVector3CurveTangents
{
public string name;
public TangentMode[] tangentsX;
public TangentMode[] tangentsY;
public TangentMode[] tangentsZ;
}
///
/// Stores tangent modes for an float animation curve (one mode for each keyframe).
///
[SerializeObject]
internal class EditorFloatCurveTangents
{
public string name;
public TangentMode[] tangents;
}
///
/// Stores tangent information for all curves in an animation clip.
///
[SerializeObject]
internal class EditorAnimClipTangents
{
public EditorVector3CurveTangents[] positionCurves;
public EditorVector3CurveTangents[] rotationCurves;
public EditorVector3CurveTangents[] scaleCurves;
public EditorFloatCurveTangents[] floatCurves;
}
///
/// Stores animation clip data for clips that are currently being edited.
///
internal class EditorAnimClipInfo
{
public AnimationClip clip;
public bool isImported;
public int sampleRate;
public Dictionary curves = new Dictionary();
public AnimationEvent[] events = new AnimationEvent[0];
///
/// Loads curve and event information from the provided clip, and creates a new instance of this object containing
/// the required data for editing the source clip in the animation editor.
///
/// Clip to load.
/// Editor specific editable information about an animation clip.
public static EditorAnimClipInfo Create(AnimationClip clip)
{
EditorAnimClipInfo clipInfo = new EditorAnimClipInfo();
clipInfo.clip = clip;
clipInfo.isImported = IsClipImported(clip);
clipInfo.sampleRate = (int)clip.SampleRate;
AnimationCurves clipCurves = clip.Curves;
EditorAnimClipTangents editorCurveData = null;
string resourcePath = ProjectLibrary.GetPath(clip);
if (!string.IsNullOrEmpty(resourcePath))
{
LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
string clipName = PathEx.GetTail(resourcePath);
if (entry != null && entry.Type == LibraryEntryType.File)
{
FileEntry fileEntry = (FileEntry)entry;
ResourceMeta[] metas = fileEntry.ResourceMetas;
if (clipInfo.isImported)
{
for (int i = 0; i < metas.Length; i++)
{
if (clipName == metas[i].SubresourceName)
{
editorCurveData = metas[i].EditorData as EditorAnimClipTangents;
break;
}
}
}
else
{
if(metas.Length > 0)
editorCurveData = metas[0].EditorData as EditorAnimClipTangents;
}
}
}
if (editorCurveData == null)
editorCurveData = new EditorAnimClipTangents();
int globalCurveIdx = 0;
Action loadVector3Curve =
(curves, tangents, subPath) =>
{
foreach (var curveEntry in curves)
{
TangentMode[] tangentsX = null;
TangentMode[] tangentsY = null;
TangentMode[] tangentsZ = null;
if (tangents != null)
{
foreach (var tangentEntry in tangents)
{
if (tangentEntry.name == curveEntry.name)
{
tangentsX = tangentEntry.tangentsX;
tangentsY = tangentEntry.tangentsY;
tangentsZ = tangentEntry.tangentsZ;
break;
}
}
}
// Convert compound curve to three per-component curves
AnimationCurve[] componentCurves = AnimationUtility.SplitCurve3D(curveEntry.curve);
FieldAnimCurves fieldCurves = new FieldAnimCurves();
fieldCurves.type = SerializableProperty.FieldType.Vector3;
fieldCurves.curveInfos = new EdCurveDrawInfo[3];
fieldCurves.isPropertyCurve = !clipInfo.isImported;
fieldCurves.curveInfos[0] = new EdCurveDrawInfo();
fieldCurves.curveInfos[0].curve = new EdAnimationCurve(componentCurves[0], tangentsX);
fieldCurves.curveInfos[0].color = GetUniqueColor(globalCurveIdx++);
fieldCurves.curveInfos[1] = new EdCurveDrawInfo();
fieldCurves.curveInfos[1].curve = new EdAnimationCurve(componentCurves[1], tangentsY);
fieldCurves.curveInfos[1].color = GetUniqueColor(globalCurveIdx++);
fieldCurves.curveInfos[2] = new EdCurveDrawInfo();
fieldCurves.curveInfos[2].curve = new EdAnimationCurve(componentCurves[2], tangentsZ);
fieldCurves.curveInfos[2].color = GetUniqueColor(globalCurveIdx++);
string curvePath = curveEntry.name.TrimEnd('/') + subPath;
clipInfo.curves[curvePath] = fieldCurves;
}
};
// Convert rotation from quaternion to euler
NamedQuaternionCurve[] rotationCurves = clipCurves.Rotation;
NamedVector3Curve[] eulerRotationCurves = new NamedVector3Curve[rotationCurves.Length];
for(int i = 0; i < rotationCurves.Length; i++)
{
eulerRotationCurves[i] = new NamedVector3Curve();
eulerRotationCurves[i].name = rotationCurves[i].name;
eulerRotationCurves[i].flags = rotationCurves[i].flags;
eulerRotationCurves[i].curve = AnimationUtility.QuaternionToEulerCurve(rotationCurves[i].curve);
}
loadVector3Curve(clipCurves.Position, editorCurveData.positionCurves, "/Position");
loadVector3Curve(eulerRotationCurves, editorCurveData.rotationCurves, "/Rotation");
loadVector3Curve(clipCurves.Scale, editorCurveData.scaleCurves, "/Scale");
// Find which individual float curves belong to the same field
Dictionary[]> floatCurveMapping = new Dictionary[]>();
{
int curveIdx = 0;
foreach (var curveEntry in clipCurves.Generic)
{
string path = curveEntry.name;
string pathNoSuffix = null;
string pathSuffix;
if (path.Length >= 2)
{
pathSuffix = path.Substring(path.Length - 2, 2);
pathNoSuffix = path.Substring(0, path.Length - 2);
}
else
pathSuffix = "";
int tangentIdx = -1;
int currentTangentIdx = 0;
foreach (var tangentEntry in editorCurveData.floatCurves)
{
if (tangentEntry.name == curveEntry.name)
{
tangentIdx = currentTangentIdx;
break;
}
currentTangentIdx++;
}
Animation.PropertySuffixInfo suffixInfo;
if (Animation.PropertySuffixInfos.TryGetValue(pathSuffix, out suffixInfo))
{
Tuple[] curveInfo;
if (!floatCurveMapping.TryGetValue(pathNoSuffix, out curveInfo))
curveInfo = new Tuple[4];
curveInfo[suffixInfo.elementIdx] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector);
floatCurveMapping[pathNoSuffix] = curveInfo;
}
else
{
Tuple[] curveInfo = new Tuple[4];
curveInfo[0] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector);
floatCurveMapping[path] = curveInfo;
}
curveIdx++;
}
}
foreach (var KVP in floatCurveMapping)
{
int numCurves = 0;
for (int i = 0; i < 4; i++)
{
if (KVP.Value[i] == null)
continue;
numCurves++;
}
if (numCurves == 0)
continue; // Invalid curve
FieldAnimCurves fieldCurves = new FieldAnimCurves();
// Deduce type (note that all single value types are assumed to be float even if their source type is int or bool)
if (numCurves == 1)
fieldCurves.type = SerializableProperty.FieldType.Float;
else if (numCurves == 2)
fieldCurves.type = SerializableProperty.FieldType.Vector2;
else if (numCurves == 3)
fieldCurves.type = SerializableProperty.FieldType.Vector3;
else // 4 curves
{
bool isVector = KVP.Value[0].Item3;
if (isVector)
fieldCurves.type = SerializableProperty.FieldType.Vector4;
else
fieldCurves.type = SerializableProperty.FieldType.Color;
}
bool isMorphCurve = false;
string curvePath = KVP.Key;
fieldCurves.curveInfos = new EdCurveDrawInfo[numCurves];
for (int i = 0; i < numCurves; i++)
{
int curveIdx = KVP.Value[i].Item1;
int tangentIdx = KVP.Value[i].Item2;
TangentMode[] tangents = null;
if (tangentIdx != -1)
tangents = editorCurveData.floatCurves[tangentIdx].tangents;
fieldCurves.curveInfos[i] = new EdCurveDrawInfo();
fieldCurves.curveInfos[i].curve = new EdAnimationCurve(clipCurves.Generic[curveIdx].curve, tangents);
fieldCurves.curveInfos[i].color = GetUniqueColor(globalCurveIdx++);
if (clipCurves.Generic[curveIdx].flags.HasFlag(AnimationCurveFlags.MorphFrame))
{
curvePath = "MorphShapes/Frames/" + KVP.Key;
isMorphCurve = true;
}
else if (clipCurves.Generic[curveIdx].flags.HasFlag(AnimationCurveFlags.MorphWeight))
{
curvePath = "MorphShapes/Weight/" + KVP.Key;
isMorphCurve = true;
}
}
fieldCurves.isPropertyCurve = !clipInfo.isImported && !isMorphCurve;
clipInfo.curves[curvePath] = fieldCurves;
}
// Add events
clipInfo.events = clip.Events;
return clipInfo;
}
///
/// Checks is the specified animation clip is imported from an external file, or created within the editor.
///
/// Clip to check.
/// True if the clip is imported from an external file (e.g. FBX file), or false if the clip is a native
/// resource created within the editor.
public static bool IsClipImported(AnimationClip clip)
{
string resourcePath = ProjectLibrary.GetPath(clip);
return ProjectLibrary.IsSubresource(resourcePath);
}
///
/// Checks does a curve with the specified path represent a curve affecting a morph shape.
///
/// Path of the curve to check.
/// True if morph shape frame or weight animation, false otherwise.
public static bool IsMorphShapeCurve(string path)
{
if (string.IsNullOrEmpty(path))
return false;
string trimmedPath = path.Trim('/');
string[] entries = trimmedPath.Split('/');
if (entries.Length < 3)
return false;
if (entries[entries.Length - 3] != "MorphShapes")
return false;
return entries[entries.Length - 2] == "Weight" || entries[entries.Length - 2] == "Frames";
}
///
/// Applies any changes made to the animation curves or events to the actual animation clip. Only works for
/// non-imported animation clips.
///
/// Tangent modes for all the saved animation curves.
public void Apply(out EditorAnimClipTangents tangents)
{
if (isImported || clip == null)
{
tangents = null;
return;
}
List positionCurves = new List();
List rotationCurves = new List();
List scaleCurves = new List();
List floatCurves = new List();
List positionTangents = new List();
List rotationTangents = new List();
List scaleTangents = new List();
List floatTangents = new List();
foreach (var kvp in curves)
{
string[] pathEntries = kvp.Key.Split('/');
if (pathEntries.Length == 0)
continue;
string lastEntry = pathEntries[pathEntries.Length - 1];
if (lastEntry == "Position" || lastEntry == "Rotation" || lastEntry == "Scale")
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < pathEntries.Length - 2; i++)
sb.Append(pathEntries[i] + "/");
if (pathEntries.Length > 1)
sb.Append(pathEntries[pathEntries.Length - 2]);
string curvePath = sb.ToString();
NamedVector3Curve curve = new NamedVector3Curve();
curve.name = curvePath;
curve.curve = AnimationUtility.CombineCurve3D(new[]
{
new AnimationCurve(kvp.Value.curveInfos[0].curve.KeyFrames),
new AnimationCurve(kvp.Value.curveInfos[1].curve.KeyFrames),
new AnimationCurve(kvp.Value.curveInfos[2].curve.KeyFrames)
});
EditorVector3CurveTangents curveTangents = new EditorVector3CurveTangents();
curveTangents.name = curvePath;
curveTangents.tangentsX = kvp.Value.curveInfos[0].curve.TangentModes;
curveTangents.tangentsY = kvp.Value.curveInfos[1].curve.TangentModes;
curveTangents.tangentsZ = kvp.Value.curveInfos[2].curve.TangentModes;
if (lastEntry == "Position")
{
positionCurves.Add(curve);
positionTangents.Add(curveTangents);
}
else if (lastEntry == "Rotation")
{
NamedQuaternionCurve quatCurve = new NamedQuaternionCurve();
quatCurve.name = curve.name;
quatCurve.curve = AnimationUtility.EulerToQuaternionCurve(curve.curve);
rotationCurves.Add(quatCurve);
rotationTangents.Add(curveTangents);
}
else if (lastEntry == "Scale")
{
scaleCurves.Add(curve);
scaleTangents.Add(curveTangents);
}
}
else
{
Action addCurve = (idx, path, subPath, flags) =>
{
string fullPath = path + subPath;
NamedFloatCurve curve = new NamedFloatCurve(fullPath,
new AnimationCurve(kvp.Value.curveInfos[idx].curve.KeyFrames));
curve.flags = flags;
EditorFloatCurveTangents curveTangents = new EditorFloatCurveTangents();
curveTangents.name = fullPath;
curveTangents.tangents = kvp.Value.curveInfos[idx].curve.TangentModes;
floatCurves.Add(curve);
floatTangents.Add(curveTangents);
};
switch (kvp.Value.type)
{
case SerializableProperty.FieldType.Vector2:
addCurve(0, kvp.Key, ".x", 0);
addCurve(1, kvp.Key, ".y", 0);
break;
case SerializableProperty.FieldType.Vector3:
addCurve(0, kvp.Key, ".x", 0);
addCurve(1, kvp.Key, ".y", 0);
addCurve(2, kvp.Key, ".z", 0);
break;
case SerializableProperty.FieldType.Vector4:
addCurve(0, kvp.Key, ".x", 0);
addCurve(1, kvp.Key, ".y", 0);
addCurve(2, kvp.Key, ".z", 0);
addCurve(3, kvp.Key, ".w", 0);
break;
case SerializableProperty.FieldType.Color:
addCurve(0, kvp.Key, ".r", 0);
addCurve(1, kvp.Key, ".g", 0);
addCurve(2, kvp.Key, ".b", 0);
addCurve(3, kvp.Key, ".a", 0);
break;
case SerializableProperty.FieldType.Bool:
case SerializableProperty.FieldType.Int:
case SerializableProperty.FieldType.Float:
{
AnimationCurveFlags flags = 0;
string path = kvp.Key;
if (IsMorphShapeCurve(kvp.Key))
{
string trimmedPath = path.Trim('/');
string[] entries = trimmedPath.Split('/');
bool isWeight = entries[entries.Length - 2] == "Weight";
if (isWeight)
flags = AnimationCurveFlags.MorphWeight;
else
flags = AnimationCurveFlags.MorphFrame;
path = entries[entries.Length - 1];
}
addCurve(0, path, "", flags);
}
break;
}
}
}
AnimationCurves newClipCurves = new AnimationCurves();
newClipCurves.Position = positionCurves.ToArray();
newClipCurves.Rotation = rotationCurves.ToArray();
newClipCurves.Scale = scaleCurves.ToArray();
newClipCurves.Generic = floatCurves.ToArray();
clip.Curves = newClipCurves;
clip.Events = events;
clip.SampleRate = sampleRate;
tangents = new EditorAnimClipTangents();
tangents.positionCurves = positionTangents.ToArray();
tangents.rotationCurves = rotationTangents.ToArray();
tangents.scaleCurves = scaleTangents.ToArray();
tangents.floatCurves = floatTangents.ToArray();
}
///
/// Saves the animation curves and events stored in this object, into the associated animation clip resource.
/// Relevant animation clip resource must already be created and exist in the project library.
///
public void SaveToClip()
{
if (!isImported)
{
EditorAnimClipTangents tangents;
Apply(out tangents);
string resourcePath = ProjectLibrary.GetPath(clip);
ProjectLibrary.Save(clip);
ProjectLibrary.SetEditorData(resourcePath, tangents);
}
else
{
string resourcePath = ProjectLibrary.GetPath(clip);
LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
if (entry != null && entry.Type == LibraryEntryType.File)
{
FileEntry fileEntry = (FileEntry)entry;
MeshImportOptions meshImportOptions = (MeshImportOptions)fileEntry.Options;
string clipName = PathEx.GetTail(resourcePath);
List newEvents = new List();
newEvents.AddRange(meshImportOptions.AnimationEvents);
bool isExisting = false;
for (int i = 0; i < newEvents.Count; i++)
{
if (newEvents[i].Name == clipName)
{
newEvents[i].Events = events;
isExisting = true;
break;
}
}
if (!isExisting)
{
ImportedAnimationEvents newEntry = new ImportedAnimationEvents();
newEntry.Name = clipName;
newEntry.Events = events;
newEvents.Add(newEntry);
}
meshImportOptions.AnimationEvents = newEvents.ToArray();
ProjectLibrary.Reimport(resourcePath, meshImportOptions, true);
}
}
}
///
/// Generates a unique color based on the provided index.
///
/// Index to use for generating a color. Should be less than 30 in order to guarantee reasonably
/// different colors.
/// Unique color.
public static Color GetUniqueColor(int idx)
{
const int COLOR_SPACING = 359 / 15;
float hue = ((idx * COLOR_SPACING) % 359) / 359.0f;
return Color.HSV2RGB(new Color(hue, 175.0f / 255.0f, 175.0f / 255.0f));
}
}
/** @} */
}