using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Poiyomi.ModularShaderSystem.CibbiExtensions; using UnityEditor; using UnityEngine; namespace Poiyomi.ModularShaderSystem { public static class ShaderGenerator { public static void GenerateShader(string path, ModularShader shader, bool hideVariants = false) { GenerateShader(path, shader, null, hideVariants); } public static void GenerateShader(string path, ModularShader shader, Action postGeneration, bool hideVariants = false) { path = GetPathRelativeToProject(path); var modules = FindAllModules(shader); var freshAssets = new Dictionary(); freshAssets.AddFreshShaderToList(shader.ShaderTemplate); freshAssets.AddFreshShaderToList(shader.ShaderPropertiesTemplate); foreach (var template in modules.SelectMany(x => x.Templates)) freshAssets.AddFreshShaderToList(template.Template); foreach (var function in modules.SelectMany(x => x.Functions)) freshAssets.AddFreshShaderToList(function.ShaderFunctionCode); var possibleVariants = GetShaderVariants(modules); var contexts = new List(); var completePropertiesBlock = GetPropertiesBlock(shader, modules, freshAssets); foreach (var variant in possibleVariants) { contexts.Add(new ShaderContext { Shader = shader, PostGeneration = postGeneration, ActiveEnablers = variant, FreshAssets = freshAssets, FilePath = path, PropertiesBlock = completePropertiesBlock, AreVariantsHidden = hideVariants }); } contexts.AsParallel().ForAll(x => x.GenerateShader()); try { AssetDatabase.StartAssetEditing(); if (shader.LastGeneratedShaders != null) { foreach (Shader generatedShader in shader.LastGeneratedShaders.Where(x => x != null)) { string assetPath = AssetDatabase.GetAssetPath(generatedShader); if (string.IsNullOrWhiteSpace(assetPath)) File.Delete(assetPath); } } shader.LastGeneratedShaders = new List(); foreach (var context in contexts) { string filePath = $"{path}/" + context.VariantFileName; if (File.Exists(filePath)) { FileAttributes fileAttributes = File.GetAttributes(filePath) & ~FileAttributes.ReadOnly; File.SetAttributes(filePath, fileAttributes); File.WriteAllText(filePath, context.ShaderFile.ToString()); File.SetAttributes(filePath, fileAttributes | FileAttributes.ReadOnly); } else { File.WriteAllText(filePath, context.ShaderFile.ToString()); File.SetAttributes(filePath, File.GetAttributes(filePath) | FileAttributes.ReadOnly); } } } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.Refresh(); ApplyDefaultTextures(contexts); foreach (var context in contexts) shader.LastGeneratedShaders.Add(AssetDatabase.LoadAssetAtPath($"{path}/" + context.VariantFileName)); AssetDatabase.Refresh(); } private static string GetPathRelativeToProject(string path) { if (!Directory.Exists(path)) throw new DirectoryNotFoundException($"The folder \"{path}\" is not found"); if (!path.Contains(Application.dataPath) && !path.StartsWith("Assets")) throw new DirectoryNotFoundException($"The folder \"{path}\" is not part of the unity project"); if(!path.StartsWith("Assets")) path = path.Replace(Application.dataPath, "Assets"); return path; } public static void GenerateMinimalShader(string path, ModularShader shader, IEnumerable materials, Action postGeneration = null) { path = GetPathRelativeToProject(path); var modules = FindAllModules(shader); var possibleVariants = GetMinimalVariants(modules, materials); var contexts = new List(); foreach (var (variant, variantMaterials) in possibleVariants) { AssetDatabase.TryGetGUIDAndLocalFileIdentifier(variantMaterials[0], out string guid, out long _); contexts.Add(new ShaderContext { Shader = shader, PostGeneration = postGeneration, ActiveEnablers = variant, FilePath = path, OptimizedShader = true, Materials = variantMaterials, Guid = guid }); } contexts.GenerateMinimalShaders(); } public static List EnqueueShadersToGenerate(string path, ModularShader shader, IEnumerable materials, Action postGeneration = null) { path = GetPathRelativeToProject(path); var modules = FindAllModules(shader); var possibleVariants = GetMinimalVariants(modules, materials); var contexts = new List(); foreach (var (variant, variantMaterials) in possibleVariants) { AssetDatabase.TryGetGUIDAndLocalFileIdentifier(variantMaterials[0], out string guid, out long _); contexts.Add(new ShaderContext { Shader = shader, PostGeneration = postGeneration, ActiveEnablers = variant, FilePath = path, OptimizedShader = true, Materials = variantMaterials, Guid = guid }); } return contexts; } public static void GenerateMinimalShaders(this List contexts) { if (contexts == null || contexts.Count == 0) return; var alreadyDoneShaders = new List(); var freshAssets = new Dictionary(); foreach (var context in contexts) { context.FreshAssets = freshAssets; if (alreadyDoneShaders.Contains(context.Shader)) continue; var shader = context.Shader; var modules = FindAllModules(shader); freshAssets.AddFreshShaderToList(shader.ShaderTemplate); freshAssets.AddFreshShaderToList(shader.ShaderPropertiesTemplate); foreach (var template in modules.SelectMany(x => x.Templates)) freshAssets.AddFreshShaderToList(template.Template); foreach (var function in modules.SelectMany(x => x.Functions)) freshAssets.AddFreshShaderToList(function.ShaderFunctionCode); alreadyDoneShaders.Add(shader); } EditorUtility.DisplayProgressBar("Generating Optimized Shaders", "generating shader files", 1 / (contexts.Count + 3)); contexts.AsParallel().ForAll(x => x.GenerateShader()); try { AssetDatabase.StartAssetEditing(); int i = 0; foreach (var context in contexts) { EditorUtility.DisplayProgressBar("Generating Optimized Shaders", "Saving " + context.VariantFileName, 1 + i / (contexts.Count + 3)); File.WriteAllText($"{context.FilePath}/" + context.VariantFileName, context.ShaderFile.ToString()); i++; } } finally { EditorUtility.DisplayProgressBar("Generating Optimized Shaders", "waiting for unity to compile shaders", contexts.Count - 2 / (contexts.Count + 3)); AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); } ApplyDefaultTextures(contexts); EditorUtility.DisplayProgressBar("Generating Optimized Shaders", "applying shaders to materials", contexts.Count - 1 / (contexts.Count + 3)); foreach (var context in contexts) { var shader = Shader.Find(context.ShaderName); foreach (var material in context.Materials) { material.shader = shader; } } EditorUtility.ClearProgressBar(); } private static List> GetShaderVariants(List modules) { var dictionary = new Dictionary>(); foreach (ShaderModule module in modules) { if (module == null) continue; foreach (EnableProperty property in module.EnableProperties) { if (property == null || string.IsNullOrWhiteSpace(property.Name) || !(module.Templates?.Any(x => x.NeedsVariant) ?? false)) continue; if (dictionary.ContainsKey(property.Name)) dictionary[property.Name].Add(property.EnableValue); else dictionary.Add(property.Name, new List(new[] { property.EnableValue })); } } var keys = dictionary.Keys.ToList(); foreach (KeyValuePair> keyValuePair in dictionary) if(!keyValuePair.Value.Contains(0)) keyValuePair.Value.Insert(0,0); var states = new List>(); UnrollVariants(states, new Dictionary(), dictionary, keys); return states; } private static List<(Dictionary, List)> GetMinimalVariants(List modules, IEnumerable materials) { var enablers = new List(); foreach (ShaderModule module in modules) { if (module == null) continue; foreach (EnableProperty property in module.EnableProperties) { if (property == null || string.IsNullOrWhiteSpace(property.Name)) continue; enablers.Add(property.Name); } } enablers = enablers.Distinct().ToList(); var states = new List<(Dictionary, List)>(); foreach (Material material in materials) { var state = new Dictionary(); foreach (string enabler in enablers) state.Add(enabler, (int)material.GetFloat(enabler)); var equalState = states.Where(x => { var keys = state.Keys; foreach (string key in keys) if (x.Item1[key] != state[key]) return false; return true; }).FirstOrDefault(); if(equalState == (null, null)) states.Add((state, new List(new [] {material}))); else equalState.Item2.Add(material); } return states; } private static void UnrollVariants(ICollection> states, Dictionary current, IReadOnlyDictionary> dictionary, IReadOnlyList keys) { if (current.Count == keys.Count) { states.Add(current); return; } foreach (var value in dictionary[keys[current.Count]]) { var next = new Dictionary(current); next[keys[current.Count]] = value; UnrollVariants(states, next, dictionary, keys); } } public static string GetVariantCode(Dictionary activeEnablers) { var keys = activeEnablers.Keys.OrderBy(x => x).ToList(); bool isAllZeroes = true; var b = new StringBuilder(); foreach (string key in keys) { if (activeEnablers[key] != 0) isAllZeroes = false; b.Append($"-{activeEnablers[key]}"); } return isAllZeroes ? "" : b.ToString(); } private static void AddFreshShaderToList(this Dictionary dictionary, TemplateAsset asset) { if ((object)asset == null) return; if (asset.Equals(null)) return; if (dictionary.ContainsKey(asset)) return; string assetName = asset.name; string assetPath = AssetDatabase.GetAssetPath(asset); var genericAsset = AssetDatabase.LoadMainAssetAtPath(assetPath); TemplateAsset template = null; switch (genericAsset) { case TemplateCollectionAsset collection: template = collection.Templates.FirstOrDefault(x => x.name.Equals(assetName)); break; case TemplateAsset t: template = t; break; } dictionary.Add(asset, template); } private static TemplateAsset GetTemplate(this Dictionary dictionary, TemplateAsset asset) { if ((object)asset == null) return null; if (asset.Equals(null)) return null; return dictionary.TryGetValue(asset, out TemplateAsset result) ? result : null; } public class ShaderContext { public ModularShader Shader; public Dictionary ActiveEnablers; public Dictionary FreshAssets; public Action PostGeneration; private List _liveUpdateEnablers; public string FilePath; public string VariantFileName; public string VariantName; public string ShaderName; public string PropertiesBlock; public bool AreVariantsHidden; public bool OptimizedShader; public List Materials; public StringBuilder ShaderFile; private List _modules; private List _functions; private List _reorderedFunctions; private Dictionary _modulesByFunctions; public string Guid; public List Modules => _modules; public void GenerateShader() { _modules = FindActiveModules(Shader, ActiveEnablers); GetLiveUpdateEnablers(); ShaderFile = new StringBuilder(); VariantName = GetVariantCode(ActiveEnablers); VariantFileName = OptimizedShader ? $"{Shader.Name}{(string.IsNullOrEmpty(VariantName) ? "" : $"-g-{Guid}")}.shader" : $"{Shader.Name}{(string.IsNullOrEmpty(VariantName) ? "" : $"-v{VariantName}")}.shader"; VariantFileName = string.Join("_", VariantFileName.Split(Path.GetInvalidFileNameChars())); if (OptimizedShader) ShaderName = $"Hidden/{Shader.ShaderPath}-g-{Guid}"; else if (AreVariantsHidden && !string.IsNullOrEmpty(VariantName)) ShaderName = $"Hidden/{Shader.ShaderPath}-v{VariantName}"; else ShaderName = $"{Shader.ShaderPath}{VariantName}"; ShaderFile.AppendLine($"Shader \"{ShaderName}\""); ShaderFile.AppendLine("{"); ShaderFile.Append(string.IsNullOrEmpty(PropertiesBlock) ? GetPropertiesBlock(Shader, _modules, FreshAssets, false) : PropertiesBlock); WriteShaderSkeleton(); _functions = new List(); _reorderedFunctions = new List(); _modulesByFunctions = new Dictionary(); foreach (var module in _modules) { _functions.AddRange(module.Functions); foreach (ShaderFunction function in module.Functions) { _modulesByFunctions.Add(function, module); } } WriteVariablesToKeywords(); WriteFunctionCallsToKeywords(); WriteFunctionsToKeywords(); if (!string.IsNullOrWhiteSpace(Shader.CustomEditor)) ShaderFile.AppendLine($"CustomEditor \"{Shader.CustomEditor}\""); ShaderFile.AppendLine("}"); PostGeneration?.Invoke(ShaderFile, this); RemoveKeywords(); ShaderFile.Replace("\r\n", "\n"); ShaderFile = CleanupShaderFile(ShaderFile); } private void GetLiveUpdateEnablers() { _liveUpdateEnablers = new List(); var staticEnablers = ActiveEnablers.Keys.ToList(); foreach (var property in _modules.SelectMany(x => x.EnableProperties)) { if(property != null && !string.IsNullOrWhiteSpace(property.Name) && !staticEnablers.Contains(property.Name)) _liveUpdateEnablers.Add(property); } _liveUpdateEnablers = _liveUpdateEnablers.Distinct().ToList(); } private void WriteFunctionCallsToKeywords() { foreach (var startKeyword in _functions.Where(x => x.AppendAfter?.StartsWith("#K#") ?? false).Select(x => x.AppendAfter).Distinct()) { if (!ShaderFile.Contains(startKeyword)) continue; var callSequence = new StringBuilder(); WriteFunctionCallSequence(callSequence, startKeyword); var m = Regex.Matches(ShaderFile.ToString(), $@"{startKeyword}(\s|$)", RegexOptions.Multiline); for (int i = m.Count - 1; i >= 0; i--) ShaderFile.Insert(m[i].Index, callSequence.ToString()); } } private void WriteShaderSkeleton() { ShaderFile.AppendLine("SubShader"); ShaderFile.AppendLine("{"); ShaderFile.AppendLine(FreshAssets.GetTemplate(Shader.ShaderTemplate).Template); Dictionary moduleByTemplate = new Dictionary(); Dictionary<(string, string), string> convertedKeyword = new Dictionary<(string, string), string>(); int instanceCounter = 0; foreach (var module in _modules) foreach (var template in module.Templates) moduleByTemplate.Add(template, module); foreach (var template in _modules.SelectMany(x => x.Templates).OrderBy(x => x.Queue)) { var freshTemplate = FreshAssets.GetTemplate(template.Template); var module = moduleByTemplate[template]; if (freshTemplate == null) continue; bool hasEnabler = module.EnableProperties.Any(x => x != null && !string.IsNullOrEmpty(x.Name)); bool isFilteredIn = hasEnabler && module.EnableProperties.All(x => (x == null || string.IsNullOrEmpty(x.Name)) || ActiveEnablers.TryGetValue(x.Name, out _)); bool needsIf = hasEnabler && !isFilteredIn && !template.NeedsVariant; var tmp = new StringBuilder(); if (!needsIf) { tmp.AppendLine(freshTemplate.Template); } else { string condition = string.Join(" && ", module.EnableProperties .Where(x => (x != null && !string.IsNullOrEmpty(x.Name)) && !ActiveEnablers.TryGetValue(x.Name, out _)) .Select(x => $"{x.Name} == {x.EnableValue}")); tmp.AppendLine($"if({condition})"); tmp.AppendLine("{"); tmp.AppendLine(freshTemplate.Template); tmp.AppendLine("}"); } MatchCollection mki = Regex.Matches(tmp.ToString(), @"#KI#\S*", RegexOptions.Multiline); for (int i = mki.Count - 1; i >= 0; i--) { string newKeyword; if (convertedKeyword.TryGetValue((module.Id, mki[i].Value), out string replacedKeyword)) { newKeyword = replacedKeyword; } else { newKeyword = $"{mki[i].Value}{instanceCounter++}"; convertedKeyword.Add((module.Id, mki[i].Value), newKeyword); } tmp.Replace(mki[i].Value, newKeyword); } foreach (var keyword in template.Keywords.Count == 0 ? new[] { MSSConstants.DEFAULT_CODE_KEYWORD } : template.Keywords.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray()) { MatchCollection m = Regex.Matches(ShaderFile.ToString(), $@"#K#{keyword}(\s|$)", RegexOptions.Multiline); for (int i = m.Count - 1; i >= 0; i--) ShaderFile.Insert(m[i].Index, tmp.ToString()); if (convertedKeyword.TryGetValue((module.Id, $@"#KI#{keyword}"), out string replacedKeyword)) { m = Regex.Matches(ShaderFile.ToString(), $@"{replacedKeyword}(\s|$)", RegexOptions.Multiline); for (int i = m.Count - 1; i >= 0; i--) ShaderFile.Insert(m[i].Index, tmp.ToString()); } } } MatchCollection mkr = Regex.Matches(ShaderFile.ToString(), @"#KI#\S*", RegexOptions.Multiline); for (int i = mkr.Count - 1; i >= 0; i--) ShaderFile.Remove(mkr[i].Index, mkr[i].Length); ShaderFile.AppendLine("}"); } private void WriteVariablesToKeywords() { var variableDeclarations = new Dictionary>(); foreach (ShaderFunction function in _functions) { if (function.VariableKeywords.Count > 0) { foreach (string keyword in function.VariableKeywords) { if (!variableDeclarations.ContainsKey(keyword)) variableDeclarations.Add(keyword, new List()); foreach (Variable variable in function.UsedVariables) variableDeclarations[keyword].Add(variable); } } else { if (!variableDeclarations.ContainsKey(MSSConstants.DEFAULT_VARIABLES_KEYWORD)) variableDeclarations.Add(MSSConstants.DEFAULT_VARIABLES_KEYWORD, new List()); foreach (Variable variable in function.UsedVariables) variableDeclarations[MSSConstants.DEFAULT_VARIABLES_KEYWORD].Add(variable); } } foreach (var declaration in variableDeclarations) { declaration.Value.AddRange(_liveUpdateEnablers.Select(x => x.ToVariable())); var decCode = string.Join("\n", declaration.Value.Distinct().OrderBy(x => x.Type).Select(x => x.GetDefinition())) + "\n\n"; MatchCollection m = Regex.Matches(ShaderFile.ToString(), $@"#K#{declaration.Key}\s", RegexOptions.Multiline); for (int i = m.Count - 1; i >= 0; i--) ShaderFile.Insert(m[i].Index, decCode); } } private void WriteFunctionsToKeywords() { var keywordedCode = new Dictionary)>(); foreach (ShaderFunction function in _reorderedFunctions) { var freshAsset = FreshAssets.GetTemplate(function.ShaderFunctionCode); if (function.CodeKeywords.Count > 0) { foreach (string keyword in function.CodeKeywords) { if (!keywordedCode.ContainsKey(keyword)) keywordedCode.Add(keyword, (new StringBuilder(), new List())); if (freshAsset == null) continue; (StringBuilder builder, List assets) = keywordedCode[keyword]; if (assets.Contains(freshAsset)) continue; builder.AppendLine(freshAsset.Template); assets.Add(freshAsset); } } else { if (!keywordedCode.ContainsKey(MSSConstants.DEFAULT_CODE_KEYWORD)) keywordedCode.Add(MSSConstants.DEFAULT_CODE_KEYWORD, (new StringBuilder(), new List())); if (freshAsset == null) continue; (StringBuilder builder, List assets) = keywordedCode[MSSConstants.DEFAULT_CODE_KEYWORD]; if (assets.Contains(freshAsset)) continue; builder.AppendLine(freshAsset.Template); assets.Add(freshAsset); } } foreach (var code in keywordedCode) { MatchCollection m = Regex.Matches(ShaderFile.ToString(), $@"#K#{code.Key}\s", RegexOptions.Multiline); for (int i = m.Count - 1; i >= 0; i--) ShaderFile.Insert(m[i].Index, code.Value.Item1.ToString()); } } private void WriteFunctionCallSequence(StringBuilder callSequence, string appendAfter) { foreach (var function in _functions.Where(x => x.AppendAfter.Equals(appendAfter)).OrderBy(x => x.Queue)) { _reorderedFunctions.Add(function); ShaderModule module = _modulesByFunctions[function]; bool hasEnabler = module.EnableProperties.Any(x => x != null && !string.IsNullOrEmpty(x.Name)); bool isFilteredIn = hasEnabler && module.EnableProperties.All(x => (x == null || string.IsNullOrEmpty(x.Name)) || ActiveEnablers.TryGetValue(x.Name, out _)); bool needsIf = hasEnabler && !isFilteredIn; if (needsIf) { string condition = string.Join(" && ", module.EnableProperties .Where(x => (x != null && !string.IsNullOrEmpty(x.Name)) && !ActiveEnablers.TryGetValue(x.Name, out _)) .Select(x => $"{x.Name} == {x.EnableValue}")); callSequence.AppendLine($"if({condition})"); callSequence.AppendLine("{"); } callSequence.AppendLine($"{function.Name}();"); WriteFunctionCallSequence(callSequence, function.Name); if (needsIf) callSequence.AppendLine("}"); } } private void RemoveKeywords() { int current = 0; while (current < ShaderFile.Length) { if (ShaderFile.Length >= current + 3 && ShaderFile[current] == '#' && ShaderFile[current + 1] == 'K' && ShaderFile[current + 2] == '#') { int end = current+3; bool stillToRemove = true; while (end < ShaderFile.Length) { if (char.IsWhiteSpace(ShaderFile[end])) { ShaderFile.Remove(current, end - current); stillToRemove = false; break; } end++; } if(stillToRemove) ShaderFile.Remove(current, end - current); } current++; } } private static bool CheckPropertyBlockLine(StringBuilder builder, StringReader reader, string line, ref int tabs, ref bool deleteEmptyLine) { string ln = null; line = line.Trim(); if (string.IsNullOrEmpty(line)) { if (deleteEmptyLine) return false; deleteEmptyLine = true; } else { deleteEmptyLine = false; } if (line.StartsWith("}") && (ln = reader.ReadLine()) != null && ln.Trim().StartsWith("SubShader")) tabs--; builder.AppendLineTabbed(tabs, line); if (!string.IsNullOrWhiteSpace(ln)) if (CheckPropertyBlockLine(builder, reader, ln, ref tabs, ref deleteEmptyLine)) return true; if (line.StartsWith("}") && ln != null && ln.Trim().StartsWith("SubShader")) return true; return false; } private static StringBuilder CleanupShaderFile(StringBuilder shaderVariant) { var finalFile = new StringBuilder(); using (var sr = new StringReader(shaderVariant.ToString())) { string line; int tabs = 0; bool deleteEmptyLine = false; while ((line = sr.ReadLine()) != null) { line = line.Trim(); if (string.IsNullOrEmpty(line)) { if (deleteEmptyLine) continue; deleteEmptyLine = true; } else { deleteEmptyLine = false; } if (line.StartsWith("Properties")) { finalFile.AppendLineTabbed(tabs, line); string ln = sr.ReadLine()?.Trim(); // When the previous line is the one containing "Properties" we always know finalFile.AppendLineTabbed(tabs, ln); // that the next line is "{" so we just write it down before increasing the tabs tabs++; while ((ln = sr.ReadLine()) != null) // we should be escaping this loop way before actually meeting the condition, but you never know { if (CheckPropertyBlockLine(finalFile, sr, ln, ref tabs, ref deleteEmptyLine)) break; } continue; } if (!line.StartsWith("//") && (line.StartsWith("}") || line.EndsWith("}") && !line.Contains("{"))) tabs--; finalFile.AppendLineTabbed(tabs, line); if (!line.StartsWith("//") && (line.StartsWith("{") || line.EndsWith("{"))) tabs++; } } return finalFile; } } private static string GetPropertiesBlock(ModularShader shader, List modules, Dictionary freshAssets, bool includeEnablers = true) { var block = new StringBuilder(); block.AppendLine("Properties"); block.AppendLine("{"); if (shader.UseTemplatesForProperties) { var freshTemplate = freshAssets.GetTemplate(shader.ShaderPropertiesTemplate); if (freshTemplate != null) block.AppendLine(freshTemplate.Template); block.AppendLine($"#K#{MSSConstants.TEMPLATE_PROPERTIES_KEYWORD}"); } else { List properties = new List(); properties.AddRange(shader.Properties.Where(x => !string.IsNullOrWhiteSpace(x.Name) || x.Attributes.Count > 0)); foreach (var module in modules.Where(x => x != null)) { properties.AddRange(module.Properties.Where(x => !string.IsNullOrWhiteSpace(x.Name) || x.Attributes.Count > 0)); if (module.EnableProperties.Count > 0 && includeEnablers) properties.AddRange(module.EnableProperties.Where(x => !string.IsNullOrWhiteSpace(x.Name))); } foreach (var prop in properties.Distinct()) { if (string.IsNullOrWhiteSpace(prop.Type) && !string.IsNullOrWhiteSpace(prop.Name)) { prop.Type = "Float"; prop.DefaultValue = "0.0"; } string attributes = prop.Attributes.Count == 0 ? "" : $"[{string.Join("][", prop.Attributes)}]"; block.AppendLine(string.IsNullOrWhiteSpace(prop.Name) ? attributes : $"{attributes} {prop.Name}(\"{prop.DisplayName}\", {prop.Type}) = {prop.DefaultValue}"); } } block.AppendLine("}"); return block.ToString(); } private static void ApplyDefaultTextures(List contexts) { foreach (var context in contexts) { var importedShader = AssetImporter.GetAtPath($"{context.FilePath}/" + context.VariantFileName) as ShaderImporter; var customTextures = context.Modules.SelectMany(x => x.Properties).Where(x => x.DefaultTextureAsset != null).ToList(); customTextures.AddRange(context.Shader.Properties.Where(x => x.DefaultTextureAsset != null).ToList()); if (importedShader != null) { importedShader.SetDefaultTextures(customTextures.Select(x => x.Name).ToArray(), customTextures.Select(x => x.DefaultTextureAsset).ToArray()); importedShader.SetNonModifiableTextures(customTextures.Select(x => x.Name).ToArray(), customTextures.Select(x => x.DefaultTextureAsset).ToArray()); } AssetDatabase.ImportAsset($"{context.FilePath}/" + context.VariantFileName); } } public static List FindAllModules(ModularShader shader) { List modules = new List(); if (shader == null) return modules; HashSet collectionIDs = new HashSet(); FindModules(shader.BaseModules.Where(x => x != null), modules, collectionIDs); FindModules(shader.AdditionalModules.Where(x => x != null), modules, collectionIDs); return modules; } public static void FindModules(IEnumerable modules, List output, HashSet collectionIDs) { foreach (var module in modules) { switch (module) { case ModuleCollection collection: if (collectionIDs.Contains(collection.Id)) continue; collectionIDs.Add(collection.Id); FindModules(collection.Modules, output, collectionIDs); break; default: output.Add(module); break; } } } public static List FindAllProperties(ModularShader shader) { List properties = new List(); if (shader == null) return properties; properties.AddRange(shader.Properties.Where(x => !string.IsNullOrWhiteSpace(x.Name) || x.Attributes.Count == 0)); foreach (var module in shader.BaseModules.Where(x => x != null)) { properties.AddRange(module.Properties.Where(x => !string.IsNullOrWhiteSpace(x.Name) || x.Attributes.Count == 0)); if (module.EnableProperties.Count > 0) properties.AddRange(module.EnableProperties.Where(x => !string.IsNullOrWhiteSpace(x.Name))); } foreach (var module in shader.AdditionalModules.Where(x => x != null)) { properties.AddRange(module.Properties.Where(x => !string.IsNullOrWhiteSpace(x.Name) || x.Attributes.Count == 0)); if (module.EnableProperties.Count > 0) properties.AddRange(module.EnableProperties.Where(x => !string.IsNullOrWhiteSpace(x.Name))); } return properties.Distinct().ToList(); } public static List FindAllFunctions(ModularShader shader) { var functions = new List(); if (shader == null) return functions; foreach (var module in shader.BaseModules) functions.AddRange(module.Functions); foreach (var module in shader.AdditionalModules) functions.AddRange(module.Functions); return functions; } public static List FindActiveModules(ModularShader shader, Dictionary activeEnablers) { List modules = new List(); if (shader == null) return modules; HashSet collectionIDs = new HashSet(); FindActiveModules(shader.BaseModules, activeEnablers, modules, collectionIDs); FindActiveModules(shader.AdditionalModules, activeEnablers, modules, collectionIDs); return modules.Distinct().ToList(); } private static void FindActiveModules(IEnumerable modules, Dictionary activeEnablers, List output, HashSet collectionIDs) { foreach (var module in modules) { if (module == null) continue; if (module is ModuleCollection collection) { if (collectionIDs.Contains(collection.Id)) continue; collectionIDs.Add(collection.Id); FindActiveModules(collection.Modules, activeEnablers, output, collectionIDs); continue; } bool hasEnabler = module.EnableProperties.Any(x => x != null && !string.IsNullOrEmpty(x.Name)); bool hasKey = hasEnabler && module.EnableProperties.Any(x => activeEnablers.TryGetValue(x.Name, out _)); if (!hasEnabler || !hasKey || (module.EnableProperties.All(x => { if (x.Name == null || string.IsNullOrEmpty(x.Name)) return true; if (!activeEnablers.TryGetValue(x.Name, out int value)) return true; return x.EnableValue == value; }))) output.Add(module); } } public static List CheckShaderIssues(ModularShader shader) { List errors = new List(); var modules = FindAllModules(shader); for (int i = 0; i < modules.Count; i++) { var dependencies = new List(modules[i].ModuleDependencies); for (int j = 0; j < modules.Count; j++) { if (modules[j].IncompatibleWith.Any(x => x.Equals(modules[i].Id))) errors.Add($"Module \"{modules[j].Name}\" ({modules[j].Id}) is incompatible with module \"{modules[i].name}\" ({modules[i].Id})."); if (i != j && modules[i].Id.Equals(modules[j].Id)) errors.Add($"Module \"{modules[i].Name}\" ({modules[i].Id}) is duplicate."); if (modules[i] is CibbiExtensions.ModuleCollection) { foreach (ShaderModule module in ((CibbiExtensions.ModuleCollection)modules[i]).Modules) { if (module.Id.Equals(modules[j].Id)) errors.Add($"Module \"{modules[i].Name}\" ({modules[i].Id}) is duplicate in ModuleCollection: {modules[i]?.Name} ({modules[i].Id})"); } } if (dependencies.Contains(modules[j].Id)) dependencies.Remove(modules[j].Id); } foreach (string t in dependencies) errors.Add($"Module \"{modules[i].Name}\" ({modules[i].Id}) has missing dependency id \"{t}\"."); } return errors; } public static List CheckShaderIssues(List modules) { List errors = new List(); for (int i = 0; i < modules.Count; i++) { var dependencies = new List(modules[i].ModuleDependencies); for (int j = 0; j < modules.Count; j++) { if (modules[j].IncompatibleWith.Any(x => x.Equals(modules[i].Id))) errors.Add($"Module \"{modules[j].Name}\" ({modules[j].Id}) is incompatible with module \"{modules[i].name}\" ({modules[i].Id})."); if (i != j && modules[i].Id.Equals(modules[j].Id)) errors.Add($"Module \"{modules[i].Name}\" ({modules[i].Id}) is duplicate."); if (dependencies.Contains(modules[j].Id)) dependencies.Remove(modules[j].Id); } foreach (string t in dependencies) errors.Add($"Module \"{modules[i].Name}\" ({modules[i].Id}) has missing dependency id \"{t}\"."); } return errors; } } }