Prechádzať zdrojové kódy

[unity] Implemented first experimental preview package version of delayed on-demand loading of Atlas assets. See #1890.

Harald Csaszar 1 rok pred
rodič
commit
76e85387d1
40 zmenil súbory, kde vykonal 1471 pridanie a 4 odobranie
  1. 9 1
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineAtlasAssetInspector.cs
  2. 71 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineBuildProcessor.cs
  3. 1 1
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineEditorUtilities.cs
  4. 35 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AtlasAssetBase.cs
  5. 90 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/OnDemandTextureLoader.cs
  6. 11 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/OnDemandTextureLoader.cs.meta
  7. 4 1
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineAtlasAsset.cs
  8. 22 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
  9. 1 1
      spine-unity/Assets/Spine/package.json
  10. 8 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Documentation.meta
  11. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Documentation/README.md
  12. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Documentation/README.md.meta
  13. 8 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor.meta
  14. 94 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/AddressablesTextureLoaderInspector.cs
  15. 11 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/AddressablesTextureLoaderInspector.cs.meta
  16. 25 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/spine-addressables-editor.asmdef
  17. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/spine-addressables-editor.asmdef.meta
  18. 26 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/LICENSE.md
  19. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/LICENSE.md.meta
  20. 8 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime.meta
  21. 96 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/AddressablesTextureLoader.cs
  22. 11 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/AddressablesTextureLoader.cs.meta
  23. 20 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/spine-addressables.asmdef
  24. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/spine-addressables.asmdef.meta
  25. 22 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/package.json
  26. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.addressables/package.json.meta
  27. 8 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor.meta
  28. 390 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/GenericOnDemandTextureLoaderInspector.cs
  29. 11 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/GenericOnDemandTextureLoaderInspector.cs.meta
  30. 20 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/spine-on-demand-loading-editor.asmdef
  31. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/spine-on-demand-loading-editor.asmdef.meta
  32. 26 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/LICENSE.md
  33. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/LICENSE.md.meta
  34. 8 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime.meta
  35. 316 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/GenericOnDemandTextureLoader.cs
  36. 11 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/GenericOnDemandTextureLoader.cs.meta
  37. 17 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/spine-on-demand-loading.asmdef
  38. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/spine-on-demand-loading.asmdef.meta
  39. 21 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/package.json
  40. 7 0
      spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/package.json.meta

+ 9 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineAtlasAssetInspector.cs

@@ -42,7 +42,7 @@ namespace Spine.Unity.Editor {
 
 	[CustomEditor(typeof(SpineAtlasAsset)), CanEditMultipleObjects]
 	public class SpineAtlasAssetInspector : UnityEditor.Editor {
-		SerializedProperty atlasFile, materials;
+		SerializedProperty atlasFile, materials, textureLoadingMode, onDemandTextureLoader;
 		SpineAtlasAsset atlasAsset;
 
 		GUIContent spriteSlicesLabel;
@@ -70,6 +70,8 @@ namespace Spine.Unity.Editor {
 			SpineEditorUtilities.ConfirmInitialization();
 			atlasFile = serializedObject.FindProperty("atlasFile");
 			materials = serializedObject.FindProperty("materials");
+			textureLoadingMode = serializedObject.FindProperty("textureLoadingMode");
+			onDemandTextureLoader = serializedObject.FindProperty("onDemandTextureLoader");
 			materials.isExpanded = true;
 			atlasAsset = (SpineAtlasAsset)target;
 #if REGION_BAKING_MESH
@@ -132,6 +134,12 @@ namespace Spine.Unity.Editor {
 				}
 			}
 
+			if (textureLoadingMode != null) {
+				EditorGUILayout.Space();
+				EditorGUILayout.PropertyField(textureLoadingMode);
+				EditorGUILayout.PropertyField(onDemandTextureLoader);
+			}
+
 			EditorGUILayout.Space();
 			if (SpineInspectorUtility.LargeCenteredButton(SpineInspectorUtility.TempContent("Set Mipmap Bias to " + SpinePreferences.DEFAULT_MIPMAPBIAS, tooltip: "This may help textures with mipmaps be less blurry when used for 2D sprites."))) {
 				foreach (Material m in atlasAsset.materials) {

+ 71 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineBuildProcessor.cs

@@ -46,6 +46,8 @@
 #define HAS_SAVE_ASSET_IF_DIRTY
 #endif
 
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
+
 using System.Collections.Generic;
 using UnityEditor;
 using UnityEditor.Build;
@@ -60,6 +62,9 @@ namespace Spine.Unity.Editor {
 
 #if HAS_ON_POSTPROCESS_PREFAB
 		static List<string> prefabsToRestore = new List<string>();
+#endif
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+		static List<string> textureLoadersToRestore = new List<string>();
 #endif
 		static Dictionary<string, string> spriteAtlasTexturesToRestore = new Dictionary<string, string>();
 
@@ -68,6 +73,9 @@ namespace Spine.Unity.Editor {
 #if HAS_ON_POSTPROCESS_PREFAB
 			if (SpineEditorUtilities.Preferences.removePrefabPreviewMeshes)
 				PreprocessSpinePrefabMeshes();
+#endif
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+			PreprocessOnDemandTextureLoaders();
 #endif
 			PreprocessSpriteAtlases();
 		}
@@ -77,6 +85,9 @@ namespace Spine.Unity.Editor {
 #if HAS_ON_POSTPROCESS_PREFAB
 			if (SpineEditorUtilities.Preferences.removePrefabPreviewMeshes)
 				PostprocessSpinePrefabMeshes();
+#endif
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+			PostprocessOnDemandTextureLoaders();
 #endif
 			PostprocessSpriteAtlases();
 		}
@@ -132,6 +143,66 @@ namespace Spine.Unity.Editor {
 			}
 		}
 #endif
+
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+		internal static void PreprocessOnDemandTextureLoaders () {
+			BuildUtilities.IsInSkeletonAssetBuildPreProcessing = true;
+			try {
+				AssetDatabase.StartAssetEditing();
+				textureLoadersToRestore.Clear();
+				string[] loaderAssets = AssetDatabase.FindAssets("t:OnDemandTextureLoader");
+				foreach (string loaderAsset in loaderAssets) {
+					string assetPath = AssetDatabase.GUIDToAssetPath(loaderAsset);
+					OnDemandTextureLoader loader = AssetDatabase.LoadAssetAtPath<OnDemandTextureLoader>(assetPath);
+					bool isLoaderUsed = loader.atlasAsset && loader.atlasAsset.OnDemandTextureLoader == loader &&
+						loader.atlasAsset.TextureLoadingMode == AtlasAssetBase.LoadingMode.OnDemand;
+					if (isLoaderUsed) {
+						IEnumerable<Material> modifiedMaterials;
+						textureLoadersToRestore.Add(assetPath);
+						loader.AssignPlaceholderTextures(out modifiedMaterials);
+
+#if HAS_SAVE_ASSET_IF_DIRTY
+						foreach (Material material in modifiedMaterials) {
+							AssetDatabase.SaveAssetIfDirty(material);
+						}
+#endif
+					}
+				}
+				EditorUtility.UnloadUnusedAssetsImmediate();
+				AssetDatabase.StopAssetEditing();
+#if !HAS_SAVE_ASSET_IF_DIRTY
+				if (textureLoadersToRestore.Length > 0)
+					AssetDatabase.SaveAssets();
+#endif
+			} finally {
+				BuildUtilities.IsInSkeletonAssetBuildPreProcessing = false;
+			}
+		}
+
+		internal static void PostprocessOnDemandTextureLoaders () {
+			BuildUtilities.IsInSkeletonAssetBuildPostProcessing = true;
+			try {
+				foreach (string assetPath in textureLoadersToRestore) {
+					OnDemandTextureLoader loader = AssetDatabase.LoadAssetAtPath<OnDemandTextureLoader>(assetPath);
+					IEnumerable<Material> modifiedMaterials;
+					loader.AssignTargetTextures(out modifiedMaterials);
+#if HAS_SAVE_ASSET_IF_DIRTY
+					foreach (Material material in modifiedMaterials) {
+						AssetDatabase.SaveAssetIfDirty(material);
+					}
+#endif
+				}
+#if !HAS_SAVE_ASSET_IF_DIRTY
+				if (textureLoadersToRestore.Count > 0)
+					AssetDatabase.SaveAssets();
+#endif
+				textureLoadersToRestore.Clear();
+
+			} finally {
+				BuildUtilities.IsInSkeletonAssetBuildPostProcessing = false;
+			}
+		}
+#endif
 		internal static void PreprocessSpriteAtlases () {
 			BuildUtilities.IsInSpriteAtlasBuildPreProcessing = true;
 			try {

+ 1 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineEditorUtilities.cs

@@ -238,7 +238,7 @@ namespace Spine.Unity.Editor {
 		}
 
 		public static void ConfirmInitialization () {
-			if (!initialized || Icons.skeleton == null)
+			if (!initialized)
 				Initialize();
 		}
 

+ 35 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AtlasAssetBase.cs

@@ -27,6 +27,8 @@
  * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
+
 using System.Collections.Generic;
 using UnityEngine;
 
@@ -39,5 +41,38 @@ namespace Spine.Unity {
 		public abstract bool IsLoaded { get; }
 		public abstract void Clear ();
 		public abstract Atlas GetAtlas (bool onlyMetaData = false);
+
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+		public enum LoadingMode {
+			Normal = 0,
+			OnDemand
+		}
+		public virtual LoadingMode TextureLoadingMode {
+			get { return textureLoadingMode; }
+			set { textureLoadingMode = value; }
+		}
+		public OnDemandTextureLoader OnDemandTextureLoader {
+			get { return onDemandTextureLoader; }
+			set { onDemandTextureLoader = value; }
+		}
+
+		public virtual void BeginCustomTextureLoading () {
+			if (onDemandTextureLoader)
+				onDemandTextureLoader.BeginCustomTextureLoading();
+		}
+
+		public virtual void EndCustomTextureLoading () {
+			if (onDemandTextureLoader)
+				onDemandTextureLoader.EndCustomTextureLoading();
+		}
+
+		public virtual void RequireTexturesLoaded (Material material, ref Material overrideMaterial) {
+			if (onDemandTextureLoader)
+				onDemandTextureLoader.RequestLoadMaterialTextures(material, ref overrideMaterial);
+		}
+
+		[SerializeField] protected LoadingMode textureLoadingMode = LoadingMode.Normal;
+		[SerializeField] protected OnDemandTextureLoader onDemandTextureLoader = null;
+#endif
 	}
 }

+ 90 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/OnDemandTextureLoader.cs

@@ -0,0 +1,90 @@
+/******************************************************************************
+ * 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.
+ *****************************************************************************/
+
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+using System.Collections.Generic;
+using UnityEngine;
+
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+namespace Spine.Unity {
+	public abstract class OnDemandTextureLoader : ScriptableObject {
+		public AtlasAssetBase atlasAsset;
+
+		/// <param name="originalTextureName">Original texture name without extension.</param>
+		/// <returns>The placeholder texture's name for a given original target texture name.</returns>
+		public abstract string GetPlaceholderTextureName (string originalTextureName);
+		/// <summary>
+		/// Assigns previously setup placeholder textures at each Material of the associated AtlasAssetBase.</summary>
+		/// <returns>True on success, false if the placeholder texture could not be assigned at any of the
+		/// AtlasAssetBase's materials.</returns>
+		public abstract bool AssignPlaceholderTextures (out IEnumerable<Material> modifiedMaterials);
+		/// <summary>
+		/// Returns whether any placeholder textures are assigned at the Material of the associated AtlasAssetBase.
+		/// </summary>
+		/// <param name="placeholderMaterials">A newly created list of materials which has a placeholder texture assigned.</param>
+		/// <returns>True, if any placeholder texture is assigned at a Material of the associated AtlasAssetBase.</returns>
+		public abstract bool HasPlaceholderTexturesAssigned (out List<Material> placeholderMaterials);
+		/// <summary>
+		/// Assigns previously setup target textures at each Material where placeholder textures are setup.</summary>
+		/// <returns>True on success, false if the target texture could not be assigned at any of the
+		/// AtlasAssetBase's materials.</returns>
+		public abstract bool AssignTargetTextures (out IEnumerable<Material> modifiedMaterials);
+		public abstract void BeginCustomTextureLoading ();
+		public abstract void EndCustomTextureLoading ();
+		public abstract bool HasPlaceholderAssigned (Material material);
+		public abstract void RequestLoadMaterialTextures (Material material, ref Material overrideMaterial);
+		public abstract void Clear (bool clearAtlasAsset = false);
+
+		#region Event delegates
+		public delegate void TextureLoadDelegate (OnDemandTextureLoader loader, Material material, int textureIndex);
+		protected event TextureLoadDelegate onTextureLoaded;
+		protected event TextureLoadDelegate onTextureUnloaded;
+
+		public event TextureLoadDelegate TextureLoaded {
+			add { onTextureLoaded += value; }
+			remove { onTextureLoaded -= value; }
+		}
+		public event TextureLoadDelegate TextureUnloaded {
+			add { onTextureUnloaded += value; }
+			remove { onTextureUnloaded -= value; }
+		}
+
+		protected void OnTextureLoaded (Material material, int textureIndex) {
+			if (onTextureLoaded != null)
+				onTextureLoaded(this, material, textureIndex);
+		}
+		protected void OnTextureUnloaded (Material material, int textureIndex) {
+			if (onTextureUnloaded != null)
+				onTextureUnloaded(this, material, textureIndex);
+		}
+		#endregion
+	}
+}
+#endif

+ 11 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/OnDemandTextureLoader.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 57d54a357da79654aad505112bb63b9e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 4 - 1
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineAtlasAsset.cs

@@ -263,7 +263,10 @@ namespace Spine.Unity {
 					Debug.LogError("Material is missing texture: " + other.name, other);
 					return;
 				}
-				if (other.mainTexture.name == name) {
+				string textureName = other.mainTexture.name;
+				if (textureName == name ||
+					(atlasAsset.OnDemandTextureLoader != null &&
+					textureName == atlasAsset.OnDemandTextureLoader.GetPlaceholderTextureName(name))) {
 					material = other;
 					break;
 				}

+ 22 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs

@@ -49,6 +49,7 @@
 
 #define SPINE_OPTIONAL_RENDEROVERRIDE
 #define SPINE_OPTIONAL_MATERIALOVERRIDE
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
 
 using System.Collections.Generic;
 using UnityEngine;
@@ -565,6 +566,10 @@ namespace Spine.Unity {
 				AssignSpriteMaskMaterials();
 			}
 #endif
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+			if (Application.isPlaying)
+				HandleOnDemandLoading();
+#endif
 
 #if PER_MATERIAL_PROPERTY_BLOCKS
 			if (fixDrawOrder && meshRenderer.sharedMaterials.Length > 2) {
@@ -743,6 +748,23 @@ namespace Spine.Unity {
 
 #endif //#if BUILT_IN_SPRITE_MASK_COMPONENT
 
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+		void HandleOnDemandLoading () {
+			foreach (AtlasAssetBase atlasAsset in skeletonDataAsset.atlasAssets) {
+				if (atlasAsset.TextureLoadingMode != AtlasAssetBase.LoadingMode.Normal) {
+					atlasAsset.BeginCustomTextureLoading();
+					for (int i = 0, count = meshRenderer.sharedMaterials.Length; i < count; ++i) {
+						Material overrideMaterial = null;
+						atlasAsset.RequireTexturesLoaded(meshRenderer.sharedMaterials[i], ref overrideMaterial);
+						if (overrideMaterial != null)
+							meshRenderer.sharedMaterials[i] = overrideMaterial;
+					}
+					atlasAsset.EndCustomTextureLoading();
+				}
+			}
+		}
+#endif
+
 #if PER_MATERIAL_PROPERTY_BLOCKS
 		private MaterialPropertyBlock reusedPropertyBlock;
 		public static readonly int SUBMESH_DUMMY_PARAM_ID = Shader.PropertyToID("_Submesh");

+ 1 - 1
spine-unity/Assets/Spine/package.json

@@ -2,7 +2,7 @@
 	"name": "com.esotericsoftware.spine.spine-unity",
 	"displayName": "spine-unity Runtime",
 	"description": "This plugin provides the spine-unity runtime core.",
-	"version": "4.1.20",
+	"version": "4.1.21",
 	"unity": "2018.3",
 	"author": {
 		"name": "Esoteric Software",

+ 8 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Documentation.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 189dc4f0c0a8a6141b2c0f4c95fba6bd
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Documentation/README.md

@@ -0,0 +1,7 @@
+## Spine Addressables Extensions [Experimental]
+
+This experimental plugin provides integration of Addressables on-demand texture loading for the spine-unity runtime. Please be sure to test this package first and create backups of your project before using.
+
+### Usage
+
+First declare your target Material textures as addressable. Then select the SpineAtlasAsset, right-click the SpineAtlasAsset Inspector heading and select 'Add Addressables Loader'. This generates an 'AddressableTextureLoader' asset providing configuration parameters and sets up low-resolution placeholder textures which are automatically assigned in a pre-build step when building your game executable.

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Documentation/README.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 40d839470102af5458205023f6984224
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6c3739a10096f12488814c2027b2f5c1
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 94 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/AddressablesTextureLoaderInspector.cs

@@ -0,0 +1,94 @@
+/******************************************************************************
+ * 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.
+ *****************************************************************************/
+
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using UnityEngine.ResourceManagement.AsyncOperations;
+
+namespace Spine.Unity.Editor {
+
+	using GenericTextureLoader = GenericOnDemandTextureLoader<AddressableTextureReference, AddressableRequest>;
+	using GenericTextureLoaderInspector = GenericOnDemandTextureLoaderInspector<AddressableTextureReference, AddressableRequest>;
+	using PlaceholderMaterialMapping = AddressablesTextureLoader.PlaceholderMaterialMapping;
+	using PlaceholderTextureMapping = AddressablesTextureLoader.PlaceholderTextureMapping;
+
+	[CustomEditor(typeof(AddressablesTextureLoader)), CanEditMultipleObjects]
+	public class AddressablesTextureLoaderInspector : GenericTextureLoaderInspector {
+		public string LoaderSuffix { get { return "_Addressable"; } }
+
+		public class AddressablesMethodImplementations : StaticMethodImplementations {
+			public override GenericTextureLoader GetOrCreateLoader (string loaderPath) {
+				GenericTextureLoader loader = AssetDatabase.LoadAssetAtPath<GenericTextureLoader>(loaderPath);
+				if (loader == null) {
+					loader = GenericTextureLoader.CreateInstance<GenericTextureLoader>();
+					AssetDatabase.CreateAsset(loader, loaderPath);
+					loader = AssetDatabase.LoadAssetAtPath<GenericTextureLoader>(loaderPath);
+				} else {
+					loader.Clear(clearAtlasAsset: false);
+				}
+				return loader;
+			}
+
+			public override bool SetupOnDemandLoadingReference (
+				ref AddressableTextureReference targetTextureReference, Texture targetTexture) {
+
+				string targetTextureGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(targetTexture));
+				if (string.IsNullOrEmpty(targetTextureGUID))
+					return false;
+				targetTextureReference.assetReference = new AssetReferenceTexture(targetTextureGUID);
+				return targetTextureReference.assetReference.IsValid();
+			}
+		}
+
+#region Context Menu Item
+		[MenuItem("CONTEXT/AtlasAssetBase/Add Addressables Loader")]
+		static void AddAddressablesLoader (MenuCommand cmd) {
+			if (staticMethods == null)
+				staticMethods = new AddressablesMethodImplementations();
+			staticMethods.AddOnDemandLoader(cmd);
+		}
+		#endregion
+
+		protected override StaticMethodImplementations CreateStaticMethodImplementations () {
+			return new AddressablesMethodImplementations();
+		}
+
+		protected override void DrawSingleLineTargetTextureProperty (SerializedProperty property) {
+			EditorGUILayout.PropertyField(property.FindPropertyRelative("assetReference"), GUIContent.none, true);
+		}
+	}
+}
+#endif

+ 11 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/AddressablesTextureLoaderInspector.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 31994ef86a03628459a588131067023f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 25 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/spine-addressables-editor.asmdef

@@ -0,0 +1,25 @@
+{
+    "name": "spine-addressables-editor",
+    "rootNamespace": "",
+    "references": [
+        "spine-unity",
+        "spine-unity-editor",
+        "spine-addressables",
+        "spine-on-demand-loading",
+        "spine-on-demand-loading-editor",
+        "Unity.Addressables",
+        "Unity.Addressables.Editor",
+        "Unity.ResourceManager"
+    ],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Editor/spine-addressables-editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0743404dced125d4b8d86290e414812c
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 26 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/LICENSE.md

@@ -0,0 +1,26 @@
+# 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.

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/LICENSE.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3369e909ffdf92847a4b8d95b983a7ff
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 219b86dfc3643544e80d60785c29d034
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 96 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/AddressablesTextureLoader.cs

@@ -0,0 +1,96 @@
+/******************************************************************************
+ * 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.
+ *****************************************************************************/
+
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using UnityEngine.ResourceManagement.AsyncOperations;
+
+namespace Spine.Unity {
+
+	[System.Serializable]
+	public struct AddressableTextureReference : ITargetTextureReference {
+		[SerializeField] public AssetReferenceTexture assetReference;
+
+#if UNITY_EDITOR
+		public Texture EditorTexture {
+			get {
+				return (Texture)assetReference.editorAsset;
+			}
+		}
+#endif
+	}
+
+	public struct AddressableRequest : IOnDemandRequest {
+		public AsyncOperationHandle<Texture> handle;
+
+		public bool WasRequested {
+			get { return handle.IsValid(); }
+		}
+
+		public bool WasSuccessfullyLoaded {
+			get { return handle.IsValid() && handle.Status == AsyncOperationStatus.Succeeded; }
+		}
+
+		public bool IsTarget (Texture texture) {
+			return handle.Result == texture;
+		}
+
+		public void Release () {
+			Addressables.Release(handle);
+		}
+	}
+
+	[System.Serializable]
+	public class AddressablesTextureLoader : GenericOnDemandTextureLoader<AddressableTextureReference, AddressableRequest> {
+		public override void CreateTextureRequest (AddressableTextureReference targetReference,
+			MaterialOnDemandData materialData, int textureIndex, Material materialToUpdate) {
+
+			materialData.textureRequests[textureIndex].handle = targetReference.assetReference.LoadAssetAsync<Texture>();
+			materialData.textureRequests[textureIndex].handle.Completed += (obj) => {
+				if (obj.Status == AsyncOperationStatus.Succeeded) {
+					materialToUpdate.mainTexture = (Texture)targetReference.assetReference.Asset;
+					OnTextureLoaded(materialToUpdate, textureIndex);
+				}
+			};
+		}
+
+		public override Texture GetAlreadyLoadedTexture (int materialIndex, int textureIndex) {
+			AddressableTextureReference targetReference = placeholderMap[materialIndex].textures[textureIndex].targetTextureReference;
+			return (Texture)targetReference.assetReference.Asset;
+		}
+	}
+}
+#endif

+ 11 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/AddressablesTextureLoader.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fbdfbb13f312e5041973e2127739f846
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: c33b04c403c19614395e83e691d3170a, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 20 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/spine-addressables.asmdef

@@ -0,0 +1,20 @@
+{
+    "name": "spine-addressables",
+    "rootNamespace": "",
+    "references": [
+        "spine-unity",
+        "spine-csharp",
+        "spine-on-demand-loading",
+        "Unity.Addressables",
+        "Unity.ResourceManager"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/Runtime/spine-addressables.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d2261bc30a9ceb04ebbc867c3654e30e
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 22 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/package.json

@@ -0,0 +1,22 @@
+{
+	"name": "com.esotericsoftware.spine.addressables",
+	"displayName": "Spine Addressables Extensions [Experimental]",
+	"description": "This experimental plugin provides integration of Addressables on-demand texture loading for the spine-unity runtime.\nPlease be sure to test this package first and create backups of your project before using.\n\nUsage: First declare your target Material textures as addressable. Then select the SpineAtlasAsset, right-click the SpineAtlasAsset Inspector heading and select 'Add Addressables Loader'. This generates an 'AddressableTextureLoader' asset providing configuration parameters and sets up low-resolution placeholder textures which are automatically assigned in a pre-build step when building your game executable.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime (via the spine-unity unitypackage), version 4.1.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
+	"version": "4.1.0-preview.1",
+	"unity": "2018.3",
+	"author": {
+		"name": "Esoteric Software",
+		"email": "[email protected]",
+		"url": "http://esotericsoftware.com/"
+	},
+	"dependencies": {
+		"com.unity.addressables": "1.18.19",
+		"com.esotericsoftware.spine.spine-unity": "4.1.21",
+		"com.esotericsoftware.spine.on-demand-loading": "4.1.0"
+	},
+	"keywords": [
+		"spine",
+		"addressables",
+		"preview"
+	]
+}

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.addressables/package.json.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ece84be6a21fe8841a65fdc9d5038711
+PackageManifestImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f7d10bf3e021d1845b6cb3ffcc0d4cb2
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 390 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/GenericOnDemandTextureLoaderInspector.cs

@@ -0,0 +1,390 @@
+/******************************************************************************
+ * 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 UnityEditor;
+using UnityEngine;
+
+namespace Spine.Unity.Editor {
+
+	/// <summary>
+	/// Base class for GenericOnDemandTextureLoader Inspector subclasses.
+	/// For reference, see the <see cref="AddressablesTextureLoaderInspector"/> class available
+	/// in the com.esotericsoftware.spine.addressables UPM package.
+	/// </summary>
+	/// <typeparam name="TargetReference">The implementation struct which holds an on-demand loading reference
+	/// to the target texture to be loaded, derived from ITargetTextureReference.</typeparam>
+	/// <typeparam name="TextureRequest">The implementation struct covering a single texture loading request,
+	/// derived from IOnDemandRequest</typeparam>
+	[InitializeOnLoad]
+	[CustomEditor(typeof(GenericOnDemandTextureLoader<,>)), CanEditMultipleObjects]
+	public abstract class GenericOnDemandTextureLoaderInspector<TargetReference, TextureRequest> : UnityEditor.Editor
+		where TargetReference : Spine.Unity.ITargetTextureReference
+		where TextureRequest : Spine.Unity.IOnDemandRequest {
+
+		protected SerializedProperty atlasAsset;
+		protected SerializedProperty maxPlaceholderSize;
+		protected SerializedProperty placeholderMap;
+		protected SerializedProperty unloadAfterSecondsUnused;
+		static protected bool placeholdersFoldout = true;
+		protected SerializedProperty loadedDataAtMaterial;
+		protected GenericOnDemandTextureLoader<TargetReference, TextureRequest> loader;
+		protected GUIContent placeholderTexturesLabel;
+
+		/// <summary>
+		/// Called via InitializeOnLoad attribute upon Editor startup or compilation.
+		/// </summary>
+		static GenericOnDemandTextureLoaderInspector () {
+#if NEWPLAYMODECALLBACKS
+			EditorApplication.playModeStateChanged += OnPlaymodeChanged;
+#else
+			EditorApplication.playmodeStateChanged += OnPlaymodeChanged;
+#endif
+		}
+
+		/// <summary>
+		/// 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.
+		/// </summary>
+		public abstract class StaticMethodImplementations {
+
+			public abstract GenericOnDemandTextureLoader<TargetReference, TextureRequest> GetOrCreateLoader (string loaderPath);
+
+			/// <summary>
+			/// 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".
+			/// </summary>
+			public string LoaderSuffix { get; }
+
+			public abstract bool SetupOnDemandLoadingReference (
+				ref TargetReference targetTextureReference, Texture targetTexture);
+
+			/// <summary>
+			/// Create a context menu wrapper in the main class for this generic implementation using the code below.
+			/// <code>
+			/// [MenuItem("CONTEXT/AtlasAssetBase/Add YourSubclass Loader")]
+			///	static void AddYourSubclassLoader (MenuCommand cmd) {
+			///		if (staticMethods == null)
+			///			staticMethods = new YourSubclassMethodImplementations ();
+			///		staticMethods.AddOnDemandLoader(cmd);
+			///	}
+			/// </code>
+			/// </summary>
+			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<TargetReference, TextureRequest> loader = staticMethods.GetOrCreateLoader(loaderPath);
+				staticMethods.SetupForAtlasAsset(loader, atlasAsset);
+
+				EditorUtility.SetDirty(loader);
+				AssetDatabase.SaveAssets();
+			}
+
+			public virtual void SetupForAtlasAsset (GenericOnDemandTextureLoader<TargetReference, TextureRequest> loader, AtlasAssetBase atlasAsset) {
+				if (loader.placeholderMap != null && loader.placeholderMap.Length > 0) {
+					IEnumerable<Material> 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<TargetReference, TextureRequest>.PlaceholderMaterialMapping[materialCount];
+				GenericOnDemandTextureLoader<TargetReference, TextureRequest>.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<TargetReference, TextureRequest>.PlaceholderTextureMapping[1]; // Todo: currently only main texture is supported.
+					int textureIndex = 0;
+
+					GenericOnDemandTextureLoader<TargetReference, TextureRequest>.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;
+			}
+
+			public virtual Texture CreatePlaceholderTextureFor (Texture originalTexture, int maxPlaceholderSize,
+			GenericOnDemandTextureLoader<TargetReference, TextureRequest> 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<Texture>(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) {
+						Texture2D texture2D = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
+						if (texture2D) {
+							Color[] maxTextureSizePixels = texture2D.GetPixels();
+							texture2D.SetPixels(maxTextureSizePixels);
+
+							var bytes = texture2D.EncodeToPNG();
+							string targetPath = Application.dataPath + "/../" + texturePath;
+							System.IO.File.WriteAllBytes(targetPath, bytes);
+							texture2D.Apply(updateMipmaps: true, makeNoLongerReadable: true);
+							EditorUtility.SetDirty(texture2D);
+							AssetDatabase.SaveAssets();
+						}
+					}
+					placeholderTexture = AssetDatabase.LoadAssetAtPath<Texture>(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");
+			maxPlaceholderSize = serializedObject.FindProperty("maxPlaceholderSize");
+			placeholderMap = serializedObject.FindProperty("placeholderMap");
+			unloadAfterSecondsUnused = serializedObject.FindProperty("unloadAfterSecondsUnused");
+			loadedDataAtMaterial = serializedObject.FindProperty("loadedDataAtMaterial");
+			placeholderTexturesLabel = new GUIContent("Placeholder Textures");
+			loader = (GenericOnDemandTextureLoader<TargetReference, TextureRequest>)target;
+
+			if (staticMethods == null)
+				staticMethods = CreateStaticMethodImplementations ();
+		}
+
+#if NEWPLAYMODECALLBACKS
+		static void OnPlaymodeChanged (PlayModeStateChange mode) {
+			bool assignTargetTextures = mode == PlayModeStateChange.ExitingPlayMode;
+#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<OnDemandTextureLoader>(assetPath);
+				AssignTargetTexturesAtLoader(loader);
+			}
+		}
+
+		public static void AssignTargetTexturesAtLoader (OnDemandTextureLoader loader) {
+			List<Material> placeholderMaterials;
+			bool anyPlaceholdersAssigned = loader.HasPlaceholderTexturesAssigned(out placeholderMaterials);
+			if (anyPlaceholdersAssigned) {
+				Debug.Log("OnDemandTextureLoader detected placeholders assigned at one or more materials. Resetting to target textures.", loader);
+				AssetDatabase.StartAssetEditing();
+				IEnumerable<Material> modifiedMaterials;
+				loader.AssignTargetTextures(out modifiedMaterials);
+				foreach (Material placeholderMaterial in placeholderMaterials) {
+					EditorUtility.SetDirty(placeholderMaterial);
+				}
+				AssetDatabase.StopAssetEditing();
+				AssetDatabase.SaveAssets();
+			}
+		}
+
+		/// <summary>
+		/// Override this method in your implementation subclass as follows.
+		/// <code>
+		/// protected override StaticMethodImplementations CreateStaticMethodImplementations () {
+		///		return new YourStaticMethodImplementationsSubclass();
+		/// }
+		/// </code>
+		/// </summary>
+		protected abstract StaticMethodImplementations CreateStaticMethodImplementations ();
+
+		/// <summary>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.
+		/// </summary>
+		/// <param name="textureMapping">SerializedProperty pointing to a
+		/// PlaceholderTextureMapping object of the placeholderMap array.</param>
+		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();
+		}
+
+		/// <summary>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.
+		/// </summary>
+		/// <param name="property">SerializedProperty pointing to a
+		/// TargetReference object of the PlaceholderTextureMapping entry.</param>
+		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(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<TargetReference, TextureRequest> loader) {
+			foreach (var materialMap in loader.placeholderMap) {
+				Texture texture = materialMap.textures[0].placeholderTexture;
+				if (texture)
+					AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(texture));
+			}
+			loader.Clear(clearAtlasAsset: false);
+			AssetDatabase.SaveAssets();
+		}
+
+		public void ReinitPlaceholderTextures (GenericOnDemandTextureLoader<TargetReference, TextureRequest> loader) {
+			AssignTargetTextures(loader);
+			DeletePlaceholderTextures(loader);
+			staticMethods.SetupForAtlasAsset(loader, loader.atlasAsset);
+			EditorUtility.SetDirty(loader);
+			AssetDatabase.SaveAssets();
+		}
+
+		public bool AssignPlaceholderTextures (GenericOnDemandTextureLoader<TargetReference, TextureRequest> loader) {
+			// re-setup placeholders to ensure the mapping is up to date.
+			staticMethods.SetupForAtlasAsset(loader, loader.atlasAsset);
+			IEnumerable<Material> modifiedMaterials;
+			return loader.AssignPlaceholderTextures(out modifiedMaterials);
+		}
+
+		public bool AssignTargetTextures (GenericOnDemandTextureLoader<TargetReference, TextureRequest> loader) {
+			IEnumerable<Material> modifiedMaterials;
+			return loader.AssignTargetTextures(out modifiedMaterials);
+		}
+	}
+}
+#endif

+ 11 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/GenericOnDemandTextureLoaderInspector.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c3eb807840ae1934b9346f35db2c8fac
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 20 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/spine-on-demand-loading-editor.asmdef

@@ -0,0 +1,20 @@
+{
+    "name": "spine-on-demand-loading-editor",
+    "rootNamespace": "",
+    "references": [
+        "spine-unity",
+        "spine-unity-editor",
+        "spine-on-demand-loading"
+    ],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Editor/spine-on-demand-loading-editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 469d9d10b699b6b42a8727851decc63e
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 26 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/LICENSE.md

@@ -0,0 +1,26 @@
+# 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.

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/LICENSE.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: efea0eae41e05cb438aabbfb99211f72
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8258ac8697ed60b46a52a3dca4f406e4
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 316 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/GenericOnDemandTextureLoader.cs

@@ -0,0 +1,316 @@
+/******************************************************************************
+ * 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.
+ *****************************************************************************/
+
+#define SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+#if SPINE_OPTIONAL_ON_DEMAND_LOADING
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace Spine.Unity {
+
+	/// <summary>
+	/// Interface to derive a concrete target reference struct from which holds
+	/// an on-demand loading reference to the target texture to be loaded.
+	/// </summary>
+	public interface ITargetTextureReference {
+#if UNITY_EDITOR
+		Texture EditorTexture { get; }
+#endif
+	}
+
+	/// <summary>
+	/// Interface to derive a concrete request handler struct from which covers
+	/// a single texture loading request.
+	/// </summary>
+	public interface IOnDemandRequest {
+		bool WasRequested { get; }
+		bool WasSuccessfullyLoaded { get; }
+		bool IsTarget (Texture texture);
+		void Release ();
+	}
+
+	/// <summary>
+	/// Base class to derive your own OnDemandTextureLoader subclasses from which already provides
+	/// the general loading and unloading framework.
+	/// For reference, see the <see cref="AddressablesTextureLoader"/> class available
+	/// in the com.esotericsoftware.spine.addressables UPM package.
+	/// </summary>
+	/// <typeparam name="TargetReference">The implementation struct which holds an on-demand loading reference
+	/// to the target texture to be loaded, derived from ITargetTextureReference.</typeparam>
+	/// <typeparam name="TextureRequest">The implementation struct covering a single texture loading request,
+	/// derived from IOnDemandRequest</typeparam>
+	[System.Serializable]
+	public abstract class GenericOnDemandTextureLoader<TargetReference, TextureRequest> : OnDemandTextureLoader
+		where TargetReference : ITargetTextureReference
+		where TextureRequest : IOnDemandRequest {
+
+		[System.Serializable]
+		public struct PlaceholderTextureMapping {
+			public Texture placeholderTexture;
+			public TargetReference targetTextureReference;
+		}
+
+		/// <summary>
+		/// Unfortunately serialization of jagged arrays PlaceholderTextureMapping[][] is not supported,
+		/// so we need to use this class with a 1D-array PlaceholderMaterialMapping[] as a workaround.
+		/// </summary>
+		[System.Serializable]
+		public struct PlaceholderMaterialMapping {
+
+			public PlaceholderTextureMapping[] textures;
+		}
+
+		// Note: not System.Serializabe on purpose. Would be unnecessary and causes problems otherwise.
+		public struct MaterialOnDemandData {
+			public int lastFrameRequested;
+			public TextureRequest[] textureRequests;
+		}
+
+		void Reset () {
+			Clear(clearAtlasAsset: true);
+		}
+
+		public override void Clear (bool clearAtlasAsset = false) {
+			if (clearAtlasAsset) atlasAsset = null;
+			placeholderMap = null;
+			loadedDataAtMaterial = null;
+		}
+
+		public override string GetPlaceholderTextureName (string originalTextureName) {
+			return originalTextureName + "_low";
+		}
+
+		public override bool AssignPlaceholderTextures (out IEnumerable<Material> modifiedMaterials) {
+			modifiedMaterials = null;
+			if (!atlasAsset) return false;
+
+			int materialIndex = 0;
+			foreach (Material targetMaterial in atlasAsset.Materials) {
+				if (materialIndex >= placeholderMap.Length) {
+					Debug.LogError(string.Format("Failed to assign placeholder textures at {0}, material #{1} {2}. " +
+						"It seems like the GenericOnDemandTextureLoader asset was not setup accordingly for the AtlasAsset.",
+						atlasAsset, materialIndex+1, targetMaterial), this);
+					return false;
+				}
+				Texture activeTexture = targetMaterial.mainTexture;
+				int textureIndex = 0; // Todo: currently only main texture is supported.
+
+				int mapIndex = materialIndex;
+#if UNITY_EDITOR
+				if (!Application.isPlaying) {
+					int foundMapIndex = Array.FindIndex(placeholderMap,
+						entry => entry.textures[textureIndex].targetTextureReference.EditorTexture == activeTexture);
+					if (foundMapIndex >= 0)
+						mapIndex = foundMapIndex;
+				}
+#endif
+				Texture placeholderTexture = placeholderMap[mapIndex].textures[textureIndex].placeholderTexture;
+				if (placeholderTexture == null) {
+					Debug.LogWarning(string.Format("Placeholder texture set to null at {0}, for material #{1} {2}. " +
+						"It seems like the GenericOnDemandTextureLoader asset was not setup accordingly for the AtlasAsset.",
+						atlasAsset, materialIndex + 1, targetMaterial), this);
+				}
+				else {
+					targetMaterial.mainTexture = placeholderTexture;
+				}
+				++materialIndex;
+			}
+			modifiedMaterials = atlasAsset.Materials;
+			return true;
+		}
+
+		public override bool HasPlaceholderTexturesAssigned (out List<Material> placeholderMaterials) {
+			placeholderMaterials = null;
+			if (!atlasAsset) return false;
+
+			bool anyPlaceholderAssigned = false;
+
+			int materialIndex = 0;
+			foreach (Material material in atlasAsset.Materials) {
+				if (materialIndex >= placeholderMap.Length)
+					return false;
+				bool hasPlaceholderAssigned = HasPlaceholderAssigned(material);
+				if (hasPlaceholderAssigned) {
+					anyPlaceholderAssigned = true;
+					if (placeholderMaterials == null) placeholderMaterials = new List<Material>();
+					placeholderMaterials.Add(material);
+				}
+			}
+			return anyPlaceholderAssigned;
+		}
+
+		public override bool AssignTargetTextures (out IEnumerable<Material> modifiedMaterials) {
+			modifiedMaterials = null;
+			if (!atlasAsset) return false;
+			BeginCustomTextureLoading();
+			int i = 0;
+			foreach (Material targetMaterial in atlasAsset.Materials) {
+				if (i >= placeholderMap.Length) {
+					Debug.LogError(string.Format("Failed to assign target textures at {0}, material #{1} {2}. " +
+						"It seems like the OnDemandTextureLoader asset was not setup accordingly for the AtlasAsset.",
+						atlasAsset, i + 1, targetMaterial), this);
+					return false;
+				}
+				Material ignoredArgument = null;
+				RequestLoadMaterialTextures(targetMaterial, ref ignoredArgument);
+				++i;
+			}
+			modifiedMaterials = atlasAsset.Materials;
+			EndCustomTextureLoading();
+			return true;
+		}
+
+		public override void BeginCustomTextureLoading () {
+			if (loadedDataAtMaterial == null || (loadedDataAtMaterial.Length == 0 && placeholderMap.Length > 0)) {
+				loadedDataAtMaterial = new MaterialOnDemandData[placeholderMap.Length];
+				for (int i = 0, count = loadedDataAtMaterial.Length; i < count; ++i) {
+					loadedDataAtMaterial[i].lastFrameRequested = -1;
+					int texturesAtMaterial = placeholderMap[i].textures.Length;
+					loadedDataAtMaterial[i].textureRequests = new TextureRequest[texturesAtMaterial];
+				}
+			}
+		}
+
+		public override void EndCustomTextureLoading () {
+#if UNITY_EDITOR
+			if (!Application.isPlaying)
+				return;
+#endif
+			UnloadUnusedTextures();
+		}
+
+		public override bool HasPlaceholderAssigned (Material material) {
+			Texture currentTexture = material.mainTexture;
+			int textureIndex = 0; // Todo: currently only main texture is supported.
+			int foundMaterialIndex = Array.FindIndex(placeholderMap, entry => entry.textures[textureIndex].placeholderTexture == currentTexture);
+			return foundMaterialIndex >= 0;
+		}
+
+		public override void RequestLoadMaterialTextures (Material material, ref Material overrideMaterial) {
+			if (!material || !material.mainTexture) return;
+
+			Texture currentTexture = material.mainTexture;
+			int textureIndex = 0; // Todo: currently only main texture is supported.
+
+			int foundMaterialIndex = Array.FindIndex(placeholderMap, entry => entry.textures[textureIndex].placeholderTexture == currentTexture);
+			if (foundMaterialIndex >= 0)
+				RequestLoadTexture(material, foundMaterialIndex, textureIndex);
+
+			int loadedMaterialIndex = Array.FindIndex(loadedDataAtMaterial, entry =>
+				entry.textureRequests[textureIndex].WasRequested &&
+				entry.textureRequests[textureIndex].IsTarget(currentTexture));
+			if (loadedMaterialIndex >= 0)
+				loadedDataAtMaterial[loadedMaterialIndex].lastFrameRequested = Time.frameCount;
+		}
+
+		protected virtual void RequestLoadTexture (Material material, int materialIndex, int textureIndex) {
+			PlaceholderTextureMapping[] placeholderTextures = placeholderMap[materialIndex].textures;
+			TargetReference targetReference = placeholderTextures[textureIndex].targetTextureReference;
+			loadedDataAtMaterial[materialIndex].lastFrameRequested = Time.frameCount;
+
+#if UNITY_EDITOR
+			if (!Application.isPlaying) {
+				if (targetReference.EditorTexture != null)
+					material.mainTexture = targetReference.EditorTexture;
+				return;
+			}
+#endif
+			MaterialOnDemandData materialData = loadedDataAtMaterial[materialIndex];
+			if (materialData.textureRequests[textureIndex].WasRequested) {
+				Texture loadedTexture = GetAlreadyLoadedTexture(materialIndex, textureIndex);
+				if (loadedTexture != null)
+					material.mainTexture = loadedTexture;
+				return;
+			}
+
+			CreateTextureRequest(targetReference, materialData, textureIndex, material);
+		}
+
+		public abstract Texture GetAlreadyLoadedTexture (int materialIndex, int textureIndex);
+
+		public abstract void CreateTextureRequest(TargetReference targetReference,
+			MaterialOnDemandData materialData, int textureIndex, Material materialToUpdate);
+
+		public virtual void UnloadUnusedTextures () {
+			int currentFrameCount = Time.frameCount;
+			float timePerFrame = Time.smoothDeltaTime;
+			float deltaFramesToUnload = unloadAfterSecondsUnused / timePerFrame;
+
+			for (int materialIndex = 0, materialCount = loadedDataAtMaterial.Length; materialIndex < materialCount; ++materialIndex) {
+				MaterialOnDemandData materialData = loadedDataAtMaterial[materialIndex];
+				int textureCount = materialData.textureRequests.Length;
+
+				for (int textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
+					TextureRequest textureRequest = materialData.textureRequests[textureIndex];
+					if (textureRequest.WasSuccessfullyLoaded &&
+						currentFrameCount - materialData.lastFrameRequested > deltaFramesToUnload) {
+						RequestUnloadTexture(materialIndex, textureIndex);
+					}
+				}
+			}
+		}
+
+		public virtual void RequestUnloadTexture (int materialIndex, int textureIndex) {
+			if (materialIndex >= loadedDataAtMaterial.Length) return;
+
+			bool wasReleased = false;
+			PlaceholderTextureMapping[] placeholderTextures = placeholderMap[materialIndex].textures;
+			MaterialOnDemandData materialData = loadedDataAtMaterial[materialIndex];
+			if (materialData.textureRequests[textureIndex].WasRequested) {
+				materialData.textureRequests[textureIndex].Release();
+				wasReleased = true;
+			}
+
+			// reset material textures to placeholder textures.
+			Material targetMaterial = atlasAsset.Materials.ElementAt(materialIndex);
+			if (targetMaterial) {
+				targetMaterial.mainTexture = placeholderTextures[textureIndex].placeholderTexture;
+				if (wasReleased)
+					OnTextureUnloaded(targetMaterial, textureIndex);
+			}
+		}
+
+		public int maxPlaceholderSize = 128;
+		public float unloadAfterSecondsUnused = 60.0f;
+
+		/// <summary>A map from placeholder to on-demand-loaded target textures.
+		/// This array holds PlaceholderMaterialMapping for each Material,
+		/// where each <c>PlaceholderMaterialMapping.textures</c> contains a Texture-to-TextureReference mapping
+		/// for each Texture at the Material.</summary>
+		public PlaceholderMaterialMapping[] placeholderMap;
+
+		/// <summary>An array holding loaded data for each Material.</summary>
+		protected MaterialOnDemandData[] loadedDataAtMaterial;
+	}
+}
+#endif

+ 11 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/GenericOnDemandTextureLoader.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5964cfa446b05ea4e8c3622d3a1b9430
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 17 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/spine-on-demand-loading.asmdef

@@ -0,0 +1,17 @@
+{
+    "name": "spine-on-demand-loading",
+    "rootNamespace": "",
+    "references": [
+        "spine-unity",
+        "spine-csharp"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/Runtime/spine-on-demand-loading.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 2eb352bed1bae98439c9adcbb05e6b90
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 21 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/package.json

@@ -0,0 +1,21 @@
+{
+	"name": "com.esotericsoftware.spine.on-demand-loading",
+	"displayName": "Spine On-Demand Loading Extensions [Experimental]",
+	"description": "This experimental plugin provides a generic basic implementation of on-demand texture loading for the spine-unity runtime. You might want to use the available com.esotericsoftware.spine.addressables package which depends on this package.\nPlease be sure to test this package first and create backups of your project before using.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime (via the spine-unity unitypackage), version 4.1.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
+	"version": "4.1.0",
+	"unity": "2018.3",
+	"author": {
+		"name": "Esoteric Software",
+		"email": "[email protected]",
+		"url": "http://esotericsoftware.com/"
+	},
+	"dependencies": {
+		"com.esotericsoftware.spine.spine-unity": "4.1.21"
+	},
+	"keywords": [
+		"spine",
+		"on-demand",
+		"loading",
+		"preview"
+	]
+}

+ 7 - 0
spine-unity/Modules/com.esotericsoftware.spine.on-demand-loading/package.json.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8b808d40cc8a54b4ab5b0dbf607e15a8
+PackageManifestImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: