Skip to content

Commit

Permalink
Merge branch 'release/v7.4.0' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
HarrisonHough committed Jan 15, 2025
2 parents 5966ed9 + 2f6bb86 commit 0f11fca
Show file tree
Hide file tree
Showing 27 changed files with 351 additions and 124 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 125 additions & 0 deletions Editor/Core/Scripts/EditorAvatarLoader.cs
Original file line number Diff line number Diff line change
@@ -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<AvatarContext> executor;
private float startTime;

public Action<AvatarContext> OnCompleted;

/// <summary>
/// This class constructor is used to any required fields.
/// </summary>
/// <param name="useDefaultGLTFDeferAgent">Use default defer agent</param>
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;

/// <summary>
/// Runs through the process of loading the avatar and creating a game object via the <c>OperationExecutor</c>.
/// </summary>
/// <param name="url">The URL to the avatars .glb file.</param>
public async Task<AvatarContext> 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>();
avatarData.AvatarId = avatar.name;
avatarData.AvatarMetadata = context.Metadata;
OnCompleted?.Invoke(context);
return context;
}

private static async Task<string> 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;
}
}

}
11 changes: 11 additions & 0 deletions Editor/Core/Scripts/EditorAvatarLoader.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<AvatarLoaderEditor>();
var window = GetWindow<AvatarLoaderWindow>();
window.titleContent = new GUIContent(AVATAR_LOADER);
window.minSize = new Vector2(500, 300);
}
Expand Down Expand Up @@ -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<AvatarContext> 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<EyeAnimationHandler>();
if (useVoiceToAnim) avatar.AddComponent<VoiceHandler>();
DestroyImmediate(args.Avatar, true);
DestroyImmediate((GameObject) context.Data, true);
Selection.activeObject = avatar;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using ReadyPlayerMe.Core.Analytics;
using ReadyPlayerMe.Core.Data;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
Expand Down
5 changes: 3 additions & 2 deletions Editor/Core/Scripts/Utilities/PrefabHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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";
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions Runtime/AvatarCreator/Scripts/Managers/AvatarManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,50 @@ public async Task<GameObject> UpdateAsset(AssetType assetType, object assetId)

return await inCreatorAvatarLoader.Load(avatarId, gender, data);
}

public async Task<GameObject> UpdateAssets(Dictionary<AssetType, object> assetIdByType)
{
var payload = new AvatarProperties
{
Assets = new Dictionary<AssetType, object>()
};
// 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);
}

/// <summary>
/// Function that checks if body shapes are enabled in the studio. This validation is performed only in the editor.
Expand Down
6 changes: 3 additions & 3 deletions Runtime/Core/Scripts/Animation/VoiceHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using UnityEngine.Android;
#endif


namespace ReadyPlayerMe.Core
{
/// <summary>
Expand Down Expand Up @@ -111,9 +110,10 @@ public void InitializeAudio()
{
try
{

if (AudioSource == null)
{
AudioSource = gameObject.AddComponent<AudioSource>();
AudioSource = GetComponent<AudioSource>() ?? gameObject.AddComponent<AudioSource>();
}

switch (AudioProvider)
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit 0f11fca

Please sign in to comment.