using System; using System.Collections.Generic; using System.Text; using JetBrains.Annotations; using UnityEngine; using UnityEngine.Rendering; namespace UnityEditor.Rendering { /// /// Material Upgrader class. /// public partial class MaterialUpgrader { internal static readonly string k_DialogKey = $"{nameof(UnityEditor)}.{nameof(Rendering)}.{nameof(MaterialUpgrader)}.ConfirmMaterialConversion"; #region Internal API /// /// Represents an entry describing material properties /// internal class MaterialInfo { /// /// The material name /// public string Name { get; set; } /// /// The material being evaluated. /// [CanBeNull] public Material Material { get; set; } /// /// The current shader of the material. /// public string ShaderName { get; set; } /// /// If the material is a variant /// public bool IsVariant { get; set; } /// /// The base material name /// public string BaseMaterialName { get; set; } /// /// The base material is is true /// [CanBeNull] public Material BaseMaterial { get; set; } /// /// Determines whether the specified object is equal to the current . /// /// The object to compare with the current object. /// true if the specified object is equal to the current object; otherwise, false. public override bool Equals(object obj) { if (obj is not MaterialInfo other) return false; return string.Equals(ShaderName, other.ShaderName, StringComparison.OrdinalIgnoreCase); } /// /// Serves as the default hash function. /// /// A hash code for the current object. public override int GetHashCode() { return ShaderName?.GetHashCode() ?? 0; } } /// /// Represents an entry describing whether a material is available for upgrade, /// along with the reason if it's not. /// internal class MaterialUpgradeEntry { /// /// The material being evaluated. /// public MaterialInfo MaterialInfo { get; set; } /// /// Indicates whether the material is available for upgrade. /// public bool AvailableForUpgrade { get; set; } /// /// If the material is not available for upgrade, this provides the reason why. /// Empty if is true. /// public string NotAvailableForUpgradeReason { get; set; } /// /// Determines whether the specified object is equal to the current . /// /// The object to compare with the current object. /// true if the specified object is equal to the current object; otherwise, false. public override bool Equals(object obj) { if (obj is not MaterialUpgradeEntry other) return false; return Equals(MaterialInfo, other.MaterialInfo) && AvailableForUpgrade == other.AvailableForUpgrade && string.Equals(NotAvailableForUpgradeReason, other.NotAvailableForUpgradeReason, StringComparison.OrdinalIgnoreCase); } /// /// Serves as the default hash function. /// /// A hash code for the current object. public override int GetHashCode() { return HashCode.Combine(MaterialInfo, AvailableForUpgrade, NotAvailableForUpgradeReason); } } static MaterialUpgrader GetUpgrader(List upgraders, Material material) { if (material == null || material.shader == null) return null; string shaderName = material.shader.name; for (int i = 0; i != upgraders.Count; i++) { if (upgraders[i].OldShaderPath == shaderName) return upgraders[i]; } return null; } //@TODO: Only do this when it exceeds memory consumption... static void SaveAssetsAndFreeMemory() { AssetDatabase.SaveAssets(); GC.Collect(); EditorUtility.UnloadUnusedAssetsImmediate(); AssetDatabase.Refresh(); } /// /// Returns if the material has a mapping upgrader /// /// The available upgrader list /// The material to check /// static bool IsMaterialUpgradable(List upgraders, Material material) { if (material == null || upgraders == null) throw new ArgumentException("Invalid input: upgraders or material is null."); return GetUpgrader(upgraders, material) != null; } /// Extracts shader paths from a list of upgraders into two sets: /// one for shaders to upgrade (old) and one for shaders already upgraded (new). internal static void GetUpgraderShaderPaths(List upgraders, out HashSet oldShaders, out HashSet newShaders) { oldShaders = new HashSet(); newShaders = new HashSet(); if (upgraders == null) return; foreach (var upgrader in upgraders) { if (!string.IsNullOrEmpty(upgrader.OldShaderPath)) oldShaders.Add(upgrader.OldShaderPath); if (!string.IsNullOrEmpty(upgrader.NewShaderPath)) newShaders.Add(upgrader.NewShaderPath); } } internal static MaterialInfo ToMaterialInfo(Material material) { Material baseMaterial = material; if (material.isVariant) { // Traverse up to the root material while (baseMaterial.isVariant && baseMaterial.parent != null) { baseMaterial = baseMaterial.parent; } } return new MaterialInfo { Name = material.name, Material = material, ShaderName = material.shader.name, IsVariant = material.isVariant, BaseMaterialName = baseMaterial.name, BaseMaterial = baseMaterial }; } internal static List GatherInfo(IEnumerable materials) { var materialsInfo = new List(); foreach (var material in materials) { if (material == null || material.shader == null) continue; materialsInfo.Add(ToMaterialInfo(material)); } return materialsInfo; } internal static IEnumerable FetchUpgradeOptions(HashSet upgradersAvailable, HashSet shaderNamesToIgnore, List materialInfo) { foreach (var material in materialInfo) { var shaderName = material.ShaderName; bool isShaderIgnored = shaderNamesToIgnore.Contains(shaderName); if (isShaderIgnored) continue; bool isUpgradable = !material.IsVariant && upgradersAvailable.Contains(shaderName); string reason = isUpgradable ? string.Empty : GenerateReason(material); yield return new MaterialUpgradeEntry { MaterialInfo = material, AvailableForUpgrade = isUpgradable, NotAvailableForUpgradeReason = reason }; } } internal static List FetchAllMaterialsInProject() { List materials = new(AssetDatabaseHelper.FindAssets(".mat")); // Add the build in material of terrain ( must be always available ) if (Terrain.activeTerrains.Length > 0) { materials.Add(Terrain.activeTerrain.materialTemplate); } return materials; } /// /// Given a set of material assets in the project and determines whether they are eligible for upgrade. /// /// A list of instances to use for determining upgradability. /// A list of materials to check for upgrade options, if null Unity asumes that you want all project materials. /// /// An enumerable of representing each material and whether it can be upgraded. /// /// Thrown if is null. internal static IEnumerable FetchUpgradeOptions(List upgraders, List materials = null) { if (upgraders == null) throw new ArgumentNullException(nameof(upgraders)); // If no materials are provided, gather all materials in the project. if (materials == null) { materials = FetchAllMaterialsInProject(); } var materialInfo = GatherInfo(materials); GetUpgraderShaderPaths(upgraders, out var upgradersAvailable, out var shaderNamesToIgnore); return FetchUpgradeOptions(upgradersAvailable, shaderNamesToIgnore, materialInfo); } internal static string GenerateReason(MaterialInfo material) { string reason = $"No upgrader available to convert material '{material.Name}'"; if (material.IsVariant) { reason += $", a variant of '{material.BaseMaterialName}',"; } reason += $" using shader '{material.BaseMaterialName}'."; return reason; } static StringBuilder s_UpgradeLog = new StringBuilder(); internal static string PerformUpgradeInternal( List materialUpgrades, List upgraders, HashSet shaderNamesToIgnore, string progressBarName, bool showProgressBar = true, UpgradeFlags flags = UpgradeFlags.None) { s_UpgradeLog.Clear(); if (materialUpgrades.Count > 0) { s_UpgradeLog.AppendLine($"{progressBarName}"); for (int materialIndex = 0; materialIndex < materialUpgrades.Count; ++materialIndex) { var entry = materialUpgrades[materialIndex]; if (showProgressBar) { if (EditorUtility.DisplayCancelableProgressBar(progressBarName, $"({materialIndex} of {materialUpgrades.Count}) {entry.MaterialInfo.Name}", (float)materialIndex / (float)materialUpgrades.Count)) { s_UpgradeLog.AppendLine("Process cancelled by user."); break; } } if (!entry.AvailableForUpgrade || shaderNamesToIgnore.Contains(entry.MaterialInfo.ShaderName)) { s_UpgradeLog.AppendLine($"Skipping material: {entry.MaterialInfo.Name} - {entry.NotAvailableForUpgradeReason}"); } else { s_UpgradeLog.AppendLine($"Upgrading material: {entry.MaterialInfo.Name} using shader: {entry.MaterialInfo.ShaderName}"); Upgrade(entry.MaterialInfo.Material, upgraders, flags); } } AssetDatabase.SaveAssets(); if (showProgressBar) EditorUtility.ClearProgressBar(); } return s_UpgradeLog.ToString(); } static void PerformUpgrade(List materialUpgrades, List upgraders, HashSet shaderNamesToIgnore, string progressBarName, UpgradeFlags flags = UpgradeFlags.None) { if (materialUpgrades == null || materialUpgrades.Count == 0) { Debug.LogWarning("No materials found for upgrade."); return; } bool CanPerformUpgrade() { const string title = "Confirm Material Conversion"; const string message = "This action will modify materials and cannot be easily undone. It is strongly recommended to have a backup or use version control before continuing."; const string proceed = "Proceed"; const string cancel = "Cancel"; if (Application.isBatchMode) return true; return EditorUtility.DisplayDialog(title, message, proceed, cancel, DialogOptOutDecisionType.ForThisMachine, k_DialogKey); } if (CanPerformUpgrade()) { string upgradeLog = PerformUpgradeInternal(materialUpgrades, upgraders, shaderNamesToIgnore, progressBarName, true, flags); Debug.Log(upgradeLog); } } #endregion #region Public API /// /// Checking if project folder contains any materials that can not be automatic upgraded. /// /// List if /// Returns true if at least one material uses a non-built-in shader public static bool ProjectContainsNonAutomaticUpgradePath(List upgraders) { foreach (var material in AssetDatabaseHelper.FindAssets(".mat")) { if (!IsMaterialUpgradable(upgraders, material)) return true; } return false; } /// /// Upgrade the project folder. /// /// List of upgraders. /// Name of the progress bar. /// Material Upgrader flags. public static void UpgradeProjectFolder(List upgraders, string progressBarName, UpgradeFlags flags = UpgradeFlags.None) { HashSet shaderNamesToIgnore = new HashSet(); UpgradeProjectFolder(upgraders, shaderNamesToIgnore, progressBarName, flags); } /// /// Fetches all upgraders that support the given render pipeline asset type /// /// The RP asset type /// A list with all the upgraders in for the given pipeline public static List FetchAllUpgradersForPipeline(Type renderPipelineAssetType) { if (!typeof(RenderPipelineAsset).IsAssignableFrom(renderPipelineAssetType)) throw new ArgumentException($"Type '{renderPipelineAssetType.FullName}' must inherit from RenderPipelineAsset.", nameof(renderPipelineAssetType)); return MaterialUpgraderRegistry.instance.GetMaterialUpgradersForPipeline(renderPipelineAssetType); } /// /// Fetches all materials in the project that are upgradable to the given render pipeline asset type /// /// The RP asset type /// A list with all the materials in the project public static List FetchAllUpgradableMaterialsForPipeline(Type renderPipelineAssetType) { var upgraders = FetchAllUpgradersForPipeline(renderPipelineAssetType); if (upgraders == null || upgraders.Count == 0) { Debug.LogWarning($"No material upgraders found for pipeline: {renderPipelineAssetType.Name}"); return new List(); } var allMaterials = FetchAllMaterialsInProject(); var materialsAvailableForUpgrade = new List(); foreach (var option in FetchUpgradeOptions(upgraders, allMaterials)) { if (!option.AvailableForUpgrade) continue; materialsAvailableForUpgrade.Add(option.MaterialInfo.Material); } return materialsAvailableForUpgrade; } /// /// Upgrade a material. /// /// Material to upgrade. /// Material upgrader. /// Material Upgrader flags. public static void Upgrade(Material material, MaterialUpgrader upgrader, UpgradeFlags flags) { using (ListPool.Get(out List upgraders)) { upgraders.Add(upgrader); Upgrade(material, upgraders, flags); } } /// /// Upgrade a material. /// /// Material to upgrade. /// List of Material upgraders. /// Material Upgrader flags. public static void Upgrade(Material material, List upgraders, UpgradeFlags flags) { if (material == null || upgraders == null || upgraders.Count == 0) return; string message = string.Empty; if (Upgrade(material, upgraders, flags, ref message)) return; if (!string.IsNullOrEmpty(message)) { Debug.Log(message); } } /// /// Upgrade a material. /// /// Material to upgrade. /// List of Material upgraders. /// Material upgrader flags. /// Error message to be outputted when no material upgraders are suitable for given material if the flags is used. /// Returns true if the upgrader was found for the passed in material. public static bool Upgrade(Material material, List upgraders, UpgradeFlags flags, ref string message) { if (material == null) return false; var upgrader = GetUpgrader(upgraders, material); if (upgrader != null) { upgrader.Upgrade(material, flags); return true; } if ((flags & UpgradeFlags.LogMessageWhenNoUpgraderFound) == UpgradeFlags.LogMessageWhenNoUpgraderFound) { message = $"{material.name} material was not upgraded. There's no upgrader to convert {material.shader.name} shader to selected pipeline"; return false; } return true; } /// /// Upgrade the project folder. /// /// List of upgraders. /// Set of shader names to ignore. /// Name of the progress bar. /// Material Upgrader flags. public static void UpgradeProjectFolder(List upgraders, HashSet shaderNamesToIgnore, string progressBarName, UpgradeFlags flags = UpgradeFlags.None) { using (ListPool.Get(out var tmp)) { tmp.AddRange(FetchUpgradeOptions(upgraders)); PerformUpgrade(tmp, upgraders, shaderNamesToIgnore, progressBarName, flags); } } /// /// Upgrade the selection. /// /// List of upgraders. /// Name of the progress bar. /// Material Upgrader flags. public static void UpgradeSelection(List upgraders, string progressBarName, UpgradeFlags flags = UpgradeFlags.None) { HashSet shaderNamesToIgnore = new HashSet(); UpgradeSelection(upgraders, shaderNamesToIgnore, progressBarName, flags); } /// /// Upgrade the selection. /// /// List of upgraders. /// Set of shader names to ignore. /// Name of the progress bar. /// Material Upgrader flags. public static void UpgradeSelection(List upgraders, HashSet shaderNamesToIgnore, string progressBarName, UpgradeFlags flags = UpgradeFlags.None) { using (ListPool.Get(out var tmp)) { using (ListPool.Get(out var selectedMaterials)) { var selection = Selection.objects; if (selection != null) { for (int i = 0; i < selection.Length; ++i) { if (selection[i] is Material m) selectedMaterials.Add(m); } } tmp.AddRange(FetchUpgradeOptions(upgraders, selectedMaterials)); } PerformUpgrade(tmp, upgraders, shaderNamesToIgnore, progressBarName, flags); } } #endregion } }