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 searchFolders, string outputFolder) { return Preview(searchFolders, outputFolder, null); } public static NBShaderVariantCollectionBuildResult Preview( IEnumerable searchFolders, string outputFolder, IEnumerable tiers) { var validFolders = GetValidSearchFolders(searchFolders); var materials = CollectMaterialsInValidFolders(validFolders); return BuildResult(validFolders, materials, NormalizeOutputFolder(outputFolder), NormalizeTiers(tiers), false); } public static NBShaderVariantCollectionBuildResult Generate(IEnumerable searchFolders, string outputFolder) { return Generate(searchFolders, outputFolder, null); } public static NBShaderVariantCollectionBuildResult Generate( IEnumerable searchFolders, string outputFolder, IEnumerable 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 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(); var seen = new HashSet(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(path); if (NBShaderMaterialIntentResolver.IsNBShaderMaterial(material)) result.Add(material); } return result.ToArray(); } public static string[] GetValidSearchFolders(IEnumerable searchFolders) { if (searchFolders == null) return new string[0]; var result = new List(); var seen = new HashSet(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(); var seen = new HashSet(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(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 tiers) { if (tiers == null) return CloneDefaultTiers(); var selected = new HashSet(); foreach (var tier in tiers) { if (IsValidTier(tier)) selected.Add(tier); } var result = new List(); 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; } } }