res-avatar-unity/Assets/_PoiyomiShaders/Scripts/Editor/ModularShaderSystem/ShaderGenerator.cs

980 lines
45 KiB
C#
Raw Normal View History

2023-07-16 02:51:23 +00:00
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<StringBuilder, ShaderContext> postGeneration, bool hideVariants = false)
{
path = GetPathRelativeToProject(path);
var modules = FindAllModules(shader);
var freshAssets = new Dictionary<TemplateAsset, TemplateAsset>();
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<ShaderContext>();
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<Shader>();
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<Shader>($"{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<Material> materials, Action<StringBuilder, ShaderContext> postGeneration = null)
{
path = GetPathRelativeToProject(path);
var modules = FindAllModules(shader);
var possibleVariants = GetMinimalVariants(modules, materials);
var contexts = new List<ShaderContext>();
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<ShaderContext> EnqueueShadersToGenerate(string path, ModularShader shader, IEnumerable<Material> materials, Action<StringBuilder, ShaderContext> postGeneration = null)
{
path = GetPathRelativeToProject(path);
var modules = FindAllModules(shader);
var possibleVariants = GetMinimalVariants(modules, materials);
var contexts = new List<ShaderContext>();
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<ShaderContext> contexts)
{
if (contexts == null || contexts.Count == 0) return;
var alreadyDoneShaders = new List<ModularShader>();
var freshAssets = new Dictionary<TemplateAsset, TemplateAsset>();
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<Dictionary<string, int>> GetShaderVariants(List<ShaderModule> modules)
{
var dictionary = new Dictionary<string, List<int>>();
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<int>(new[] { property.EnableValue }));
}
}
var keys = dictionary.Keys.ToList();
foreach (KeyValuePair<string,List<int>> keyValuePair in dictionary)
if(!keyValuePair.Value.Contains(0))
keyValuePair.Value.Insert(0,0);
var states = new List<Dictionary<string, int>>();
UnrollVariants(states, new Dictionary<string, int>(), dictionary, keys);
return states;
}
private static List<(Dictionary<string, int>, List<Material>)> GetMinimalVariants(List<ShaderModule> modules, IEnumerable<Material> materials)
{
var enablers = new List<string>();
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<string, int>, List<Material>)>();
foreach (Material material in materials)
{
var state = new Dictionary<string, int>();
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<Material>(new [] {material})));
else
equalState.Item2.Add(material);
}
return states;
}
private static void UnrollVariants(ICollection<Dictionary<string, int>> states, Dictionary<string, int> current, IReadOnlyDictionary<string, List<int>> dictionary, IReadOnlyList<string> keys)
{
if (current.Count == keys.Count)
{
states.Add(current);
return;
}
foreach (var value in dictionary[keys[current.Count]])
{
var next = new Dictionary<string, int>(current);
next[keys[current.Count]] = value;
UnrollVariants(states, next, dictionary, keys);
}
}
public static string GetVariantCode(Dictionary<string, int> 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<TemplateAsset, TemplateAsset> 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<TemplateAsset, TemplateAsset> 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<string, int> ActiveEnablers;
public Dictionary<TemplateAsset, TemplateAsset> FreshAssets;
public Action<StringBuilder, ShaderContext> PostGeneration;
private List<EnableProperty> _liveUpdateEnablers;
public string FilePath;
public string VariantFileName;
public string VariantName;
public string ShaderName;
public string PropertiesBlock;
public bool AreVariantsHidden;
public bool OptimizedShader;
public List<Material> Materials;
public StringBuilder ShaderFile;
private List<ShaderModule> _modules;
private List<ShaderFunction> _functions;
private List<ShaderFunction> _reorderedFunctions;
private Dictionary<ShaderFunction, ShaderModule> _modulesByFunctions;
public string Guid;
public List<ShaderModule> 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<ShaderFunction>();
_reorderedFunctions = new List<ShaderFunction>();
_modulesByFunctions = new Dictionary<ShaderFunction, ShaderModule>();
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<EnableProperty>();
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<ModuleTemplate, ShaderModule> moduleByTemplate = new Dictionary<ModuleTemplate, ShaderModule>();
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<string,List<Variable>>();
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<Variable>());
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<Variable>());
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<string,(StringBuilder, List<TemplateAsset>)>();
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<TemplateAsset>()));
if (freshAsset == null) continue;
(StringBuilder builder, List<TemplateAsset> 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<TemplateAsset>()));
if (freshAsset == null) continue;
(StringBuilder builder, List<TemplateAsset> 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<ShaderModule> modules, Dictionary<TemplateAsset, TemplateAsset> 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<Property> properties = new List<Property>();
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<ShaderContext> 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<ShaderModule> FindAllModules(ModularShader shader)
{
List<ShaderModule> modules = new List<ShaderModule>();
if (shader == null) return modules;
HashSet<string> collectionIDs = new HashSet<string>();
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<ShaderModule> modules, List<ShaderModule> output, HashSet<string> 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<Property> FindAllProperties(ModularShader shader)
{
List<Property> properties = new List<Property>();
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<ShaderFunction> FindAllFunctions(ModularShader shader)
{
var functions = new List<ShaderFunction>();
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<ShaderModule> FindActiveModules(ModularShader shader, Dictionary<string, int> activeEnablers)
{
List<ShaderModule> modules = new List<ShaderModule>();
if (shader == null) return modules;
HashSet<string> collectionIDs = new HashSet<string>();
FindActiveModules(shader.BaseModules, activeEnablers, modules, collectionIDs);
FindActiveModules(shader.AdditionalModules, activeEnablers, modules, collectionIDs);
return modules.Distinct().ToList();
}
private static void FindActiveModules(IEnumerable<ShaderModule> modules, Dictionary<string, int> activeEnablers, List<ShaderModule> output, HashSet<string> 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<string> CheckShaderIssues(ModularShader shader)
{
List<string> errors = new List<string>();
var modules = FindAllModules(shader);
for (int i = 0; i < modules.Count; i++)
{
var dependencies = new List<string>(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<string> CheckShaderIssues(List<ShaderModule> modules)
{
List<string> errors = new List<string>();
for (int i = 0; i < modules.Count; i++)
{
var dependencies = new List<string>(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;
}
}
}