From d5a685dae96c614c85abd6795438416a1b390224 Mon Sep 17 00:00:00 2001 From: Harrison Date: Fri, 10 Jan 2025 10:02:02 +0200 Subject: [PATCH] feat: changed editor avatar storage location --- Editor/Core/Scripts/EditorAvatarLoader.cs | 125 ++++++++++++++++++ .../Core/Scripts/EditorAvatarLoader.cs.meta | 11 ++ ...rLoaderEditor.cs => AvatarLoaderWindow.cs} | 50 ++----- ...tor.cs.meta => AvatarLoaderWindow.cs.meta} | 0 Editor/Core/Scripts/Utilities/PrefabHelper.cs | 5 +- Runtime/Core/Scripts/Caching/AvatarCache.cs | 33 ++--- .../Core/Scripts/Caching/AvatarManifest.cs | 3 +- .../Core/Scripts/Utils/DirectoryUtility.cs | 17 +-- Tests/Editor/AvatarLoaderWindowTests.cs | 8 +- 9 files changed, 168 insertions(+), 84 deletions(-) create mode 100644 Editor/Core/Scripts/EditorAvatarLoader.cs create mode 100644 Editor/Core/Scripts/EditorAvatarLoader.cs.meta rename Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/{AvatarLoaderEditor.cs => AvatarLoaderWindow.cs} (65%) rename Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/{AvatarLoaderEditor.cs.meta => AvatarLoaderWindow.cs.meta} (100%) diff --git a/Editor/Core/Scripts/EditorAvatarLoader.cs b/Editor/Core/Scripts/EditorAvatarLoader.cs new file mode 100644 index 00000000..b35826d1 --- /dev/null +++ b/Editor/Core/Scripts/EditorAvatarLoader.cs @@ -0,0 +1,125 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ReadyPlayerMe.Core; +using ReadyPlayerMe.Loader; +using UnityEditor; +using UnityEngine; +using UnityEngine.Networking; +using Object = UnityEngine.Object; + +public class EditorAvatarLoader +{ + private const string TAG = nameof(EditorAvatarLoader); + + private readonly bool avatarCachingEnabled; + + /// Scriptable Object Avatar API request parameters configuration + public AvatarConfig AvatarConfig; + + /// Importer to use to import glTF + public IImporter Importer; + + private string avatarUrl; + private OperationExecutor executor; + private float startTime; + + public Action OnCompleted; + + /// + /// This class constructor is used to any required fields. + /// + /// Use default defer agent + public EditorAvatarLoader() + { + AvatarLoaderSettings loaderSettings = AvatarLoaderSettings.LoadSettings(); + Importer = new GltFastAvatarImporter(); + AvatarConfig = loaderSettings.AvatarConfig != null ? loaderSettings.AvatarConfig : null; + } + + /// Set the timeout for download requests + public int Timeout { get; set; } = 20; + + /// + /// Runs through the process of loading the avatar and creating a game object via the OperationExecutor. + /// + /// The URL to the avatars .glb file. + public async Task Load(string url) + { + var context = new AvatarContext(); + context.Url = url; + context.AvatarCachingEnabled = false; + context.AvatarConfig = AvatarConfig; + context.ParametersHash = AvatarCache.GetAvatarConfigurationHash(AvatarConfig); + + // process url + var urlProcessor = new UrlProcessor(); + context = await urlProcessor.Execute(context, CancellationToken.None); + // get metadata + var metadataDownloader = new MetadataDownloader(); + context = await metadataDownloader.Execute(context, CancellationToken.None); + //download avatar into asset folder + context.AvatarUri.LocalModelPath = await DownloadAvatarModel(context.AvatarUri); + if (string.IsNullOrEmpty(context.AvatarUri.LocalModelPath)) + { + Debug.LogError($"Failed to download avatar model from {context.AvatarUri.ModelUrl}"); + return null; + } + // import model + context.Bytes = await File.ReadAllBytesAsync(context.AvatarUri.LocalModelPath); + context = await Importer.Execute(context, CancellationToken.None); + // Process the avatar + var avatarProcessor = new AvatarProcessor(); + context = await avatarProcessor.Execute(context, CancellationToken.None); + + var avatar = (GameObject) context.Data; + avatar.SetActive(true); + + var avatarData = avatar.AddComponent(); + avatarData.AvatarId = avatar.name; + avatarData.AvatarMetadata = context.Metadata; + OnCompleted?.Invoke(context); + return context; + } + + private static async Task DownloadAvatarModel(AvatarUri avatarUri) + { + var folderPath = Path.Combine(Application.dataPath, "Ready Player Me/Avatars"); + // Ensure the folder exists + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + // Create the full file path + var fullPath = Path.Combine(folderPath, avatarUri.Guid + ".glb"); + + // Start the download + using (UnityWebRequest request = UnityWebRequest.Get(avatarUri.ModelUrl)) + { + Debug.Log($"Downloading {avatarUri.ModelUrl}..."); + var operation = request.SendWebRequest(); + + while (!operation.isDone) + { + await Task.Yield(); // Await completion of the web request + } + + if (request.result == UnityWebRequest.Result.Success) + { + // Write the downloaded data to the file + await File.WriteAllBytesAsync(fullPath, request.downloadHandler.data); + Debug.Log($"File saved to: {fullPath}"); + + // Refresh the AssetDatabase to recognize the new file + AssetDatabase.Refresh(); + Debug.Log("AssetDatabase refreshed."); + return fullPath; + } + Debug.LogError($"Failed to download file: {request.error}"); + return null; + } + } + +} diff --git a/Editor/Core/Scripts/EditorAvatarLoader.cs.meta b/Editor/Core/Scripts/EditorAvatarLoader.cs.meta new file mode 100644 index 00000000..77ead097 --- /dev/null +++ b/Editor/Core/Scripts/EditorAvatarLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9efec2cb7093d1b45a5363a7ec51b3bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderEditor.cs b/Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderWindow.cs similarity index 65% rename from Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderEditor.cs rename to Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderWindow.cs index d9bcca86..9c398e93 100644 --- a/Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderEditor.cs +++ b/Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderWindow.cs @@ -5,9 +5,8 @@ namespace ReadyPlayerMe.Core.Editor { - public class AvatarLoaderEditor : EditorWindow + public class AvatarLoaderWindow : EditorWindow { - private const string TAG = nameof(AvatarLoaderEditor); private const string AVATAR_LOADER = "Avatar Loader"; private const string LOAD_AVATAR_BUTTON = "LoadAvatarButton"; private const string HEADER_LABEL = "HeaderLabel"; @@ -25,11 +24,12 @@ public class AvatarLoaderEditor : EditorWindow private bool useEyeAnimations; private bool useVoiceToAnim; + private EditorAvatarLoader editorAvatarLoader; [MenuItem("Tools/Ready Player Me/Avatar Loader", priority = 1)] public static void ShowWindow() { - var window = GetWindow(); + var window = GetWindow(); window.titleContent = new GUIContent(AVATAR_LOADER); window.minSize = new Vector2(500, 300); } @@ -82,53 +82,23 @@ private void LoadAvatar(string url) { avatarLoaderSettings = AvatarLoaderSettings.LoadSettings(); } - var avatarLoader = new AvatarObjectLoader(); - avatarLoader.OnFailed += Failed; - avatarLoader.OnCompleted += Completed; - avatarLoader.OperationCompleted += OnOperationCompleted; - if (avatarLoaderSettings != null) - { - avatarLoader.AvatarConfig = avatarLoaderSettings.AvatarConfig; - if (avatarLoaderSettings.GLTFDeferAgent != null) - { - avatarLoader.GLTFDeferAgent = avatarLoaderSettings.GLTFDeferAgent; - } - } - avatarLoader.LoadAvatar(url); + editorAvatarLoader = new EditorAvatarLoader(); + editorAvatarLoader.OnCompleted += Completed; + editorAvatarLoader.Load(url); } - private void OnOperationCompleted(object sender, IOperation e) - { - if (e.GetType() == typeof(MetadataDownloader)) - { - AnalyticsEditorLogger.EventLogger.LogMetadataDownloaded(EditorApplication.timeSinceStartup - startTime); - } - } - - private void Failed(object sender, FailureEventArgs args) - { - Debug.LogError($"{args.Type} - {args.Message}"); - } - - private void Completed(object sender, CompletionEventArgs args) + private void Completed(AvatarContext context) { AnalyticsEditorLogger.EventLogger.LogAvatarLoaded(EditorApplication.timeSinceStartup - startTime); - if (avatarLoaderSettings == null) { avatarLoaderSettings = AvatarLoaderSettings.LoadSettings(); } - var paramHash = AvatarCache.GetAvatarConfigurationHash(avatarLoaderSettings.AvatarConfig); - var path = $"{DirectoryUtility.GetRelativeProjectPath(args.Avatar.name, paramHash)}/{args.Avatar.name}"; - if (!avatarLoaderSettings.AvatarCachingEnabled) - { - SDKLogger.LogWarning(TAG, "Enable Avatar Caching to generate a prefab in the project folder."); - return; - } - var avatar = PrefabHelper.CreateAvatarPrefab(args.Metadata, path, avatarConfig: avatarLoaderSettings.AvatarConfig); + var path = $@"Assets\Ready Player Me\Avatars\{context.AvatarUri.Guid}"; + var avatar = PrefabHelper.CreateAvatarPrefab(context.Metadata, path, avatarConfig: avatarLoaderSettings.AvatarConfig); if (useEyeAnimations) avatar.AddComponent(); if (useVoiceToAnim) avatar.AddComponent(); - DestroyImmediate(args.Avatar, true); + DestroyImmediate((GameObject) context.Data, true); Selection.activeObject = avatar; } } diff --git a/Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderEditor.cs.meta b/Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderWindow.cs.meta similarity index 100% rename from Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderEditor.cs.meta rename to Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderWindow.cs.meta diff --git a/Editor/Core/Scripts/Utilities/PrefabHelper.cs b/Editor/Core/Scripts/Utilities/PrefabHelper.cs index dd1d32ef..5203e1f1 100644 --- a/Editor/Core/Scripts/Utilities/PrefabHelper.cs +++ b/Editor/Core/Scripts/Utilities/PrefabHelper.cs @@ -6,6 +6,7 @@ namespace ReadyPlayerMe.Core.Editor public static class PrefabHelper { private const string TAG = nameof(PrefabHelper); + public static void TransferPrefabByGuid(string guid, string newPath) { var path = AssetDatabase.GUIDToAssetPath(guid); @@ -18,7 +19,7 @@ public static void TransferPrefabByGuid(string guid, string newPath) AssetDatabase.Refresh(); Selection.activeObject = AssetDatabase.LoadAssetAtPath(newPath, typeof(GameObject)); } - + public static GameObject CreateAvatarPrefab(AvatarMetadata avatarMetadata, string path, string prefabPath = null, AvatarConfig avatarConfig = null) { var modelFilePath = $"{path}.glb"; @@ -34,7 +35,7 @@ public static GameObject CreateAvatarPrefab(AvatarMetadata avatarMetadata, strin CreatePrefab(newAvatar, prefabPath ?? $"{path}.prefab"); return newAvatar; } - + public static void CreatePrefab(GameObject source, string path) { PrefabUtility.SaveAsPrefabAssetAndConnect(source, path, InteractionMode.AutomatedAction, out var success); diff --git a/Runtime/Core/Scripts/Caching/AvatarCache.cs b/Runtime/Core/Scripts/Caching/AvatarCache.cs index 388f864b..67d21773 100644 --- a/Runtime/Core/Scripts/Caching/AvatarCache.cs +++ b/Runtime/Core/Scripts/Caching/AvatarCache.cs @@ -21,11 +21,7 @@ public static string GetAvatarConfigurationHash(AvatarConfig avatarConfig = null /// Clears the avatars from the persistent cache. public static void Clear() { - var path = DirectoryUtility.GetAvatarsDirectoryPath(); - DeleteFolder(path); -#if UNITY_EDITOR DeleteFolder(DirectoryUtility.GetAvatarsPersistantPath()); -#endif } private static void DeleteFolder(string path) @@ -45,7 +41,7 @@ private static void DeleteFolder(string path) public static string[] GetExistingAvatarIds() { - var path = DirectoryUtility.GetAvatarsDirectoryPath(); + var path = DirectoryUtility.GetAvatarsPersistantPath(); if (!Directory.Exists(path)) return Array.Empty(); var directoryInfo = new DirectoryInfo(path); var avatarIds = directoryInfo.GetDirectories().Select(subdir => subdir.Name).ToArray(); @@ -55,43 +51,34 @@ public static string[] GetExistingAvatarIds() /// Deletes all data for a specific avatar variant (based on parameter hash) from persistent cache. public static void DeleteAvatarVariantFolder(string guid, string paramHash) { - DeleteFolder($"{DirectoryUtility.GetAvatarsDirectoryPath()}/{guid}/{paramHash}"); + DeleteFolder($"{DirectoryUtility.GetAvatarsPersistantPath()}/{guid}/{paramHash}"); } /// Deletes stored data a specific avatar from persistent cache. public static void DeleteAvatarFolder(string guid) { - var path = $"{DirectoryUtility.GetAvatarsDirectoryPath()}/{guid}"; + var path = $"{DirectoryUtility.GetAvatarsPersistantPath()}/{guid}"; DeleteFolder(path); } /// deletes a specific avatar model (.glb file) from persistent cache, while leaving the metadata.json file public static void DeleteAvatarModel(string guid, string parametersHash) { - var path = $"{DirectoryUtility.GetAvatarsDirectoryPath()}/{guid}/{parametersHash}"; + var path = $"{DirectoryUtility.GetAvatarsPersistantPath()}/{guid}/{parametersHash}"; if (Directory.Exists(path)) { var info = new DirectoryInfo(path); - -#if UNITY_EDITOR - foreach (DirectoryInfo dir in info.GetDirectories()) - { - AssetDatabase.DeleteAsset($"Assets/{DirectoryUtility.DefaultAvatarFolder}/{guid}/{dir.Name}"); - } - -#else foreach (DirectoryInfo dir in info.GetDirectories()) { Directory.Delete(dir.FullName, true); } -#endif } } /// Is there any avatars present in the persistent cache. public static bool IsCacheEmpty() { - var path = DirectoryUtility.GetAvatarsDirectoryPath(); + var path = DirectoryUtility.GetAvatarsPersistantPath(); return !Directory.Exists(path) || Directory.GetFiles(path).Length == 0 && Directory.GetDirectories(path).Length == 0; } @@ -99,7 +86,7 @@ public static bool IsCacheEmpty() /// Total Avatars stored in persistent cache. public static int GetAvatarCount() { - var path = DirectoryUtility.GetAvatarsDirectoryPath(); + var path = DirectoryUtility.GetAvatarsPersistantPath(); return !Directory.Exists(path) ? 0 : new DirectoryInfo(path).GetDirectories().Length; } @@ -107,7 +94,7 @@ public static int GetAvatarCount() /// Total Avatar variants stored for specific avatar GUID in persistent cache. public static int GetAvatarVariantCount(string avatarGuid) { - var path = $"{DirectoryUtility.GetAvatarsDirectoryPath()}/{avatarGuid}"; + var path = $"{DirectoryUtility.GetAvatarsPersistantPath()}/{avatarGuid}"; return !Directory.Exists(path) ? 0 : new DirectoryInfo(path).GetDirectories().Length; } @@ -115,19 +102,19 @@ public static int GetAvatarVariantCount(string avatarGuid) /// Total size of avatar stored in persistent cache. Returns total bytes. public static long GetCacheSize() { - var path = DirectoryUtility.GetAvatarsDirectoryPath(); + var path = DirectoryUtility.GetAvatarsPersistantPath(); return !Directory.Exists(path) ? 0 : DirectoryUtility.GetDirectorySize(new DirectoryInfo(path)); } public static float GetCacheSizeInMb() { - var path = DirectoryUtility.GetAvatarsDirectoryPath(); + var path = DirectoryUtility.GetAvatarsPersistantPath(); return !Directory.Exists(path) ? 0 : DirectoryUtility.GetFolderSizeInMb(path); } public static float GetAvatarDataSizeInMb(string avatarGuid) { - var path = $"{DirectoryUtility.GetAvatarsDirectoryPath()}/{avatarGuid}"; + var path = $"{DirectoryUtility.GetAvatarsPersistantPath()}/{avatarGuid}"; return DirectoryUtility.GetFolderSizeInMb(path); } } diff --git a/Runtime/Core/Scripts/Caching/AvatarManifest.cs b/Runtime/Core/Scripts/Caching/AvatarManifest.cs index 6c87f3cb..298c0d17 100644 --- a/Runtime/Core/Scripts/Caching/AvatarManifest.cs +++ b/Runtime/Core/Scripts/Caching/AvatarManifest.cs @@ -72,10 +72,9 @@ private void WriteToFile(string json) private string GetFilePath() { - return $"{DirectoryUtility.GetAvatarsDirectoryPath()}{RELATIVE_PATH}"; + return $"{DirectoryUtility.GetAvatarsPersistantPath()}{RELATIVE_PATH}"; } - private string ReadFromFile() { var path = GetFilePath(); diff --git a/Runtime/Core/Scripts/Utils/DirectoryUtility.cs b/Runtime/Core/Scripts/Utils/DirectoryUtility.cs index e4354c9c..7d6e81df 100644 --- a/Runtime/Core/Scripts/Utils/DirectoryUtility.cs +++ b/Runtime/Core/Scripts/Utils/DirectoryUtility.cs @@ -26,10 +26,10 @@ public static void ValidateDirectory(string path) public static string GetAvatarSaveDirectory(string guid, string paramsHash = null) { - return paramsHash == null ? $"{GetAvatarsDirectoryPath()}/{guid}" : $"{GetAvatarsDirectoryPath()}/{guid}/{paramsHash}"; + return paramsHash == null ? $"{GetAvatarsPersistantPath()}/{guid}" : $"{GetAvatarsPersistantPath()}/{guid}/{paramsHash}"; } - public static string GetRelativeProjectPath(string guid, string paramsHash = null) + public static string GetEditorStorageFolder(string guid, string paramsHash = null) { return paramsHash == null ? $"Assets/{DefaultAvatarFolder}/{guid}" : $"Assets/{DefaultAvatarFolder}/{guid}/{paramsHash}"; } @@ -37,11 +37,11 @@ public static string GetRelativeProjectPath(string guid, string paramsHash = nul public static long GetDirectorySize(DirectoryInfo directoryInfo) { // Add file sizes. - FileInfo[] fileInfos = directoryInfo.GetFiles(); + var fileInfos = directoryInfo.GetFiles(); var size = fileInfos.Sum(fi => fi.Length); // Add subdirectory sizes. - DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories(); + var directoryInfos = directoryInfo.GetDirectories(); size += directoryInfos.Sum(GetDirectorySize); return size; } @@ -57,15 +57,6 @@ private static float BytesToMegabytes(long bytes) return bytes / BYTES_IN_MEGABYTE; } - public static string GetAvatarsDirectoryPath() - { -#if UNITY_EDITOR - return $"{Application.dataPath}/{DefaultAvatarFolder}"; -#else - return GetAvatarsPersistantPath(); -#endif - } - public static string GetAvatarsPersistantPath() { return $"{Application.persistentDataPath}/{DefaultAvatarFolder}"; diff --git a/Tests/Editor/AvatarLoaderWindowTests.cs b/Tests/Editor/AvatarLoaderWindowTests.cs index dc3007a6..8908ea49 100644 --- a/Tests/Editor/AvatarLoaderWindowTests.cs +++ b/Tests/Editor/AvatarLoaderWindowTests.cs @@ -28,11 +28,11 @@ public void Cleanup() [UnityTest] public IEnumerator Avatar_Loaded_Stored_And_No_Overrides() { - var window = EditorWindow.GetWindow(); + var window = EditorWindow.GetWindow(); - var loadAvatarMethod = typeof(AvatarLoaderEditor).GetMethod("LoadAvatar", BindingFlags.NonPublic | BindingFlags.Instance); - var useEyeAnimationsToggle = typeof(AvatarLoaderEditor).GetField("useEyeAnimations", BindingFlags.NonPublic | BindingFlags.Instance); - var useVoiceToAnimToggle = typeof(AvatarLoaderEditor).GetField("useVoiceToAnim", BindingFlags.NonPublic | BindingFlags.Instance); + var loadAvatarMethod = typeof(AvatarLoaderWindow).GetMethod("LoadAvatar", BindingFlags.NonPublic | BindingFlags.Instance); + var useEyeAnimationsToggle = typeof(AvatarLoaderWindow).GetField("useEyeAnimations", BindingFlags.NonPublic | BindingFlags.Instance); + var useVoiceToAnimToggle = typeof(AvatarLoaderWindow).GetField("useVoiceToAnim", BindingFlags.NonPublic | BindingFlags.Instance); var previousUseEyeAnimations = (bool) useEyeAnimationsToggle.GetValue(window); var previousUseVoiceToAnim = (bool) useVoiceToAnimToggle.GetValue(window); useEyeAnimationsToggle.SetValue(window, false);