Skip to content

Commit 0f11fca

Browse files
committed
Merge branch 'release/v7.4.0' into main
2 parents 5966ed9 + 2f6bb86 commit 0f11fca

File tree

27 files changed

+351
-124
lines changed

27 files changed

+351
-124
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55

6+
## [7.4.0] - 2025.01.15
7+
8+
## Updated
9+
- 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`.
10+
- 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)
11+
- 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)
12+
613
## [7.3.1] - 2024.10.30
714

815
## Updated
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using ReadyPlayerMe.Core;
6+
using ReadyPlayerMe.Loader;
7+
using UnityEditor;
8+
using UnityEngine;
9+
using UnityEngine.Networking;
10+
using Object = UnityEngine.Object;
11+
12+
public class EditorAvatarLoader
13+
{
14+
private const string TAG = nameof(EditorAvatarLoader);
15+
16+
private readonly bool avatarCachingEnabled;
17+
18+
/// Scriptable Object Avatar API request parameters configuration
19+
public AvatarConfig AvatarConfig;
20+
21+
/// Importer to use to import glTF
22+
public IImporter Importer;
23+
24+
private string avatarUrl;
25+
private OperationExecutor<AvatarContext> executor;
26+
private float startTime;
27+
28+
public Action<AvatarContext> OnCompleted;
29+
30+
/// <summary>
31+
/// This class constructor is used to any required fields.
32+
/// </summary>
33+
/// <param name="useDefaultGLTFDeferAgent">Use default defer agent</param>
34+
public EditorAvatarLoader()
35+
{
36+
AvatarLoaderSettings loaderSettings = AvatarLoaderSettings.LoadSettings();
37+
Importer = new GltFastAvatarImporter();
38+
AvatarConfig = loaderSettings.AvatarConfig != null ? loaderSettings.AvatarConfig : null;
39+
}
40+
41+
/// Set the timeout for download requests
42+
public int Timeout { get; set; } = 20;
43+
44+
/// <summary>
45+
/// Runs through the process of loading the avatar and creating a game object via the <c>OperationExecutor</c>.
46+
/// </summary>
47+
/// <param name="url">The URL to the avatars .glb file.</param>
48+
public async Task<AvatarContext> Load(string url)
49+
{
50+
var context = new AvatarContext();
51+
context.Url = url;
52+
context.AvatarCachingEnabled = false;
53+
context.AvatarConfig = AvatarConfig;
54+
context.ParametersHash = AvatarCache.GetAvatarConfigurationHash(AvatarConfig);
55+
56+
// process url
57+
var urlProcessor = new UrlProcessor();
58+
context = await urlProcessor.Execute(context, CancellationToken.None);
59+
// get metadata
60+
var metadataDownloader = new MetadataDownloader();
61+
context = await metadataDownloader.Execute(context, CancellationToken.None);
62+
//download avatar into asset folder
63+
context.AvatarUri.LocalModelPath = await DownloadAvatarModel(context.AvatarUri);
64+
if (string.IsNullOrEmpty(context.AvatarUri.LocalModelPath))
65+
{
66+
Debug.LogError($"Failed to download avatar model from {context.AvatarUri.ModelUrl}");
67+
return null;
68+
}
69+
// import model
70+
context.Bytes = await File.ReadAllBytesAsync(context.AvatarUri.LocalModelPath);
71+
context = await Importer.Execute(context, CancellationToken.None);
72+
// Process the avatar
73+
var avatarProcessor = new AvatarProcessor();
74+
context = await avatarProcessor.Execute(context, CancellationToken.None);
75+
76+
var avatar = (GameObject) context.Data;
77+
avatar.SetActive(true);
78+
79+
var avatarData = avatar.AddComponent<AvatarData>();
80+
avatarData.AvatarId = avatar.name;
81+
avatarData.AvatarMetadata = context.Metadata;
82+
OnCompleted?.Invoke(context);
83+
return context;
84+
}
85+
86+
private static async Task<string> DownloadAvatarModel(AvatarUri avatarUri)
87+
{
88+
var folderPath = Path.Combine(Application.dataPath, $"Ready Player Me/Avatars/{avatarUri.Guid}");
89+
// Ensure the folder exists
90+
if (!Directory.Exists(folderPath))
91+
{
92+
Directory.CreateDirectory(folderPath);
93+
}
94+
95+
// Create the full file path
96+
var fullPath = Path.Combine(folderPath, avatarUri.Guid + ".glb");
97+
98+
// Start the download
99+
using (UnityWebRequest request = UnityWebRequest.Get(avatarUri.ModelUrl))
100+
{
101+
Debug.Log($"Downloading {avatarUri.ModelUrl}...");
102+
var operation = request.SendWebRequest();
103+
104+
while (!operation.isDone)
105+
{
106+
await Task.Yield(); // Await completion of the web request
107+
}
108+
109+
if (request.result == UnityWebRequest.Result.Success)
110+
{
111+
// Write the downloaded data to the file
112+
await File.WriteAllBytesAsync(fullPath, request.downloadHandler.data);
113+
Debug.Log($"File saved to: {fullPath}");
114+
115+
// Refresh the AssetDatabase to recognize the new file
116+
AssetDatabase.Refresh();
117+
Debug.Log("AssetDatabase refreshed.");
118+
return fullPath;
119+
}
120+
Debug.LogError($"Failed to download file: {request.error}");
121+
return null;
122+
}
123+
}
124+
125+
}

Editor/Core/Scripts/EditorAvatarLoader.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderEditor.cs renamed to Editor/Core/Scripts/UI/EditorWindows/AvatarLoaderEditor/AvatarLoaderWindow.cs

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
namespace ReadyPlayerMe.Core.Editor
77
{
8-
public class AvatarLoaderEditor : EditorWindow
8+
public class AvatarLoaderWindow : EditorWindow
99
{
10-
private const string TAG = nameof(AvatarLoaderEditor);
1110
private const string AVATAR_LOADER = "Avatar Loader";
1211
private const string LOAD_AVATAR_BUTTON = "LoadAvatarButton";
1312
private const string HEADER_LABEL = "HeaderLabel";
@@ -25,11 +24,12 @@ public class AvatarLoaderEditor : EditorWindow
2524

2625
private bool useEyeAnimations;
2726
private bool useVoiceToAnim;
27+
private EditorAvatarLoader editorAvatarLoader;
2828

2929
[MenuItem("Tools/Ready Player Me/Avatar Loader", priority = 1)]
3030
public static void ShowWindow()
3131
{
32-
var window = GetWindow<AvatarLoaderEditor>();
32+
var window = GetWindow<AvatarLoaderWindow>();
3333
window.titleContent = new GUIContent(AVATAR_LOADER);
3434
window.minSize = new Vector2(500, 300);
3535
}
@@ -82,53 +82,23 @@ private void LoadAvatar(string url)
8282
{
8383
avatarLoaderSettings = AvatarLoaderSettings.LoadSettings();
8484
}
85-
var avatarLoader = new AvatarObjectLoader();
86-
avatarLoader.OnFailed += Failed;
87-
avatarLoader.OnCompleted += Completed;
88-
avatarLoader.OperationCompleted += OnOperationCompleted;
89-
if (avatarLoaderSettings != null)
90-
{
91-
avatarLoader.AvatarConfig = avatarLoaderSettings.AvatarConfig;
92-
if (avatarLoaderSettings.GLTFDeferAgent != null)
93-
{
94-
avatarLoader.GLTFDeferAgent = avatarLoaderSettings.GLTFDeferAgent;
95-
}
96-
}
97-
avatarLoader.LoadAvatar(url);
85+
editorAvatarLoader = new EditorAvatarLoader();
86+
editorAvatarLoader.OnCompleted += Completed;
87+
editorAvatarLoader.Load(url);
9888
}
9989

100-
private void OnOperationCompleted(object sender, IOperation<AvatarContext> e)
101-
{
102-
if (e.GetType() == typeof(MetadataDownloader))
103-
{
104-
AnalyticsEditorLogger.EventLogger.LogMetadataDownloaded(EditorApplication.timeSinceStartup - startTime);
105-
}
106-
}
107-
108-
private void Failed(object sender, FailureEventArgs args)
109-
{
110-
Debug.LogError($"{args.Type} - {args.Message}");
111-
}
112-
113-
private void Completed(object sender, CompletionEventArgs args)
90+
private void Completed(AvatarContext context)
11491
{
11592
AnalyticsEditorLogger.EventLogger.LogAvatarLoaded(EditorApplication.timeSinceStartup - startTime);
116-
11793
if (avatarLoaderSettings == null)
11894
{
11995
avatarLoaderSettings = AvatarLoaderSettings.LoadSettings();
12096
}
121-
var paramHash = AvatarCache.GetAvatarConfigurationHash(avatarLoaderSettings.AvatarConfig);
122-
var path = $"{DirectoryUtility.GetRelativeProjectPath(args.Avatar.name, paramHash)}/{args.Avatar.name}";
123-
if (!avatarLoaderSettings.AvatarCachingEnabled)
124-
{
125-
SDKLogger.LogWarning(TAG, "Enable Avatar Caching to generate a prefab in the project folder.");
126-
return;
127-
}
128-
var avatar = PrefabHelper.CreateAvatarPrefab(args.Metadata, path, avatarConfig: avatarLoaderSettings.AvatarConfig);
97+
var path = $@"Assets\Ready Player Me\Avatars\{context.AvatarUri.Guid}";
98+
var avatar = PrefabHelper.CreateAvatarPrefab(context.Metadata, path, avatarConfig: avatarLoaderSettings.AvatarConfig);
12999
if (useEyeAnimations) avatar.AddComponent<EyeAnimationHandler>();
130100
if (useVoiceToAnim) avatar.AddComponent<VoiceHandler>();
131-
DestroyImmediate(args.Avatar, true);
101+
DestroyImmediate((GameObject) context.Data, true);
132102
Selection.activeObject = avatar;
133103
}
134104
}

Editor/Core/Scripts/UI/EditorWindows/SetupGuide/SetupGuide.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using ReadyPlayerMe.Core.Analytics;
2-
using ReadyPlayerMe.Core.Data;
32
using UnityEditor;
43
using UnityEditor.UIElements;
54
using UnityEngine;

Editor/Core/Scripts/Utilities/PrefabHelper.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace ReadyPlayerMe.Core.Editor
66
public static class PrefabHelper
77
{
88
private const string TAG = nameof(PrefabHelper);
9+
910
public static void TransferPrefabByGuid(string guid, string newPath)
1011
{
1112
var path = AssetDatabase.GUIDToAssetPath(guid);
@@ -18,7 +19,7 @@ public static void TransferPrefabByGuid(string guid, string newPath)
1819
AssetDatabase.Refresh();
1920
Selection.activeObject = AssetDatabase.LoadAssetAtPath(newPath, typeof(GameObject));
2021
}
21-
22+
2223
public static GameObject CreateAvatarPrefab(AvatarMetadata avatarMetadata, string path, string prefabPath = null, AvatarConfig avatarConfig = null)
2324
{
2425
var modelFilePath = $"{path}.glb";
@@ -34,7 +35,7 @@ public static GameObject CreateAvatarPrefab(AvatarMetadata avatarMetadata, strin
3435
CreatePrefab(newAvatar, prefabPath ?? $"{path}.prefab");
3536
return newAvatar;
3637
}
37-
38+
3839
public static void CreatePrefab(GameObject source, string path)
3940
{
4041
PrefabUtility.SaveAsPrefabAssetAndConnect(source, path, InteractionMode.AutomatedAction, out var success);

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ Steps for trying out avatar creator sample can be found [here.](Documentation~/A
3434
A guide for customizing avatar creator can be found [here.](Documentation~/CustomizationGuide.md)
3535

3636
### Note
37-
- [*]Camera support is only provided for Windows and WebGL, using Unity’s webcam native API.
37+
- Camera support is only provided for Windows and WebGL, using Unity’s webcam native API.
3838
- Unity does not have a native file picker, so we have discontinued support for this feature.
3939
- To add support for file picker (for selfies) you have to implement it yourself

Runtime/AvatarCreator/Scripts/Managers/AvatarManager.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,50 @@ public async Task<GameObject> UpdateAsset(AssetType assetType, object assetId)
229229

230230
return await inCreatorAvatarLoader.Load(avatarId, gender, data);
231231
}
232+
233+
public async Task<GameObject> UpdateAssets(Dictionary<AssetType, object> assetIdByType)
234+
{
235+
var payload = new AvatarProperties
236+
{
237+
Assets = new Dictionary<AssetType, object>()
238+
};
239+
// if it contains top, bottom or footwear, remove outfit
240+
if (assetIdByType.ContainsKey(AssetType.Top) || assetIdByType.ContainsKey(AssetType.Bottom) || assetIdByType.ContainsKey(AssetType.Footwear))
241+
{
242+
payload.Assets.Add(AssetType.Outfit, string.Empty);
243+
}
244+
245+
// Convert costume to outfit
246+
foreach (var assetType in assetIdByType.Keys)
247+
{
248+
payload.Assets.Add(assetType == AssetType.Costume ? AssetType.Outfit : assetType, assetIdByType[assetType]);
249+
}
250+
251+
byte[] data;
252+
try
253+
{
254+
data = await avatarAPIRequests.UpdateAvatar(avatarId, payload, avatarConfigParameters);
255+
}
256+
catch (Exception e)
257+
{
258+
HandleException(e);
259+
return null;
260+
}
261+
262+
if (ctxSource.IsCancellationRequested)
263+
{
264+
return null;
265+
}
266+
foreach (var assetType in assetIdByType)
267+
{
268+
if (assetType.Key != AssetType.BodyShape)
269+
{
270+
await ValidateBodyShapeUpdate(assetType.Key, assetType.Value);
271+
}
272+
}
273+
274+
return await inCreatorAvatarLoader.Load(avatarId, gender, data);
275+
}
232276

233277
/// <summary>
234278
/// Function that checks if body shapes are enabled in the studio. This validation is performed only in the editor.

Runtime/Core/Scripts/Animation/VoiceHandler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using UnityEngine.Android;
99
#endif
1010

11-
1211
namespace ReadyPlayerMe.Core
1312
{
1413
/// <summary>
@@ -111,9 +110,10 @@ public void InitializeAudio()
111110
{
112111
try
113112
{
113+
114114
if (AudioSource == null)
115115
{
116-
AudioSource = gameObject.AddComponent<AudioSource>();
116+
AudioSource = GetComponent<AudioSource>() ?? gameObject.AddComponent<AudioSource>();
117117
}
118118

119119
switch (AudioProvider)
@@ -169,7 +169,7 @@ private float GetAmplitude()
169169
{
170170
var currentPosition = AudioSource.timeSamples;
171171
var remaining = AudioSource.clip.samples - currentPosition;
172-
if (remaining > 0 && remaining < AUDIO_SAMPLE_LENGTH)
172+
if (remaining >= 0 && remaining < AUDIO_SAMPLE_LENGTH)
173173
{
174174
return 0f;
175175
}

0 commit comments

Comments
 (0)