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
}
}