From 201f0bd7d58393e54b324ffa0b600e7d81bb83fd Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Wed, 22 Apr 2020 16:31:27 +0200 Subject: [PATCH 1/9] [unity] Fixed an out of bounds access in SkeletonMecanim when multiple interruptions occur. Closes #1661. --- .../Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 a74d09875..fb075070a 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; From a0c0db0f5a6b50bf7faa12930b39e483042ce9fa Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Fri, 24 Apr 2020 16:56:53 +0200 Subject: [PATCH 2/9] [csharp] Fixed ColorTimeline and TwoColorTimeline result colors not being clamped. Closes #1664. --- spine-csharp/src/Animation.cs | 11 +++++++++++ spine-csharp/src/Slot.cs | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index e6d172aad..8fff49ba0 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(); } } 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; } From 37d569414de7393c660066bad7e26fa79768c053 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Wed, 29 Apr 2020 16:08:29 +0200 Subject: [PATCH 3/9] [csharp] Minor cleanup: removed unused variable, added null tests to legacy SpriteAttacher class. --- spine-csharp/src/Animation.cs | 1 - .../Scripts/Sample Components/Legacy/SpriteAttacher.cs | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index 8fff49ba0..e0107fb84 100644 --- a/spine-csharp/src/Animation.cs +++ b/spine-csharp/src/Animation.cs @@ -981,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-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)); } } From 96bb3630f28682aced3fe7631dd699df053b1e3c Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Fri, 17 Apr 2020 19:25:55 +0200 Subject: [PATCH 4/9] [unity] Fixed a bug where _STRAIGHT_ALPHA_INPUT shader keyword was not set when default import settings are applied automatically (bool parameter was set, but the keyword would only be added when the Inspector is active). --- .../Spine/Runtime/spine-unity/Utility/MaterialChecks.cs | 5 +++++ 1 file changed, 5 insertions(+) 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) From d35550d4f42fdff3d6f913d8dd5b7ce21d970627 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Fri, 17 Apr 2020 19:31:28 +0200 Subject: [PATCH 5/9] [unity] Now supporting Unity's SpriteAtlas as atlas provider (instead of `.atlas.txt` and `.png` files) alongside a skeleton data file. Accessible via a new tool window "Window - Spine - SpriteAtlas Import". --- CHANGELOG.md | 1 + .../SpineSpriteAtlasAssetInspector.cs | 153 +++++++ .../SpineSpriteAtlasAssetInspector.cs.meta | 11 + .../Editor/Utility/AssetUtility.cs | 190 ++++++++- .../Editor/Windows/SpriteAtlasImportWindow.cs | 170 ++++++++ .../Windows/SpriteAtlasImportWindow.cs.meta | 11 + .../Asset Types/SpineSpriteAtlasAsset.cs | 394 ++++++++++++++++++ .../Asset Types/SpineSpriteAtlasAsset.cs.meta | 11 + .../spine-unity/Utility/AtlasUtilities.cs | 21 +- 9 files changed, 948 insertions(+), 14 deletions(-) create mode 100644 spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs create mode 100644 spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs.meta create mode 100644 spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs create mode 100644 spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs.meta create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 99250d5cc..187422cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,6 +227,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-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..71c057ff7 --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs @@ -0,0 +1,394 @@ +/****************************************************************************** + * 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]; + + 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/Utility/AtlasUtilities.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs index ad7b2e42f..d65480ff9 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 { @@ -531,7 +531,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) From 22c5144bdede7736e56d7c9ebc58fc54b282d9f0 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Wed, 29 Apr 2020 20:27:38 +0200 Subject: [PATCH 6/9] [unity] Fixed a build error (build to platform only) in 'Spine Examples' after package.json structures and asmdef files have been added. --- .../Editor/spine-unity-examples-editor.asmdef | 18 ++++++++++++++++++ .../spine-unity-examples-editor.asmdef.meta | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef create mode 100644 spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef.meta 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: From d499dba7c4bd184a3a56b017b59d9faf5230327c Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Wed, 29 Apr 2020 20:35:47 +0200 Subject: [PATCH 7/9] [unity] Unity SpriteAtlas support: Upon build to platform, the internally generated sprite atlas texture is used instead of the png file generated for the editor (to reduce additional memory usage). This completes Unity SpriteAtlas support, together with previous commit d35550d. Closes #940. --- .../Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs | 3 +++ 1 file changed, 3 insertions(+) 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 index 71c057ff7..12b1a7005 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs @@ -190,6 +190,9 @@ namespace Spine.Unity { texture = AccessPackedTexture(sprites); Material material = materials[0]; + #if !UNITY_EDITOR + material.mainTexture = texture; + #endif Spine.AtlasPage page = new AtlasPage(); page.name = spriteAtlas.name; From e649ef05b7fc552c9063e47ecda76dc93064000d Mon Sep 17 00:00:00 2001 From: Nathan Sweet Date: Fri, 1 May 2020 20:48:29 +0200 Subject: [PATCH 8/9] Added AnimationState listener tests for looping and queuing a second animation. --- .../spine/AnimationStateTests.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) 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."); } From de203d57155bc3edc936e67ed2d8ae802c25734c Mon Sep 17 00:00:00 2001 From: Nathan Sweet Date: Tue, 5 May 2020 14:44:22 +0200 Subject: [PATCH 9/9] Fixed TransformMode.noRotationOrReflection applying skeleton scale twice. This transform mode inherits scale from the parent, which will have skeleton scale, so should not have it applied again. closes #1668 --- .../spine-libgdx/src/com/esotericsoftware/spine/Bone.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e6ded39db..a789ad32d 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java @@ -169,7 +169,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: {