Files
ichni_Official/Packages/NB_FX/NBShaders2/Editor/FeatureLevel/NBShaderVariantCollectionBuilder.cs
SoulliesOfficial 40fa80cd70 NB_FX
2026-07-17 17:46:16 -04:00

475 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using NBShader;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
namespace NBShaders2.Editor.FeatureLevel
{
public static class NBShaderVariantCollectionBuilder
{
public const string DefaultOutputFolder = "Assets/NBShader/ShaderVariantCollections";
private static readonly NBShaderFeatureTier[] Tiers =
{
NBShaderFeatureTier.Low,
NBShaderFeatureTier.Medium,
NBShaderFeatureTier.High,
NBShaderFeatureTier.Ultra
};
public static NBShaderVariantCollectionBuildResult Preview(IEnumerable<string> searchFolders, string outputFolder)
{
return Preview(searchFolders, outputFolder, null);
}
public static NBShaderVariantCollectionBuildResult Preview(
IEnumerable<string> searchFolders,
string outputFolder,
IEnumerable<NBShaderFeatureTier> tiers)
{
var validFolders = GetValidSearchFolders(searchFolders);
var materials = CollectMaterialsInValidFolders(validFolders);
return BuildResult(validFolders, materials, NormalizeOutputFolder(outputFolder), NormalizeTiers(tiers), false);
}
public static NBShaderVariantCollectionBuildResult Generate(IEnumerable<string> searchFolders, string outputFolder)
{
return Generate(searchFolders, outputFolder, null);
}
public static NBShaderVariantCollectionBuildResult Generate(
IEnumerable<string> searchFolders,
string outputFolder,
IEnumerable<NBShaderFeatureTier> tiers)
{
var validFolders = GetValidSearchFolders(searchFolders);
var materials = CollectMaterialsInValidFolders(validFolders);
var normalizedOutputFolder = NormalizeOutputFolder(outputFolder);
var normalizedTiers = NormalizeTiers(tiers);
if (normalizedTiers.Length == 0)
return BuildResult(validFolders, materials, normalizedOutputFolder, normalizedTiers, false, "No NBShader feature tiers were selected.");
if (materials.Length == 0)
return BuildResult(validFolders, materials, normalizedOutputFolder, normalizedTiers, false, "No NBShader materials were found in the selected folders.");
string errorMessage;
if (!EnsureOutputFolder(normalizedOutputFolder, out errorMessage))
return BuildResult(validFolders, materials, normalizedOutputFolder, normalizedTiers, false, errorMessage);
return BuildResult(validFolders, materials, normalizedOutputFolder, normalizedTiers, true);
}
public static Material[] CollectMaterials(IEnumerable<string> searchFolders)
{
var folders = GetValidSearchFolders(searchFolders);
return CollectMaterialsInValidFolders(folders);
}
private static Material[] CollectMaterialsInValidFolders(string[] folders)
{
if (folders.Length == 0)
return new Material[0];
var guids = AssetDatabase.FindAssets("t:Material", folders);
var result = new List<Material>();
var seen = new HashSet<string>(StringComparer.Ordinal);
for (var i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
if (string.IsNullOrEmpty(path) || !seen.Add(path))
continue;
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
if (NBShaderMaterialIntentResolver.IsNBShaderMaterial(material))
result.Add(material);
}
return result.ToArray();
}
public static string[] GetValidSearchFolders(IEnumerable<string> searchFolders)
{
if (searchFolders == null)
return new string[0];
var result = new List<string>();
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var searchFolder in searchFolders)
{
var path = NormalizeAssetPath(searchFolder);
if (string.IsNullOrEmpty(path) || !AssetDatabase.IsValidFolder(path) || !seen.Add(path))
continue;
result.Add(path);
}
return result.ToArray();
}
internal static string NormalizeAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
path = path.Replace('\\', '/').Trim();
if (path.StartsWith("Assets", StringComparison.Ordinal) ||
path.StartsWith("Packages", StringComparison.Ordinal))
{
return path.TrimEnd('/');
}
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/').TrimEnd('/');
var fullPath = Path.GetFullPath(path).Replace('\\', '/').TrimEnd('/');
if (fullPath.StartsWith(projectRoot + "/", StringComparison.OrdinalIgnoreCase))
return fullPath.Substring(projectRoot.Length + 1);
return string.Empty;
}
internal static string ToAbsolutePath(string assetPath)
{
assetPath = NormalizeAssetPath(assetPath);
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/').TrimEnd('/');
if (string.IsNullOrEmpty(assetPath))
return projectRoot;
return Path.Combine(projectRoot, assetPath).Replace('\\', '/');
}
public static string GetOutputPath(string outputFolder, NBShaderFeatureTier tier)
{
outputFolder = NormalizeOutputFolder(outputFolder);
if (string.IsNullOrEmpty(outputFolder))
outputFolder = DefaultOutputFolder;
return outputFolder.TrimEnd('/') + "/NBShader_" + tier + ".shadervariants";
}
private static NBShaderVariantCollectionBuildResult BuildResult(
string[] validFolders,
Material[] materials,
string outputFolder,
NBShaderFeatureTier[] tiers,
bool writeAssets,
string errorMessage = null)
{
if (tiers == null)
tiers = CloneDefaultTiers();
var tierResults = new NBShaderVariantCollectionTierResult[tiers.Length];
for (var i = 0; i < tiers.Length; i++)
{
var tier = tiers[i];
var buildInfo = NBShaderFeatureLevelEditorAPI.GetBuildInfo(
tier,
materials,
NBShaderBuildInfoMode.ExactMaterialVariants);
var variants = BuildSvcVariants(buildInfo);
var path = GetOutputPath(outputFolder, tier);
var generated = false;
string tierError = null;
if (writeAssets)
generated = WriteCollection(path, variants, out tierError);
tierResults[i] = new NBShaderVariantCollectionTierResult(
tier,
buildInfo != null ? buildInfo.materials.Length : 0,
buildInfo != null ? buildInfo.includedPassNames.Length : 0,
variants.Length,
path,
generated,
tierError);
}
return new NBShaderVariantCollectionBuildResult(
validFolders,
materials,
outputFolder,
tierResults,
errorMessage);
}
private static SvcVariant[] BuildSvcVariants(NBShaderBuildInfoSet buildInfo)
{
if (buildInfo == null)
return new SvcVariant[0];
var result = new List<SvcVariant>();
var seen = new HashSet<string>(StringComparer.Ordinal);
var variants = buildInfo.variants;
for (var i = 0; i < variants.Length; i++)
{
var variant = variants[i];
if (variant == null || variant.shader == null)
continue;
var keywords = variant.keywords;
var key = variant.shader.name + "|" + variant.passType + "|" + string.Join(";", keywords);
if (!seen.Add(key))
continue;
result.Add(new SvcVariant(variant.shader, variant.passType, keywords));
}
return result.ToArray();
}
private static bool WriteCollection(string path, SvcVariant[] variants, out string errorMessage)
{
errorMessage = null;
var existing = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
var collection = existing as ShaderVariantCollection;
if (existing != null && collection == null)
{
errorMessage = "Output path already exists and is not a ShaderVariantCollection:\n" + path;
return false;
}
if (collection == null)
{
collection = new ShaderVariantCollection();
AssetDatabase.CreateAsset(collection, path);
}
else
{
collection.Clear();
}
for (var i = 0; i < variants.Length; i++)
{
var variant = variants[i];
try
{
collection.Add(new ShaderVariantCollection.ShaderVariant(
variant.shader,
variant.passType,
variant.keywords));
}
catch (Exception ex)
{
Debug.LogWarning("Failed to add NBShader SVC variant: " + ex.Message);
}
}
EditorUtility.SetDirty(collection);
AssetDatabase.SaveAssetIfDirty(collection);
return true;
}
private static bool EnsureOutputFolder(string outputFolder, out string errorMessage)
{
errorMessage = null;
if (string.IsNullOrEmpty(outputFolder))
{
errorMessage = "Output folder is empty.";
return false;
}
if (AssetDatabase.IsValidFolder(outputFolder))
return true;
if (!outputFolder.StartsWith("Assets/", StringComparison.Ordinal) &&
!string.Equals(outputFolder, "Assets", StringComparison.Ordinal))
{
errorMessage = "Output folder must already exist unless it is under Assets:\n" + outputFolder;
return false;
}
var parts = outputFolder.Split('/');
var current = parts[0];
for (var i = 1; i < parts.Length; i++)
{
var next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, parts[i]);
current = next;
}
if (AssetDatabase.IsValidFolder(outputFolder))
return true;
errorMessage = "Could not create output folder:\n" + outputFolder;
return false;
}
private static string NormalizeOutputFolder(string outputFolder)
{
if (string.IsNullOrEmpty(outputFolder))
return DefaultOutputFolder;
return NormalizeAssetPath(outputFolder);
}
private static NBShaderFeatureTier[] NormalizeTiers(IEnumerable<NBShaderFeatureTier> tiers)
{
if (tiers == null)
return CloneDefaultTiers();
var selected = new HashSet<NBShaderFeatureTier>();
foreach (var tier in tiers)
{
if (IsValidTier(tier))
selected.Add(tier);
}
var result = new List<NBShaderFeatureTier>();
for (var i = 0; i < Tiers.Length; i++)
{
var tier = Tiers[i];
if (selected.Contains(tier))
result.Add(tier);
}
return result.ToArray();
}
private static bool IsValidTier(NBShaderFeatureTier tier)
{
return tier == NBShaderFeatureTier.Low ||
tier == NBShaderFeatureTier.Medium ||
tier == NBShaderFeatureTier.High ||
tier == NBShaderFeatureTier.Ultra;
}
private static NBShaderFeatureTier[] CloneDefaultTiers()
{
return (NBShaderFeatureTier[])Tiers.Clone();
}
private sealed class SvcVariant
{
public readonly Shader shader;
public readonly PassType passType;
public readonly string[] keywords;
public SvcVariant(Shader shader, PassType passType, string[] keywords)
{
this.shader = shader;
this.passType = passType;
this.keywords = keywords != null ? (string[])keywords.Clone() : new string[0];
}
}
}
public sealed class NBShaderVariantCollectionBuildResult
{
private readonly string[] m_ValidSearchFolders;
private readonly Material[] m_Materials;
private readonly NBShaderVariantCollectionTierResult[] m_Tiers;
public readonly string outputFolder;
public readonly string errorMessage;
public string[] validSearchFolders { get { return (string[])m_ValidSearchFolders.Clone(); } }
public Material[] materials { get { return (Material[])m_Materials.Clone(); } }
public NBShaderVariantCollectionTierResult[] tiers { get { return (NBShaderVariantCollectionTierResult[])m_Tiers.Clone(); } }
public int materialCount { get { return m_Materials.Length; } }
public int validSearchFolderCount { get { return m_ValidSearchFolders.Length; } }
public int generatedCount { get { return CountGenerated(m_Tiers); } }
public bool hasError { get { return !string.IsNullOrEmpty(firstErrorMessage); } }
public bool hasTierError { get { return HasTierError(m_Tiers); } }
public bool allTiersGenerated { get { return m_Tiers.Length > 0 && generatedCount == m_Tiers.Length; } }
public bool succeeded { get { return !hasError && allTiersGenerated; } }
public string firstErrorMessage { get { return GetFirstErrorMessage(errorMessage, m_Tiers); } }
public NBShaderVariantCollectionBuildResult(
string[] validSearchFolders,
Material[] materials,
string outputFolder,
NBShaderVariantCollectionTierResult[] tiers,
string errorMessage)
{
m_ValidSearchFolders = validSearchFolders != null ? (string[])validSearchFolders.Clone() : new string[0];
m_Materials = materials != null ? (Material[])materials.Clone() : new Material[0];
this.outputFolder = outputFolder;
m_Tiers = tiers != null ? (NBShaderVariantCollectionTierResult[])tiers.Clone() : new NBShaderVariantCollectionTierResult[0];
this.errorMessage = errorMessage;
}
private static int CountGenerated(NBShaderVariantCollectionTierResult[] tiers)
{
if (tiers == null || tiers.Length == 0)
return 0;
var count = 0;
for (var i = 0; i < tiers.Length; i++)
{
var tier = tiers[i];
if (tier != null && tier.generated)
count++;
}
return count;
}
private static bool HasTierError(NBShaderVariantCollectionTierResult[] tiers)
{
if (tiers == null || tiers.Length == 0)
return false;
for (var i = 0; i < tiers.Length; i++)
{
var tier = tiers[i];
if (tier != null && tier.hasError)
return true;
}
return false;
}
private static string GetFirstErrorMessage(string resultError, NBShaderVariantCollectionTierResult[] tiers)
{
if (!string.IsNullOrEmpty(resultError))
return resultError;
if (tiers == null || tiers.Length == 0)
return null;
for (var i = 0; i < tiers.Length; i++)
{
var tier = tiers[i];
if (tier != null && tier.hasError)
return tier.errorMessage;
}
return null;
}
}
public sealed class NBShaderVariantCollectionTierResult
{
public readonly NBShaderFeatureTier tier;
public readonly int materialCount;
public readonly int passCount;
public readonly int variantCount;
public readonly string outputAssetPath;
public readonly bool generated;
public readonly string errorMessage;
public bool hasError { get { return !string.IsNullOrEmpty(errorMessage); } }
public NBShaderVariantCollectionTierResult(
NBShaderFeatureTier tier,
int materialCount,
int passCount,
int variantCount,
string outputAssetPath,
bool generated,
string errorMessage)
{
this.tier = tier;
this.materialCount = materialCount;
this.passCount = passCount;
this.variantCount = variantCount;
this.outputAssetPath = outputAssetPath;
this.generated = generated;
this.errorMessage = errorMessage;
}
}
}