diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f5d8abb..638883106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -356,6 +356,7 @@ 3) Copy the original material, add *_Outline* to its name and set the shader to `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly`. 4) Assign this *_Outline* material at the `RenderExistingMesh` component under *Replacement Materials*. * Added `Outline Shaders URP` example scene to URP extension module to demonstrate the above additions. + * Added support for Unity's [`SpriteAtlas`](https://docs.unity3d.com/Manual/class-SpriteAtlas.html) as atlas provider (as an alternative to `.atlas.txt` and `.png` files) alongside a skeleton data file. There is now an additional `Spine SpriteAtlas Import` tool window accessible via `Window - Spine - SpriteAtlas Import`. Additional information can be found in a new section on the [spine-unity documentation page](http://esotericsoftware.com/spine-unity#Advanced---Using-Unity-SpriteAtlas-as-Atlas-Provider). * **Changes of default values** * `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`. diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index e6d172aad..e0107fb84 100644 --- a/spine-csharp/src/Animation.cs +++ b/spine-csharp/src/Animation.cs @@ -725,6 +725,7 @@ namespace Spine { slot.g += (slotData.g - slot.g) * alpha; slot.b += (slotData.b - slot.b) * alpha; slot.a += (slotData.a - slot.a) * alpha; + slot.ClampColor(); return; } return; @@ -758,6 +759,7 @@ namespace Spine { slot.g = g; slot.b = b; slot.a = a; + slot.ClampColor(); } else { float br, bg, bb, ba; if (blend == MixBlend.Setup) { @@ -775,6 +777,7 @@ namespace Spine { slot.g = bg + ((g - bg) * alpha); slot.b = bb + ((b - bb) * alpha); slot.a = ba + ((a - ba) * alpha); + slot.ClampColor(); } } } @@ -839,18 +842,22 @@ namespace Spine { slot.g = slotData.g; slot.b = slotData.b; slot.a = slotData.a; + slot.ClampColor(); slot.r2 = slotData.r2; slot.g2 = slotData.g2; slot.b2 = slotData.b2; + slot.ClampSecondColor(); return; case MixBlend.First: slot.r += (slot.r - slotData.r) * alpha; slot.g += (slot.g - slotData.g) * alpha; slot.b += (slot.b - slotData.b) * alpha; slot.a += (slot.a - slotData.a) * alpha; + slot.ClampColor(); slot.r2 += (slot.r2 - slotData.r2) * alpha; slot.g2 += (slot.g2 - slotData.g2) * alpha; slot.b2 += (slot.b2 - slotData.b2) * alpha; + slot.ClampSecondColor(); return; } return; @@ -893,9 +900,11 @@ namespace Spine { slot.g = g; slot.b = b; slot.a = a; + slot.ClampColor(); slot.r2 = r2; slot.g2 = g2; slot.b2 = b2; + slot.ClampSecondColor(); } else { float br, bg, bb, ba, br2, bg2, bb2; if (blend == MixBlend.Setup) { @@ -919,9 +928,11 @@ namespace Spine { slot.g = bg + ((g - bg) * alpha); slot.b = bb + ((b - bb) * alpha); slot.a = ba + ((a - ba) * alpha); + slot.ClampColor(); slot.r2 = br2 + ((r2 - br2) * alpha); slot.g2 = bg2 + ((g2 - bg2) * alpha); slot.b2 = bb2 + ((b2 - bb2) * alpha); + slot.ClampSecondColor(); } } @@ -970,7 +981,6 @@ namespace Spine { public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { - string attachmentName; Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; if (direction == MixDirection.Out) { diff --git a/spine-csharp/src/Slot.cs b/spine-csharp/src/Slot.cs index 1c1c1fb34..c7e45a886 100644 --- a/spine-csharp/src/Slot.cs +++ b/spine-csharp/src/Slot.cs @@ -106,6 +106,13 @@ namespace Spine { /// color tinting. public float A { get { return a; } set { a = value; } } + public void ClampColor() { + r = MathUtils.Clamp(r, 0, 1); + g = MathUtils.Clamp(g, 0, 1); + b = MathUtils.Clamp(b, 0, 1); + a = MathUtils.Clamp(a, 0, 1); + } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. /// public float R2 { get { return r2; } set { r2 = value; } } @@ -118,6 +125,12 @@ namespace Spine { /// Whether R2 G2 B2 are used to tint the slot's attachment for two color tinting. False if two color tinting is not used. public bool HasSecondColor { get { return data.hasSecondColor; } set { data.hasSecondColor = value; } } + public void ClampSecondColor () { + r2 = MathUtils.Clamp(r2, 0, 1); + g2 = MathUtils.Clamp(g2, 0, 1); + b2 = MathUtils.Clamp(b2, 0, 1); + } + public Attachment Attachment { /// The current attachment for the slot, or null if the slot has no attachment. get { return attachment; } diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java index 7860c9f16..9d8b0f586 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java @@ -797,6 +797,48 @@ public class AnimationStateTests { System.exit(0); } + setup("looping", // 28 + expect(0, "start", 0, 0), // + expect(0, "event 0", 0, 0), // + expect(0, "event 14", 0.5f, 0.5f), // + expect(0, "event 30", 1, 1), // + expect(0, "complete", 1, 1), // + expect(0, "event 0", 1, 1), // + expect(0, "event 14", 1.5f, 1.5f), // + expect(0, "event 30", 2, 2), // + expect(0, "complete", 2, 2), // + expect(0, "event 0", 2, 2), // + expect(0, "event 14", 2.5f, 2.5f), // + expect(0, "end", 2.6f, 2.7f), // + expect(0, "dispose", 2.6f, 2.7f) // + ); + state.setAnimation(0, "events0", true).setTrackEnd(2.6f); + run(0.1f, 1000, null); + + setup("set next", // 29 + expect(0, "start", 0, 0), // + expect(0, "event 0", 0, 0), // + expect(0, "event 14", 0.5f, 0.5f), // + expect(0, "event 30", 1, 1), // + expect(0, "complete", 1, 1), // + expect(0, "interrupt", 1.1f, 1.1f), // + + expect(1, "start", 0.1f, 1.1f), // + expect(1, "event 0", 0.1f, 1.1f), // + + expect(0, "end", 1.1f, 1.2f), // + expect(0, "dispose", 1.1f, 1.2f), // + + expect(1, "event 14", 0.5f, 1.5f), // + expect(1, "event 30", 1, 2), // + expect(1, "complete", 1, 2), // + expect(1, "end", 1, 2.1f), // + expect(1, "dispose", 1, 2.1f) // + ); + state.setAnimation(0, "events0", false); + state.addAnimation(0, "events1", false, 0).setTrackEnd(1); + run(0.1f, 1000, null); + System.out.println("AnimationState tests passed."); } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java index a0f3317ca..b4485c4b7 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java @@ -168,7 +168,7 @@ public class Bone implements Updatable { b = pa * lb - pb * ld; c = pc * la + pd * lc; d = pc * lb + pd * ld; - break; + return; } case noScale: case noScaleOrReflection: { diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/Legacy/SpriteAttacher.cs b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/Legacy/SpriteAttacher.cs index 7334d8599..40a9856ca 100644 --- a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/Legacy/SpriteAttacher.cs +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/Legacy/SpriteAttacher.cs @@ -60,6 +60,8 @@ namespace Spine.Unity.Examples { if (applyPMA) { try { + if (sprite == null) + return; sprite.texture.GetPixel(0, 0); } catch (UnityException e) { Debug.LogFormat("Texture of {0} ({1}) is not read/write enabled. SpriteAttacher requires this in order to work with a SkeletonRenderer that renders premultiplied alpha. Please check the texture settings.", sprite.name, sprite.texture.name); @@ -124,7 +126,10 @@ namespace Spine.Unity.Examples { spineSlot = spineSlot ?? skeletonComponent.Skeleton.FindSlot(slot); Shader attachmentShader = applyPMA ? Shader.Find(DefaultPMAShader) : Shader.Find(DefaultStraightAlphaShader); - attachment = applyPMA ? sprite.ToRegionAttachmentPMAClone(attachmentShader) : sprite.ToRegionAttachment(SpriteAttacher.GetPageFor(sprite.texture, attachmentShader)); + if (sprite == null) + attachment = null; + else + attachment = applyPMA ? sprite.ToRegionAttachmentPMAClone(attachmentShader) : sprite.ToRegionAttachment(SpriteAttacher.GetPageFor(sprite.texture, attachmentShader)); } } diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef new file mode 100644 index 000000000..f7b1ac0ec --- /dev/null +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "spine-unity-examples-editor", + "references": [ + "spine-unity", + "spine-unity-examples" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef.meta b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef.meta new file mode 100644 index 000000000..8b616dccc --- /dev/null +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 39599136c72c0b64b925d3ff2885aecb +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs new file mode 100644 index 000000000..14451ee0b --- /dev/null +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs @@ -0,0 +1,153 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "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 LLC 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 + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using Spine; + +namespace Spine.Unity.Editor { + using Event = UnityEngine.Event; + + [CustomEditor(typeof(SpineSpriteAtlasAsset)), CanEditMultipleObjects] + public class SpineSpriteAtlasAssetInspector : UnityEditor.Editor { + SerializedProperty atlasFile, materials; + SpineSpriteAtlasAsset atlasAsset; + + static List GetRegions (Atlas atlas) { + FieldInfo regionsField = SpineInspectorUtility.GetNonPublicField(typeof(Atlas), "regions"); + return (List)regionsField.GetValue(atlas); + } + + void OnEnable () { + SpineEditorUtilities.ConfirmInitialization(); + atlasFile = serializedObject.FindProperty("spriteAtlasFile"); + materials = serializedObject.FindProperty("materials"); + materials.isExpanded = true; + atlasAsset = (SpineSpriteAtlasAsset)target; + + if (!SpineSpriteAtlasAsset.AnySpriteAtlasNeedsRegionsLoaded()) + return; + EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted; + EditorApplication.update += SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted; + } + + void OnDisable () { + EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted; + } + + override public void OnInspectorGUI () { + if (serializedObject.isEditingMultipleObjects) { + DrawDefaultInspector(); + return; + } + + serializedObject.Update(); + atlasAsset = atlasAsset ?? (SpineSpriteAtlasAsset)target; + + if (atlasAsset.RegionsNeedLoading) { + if (GUILayout.Button(SpineInspectorUtility.TempContent("Load regions by entering Play mode"), GUILayout.Height(20))) { + EditorApplication.isPlaying = true; + } + } + + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(atlasFile); + EditorGUILayout.PropertyField(materials, true); + if (EditorGUI.EndChangeCheck()) { + serializedObject.ApplyModifiedProperties(); + atlasAsset.Clear(); + atlasAsset.GetAtlas(); + atlasAsset.updateRegionsInPlayMode = true; + } + + if (materials.arraySize == 0) { + EditorGUILayout.HelpBox("No materials", MessageType.Error); + return; + } + + for (int i = 0; i < materials.arraySize; i++) { + SerializedProperty prop = materials.GetArrayElementAtIndex(i); + var material = (Material)prop.objectReferenceValue; + if (material == null) { + EditorGUILayout.HelpBox("Materials cannot be null.", MessageType.Error); + return; + } + } + + if (atlasFile.objectReferenceValue != null) { + int baseIndent = EditorGUI.indentLevel; + + var regions = SpineSpriteAtlasAssetInspector.GetRegions(atlasAsset.GetAtlas()); + int regionsCount = regions.Count; + using (new EditorGUILayout.HorizontalScope()) { + EditorGUILayout.LabelField("Atlas Regions", EditorStyles.boldLabel); + EditorGUILayout.LabelField(string.Format("{0} regions total", regionsCount)); + } + AtlasPage lastPage = null; + for (int i = 0; i < regionsCount; i++) { + if (lastPage != regions[i].page) { + if (lastPage != null) { + EditorGUILayout.Separator(); + EditorGUILayout.Separator(); + } + lastPage = regions[i].page; + Material mat = ((Material)lastPage.rendererObject); + if (mat != null) { + EditorGUI.indentLevel = baseIndent; + using (new GUILayout.HorizontalScope()) + using (new EditorGUI.DisabledGroupScope(true)) + EditorGUILayout.ObjectField(mat, typeof(Material), false, GUILayout.Width(250)); + EditorGUI.indentLevel = baseIndent + 1; + } else { + EditorGUILayout.HelpBox("Page missing material!", MessageType.Warning); + } + } + + string regionName = regions[i].name; + Texture2D icon = SpineEditorUtilities.Icons.image; + if (regionName.EndsWith(" ")) { + regionName = string.Format("'{0}'", regions[i].name); + icon = SpineEditorUtilities.Icons.warning; + EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon, "Region name ends with whitespace. This may cause errors. Please check your source image filenames.")); + } else { + EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon)); + } + } + EditorGUI.indentLevel = baseIndent; + } + + if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current)) + atlasAsset.Clear(); + } + } + +} diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs.meta b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs.meta new file mode 100644 index 000000000..238789e90 --- /dev/null +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f063dc5ff6881db4a9ee2e059812cba2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs index a32a5d4b7..d95dfb0f2 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs @@ -43,6 +43,10 @@ #define NEW_PREFERENCES_SETTINGS_PROVIDER #endif +#if UNITY_2018_2_OR_NEWER +#define EXPOSES_SPRITE_ATLAS_UTILITIES +#endif + using UnityEngine; using UnityEditor; using System.Collections.Generic; @@ -60,6 +64,7 @@ namespace Spine.Unity.Editor { public const string SkeletonDataSuffix = "_SkeletonData"; public const string AtlasSuffix = "_Atlas"; + public const string SpriteAtlasSuffix = "_SpriteAtlas"; /// HACK: This list keeps the asset reference temporarily during importing. /// @@ -251,6 +256,7 @@ namespace Spine.Unity.Editor { bool reimport = false) { var atlasPaths = new List(); + var spriteAtlasPaths = new List(); var imagePaths = new List(); var skeletonPaths = new List(); CompatibilityProblemInfo compatibilityProblemInfo = null; @@ -267,6 +273,9 @@ namespace Spine.Unity.Editor { if (str.EndsWith(".atlas.txt", System.StringComparison.Ordinal)) atlasPaths.Add(str); break; + case ".spriteatlas": + spriteAtlasPaths.Add(str); + break; case ".png": case ".jpg": imagePaths.Add(str); @@ -294,7 +303,6 @@ namespace Spine.Unity.Editor { AtlasAssetBase atlas = IngestSpineAtlas(atlasText, texturesWithoutMetaFile); atlases.Add(atlas); } - AddDependentSkeletonIfAtlasChanged(skeletonPaths, atlasPaths); // Import skeletons and match them with atlases. @@ -575,6 +583,164 @@ namespace Spine.Unity.Editor { return (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAssetBase)); } + public static bool SpriteAtlasSettingsNeedAdjustment (UnityEngine.U2D.SpriteAtlas spriteAtlas) { + #if EXPOSES_SPRITE_ATLAS_UTILITIES + UnityEditor.U2D.SpriteAtlasPackingSettings packingSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetPackingSettings(spriteAtlas); + UnityEditor.U2D.SpriteAtlasTextureSettings textureSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetTextureSettings(spriteAtlas); + + bool areSettingsAsDesired = + packingSettings.enableRotation == true && + packingSettings.enableTightPacking == false && + textureSettings.readable == true && + textureSettings.generateMipMaps == false; + // note: platformSettings.textureCompression is always providing "Compressed", so we have to skip it. + return !areSettingsAsDesired; + #else + return false; + #endif + } + + public static bool AdjustSpriteAtlasSettings (UnityEngine.U2D.SpriteAtlas spriteAtlas) { + #if EXPOSES_SPRITE_ATLAS_UTILITIES + UnityEditor.U2D.SpriteAtlasPackingSettings packingSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetPackingSettings(spriteAtlas); + UnityEditor.U2D.SpriteAtlasTextureSettings textureSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetTextureSettings(spriteAtlas); + + packingSettings.enableRotation = true; + packingSettings.enableTightPacking = false; + UnityEditor.U2D.SpriteAtlasExtensions.SetPackingSettings(spriteAtlas, packingSettings); + + textureSettings.readable = true; + textureSettings.generateMipMaps = false; + UnityEditor.U2D.SpriteAtlasExtensions.SetTextureSettings(spriteAtlas, textureSettings); + + TextureImporterPlatformSettings platformSettings = new TextureImporterPlatformSettings(); + platformSettings.textureCompression = TextureImporterCompression.Uncompressed; + platformSettings.crunchedCompression = false; + UnityEditor.U2D.SpriteAtlasExtensions.SetPlatformSettings(spriteAtlas, platformSettings); + + string atlasPath = AssetDatabase.GetAssetPath(spriteAtlas); + Debug.Log(string.Format("Adjusted unsuitable SpriteAtlas settings '{0}'", atlasPath), spriteAtlas); + return false; + #else + return true; + #endif + } + + public static bool GeneratePngFromSpriteAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlas, out string texturePath) { + texturePath = System.IO.Path.ChangeExtension(AssetDatabase.GetAssetPath(spriteAtlas), ".png"); + if (spriteAtlas == null) + return false; + + Texture2D tempTexture = SpineSpriteAtlasAsset.AccessPackedTextureEditor(spriteAtlas); + if (tempTexture == null) + return false; + + byte[] bytes = null; + try { + bytes = tempTexture.EncodeToPNG(); + } + catch (System.Exception) { + // handled below + } + if (bytes == null || bytes.Length == 0) { + Debug.LogError("Could not read Compressed SpriteAtlas. Please enable 'Read/Write Enabled' and ensure 'Compression' is set to 'None' in Inspector.", spriteAtlas); + return false; + } + System.IO.File.WriteAllBytes(texturePath, bytes); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return System.IO.File.Exists(texturePath); + } + + public static AtlasAssetBase IngestSpriteAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlas, List texturesWithoutMetaFile) { + if (spriteAtlas == null) { + Debug.LogWarning("SpriteAtlas source cannot be null!"); + return null; + } + + if (SpriteAtlasSettingsNeedAdjustment(spriteAtlas)) { + // settings need to be adjusted via the 'Spine SpriteAtlas Import' window if you want to use it as a Spine atlas. + return null; + } + + Texture2D texture = null; + { // only one page file + string texturePath; + GeneratePngFromSpriteAtlas(spriteAtlas, out texturePath); + texture = AssetDatabase.LoadAssetAtPath(texturePath); + if (texture == null && System.IO.File.Exists(texturePath)) { + EditorUtility.SetDirty(spriteAtlas); + return null; // next iteration will load the texture as well. + } + } + + string primaryName = spriteAtlas.name; + string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spriteAtlas)).Replace('\\', '/'); + + string atlasPath = assetPath + "/" + primaryName + SpriteAtlasSuffix + ".asset"; + + SpineSpriteAtlasAsset atlasAsset = AssetDatabase.LoadAssetAtPath(atlasPath); + + List vestigialMaterials = new List(); + + if (atlasAsset == null) + atlasAsset = SpineSpriteAtlasAsset.CreateInstance(); + else { + foreach (Material m in atlasAsset.materials) + vestigialMaterials.Add(m); + } + + protectFromStackGarbageCollection.Add(atlasAsset); + atlasAsset.spriteAtlasFile = spriteAtlas; + + int pagesCount = 1; + var populatingMaterials = new List(pagesCount); + + { + string pageName = "SpriteAtlas"; + + string materialPath = assetPath + "/" + primaryName + "_" + pageName + ".mat"; + Material mat = AssetDatabase.LoadAssetAtPath(materialPath); + + if (mat == null) { + mat = new Material(Shader.Find(SpineEditorUtilities.Preferences.defaultShader)); + ApplyPMAOrStraightAlphaSettings(mat, SpineEditorUtilities.Preferences.textureSettingsReference); + AssetDatabase.CreateAsset(mat, materialPath); + } + else { + vestigialMaterials.Remove(mat); + } + + if (texture != null) + mat.mainTexture = texture; + + EditorUtility.SetDirty(mat); + // note: don't call AssetDatabase.SaveAssets() since this would trigger OnPostprocessAllAssets() every time unnecessarily. + populatingMaterials.Add(mat); //atlasAsset.materials[i] = mat; + } + + atlasAsset.materials = populatingMaterials.ToArray(); + + for (int i = 0; i < vestigialMaterials.Count; i++) + AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(vestigialMaterials[i])); + + if (AssetDatabase.GetAssetPath(atlasAsset) == "") + AssetDatabase.CreateAsset(atlasAsset, atlasPath); + else + atlasAsset.Clear(); + + atlasAsset.GetAtlas(); + atlasAsset.updateRegionsInPlayMode = true; + + EditorUtility.SetDirty(atlasAsset); + AssetDatabase.SaveAssets(); + + Debug.Log(string.Format("{0} :: Imported with {1} material", atlasAsset.name, atlasAsset.materials.Length), atlasAsset); + + protectFromStackGarbageCollection.Remove(atlasAsset); + return (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAssetBase)); + } + static bool SetDefaultTextureSettings (string texturePath, SpineAtlasAsset atlasAsset) { TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath); if (texImporter == null) { @@ -595,7 +761,7 @@ namespace Spine.Unity.Editor { return true; } - #if NEW_PREFERENCES_SETTINGS_PROVIDER +#if NEW_PREFERENCES_SETTINGS_PROVIDER static bool SetReferenceTextureSettings (string texturePath, SpineAtlasAsset atlasAsset, string referenceAssetPath) { var texturePreset = AssetDatabase.LoadAssetAtPath(referenceAssetPath); bool isTexturePreset = texturePreset != null && texturePreset.GetTargetTypeName() == "TextureImporter"; @@ -613,7 +779,7 @@ namespace Spine.Unity.Editor { AssetDatabase.SaveAssets(); return true; } - #else +#else static bool SetReferenceTextureSettings (string texturePath, SpineAtlasAsset atlasAsset, string referenceAssetPath) { TextureImporter reference = TextureImporter.GetAtPath(referenceAssetPath) as TextureImporter; if (reference == null) @@ -642,7 +808,7 @@ namespace Spine.Unity.Editor { AssetDatabase.SaveAssets(); return true; } - #endif +#endif static void ApplyPMAOrStraightAlphaSettings (Material material, string referenceTextureSettings) { bool isUsingPMAWorkflow = string.IsNullOrEmpty(referenceTextureSettings) || @@ -650,9 +816,9 @@ namespace Spine.Unity.Editor { MaterialChecks.EnablePMAAtMaterial(material, isUsingPMAWorkflow); } - #endregion +#endregion - #region Import SkeletonData (json or binary) +#region Import SkeletonData (json or binary) internal static string GetSkeletonDataAssetFilePath(TextAsset spineJson) { string primaryName = Path.GetFileNameWithoutExtension(spineJson.name); string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)).Replace('\\', '/'); @@ -996,7 +1162,7 @@ namespace Spine.Unity.Editor { return null; } - string spineGameObjectName = string.Format("Spine GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", "")); + string spineGameObjectName = string.Format("Spine GameObject ({0})", skeletonDataAsset.name.Replace(AssetUtility.SkeletonDataSuffix, "")); GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory, typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation)); SkeletonAnimation newSkeletonAnimation = go.GetComponent(); @@ -1025,19 +1191,19 @@ namespace Spine.Unity.Editor { /// Handles creating a new GameObject in the Unity Editor. This uses the new ObjectFactory API where applicable. public static GameObject NewGameObject (string name, bool useObjectFactory) { - #if NEW_PREFAB_SYSTEM +#if NEW_PREFAB_SYSTEM if (useObjectFactory) return ObjectFactory.CreateGameObject(name); - #endif +#endif return new GameObject(name); } /// Handles creating a new GameObject in the Unity Editor. This uses the new ObjectFactory API where applicable. public static GameObject NewGameObject (string name, bool useObjectFactory, params System.Type[] components) { - #if NEW_PREFAB_SYSTEM +#if NEW_PREFAB_SYSTEM if (useObjectFactory) return ObjectFactory.CreateGameObject(name, components); - #endif +#endif return new GameObject(name, components); } @@ -1075,7 +1241,7 @@ namespace Spine.Unity.Editor { return null; } - string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", "")); + string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace(AssetUtility.SkeletonDataSuffix, "")); GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory, typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonMecanim)); diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs new file mode 100644 index 000000000..3e6d86620 --- /dev/null +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs @@ -0,0 +1,170 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "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 LLC 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 + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +namespace Spine.Unity.Editor { + + using Editor = UnityEditor.Editor; + using Icons = SpineEditorUtilities.Icons; + + public class SpriteAtlasImportWindow : EditorWindow { + const bool IsUtilityWindow = false; + + [MenuItem("Window/Spine/SpriteAtlas Import", false, 5000)] + public static void Init (MenuCommand command) { + var window = EditorWindow.GetWindow(IsUtilityWindow); + window.minSize = new Vector2(284f, 256f); + window.maxSize = new Vector2(500f, 256f); + window.titleContent = new GUIContent("Spine SpriteAtlas Import", Icons.spine); + window.Show(); + } + + public UnityEngine.U2D.SpriteAtlas spriteAtlasAsset; + public TextAsset skeletonDataFile; + public SpineSpriteAtlasAsset spineSpriteAtlasAsset; + + SerializedObject so; + + void OnEnable () { + if (!SpineSpriteAtlasAsset.AnySpriteAtlasNeedsRegionsLoaded()) + return; + EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted; + EditorApplication.update += SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted; + } + + void OnDisable () { + EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted; + } + + void OnGUI () { + so = so ?? new SerializedObject(this); + + EditorGUIUtility.wideMode = true; + EditorGUILayout.LabelField("Spine SpriteAtlas Import", EditorStyles.boldLabel); + + using (new SpineInspectorUtility.BoxScope()) { + EditorGUI.BeginChangeCheck(); + var spriteAtlasAssetProperty = so.FindProperty("spriteAtlasAsset"); + EditorGUILayout.PropertyField(spriteAtlasAssetProperty, new GUIContent("SpriteAtlas", EditorGUIUtility.IconContent("SpriteAtlas Icon").image)); + if (EditorGUI.EndChangeCheck()) { + so.ApplyModifiedProperties(); + if (spriteAtlasAsset != null) { + if (AssetUtility.SpriteAtlasSettingsNeedAdjustment(spriteAtlasAsset)) { + AssetUtility.AdjustSpriteAtlasSettings(spriteAtlasAsset); + } + GenerateAssetsFromSpriteAtlas(spriteAtlasAsset); + } + } + + var spineSpriteAtlasAssetProperty = so.FindProperty("spineSpriteAtlasAsset"); + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(spineSpriteAtlasAssetProperty, new GUIContent("SpineSpriteAtlasAsset", EditorGUIUtility.IconContent("ScriptableObject Icon").image)); + if (spineSpriteAtlasAssetProperty.objectReferenceValue == null) { + spineSpriteAtlasAssetProperty.objectReferenceValue = spineSpriteAtlasAsset = FindSpineSpriteAtlasAsset(spriteAtlasAsset); + } + if (EditorGUI.EndChangeCheck()) { + so.ApplyModifiedProperties(); + } + EditorGUILayout.Space(); + + using (new EditorGUI.DisabledScope(spineSpriteAtlasAsset == null)) { + if (SpineInspectorUtility.LargeCenteredButton(new GUIContent("Load regions by entering Play mode"))) { + GenerateAssetsFromSpriteAtlas(spriteAtlasAsset); + SpineSpriteAtlasAsset.UpdateByStartingEditorPlayMode(); + } + } + + using (new SpineInspectorUtility.BoxScope()) { + if (spriteAtlasAsset == null) { + EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Please assign SpriteAtlas file.", Icons.warning), GUILayout.Height(46)); + } + else if (spineSpriteAtlasAsset == null || spineSpriteAtlasAsset.RegionsNeedLoading) { + EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Please hit 'Load regions ..' to load\nregion info. Play mode is started\nand stopped automatically.", Icons.warning), GUILayout.Height(54)); + } + else { + EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("SpriteAtlas imported\nsuccessfully.", Icons.spine), GUILayout.Height(46)); + } + } + } + + bool isAtlasComplete = (spineSpriteAtlasAsset != null && !spineSpriteAtlasAsset.RegionsNeedLoading); + bool canImportSkeleton = (spriteAtlasAsset != null && skeletonDataFile != null); + using (new SpineInspectorUtility.BoxScope()) { + + using (new EditorGUI.DisabledScope(!isAtlasComplete)) { + var skeletonDataAssetProperty = so.FindProperty("skeletonDataFile"); + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(skeletonDataAssetProperty, SpineInspectorUtility.TempContent("Skeleton json/skel file", Icons.spine)); + if (EditorGUI.EndChangeCheck()) { + so.ApplyModifiedProperties(); + } + EditorGUILayout.Space(); + } + using (new EditorGUI.DisabledScope(!canImportSkeleton)) { + if (SpineInspectorUtility.LargeCenteredButton(new GUIContent("Import Skeleton"))) { + //AssetUtility.IngestSpriteAtlas(spriteAtlasAsset, null); + string skeletonPath = AssetDatabase.GetAssetPath(skeletonDataFile); + string[] skeletons = new string[] { skeletonPath }; + AssetUtility.ImportSpineContent(skeletons, null); + } + } + } + } + + void GenerateAssetsFromSpriteAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlasAsset) { + AssetUtility.IngestSpriteAtlas(spriteAtlasAsset, null); + string texturePath; + if (AssetUtility.GeneratePngFromSpriteAtlas(spriteAtlasAsset, out texturePath)) { + Debug.Log(string.Format("Generated SpriteAtlas texture '{0}'", texturePath), spriteAtlasAsset); + } + } + + SpineSpriteAtlasAsset FindSpineSpriteAtlasAsset (UnityEngine.U2D.SpriteAtlas spriteAtlasAsset) { + string path = AssetDatabase.GetAssetPath(spriteAtlasAsset).Replace(".spriteatlas", AssetUtility.SpriteAtlasSuffix + ".asset"); + if (System.IO.File.Exists(path)) { + return AssetDatabase.LoadAssetAtPath(path); + } + return null; + } + + SkeletonDataAsset FindSkeletonDataAsset (TextAsset skeletonDataFile) { + string path = AssetDatabase.GetAssetPath(skeletonDataFile); + path = path.Replace(".json", AssetUtility.SkeletonDataSuffix + ".asset"); + path = path.Replace(".skel.bytes", AssetUtility.SkeletonDataSuffix + ".asset"); + if (System.IO.File.Exists(path)) { + return AssetDatabase.LoadAssetAtPath(path); + } + return null; + } + } +} diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs.meta b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs.meta new file mode 100644 index 000000000..2eb64bdcf --- /dev/null +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5b99b091defeef439a0cb8c99fd8a51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs new file mode 100644 index 000000000..12b1a7005 --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs @@ -0,0 +1,397 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "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 LLC 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 + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if UNITY_2018_2_OR_NEWER +#define EXPOSES_SPRITE_ATLAS_UTILITIES +#endif + +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using Spine; +using UnityEngine.U2D; + +#if UNITY_EDITOR +using UnityEditor; +using System.Reflection; +#endif + +namespace Spine.Unity { + /// Loads and stores a Spine atlas and list of materials. + [CreateAssetMenu(fileName = "New Spine SpriteAtlas Asset", menuName = "Spine/Spine SpriteAtlas Asset")] + public class SpineSpriteAtlasAsset : AtlasAssetBase { + public SpriteAtlas spriteAtlasFile; + public Material[] materials; + protected Atlas atlas; + public bool updateRegionsInPlayMode; + + [System.Serializable] + protected class SavedRegionInfo { + public float x, y, width, height; + public SpritePackingRotation packingRotation; + } + [SerializeField] protected SavedRegionInfo[] savedRegions; + + public override bool IsLoaded { get { return this.atlas != null; } } + + public override IEnumerable Materials { get { return materials; } } + public override int MaterialCount { get { return materials == null ? 0 : materials.Length; } } + public override Material PrimaryMaterial { get { return materials[0]; } } + + #if UNITY_EDITOR + static MethodInfo GetPackedSpritesMethod, GetPreviewTexturesMethod; + #if !EXPOSES_SPRITE_ATLAS_UTILITIES + static MethodInfo PackAtlasesMethod; + #endif + #endif + + #region Runtime Instantiation + /// + /// Creates a runtime AtlasAsset + public static SpineSpriteAtlasAsset CreateRuntimeInstance (SpriteAtlas spriteAtlasFile, Material[] materials, bool initialize) { + SpineSpriteAtlasAsset atlasAsset = ScriptableObject.CreateInstance(); + atlasAsset.Reset(); + atlasAsset.spriteAtlasFile = spriteAtlasFile; + atlasAsset.materials = materials; + + if (initialize) + atlasAsset.GetAtlas(); + + return atlasAsset; + } + #endregion + + void Reset () { + Clear(); + } + + public override void Clear () { + atlas = null; + } + + /// The atlas or null if it could not be loaded. + public override Atlas GetAtlas () { + if (spriteAtlasFile == null) { + Debug.LogError("SpriteAtlas file not set for SpineSpriteAtlasAsset: " + name, this); + Clear(); + return null; + } + + if (materials == null || materials.Length == 0) { + Debug.LogError("Materials not set for SpineSpriteAtlasAsset: " + name, this); + Clear(); + return null; + } + + if (atlas != null) return atlas; + + try { + atlas = LoadAtlas(spriteAtlasFile); + return atlas; + } catch (Exception ex) { + Debug.LogError("Error analyzing SpriteAtlas for SpineSpriteAtlasAsset: " + name + "\n" + ex.Message + "\n" + ex.StackTrace, this); + return null; + } + } + + protected void AssignRegionsFromSavedRegions (Sprite[] sprites, Atlas usedAtlas) { + + if (savedRegions == null || savedRegions.Length != sprites.Length) + return; + + int i = 0; + foreach (var region in usedAtlas) { + var savedRegion = savedRegions[i]; + var page = region.page; + + region.degrees = savedRegion.packingRotation == SpritePackingRotation.None ? 0 : 90; + region.rotate = region.degrees != 0; + + float x = savedRegion.x; + float y = savedRegion.y; + float width = savedRegion.width; + float height = savedRegion.height; + + region.u = x / (float)page.width; + region.v = y / (float)page.height; + if (region.rotate) { + region.u2 = (x + height) / (float)page.width; + region.v2 = (y + width) / (float)page.height; + } + else { + region.u2 = (x + width) / (float)page.width; + region.v2 = (y + height) / (float)page.height; + } + region.x = (int)x; + region.y = (int)y; + region.width = Math.Abs((int)width); + region.height = Math.Abs((int)height); + + // flip upside down + var temp = region.v; + region.v = region.v2; + region.v2 = temp; + + region.originalWidth = (int)width; + region.originalHeight = (int)height; + + // note: currently sprite pivot offsets are ignored. + // var sprite = sprites[i]; + region.offsetX = 0;//sprite.pivot.x; + region.offsetY = 0;//sprite.pivot.y; + + ++i; + } + } + + private Atlas LoadAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlas) { + + List pages = new List(); + List regions = new List(); + + Sprite[] sprites = new UnityEngine.Sprite[spriteAtlas.spriteCount]; + spriteAtlas.GetSprites(sprites); + if (sprites.Length == 0) + return new Atlas(pages, regions); + + Texture2D texture = null; + #if UNITY_EDITOR + if (!Application.isPlaying) + texture = AccessPackedTextureEditor(spriteAtlas); + else + #endif + texture = AccessPackedTexture(sprites); + + Material material = materials[0]; + #if !UNITY_EDITOR + material.mainTexture = texture; + #endif + + Spine.AtlasPage page = new AtlasPage(); + page.name = spriteAtlas.name; + page.width = texture.width; + page.height = texture.height; + page.format = Spine.Format.RGBA8888; + + page.minFilter = TextureFilter.Linear; + page.magFilter = TextureFilter.Linear; + page.uWrap = TextureWrap.ClampToEdge; + page.vWrap = TextureWrap.ClampToEdge; + page.rendererObject = material; + pages.Add(page); + + sprites = AccessPackedSprites(spriteAtlas); + + int i = 0; + for ( ; i < sprites.Length; ++i) { + var sprite = sprites[i]; + AtlasRegion region = new AtlasRegion(); + region.name = sprite.name.Replace("(Clone)", ""); + region.page = page; + region.degrees = sprite.packingRotation == SpritePackingRotation.None ? 0 : 90; + region.rotate = region.degrees != 0; + + region.u2 = 1; + region.v2 = 1; + region.width = page.width; + region.height = page.height; + region.originalWidth = page.width; + region.originalHeight = page.height; + + region.index = i; + regions.Add(region); + } + + var atlas = new Atlas(pages, regions); + AssignRegionsFromSavedRegions(sprites, atlas); + + return atlas; + } + +#if UNITY_EDITOR + public static void UpdateByStartingEditorPlayMode () { + EditorApplication.isPlaying = true; + } + + public static bool AnySpriteAtlasNeedsRegionsLoaded () { + string[] guids = UnityEditor.AssetDatabase.FindAssets("t:SpineSpriteAtlasAsset"); + foreach (var guid in guids) { + string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); + if (!string.IsNullOrEmpty(path)) { + var atlasAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (atlasAsset) { + if (atlasAsset.RegionsNeedLoading) + return true; + } + } + } + return false; + } + + public static void UpdateWhenEditorPlayModeStarted () { + if (!EditorApplication.isPlaying) + return; + + EditorApplication.update -= UpdateWhenEditorPlayModeStarted; + string[] guids = UnityEditor.AssetDatabase.FindAssets("t:SpineSpriteAtlasAsset"); + if (guids.Length == 0) + return; + + Debug.Log("Updating SpineSpriteAtlasAssets"); + foreach (var guid in guids) { + string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); + if (!string.IsNullOrEmpty(path)) { + var atlasAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (atlasAsset) { + atlasAsset.atlas = atlasAsset.LoadAtlas(atlasAsset.spriteAtlasFile); + atlasAsset.LoadRegionsInEditorPlayMode(); + Debug.Log(string.Format("Updated regions of '{0}'", atlasAsset.name), atlasAsset); + } + } + } + + EditorApplication.isPlaying = false; + } + + public bool RegionsNeedLoading { + get { return savedRegions == null || savedRegions.Length == 0 || updateRegionsInPlayMode; } + } + + public void LoadRegionsInEditorPlayMode () { + + Sprite[] sprites = null; + System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasExtensions,UnityEditor"); + var method = T.GetMethod("GetPackedSprites", BindingFlags.NonPublic | BindingFlags.Static); + if (method != null) { + object retval = method.Invoke(null, new object[] { spriteAtlasFile }); + var spritesArray = retval as Sprite[]; + if (spritesArray != null && spritesArray.Length > 0) { + sprites = spritesArray; + } + } + if (sprites == null) { + sprites = new UnityEngine.Sprite[spriteAtlasFile.spriteCount]; + spriteAtlasFile.GetSprites(sprites); + } + if (sprites.Length == 0) { + Debug.LogWarning(string.Format("SpriteAtlas '{0}' contains no sprites. Please make sure all assigned images are set to import type 'Sprite'.", spriteAtlasFile.name), spriteAtlasFile); + return; + } + else if (sprites[0].packingMode == SpritePackingMode.Tight) { + Debug.LogError(string.Format("SpriteAtlas '{0}': Tight packing is not supported. Please disable 'Tight Packing' in the SpriteAtlas Inspector.", spriteAtlasFile.name), spriteAtlasFile); + return; + } + + if (savedRegions == null || savedRegions.Length != sprites.Length) + savedRegions = new SavedRegionInfo[sprites.Length]; + + int i = 0; + foreach (var region in atlas) { + var sprite = sprites[i]; + var rect = sprite.textureRect; + float x = rect.min.x; + float y = rect.min.y; + float width = rect.width; + float height = rect.height; + + var savedRegion = new SavedRegionInfo(); + savedRegion.x = x; + savedRegion.y = y; + savedRegion.width = width; + savedRegion.height = height; + savedRegion.packingRotation = sprite.packingRotation; + savedRegions[i] = savedRegion; + + ++i; + } + updateRegionsInPlayMode = false; + AssignRegionsFromSavedRegions(sprites, atlas); + EditorUtility.SetDirty(this); + AssetDatabase.SaveAssets(); + } + + public static Texture2D AccessPackedTextureEditor (SpriteAtlas spriteAtlas) { + #if EXPOSES_SPRITE_ATLAS_UTILITIES + UnityEditor.U2D.SpriteAtlasUtility.PackAtlases(new SpriteAtlas[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget); + #else + /*if (PackAtlasesMethod == null) { + System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasUtility,UnityEditor"); + PackAtlasesMethod = T.GetMethod("PackAtlases", BindingFlags.NonPublic | BindingFlags.Static); + } + if (PackAtlasesMethod != null) { + PackAtlasesMethod.Invoke(null, new object[] { new SpriteAtlas[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget }); + }*/ + #endif + if (GetPreviewTexturesMethod == null) { + System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasExtensions,UnityEditor"); + GetPreviewTexturesMethod = T.GetMethod("GetPreviewTextures", BindingFlags.NonPublic | BindingFlags.Static); + } + if (GetPreviewTexturesMethod != null) { + object retval = GetPreviewTexturesMethod.Invoke(null, new object[] { spriteAtlas }); + var textures = retval as Texture2D[]; + if (textures.Length > 0) + return textures[0]; + } + return null; + } +#endif + public static Texture2D AccessPackedTexture (Sprite[] sprites) { + return sprites[0].texture; + } + + + public static Sprite[] AccessPackedSprites (UnityEngine.U2D.SpriteAtlas spriteAtlas) { + Sprite[] sprites = null; +#if UNITY_EDITOR + if (!Application.isPlaying) { + + if (GetPackedSpritesMethod == null) { + System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasExtensions,UnityEditor"); + GetPackedSpritesMethod = T.GetMethod("GetPackedSprites", BindingFlags.NonPublic | BindingFlags.Static); + } + if (GetPackedSpritesMethod != null) { + object retval = GetPackedSpritesMethod.Invoke(null, new object[] { spriteAtlas }); + var spritesArray = retval as Sprite[]; + if (spritesArray != null && spritesArray.Length > 0) { + sprites = spritesArray; + } + } + } +#endif + if (sprites == null) { + sprites = new UnityEngine.Sprite[spriteAtlas.spriteCount]; + spriteAtlas.GetSprites(sprites); + if (sprites.Length == 0) + return null; + } + return sprites; + } + } +} diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs.meta new file mode 100644 index 000000000..e61e24e07 --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce59897dd7e6cbc4690a05ebaf975dff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs index d98ca9b16..aaa950f81 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs @@ -417,9 +417,8 @@ namespace Spine.Unity { var interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer); layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0; if (!layerInfos.isLastFrameOfInterruption) { - layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count; - animator.GetNextAnimatorClipInfo(layer, interruptingClipInfos); + layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count; float oldTime = layerInfos.interruptingStateInfo.normalizedTime; float newTime = interruptingStateInfo.normalizedTime; layerInfos.interruptingClipTimeAddition = newTime - oldTime; diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs index 9543f2d15..51c77b574 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs @@ -29,7 +29,7 @@ using UnityEngine; using System.Collections.Generic; -using System.Collections; +using System; namespace Spine.Unity.AttachmentTools { @@ -537,7 +537,24 @@ namespace Spine.Unity.AttachmentTools { bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) { var spriteTexture = s.texture; - var r = s.textureRect; + Rect r; + if (!s.packed || s.packingMode == SpritePackingMode.Rectangle) { + r = s.textureRect; + } + else { + r = new Rect(); + r.xMin = Math.Min(s.uv[0].x, s.uv[1].x) * spriteTexture.width; + r.xMax = Math.Max(s.uv[0].x, s.uv[1].x) * spriteTexture.width; + r.yMin = Math.Min(s.uv[0].y, s.uv[2].y) * spriteTexture.height; + r.yMax = Math.Max(s.uv[0].y, s.uv[2].y) * spriteTexture.height; + #if UNITY_EDITOR + if (s.uv.Length > 4) { + Debug.LogError("When using a tightly packed SpriteAtlas with Spine, you may only access Sprites that are packed as 'FullRect' from it! " + + "You can either disable 'Tight Packing' at the whole SpriteAtlas, or change the single Sprite's TextureImporter Setting 'MeshType' to 'Full Rect'." + + "Sprite Asset: " + s.name, s); + } + #endif + } var newTexture = new Texture2D((int)r.width, (int)r.height, textureFormat, mipmaps, linear); newTexture.CopyTextureAttributesFrom(spriteTexture); if (applyPMA) diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/MaterialChecks.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/MaterialChecks.cs index 3d34346d5..ce905ea55 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/MaterialChecks.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/MaterialChecks.cs @@ -38,6 +38,7 @@ namespace Spine.Unity { static readonly int STRAIGHT_ALPHA_PARAM_ID = Shader.PropertyToID("_StraightAlphaInput"); static readonly string ALPHAPREMULTIPLY_ON_KEYWORD = "_ALPHAPREMULTIPLY_ON"; + static readonly string STRAIGHT_ALPHA_KEYWORD = "_STRAIGHT_ALPHA_INPUT"; public static readonly string kPMANotSupportedLinearMessage = "Warning: Premultiply-alpha atlas textures not supported in Linear color space!\n\nPlease\n" @@ -134,6 +135,10 @@ namespace Spine.Unity { public static void EnablePMAAtMaterial (Material material, bool enablePMA) { if (material.HasProperty(STRAIGHT_ALPHA_PARAM_ID)) { material.SetInt(STRAIGHT_ALPHA_PARAM_ID, enablePMA ? 0 : 1); + if (enablePMA) + material.DisableKeyword(STRAIGHT_ALPHA_KEYWORD); + else + material.EnableKeyword(STRAIGHT_ALPHA_KEYWORD); } else { if (enablePMA)