diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b61181..4b32aaca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [7.4.0] - 2025.01.15 + +## Updated +- Avatar caching location is now changed when running in the Unity Editor it will now be stored in the `Application.persistentDataPath` directory as it already did for builds. However when loading avatars from the Avatar Loader Editor window it will still store them in the `Assets/Ready Player Me/Avatars folder`. +- AvatarManager and AvatarHandler classes updated so that in the Avatar Creator Elements sample it will re-equip hair when headwear is removed. [#330](https://github.com/readyplayerme/rpm-unity-sdk-core/pull/330) +- AvatarConfigProcessor updated so that by default if morph targets are not set, it will set it to None to improve file size. [#326](https://github.com/readyplayerme/rpm-unity-sdk-core/pull/326) + ## [7.3.1] - 2024.10.30 ## Updated diff --git a/Editor/Core/Scripts/EditorAvatarLoader.cs b/Editor/Core/Scripts/EditorAvatarLoader.cs new file mode 100644 index 00000000..bafc75f9 --- /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/{avatarUri.Guid}"); + // 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/UI/EditorWindows/SetupGuide/SetupGuide.cs b/Editor/Core/Scripts/UI/EditorWindows/SetupGuide/SetupGuide.cs index 55ead9b0..75fa87bd 100644 --- a/Editor/Core/Scripts/UI/EditorWindows/SetupGuide/SetupGuide.cs +++ b/Editor/Core/Scripts/UI/EditorWindows/SetupGuide/SetupGuide.cs @@ -1,5 +1,4 @@ using ReadyPlayerMe.Core.Analytics; -using ReadyPlayerMe.Core.Data; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; 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/README.md b/README.md index 15a70e38..42dcb604 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ Steps for trying out avatar creator sample can be found [here.](Documentation~/A A guide for customizing avatar creator can be found [here.](Documentation~/CustomizationGuide.md) ### Note -- [*]Camera support is only provided for Windows and WebGL, using Unity’s webcam native API. +- Camera support is only provided for Windows and WebGL, using Unity’s webcam native API. - Unity does not have a native file picker, so we have discontinued support for this feature. - To add support for file picker (for selfies) you have to implement it yourself diff --git a/Runtime/AvatarCreator/Scripts/Managers/AvatarManager.cs b/Runtime/AvatarCreator/Scripts/Managers/AvatarManager.cs index 46a7558f..417f274c 100644 --- a/Runtime/AvatarCreator/Scripts/Managers/AvatarManager.cs +++ b/Runtime/AvatarCreator/Scripts/Managers/AvatarManager.cs @@ -229,6 +229,50 @@ public async Task UpdateAsset(AssetType assetType, object assetId) return await inCreatorAvatarLoader.Load(avatarId, gender, data); } + + public async Task UpdateAssets(Dictionary assetIdByType) + { + var payload = new AvatarProperties + { + Assets = new Dictionary() + }; + // if it contains top, bottom or footwear, remove outfit + if (assetIdByType.ContainsKey(AssetType.Top) || assetIdByType.ContainsKey(AssetType.Bottom) || assetIdByType.ContainsKey(AssetType.Footwear)) + { + payload.Assets.Add(AssetType.Outfit, string.Empty); + } + + // Convert costume to outfit + foreach (var assetType in assetIdByType.Keys) + { + payload.Assets.Add(assetType == AssetType.Costume ? AssetType.Outfit : assetType, assetIdByType[assetType]); + } + + byte[] data; + try + { + data = await avatarAPIRequests.UpdateAvatar(avatarId, payload, avatarConfigParameters); + } + catch (Exception e) + { + HandleException(e); + return null; + } + + if (ctxSource.IsCancellationRequested) + { + return null; + } + foreach (var assetType in assetIdByType) + { + if (assetType.Key != AssetType.BodyShape) + { + await ValidateBodyShapeUpdate(assetType.Key, assetType.Value); + } + } + + return await inCreatorAvatarLoader.Load(avatarId, gender, data); + } /// /// Function that checks if body shapes are enabled in the studio. This validation is performed only in the editor. diff --git a/Runtime/Core/Scripts/Animation/VoiceHandler.cs b/Runtime/Core/Scripts/Animation/VoiceHandler.cs index 61e59a68..64713573 100644 --- a/Runtime/Core/Scripts/Animation/VoiceHandler.cs +++ b/Runtime/Core/Scripts/Animation/VoiceHandler.cs @@ -8,7 +8,6 @@ using UnityEngine.Android; #endif - namespace ReadyPlayerMe.Core { /// @@ -111,9 +110,10 @@ public void InitializeAudio() { try { + if (AudioSource == null) { - AudioSource = gameObject.AddComponent(); + AudioSource = GetComponent() ?? gameObject.AddComponent(); } switch (AudioProvider) @@ -169,7 +169,7 @@ private float GetAmplitude() { var currentPosition = AudioSource.timeSamples; var remaining = AudioSource.clip.samples - currentPosition; - if (remaining > 0 && remaining < AUDIO_SAMPLE_LENGTH) + if (remaining >= 0 && remaining < AUDIO_SAMPLE_LENGTH) { return 0f; } 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/Data/ApplicationData.cs b/Runtime/Core/Scripts/Data/ApplicationData.cs index 8574e617..885911a1 100644 --- a/Runtime/Core/Scripts/Data/ApplicationData.cs +++ b/Runtime/Core/Scripts/Data/ApplicationData.cs @@ -6,7 +6,7 @@ namespace ReadyPlayerMe.Core { public static class ApplicationData { - public const string SDK_VERSION = "v7.3.1"; + public const string SDK_VERSION = "v7.4.0"; private const string TAG = "ApplicationData"; private const string DEFAULT_RENDER_PIPELINE = "Built-In Render Pipeline"; private static readonly AppData Data; diff --git a/Runtime/Core/Scripts/Data/ScriptableObjects/AvatarConfig.cs b/Runtime/Core/Scripts/Data/ScriptableObjects/AvatarConfig.cs index 30635c96..625989f8 100644 --- a/Runtime/Core/Scripts/Data/ScriptableObjects/AvatarConfig.cs +++ b/Runtime/Core/Scripts/Data/ScriptableObjects/AvatarConfig.cs @@ -60,6 +60,9 @@ public class AvatarConfig : ScriptableObject new ShaderPropertyMapping("occlusionTexture_strength", "", ShaderPropertyType.Float), }; - public List MorphTargets = new List(); + public List MorphTargets = new List + { + "none", + }; } } diff --git a/Runtime/Core/Scripts/Utils/AvatarAPIParameters.cs b/Runtime/Core/Scripts/Utils/AvatarAPIParameters.cs index dc7ed394..d21809d0 100644 --- a/Runtime/Core/Scripts/Utils/AvatarAPIParameters.cs +++ b/Runtime/Core/Scripts/Utils/AvatarAPIParameters.cs @@ -7,6 +7,7 @@ public static class AvatarAPIParameters public const string TEXTURE_ATLAS = "textureAtlas"; public const string TEXTURE_SIZE_LIMIT = "textureSizeLimit"; public const string TEXTURE_CHANNELS = "textureChannels"; + public const string TEXTURE_FORMAT = "textureFormat"; public const string MORPH_TARGETS = "morphTargets"; public const string USE_HANDS = "useHands"; public const string USE_DRACO = "useDracoMeshCompression"; diff --git a/Runtime/Core/Scripts/Utils/AvatarConfigProcessor.cs b/Runtime/Core/Scripts/Utils/AvatarConfigProcessor.cs index 572c48f1..11cc2662 100644 --- a/Runtime/Core/Scripts/Utils/AvatarConfigProcessor.cs +++ b/Runtime/Core/Scripts/Utils/AvatarConfigProcessor.cs @@ -34,9 +34,16 @@ public static string ProcessAvatarConfiguration(AvatarConfig avatarConfig) { queryBuilder.AddKeyValue(AvatarAPIParameters.MORPH_TARGETS, CombineMorphTargetNames(avatarConfig.MorphTargets)); } + else + { + // If no morph targets are set, we set the value to "none" to prevent unwanted blendshapes. + queryBuilder.AddKeyValue(AvatarAPIParameters.MORPH_TARGETS, "none"); + } queryBuilder.AddKeyValue(AvatarAPIParameters.USE_HANDS, GetBoolStringValue(avatarConfig.UseHands)); queryBuilder.AddKeyValue(AvatarAPIParameters.USE_DRACO, GetBoolStringValue(avatarConfig.UseDracoCompression)); queryBuilder.AddKeyValue(AvatarAPIParameters.USE_MESHOPT, GetBoolStringValue(avatarConfig.UseMeshOptCompression)); + // TODO: Add later when edge cases are handled. + //queryBuilder.AddKeyValue(AvatarAPIParameters.TEXTURE_FORMAT, "jpeg"); return queryBuilder.Query; } 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/Runtime/Core/Scripts/Utils/WebRequestDispatcher.cs b/Runtime/Core/Scripts/Utils/WebRequestDispatcher.cs index 066386ca..0dfd701d 100644 --- a/Runtime/Core/Scripts/Utils/WebRequestDispatcher.cs +++ b/Runtime/Core/Scripts/Utils/WebRequestDispatcher.cs @@ -44,7 +44,6 @@ public async Task SendRequest( request.SetRequestHeader(header.Key, header.Value); } } - downloadHandler ??= new DownloadHandlerBuffer(); request.downloadHandler = downloadHandler; diff --git a/Samples~/AvatarCreatorSamples/AvatarCreatorElements/Scripts/AvatarHandler.cs b/Samples~/AvatarCreatorSamples/AvatarCreatorElements/Scripts/AvatarHandler.cs index abcb26c3..9bdbef82 100644 --- a/Samples~/AvatarCreatorSamples/AvatarCreatorElements/Scripts/AvatarHandler.cs +++ b/Samples~/AvatarCreatorSamples/AvatarCreatorElements/Scripts/AvatarHandler.cs @@ -1,11 +1,10 @@ -using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ReadyPlayerMe.AvatarCreator; using ReadyPlayerMe.Core; using UnityEngine; using UnityEngine.Events; -using UnityEngine.Serialization; using TaskExtensions = ReadyPlayerMe.AvatarCreator.TaskExtensions; namespace ReadyPlayerMe.Samples.AvatarCreatorElements @@ -35,7 +34,19 @@ public async Task LoadAvatar(string avatarId) public async Task SelectAsset(IAssetData assetData) { OnAvatarLoading?.Invoke(); - var newAvatar = await avatarManager.UpdateAsset(assetData.AssetType, assetData.Id); + GameObject newAvatar; + if (assetData.AssetType == AssetType.Headwear && ActiveAvatarProperties.Assets.ContainsKey(AssetType.HairStyle)) + { + var assets = new Dictionary(); + assets.Add(AssetType.HairStyle, ActiveAvatarProperties.Assets[AssetType.HairStyle]); + assets.Add(AssetType.Headwear, assetData.Id); + newAvatar = await avatarManager.UpdateAssets(assets); + } + else + { + newAvatar = await avatarManager.UpdateAsset(assetData.AssetType, assetData.Id); + } + ActiveAvatarProperties.Assets[assetData.AssetType] = assetData.Id; SetupLoadedAvatar(newAvatar, ActiveAvatarProperties); } diff --git a/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/AssetButtonCreator.cs b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/AssetButtonCreator.cs index dde1ab1f..a8832aaa 100644 --- a/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/AssetButtonCreator.cs +++ b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/AssetButtonCreator.cs @@ -71,7 +71,7 @@ private void SetSelectedIcon(string assetId, AssetType category) SelectButton(category, buttonsById[assetId]); } - public void CreateColorUI(Dictionary colorLibrary, Action onClick) + public void CreateColorUI(Dictionary colorLibrary, Action onClick, Dictionary colorAssets) { foreach (var colorPalette in colorLibrary) { @@ -82,8 +82,13 @@ public void CreateColorUI(Dictionary colorLibrary, Acti var button = AddColorButton(assetIndex, parent.transform, colorPalette.Key, onClick); button.SetColor(assetColor.HexColor); - // By default first color is applied on initial draft - if (assetIndex == 0) + int equippedValue = 0; + if (colorAssets.TryGetValue(assetColor.AssetType, out var value)) + { + equippedValue = value; + } + + if (assetIndex == equippedValue) { SelectButton(colorPalette.Key, button); } @@ -171,31 +176,31 @@ private void ConfigureOutfitSelection(AssetType category) case AssetType.Top: case AssetType.Bottom: case AssetType.Footwear: - { - if (selectedButtonsByCategory.TryGetValue(AssetType.Outfit, out AssetButton outfitButton)) { - outfitButton.SetSelect(false); + if (selectedButtonsByCategory.TryGetValue(AssetType.Outfit, out AssetButton outfitButton)) + { + outfitButton.SetSelect(false); + } + break; } - break; - } case AssetType.Outfit: - { - if (selectedButtonsByCategory.TryGetValue(AssetType.Top, out AssetButton topButton)) { - topButton.SetSelect(false); + if (selectedButtonsByCategory.TryGetValue(AssetType.Top, out AssetButton topButton)) + { + topButton.SetSelect(false); + } + + if (selectedButtonsByCategory.TryGetValue(AssetType.Bottom, out AssetButton bottomButton)) + { + bottomButton.SetSelect(false); + } + + if (selectedButtonsByCategory.TryGetValue(AssetType.Footwear, out AssetButton footwearButton)) + { + footwearButton.SetSelect(false); + } + break; } - - if (selectedButtonsByCategory.TryGetValue(AssetType.Bottom, out AssetButton bottomButton)) - { - bottomButton.SetSelect(false); - } - - if (selectedButtonsByCategory.TryGetValue(AssetType.Footwear, out AssetButton footwearButton)) - { - footwearButton.SetSelect(false); - } - break; - } } } diff --git a/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs index 0452a565..eda29bd6 100644 --- a/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs +++ b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Reflection; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -171,6 +173,7 @@ private async Task LoadAvatar() else { var id = AvatarCreatorData.AvatarProperties.Id; + if (!AvatarCreatorData.IsExistingAvatar) { var avatarTemplateResponse = await avatarManager.CreateAvatarFromTemplateAsync(id); @@ -198,10 +201,32 @@ private async Task LoadAvatarColors() { var startTime = Time.time; var colors = await avatarManager.LoadAvatarColors(); - assetButtonCreator.CreateColorUI(colors, UpdateAvatar); + var equippedColors = GetEquippedColors(); + + assetButtonCreator.CreateColorUI(colors, UpdateAvatar, equippedColors); SDKLogger.Log(TAG, $"All colors loaded in {Time.time - startTime:F2}s"); } + private Dictionary GetEquippedColors() + { + var colorAssetTypes = AssetTypeHelper.GetAssetTypesByFilter(AssetFilter.Color).ToHashSet(); + return AvatarCreatorData.AvatarProperties.Assets + .Where(kvp => colorAssetTypes.Contains(kvp.Key)) + .ToDictionary( + kvp => kvp.Key, + kvp => + { + try + { + return Convert.ToInt32(kvp.Value); + } + catch + { + return 0; + } + }); + } + private void CreateUI() { categoryUICreator.Setup(); diff --git a/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/Utils/AssetTypeHelper.cs b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/Utils/AssetTypeHelper.cs new file mode 100644 index 00000000..5cc4f65c --- /dev/null +++ b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/Utils/AssetTypeHelper.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using ReadyPlayerMe.AvatarCreator; + +namespace ReadyPlayerMe.Samples.AvatarCreatorWizard +{ + public static class AssetTypeHelper + { + public static IEnumerable GetAssetTypesByFilter(AssetFilter filter) + { + return Enum.GetValues(typeof(AssetType)) + .Cast() + .Where(assetType => + { + var fieldInfo = typeof(AssetType).GetField(assetType.ToString()); + var attribute = fieldInfo?.GetCustomAttribute(); + return attribute?.filter == filter; + }); + } + } +} \ No newline at end of file diff --git a/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/Utils/AssetTypeHelper.cs.meta b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/Utils/AssetTypeHelper.cs.meta new file mode 100644 index 00000000..2200ab84 --- /dev/null +++ b/Samples~/AvatarCreatorSamples/AvatarCreatorWizard/Scripts/Utils/AssetTypeHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6db2b85ffc40417478f7b3a97b15401e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/AvatarConfigProcessorTests.cs b/Tests/Editor/AvatarConfigProcessorTests.cs index b06a10d8..5bb36701 100644 --- a/Tests/Editor/AvatarConfigProcessorTests.cs +++ b/Tests/Editor/AvatarConfigProcessorTests.cs @@ -11,7 +11,7 @@ public class AvatarConfigProcessorTests private const string MORPHTARGETS_EXPECTED_DEFAULT = "mouthOpen,mouthSmile"; private const string MORPHTARGETS_EXPECTED_NONE = "none"; private const string AVATAR_QUERY_PARAMS_ACTUAL = - "pose=T&lod=0&textureAtlas=none&textureSizeLimit=1024&textureChannels=baseColor,normal,metallicRoughness,emissive,occlusion&useHands=false&useDracoMeshCompression=false&useMeshOptCompression=false"; + "pose=T&lod=0&textureAtlas=none&textureSizeLimit=1024&textureChannels=baseColor,normal,metallicRoughness,emissive,occlusion&morphTargets=none&useHands=false&useDracoMeshCompression=false&useMeshOptCompression=false&textureFormat=jpeg"; private readonly string[] morphTargetsDefault = { "mouthOpen", "mouthSmile" }; private readonly string[] morphTargetsNone = { "none" }; private readonly TextureChannel[] textureChannelsAll = diff --git a/Tests/Editor/AvatarLoaderWindowTests.cs b/Tests/Editor/AvatarLoaderWindowTests.cs index dc3007a6..dbe0eb21 100644 --- a/Tests/Editor/AvatarLoaderWindowTests.cs +++ b/Tests/Editor/AvatarLoaderWindowTests.cs @@ -17,8 +17,7 @@ public class AvatarLoaderWindowTests [TearDown] public void Cleanup() { - TestUtils.DeleteAvatarDirectoryIfExists(TestAvatarData.DefaultAvatarUri.Guid, true); - TestUtils.DeleteCachedAvatar(TestAvatarData.DefaultAvatarUri.Guid); + TestUtils.DeleteEditorAvatarDirectoryIfExists(TestAvatarData.DefaultAvatarUri.Guid, true); if (avatar != null) { Object.DestroyImmediate(avatar); @@ -28,11 +27,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); @@ -46,7 +45,7 @@ public IEnumerator Avatar_Loaded_Stored_And_No_Overrides() { yield return null; avatar = GameObject.Find(TestAvatarData.DefaultAvatarUri.Guid); - } while (avatar == null && System.DateTime.Now.Subtract(time).Seconds < 5); + } while (avatar == null && System.DateTime.Now.Subtract(time).Seconds < 10); window.Close(); Assert.IsNotNull(avatar); diff --git a/Tests/Editor/Common/TestUtils.cs b/Tests/Editor/Common/TestUtils.cs index d949dff0..0f070681 100644 --- a/Tests/Editor/Common/TestUtils.cs +++ b/Tests/Editor/Common/TestUtils.cs @@ -31,6 +31,15 @@ public static void DeleteAvatarDirectoryIfExists(string avatarGuid, bool recursi } } + public static void DeleteEditorAvatarDirectoryIfExists(string avatarGuid, bool recursive = false) + { + var path = $"{Application.dataPath}/Assets/Ready Player Me/Avatars/{avatarGuid}"; + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive); + } + } + public static void DeleteCachedAvatar(string avatarGuid) { var deleteAsset = AssetDatabase.DeleteAsset($"Assets/Ready Player Me/Avatars/{avatarGuid}"); diff --git a/package.json b/package.json index e06673c0..40c3961d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.readyplayerme.core", - "version": "7.3.1", + "version": "7.4.0", "displayName": "Ready Player Me Core", "description": "This Module contains all the core functionality required for using Ready Player Me avatars in Unity, including features such as: \n - Module management and automatic package setup logic\n - Avatar loading from .glb files \n - Avatar creation \n - Avatar and 2D render requests \n - Optional Analytics\n - Custom editor windows\n - Sample scenes and assets", "unity": "2020.3",