/******************************************************************************
* Spine Runtimes Software License v2.5
*
* Copyright (c) 2013-2016, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable, and
* non-transferable license to use, install, execute, and perform the Spine
* Runtimes software and derivative works solely for personal or internal
* use. Without the written permission of Esoteric Software (see Section 2 of
* the Spine Software License Agreement), you may not (a) modify, translate,
* adapt, or develop new applications using the Spine Runtimes or otherwise
* create derivative works or improvements of the Spine Runtimes or (b) remove,
* delete, alter, or obscure any trademarks or any copyright, trademark, patent,
* or other intellectual property or proprietary rights notices on or in the
* Software, including any copy thereof. Redistributions in binary or source
* form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
* USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
// Contributed by: Mitch Thompson
#define SPINE_SKELETON_ANIMATOR
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.IO;
using Spine;
namespace Spine.Unity.Editor {
///
/// [SUPPORTS]
/// Linear, Constant, and Bezier Curves*
/// Inverse Kinematics*
/// Inherit Rotation
/// Translate Timeline
/// Rotate Timeline
/// Scale Timeline**
/// Event Timeline***
/// Attachment Timeline
///
/// RegionAttachment
/// MeshAttachment (optionally Skinned)
///
/// [LIMITATIONS]
/// *Bezier Curves are baked into the animation at 60fps and are not realtime. Use bakeIncrement constant to adjust key density if desired.
/// *Inverse Kinematics is baked into the animation at 60fps and are not realtime. Use bakeIncrement constant to adjust key density if desired.
/// ***Events may only fire 1 type of data per event in Unity safely so priority to String data if present in Spine key, otherwise a Float is sent whether the Spine key was Int or Float with priority given to Int.
///
/// [DOES NOT SUPPORT]
/// FFD (Unity does not provide access to BlendShapes with code)
/// Color Keys (Maybe one day when Unity supports full FBX standard and provides access with code)
/// Draw Order Keyframes
///
public static class SkeletonBaker {
#region SkeletonAnimator's Mecanim Clips
#if SPINE_SKELETON_ANIMATOR
public static void GenerateMecanimAnimationClips (SkeletonDataAsset skeletonDataAsset) {
var data = skeletonDataAsset.GetSkeletonData(true);
if (data == null) {
Debug.LogError("SkeletonData loading failed!", skeletonDataAsset);
return;
}
string dataPath = AssetDatabase.GetAssetPath(skeletonDataAsset);
string controllerPath = dataPath.Replace(SpineEditorUtilities.SkeletonDataSuffix, "_Controller").Replace(".asset", ".controller");
UnityEditor.Animations.AnimatorController controller;
if (skeletonDataAsset.controller != null) {
controller = (UnityEditor.Animations.AnimatorController)skeletonDataAsset.controller;
controllerPath = AssetDatabase.GetAssetPath(controller);
} else {
if (File.Exists(controllerPath)) {
if (EditorUtility.DisplayDialog("Controller Overwrite Warning", "Unknown Controller already exists at: " + controllerPath, "Update", "Overwrite")) {
controller = (UnityEditor.Animations.AnimatorController)AssetDatabase.LoadAssetAtPath(controllerPath, typeof(RuntimeAnimatorController));
} else {
controller = (UnityEditor.Animations.AnimatorController)UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
}
} else {
controller = (UnityEditor.Animations.AnimatorController)UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
}
}
skeletonDataAsset.controller = controller;
EditorUtility.SetDirty(skeletonDataAsset);
UnityEngine.Object[] objs = AssetDatabase.LoadAllAssetsAtPath(controllerPath);
var unityAnimationClipTable = new Dictionary();
var spineAnimationTable = new Dictionary();
foreach (var o in objs) {
//Debug.LogFormat("({0}){1} : {3} + {2} + {4}", o.GetType(), o.name, o.hideFlags, o.GetInstanceID(), o.GetHashCode());
// There is a bug in Unity 5.3.3 (and likely before) that creates
// a duplicate AnimationClip when you duplicate a Mecanim Animator State.
// These duplicates seem to be identifiable by their HideFlags, so we'll exclude them.
if (o is AnimationClip) {
var clip = o as AnimationClip;
if (!clip.HasFlag(HideFlags.HideInHierarchy)) {
if (unityAnimationClipTable.ContainsKey(clip.name)) {
Debug.LogWarningFormat("Duplicate AnimationClips were found named {0}", clip.name);
}
unityAnimationClipTable.Add(clip.name, clip);
}
}
}
foreach (var animations in data.Animations) {
string animationName = animations.Name; // Review for unsafe names. Requires runtime implementation too.
spineAnimationTable.Add(animationName, animations);
if (unityAnimationClipTable.ContainsKey(animationName) == false) {
AnimationClip newClip = new AnimationClip {
name = animationName
};
//AssetDatabase.CreateAsset(newClip, Path.GetDirectoryName(dataPath) + "/" + animationName + ".asset");
AssetDatabase.AddObjectToAsset(newClip, controller);
unityAnimationClipTable.Add(animationName, newClip);
}
AnimationClip clip = unityAnimationClipTable[animationName];
clip.SetCurve("", typeof(GameObject), "dummy", AnimationCurve.Linear(0, 0, animations.Duration, 0));
var settings = AnimationUtility.GetAnimationClipSettings(clip);
settings.stopTime = animations.Duration;
SetAnimationSettings(clip, settings);
AnimationUtility.SetAnimationEvents(clip, new AnimationEvent[0]);
foreach (Timeline t in animations.Timelines) {
if (t is EventTimeline)
ParseEventTimeline((EventTimeline)t, clip, SendMessageOptions.DontRequireReceiver);
}
EditorUtility.SetDirty(clip);
unityAnimationClipTable.Remove(animationName);
}
foreach (var clip in unityAnimationClipTable.Values) {
AnimationClip.DestroyImmediate(clip, true);
}
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
}
static bool HasFlag (this UnityEngine.Object o, HideFlags flagToCheck) {
return (o.hideFlags & flagToCheck) == flagToCheck;
}
#endif
#endregion
#region Prefab and AnimationClip Baking
///
/// Interval between key sampling for Bezier curves, IK controlled bones, and Inherit Rotation effected bones.
///
const float BakeIncrement = 1 / 60f;
public static void BakeToPrefab (SkeletonDataAsset skeletonDataAsset, ExposedList skins, string outputPath = "", bool bakeAnimations = true, bool bakeIK = true, SendMessageOptions eventOptions = SendMessageOptions.DontRequireReceiver) {
if (skeletonDataAsset == null || skeletonDataAsset.GetSkeletonData(true) == null) {
Debug.LogError("Could not export Spine Skeleton because SkeletonDataAsset is null or invalid!");
return;
}
if (outputPath == "") {
outputPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(skeletonDataAsset)) + "/Baked";
System.IO.Directory.CreateDirectory(outputPath);
}
var skeletonData = skeletonDataAsset.GetSkeletonData(true);
bool hasAnimations = bakeAnimations && skeletonData.Animations.Count > 0;
UnityEditor.Animations.AnimatorController controller = null;
if (hasAnimations) {
string controllerPath = outputPath + "/" + skeletonDataAsset.skeletonJSON.name + " Controller.controller";
bool newAnimContainer = false;
var runtimeController = AssetDatabase.LoadAssetAtPath(controllerPath, typeof(RuntimeAnimatorController));
if (runtimeController != null) {
controller = (UnityEditor.Animations.AnimatorController)runtimeController;
} else {
controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
newAnimContainer = true;
}
var existingClipTable = new Dictionary();
var unusedClipNames = new List();
Object[] animObjs = AssetDatabase.LoadAllAssetsAtPath(controllerPath);
foreach (Object o in animObjs) {
if (o is AnimationClip) {
var clip = (AnimationClip)o;
existingClipTable.Add(clip.name, clip);
unusedClipNames.Add(clip.name);
}
}
Dictionary> slotLookup = new Dictionary>();
int skinCount = skins.Count;
for (int s = 0; s < skeletonData.Slots.Count; s++) {
List attachmentNames = new List();
for (int i = 0; i < skinCount; i++) {
var skin = skins.Items[i];
List temp = new List();
skin.FindNamesForSlot(s, temp);
foreach (string str in temp) {
if (!attachmentNames.Contains(str))
attachmentNames.Add(str);
}
}
slotLookup.Add(s, attachmentNames);
}
foreach (var anim in skeletonData.Animations) {
AnimationClip clip = null;
if (existingClipTable.ContainsKey(anim.Name)) {
clip = existingClipTable[anim.Name];
}
clip = ExtractAnimation(anim.Name, skeletonData, slotLookup, bakeIK, eventOptions, clip);
if (unusedClipNames.Contains(clip.name)) {
unusedClipNames.Remove(clip.name);
} else {
AssetDatabase.AddObjectToAsset(clip, controller);
controller.AddMotion(clip);
}
}
if (newAnimContainer) {
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(controllerPath, ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
} else {
foreach (string str in unusedClipNames) {
AnimationClip.DestroyImmediate(existingClipTable[str], true);
}
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(controllerPath, ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
}
}
foreach (var skin in skins) {
bool newPrefab = false;
string prefabPath = outputPath + "/" + skeletonDataAsset.skeletonJSON.name + " (" + skin.Name + ").prefab";
Object prefab = AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject));
if (prefab == null) {
prefab = PrefabUtility.CreateEmptyPrefab(prefabPath);
newPrefab = true;
}
Dictionary meshTable = new Dictionary();
List unusedMeshNames = new List();
Object[] assets = AssetDatabase.LoadAllAssetsAtPath(prefabPath);
foreach (var obj in assets) {
if (obj is Mesh) {
meshTable.Add(obj.name, (Mesh)obj);
unusedMeshNames.Add(obj.name);
}
}
GameObject prefabRoot = new GameObject("root");
Dictionary slotTable = new Dictionary();
Dictionary boneTable = new Dictionary();
List boneList = new List();
//create bones
for (int i = 0; i < skeletonData.Bones.Count; i++) {
var boneData = skeletonData.Bones.Items[i];
Transform boneTransform = new GameObject(boneData.Name).transform;
boneTransform.parent = prefabRoot.transform;
boneTable.Add(boneTransform.name, boneTransform);
boneList.Add(boneTransform);
}
for (int i = 0; i < skeletonData.Bones.Count; i++) {
var boneData = skeletonData.Bones.Items[i];
Transform boneTransform = boneTable[boneData.Name];
Transform parentTransform = null;
if (i > 0)
parentTransform = boneTable[boneData.Parent.Name];
else
parentTransform = boneTransform.parent;
boneTransform.parent = parentTransform;
boneTransform.localPosition = new Vector3(boneData.X, boneData.Y, 0);
var tm = boneData.TransformMode;
if (tm.InheritsRotation())
boneTransform.localRotation = Quaternion.Euler(0, 0, boneData.Rotation);
else
boneTransform.rotation = Quaternion.Euler(0, 0, boneData.Rotation);
if (tm.InheritsScale())
boneTransform.localScale = new Vector3(boneData.ScaleX, boneData.ScaleY, 1);
}
//create slots and attachments
for (int i = 0; i < skeletonData.Slots.Count; i++) {
var slotData = skeletonData.Slots.Items[i];
Transform slotTransform = new GameObject(slotData.Name).transform;
slotTransform.parent = prefabRoot.transform;
slotTable.Add(slotData.Name, slotTransform);
List attachments = new List();
List attachmentNames = new List();
skin.FindAttachmentsForSlot(i, attachments);
skin.FindNamesForSlot(i, attachmentNames);
if (skin != skeletonData.DefaultSkin) {
skeletonData.DefaultSkin.FindAttachmentsForSlot(i, attachments);
skeletonData.DefaultSkin.FindNamesForSlot(i, attachmentNames);
}
for (int a = 0; a < attachments.Count; a++) {
var attachment = attachments[a];
string attachmentName = attachmentNames[a];
string attachmentMeshName = "[" + slotData.Name + "] " + attachmentName;
Vector3 offset = Vector3.zero;
float rotation = 0;
Mesh mesh = null;
Material material = null;
bool isWeightedMesh = false;
if (meshTable.ContainsKey(attachmentMeshName))
mesh = meshTable[attachmentMeshName];
if (attachment is RegionAttachment) {
var regionAttachment = (RegionAttachment)attachment;
offset.x = regionAttachment.X;
offset.y = regionAttachment.Y;
rotation = regionAttachment.Rotation;
mesh = ExtractRegionAttachment(attachmentMeshName, regionAttachment, mesh);
material = attachment.GetMaterial();
unusedMeshNames.Remove(attachmentMeshName);
if (newPrefab || meshTable.ContainsKey(attachmentMeshName) == false)
AssetDatabase.AddObjectToAsset(mesh, prefab);
} else if (attachment is MeshAttachment) {
var meshAttachment = (MeshAttachment)attachment;
isWeightedMesh = (meshAttachment.Bones != null);
offset.x = 0;
offset.y = 0;
rotation = 0;
if (isWeightedMesh)
mesh = ExtractWeightedMeshAttachment(attachmentMeshName, meshAttachment, i, skeletonData, boneList, mesh);
else
mesh = ExtractMeshAttachment(attachmentMeshName, meshAttachment, mesh);
material = attachment.GetMaterial();
unusedMeshNames.Remove(attachmentMeshName);
if (newPrefab || meshTable.ContainsKey(attachmentMeshName) == false)
AssetDatabase.AddObjectToAsset(mesh, prefab);
} else
continue;
Transform attachmentTransform = new GameObject(attachmentName).transform;
attachmentTransform.parent = slotTransform;
attachmentTransform.localPosition = offset;
attachmentTransform.localRotation = Quaternion.Euler(0, 0, rotation);
if (isWeightedMesh) {
attachmentTransform.position = Vector3.zero;
attachmentTransform.rotation = Quaternion.identity;
var skinnedMeshRenderer = attachmentTransform.gameObject.AddComponent();
skinnedMeshRenderer.rootBone = boneList[0];
skinnedMeshRenderer.bones = boneList.ToArray();
skinnedMeshRenderer.sharedMesh = mesh;
} else {
attachmentTransform.gameObject.AddComponent().sharedMesh = mesh;
attachmentTransform.gameObject.AddComponent();
}
attachmentTransform.GetComponent().sharedMaterial = material;
attachmentTransform.GetComponent().sortingOrder = i;
if (attachmentName != slotData.AttachmentName)
attachmentTransform.gameObject.SetActive(false);
}
}
foreach (var slotData in skeletonData.Slots) {
Transform slotTransform = slotTable[slotData.Name];
slotTransform.parent = boneTable[slotData.BoneData.Name];
slotTransform.localPosition = Vector3.zero;
slotTransform.localRotation = Quaternion.identity;
slotTransform.localScale = Vector3.one;
}
if (hasAnimations) {
var animator = prefabRoot.AddComponent();
animator.applyRootMotion = false;
animator.runtimeAnimatorController = (RuntimeAnimatorController)controller;
EditorGUIUtility.PingObject(controller);
}
if (newPrefab) {
PrefabUtility.ReplacePrefab(prefabRoot, prefab, ReplacePrefabOptions.ConnectToPrefab);
} else {
foreach (string str in unusedMeshNames) {
Mesh.DestroyImmediate(meshTable[str], true);
}
PrefabUtility.ReplacePrefab(prefabRoot, prefab, ReplacePrefabOptions.ReplaceNameBased);
}
EditorGUIUtility.PingObject(prefab);
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
GameObject.DestroyImmediate(prefabRoot);
}
}
#region Attachment Baking
static Bone DummyBone;
static Slot DummySlot;
internal static Bone GetDummyBone () {
if (DummyBone != null)
return DummyBone;
SkeletonData skelData = new SkeletonData();
BoneData data = new BoneData(0, "temp", null) {
ScaleX = 1,
ScaleY = 1,
Length = 100
};
skelData.Bones.Add(data);
Skeleton skeleton = new Skeleton(skelData);
Bone bone = new Bone(data, skeleton, null);
bone.UpdateWorldTransform();
DummyBone = bone;
return DummyBone;
}
internal static Slot GetDummySlot () {
if (DummySlot != null)
return DummySlot;
Bone bone = GetDummyBone();
SlotData data = new SlotData(0, "temp", bone.Data);
Slot slot = new Slot(data, bone);
DummySlot = slot;
return DummySlot;
}
internal static Mesh ExtractRegionAttachment (string name, RegionAttachment attachment, Mesh mesh = null, bool centered = true) {
var bone = GetDummyBone();
if (centered) {
bone.X = -attachment.X;
bone.Y = -attachment.Y;
}
bone.UpdateWorldTransform();
Vector2[] uvs = ExtractUV(attachment.UVs);
float[] floatVerts = new float[8];
attachment.ComputeWorldVertices(bone, floatVerts, 0);
Vector3[] verts = ExtractVerts(floatVerts);
//unrotate verts now that they're centered
if (centered) {
for (int i = 0; i < verts.Length; i++)
verts[i] = Quaternion.Euler(0, 0, -attachment.Rotation) * verts[i];
}
int[] triangles = { 1, 3, 0, 2, 3, 1 };
Color color = attachment.GetColor();
if (mesh == null)
mesh = new Mesh();
mesh.triangles = new int[0];
mesh.vertices = verts;
mesh.uv = uvs;
mesh.triangles = triangles;
mesh.colors = new [] { color, color, color, color };
mesh.RecalculateBounds();
mesh.RecalculateNormals();
mesh.name = name;
return mesh;
}
internal static Mesh ExtractMeshAttachment (string name, MeshAttachment attachment, Mesh mesh = null) {
var slot = GetDummySlot();
slot.Bone.X = 0;
slot.Bone.Y = 0;
slot.Bone.UpdateWorldTransform();
Vector2[] uvs = ExtractUV(attachment.UVs);
float[] floatVerts = new float[attachment.WorldVerticesLength];
attachment.ComputeWorldVertices(slot, floatVerts);
Vector3[] verts = ExtractVerts(floatVerts);
int[] triangles = attachment.Triangles;
Color color = attachment.GetColor();
if (mesh == null)
mesh = new Mesh();
mesh.triangles = new int[0];
mesh.vertices = verts;
mesh.uv = uvs;
mesh.triangles = triangles;
Color[] colors = new Color[verts.Length];
for (int i = 0; i < verts.Length; i++)
colors[i] = color;
mesh.colors = colors;
mesh.RecalculateBounds();
mesh.RecalculateNormals();
mesh.name = name;
return mesh;
}
public class BoneWeightContainer {
public struct Pair {
public Transform bone;
public float weight;
public Pair (Transform bone, float weight) {
this.bone = bone;
this.weight = weight;
}
}
public List bones;
public List weights;
public List pairs;
public BoneWeightContainer () {
this.bones = new List();
this.weights = new List();
this.pairs = new List();
}
public void Add (Transform transform, float weight) {
bones.Add(transform);
weights.Add(weight);
pairs.Add(new Pair(transform, weight));
}
}
internal static Mesh ExtractWeightedMeshAttachment (string name, MeshAttachment attachment, int slotIndex, SkeletonData skeletonData, List boneList, Mesh mesh = null) {
if (!attachment.IsWeighted())
throw new System.ArgumentException("Mesh is not weighted.", "attachment");
Skeleton skeleton = new Skeleton(skeletonData);
skeleton.UpdateWorldTransform();
float[] floatVerts = new float[attachment.WorldVerticesLength];
attachment.ComputeWorldVertices(skeleton.Slots.Items[slotIndex], floatVerts);
Vector2[] uvs = ExtractUV(attachment.UVs);
Vector3[] verts = ExtractVerts(floatVerts);
int[] triangles = attachment.Triangles;
Color color = new Color(attachment.R, attachment.G, attachment.B, attachment.A);
mesh = mesh ?? new Mesh();
mesh.triangles = new int[0];
mesh.vertices = verts;
mesh.uv = uvs;
mesh.triangles = triangles;
Color[] colors = new Color[verts.Length];
for (int i = 0; i < verts.Length; i++)
colors[i] = color;
mesh.colors = colors;
mesh.name = name;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
// Handle weights and binding
var weightTable = new Dictionary();
var warningBuilder = new System.Text.StringBuilder();
int[] bones = attachment.Bones;
float[] weights = attachment.Vertices;
for (int w = 0, v = 0, b = 0, n = bones.Length; v < n; w += 2) {
int nn = bones[v++] + v;
for (; v < nn; v++, b += 3) {
Transform boneTransform = boneList[bones[v]];
int vIndex = w / 2;
BoneWeightContainer container;
if (weightTable.ContainsKey(vIndex))
container = weightTable[vIndex];
else {
container = new BoneWeightContainer();
weightTable.Add(vIndex, container);
}
float weight = weights[b + 2];
container.Add(boneTransform, weight);
}
}
BoneWeight[] boneWeights = new BoneWeight[weightTable.Count];
for (int i = 0; i < weightTable.Count; i++) {
BoneWeight bw = new BoneWeight();
var container = weightTable[i];
var pairs = container.pairs.OrderByDescending(pair => pair.weight).ToList();
for (int b = 0; b < pairs.Count; b++) {
if (b > 3) {
if (warningBuilder.Length == 0)
warningBuilder.Insert(0, "[Weighted Mesh: " + name + "]\r\nUnity only supports 4 weight influences per vertex! The 4 strongest influences will be used.\r\n");
warningBuilder.AppendFormat("{0} ignored on vertex {1}!\r\n", pairs[b].bone.name, i);
continue;
}
int boneIndex = boneList.IndexOf(pairs[b].bone);
float weight = pairs[b].weight;
switch (b) {
case 0:
bw.boneIndex0 = boneIndex;
bw.weight0 = weight;
break;
case 1:
bw.boneIndex1 = boneIndex;
bw.weight1 = weight;
break;
case 2:
bw.boneIndex2 = boneIndex;
bw.weight2 = weight;
break;
case 3:
bw.boneIndex3 = boneIndex;
bw.weight3 = weight;
break;
}
}
boneWeights[i] = bw;
}
Matrix4x4[] bindPoses = new Matrix4x4[boneList.Count];
for (int i = 0; i < boneList.Count; i++) {
bindPoses[i] = boneList[i].worldToLocalMatrix;
}
mesh.boneWeights = boneWeights;
mesh.bindposes = bindPoses;
string warningString = warningBuilder.ToString();
if (warningString.Length > 0)
Debug.LogWarning(warningString);
return mesh;
}
internal static Vector2[] ExtractUV (float[] floats) {
Vector2[] arr = new Vector2[floats.Length / 2];
for (int i = 0; i < floats.Length; i += 2) {
arr[i / 2] = new Vector2(floats[i], floats[i + 1]);
}
return arr;
}
internal static Vector3[] ExtractVerts (float[] floats) {
Vector3[] arr = new Vector3[floats.Length / 2];
for (int i = 0; i < floats.Length; i += 2) {
arr[i / 2] = new Vector3(floats[i], floats[i + 1], 0);// *scale;
}
return arr;
}
#endregion
#region Animation Baking
static AnimationClip ExtractAnimation (string name, SkeletonData skeletonData, Dictionary> slotLookup, bool bakeIK, SendMessageOptions eventOptions, AnimationClip clip = null) {
var animation = skeletonData.FindAnimation(name);
var timelines = animation.Timelines;
if (clip == null) {
clip = new AnimationClip();
} else {
clip.ClearCurves();
AnimationUtility.SetAnimationEvents(clip, new AnimationEvent[0]);
}
clip.name = name;
Skeleton skeleton = new Skeleton(skeletonData);
List ignoreRotateTimelineIndexes = new List();
if (bakeIK) {
foreach (IkConstraint i in skeleton.IkConstraints) {
foreach (Bone b in i.Bones) {
int index = skeleton.FindBoneIndex(b.Data.Name);
ignoreRotateTimelineIndexes.Add(index);
BakeBoneConstraints(b, animation, clip);
}
}
}
foreach (Bone b in skeleton.Bones) {
if (!b.Data.TransformMode.InheritsRotation()) {
int index = skeleton.FindBoneIndex(b.Data.Name);
if (ignoreRotateTimelineIndexes.Contains(index) == false) {
ignoreRotateTimelineIndexes.Add(index);
BakeBoneConstraints(b, animation, clip);
}
}
}
foreach (Timeline t in timelines) {
skeleton.SetToSetupPose();
if (t is ScaleTimeline) {
ParseScaleTimeline(skeleton, (ScaleTimeline)t, clip);
} else if (t is TranslateTimeline) {
ParseTranslateTimeline(skeleton, (TranslateTimeline)t, clip);
} else if (t is RotateTimeline) {
//bypass any rotation keys if they're going to get baked anyway to prevent localEulerAngles vs Baked collision
if (ignoreRotateTimelineIndexes.Contains(((RotateTimeline)t).BoneIndex) == false)
ParseRotateTimeline(skeleton, (RotateTimeline)t, clip);
} else if (t is AttachmentTimeline) {
ParseAttachmentTimeline(skeleton, (AttachmentTimeline)t, slotLookup, clip);
} else if (t is EventTimeline) {
ParseEventTimeline((EventTimeline)t, clip, eventOptions);
}
}
var settings = AnimationUtility.GetAnimationClipSettings(clip);
settings.loopTime = true;
settings.stopTime = Mathf.Max(clip.length, 0.001f);
SetAnimationSettings(clip, settings);
clip.EnsureQuaternionContinuity();
EditorUtility.SetDirty(clip);
return clip;
}
static int BinarySearch (float[] values, float target) {
int low = 0;
int high = values.Length - 2;
if (high == 0) return 1;
int current = (int)((uint)high >> 1);
while (true) {
if (values[(current + 1)] <= target)
low = current + 1;
else
high = current;
if (low == high) return (low + 1);
current = (int)((uint)(low + high) >> 1);
}
}
static void BakeBoneConstraints (Bone bone, Spine.Animation animation, AnimationClip clip) {
Skeleton skeleton = bone.Skeleton;
bool inheritRotation = bone.Data.TransformMode.InheritsRotation();
animation.PoseSkeleton(skeleton, 0);
skeleton.UpdateWorldTransform();
float duration = animation.Duration;
AnimationCurve curve = new AnimationCurve();
List keys = new List();
float rotation = bone.AppliedRotation;
if (!inheritRotation)
rotation = GetUninheritedAppliedRotation(bone);
keys.Add(new Keyframe(0, rotation, 0, 0));
int listIndex = 1;
float r = rotation;
int steps = Mathf.CeilToInt(duration / BakeIncrement);
float currentTime = 0;
float angle = rotation;
for (int i = 1; i <= steps; i++) {
currentTime += BakeIncrement;
if (i == steps)
currentTime = duration;
animation.PoseSkeleton(skeleton, currentTime, true);
skeleton.UpdateWorldTransform();
int pIndex = listIndex - 1;
Keyframe pk = keys[pIndex];
pk = keys[pIndex];
rotation = inheritRotation ? bone.AppliedRotation : GetUninheritedAppliedRotation(bone);
angle += Mathf.DeltaAngle(angle, rotation);
r = angle;
float rOut = (r - pk.value) / (currentTime - pk.time);
pk.outTangent = rOut;
keys.Add(new Keyframe(currentTime, r, rOut, 0));
keys[pIndex] = pk;
listIndex++;
}
curve = EnsureCurveKeyCount(new AnimationCurve(keys.ToArray()));
string path = GetPath(bone.Data);
string propertyName = "localEulerAnglesBaked";
EditorCurveBinding xBind = EditorCurveBinding.FloatCurve(path, typeof(Transform), propertyName + ".x");
AnimationUtility.SetEditorCurve(clip, xBind, new AnimationCurve());
EditorCurveBinding yBind = EditorCurveBinding.FloatCurve(path, typeof(Transform), propertyName + ".y");
AnimationUtility.SetEditorCurve(clip, yBind, new AnimationCurve());
EditorCurveBinding zBind = EditorCurveBinding.FloatCurve(path, typeof(Transform), propertyName + ".z");
AnimationUtility.SetEditorCurve(clip, zBind, curve);
}
static void ParseTranslateTimeline (Skeleton skeleton, TranslateTimeline timeline, AnimationClip clip) {
var boneData = skeleton.Data.Bones.Items[timeline.BoneIndex];
var bone = skeleton.Bones.Items[timeline.BoneIndex];
AnimationCurve xCurve = new AnimationCurve();
AnimationCurve yCurve = new AnimationCurve();
AnimationCurve zCurve = new AnimationCurve();
float endTime = timeline.Frames[(timeline.FrameCount * 3) - 3];
float currentTime = timeline.Frames[0];
List xKeys = new List();
List yKeys = new List();
xKeys.Add(new Keyframe(timeline.Frames[0], timeline.Frames[1] + boneData.X, 0, 0));
yKeys.Add(new Keyframe(timeline.Frames[0], timeline.Frames[2] + boneData.Y, 0, 0));
int listIndex = 1;
int frameIndex = 1;
int f = 3;
float[] frames = timeline.Frames;
skeleton.SetToSetupPose();
float lastTime = 0;
while (currentTime < endTime) {
int pIndex = listIndex - 1;
float curveType = timeline.GetCurveType(frameIndex - 1);
if (curveType == 0) {
//linear
Keyframe px = xKeys[pIndex];
Keyframe py = yKeys[pIndex];
float time = frames[f];
float x = frames[f + 1] + boneData.X;
float y = frames[f + 2] + boneData.Y;
float xOut = (x - px.value) / (time - px.time);
float yOut = (y - py.value) / (time - py.time);
px.outTangent = xOut;
py.outTangent = yOut;
xKeys.Add(new Keyframe(time, x, xOut, 0));
yKeys.Add(new Keyframe(time, y, yOut, 0));
xKeys[pIndex] = px;
yKeys[pIndex] = py;
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
lastTime = time;
listIndex++;
} else if (curveType == 1) {
//stepped
Keyframe px = xKeys[pIndex];
Keyframe py = yKeys[pIndex];
float time = frames[f];
float x = frames[f + 1] + boneData.X;
float y = frames[f + 2] + boneData.Y;
float xOut = float.PositiveInfinity;
float yOut = float.PositiveInfinity;
px.outTangent = xOut;
py.outTangent = yOut;
xKeys.Add(new Keyframe(time, x, xOut, 0));
yKeys.Add(new Keyframe(time, y, yOut, 0));
xKeys[pIndex] = px;
yKeys[pIndex] = py;
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
lastTime = time;
listIndex++;
} else if (curveType == 2) {
//bezier
Keyframe px = xKeys[pIndex];
Keyframe py = yKeys[pIndex];
float time = frames[f];
int steps = Mathf.FloorToInt((time - px.time) / BakeIncrement);
for (int i = 1; i <= steps; i++) {
currentTime += BakeIncrement;
if (i == steps)
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
px = xKeys[listIndex - 1];
py = yKeys[listIndex - 1];
float xOut = (bone.X - px.value) / (currentTime - px.time);
float yOut = (bone.Y - py.value) / (currentTime - py.time);
px.outTangent = xOut;
py.outTangent = yOut;
xKeys.Add(new Keyframe(currentTime, bone.X, xOut, 0));
yKeys.Add(new Keyframe(currentTime, bone.Y, yOut, 0));
xKeys[listIndex - 1] = px;
yKeys[listIndex - 1] = py;
listIndex++;
lastTime = currentTime;
}
}
frameIndex++;
f += 3;
}
xCurve = EnsureCurveKeyCount(new AnimationCurve(xKeys.ToArray()));
yCurve = EnsureCurveKeyCount(new AnimationCurve(yKeys.ToArray()));
string path = GetPath(boneData);
const string propertyName = "localPosition";
clip.SetCurve(path, typeof(Transform), propertyName + ".x", xCurve);
clip.SetCurve(path, typeof(Transform), propertyName + ".y", yCurve);
clip.SetCurve(path, typeof(Transform), propertyName + ".z", zCurve);
}
static void ParseScaleTimeline (Skeleton skeleton, ScaleTimeline timeline, AnimationClip clip) {
var boneData = skeleton.Data.Bones.Items[timeline.BoneIndex];
var bone = skeleton.Bones.Items[timeline.BoneIndex];
AnimationCurve xCurve = new AnimationCurve();
AnimationCurve yCurve = new AnimationCurve();
AnimationCurve zCurve = new AnimationCurve();
float endTime = timeline.Frames[(timeline.FrameCount * 3) - 3];
float currentTime = timeline.Frames[0];
List xKeys = new List();
List yKeys = new List();
xKeys.Add(new Keyframe(timeline.Frames[0], timeline.Frames[1] * boneData.ScaleX, 0, 0));
yKeys.Add(new Keyframe(timeline.Frames[0], timeline.Frames[2] * boneData.ScaleY, 0, 0));
int listIndex = 1;
int frameIndex = 1;
int f = 3;
float[] frames = timeline.Frames;
skeleton.SetToSetupPose();
float lastTime = 0;
while (currentTime < endTime) {
int pIndex = listIndex - 1;
float curveType = timeline.GetCurveType(frameIndex - 1);
if (curveType == 0) {
//linear
Keyframe px = xKeys[pIndex];
Keyframe py = yKeys[pIndex];
float time = frames[f];
float x = frames[f + 1] * boneData.ScaleX;
float y = frames[f + 2] * boneData.ScaleY;
float xOut = (x - px.value) / (time - px.time);
float yOut = (y - py.value) / (time - py.time);
px.outTangent = xOut;
py.outTangent = yOut;
xKeys.Add(new Keyframe(time, x, xOut, 0));
yKeys.Add(new Keyframe(time, y, yOut, 0));
xKeys[pIndex] = px;
yKeys[pIndex] = py;
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
lastTime = time;
listIndex++;
} else if (curveType == 1) {
//stepped
Keyframe px = xKeys[pIndex];
Keyframe py = yKeys[pIndex];
float time = frames[f];
float x = frames[f + 1] * boneData.ScaleX;
float y = frames[f + 2] * boneData.ScaleY;
float xOut = float.PositiveInfinity;
float yOut = float.PositiveInfinity;
px.outTangent = xOut;
py.outTangent = yOut;
xKeys.Add(new Keyframe(time, x, xOut, 0));
yKeys.Add(new Keyframe(time, y, yOut, 0));
xKeys[pIndex] = px;
yKeys[pIndex] = py;
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
lastTime = time;
listIndex++;
} else if (curveType == 2) {
//bezier
Keyframe px = xKeys[pIndex];
Keyframe py = yKeys[pIndex];
float time = frames[f];
int steps = Mathf.FloorToInt((time - px.time) / BakeIncrement);
for (int i = 1; i <= steps; i++) {
currentTime += BakeIncrement;
if (i == steps)
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
px = xKeys[listIndex - 1];
py = yKeys[listIndex - 1];
float xOut = (bone.ScaleX - px.value) / (currentTime - px.time);
float yOut = (bone.ScaleY - py.value) / (currentTime - py.time);
px.outTangent = xOut;
py.outTangent = yOut;
xKeys.Add(new Keyframe(currentTime, bone.ScaleX, xOut, 0));
yKeys.Add(new Keyframe(currentTime, bone.ScaleY, yOut, 0));
xKeys[listIndex - 1] = px;
yKeys[listIndex - 1] = py;
listIndex++;
lastTime = currentTime;
}
}
frameIndex++;
f += 3;
}
xCurve = EnsureCurveKeyCount(new AnimationCurve(xKeys.ToArray()));
yCurve = EnsureCurveKeyCount(new AnimationCurve(yKeys.ToArray()));
string path = GetPath(boneData);
string propertyName = "localScale";
clip.SetCurve(path, typeof(Transform), propertyName + ".x", xCurve);
clip.SetCurve(path, typeof(Transform), propertyName + ".y", yCurve);
clip.SetCurve(path, typeof(Transform), propertyName + ".z", zCurve);
}
static void ParseRotateTimeline (Skeleton skeleton, RotateTimeline timeline, AnimationClip clip) {
var boneData = skeleton.Data.Bones.Items[timeline.BoneIndex];
var bone = skeleton.Bones.Items[timeline.BoneIndex];
AnimationCurve curve = new AnimationCurve();
float endTime = timeline.Frames[(timeline.FrameCount * 2) - 2];
float currentTime = timeline.Frames[0];
List keys = new List();
float rotation = timeline.Frames[1] + boneData.Rotation;
keys.Add(new Keyframe(timeline.Frames[0], rotation, 0, 0));
int listIndex = 1;
int frameIndex = 1;
int f = 2;
float[] frames = timeline.Frames;
skeleton.SetToSetupPose();
float lastTime = 0;
float angle = rotation;
while (currentTime < endTime) {
int pIndex = listIndex - 1;
float curveType = timeline.GetCurveType(frameIndex - 1);
if (curveType == 0) {
//linear
Keyframe pk = keys[pIndex];
float time = frames[f];
rotation = frames[f + 1] + boneData.Rotation;
angle += Mathf.DeltaAngle(angle, rotation);
float r = angle;
float rOut = (r - pk.value) / (time - pk.time);
pk.outTangent = rOut;
keys.Add(new Keyframe(time, r, rOut, 0));
keys[pIndex] = pk;
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
lastTime = time;
listIndex++;
} else if (curveType == 1) {
//stepped
Keyframe pk = keys[pIndex];
float time = frames[f];
rotation = frames[f + 1] + boneData.Rotation;
angle += Mathf.DeltaAngle(angle, rotation);
float r = angle;
float rOut = float.PositiveInfinity;
pk.outTangent = rOut;
keys.Add(new Keyframe(time, r, rOut, 0));
keys[pIndex] = pk;
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
lastTime = time;
listIndex++;
} else if (curveType == 2) {
//bezier
Keyframe pk = keys[pIndex];
float time = frames[f];
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
skeleton.UpdateWorldTransform();
rotation = frames[f + 1] + boneData.Rotation;
angle += Mathf.DeltaAngle(angle, rotation);
float r = angle;
int steps = Mathf.FloorToInt((time - pk.time) / BakeIncrement);
for (int i = 1; i <= steps; i++) {
currentTime += BakeIncrement;
if (i == steps)
currentTime = time;
timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixPose.Setup, MixDirection.In);
skeleton.UpdateWorldTransform();
pk = keys[listIndex - 1];
rotation = bone.Rotation;
angle += Mathf.DeltaAngle(angle, rotation);
r = angle;
float rOut = (r - pk.value) / (currentTime - pk.time);
pk.outTangent = rOut;
keys.Add(new Keyframe(currentTime, r, rOut, 0));
keys[listIndex - 1] = pk;
listIndex++;
lastTime = currentTime;
}
}
frameIndex++;
f += 2;
}
curve = EnsureCurveKeyCount(new AnimationCurve(keys.ToArray()));
string path = GetPath(boneData);
const string propertyName = "localEulerAnglesBaked";
EditorCurveBinding xBind = EditorCurveBinding.FloatCurve(path, typeof(Transform), propertyName + ".x");
AnimationUtility.SetEditorCurve(clip, xBind, new AnimationCurve());
EditorCurveBinding yBind = EditorCurveBinding.FloatCurve(path, typeof(Transform), propertyName + ".y");
AnimationUtility.SetEditorCurve(clip, yBind, new AnimationCurve());
EditorCurveBinding zBind = EditorCurveBinding.FloatCurve(path, typeof(Transform), propertyName + ".z");
AnimationUtility.SetEditorCurve(clip, zBind, curve);
}
static void ParseEventTimeline (EventTimeline timeline, AnimationClip clip, SendMessageOptions eventOptions) {
float[] frames = timeline.Frames;
var events = timeline.Events;
List animEvents = new List();
for (int i = 0; i < frames.Length; i++) {
var ev = events[i];
AnimationEvent ae = new AnimationEvent();
//MITCH: left todo: Deal with Mecanim's zero-time missed event
ae.time = frames[i];
ae.functionName = ev.Data.Name;
ae.messageOptions = eventOptions;
if (!string.IsNullOrEmpty(ev.String)) {
ae.stringParameter = ev.String;
} else {
if (ev.Int == 0 && ev.Float == 0) {
//do nothing, raw function
} else {
if (ev.Int != 0)
ae.floatParameter = (float)ev.Int;
else
ae.floatParameter = ev.Float;
}
}
animEvents.Add(ae);
}
AnimationUtility.SetAnimationEvents(clip, animEvents.ToArray());
}
static void ParseAttachmentTimeline (Skeleton skeleton, AttachmentTimeline timeline, Dictionary> slotLookup, AnimationClip clip) {
var attachmentNames = slotLookup[timeline.SlotIndex];
string bonePath = GetPath(skeleton.Slots.Items[timeline.SlotIndex].Bone.Data);
string slotPath = bonePath + "/" + skeleton.Slots.Items[timeline.SlotIndex].Data.Name;
Dictionary curveTable = new Dictionary();
foreach (string str in attachmentNames) {
curveTable.Add(str, new AnimationCurve());
}
float[] frames = timeline.Frames;
if (frames[0] != 0) {
string startingName = skeleton.Slots.Items[timeline.SlotIndex].Data.AttachmentName;
foreach (var pair in curveTable) {
if (startingName == "" || startingName == null) {
pair.Value.AddKey(new Keyframe(0, 0, float.PositiveInfinity, float.PositiveInfinity));
} else {
if (pair.Key == startingName) {
pair.Value.AddKey(new Keyframe(0, 1, float.PositiveInfinity, float.PositiveInfinity));
} else {
pair.Value.AddKey(new Keyframe(0, 0, float.PositiveInfinity, float.PositiveInfinity));
}
}
}
}
float currentTime = timeline.Frames[0];
float endTime = frames[frames.Length - 1];
int f = 0;
while (currentTime < endTime) {
float time = frames[f];
int frameIndex = (time >= frames[frames.Length - 1] ? frames.Length : BinarySearch(frames, time)) - 1;
string name = timeline.AttachmentNames[frameIndex];
foreach (var pair in curveTable) {
if (name == "") {
pair.Value.AddKey(new Keyframe(time, 0, float.PositiveInfinity, float.PositiveInfinity));
} else {
if (pair.Key == name) {
pair.Value.AddKey(new Keyframe(time, 1, float.PositiveInfinity, float.PositiveInfinity));
} else {
pair.Value.AddKey(new Keyframe(time, 0, float.PositiveInfinity, float.PositiveInfinity));
}
}
}
currentTime = time;
f += 1;
}
foreach (var pair in curveTable) {
string path = slotPath + "/" + pair.Key;
string prop = "m_IsActive";
clip.SetCurve(path, typeof(GameObject), prop, pair.Value);
}
}
static AnimationCurve EnsureCurveKeyCount (AnimationCurve curve) {
if (curve.length == 1)
curve.AddKey(curve.keys[0].time + 0.25f, curve.keys[0].value);
return curve;
}
static float GetUninheritedAppliedRotation (Bone b) {
Bone parent = b.Parent;
float angle = b.AppliedRotation;
while (parent != null) {
angle -= parent.AppliedRotation;
parent = parent.Parent;
}
return angle;
}
#endregion
#endregion
static string GetPath (BoneData b) {
return GetPathRecurse(b).Substring(1);
}
static string GetPathRecurse (BoneData b) {
if (b == null) return "";
return GetPathRecurse(b.Parent) + "/" + b.Name;
}
static void SetAnimationSettings (AnimationClip clip, AnimationClipSettings settings) {
AnimationUtility.SetAnimationClipSettings(clip, settings);
}
}
}