using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace UnityEditor.Rendering.Universal.Converters { enum IdentifierType { kNullIdentifier = 0, kImportedAsset = 1, kSceneObject = 2, kSourceAsset = 3, kBuiltInAsset = 4 }; internal static class ReadonlyMaterialMap { public static readonly Dictionary Map = new Dictionary { {"Default-Diffuse", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat"}, {"Default-Material", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat"}, {"Default-ParticleSystem", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/ParticlesUnlit.mat"}, {"Default-Particle", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/ParticlesUnlit.mat"}, {"Default-Terrain-Diffuse", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/TerrainLit.mat"}, {"Default-Terrain-Specular", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/TerrainLit.mat"}, {"Default-Terrain-Standard", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/TerrainLit.mat"}, {"Sprites-Default", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Sprite-Unlit-Default.mat"}, {"Sprites-Mask", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Sprite-Unlit-Default.mat"}, {"SpatialMappingOcclusion", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/SpatialMappingOcclusion.mat"}, {"SpatialMappingWireframe", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/SpatialMappingWireframe.mat"}, // TODO: These currently render in URP, but they are using BIRP shaders. Create a task to convert these. // {"Default UI Material", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat"}, // {"ETC1 Supported UI Material", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat"}, // {"Default-Line", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat"}, // {"Default-Skybox", "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat"}, }; } internal class ReadonlyMaterialConverter : RenderPipelineConverter { public override string name => "Readonly Material Converter"; public override string info => "Converts references to Built-In readonly materials to URP readonly materials. This will create temporarily a .index file and that can take a long time."; public override Type container => typeof(BuiltInToURPConverterContainer); public override bool needsIndexing => true; List guids = new List(); public override void OnInitialize(InitializeConverterContext ctx, Action callback) { var context = Search.SearchService.CreateContext("asset", "urp:convert-readonly"); Search.SearchService.Request(context, (c, items) => { // we're going to do this step twice in order to get them ordered, but it should be fast var orderedRequest = items.OrderBy(req => { GlobalObjectId.TryParse(req.id, out var gid); return gid.assetGUID; }); foreach (var r in orderedRequest) { if (r == null || !GlobalObjectId.TryParse(r.id, out var gid)) { continue; } var label = r.provider.fetchLabel(r, r.context); var description = r.provider.fetchDescription(r, r.context); var item = new ConverterItemDescriptor() { name = description.Split('/').Last().Split('.').First(), info = $"{label}", }; guids.Add(gid.ToString()); ctx.AddAssetToConvert(item); } callback.Invoke(); }); context?.Dispose(); } public override void OnRun(ref RunItemContext ctx) { var obj = LoadObject(ref ctx); var result = true; var errorString = new StringBuilder(); if (obj != null) { var materials = MaterialReferenceBuilder.GetMaterialsFromObject(obj); foreach (var material in materials) { if (material == null) { continue; } // there might be multiple materials on this object, we only care about the ones we explicitly try to remap that fail if (!MaterialReferenceBuilder.GetIsReadonlyMaterial(material)) continue; if (!ReadonlyMaterialMap.Map.ContainsKey(material.name)) continue; if (!ReassignMaterial(obj, material.name, ReadonlyMaterialMap.Map[material.name])) { result = false; errorString.AppendLine($"Material {material.name} failed to be reassigned"); } } } else { result = false; errorString.AppendLine($"Object {ctx.item.descriptor.name} could not be loaded"); } if (!result) { ctx.didFail = true; ctx.info = errorString.ToString(); } else { // make sure the changes get saved EditorUtility.SetDirty(obj); var currentScene = SceneManager.GetActiveScene(); EditorSceneManager.SaveScene(currentScene); } } public override void OnClicked(int index) { if (GlobalObjectId.TryParse(guids[index], out var gid)) { var containerPath = AssetDatabase.GUIDToAssetPath(gid.assetGUID); EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath(containerPath)); } } private static bool ReassignMaterial(Object obj, string oldMaterialName, string newMaterialPath) { var result = true; // do the reflection to make sure we get the right material reference if (obj is GameObject go) { foreach (var key in MaterialReferenceBuilder.GetComponentTypes()) { var components = go.GetComponentsInChildren(key); foreach (var component in components) { result &= ReassignMaterialOnComponentOrObject(component, oldMaterialName, newMaterialPath); } } } else { result &= ReassignMaterialOnComponentOrObject(obj, oldMaterialName, newMaterialPath); } return result; } private static bool ReassignMaterialOnComponentOrObject(Object obj, string oldMaterialName, string newMaterialPath) { var result = true; var materialProperties = obj.GetType().GetMaterialPropertiesWithoutLeaking(); foreach (var property in materialProperties) { var materialValue = property.GetGetMethod().GetMaterialFromMethod(obj, (methodName, objectName) => $"The method {methodName} was not found on {objectName}. Ignoring this property."); if (materialValue is Material material) { if (material.name.Equals(oldMaterialName, StringComparison.OrdinalIgnoreCase)) { var newMaterial = AssetDatabase.LoadAssetAtPath(newMaterialPath); if (newMaterial != null) { var setMethod = property.GetSetMethod(); if (setMethod != null) { setMethod.Invoke(obj, new object[] { newMaterial }); } else { // failed to set the material from the SetMethod result = false; } } else { // a material we expected to exist does not result = false; } } } else if (materialValue is Material[] materialList) { for (int i = 0; i < materialList.Length; i++) { var mat = materialList[i]; if (mat == null) { continue; } if (mat.name.Equals(oldMaterialName, StringComparison.OrdinalIgnoreCase)) { var newMaterial = AssetDatabase.LoadAssetAtPath(newMaterialPath); if (newMaterial != null) { materialList[i] = newMaterial; } else { // a material we expected to exist does not result = false; } } } var setMethod = property.GetSetMethod(); if (setMethod != null) { setMethod.Invoke(obj, new object[] { materialList }); } else { // failed to set the material from the SetMethod result = false; } } } return result; } private Object LoadObject(ref RunItemContext ctx) { var item = ctx.item; var guid = guids[item.index]; if (GlobalObjectId.TryParse(guid, out var gid)) { var obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(gid); if (!obj) { // Open container scene if (gid.identifierType == (int)IdentifierType.kSceneObject) { var containerPath = AssetDatabase.GUIDToAssetPath(gid.assetGUID); var mainInstanceID = AssetDatabase.LoadAssetAtPath(containerPath); AssetDatabase.OpenAsset(mainInstanceID); // if we have a prefab open, then we already have the object we need to update var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); if (prefabStage != null) { obj = mainInstanceID; } // Reload object if it is still null if (obj == null) { obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(gid); if (!obj) { ctx.didFail = true; ctx.info = $"Object {gid.assetGUID} failed to load..."; } } } } return obj; } ctx.didFail = true; ctx.info = $"Failed to parse Global ID {item.descriptor.info}..."; return null; } } }