using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using UnityEditor.ShaderGraph; using System.Text.RegularExpressions; using UnityEditor.ShaderGraph.Drawing; using UnityEngine; using UnityEngine.Pool; using UnityEngine.Rendering.ShaderGraph; namespace UnityEditor.Graphing { class SlotConfigurationException : Exception { public SlotConfigurationException(string message) : base(message) { } } static class NodeUtils { static string NodeDocSuffix = "-Node"; public static void SlotConfigurationExceptionIfBadConfiguration(AbstractMaterialNode node, IEnumerable expectedInputSlots, IEnumerable expectedOutputSlots) { var missingSlots = new List(); var inputSlots = expectedInputSlots as IList ?? expectedInputSlots.ToList(); missingSlots.AddRange(inputSlots.Except(node.GetInputSlots().Select(x => x.id))); var outputSlots = expectedOutputSlots as IList ?? expectedOutputSlots.ToList(); missingSlots.AddRange(outputSlots.Except(node.GetOutputSlots().Select(x => x.id))); if (missingSlots.Count == 0) return; var toPrint = missingSlots.Select(x => x.ToString()); throw new SlotConfigurationException(string.Format("Missing slots {0} on node {1}", string.Join(", ", toPrint.ToArray()), node)); } public static IEnumerable GetAllEdges(AbstractMaterialNode node) { var result = new List(); var validSlots = ListPool.Get(); validSlots.AddRange(node.GetInputSlots()); for (int index = 0; index < validSlots.Count; index++) { var inputSlot = validSlots[index]; result.AddRange(node.owner.GetEdges(inputSlot.slotReference)); } validSlots.Clear(); validSlots.AddRange(node.GetOutputSlots()); for (int index = 0; index < validSlots.Count; index++) { var outputSlot = validSlots[index]; result.AddRange(node.owner.GetEdges(outputSlot.slotReference)); } ListPool.Release(validSlots); return result; } public static string GetDuplicateSafeNameForSlot(AbstractMaterialNode node, int slotId, string name) { List slots = new List(); node.GetSlots(slots); name = name.Trim(); return GraphUtil.SanitizeName(slots.Where(p => p.id != slotId).Select(p => p.RawDisplayName()), "{0} ({1})", name); } // CollectNodesNodeFeedsInto looks at the current node and calculates // which child nodes it depends on for it's calculation. // Results are returned depth first so by processing each node in // order you can generate a valid code block. public enum IncludeSelf { Include, Exclude } public static SlotReference DepthFirstCollectRedirectNodeFromNode(RedirectNodeData node) { var inputSlot = node.FindSlot(RedirectNodeData.kInputSlotID); foreach (var edge in node.owner.GetEdges(inputSlot.slotReference)) { // get the input details var outputSlotRef = edge.outputSlot; var inputNode = outputSlotRef.node; // If this is a redirect node we continue to look for the top one if (inputNode is RedirectNodeData redirectNode) { return DepthFirstCollectRedirectNodeFromNode(redirectNode); } return outputSlotRef; } // If no edges it is the first redirect node without an edge going into it and we should return the slot ref return node.GetSlotReference(RedirectNodeData.kInputSlotID); } public static void DepthFirstCollectNodesFromNode(List nodeList, AbstractMaterialNode node, IncludeSelf includeSelf = IncludeSelf.Include, List> keywordPermutation = null, bool ignoreActiveState = false) { // no where to start if (node == null) return; // already added this node if (nodeList.Contains(node)) return; IEnumerable ids; // If this node is a keyword node and we have an active keyword permutation // The only valid port id is the port that corresponds to that keywords value in the active permutation if (node is KeywordNode keywordNode && keywordPermutation != null) { var valueInPermutation = keywordPermutation.Where(x => x.Key == keywordNode.keyword).FirstOrDefault(); ids = new int[] { keywordNode.GetSlotIdForPermutation(valueInPermutation) }; } else { ids = node.GetInputSlots().Select(x => x.id); } foreach (var slot in ids) { foreach (var edge in node.owner.GetEdges(node.GetSlotReference(slot))) { var outputNode = edge.outputSlot.node; if (outputNode != null) DepthFirstCollectNodesFromNode(nodeList, outputNode, keywordPermutation: keywordPermutation, ignoreActiveState: ignoreActiveState); } } if (includeSelf == IncludeSelf.Include && (node.isActive || ignoreActiveState)) nodeList.Add(node); } internal static List GetParentNodes(AbstractMaterialNode node) { List nodeList = new List(); var ids = node.GetInputSlots().Select(x => x.id); foreach (var slot in ids) { if (node.owner == null) break; foreach (var edge in node.owner.GetEdges(node.FindSlot(slot).slotReference)) { var outputNode = ((Edge)edge).outputSlot.node; if (outputNode != null) { nodeList.Add(outputNode); } } } return nodeList; } private static bool ActiveLeafExists(AbstractMaterialNode node) { //if our active state has been explicitly set to a value use it switch (node.activeState) { case AbstractMaterialNode.ActiveState.Implicit: break; case AbstractMaterialNode.ActiveState.ExplicitInactive: return false; case AbstractMaterialNode.ActiveState.ExplicitActive: return true; } List parentNodes = GetParentNodes(node); //at this point we know we are not explicitly set to a state, //so there is no reason to be inactive if (parentNodes.Count == 0) { return true; } bool output = false; foreach (var parent in parentNodes) { output |= ActiveLeafExists(parent); if (output) { break; } } return output; } private static List GetChildNodes(AbstractMaterialNode node) { List nodeList = new List(); var ids = node.GetOutputSlots().Select(x => x.id); foreach (var slot in ids) { foreach (var edge in node.owner.GetEdges(node.FindSlot(slot).slotReference)) { var inputNode = ((Edge)edge).inputSlot.node; if (inputNode != null) { nodeList.Add(inputNode); } } } return nodeList; } private static bool ActiveRootExists(AbstractMaterialNode node) { //if our active state has been explicitly set to a value use it switch (node.activeState) { case AbstractMaterialNode.ActiveState.Implicit: break; case AbstractMaterialNode.ActiveState.ExplicitInactive: return false; case AbstractMaterialNode.ActiveState.ExplicitActive: return true; } List childNodes = GetChildNodes(node); //at this point we know we are not explicitly set to a state, //so there is no reason to be inactive if (childNodes.Count == 0) { return true; } bool output = false; foreach (var child in childNodes) { output |= ActiveRootExists(child); if (output) { break; } } return output; } private static void ActiveTreeExists(AbstractMaterialNode node, out bool activeLeaf, out bool activeRoot, out bool activeTree) { activeLeaf = ActiveLeafExists(node); activeRoot = ActiveRootExists(node); activeTree = activeRoot && activeLeaf; } //First pass check if node is now active after a change, so just check if there is a valid "tree" : a valid upstream input path, // and a valid downstream output path, or "leaf" and "root". If this changes the node's active state, then anything connected may // change as well, so update the "forrest" or all connectected trees of this nodes leaves. // NOTE: I cannot think if there is any case where the entirety of the connected graph would need to change, but if there are bugs // on certain nodes farther away from the node not updating correctly, a possible solution may be to get the entirety of the connected // graph instead of just what I have declared as the "local" connected graph public static void ReevaluateActivityOfConnectedNodes(AbstractMaterialNode node, PooledHashSet changedNodes = null) { var forest = GetForest(node); ReevaluateActivityOfNodeList(forest, changedNodes); } public static void ReevaluateActivityOfNodeList(IEnumerable nodes, PooledHashSet changedNodes = null) { bool getChangedNodes = changedNodes != null; foreach (AbstractMaterialNode n in nodes) { if (n.activeState != AbstractMaterialNode.ActiveState.Implicit) continue; ActiveTreeExists(n, out _, out _, out bool at); if (n.isActive != at && getChangedNodes) { changedNodes.Add(n); } n.SetActive(at, false); } } //Go to the leaves of the node, then get all trees with those leaves private static HashSet GetForest(AbstractMaterialNode node) { var initial = new HashSet { node }; var upstream = new HashSet(); PreviewManager.PropagateNodes(initial, PreviewManager.PropagationDirection.Upstream, upstream); var forest = new HashSet(); PreviewManager.PropagateNodes(upstream, PreviewManager.PropagationDirection.Downstream, forest); return forest; } public static void GetDownsteamNodesForNode(List nodeList, AbstractMaterialNode node) { // no where to start if (node == null) return; // Recursively traverse downstream from the original node // Traverse down each edge and continue on any connected downstream nodes // Only nodes with no nodes further downstream are added to node list bool hasDownstream = false; var ids = node.GetOutputSlots().Select(x => x.id); foreach (var slot in ids) { foreach (var edge in node.owner.GetEdges(node.FindSlot(slot).slotReference)) { var inputNode = ((Edge)edge).inputSlot.node; if (inputNode != null) { hasDownstream = true; GetDownsteamNodesForNode(nodeList, inputNode); } } } // No more nodes downstream from here if (!hasDownstream) nodeList.Add(node); } public static void CollectNodeSet(HashSet nodeSet, MaterialSlot slot) { var node = slot.owner; var graph = node.owner; foreach (var edge in graph.GetEdges(node.GetSlotReference(slot.id))) { var outputNode = edge.outputSlot.node; if (outputNode != null) { CollectNodeSet(nodeSet, outputNode); } } } public static void CollectNodeSet(HashSet nodeSet, AbstractMaterialNode node) { if (!nodeSet.Add(node)) { return; } using (ListPool.Get(out var slots)) { node.GetInputSlots(slots); foreach (var slot in slots) { CollectNodeSet(nodeSet, slot); } } } public static void CollectNodesNodeFeedsInto(List nodeList, AbstractMaterialNode node, IncludeSelf includeSelf = IncludeSelf.Include) { if (node == null) return; if (nodeList.Contains(node)) return; foreach (var slot in node.GetOutputSlots()) { foreach (var edge in node.owner.GetEdges(slot.slotReference)) { var inputNode = edge.inputSlot.node; CollectNodesNodeFeedsInto(nodeList, inputNode); } } if (includeSelf == IncludeSelf.Include) nodeList.Add(node); } public static string GetDocumentationString(string pageName) { return Documentation.GetPageLink(pageName.Replace(" ", "-") + NodeDocSuffix); } static Stack s_SlotStack = new Stack(); public static ShaderStage GetEffectiveShaderStage(MaterialSlot initialSlot, bool goingBackwards) { var graph = initialSlot.owner.owner; s_SlotStack.Clear(); s_SlotStack.Push(initialSlot); while (s_SlotStack.Any()) { var slot = s_SlotStack.Pop(); ShaderStage stage; if (slot.stageCapability.TryGetShaderStage(out stage)) return stage; if (goingBackwards && slot.isInputSlot) { foreach (var edge in graph.GetEdges(slot.slotReference)) { var node = edge.outputSlot.node; s_SlotStack.Push(node.FindOutputSlot(edge.outputSlot.slotId)); } } else if (!goingBackwards && slot.isOutputSlot) { foreach (var edge in graph.GetEdges(slot.slotReference)) { var node = edge.inputSlot.node; s_SlotStack.Push(node.FindInputSlot(edge.inputSlot.slotId)); } } else { var ownerSlots = Enumerable.Empty(); if (goingBackwards && slot.isOutputSlot) ownerSlots = slot.owner.GetInputSlots(slot); else if (!goingBackwards && slot.isInputSlot) ownerSlots = slot.owner.GetOutputSlots(slot); foreach (var ownerSlot in ownerSlots) s_SlotStack.Push(ownerSlot); } } // We default to fragment shader stage if all connected nodes were compatible with both. return ShaderStage.Fragment; } public static ShaderStageCapability GetEffectiveShaderStageCapability(MaterialSlot initialSlot, bool goingBackwards) { var graph = initialSlot.owner.owner; s_SlotStack.Clear(); s_SlotStack.Push(initialSlot); ShaderStageCapability capabilities = ShaderStageCapability.All; while (s_SlotStack.Any()) { var slot = s_SlotStack.Pop(); // Clear any stages from the total capabilities that this slot doesn't support (e.g. if this is vertex, clear pixel) capabilities &= slot.stageCapability; // Can early out if we know nothing is compatible, otherwise we have to keep checking everything we can reach. if (capabilities == ShaderStageCapability.None) return capabilities; if (goingBackwards && slot.isInputSlot) { foreach (var edge in graph.GetEdges(slot.slotReference)) { var node = edge.outputSlot.node; s_SlotStack.Push(node.FindOutputSlot(edge.outputSlot.slotId)); } } else if (!goingBackwards && slot.isOutputSlot) { foreach (var edge in graph.GetEdges(slot.slotReference)) { var node = edge.inputSlot.node; s_SlotStack.Push(node.FindInputSlot(edge.inputSlot.slotId)); } } else { var ownerSlots = Enumerable.Empty(); if (goingBackwards && slot.isOutputSlot) ownerSlots = slot.owner.GetInputSlots(slot); else if (!goingBackwards && slot.isInputSlot) ownerSlots = slot.owner.GetOutputSlots(slot); foreach (var ownerSlot in ownerSlots) s_SlotStack.Push(ownerSlot); } } return capabilities; } public static string GetSlotDimension(ConcreteSlotValueType slotValue) { switch (slotValue) { case ConcreteSlotValueType.Vector1: return String.Empty; case ConcreteSlotValueType.Vector2: return "2"; case ConcreteSlotValueType.Vector3: return "3"; case ConcreteSlotValueType.Vector4: return "4"; case ConcreteSlotValueType.Matrix2: return "2x2"; case ConcreteSlotValueType.Matrix3: return "3x3"; case ConcreteSlotValueType.Matrix4: return "4x4"; case ConcreteSlotValueType.PropertyConnectionState: return String.Empty; default: return "Error"; } } // NOTE: there are several bugs here.. we should use ConvertToValidHLSLIdentifier() instead public static string GetHLSLSafeName(string input) { char[] arr = input.ToCharArray(); arr = Array.FindAll(arr, (c => (Char.IsLetterOrDigit(c)))); var safeName = new string(arr); if (safeName.Length > 1 && char.IsDigit(safeName[0])) { safeName = $"var{safeName}"; } return safeName; } static readonly string[] k_HLSLNumericKeywords = { "float", "half", // not technically in HLSL spec, but prob should be "real", // Unity thing, but included here "int", "uint", "bool", "min10float", "min16float", "min12int", "min16int", "min16uint" }; static readonly string[] k_HLSLNumericKeywordSuffixes = { "", "1", "2", "3", "4", "1x1", "1x2", "1x3", "1x4", "2x1", "2x2", "2x3", "2x4", "3x1", "3x2", "3x3", "3x4", "4x1", "4x2", "4x3", "4x4" }; static HashSet m_ShaderLabKeywords = new HashSet() { // these should all be lowercase, as shaderlab keywords are case insensitive "properties", "range", "bind", "bindchannels", "tags", "lod", "shader", "subshader", "category", "fallback", "dependency", "customeditor", "rect", "any", "float", "color", "int", "integer", "vector", "matrix", "2d", "cube", "3d", "2darray", "cubearray", "name", "settexture", "true", "false", "on", "off", "separatespecular", "offset", "zwrite", "zclip", "conservative", "ztest", "alphatest", "fog", "stencil", "colormask", "alphatomask", "cull", "front", "material", "ambient", "diffuse", "specular", "emission", "shininess", "blend", "blendop", "colormaterial", "lighting", "pass", "grabpass", "usepass", "gpuprogramid", "add", "sub", "revsub", "min", "max", "logicalclear", "logicalset", "logicalcopy", "logicalcopyinverted", "logicalnoop", "logicalinvert", "logicaland", "logicalnand", "logicalor", "logicalnor", "logicalxor", "logicalequiv", "logicalandreverse", "logicalandinverted", "logicalorreverse", "logicalorinverted", "multiply", "screen", "overlay", "darken", "lighten", "colordodge", "colorburn", "hardlight", "softlight", "difference", "exclusion", "hslhue", "hslsaturation", "hslcolor", "hslluminosity", "zero", "one", "dstcolor", "srccolor", "oneminusdstcolor", "srcalpha", "oneminussrccolor", "dstalpha", "oneminusdstalpha", "srcalphasaturate", "oneminussrcalpha", "constantcolor", "oneminusconstantcolor", "constantalpha", "oneminusconstantalpha", }; static HashSet m_HLSLKeywords = new HashSet() { "AppendStructuredBuffer", "asm", "asm_fragment", "auto", "BlendState", "break", "Buffer", "ByteAddressBuffer", "case", "catch", "cbuffer", "centroid", "char", "class", "column_major", "compile", "compile_fragment", "CompileShader", "const", "const_cast", "continue", "ComputeShader", "ConsumeStructuredBuffer", "default", "delete", "DepthStencilState", "DepthStencilView", "discard", "do", "double", "DomainShader", "dynamic_cast", "dword", "else", "enum", "explicit", "export", "extern", "false", "for", "friend", "fxgroup", "GeometryShader", "goto", "groupshared", "half", "Hullshader", "if", "in", "inline", "inout", "InputPatch", "interface", "line", "lineadj", "linear", "LineStream", "long", "matrix", "mutable", "namespace", "new", "nointerpolation", "noperspective", "NULL", "operator", "out", "OutputPatch", "packoffset", "pass", "pixelfragment", "PixelShader", "point", "PointStream", "precise", "private", "protected", "public", "RasterizerState", "reinterpret_cast", "RenderTargetView", "return", "register", "row_major", "RWBuffer", "RWByteAddressBuffer", "RWStructuredBuffer", "RWTexture1D", "RWTexture1DArray", "RWTexture2D", "RWTexture2DArray", "RWTexture3D", "sample", "sampler", "SamplerState", "SamplerComparisonState", "shared", "short", "signed", "sizeof", "snorm", "stateblock", "stateblock_state", "static", "static_cast", "string", "struct", "switch", "StructuredBuffer", "tbuffer", "technique", "technique10", "technique11", "template", "texture", "Texture1D", "Texture1DArray", "Texture2D", "Texture2DArray", "Texture2DMS", "Texture2DMSArray", "Texture3D", "TextureCube", "TextureCubeArray", "this", "throw", "true", "try", "typedef", "typename", "triangle", "triangleadj", "TriangleStream", "uniform", "unorm", "union", "unsigned", "using", "vector", "vertexfragment", "VertexShader", "virtual", "void", "volatile", "while" }; static HashSet m_ShaderGraphKeywords = new HashSet() { "Gradient", "UnitySamplerState", "UnityTexture2D", "UnityTexture2DArray", "UnityTexture3D", "UnityTextureCube" }; static bool m_HLSLKeywordDictionaryBuilt = false; public static bool IsHLSLKeyword(string id) { if (!m_HLSLKeywordDictionaryBuilt) { foreach (var numericKeyword in k_HLSLNumericKeywords) foreach (var suffix in k_HLSLNumericKeywordSuffixes) m_HLSLKeywords.Add(numericKeyword + suffix); m_HLSLKeywordDictionaryBuilt = true; } bool isHLSLKeyword = m_HLSLKeywords.Contains(id); return isHLSLKeyword; } public static bool IsShaderLabKeyWord(string id) { bool isShaderLabKeyword = m_ShaderLabKeywords.Contains(id.ToLower()); return isShaderLabKeyword; } public static bool IsShaderGraphKeyWord(string id) { bool isShaderGraphKeyword = m_ShaderGraphKeywords.Contains(id); return isShaderGraphKeyword; } public static string ConvertToValidHLSLIdentifier(string originalId, Func isDisallowedIdentifier = null) { // Converts " 1 var * q-30 ( 0 ) (1) " to "_1_var_q_30_0_1" if (originalId == null) originalId = ""; var result = Regex.Replace(originalId, @"^[^A-Za-z0-9_]+|[^A-Za-z0-9_]+$", ""); // trim leading/trailing bad characters (excl '_'). result = Regex.Replace(result, @"[^A-Za-z0-9]+", "_"); // replace sequences of bad characters with underscores (incl '_'). if (result.Length == 0 || Char.IsDigit(result[0]) || IsHLSLKeyword(result) || (isDisallowedIdentifier?.Invoke(result) ?? false)) result = "_" + result; return result; } private static string GetDisplaySafeName(string input) { //strip valid display characters from slot name //current valid characters are whitespace and ( ) _ separators StringBuilder cleanName = new StringBuilder(); foreach (var c in input) { if (c != ' ' && c != '(' && c != ')' && c != '_') cleanName.Append(c); } return cleanName.ToString(); } public static bool ValidateSlotName(string inName, out string errorMessage) { //check for invalid characters between display safe and hlsl safe name if (GetDisplaySafeName(inName) != GetHLSLSafeName(inName) && GetDisplaySafeName(inName) != ConvertToValidHLSLIdentifier(inName)) { errorMessage = "Slot name(s) found invalid character(s). Valid characters: A-Z, a-z, 0-9, _ ( ) "; return true; } //if clean, return null and false errorMessage = null; return false; } public static string FloatToShaderValue(float value) { if (Single.IsPositiveInfinity(value)) { return "1.#INF"; } if (Single.IsNegativeInfinity(value)) { return "-1.#INF"; } if (Single.IsNaN(value)) { return "NAN"; } return value.ToString(CultureInfo.InvariantCulture); } // A number large enough to become Infinity (~FLOAT_MAX_VALUE * 10) + explanatory comment private const string k_ShaderLabInfinityAlternatrive = "3402823500000000000000000000000000000000 /* Infinity */"; // ShaderLab doesn't support Scientific Notion nor Infinity. To stop from generating a broken shader we do this. public static string FloatToShaderValueShaderLabSafe(float value) { if (Single.IsPositiveInfinity(value)) { return k_ShaderLabInfinityAlternatrive; } if (Single.IsNegativeInfinity(value)) { return "-" + k_ShaderLabInfinityAlternatrive; } if (Single.IsNaN(value)) { return "NAN"; // A real error has occured, in this case we should break the shader. } // For single point precision, reserve 54 spaces (e-45 min + ~9 digit precision). See floating-point-numeric-types (Microsoft docs). return value.ToString("0.######################################################", CultureInfo.InvariantCulture); } } }