/****************************************************************************** * Spine Runtimes License Agreement * Last updated July 28, 2023. Replaces all prior versions. * * Copyright (c) 2013-2023, 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_2017_2_OR_NEWER #define NEWPLAYMODECALLBACKS #endif #define SPINE_OPTIONAL_ON_DEMAND_LOADING #if SPINE_OPTIONAL_ON_DEMAND_LOADING using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Spine.Unity.Editor { /// /// Base class for GenericOnDemandTextureLoader Inspector subclasses. /// For reference, see the class available /// in the com.esotericsoftware.spine.addressables UPM package. /// /// The implementation struct which holds an on-demand loading reference /// to the target texture to be loaded, derived from ITargetTextureReference. /// The implementation struct covering a single texture loading request, /// derived from IOnDemandRequest [InitializeOnLoad] [CustomEditor(typeof(GenericOnDemandTextureLoader<,>)), CanEditMultipleObjects] public abstract class GenericOnDemandTextureLoaderInspector : UnityEditor.Editor where TargetReference : Spine.Unity.ITargetTextureReference where TextureRequest : Spine.Unity.IOnDemandRequest { protected SerializedProperty atlasAsset; protected SerializedProperty skeletonDataAsset; protected SerializedProperty maxPlaceholderSize; protected SerializedProperty placeholderMap; protected SerializedProperty unloadAfterSecondsUnused; static protected bool placeholdersFoldout = true; protected SerializedProperty loadedDataAtMaterial; protected GenericOnDemandTextureLoader loader; protected GUIContent placeholderTexturesLabel; /// /// Called via InitializeOnLoad attribute upon Editor startup or compilation. /// static GenericOnDemandTextureLoaderInspector () { RegisterPlayModeChangedCallbacks(); } public static void RegisterPlayModeChangedCallbacks () { #if NEWPLAYMODECALLBACKS EditorApplication.playModeStateChanged -= OnPlaymodeChanged; EditorApplication.playModeStateChanged += OnPlaymodeChanged; #else EditorApplication.playmodeStateChanged -= OnPlaymodeChanged; EditorApplication.playmodeStateChanged += OnPlaymodeChanged; #endif } /// /// Derive your implementation subclass of this class and implement the respective abstract methods. /// Note: Unfortunately the Unity menu entries are created via static methods, so this is a workaround /// to provide virtual static functions in old C# versions. /// public abstract class StaticMethodImplementations { public abstract GenericOnDemandTextureLoader GetOrCreateLoader (string loaderPath); /// /// Returns the on-demand loader asset's filename suffix. The filename /// is determined by the AtlasAsset, while this suffix replaces the "_Atlas" suffix. /// When set to e.g. "_Addressable", the loader asset created for /// the "Skeleton_Atlas" asset is named "Skeleton_Addressable". /// public virtual string LoaderSuffix { get { return "_Loader"; } } public abstract bool SetupOnDemandLoadingReference ( ref TargetReference targetTextureReference, Texture targetTexture); /// /// Create a context menu wrapper in the main class for this generic implementation using the code below. /// /// [MenuItem("CONTEXT/AtlasAssetBase/Add YourSubclass Loader")] /// static void AddYourSubclassLoader (MenuCommand cmd) { /// if (staticMethods == null) /// staticMethods = new YourSubclassMethodImplementations (); /// staticMethods.AddOnDemandLoader(cmd); /// } /// /// public virtual void AddOnDemandLoader (MenuCommand cmd) { AtlasAssetBase atlasAsset = cmd.context as AtlasAssetBase; Debug.Log("Adding On-Demand Loader for " + atlasAsset.name, atlasAsset); if (atlasAsset.OnDemandTextureLoader != null) { Debug.LogWarning("AtlasAsset On-Demand TextureLoader is already set. " + "Please clear it if you want to assign a different one."); return; } atlasAsset.TextureLoadingMode = AtlasAssetBase.LoadingMode.OnDemand; EditorUtility.SetDirty(atlasAsset); string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset); string loaderPath = atlasAssetPath.Replace(AssetUtility.AtlasSuffix, LoaderSuffix); GenericOnDemandTextureLoader loader = staticMethods.GetOrCreateLoader(loaderPath); staticMethods.SetupForAtlasAsset(loader, atlasAsset); EditorUtility.SetDirty(loader); AssetDatabase.SaveAssets(); } public virtual void SetupForAtlasAsset (GenericOnDemandTextureLoader loader, AtlasAssetBase atlasAsset) { if (loader.placeholderMap != null && loader.placeholderMap.Length > 0) { IEnumerable modifiedMaterials; loader.AssignTargetTextures(out modifiedMaterials); // start from normal textures } if (atlasAsset == null) { Debug.LogError("AddressableTextureLoader.SetupForAtlasAsset: atlasAsset was null, aborting setup.", atlasAsset); return; } int materialCount = atlasAsset.MaterialCount; loader.placeholderMap = new GenericOnDemandTextureLoader.PlaceholderMaterialMapping[materialCount]; GenericOnDemandTextureLoader.PlaceholderMaterialMapping[] materialMap = loader.placeholderMap; atlasAsset.OnDemandTextureLoader = loader; int maxPlaceholderSize = loader.maxPlaceholderSize; int i = 0; foreach (Material targetMaterial in atlasAsset.Materials) { Texture targetTexture = targetMaterial.mainTexture; materialMap[i].textures = new GenericOnDemandTextureLoader.PlaceholderTextureMapping[1]; // Todo: currently only main texture is supported. int textureIndex = 0; GenericOnDemandTextureLoader.PlaceholderTextureMapping[] texturesMap = materialMap[i].textures; if (texturesMap[textureIndex].placeholderTexture != targetTexture) { // otherwise already set to placeholder SetupOnDemandLoadingReference(ref texturesMap[textureIndex].targetTextureReference, targetTexture); texturesMap[textureIndex].placeholderTexture = CreatePlaceholderTextureFor(targetTexture, maxPlaceholderSize, loader); } ++i; } // assign late since CreatePlaceholderTextureFor(texture) method above might save assets and clear these values. loader.placeholderMap = materialMap; loader.atlasAsset = atlasAsset; if (loader.skeletonDataAsset == null) AssignSkeletonDataAsset(loader, atlasAsset); } protected void AssignSkeletonDataAsset (GenericOnDemandTextureLoader loader, AtlasAssetBase atlasAsset) { string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset); string parentFolder = System.IO.Path.GetDirectoryName(atlasAssetPath); SkeletonDataAsset skeletonDataAsset = FindSkeletonDataAsset(parentFolder, atlasAsset); if (skeletonDataAsset) { loader.skeletonDataAsset = skeletonDataAsset; return; } string nextParentFolder = System.IO.Path.GetDirectoryName(parentFolder); skeletonDataAsset = FindSkeletonDataAsset(nextParentFolder, atlasAsset); if (skeletonDataAsset) { loader.skeletonDataAsset = skeletonDataAsset; return; } } protected SkeletonDataAsset FindSkeletonDataAsset (string searchFolder, AtlasAssetBase atlasAsset) { string[] guids = AssetDatabase.FindAssets("t:SkeletonDataAsset", new[] { searchFolder }); foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); SkeletonDataAsset skeletonDataAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (skeletonDataAsset != null) { if (skeletonDataAsset.atlasAssets.Contains(atlasAsset)) { return skeletonDataAsset; } } } return null; } public virtual Texture CreatePlaceholderTextureFor (Texture originalTexture, int maxPlaceholderSize, GenericOnDemandTextureLoader loader) { const string AssetFolderName = "LoadingPlaceholderAssets"; string originalPath = AssetDatabase.GetAssetPath(originalTexture); string parentFolder = System.IO.Path.GetDirectoryName(originalPath); string dataPath = parentFolder + "/" + AssetFolderName; if (!AssetDatabase.IsValidFolder(dataPath)) { AssetDatabase.CreateFolder(parentFolder, AssetFolderName); } string originalTextureName = System.IO.Path.GetFileNameWithoutExtension(originalPath); string texturePath = string.Format("{0}/{1}.png", dataPath, loader.GetPlaceholderTextureName(originalTextureName)); Texture placeholderTexture = AssetDatabase.LoadAssetAtPath(texturePath); if (placeholderTexture == null) { AssetDatabase.CopyAsset(originalPath, texturePath); const bool resizePhysically = true; TextureImporter importer = (TextureImporter)TextureImporter.GetAtPath(texturePath); const string defaultPlatform = "Default"; TextureImporterPlatformSettings settings = importer.GetPlatformTextureSettings(defaultPlatform); settings.maxTextureSize = maxPlaceholderSize; importer.SetPlatformTextureSettings(settings); importer.maxTextureSize = maxPlaceholderSize; importer.isReadable = resizePhysically; importer.SaveAndReimport(); if (resizePhysically) { bool hasOverrides = TextureImporterUtility.DisableOverrides(importer, out List disabledPlatforms); Texture2D texture2D = AssetDatabase.LoadAssetAtPath(texturePath); if (texture2D) { Color[] maxTextureSizePixels = texture2D.GetPixels(); // SetPixels supports only uncompressed textures using certain formats. Texture2D uncompressedTexture = new Texture2D(texture2D.width, texture2D.height, TextureFormat.RGBA32, false); uncompressedTexture.SetPixels(maxTextureSizePixels); byte[] bytes = uncompressedTexture.EncodeToPNG(); string targetPath = Application.dataPath + "/../" + texturePath; System.IO.File.WriteAllBytes(targetPath, bytes); importer.isReadable = false; importer.SaveAndReimport(); EditorUtility.SetDirty(uncompressedTexture); AssetDatabase.SaveAssets(); } if (hasOverrides) TextureImporterUtility.EnableOverrides(importer, disabledPlatforms); } placeholderTexture = AssetDatabase.LoadAssetAtPath(texturePath); } UnityEngine.Object folderObject = AssetDatabase.LoadAssetAtPath(dataPath, typeof(UnityEngine.Object)); if (folderObject != null) { EditorGUIUtility.PingObject(folderObject); } return placeholderTexture; } } public static StaticMethodImplementations staticMethods; void OnEnable () { atlasAsset = serializedObject.FindProperty("atlasAsset"); skeletonDataAsset = serializedObject.FindProperty("skeletonDataAsset"); maxPlaceholderSize = serializedObject.FindProperty("maxPlaceholderSize"); placeholderMap = serializedObject.FindProperty("placeholderMap"); unloadAfterSecondsUnused = serializedObject.FindProperty("unloadAfterSecondsUnused"); loadedDataAtMaterial = serializedObject.FindProperty("loadedDataAtMaterial"); placeholderTexturesLabel = new GUIContent("Placeholder Textures"); loader = (GenericOnDemandTextureLoader)target; if (staticMethods == null) staticMethods = CreateStaticMethodImplementations(); } #if NEWPLAYMODECALLBACKS static void OnPlaymodeChanged (PlayModeStateChange mode) { bool assignTargetTextures = mode == PlayModeStateChange.EnteredEditMode; #else static void OnPlaymodeChanged () { bool assignTargetTextures = !Application.isPlaying; #endif if (assignTargetTextures) { AssignTargetTexturesAtAllLoaders(); } } public static void AssignTargetTexturesAtAllLoaders () { string[] loaderAssets = AssetDatabase.FindAssets("t:OnDemandTextureLoader"); foreach (string loaderAsset in loaderAssets) { string assetPath = AssetDatabase.GUIDToAssetPath(loaderAsset); OnDemandTextureLoader loader = AssetDatabase.LoadAssetAtPath(assetPath); AssignTargetTexturesAtLoader(loader); } } public static void AssignTargetTexturesAtLoader (OnDemandTextureLoader loader) { List placeholderMaterials; List nullTextureMaterials; bool anyPlaceholdersAssigned = loader.HasPlaceholderTexturesAssigned(out placeholderMaterials); bool anyMaterialNull = loader.HasNullMainTexturesAssigned(out nullTextureMaterials); if (anyPlaceholdersAssigned || anyMaterialNull) { Debug.Log("OnDemandTextureLoader detected placeholders assigned or null main textures at one or more materials. Resetting to target textures.", loader); AssetDatabase.StartAssetEditing(); IEnumerable modifiedMaterials; loader.AssignTargetTextures(out modifiedMaterials); if (placeholderMaterials != null) { foreach (Material placeholderMaterial in placeholderMaterials) { EditorUtility.SetDirty(placeholderMaterial); } } if (nullTextureMaterials != null) { foreach (Material nullTextureMaterial in nullTextureMaterials) { EditorUtility.SetDirty(nullTextureMaterial); } } AssetDatabase.StopAssetEditing(); AssetDatabase.SaveAssets(); } } /// /// Override this method in your implementation subclass as follows. /// /// protected override StaticMethodImplementations CreateStaticMethodImplementations () { /// return new YourStaticMethodImplementationsSubclass(); /// } /// /// protected abstract StaticMethodImplementations CreateStaticMethodImplementations (); /// Draws a single texture mapping entry in the Inspector. /// Can be overridden in subclasses where needed. Note that DrawSingleLineTargetTextureProperty /// can be overridden as well instead of overriding this method. /// Note that for the sake of space it should be drawn as a single line if possible. /// /// SerializedProperty pointing to a /// PlaceholderTextureMapping object of the placeholderMap array. protected virtual void DrawPlaceholderMapping (SerializedProperty textureMapping) { EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5)); var placeholderTextureProp = textureMapping.FindPropertyRelative("placeholderTexture"); var targetTextureProp = textureMapping.FindPropertyRelative("targetTextureReference"); GUILayout.Space(16f); EditorGUILayout.PropertyField(placeholderTextureProp, GUIContent.none); EditorGUIUtility.labelWidth = 1; // workaround since GUIContent.none below seems to be ignored DrawSingleLineTargetTextureProperty(targetTextureProp); EditorGUIUtility.labelWidth = 0; // change back to default EditorGUILayout.EndHorizontal(); } /// Draws a single texture mapping TargetReference in the Inspector. /// Can be overridden in subclasses where needed. Note that this method is /// called inside a horizontal Inspector line of a BeginHorizontal() / EndHorizontal() /// pair, so it is limited to approximately half Inspector width. /// /// SerializedProperty pointing to a /// TargetReference object of the PlaceholderTextureMapping entry. protected virtual void DrawSingleLineTargetTextureProperty (SerializedProperty property) { EditorGUILayout.PropertyField(property, GUIContent.none, true); } public override void OnInspectorGUI () { if (serializedObject.isEditingMultipleObjects) { DrawDefaultInspector(); return; } serializedObject.Update(); EditorGUILayout.PropertyField(atlasAsset); EditorGUILayout.PropertyField(skeletonDataAsset); EditorGUILayout.PropertyField(maxPlaceholderSize); EditorGUILayout.PropertyField(unloadAfterSecondsUnused); placeholdersFoldout = EditorGUILayout.Foldout(placeholdersFoldout, placeholderTexturesLabel, true); if (placeholdersFoldout) { for (int m = 0, materialCount = placeholderMap.arraySize; m < materialCount; ++m) { // line below equals: PlaceholderTextureMapping[] materialTextures = placeholderMap[m].textures; SerializedProperty materialTextures = placeholderMap.GetArrayElementAtIndex(m).FindPropertyRelative("textures"); for (int t = 0, textureCount = materialTextures.arraySize; t < textureCount; ++t) { // line below equals: PlaceholderTextureMapping textureMapping = materialTextures[t]; SerializedProperty textureMapping = materialTextures.GetArrayElementAtIndex(t); DrawPlaceholderMapping(textureMapping); } } } if (GUILayout.Button(new GUIContent("Regenerate", "Re-initialize the placeholder texture maps."), EditorStyles.miniButton, GUILayout.Width(160f))) ReinitPlaceholderTextures(loader); GUILayout.Space(16f); EditorGUILayout.LabelField("Testing", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5)); if (GUILayout.Button(new GUIContent("Assign Placeholders", "Assign placeholder textures (for testing)."), EditorStyles.miniButton, GUILayout.Width(160f))) AssignPlaceholderTextures(loader); if (GUILayout.Button(new GUIContent("Assign Normal Textures", "Re-assign target textures."), EditorStyles.miniButton, GUILayout.Width(160f))) AssignTargetTextures(loader); EditorGUILayout.EndHorizontal(); if (!Application.isPlaying) serializedObject.ApplyModifiedProperties(); } public void DeletePlaceholderTextures (GenericOnDemandTextureLoader loader) { foreach (var materialMap in loader.placeholderMap) { var textures = materialMap.textures; if (textures == null || textures.Length == 0) continue; Texture texture = textures[0].placeholderTexture; if (texture) AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(texture)); } loader.Clear(clearAtlasAsset: false); AssetDatabase.SaveAssets(); } public void ReinitPlaceholderTextures (GenericOnDemandTextureLoader loader) { AssignTargetTextures(loader); DeletePlaceholderTextures(loader); staticMethods.SetupForAtlasAsset(loader, loader.atlasAsset); EditorUtility.SetDirty(loader); AssetDatabase.SaveAssets(); } public bool AssignPlaceholderTextures (GenericOnDemandTextureLoader loader) { // re-setup placeholders to ensure the mapping is up to date. staticMethods.SetupForAtlasAsset(loader, loader.atlasAsset); IEnumerable modifiedMaterials; return loader.AssignPlaceholderTextures(out modifiedMaterials); } public bool AssignTargetTextures (GenericOnDemandTextureLoader loader) { IEnumerable modifiedMaterials; return loader.AssignTargetTextures(out modifiedMaterials); } } } #endif