Skip to content
Open
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 @@ -9,7 +9,9 @@
"Unity.Collections",
"Unity.Mathematics",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"BasisSDK",
"BasisSDKEditor",
"BasisShims",
"Cilbox"
],
Expand Down

Large diffs are not rendered by default.

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
@@ -0,0 +1,121 @@
using System;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;

namespace Basis.Tests.Sync
{
public sealed class BasisTemporaryStorageMaterialTests
{
[Test]
public void SavePrefabToTemporaryStorage_PreservesTransientRendererMaterial()
{
string folderName = $"BasisTempMaterialTest_{Guid.NewGuid():N}";
string temporaryStorage = $"Assets/{folderName}";
string sourceMaterialPath = $"{temporaryStorage}/Source.mat";
BasisAssetBundleObject settings = null;
GameObject root = null;
Material transientMaterial = null;

try
{
AssetDatabase.CreateFolder("Assets", folderName);

Shader shader = Shader.Find("Universal Render Pipeline/Lit");
Assert.IsNotNull(shader, "URP Lit is required to construct the material test fixture.");

Texture2D authoringTexture = new Texture2D(1, 1) { name = "AuthoringReference" };
string authoringTexturePath = $"{temporaryStorage}/AuthoringReference.asset";
AssetDatabase.CreateAsset(authoringTexture, authoringTexturePath);
AssetDatabase.ImportAsset(authoringTexturePath, ImportAssetOptions.ForceSynchronousImport);
authoringTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(authoringTexturePath);

Material sourceMaterial = new Material(shader) { name = "Source" };
sourceMaterial.SetTexture("_BaseMap", authoringTexture);
AssetDatabase.CreateAsset(sourceMaterial, sourceMaterialPath);
AssetDatabase.ImportAsset(sourceMaterialPath, ImportAssetOptions.ForceSynchronousImport);
sourceMaterial = AssetDatabase.LoadAssetAtPath<Material>(sourceMaterialPath);
Assert.IsNotNull(sourceMaterial);
Assert.IsTrue(EditorUtility.IsPersistent(sourceMaterial));
Assert.AreSame(authoringTexture, sourceMaterial.GetTexture("_BaseMap"));

transientMaterial = UnityEngine.Object.Instantiate(sourceMaterial);
transientMaterial.name = "Source_Stripped";
transientMaterial.SetTexture("_BaseMap", null);
Assert.IsFalse(EditorUtility.IsPersistent(transientMaterial),
"The fixture must exercise a transient material reference.");
Assert.IsNull(transientMaterial.GetTexture("_BaseMap"));

root = new GameObject("TransientMaterialPrefabRoot");
MeshRenderer renderer = root.AddComponent<MeshRenderer>();
renderer.sharedMaterial = transientMaterial;
GameObject child = new GameObject("SharedMaterialChild");
child.transform.SetParent(root.transform, false);
MeshRenderer childRenderer = child.AddComponent<MeshRenderer>();
childRenderer.sharedMaterial = transientMaterial;

settings = ScriptableObject.CreateInstance<BasisAssetBundleObject>();
settings.TemporaryStorage = temporaryStorage;

bool wasModified = false;
string prefabPath = TemporaryStorageHandler.SavePrefabToTemporaryStorage(
root, settings, ref wasModified, out _);

Assert.IsTrue(wasModified);
UnityEngine.Object.DestroyImmediate(root);
root = null;

AssetDatabase.ImportAsset(prefabPath, ImportAssetOptions.ForceSynchronousImport);
GameObject savedPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
Assert.IsNotNull(savedPrefab, "The staged prefab could not be reloaded from the AssetDatabase.");

MeshRenderer savedRenderer = savedPrefab.GetComponent<MeshRenderer>();
Assert.IsNotNull(savedRenderer);
Assert.IsNotNull(savedRenderer.sharedMaterial,
"A transient material assigned before prefab staging was serialized as a missing material reference.");
Assert.IsTrue(EditorUtility.IsPersistent(savedRenderer.sharedMaterial),
"The staged prefab still references a non-persistent material after serialization.");
string persistedMaterialPath = AssetDatabase.GetAssetPath(savedRenderer.sharedMaterial);
Assert.IsFalse(string.IsNullOrEmpty(persistedMaterialPath),
"The staged material does not have a durable AssetDatabase path.");
Assert.IsNull(savedRenderer.sharedMaterial.GetTexture("_BaseMap"),
"The persisted build material regained the authoring-only texture that the transient clone removed.");

string[] stagedDependencies = AssetDatabase.GetDependencies(prefabPath, true);
CollectionAssert.Contains(stagedDependencies, persistedMaterialPath,
"The staged prefab dependency graph does not include its generated persistent material.");
CollectionAssert.DoesNotContain(stagedDependencies, authoringTexturePath,
"The authoring-only texture cleared from the transient material leaked back into staged dependencies.");

MeshRenderer savedChildRenderer = savedPrefab.transform.Find("SharedMaterialChild")
.GetComponent<MeshRenderer>();
Assert.AreSame(savedRenderer.sharedMaterial, savedChildRenderer.sharedMaterial,
"One transient material shared by multiple renderers should map to one persistent temporary material asset.");

Material originalMaterial = AssetDatabase.LoadAssetAtPath<Material>(sourceMaterialPath);
Assert.AreSame(authoringTexture, originalMaterial.GetTexture("_BaseMap"),
"Persisting the build clone must not mutate the authored source material asset.");
}
finally
{
if (root != null)
{
UnityEngine.Object.DestroyImmediate(root);
}

if (transientMaterial != null && !EditorUtility.IsPersistent(transientMaterial))
{
UnityEngine.Object.DestroyImmediate(transientMaterial);
}

if (settings != null)
{
UnityEngine.Object.DestroyImmediate(settings);
}

AssetDatabase.DeleteAsset(temporaryStorage);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
}
}
}
}

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 @@ -19,6 +19,13 @@ public static class BasisAssetBundlePipeline

// Static delegates
public static BeforeBuildGameobjectHandler OnBeforeBuildPrefab;
/// <summary>
/// Final prefab preparation stage. Runs after normal prefab processors and immediately before
/// Basis post-processing/staging. Consumers that replace authoring components during build
/// should subscribe here so earlier processors can finish mutating the isolated clone first.
/// For example have Cilbox serialize after NDMF has done its mutations.
/// </summary>
public static BeforeBuildGameobjectHandler OnBeforeBuildPrefabSerialization;
public static AfterBuildHandler OnAfterBuildPrefab;
public static BuildErrorHandler OnBuildErrorPrefab;

Expand Down Expand Up @@ -101,6 +108,7 @@ private static BasisBundleContentKind ResolveContentKind(bool isScene, GameObjec
prefab = Object.Instantiate(asset);
DestroyEditorOnlyInAvatar(prefab);
OnBeforeBuildPrefab?.Invoke(prefab, settings);
OnBeforeBuildPrefabSerialization?.Invoke(prefab, settings);
PostProcessAvatar(prefab);
meta = BasisBundleBuild.GenerateMetaData(prefab);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
Expand All @@ -8,12 +10,138 @@ public static class TemporaryStorageHandler
public static string SavePrefabToTemporaryStorage(GameObject prefab, BasisAssetBundleObject settings, ref bool wasModified, out string uniqueID)
{
EnsureDirectoryExists(settings.TemporaryStorage);
PersistTransientRendererMaterials(prefab, settings.TemporaryStorage);

uniqueID = BasisGenerateUniqueID.GenerateUniqueID();
string prefabPath = Path.Combine(settings.TemporaryStorage, $"{uniqueID}.prefab");
prefab = PrefabUtility.SaveAsPrefabAsset(prefab, prefabPath);
wasModified = true;
return prefabPath;
}

/// <summary>
/// Prefab assets cannot safely retain references to in-memory Material instances. Build processors
/// are allowed to clone/modify materials on the isolated prefab, so persist those transient clones
/// into the same temporary storage before crossing the prefab serialization boundary.
/// </summary>
private static void PersistTransientRendererMaterials(GameObject prefab, string temporaryStorage)
{
if (prefab == null)
{
throw new ArgumentNullException(nameof(prefab));
}

var remappedMaterials = new Dictionary<EntityId, Material>();
Renderer[] renderers = prefab.GetComponentsInChildren<Renderer>(true);
for (int rendererIndex = 0; rendererIndex < renderers.Length; rendererIndex++)
{
Renderer renderer = renderers[rendererIndex];
if (renderer == null)
{
continue;
}

Material[] materials = renderer.sharedMaterials;
bool changed = false;
for (int slot = 0; slot < materials.Length; slot++)
{
Material material = materials[slot];
if (material == null)
{
continue;
}

EntityId entityId = material.GetEntityId();
if (remappedMaterials.TryGetValue(entityId, out Material remapped))
{
materials[slot] = remapped;
changed = true;
continue;
}

if (EditorUtility.IsPersistent(material))
{
continue;
}

Material persisted = PersistMaterial(material, temporaryStorage);
remappedMaterials.Add(entityId, persisted);
materials[slot] = persisted;
changed = true;
}

if (changed)
{
renderer.sharedMaterials = materials;
EditorUtility.SetDirty(renderer);
}
}
}

private static Material PersistMaterial(Material source, string temporaryStorage)
{
string sourceName = source.name;
string assetName = SanitizeAssetFileName(sourceName);
string path = AssetDatabase.GenerateUniqueAssetPath(
Path.Combine(temporaryStorage, $"{assetName}.mat").Replace('\\', '/'));
HideFlags originalHideFlags = source.hideFlags;

try
{
// Persist the generated instance itself rather than cloning it again. Any other references
// to this same build-time material then become durable as well.
source.hideFlags = HideFlags.None;
AssetDatabase.CreateAsset(source, path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
Material loaded = AssetDatabase.LoadAssetAtPath<Material>(path);
if (loaded == null || !EditorUtility.IsPersistent(loaded))
{
throw new InvalidOperationException($"Unity did not persist the generated material at '{path}'.");
}

return loaded;
}
catch (Exception ex)
{
if (source != null && EditorUtility.IsPersistent(source))
{
AssetDatabase.DeleteAsset(path);
}
else if (AssetDatabase.LoadAssetAtPath<Material>(path) != null)
{
AssetDatabase.DeleteAsset(path);
}
else if (source != null)
{
source.hideFlags = originalHideFlags;
}

throw new InvalidOperationException(
$"Failed to persist transient build material '{sourceName}' before saving the staged prefab. " +
"The build was stopped rather than serializing a missing material reference.", ex);
}
}

private static string SanitizeAssetFileName(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "GeneratedMaterial";
}

char[] invalidChars = Path.GetInvalidFileNameChars();
char[] chars = value.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
if (Array.IndexOf(invalidChars, chars[i]) >= 0 || chars[i] == '/' || chars[i] == '\\')
{
chars[i] = '_';
}
}

string sanitized = new string(chars).Trim();
return string.IsNullOrEmpty(sanitized) ? "GeneratedMaterial" : sanitized;
}
// SaveScene lived here, but it handed the build the scene's original path while only the bundle
// name was unique, which is what let two worlds built from one scene collide. Scenes are now
// staged by BasisSceneBuildName instead.
Expand Down
Loading
Loading