#pragma warning disable 0219 /****************************************************************************** * Spine Runtimes Software License * Version 2.1 * * Copyright (c) 2013, Esoteric Software * All rights reserved. * * You are granted a perpetual, non-exclusive, non-sublicensable and * non-transferable license to install, execute and perform the Spine Runtimes * Software (the "Software") solely for internal use. Without the written * permission of Esoteric Software (typically granted by licensing Spine), you * may not (a) modify, translate, adapt or otherwise create derivative works, * improvements of the Software or develop new applications using the Software * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) 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. *****************************************************************************/ /***************************************************************************** * Spine Editor Utilities created by Mitch Thompson * Full irrevocable rights and permissions granted to Esoteric Software *****************************************************************************/ using UnityEngine; using UnityEditor; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using Spine; [InitializeOnLoad] public class SpineEditorUtilities : AssetPostprocessor { public static class Icons { public static Texture2D skeleton; public static Texture2D nullBone; public static Texture2D bone; public static Texture2D poseBones; public static Texture2D boneNib; public static Texture2D slot; public static Texture2D skinPlaceholder; public static Texture2D image; public static Texture2D boundingBox; public static Texture2D mesh; public static Texture2D skin; public static Texture2D skinsRoot; public static Texture2D animation; public static Texture2D animationRoot; public static Texture2D spine; public static Texture2D _event; public static Texture2D constraintNib; public static Texture2D warning; public static Texture2D skeletonUtility; public static Texture2D hingeChain; public static Texture2D subMeshRenderer; public static Mesh boneMesh { get { if (_boneMesh == null) { _boneMesh = new Mesh(); _boneMesh.vertices = new Vector3[4] { Vector3.zero, new Vector3(-0.1f, 0.1f, 0), Vector3.up, new Vector3(0.1f, 0.1f, 0) }; _boneMesh.uv = new Vector2[4]; _boneMesh.triangles = new int[6]{0,1,2,2,3,0}; _boneMesh.RecalculateBounds(); _boneMesh.RecalculateNormals(); } return _boneMesh; } } internal static Mesh _boneMesh; public static Material boneMaterial { get { if (_boneMaterial == null) { #if UNITY_4_3 _boneMaterial = new Material(Shader.Find("Particles/Alpha Blended")); _boneMaterial.SetColor("_TintColor", new Color(0.4f, 0.4f, 0.4f, 0.25f)); #else _boneMaterial = new Material(Shader.Find("Spine/Bones")); _boneMaterial.SetColor("_Color", new Color(0.4f, 0.4f, 0.4f, 0.25f)); #endif } return _boneMaterial; } } internal static Material _boneMaterial; public static void Initialize () { skeleton = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-skeleton.png"); nullBone = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-null.png"); bone = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-bone.png"); poseBones = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-poseBones.png"); boneNib = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-boneNib.png"); slot = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-slot.png"); skinPlaceholder = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-skinPlaceholder.png"); image = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-image.png"); boundingBox = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-boundingBox.png"); mesh = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-mesh.png"); skin = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-skinPlaceholder.png"); skinsRoot = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-skinsRoot.png"); animation = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-animation.png"); animationRoot = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-animationRoot.png"); spine = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-spine.png"); _event = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-event.png"); constraintNib = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-constraintNib.png"); warning = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-warning.png"); skeletonUtility = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-skeletonUtility.png"); hingeChain = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-hingeChain.png"); subMeshRenderer = (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/icon-subMeshRenderer.png"); } } public static string editorPath = ""; public static string editorGUIPath = ""; static Dictionary skeletonRendererTable; static Dictionary skeletonUtilityBoneTable; public static float defaultScale = 0.01f; public static float defaultMix = 0.2f; public static string defaultShader = "Spine/Skeleton"; static SpineEditorUtilities () { DirectoryInfo rootDir = new DirectoryInfo(Application.dataPath); FileInfo[] files = rootDir.GetFiles("SpineEditorUtilities.cs", SearchOption.AllDirectories); editorPath = Path.GetDirectoryName(files[0].FullName.Replace("\\", "/").Replace(Application.dataPath, "Assets")); editorGUIPath = editorPath + "/GUI"; Icons.Initialize(); skeletonRendererTable = new Dictionary(); skeletonUtilityBoneTable = new Dictionary(); EditorApplication.hierarchyWindowChanged += HierarchyWindowChanged; EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI; HierarchyWindowChanged(); } static void HierarchyWindowChanged () { skeletonRendererTable.Clear(); skeletonUtilityBoneTable.Clear(); SkeletonRenderer[] arr = Object.FindObjectsOfType(); foreach (SkeletonRenderer r in arr) skeletonRendererTable.Add(r.gameObject.GetInstanceID(), r.gameObject); SkeletonUtilityBone[] boneArr = Object.FindObjectsOfType(); foreach (SkeletonUtilityBone b in boneArr) skeletonUtilityBoneTable.Add(b.gameObject.GetInstanceID(), b); } static void HierarchyWindowItemOnGUI (int instanceId, Rect selectionRect) { if (skeletonRendererTable.ContainsKey(instanceId)) { Rect r = new Rect(selectionRect); r.x = r.width - 15; r.width = 15; GUI.Label(r, Icons.spine); } else if (skeletonUtilityBoneTable.ContainsKey(instanceId)) { Rect r = new Rect(selectionRect); r.x -= 26; if (skeletonUtilityBoneTable[instanceId] != null) { if (skeletonUtilityBoneTable[instanceId].transform.childCount == 0) r.x += 13; r.y += 2; r.width = 13; r.height = 13; if (skeletonUtilityBoneTable[instanceId].mode == SkeletonUtilityBone.Mode.Follow) { GUI.DrawTexture(r, Icons.bone); } else { GUI.DrawTexture(r, Icons.poseBones); } } } } [MenuItem("Assets/Spine/Ingest")] static void IngestSpineProjectFromSelection () { TextAsset spineJson = null; TextAsset atlasText = null; List spineJsonList = new List(); foreach (UnityEngine.Object o in Selection.objects) { if (o.GetType() != typeof(TextAsset)) continue; string fileName = Path.GetFileName(AssetDatabase.GetAssetPath(o)); if (fileName.EndsWith(".json")) spineJson = (TextAsset)o; else if (fileName.EndsWith(".atlas.txt")) atlasText = (TextAsset)o; } if (spineJson == null) { EditorUtility.DisplayDialog("Error!", "Spine JSON file not found in selection!", "OK"); return; } string primaryName = Path.GetFileNameWithoutExtension(spineJson.name); string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)); if (atlasText == null) { string atlasPath = assetPath + "/" + primaryName + ".atlas.txt"; atlasText = (TextAsset)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(TextAsset)); } AtlasAsset atlasAsset = IngestSpineAtlas(atlasText); IngestSpineProject(spineJson, atlasAsset); } static void OnPostprocessAllAssets (string[] imported, string[] deleted, string[] moved, string[] movedFromAssetPaths) { //debug // return; AtlasAsset sharedAtlas = null; System.Array.Sort(imported); foreach (string str in imported) { if (Path.GetExtension(str).ToLower() == ".json") { TextAsset spineJson = (TextAsset)AssetDatabase.LoadAssetAtPath(str, typeof(TextAsset)); if (IsSpineJSON(spineJson)) { if (sharedAtlas != null) { string spinePath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)); string atlasPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(sharedAtlas)); if (spinePath != atlasPath) sharedAtlas = null; } SkeletonDataAsset data = AutoIngestSpineProject(spineJson, sharedAtlas); if (data == null) continue; sharedAtlas = data.atlasAsset; string dir = Path.GetDirectoryName(Path.GetDirectoryName(AssetDatabase.GetAssetPath(data))); string prefabPath = Path.Combine(dir, data.skeletonJSON.name + ".prefab").Replace("\\", "/"); if (File.Exists(prefabPath) == false) { SkeletonAnimation anim = SpawnAnimatedSkeleton(data); PrefabUtility.CreatePrefab(prefabPath, anim.gameObject, ReplacePrefabOptions.ReplaceNameBased); if (EditorApplication.isPlaying) GameObject.Destroy(anim.gameObject); else GameObject.DestroyImmediate(anim.gameObject); } else { } } } } } static bool IsSpineJSON (TextAsset asset) { object obj = Json.Deserialize(new StringReader(asset.text)); if (obj == null) { Debug.LogError("Is not valid JSON"); return false; } Dictionary root = (Dictionary)obj; if (!root.ContainsKey("skeleton")) return false; Dictionary skeletonInfo = (Dictionary)root["skeleton"]; string spineVersion = (string)skeletonInfo["spine"]; //TODO: reject old versions return true; } static SkeletonDataAsset AutoIngestSpineProject (TextAsset spineJson, Object atlasSource = null) { TextAsset atlasText = null; AtlasAsset atlasAsset = null; if (atlasSource != null) { if (atlasSource.GetType() == typeof(TextAsset)) { atlasText = (TextAsset)atlasSource; } else if (atlasSource.GetType() == typeof(AtlasAsset)) { atlasAsset = (AtlasAsset)atlasSource; } } if (atlasText == null && atlasAsset == null) { string primaryName = Path.GetFileNameWithoutExtension(spineJson.name); string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)); if (atlasText == null) { string atlasPath = assetPath + "/" + primaryName + ".atlas.txt"; atlasText = (TextAsset)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(TextAsset)); if (atlasText == null) { //can't find atlas, likely because using a shared atlas bool abort = !EditorUtility.DisplayDialog("Atlas not Found", "Expecting " + spineJson.name + ".atlas\n" + "Press OK to select Atlas", "OK", "Abort"); if (abort) { //do nothing, let it error later } else { string path = EditorUtility.OpenFilePanel("Find Atlas source...", Path.GetDirectoryName(Application.dataPath) + "/" + assetPath, "txt"); if (path != "") { path = path.Replace("\\", "/"); path = path.Replace(Application.dataPath.Replace("\\", "/"), "Assets"); atlasText = (TextAsset)AssetDatabase.LoadAssetAtPath(path, typeof(TextAsset)); } } } } } if (atlasAsset == null) atlasAsset = IngestSpineAtlas(atlasText); return IngestSpineProject(spineJson, atlasAsset); } static AtlasAsset IngestSpineAtlas (TextAsset atlasText) { if (atlasText == null) { Debug.LogWarning("Atlas source cannot be null!"); return null; } string primaryName = Path.GetFileNameWithoutExtension(atlasText.name).Replace(".atlas", ""); string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(atlasText)); string atlasPath = assetPath + "/" + primaryName + "_Atlas.asset"; AtlasAsset atlasAsset = (AtlasAsset)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAsset)); if (atlasAsset == null) atlasAsset = AtlasAsset.CreateInstance(); atlasAsset.atlasFile = atlasText; //strip CR string atlasStr = atlasText.text; atlasStr = atlasStr.Replace("\r", ""); string[] atlasLines = atlasStr.Split('\n'); List pageFiles = new List(); for (int i = 0; i < atlasLines.Length-1; i++) { if (atlasLines[i].Length == 0) pageFiles.Add(atlasLines[i + 1]); } atlasAsset.materials = new Material[pageFiles.Count]; for (int i = 0; i < pageFiles.Count; i++) { string texturePath = assetPath + "/" + pageFiles[i]; Texture2D texture = (Texture2D)AssetDatabase.LoadAssetAtPath(texturePath, typeof(Texture2D)); TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath); texImporter.textureFormat = TextureImporterFormat.AutomaticTruecolor; texImporter.mipmapEnabled = false; texImporter.alphaIsTransparency = false; texImporter.maxTextureSize = 2048; EditorUtility.SetDirty(texImporter); AssetDatabase.ImportAsset(texturePath); AssetDatabase.SaveAssets(); string pageName = Path.GetFileNameWithoutExtension(pageFiles[i]); //because this looks silly if (pageName == primaryName && pageFiles.Count == 1) pageName = "Material"; string materialPath = assetPath + "/" + primaryName + "_" + pageName + ".mat"; Material mat = (Material)AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material)); if (mat == null) { mat = new Material(Shader.Find(defaultShader)); AssetDatabase.CreateAsset(mat, materialPath); } mat.mainTexture = texture; EditorUtility.SetDirty(mat); AssetDatabase.SaveAssets(); atlasAsset.materials[i] = mat; } if (AssetDatabase.GetAssetPath(atlasAsset) == "") AssetDatabase.CreateAsset(atlasAsset, atlasPath); else atlasAsset.Reset(); AssetDatabase.SaveAssets(); return (AtlasAsset)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAsset)); } static SkeletonDataAsset IngestSpineProject (TextAsset spineJson, AtlasAsset atlasAsset = null) { string primaryName = Path.GetFileNameWithoutExtension(spineJson.name); string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)); string filePath = assetPath + "/" + primaryName + "_SkeletonData.asset"; if (spineJson != null && atlasAsset != null) { SkeletonDataAsset skelDataAsset = (SkeletonDataAsset)AssetDatabase.LoadAssetAtPath(filePath, typeof(SkeletonDataAsset)); if (skelDataAsset == null) { skelDataAsset = SkeletonDataAsset.CreateInstance(); skelDataAsset.atlasAsset = atlasAsset; skelDataAsset.skeletonJSON = spineJson; skelDataAsset.fromAnimation = new string[0]; skelDataAsset.toAnimation = new string[0]; skelDataAsset.duration = new float[0]; skelDataAsset.defaultMix = defaultMix; skelDataAsset.scale = defaultScale; AssetDatabase.CreateAsset(skelDataAsset, filePath); AssetDatabase.SaveAssets(); } else { skelDataAsset.Reset(); skelDataAsset.GetSkeletonData(true); } return skelDataAsset; } else { EditorUtility.DisplayDialog("Error!", "Must specify both Spine JSON and Atlas TextAsset", "OK"); return null; } } [MenuItem("Assets/Spine/Spawn")] static void SpawnAnimatedSkeleton () { Object[] arr = Selection.objects; foreach (Object o in arr) { string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(o)); string skinName = EditorPrefs.GetString(guid + "_lastSkin", ""); SpawnAnimatedSkeleton((SkeletonDataAsset)o, skinName); SceneView.RepaintAll(); } } [MenuItem("Assets/Spine/Spawn", true)] static bool ValidateSpawnAnimatedSkeleton () { Object[] arr = Selection.objects; if (arr.Length == 0) return false; foreach (Object o in arr) { if (o.GetType() != typeof(SkeletonDataAsset)) return false; } return true; } public static SkeletonAnimation SpawnAnimatedSkeleton (SkeletonDataAsset skeletonDataAsset, string skinName) { return SpawnAnimatedSkeleton(skeletonDataAsset, skeletonDataAsset.GetSkeletonData(true).FindSkin(skinName)); } public static SkeletonAnimation SpawnAnimatedSkeleton (SkeletonDataAsset skeletonDataAsset, Skin skin = null) { GameObject go = new GameObject(skeletonDataAsset.name.Replace("_SkeletonData", ""), typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation)); SkeletonAnimation anim = go.GetComponent(); anim.skeletonDataAsset = skeletonDataAsset; bool requiresNormals = false; foreach (Material m in anim.skeletonDataAsset.atlasAsset.materials) { if (m.shader.name.Contains("Lit")) { requiresNormals = true; break; } } anim.calculateNormals = requiresNormals; SkeletonData data = skeletonDataAsset.GetSkeletonData(true); if (data == null) { string reloadAtlasPath = AssetDatabase.GetAssetPath(skeletonDataAsset.atlasAsset); skeletonDataAsset.atlasAsset = (AtlasAsset)AssetDatabase.LoadAssetAtPath(reloadAtlasPath, typeof(AtlasAsset)); data = skeletonDataAsset.GetSkeletonData(true); } if (skin == null) skin = data.DefaultSkin; if (skin == null) skin = data.Skins[0]; anim.Reset(); anim.skeleton.SetSkin(skin); anim.initialSkinName = skin.Name; anim.skeleton.Update(1); anim.state.Update(1); anim.state.Apply(anim.skeleton); anim.skeleton.UpdateWorldTransform(); return anim; } }