using System; using System.Text; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.IsolatedStorage; using System.Linq; using System.Text.RegularExpressions; using UnityEditor.Graphing; using UnityEditor.Graphing.Util; using UnityEditorInternal; using Debug = UnityEngine.Debug; using System.Reflection; using System.Runtime.Remoting.Metadata.W3cXsd2001; using UnityEditor.ProjectWindowCallback; using UnityEditor.ShaderGraph.Internal; using UnityEngine; using UnityEngine.Rendering; using Object = System.Object; namespace UnityEditor.ShaderGraph { // a structure used to track active variable dependencies in the shader code // (i.e. the use of uv0 in the pixel shader means we need a uv0 interpolator, etc.) struct Dependency { public string name; // the name of the thing public string dependsOn; // the thing above depends on this -- it reads it / calls it / requires it to be defined public Dependency(string name, string dependsOn) { this.name = name; this.dependsOn = dependsOn; } }; [System.AttributeUsage(System.AttributeTargets.Struct)] class InterpolatorPack : System.Attribute { public InterpolatorPack() { } } // attribute used to flag a field as needing an HLSL semantic applied // i.e. float3 position : POSITION; // ^ semantic [System.AttributeUsage(System.AttributeTargets.Field)] class Semantic : System.Attribute { public string semantic; public Semantic(string semantic) { this.semantic = semantic; } } // attribute used to flag a field as being optional // i.e. if it is not active, then we can omit it from the struct [System.AttributeUsage(System.AttributeTargets.Field)] class Optional : System.Attribute { public Optional() { } } // attribute used to override the HLSL type of a field with a custom type string [System.AttributeUsage(System.AttributeTargets.Field)] class OverrideType : System.Attribute { public string typeName; public OverrideType(string typeName) { this.typeName = typeName; } } // attribute used to force system generated fields to bottom of structs [System.AttributeUsage(System.AttributeTargets.Field)] class SystemGenerated : System.Attribute { public SystemGenerated() { } } // attribute used to disable a field using a preprocessor #if [System.AttributeUsage(System.AttributeTargets.Field)] class PreprocessorIf : System.Attribute { public string conditional; public PreprocessorIf(string conditional) { this.conditional = conditional; } } class NewGraphAction : EndNameEditAction { Target[] m_Targets; public Target[] targets { get => m_Targets; set => m_Targets = value; } BlockFieldDescriptor[] m_Blocks; public BlockFieldDescriptor[] blocks { get => m_Blocks; set => m_Blocks = value; } public override void Action(int instanceId, string pathName, string resourceFile) { var graph = new GraphData(); graph.AddContexts(); graph.InitializeOutputs(m_Targets, m_Blocks); graph.path = "Shader Graphs"; FileUtilities.WriteShaderGraphToDisk(pathName, graph); AssetDatabase.Refresh(); UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath(pathName); Selection.activeObject = obj; } } static class GraphUtil { internal static bool CheckForRecursiveDependencyOnPendingSave(string saveFilePath, IEnumerable subGraphNodes, string context = null) { var overwriteGUID = AssetDatabase.AssetPathToGUID(saveFilePath); if (!string.IsNullOrEmpty(overwriteGUID)) { foreach (var sgNode in subGraphNodes) { var asset = sgNode?.asset; if (asset == null) { // cannot read the asset; might be recursive but we can't tell... should we return "maybe"? // I think to be minimally intrusive to the user we can assume "No" in this case, // even though this may miss recursions in extraordinary cases. // it's more important to allow the user to save their files than to catch 100% of recursions continue; } else if ((asset.assetGuid == overwriteGUID) || asset.descendents.Contains(overwriteGUID)) { if (context != null) { Debug.LogWarning(context + " CANCELLED to avoid a generating a reference loop: the SubGraph '" + sgNode.asset.name + "' references the target file '" + saveFilePath + "'"); EditorUtility.DisplayDialog( context + " CANCELLED", "Saving the file would generate a reference loop, because the SubGraph '" + sgNode.asset.name + "' references the target file '" + saveFilePath + "'", "Cancel"); } return true; } } } return false; } internal static string ConvertCamelCase(string text, bool preserveAcronyms) { if (string.IsNullOrEmpty(text)) return string.Empty; StringBuilder newText = new StringBuilder(text.Length * 2); newText.Append(text[0]); for (int i = 1; i < text.Length; i++) { if (char.IsUpper(text[i])) if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) || (preserveAcronyms && char.IsUpper(text[i - 1]) && i < text.Length - 1 && !char.IsUpper(text[i + 1]))) newText.Append(' '); newText.Append(text[i]); } return newText.ToString(); } public static void CreateNewGraph() { var graphItem = ScriptableObject.CreateInstance(); graphItem.targets = null; ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, graphItem, string.Format("New Shader Graph.{0}", ShaderGraphImporter.Extension), null, null); } public static void CreateNewGraphWithOutputs(Target[] targets, BlockFieldDescriptor[] blockDescriptors) { var graphItem = ScriptableObject.CreateInstance(); graphItem.targets = targets; graphItem.blocks = blockDescriptors; ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, graphItem, string.Format("New Shader Graph.{0}", ShaderGraphImporter.Extension), null, null); } public static bool TryGetMetadataOfType(this Shader shader, out T obj) where T : ScriptableObject { obj = null; if (!shader.IsShaderGraphAsset()) return false; var path = AssetDatabase.GetAssetPath(shader); foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(path)) { if (asset is T metadataAsset) { obj = metadataAsset; return true; } } return false; } // this will work on ALL shadergraph-built shaders, in memory or asset based public static bool IsShaderGraph(this Material material) { var shaderGraphTag = material.GetTag("ShaderGraphShader", false, null); return !string.IsNullOrEmpty(shaderGraphTag); } // NOTE: this ONLY works for ASSET based Shaders, if you created a temporary shader in memory, it won't work public static bool IsShaderGraphAsset(this Shader shader) { var path = AssetDatabase.GetAssetPath(shader); var importer = AssetImporter.GetAtPath(path); return importer is ShaderGraphImporter; } [Obsolete("Use IsShaderGraphAsset instead", false)] public static bool IsShaderGraph(this Shader shader) => shader.IsShaderGraphAsset(); static void Visit(List outputList, Dictionary unmarkedNodes, AbstractMaterialNode node) { if (!unmarkedNodes.ContainsKey(node.objectId)) return; foreach (var slot in node.GetInputSlots()) { foreach (var edge in node.owner.GetEdges(slot.slotReference)) { var inputNode = edge.outputSlot.node; Visit(outputList, unmarkedNodes, inputNode); } } unmarkedNodes.Remove(node.objectId); outputList.Add(node); } static Dictionary s_LegacyTypeRemapping; public static Dictionary GetLegacyTypeRemapping() { if (s_LegacyTypeRemapping == null) { s_LegacyTypeRemapping = new Dictionary(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { foreach (var type in assembly.GetTypesOrNothing()) { if (type.IsAbstract) continue; foreach (var attribute in type.GetCustomAttributes(typeof(FormerNameAttribute), false)) { var legacyAttribute = (FormerNameAttribute)attribute; var serializationInfo = new SerializationHelper.TypeSerializationInfo { fullName = legacyAttribute.fullName }; s_LegacyTypeRemapping[serializationInfo] = SerializationHelper.GetTypeSerializableAsString(type); } } } } return s_LegacyTypeRemapping; } /// /// Sanitizes a supplied string such that it does not collide /// with any other name in a collection. /// /// /// A collection of names that the new name should not collide with. /// /// /// The format applied to the name if a duplicate exists. /// This must be a format string that contains `{0}` and `{1}` /// once each. An example could be `{0} ({1})`, which will append ` (n)` /// to the name for the n`th duplicate. /// /// /// The name to be sanitized. /// /// /// A name that is distinct form any name in `existingNames`. /// internal static string SanitizeName(IEnumerable existingNames, string duplicateFormat, string name, string disallowedPatternRegex = "\"") { name = Regex.Replace(name, disallowedPatternRegex, "_"); return DeduplicateName(existingNames, duplicateFormat, name); } internal static string SanitizeCategoryName(string categoryName, string disallowedPatternRegex = "\"") { return Regex.Replace(categoryName, disallowedPatternRegex, "_"); } internal static string DeduplicateName(IEnumerable existingNames, string duplicateFormat, string name) { if (!existingNames.Contains(name)) return name; string escapedDuplicateFormat = Regex.Escape(duplicateFormat); // Escaped format will escape string interpolation, so the escape characters must be removed for these. escapedDuplicateFormat = escapedDuplicateFormat.Replace(@"\{0}", @"{0}"); escapedDuplicateFormat = escapedDuplicateFormat.Replace(@"\{1}", @"{1}"); var baseRegex = new Regex(string.Format(escapedDuplicateFormat, @"^(.*)", @"(\d+)")); var baseMatch = baseRegex.Match(name); if (baseMatch.Success) name = baseMatch.Groups[1].Value; string baseNameExpression = string.Format(@"^{0}", Regex.Escape(name)); var regex = new Regex(string.Format(escapedDuplicateFormat, baseNameExpression, @"(\d+)") + "$"); var existingDuplicateNumbers = existingNames.Select(existingName => regex.Match(existingName)).Where(m => m.Success).Select(m => int.Parse(m.Groups[1].Value)).Where(n => n > 0).Distinct().ToList(); var duplicateNumber = 1; existingDuplicateNumbers.Sort(); if (existingDuplicateNumbers.Any() && existingDuplicateNumbers.First() == 1) { duplicateNumber = existingDuplicateNumbers.Last() + 1; for (var i = 1; i < existingDuplicateNumbers.Count; i++) { if (existingDuplicateNumbers[i - 1] != existingDuplicateNumbers[i] - 1) { duplicateNumber = existingDuplicateNumbers[i - 1] + 1; break; } } } return string.Format(duplicateFormat, name, duplicateNumber); } public static bool WriteToFile(string path, string content) { try { File.WriteAllText(path, content); return true; } catch (Exception e) { Debug.LogError(e); return false; } } public static void OpenFile(string path) { string filePath = Path.GetFullPath(path); if (!File.Exists(filePath)) { Debug.LogError(string.Format("Path {0} doesn't exists", path)); return; } string externalScriptEditor = ScriptEditorUtility.GetExternalScriptEditor(); if (externalScriptEditor != "internal") { InternalEditorUtility.OpenFileAtLineExternal(filePath, 0); } else { Process p = new Process(); p.StartInfo.FileName = filePath; p.EnableRaisingEvents = true; p.Exited += (Object obj, EventArgs args) => { if (p.ExitCode != 0) Debug.LogWarningFormat("Unable to open {0}: Check external editor in preferences", filePath); }; p.Start(); } } // // Find all nodes of the given type downstream from the given node // Returns a unique list. So even if a node can be reached through different paths it will be present only once. // public static List FindDownStreamNodesOfType(AbstractMaterialNode node) where NodeType : AbstractMaterialNode { // Should never be called without a node Debug.Assert(node != null); HashSet visitedNodes = new HashSet(); List vtNodes = new List(); Queue nodeStack = new Queue(); nodeStack.Enqueue(node); visitedNodes.Add(node); while (nodeStack.Count > 0) { AbstractMaterialNode visit = nodeStack.Dequeue(); // Flood fill through all the nodes foreach (var slot in visit.GetInputSlots()) { foreach (var edge in visit.owner.GetEdges(slot.slotReference)) { var inputNode = edge.outputSlot.node; if (!visitedNodes.Contains(inputNode)) { nodeStack.Enqueue(inputNode); visitedNodes.Add(inputNode); } } } // Extract vt node if (visit is NodeType) { NodeType vtNode = visit as NodeType; vtNodes.Add(vtNode); } } return vtNodes; } } }