using System;
using System.Collections.Generic;
using System.Text;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
/// <summary>
/// Material Upgrader class.
/// </summary>
public partial class MaterialUpgrader
{
internal static readonly string k_DialogKey = $"{nameof(UnityEditor)}.{nameof(Rendering)}.{nameof(MaterialUpgrader)}.ConfirmMaterialConversion";
#region Internal API
/// <summary>
/// Represents an entry describing material properties
/// </summary>
internal class MaterialInfo
{
/// <summary>
/// The material name
/// </summary>
public string Name { get; set; }
/// <summary>
/// The material being evaluated.
/// </summary>
[CanBeNull]
public Material Material { get; set; }
/// <summary>
/// The current shader of the material.
/// </summary>
public string ShaderName { get; set; }
/// <summary>
/// If the material is a variant
/// </summary>
public bool IsVariant { get; set; }
/// <summary>
/// The base material name
/// </summary>
public string BaseMaterialName { get; set; }
/// <summary>
/// The base material is <see cref="IsVariant"/> is true
/// </summary>
[CanBeNull]
public Material BaseMaterial { get; set; }
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="MaterialInfo"/>.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
public override bool Equals(object obj)
{
if (obj is not MaterialInfo other)
return false;
return string.Equals(ShaderName, other.ShaderName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
return ShaderName?.GetHashCode() ?? 0;
}
}
/// <summary>
/// Represents an entry describing whether a material is available for upgrade,
/// along with the reason if it's not.
/// </summary>
internal class MaterialUpgradeEntry
{
/// <summary>
/// The material being evaluated.
/// </summary>
public MaterialInfo MaterialInfo { get; set; }
/// <summary>
/// Indicates whether the material is available for upgrade.
/// </summary>
public bool AvailableForUpgrade { get; set; }
/// <summary>
/// If the material is not available for upgrade, this provides the reason why.
/// Empty if <see cref="AvailableForUpgrade"/> is true.
/// </summary>
public string NotAvailableForUpgradeReason { get; set; }
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="MaterialUpgradeEntry"/>.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
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);
}
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
return HashCode.Combine(MaterialInfo, AvailableForUpgrade, NotAvailableForUpgradeReason);
}
}
static MaterialUpgrader GetUpgrader(List<MaterialUpgrader> 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();
}
/// <summary>
/// Returns if the material has a mapping upgrader
/// </summary>
/// <param name="upgraders">The available upgrader list</param>
/// <param name="material">The material to check</param>
/// <returns></returns>
static bool IsMaterialUpgradable(List<MaterialUpgrader> 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<MaterialUpgrader> upgraders, out HashSet<string> oldShaders, out HashSet<string> newShaders)
{
oldShaders = new HashSet<string>();
newShaders = new HashSet<string>();
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<MaterialInfo> GatherInfo(IEnumerable<Material> materials)
{
var materialsInfo = new List<MaterialInfo>();
foreach (var material in materials)
{
if (material == null || material.shader == null)
continue;
materialsInfo.Add(ToMaterialInfo(material));
}
return materialsInfo;
}
internal static IEnumerable<MaterialUpgradeEntry> FetchUpgradeOptions(HashSet<string> upgradersAvailable, HashSet<string> shaderNamesToIgnore, List<MaterialInfo> 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<Material> FetchAllMaterialsInProject()
{
List<Material> materials = new(AssetDatabaseHelper.FindAssets<Material>(".mat"));
// Add the build in material of terrain ( must be always available )
if (Terrain.activeTerrains.Length > 0)
{
materials.Add(Terrain.activeTerrain.materialTemplate);
}
return materials;
}
/// <summary>
/// Given a set of material assets in the project and determines whether they are eligible for upgrade.
/// </summary>
/// <param name="upgraders">A list of <see cref="MaterialUpgrader"/> instances to use for determining upgradability.</param>
/// <param name="materials"> A list of materials to check for upgrade options, if null Unity asumes that you want all project materials.</param>
/// <returns>
/// An enumerable of <see cref="MaterialUpgradeEntry"/> representing each material and whether it can be upgraded.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="upgraders"/> is null.</exception>
internal static IEnumerable<MaterialUpgradeEntry> FetchUpgradeOptions(List<MaterialUpgrader> upgraders, List<Material> 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<MaterialUpgradeEntry> materialUpgrades,
List<MaterialUpgrader> upgraders,
HashSet<string> 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<MaterialUpgradeEntry> materialUpgrades, List<MaterialUpgrader> upgraders, HashSet<string> 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
/// <summary>
/// Checking if project folder contains any materials that can not be automatic upgraded.
/// </summary>
/// <param name="upgraders">List if <see cref="MaterialUpgrader"/></param>
/// <returns>Returns true if at least one material uses a non-built-in shader</returns>
public static bool ProjectContainsNonAutomaticUpgradePath(List<MaterialUpgrader> upgraders)
{
foreach (var material in AssetDatabaseHelper.FindAssets<Material>(".mat"))
{
if (!IsMaterialUpgradable(upgraders, material))
return true;
}
return false;
}
/// <summary>
/// Upgrade the project folder.
/// </summary>
/// <param name="upgraders">List of upgraders.</param>
/// <param name="progressBarName">Name of the progress bar.</param>
/// <param name="flags">Material Upgrader flags.</param>
public static void UpgradeProjectFolder(List<MaterialUpgrader> upgraders, string progressBarName, UpgradeFlags flags = UpgradeFlags.None)
{
HashSet<string> shaderNamesToIgnore = new HashSet<string>();
UpgradeProjectFolder(upgraders, shaderNamesToIgnore, progressBarName, flags);
}
/// <summary>
/// Fetches all upgraders that support the given render pipeline asset type
/// </summary>
/// <param name="renderPipelineAssetType">The RP asset type</param>
/// <returns>A list with all the upgraders in for the given pipeline</returns>
public static List<MaterialUpgrader> 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);
}
/// <summary>
/// Fetches all materials in the project that are upgradable to the given render pipeline asset type
/// </summary>
/// <param name="renderPipelineAssetType">The RP asset type</param>
/// <returns>A list with all the materials in the project</returns>
public static List<Material> 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<Material>();
}
var allMaterials = FetchAllMaterialsInProject();
var materialsAvailableForUpgrade = new List<Material>();
foreach (var option in FetchUpgradeOptions(upgraders, allMaterials))
{
if (!option.AvailableForUpgrade)
continue;
materialsAvailableForUpgrade.Add(option.MaterialInfo.Material);
}
return materialsAvailableForUpgrade;
}
/// <summary>
/// Upgrade a material.
/// </summary>
/// <param name="material">Material to upgrade.</param>
/// <param name="upgrader">Material upgrader.</param>
/// <param name="flags">Material Upgrader flags.</param>
public static void Upgrade(Material material, MaterialUpgrader upgrader, UpgradeFlags flags)
{
using (ListPool<MaterialUpgrader>.Get(out List<MaterialUpgrader> upgraders))
{
upgraders.Add(upgrader);
Upgrade(material, upgraders, flags);
}
}
/// <summary>
/// Upgrade a material.
/// </summary>
/// <param name="material">Material to upgrade.</param>
/// <param name="upgraders">List of Material upgraders.</param>
/// <param name="flags">Material Upgrader flags.</param>
public static void Upgrade(Material material, List<MaterialUpgrader> 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);
}
}
/// <summary>
/// Upgrade a material.
/// </summary>
/// <param name="material">Material to upgrade.</param>
/// <param name="upgraders">List of Material upgraders.</param>
/// <param name="flags">Material upgrader flags.</param>
/// <param name="message">Error message to be outputted when no material upgraders are suitable for given material if the flags <see cref="UpgradeFlags.LogMessageWhenNoUpgraderFound"/> is used.</param>
/// <returns>Returns true if the upgrader was found for the passed in material.</returns>
public static bool Upgrade(Material material, List<MaterialUpgrader> 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;
}
/// <summary>
/// Upgrade the project folder.
/// </summary>
/// <param name="upgraders">List of upgraders.</param>
/// <param name="shaderNamesToIgnore">Set of shader names to ignore.</param>
/// <param name="progressBarName">Name of the progress bar.</param>
/// <param name="flags">Material Upgrader flags.</param>
public static void UpgradeProjectFolder(List<MaterialUpgrader> upgraders, HashSet<string> shaderNamesToIgnore, string progressBarName, UpgradeFlags flags = UpgradeFlags.None)
{
using (ListPool<MaterialUpgradeEntry>.Get(out var tmp))
{
tmp.AddRange(FetchUpgradeOptions(upgraders));
PerformUpgrade(tmp, upgraders, shaderNamesToIgnore, progressBarName, flags);
}
}
/// <summary>
/// Upgrade the selection.
/// </summary>
/// <param name="upgraders">List of upgraders.</param>
/// <param name="progressBarName">Name of the progress bar.</param>
/// <param name="flags">Material Upgrader flags.</param>
public static void UpgradeSelection(List<MaterialUpgrader> upgraders, string progressBarName, UpgradeFlags flags = UpgradeFlags.None)
{
HashSet<string> shaderNamesToIgnore = new HashSet<string>();
UpgradeSelection(upgraders, shaderNamesToIgnore, progressBarName, flags);
}
/// <summary>
/// Upgrade the selection.
/// </summary>
/// <param name="upgraders">List of upgraders.</param>
/// <param name="shaderNamesToIgnore">Set of shader names to ignore.</param>
/// <param name="progressBarName">Name of the progress bar.</param>
/// <param name="flags">Material Upgrader flags.</param>
public static void UpgradeSelection(List<MaterialUpgrader> upgraders, HashSet<string> shaderNamesToIgnore, string progressBarName, UpgradeFlags flags = UpgradeFlags.None)
{
using (ListPool<MaterialUpgradeEntry>.Get(out var tmp))
{
using (ListPool<Material>.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
}
}