From 15ae460b7206aabb9b278abb764c273a5f0639cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=C3=AF=7E?= Date: Mon, 4 May 2026 16:33:32 +0200 Subject: [PATCH] feat: prepare ability to bundle additional assets in avatar --- .../BasisLoadhandler.cs | 53 +++++++++++++++++++ .../Scripts/Basis Components/BasisAvatar.cs | 6 +++ .../Scripts/BasisObjectReferenceContainer.cs | 10 ++++ .../BasisObjectReferenceContainer.cs.meta | 3 ++ .../Scripts/BasisProcessingAvatarOptions.cs | 20 +++++++ .../BasisAssetBundlePipeline.cs | 39 +++++++++++++- .../BasisTemporaryStorageHandler.cs | 15 ++++++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs create mode 100644 Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs.meta diff --git a/Basis/Packages/com.basis.bundlemanagement/BasisLoadhandler.cs b/Basis/Packages/com.basis.bundlemanagement/BasisLoadhandler.cs index 0bec91cc21..381f680697 100644 --- a/Basis/Packages/com.basis.bundlemanagement/BasisLoadhandler.cs +++ b/Basis/Packages/com.basis.bundlemanagement/BasisLoadhandler.cs @@ -90,6 +90,59 @@ public static async Task RequestDeIncrementOfBundle(BasisLoadableBundle loadable } } } + + public static async Task LoadAdditionalAssetInAlreadyLoadedBundle( + BasisLoadableBundle loadableBundle, + string assetPath, + bool includeSubAssets) + { + BasisDebug.Log($"Loading additional asset {assetPath} in already loaded bundle {loadableBundle.BasisRemoteBundleEncrypted.RemoteBeeFileLocation}", BasisDebug.LogTag.Networking); + + if (LoadedBundles.TryGetValue(loadableBundle.BasisRemoteBundleEncrypted.RemoteBeeFileLocation, out BasisTrackedBundleWrapper wrapper)) + { + if (wrapper.AssetBundle == null) + { + BasisDebug.LogError($"{nameof(LoadAdditionalAssetInAlreadyLoadedBundle)} was called, but the wrapper was not already loaded. It is expected that calls to this method are only made while a bundle is still being used."); + return null; + } + + AssetBundleRequest Request = includeSubAssets + ? wrapper.AssetBundle.LoadAssetWithSubAssetsAsync(assetPath) + : wrapper.AssetBundle.LoadAssetAsync(assetPath); + await Request; + + UnityEngine.Object result = Request.asset; + if (result == null) + { + BasisDebug.LogError("The additional asset returned null."); + return null; + } + if (result is GameObject or Transform or Component) + { + BasisDebug.LogError($"{nameof(LoadAdditionalAssetInAlreadyLoadedBundle)} was called, but an object of type {result.GetType().Name} was present instead. This is not allowed."); + return null; + } + + if (result is BasisObjectReferenceContainer container) + { + if (container.references == null || container.references.Length == 0) + { + BasisDebug.LogError($"{nameof(LoadAdditionalAssetInAlreadyLoadedBundle)} was called, but the object returned by the bundle was a container with no references."); + return null; + } + + return container.references[0]; + } + + return result; + } + else + { + BasisDebug.LogError($"{nameof(LoadAdditionalAssetInAlreadyLoadedBundle)} was called, but the bundle was not already loaded. It is expected that calls to this method are only made while a bundle is still being used."); + return null; + } + } + public static async Task LoadGameObjectBundle(GameObject DisabledGameobject,BasisLoadableBundle loadableBundle, bool useContentRemoval, BasisProgressReport report, CancellationToken cancellationToken, Vector3 Position, Quaternion Rotation, Vector3 Scale, bool ModifyScale, Selector Selector, Transform Parent = null, bool DestroyColliders = false,bool ChangeColidersToCorrectLayer = false, long MaxDownloadSizeInMB = 4L * 1024 * 1024 * 1024, List HarvestedHeadChop = null) { await EnsureInitializationComplete(); diff --git a/Basis/Packages/com.basis.sdk/Scripts/Basis Components/BasisAvatar.cs b/Basis/Packages/com.basis.sdk/Scripts/Basis Components/BasisAvatar.cs index db8300052b..99871988bb 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Basis Components/BasisAvatar.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Basis Components/BasisAvatar.cs @@ -183,6 +183,12 @@ public static GameObject GetGameObject(object o) /// public BasisProcessingAvatarOptions ProcessingAvatarOptions; + /// + /// Contains information used by the bundle build process. The information contained inside this is processed + /// and then altered right when the bundle is built. + /// + public BasisBundleAdditionalAssets BundleAdditionalAssets; + /// /// the animators humanScale, Cached here to stop requesting it from the animator per frame. /// diff --git a/Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs b/Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs new file mode 100644 index 0000000000..a1a1c2d8e5 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace Basis.Scripts.BasisSdk +{ + [System.Serializable] + public class BasisObjectReferenceContainer : ScriptableObject + { + public Object[] references; + } +} diff --git a/Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs.meta new file mode 100644 index 0000000000..c43eebd738 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/BasisObjectReferenceContainer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 67c81cb92a2242049a3672e673cd5c60 +timeCreated: 1777901993 \ No newline at end of file diff --git a/Basis/Packages/com.basis.sdk/Scripts/BasisProcessingAvatarOptions.cs b/Basis/Packages/com.basis.sdk/Scripts/BasisProcessingAvatarOptions.cs index 8949cb57ff..a5651eb342 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/BasisProcessingAvatarOptions.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/BasisProcessingAvatarOptions.cs @@ -1,3 +1,5 @@ +using UnityEngine; + namespace Basis.Scripts.BasisSdk { [System.Serializable] @@ -16,4 +18,22 @@ public class BasisProcessingAvatarOptions /// // disabled does work just not for all avatars public bool RemoveUnusedBlendshapes = false; } + + [System.Serializable] + public class BasisBundleAdditionalAssets + { + public BasisBundleAdditionalAsset[] deferredAssets; + } + + [System.Serializable] + public class BasisBundleAdditionalAsset + { + // Defined by user scripts, during the build. + public string key; + public Object asset; + + // Defined by Basis, in the bundle builder. + public int indexInBundle; + public string assetPath; + } } diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs index 40a3e672ea..d0c66486b0 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs @@ -64,6 +64,7 @@ public static class BasisAssetBundlePipeline try { + List additionalAssetPaths = new List(); if (isScene) { if (settings.RebakeOcclusionCulling) @@ -88,6 +89,30 @@ public static class BasisAssetBundlePipeline OnBeforeBuildPrefab?.Invoke(prefab, settings); PostProcessAvatar(prefab); + // This is not part of post-processing, because we don't want this running during Test In Editor. + if (prefab.TryGetComponent(out BasisAvatar avatar)) + { + // Not null, PostProcessAvatar ensures that. + for (var indexInArray = 0; indexInArray < avatar.BundleAdditionalAssets.deferredAssets.Length; indexInArray++) + { + var indexInBundle = 1 + indexInArray; + + var forAvatar = avatar.BundleAdditionalAssets.deferredAssets[indexInArray]; + if (forAvatar.asset == null) throw new InvalidOperationException("Cannot bundle null additional assets."); + if (forAvatar.asset is GameObject or Transform or Component) throw new InvalidOperationException("Cannot bundle GameObjects, Transforms, or Components as additional assets."); + + // We cannot just use AssetDatabase.AddObjectToAsset because the asset path of a Mesh is very often the path of the .fbx, which is a GameObject. + // We're using SaveAssetToTemporaryStorage to create a container that references the asset. + var additionalAssetPath = TemporaryStorageHandler.SaveAssetToTemporaryStorage(forAvatar.asset, settings, out _); + additionalAssetPaths.Add(additionalAssetPath); + + // Dereference it so that it doesn't get bundled into main. + forAvatar.asset = null; + forAvatar.indexInBundle = indexInBundle; + forAvatar.assetPath = additionalAssetPath; + } + } + assetPath = TemporaryStorageHandler.SavePrefabToTemporaryStorage(prefab, settings, ref wasModified, out uniqueID); if (prefab != null) @@ -99,7 +124,7 @@ public static class BasisAssetBundlePipeline AssetBundleBuild Build = new AssetBundleBuild() { assetBundleName = uniqueID, - assetNames = new string[] { assetPath } + assetNames = new[] { assetPath }.Concat(additionalAssetPaths).ToArray() }; AssetBundleBuild[] Builds = new AssetBundleBuild[] { Build }; @@ -174,6 +199,18 @@ public static void PostProcessAvatar(GameObject prefab) // We do not want to keep this data at runtime. avatar.ProcessingAvatarOptions = null; + + if (avatar.BundleAdditionalAssets == null) + { + avatar.BundleAdditionalAssets = new BasisBundleAdditionalAssets + { + deferredAssets = Array.Empty() + }; + } + else if (avatar.BundleAdditionalAssets.deferredAssets == null) + { + avatar.BundleAdditionalAssets.deferredAssets = Array.Empty(); + } } } diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs index 5c6b7aaa85..7a5c647628 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs @@ -1,4 +1,5 @@ using System.IO; +using Basis.Scripts.BasisSdk; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; @@ -14,6 +15,20 @@ public static string SavePrefabToTemporaryStorage(GameObject prefab, BasisAssetB wasModified = true; return prefabPath; } + public static string SaveAssetToTemporaryStorage(Object asset, BasisAssetBundleObject settings, out string uniqueID) + { + if (asset is GameObject or Transform or Component) throw new InvalidDataException("Cannot save asset of type GameObject, Transform or Component."); + + EnsureDirectoryExists(settings.TemporaryStorage); + uniqueID = BasisGenerateUniqueID.GenerateUniqueID(); + string assetPath = Path.Combine(settings.TemporaryStorage, $"{uniqueID}.asset"); + BasisObjectReferenceContainer referenceContainer = ScriptableObject.CreateInstance(); + referenceContainer.references = new[] { asset }; + + AssetDatabase.CreateAsset(referenceContainer, assetPath); + + return assetPath; + } public static string SaveScene(Scene sceneToCopy, BasisAssetBundleObject settings, out string uniqueID) { // Generate a unique ID