Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ namespace ECS.StreamableLoading.AssetBundles
/// </summary>
public class AssetBundleData : StreamableRefCountData<AssetBundle>
{
/// <summary>
/// Flag to enable/disable material sharing across instances.
/// When enabled, materials are cached on first instantiation and reused for subsequent instances.
/// Set to false to revert to the old behavior where each instance gets its own material copies.
/// </summary>
public static bool ENABLE_MATERIAL_SHARING = true;

private readonly string AssetBundleName;

public readonly InitialSceneStateMetadata? InitialSceneStateMetadata;
Expand All @@ -21,6 +28,12 @@ public class AssetBundleData : StreamableRefCountData<AssetBundle>
private Dictionary<string, AssetInfo>? Assets;
private readonly AssetBundleData[] Dependencies;

/// <summary>
/// Cache for original shared materials per asset, keyed by asset name.
/// Used to restore shared materials after instantiation to avoid material duplication.
/// </summary>
private Dictionary<string, Material[][]>? cachedMaterials;

public AssetBundleData(AssetBundle assetBundle, InitialSceneStateMetadata? initialSceneState, Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "")
: base(assetBundle, ReportCategory.ASSET_BUNDLES)
{
Expand Down Expand Up @@ -128,6 +141,32 @@ public bool TryGetAsset<T>(out T asset, string assetName = "") where T: Object
return true;
}

/// <summary>
/// Gets or caches the original shared materials from the prefab's renderers.
/// Materials are extracted once on first call and reused for subsequent instantiations.
/// This prevents Unity from cloning materials on each Object.Instantiate call.
/// </summary>
/// <param name="assetName">The asset name/hash used as cache key</param>
/// <param name="prefab">The original prefab GameObject from the asset bundle</param>
/// <returns>Array of material arrays, one per renderer in hierarchy order</returns>
public Material[][] GetOrCacheOriginalMaterials(string assetName, GameObject prefab)
{
cachedMaterials ??= new Dictionary<string, Material[][]>();

if (!cachedMaterials.TryGetValue(assetName, out Material[][] materials))
{
var renderers = prefab.GetComponentsInChildren<Renderer>(true);
materials = new Material[renderers.Length][];

for (int i = 0; i < renderers.Length; i++)
materials[i] = renderers[i].sharedMaterials;

cachedMaterials[assetName] = materials;
}

return materials;
}

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ namespace ECS.Unity.GLTFContainer
{
public class Utils
{
/// <summary>
/// Flag to enable/disable material sharing across instances.
/// When enabled, materials are restored from the original prefab after instantiation.
/// This flag works in conjunction with AssetBundleData.ENABLE_MATERIAL_SHARING.
/// Set to false to revert to the old behavior where each instance gets its own material copies.
/// </summary>
public static bool ENABLE_MATERIAL_SHARING = true;

public static bool TryCreateGltfObject(AssetBundleData assetBundleData, string assetHash, out GltfContainerAsset gltfContainerAsset)
{
if (!assetBundleData.TryGetAsset(out GameObject asset, assetHash))
Expand All @@ -27,12 +35,27 @@ public static bool TryCreateGltfObject(AssetBundleData assetBundleData, string a

var result = GltfContainerAsset.Create(container, assetBundleData, assetBundleData.InitialSceneStateMetadata.HasValue);

// Get or cache original materials from the prefab before instantiation (only if material sharing is enabled)
Material[][] originalMaterials = null;
bool shouldRestoreMaterials = ENABLE_MATERIAL_SHARING && AssetBundleData.ENABLE_MATERIAL_SHARING;
if (shouldRestoreMaterials)
originalMaterials = assetBundleData.GetOrCacheOriginalMaterials(assetHash, asset);

GameObject? instance = Object.Instantiate(asset, containerTransform);

// Collect all renderers, they are needed for Visibility system
using (PoolExtensions.Scope<List<Renderer>> instanceRenderers = GltfContainerAsset.RENDERERS_POOL.AutoScope())
{
instance.GetComponentsInChildren(true, instanceRenderers.Value);

// Restore shared materials from cache to avoid material duplication
// (GetComponentsInChildren traverses in the same deterministic order for prefab and instance)
if (shouldRestoreMaterials && originalMaterials != null)
{
for (int i = 0; i < instanceRenderers.Value.Count && i < originalMaterials.Length; i++)
instanceRenderers.Value[i].sharedMaterials = originalMaterials[i];
}

result.Renderers.AddRange(instanceRenderers.Value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ public static void SelfDestroy(this Object @object) =>
SafeDestroy(@object);

/// <summary>
/// Gets shared materials instead of materials if called when Application is not playing (from tests)
/// Flag to enable/disable using shared materials at runtime.
/// When enabled, GetSharedMaterials is used instead of GetMaterials to avoid creating material instances.
/// Set to false to revert to the old behavior where GetMaterials creates instance copies.
/// </summary>
public static bool USE_SHARED_MATERIALS_AT_RUNTIME = true;

/// <summary>
/// Gets materials from the renderer. By default uses GetSharedMaterials to avoid creating material instances.
/// When USE_SHARED_MATERIALS_AT_RUNTIME is false, uses GetMaterials which creates instance copies.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SafeGetMaterials(this Renderer renderer, List<Material> targetList)
Expand All @@ -85,7 +93,10 @@ public static void SafeGetMaterials(this Renderer renderer, List<Material> targe
return;
}
#endif
renderer.GetMaterials(targetList);
if (USE_SHARED_MATERIALS_AT_RUNTIME)
renderer.GetSharedMaterials(targetList);
else
renderer.GetMaterials(targetList);
}
}
}