/****************************************************************************** * 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 { /// /// Interface to derive a concrete target reference struct from which holds /// an on-demand loading reference to the target texture to be loaded. /// public interface ITargetTextureReference { #if UNITY_EDITOR Texture EditorTexture { get; } #endif } /// /// Interface to derive a concrete request handler struct from which covers /// a single texture loading request. /// public interface IOnDemandRequest { bool WasRequested { get; } bool WasSuccessfullyLoaded { get; } bool IsTarget (Texture texture); void Release (); } /// /// Base class to derive your own OnDemandTextureLoader subclasses from which already provides /// the general loading and unloading framework. /// 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 [System.Serializable] public abstract class GenericOnDemandTextureLoader : OnDemandTextureLoader where TargetReference : ITargetTextureReference where TextureRequest : IOnDemandRequest { [System.Serializable] public struct PlaceholderTextureMapping { public Texture placeholderTexture; public TargetReference targetTextureReference; } /// /// Unfortunately serialization of jagged arrays PlaceholderTextureMapping[][] is not supported, /// so we need to use this class with a 1D-array PlaceholderMaterialMapping[] as a workaround. /// [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 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 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(); placeholderMaterials.Add(material); } } return anyPlaceholderAssigned; } public override bool AssignTargetTextures (out IEnumerable 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; /// A map from placeholder to on-demand-loaded target textures. /// This array holds PlaceholderMaterialMapping for each Material, /// where each PlaceholderMaterialMapping.textures contains a Texture-to-TextureReference mapping /// for each Texture at the Material. public PlaceholderMaterialMapping[] placeholderMap; /// An array holding loaded data for each Material. protected MaterialOnDemandData[] loadedDataAtMaterial; } } #endif