123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- using System;
- using System.Collections.Generic;
- using System.Diagnostics.CodeAnalysis;
- using System.IO;
- using System.Linq;
- using System.Text;
- #if UNITY_2020_2_OR_NEWER
- using UnityEditor.AssetImporters;
- #else
- using UnityEditor.Experimental.AssetImporters;
- #endif
- using UnityEngine;
- using UnityEditor.Graphing;
- using UnityEditor.Graphing.Util;
- using UnityEditor.ShaderGraph.Internal;
- using UnityEditor.ShaderGraph.Serialization;
- using UnityEngine.Pool;
-
- namespace UnityEditor.ShaderGraph
- {
- [ExcludeFromPreset]
- [ScriptedImporter(30, Extension, -905)]
- class ShaderSubGraphImporter : ScriptedImporter
- {
- public const string Extension = "shadersubgraph";
-
- [SuppressMessage("ReSharper", "UnusedMember.Local")]
- static string[] GatherDependenciesFromSourceFile(string assetPath)
- {
- try
- {
- AssetCollection assetCollection = new AssetCollection();
- MinimalGraphData.GatherMinimalDependenciesFromFile(assetPath, assetCollection);
-
- List<string> dependencyPaths = new List<string>();
- foreach (var asset in assetCollection.assets)
- {
- // only artifact dependencies need to be declared in GatherDependenciesFromSourceFile
- // to force their imports to run before ours
- if (asset.Value.HasFlag(AssetCollection.Flags.ArtifactDependency))
- {
- var dependencyPath = AssetDatabase.GUIDToAssetPath(asset.Key);
-
- // it is unfortunate that we can't declare these dependencies unless they have a path...
- // I asked AssetDatabase team for GatherDependenciesFromSourceFileByGUID()
- if (!string.IsNullOrEmpty(dependencyPath))
- dependencyPaths.Add(dependencyPath);
- }
- }
- return dependencyPaths.ToArray();
- }
- catch (Exception e)
- {
- Debug.LogException(e);
- return new string[0];
- }
- }
-
- static bool NodeWasUsedByGraph(string nodeId, GraphData graphData)
- {
- var node = graphData.GetNodeFromId(nodeId);
- return node?.wasUsedByGenerator ?? false;
- }
-
- public override void OnImportAsset(AssetImportContext ctx)
- {
- var importLog = new ShaderGraphImporter.AssetImportErrorLog(ctx);
-
- var graphAsset = ScriptableObject.CreateInstance<SubGraphAsset>();
- var subGraphPath = ctx.assetPath;
- var subGraphGuid = AssetDatabase.AssetPathToGUID(subGraphPath);
- graphAsset.assetGuid = subGraphGuid;
- var textGraph = File.ReadAllText(subGraphPath, Encoding.UTF8);
- var messageManager = new MessageManager();
- var graphData = new GraphData
- {
- isSubGraph = true,
- assetGuid = subGraphGuid,
- messageManager = messageManager
- };
- MultiJson.Deserialize(graphData, textGraph);
-
- try
- {
- ProcessSubGraph(graphAsset, graphData, importLog);
- }
- catch (Exception e)
- {
- graphAsset.isValid = false;
- Debug.LogException(e, graphAsset);
- }
- finally
- {
- var errors = messageManager.ErrorStrings((nodeId) => NodeWasUsedByGraph(nodeId, graphData));
- int errCount = errors.Count();
- if (errCount > 0)
- {
- var firstError = errors.FirstOrDefault();
- importLog.LogError($"Sub Graph at {subGraphPath} has {errCount} error(s), the first is: {firstError}", graphAsset);
- graphAsset.isValid = false;
- }
- else
- {
- var warnings = messageManager.ErrorStrings((nodeId) => NodeWasUsedByGraph(nodeId, graphData), Rendering.ShaderCompilerMessageSeverity.Warning);
- int warningCount = warnings.Count();
- if (warningCount > 0)
- {
- var firstWarning = warnings.FirstOrDefault();
- importLog.LogWarning($"Sub Graph at {subGraphPath} has {warningCount} warning(s), the first is: {firstWarning}", graphAsset);
- }
- }
- messageManager.ClearAll();
- }
-
- Texture2D texture = Resources.Load<Texture2D>("Icons/sg_subgraph_icon");
- ctx.AddObjectToAsset("MainAsset", graphAsset, texture);
- ctx.SetMainObject(graphAsset);
-
- var metadata = ScriptableObject.CreateInstance<ShaderSubGraphMetadata>();
- metadata.hideFlags = HideFlags.HideInHierarchy;
- metadata.assetDependencies = new List<UnityEngine.Object>();
-
- AssetCollection assetCollection = new AssetCollection();
- MinimalGraphData.GatherMinimalDependenciesFromFile(assetPath, assetCollection);
-
- foreach (var asset in assetCollection.assets)
- {
- if (asset.Value.HasFlag(AssetCollection.Flags.IncludeInExportPackage))
- {
- // this sucks that we have to fully load these assets just to set the reference,
- // which then gets serialized as the GUID that we already have here. :P
-
- var dependencyPath = AssetDatabase.GUIDToAssetPath(asset.Key);
- if (!string.IsNullOrEmpty(dependencyPath))
- {
- metadata.assetDependencies.Add(
- AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(UnityEngine.Object)));
- }
- }
- }
- ctx.AddObjectToAsset("Metadata", metadata);
-
- // declare dependencies
- foreach (var asset in assetCollection.assets)
- {
- if (asset.Value.HasFlag(AssetCollection.Flags.SourceDependency))
- {
- ctx.DependsOnSourceAsset(asset.Key);
-
- // I'm not sure if this warning below is actually used or not, keeping it to be safe
- var assetPath = AssetDatabase.GUIDToAssetPath(asset.Key);
-
- // Ensure that dependency path is relative to project
- if (!string.IsNullOrEmpty(assetPath) && !assetPath.StartsWith("Packages/") && !assetPath.StartsWith("Assets/"))
- {
- importLog.LogWarning($"Invalid dependency path: {assetPath}", graphAsset);
- }
- }
-
- // NOTE: dependencies declared by GatherDependenciesFromSourceFile are automatically registered as artifact dependencies
- // HOWEVER: that path ONLY grabs dependencies via MinimalGraphData, and will fail to register dependencies
- // on GUIDs that don't exist in the project. For both of those reasons, we re-declare the dependencies here.
- if (asset.Value.HasFlag(AssetCollection.Flags.ArtifactDependency))
- {
- ctx.DependsOnArtifact(asset.Key);
- }
- }
- }
-
- static void ProcessSubGraph(SubGraphAsset asset, GraphData graph, ShaderGraphImporter.AssetImportErrorLog importLog)
- {
- var graphIncludes = new IncludeCollection();
- var registry = new FunctionRegistry(new ShaderStringBuilder(), graphIncludes, true);
-
- asset.functions.Clear();
- asset.isValid = true;
-
- graph.OnEnable();
- graph.messageManager.ClearAll();
- graph.ValidateGraph();
-
- var assetPath = AssetDatabase.GUIDToAssetPath(asset.assetGuid);
- asset.hlslName = NodeUtils.GetHLSLSafeName(Path.GetFileNameWithoutExtension(assetPath));
- asset.inputStructName = $"Bindings_{asset.hlslName}_{asset.assetGuid}_$precision";
- asset.functionName = $"SG_{asset.hlslName}_{asset.assetGuid}_$precision";
- asset.path = graph.path;
-
- var outputNode = graph.outputNode;
-
- var outputSlots = PooledList<MaterialSlot>.Get();
- outputNode.GetInputSlots(outputSlots);
-
- List<AbstractMaterialNode> nodes = new List<AbstractMaterialNode>();
- NodeUtils.DepthFirstCollectNodesFromNode(nodes, outputNode);
-
- // flag the used nodes so we can filter out errors from unused nodes
- foreach (var node in nodes)
- node.SetUsedByGenerator();
-
- // Start with a clean slate for the input/output capabilities and dependencies
- asset.inputCapabilities.Clear();
- asset.outputCapabilities.Clear();
- asset.slotDependencies.Clear();
-
- ShaderStageCapability effectiveShaderStage = ShaderStageCapability.All;
- var shaderStageCapabilityCache = new Dictionary<SlotReference, ShaderStageCapability>();
- foreach (var slot in outputSlots)
- {
- var stage = NodeUtils.GetEffectiveShaderStageCapability(slot, true, shaderStageCapabilityCache);
- if (effectiveShaderStage == ShaderStageCapability.All && stage != ShaderStageCapability.All)
- effectiveShaderStage = stage;
-
- asset.outputCapabilities.Add(new SlotCapability { slotName = slot.RawDisplayName(), capabilities = stage });
-
- // Find all unique property nodes used by this slot and record a dependency for this input/output pair
- var inputPropertyNames = new HashSet<string>();
- var nodeSet = new HashSet<AbstractMaterialNode>();
- NodeUtils.CollectNodeSet(nodeSet, slot);
- foreach (var node in nodeSet)
- {
- if (node is PropertyNode propNode && !inputPropertyNames.Contains(propNode.property.displayName))
- {
- inputPropertyNames.Add(propNode.property.displayName);
- var slotDependency = new SlotDependencyPair();
- slotDependency.inputSlotName = propNode.property.displayName;
- slotDependency.outputSlotName = slot.RawDisplayName();
- asset.slotDependencies.Add(slotDependency);
- }
- }
- }
- CollectInputCapabilities(asset, graph);
-
- asset.vtFeedbackVariables = VirtualTexturingFeedbackUtils.GetFeedbackVariables(outputNode as SubGraphOutputNode);
- asset.requirements = ShaderGraphRequirements.FromNodes(nodes, effectiveShaderStage, false);
-
- // output precision is whatever the output node has as a graph precision, falling back to the graph default
- asset.outputGraphPrecision = outputNode.graphPrecision.GraphFallback(graph.graphDefaultPrecision);
-
- // this saves the graph precision, which indicates whether this subgraph is switchable or not
- asset.subGraphGraphPrecision = graph.graphDefaultPrecision;
-
- asset.previewMode = graph.previewMode;
-
- asset.includes = graphIncludes;
-
- GatherDescendentsFromGraph(new GUID(asset.assetGuid), out var containsCircularDependency, out var descendents);
- asset.descendents.AddRange(descendents.Select(g => g.ToString()));
- asset.descendents.Sort(); // ensure deterministic order
-
- var childrenSet = new HashSet<string>();
- var anyErrors = false;
- foreach (var node in nodes)
- {
- if (node is SubGraphNode subGraphNode)
- {
- var subGraphGuid = subGraphNode.subGraphGuid;
- childrenSet.Add(subGraphGuid);
- }
-
- if (node.hasError)
- {
- anyErrors = true;
- }
- asset.children = childrenSet.ToList();
- asset.children.Sort(); // ensure deterministic order
- }
-
- if (!anyErrors && containsCircularDependency)
- {
- importLog.LogError($"Error in Graph at {assetPath}: Sub Graph contains a circular dependency.", asset);
- anyErrors = true;
- }
-
- if (anyErrors)
- {
- asset.isValid = false;
- registry.ProvideFunction(asset.functionName, sb => { });
- return;
- }
-
- foreach (var node in nodes)
- {
- if (node is IGeneratesFunction generatesFunction)
- {
- registry.builder.currentNode = node;
- generatesFunction.GenerateNodeFunction(registry, GenerationMode.ForReals);
- }
- }
-
- // Need to order the properties so that they are in the same order on a subgraph node in a shadergraph
- // as they are in the blackboard for the subgraph itself. The (blackboard) categories keep that ordering,
- // so traverse those and add those items to the ordered properties list. Needs to be used to set up the
- // function _and_ to write out the final asset data so that the function call parameter order matches as well.
- var orderedProperties = new List<AbstractShaderProperty>();
- var propertiesList = graph.properties.ToList();
- foreach (var category in graph.categories)
- {
- foreach (var child in category.Children)
- {
- var prop = propertiesList.Find(p => p.guid == child.guid);
- // Not all properties in the category are actually on the graph.
- // In particular, it seems as if keywords are not properties on sub-graphs.
- if (prop != null && !orderedProperties.Contains(prop))
- orderedProperties.Add(prop);
- }
- }
-
- // If we are importing an older file that has not had categories generated for it yet, include those now.
- orderedProperties.AddRange(graph.properties.Except(orderedProperties));
-
- // provide top level subgraph function
- // NOTE: actual concrete precision here shouldn't matter, it's irrelevant when building the subgraph asset
- registry.ProvideFunction(asset.functionName, asset.subGraphGraphPrecision, ConcretePrecision.Single, sb =>
- {
- GenerationUtils.GenerateSurfaceInputStruct(sb, asset.requirements, asset.inputStructName);
- sb.AppendNewLine();
-
- // Generate the arguments... first INPUTS
- var arguments = new List<string>();
- foreach (var prop in orderedProperties)
- {
- // apply fallback to the graph default precision (but don't convert to concrete)
- // this means "graph switchable" properties will use the precision token
- GraphPrecision propGraphPrecision = prop.precision.ToGraphPrecision(graph.graphDefaultPrecision);
- string precisionString = propGraphPrecision.ToGenericString();
- arguments.Add(prop.GetPropertyAsArgumentString(precisionString));
- if (prop.isConnectionTestable)
- {
- arguments.Add($"bool {prop.GetConnectionStateHLSLVariableName()}");
- }
- }
-
- {
- var dropdowns = graph.dropdowns;
- foreach (var dropdown in dropdowns)
- arguments.Add($"int {dropdown.referenceName}");
- }
-
- // now pass surface inputs
- arguments.Add(string.Format("{0} IN", asset.inputStructName));
-
- // Now generate output arguments
- foreach (MaterialSlot output in outputSlots)
- arguments.Add($"out {output.concreteValueType.ToShaderString(asset.outputGraphPrecision.ToGenericString())} {output.shaderOutputName}_{output.id}");
-
- // Vt Feedback output arguments (always full float4)
- foreach (var output in asset.vtFeedbackVariables)
- arguments.Add($"out {ConcreteSlotValueType.Vector4.ToShaderString(ConcretePrecision.Single)} {output}_out");
-
- // Create the function prototype from the arguments
- sb.AppendLine("void {0}({1})"
- , asset.functionName
- , arguments.Aggregate((current, next) => $"{current}, {next}"));
-
- // now generate the function
- using (sb.BlockScope())
- {
- // Just grab the body from the active nodes
- foreach (var node in nodes)
- {
- if (node is IGeneratesBodyCode generatesBodyCode)
- {
- sb.currentNode = node;
- generatesBodyCode.GenerateNodeCode(sb, GenerationMode.ForReals);
-
- if (node.graphPrecision == GraphPrecision.Graph)
- {
- // code generated by nodes that use graph precision stays in generic form with embedded tokens
- // those tokens are replaced when this subgraph function is pulled into a graph that defines the precision
- }
- else
- {
- sb.ReplaceInCurrentMapping(PrecisionUtil.Token, node.concretePrecision.ToShaderString());
- }
- }
- }
-
- foreach (var slot in outputSlots)
- {
- sb.AppendLine($"{slot.shaderOutputName}_{slot.id} = {outputNode.GetSlotValue(slot.id, GenerationMode.ForReals)};");
- }
-
- foreach (var slot in asset.vtFeedbackVariables)
- {
- sb.AppendLine($"{slot}_out = {slot};");
- }
- }
- });
-
- // save all of the node-declared functions to the subgraph asset
- foreach (var name in registry.names)
- {
- var source = registry.sources[name];
- var func = new FunctionPair(name, source.code, source.graphPrecisionFlags);
- asset.functions.Add(func);
- }
-
- var collector = new PropertyCollector();
- foreach (var node in nodes)
- {
- int previousPropertyCount = Math.Max(0, collector.propertyCount - 1);
-
- node.CollectShaderProperties(collector, GenerationMode.ForReals);
-
- // This is a stop-gap to prevent the autogenerated values from JsonObject and ShaderInput from
- // resulting in non-deterministic import data. While we should move to local ids in the future,
- // this will prevent cascading shader recompilations.
- for (int i = previousPropertyCount; i < collector.propertyCount; ++i)
- {
- var prop = collector.GetProperty(i);
- var namespaceId = node.objectId;
- var nameId = prop.referenceName;
-
- prop.OverrideObjectId(namespaceId, nameId + "_ObjectId_" + i);
- prop.OverrideGuid(namespaceId, nameId + "_Guid_" + i);
- }
- }
-
- asset.WriteData(orderedProperties, graph.keywords, graph.dropdowns, collector.properties, outputSlots, graph.unsupportedTargets);
- outputSlots.Dispose();
- }
-
- static void GatherDescendentsFromGraph(GUID rootAssetGuid, out bool containsCircularDependency, out HashSet<GUID> descendentGuids)
- {
- var dependencyMap = new Dictionary<GUID, GUID[]>();
- AssetCollection tempAssetCollection = new AssetCollection();
- using (ListPool<GUID>.Get(out var tempList))
- {
- GatherDependencyMap(rootAssetGuid, dependencyMap, tempAssetCollection);
- containsCircularDependency = ContainsCircularDependency(rootAssetGuid, dependencyMap, tempList);
- }
-
- descendentGuids = new HashSet<GUID>();
- GatherDescendentsUsingDependencyMap(rootAssetGuid, descendentGuids, dependencyMap);
- }
-
- static void GatherDependencyMap(GUID rootAssetGUID, Dictionary<GUID, GUID[]> dependencyMap, AssetCollection tempAssetCollection)
- {
- if (!dependencyMap.ContainsKey(rootAssetGUID))
- {
- // if it is a subgraph, try to recurse into it
- var assetPath = AssetDatabase.GUIDToAssetPath(rootAssetGUID);
- if (!string.IsNullOrEmpty(assetPath) && assetPath.EndsWith(Extension, true, null))
- {
- tempAssetCollection.Clear();
- MinimalGraphData.GatherMinimalDependenciesFromFile(assetPath, tempAssetCollection);
-
- var subgraphGUIDs = tempAssetCollection.assets.Where(asset => asset.Value.HasFlag(AssetCollection.Flags.IsSubGraph)).Select(asset => asset.Key).ToArray();
- dependencyMap[rootAssetGUID] = subgraphGUIDs;
-
- foreach (var guid in subgraphGUIDs)
- {
- GatherDependencyMap(guid, dependencyMap, tempAssetCollection);
- }
- }
- }
- }
-
- static void GatherDescendentsUsingDependencyMap(GUID rootAssetGUID, HashSet<GUID> descendentGuids, Dictionary<GUID, GUID[]> dependencyMap)
- {
- var dependencies = dependencyMap[rootAssetGUID];
- foreach (GUID dependency in dependencies)
- {
- if (descendentGuids.Add(dependency))
- {
- GatherDescendentsUsingDependencyMap(dependency, descendentGuids, dependencyMap);
- }
- }
- }
-
- static bool ContainsCircularDependency(GUID assetGUID, Dictionary<GUID, GUID[]> dependencyMap, List<GUID> ancestors)
- {
- if (ancestors.Contains(assetGUID))
- {
- return true;
- }
-
- ancestors.Add(assetGUID);
- foreach (var dependencyGUID in dependencyMap[assetGUID])
- {
- if (ContainsCircularDependency(dependencyGUID, dependencyMap, ancestors))
- {
- return true;
- }
- }
- ancestors.RemoveAt(ancestors.Count - 1);
-
- return false;
- }
-
- static void CollectInputCapabilities(SubGraphAsset asset, GraphData graph)
- {
- // Collect each input's capabilities. There can be multiple property nodes
- // contributing to the same input, so we cache these in a map while building
- var inputCapabilities = new Dictionary<string, SlotCapability>();
-
- var shaderStageCapabilityCache = new Dictionary<SlotReference, ShaderStageCapability>();
-
- // Walk all property node output slots, computing and caching the capabilities for that slot
- var propertyNodes = graph.GetNodes<PropertyNode>();
- foreach (var propertyNode in propertyNodes)
- {
- foreach (var slot in propertyNode.GetOutputSlots<MaterialSlot>())
- {
- var slotName = slot.RawDisplayName();
- SlotCapability capabilityInfo;
- if (!inputCapabilities.TryGetValue(slotName, out capabilityInfo))
- {
- capabilityInfo = new SlotCapability();
- capabilityInfo.slotName = slotName;
- inputCapabilities.Add(propertyNode.property.displayName, capabilityInfo);
- }
- capabilityInfo.capabilities &= NodeUtils.GetEffectiveShaderStageCapability(slot, false, shaderStageCapabilityCache);
- }
- }
- asset.inputCapabilities.AddRange(inputCapabilities.Values);
- }
- }
- }
|