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(22, 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 dependencyPaths = new List(); 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]; } } public override void OnImportAsset(AssetImportContext ctx) { var graphAsset = ScriptableObject.CreateInstance(); 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); } catch (Exception e) { graphAsset.isValid = false; Debug.LogException(e, graphAsset); } finally { if (messageManager.AnyError()) { graphAsset.isValid = false; foreach (var pair in messageManager.GetNodeMessages()) { var node = graphData.GetNodeFromId(pair.Key); foreach (var message in pair.Value) { MessageManager.Log(node, subGraphPath, message, graphAsset); } } } messageManager.ClearAll(); } Texture2D texture = Resources.Load("Icons/sg_subgraph_icon"); ctx.AddObjectToAsset("MainAsset", graphAsset, texture); ctx.SetMainObject(graphAsset); var metadata = ScriptableObject.CreateInstance(); metadata.hideFlags = HideFlags.HideInHierarchy; metadata.assetDependencies = new List(); 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/")) { Debug.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) { var registry = new FunctionRegistry(new ShaderStringBuilder(), true); registry.names.Clear(); 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}"; asset.functionName = $"SG_{asset.hlslName}_{asset.assetGuid}"; asset.path = graph.path; var outputNode = graph.outputNode; var outputSlots = PooledList.Get(); outputNode.GetInputSlots(outputSlots); List nodes = new List(); NodeUtils.DepthFirstCollectNodesFromNode(nodes, outputNode); asset.effectiveShaderStage = ShaderStageCapability.All; foreach (var slot in outputSlots) { var stage = NodeUtils.GetEffectiveShaderStageCapability(slot, true); if (stage != ShaderStageCapability.All) { asset.effectiveShaderStage = stage; break; } } asset.vtFeedbackVariables = VirtualTexturingFeedbackUtils.GetFeedbackVariables(outputNode as SubGraphOutputNode); asset.requirements = ShaderGraphRequirements.FromNodes(nodes, asset.effectiveShaderStage, false); asset.graphPrecision = graph.concretePrecision; asset.outputPrecision = outputNode.concretePrecision; asset.previewMode = graph.previewMode; 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(); 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) { Debug.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); registry.builder.ReplaceInCurrentMapping(PrecisionUtil.Token, node.concretePrecision.ToShaderString()); } } // provide top level subgraph function registry.ProvideFunction(asset.functionName, sb => { GenerationUtils.GenerateSurfaceInputStruct(sb, asset.requirements, asset.inputStructName); sb.AppendNewLine(); // Generate arguments... first INPUTS var arguments = new List(); foreach (var prop in graph.properties) { prop.ValidateConcretePrecision(asset.graphPrecision); arguments.Add(prop.GetPropertyAsArgumentString()); } // now pass surface inputs arguments.Add(string.Format("{0} IN", asset.inputStructName)); // Now generate outputs foreach (MaterialSlot output in outputSlots) arguments.Add($"out {output.concreteValueType.ToShaderString(asset.outputPrecision)} {output.shaderOutputName}_{output.id}"); // Vt Feedback arguments 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); sb.ReplaceInCurrentMapping(PrecisionUtil.Token, node.concretePrecision.ToShaderString()); } } foreach (var slot in outputSlots) { sb.AppendLine($"{slot.shaderOutputName}_{slot.id} = {outputNode.GetSlotValue(slot.id, GenerationMode.ForReals, asset.outputPrecision)};"); } foreach (var slot in asset.vtFeedbackVariables) { sb.AppendLine($"{slot}_out = {slot};"); } } }); asset.functions.AddRange(registry.names.Select(x => new FunctionPair(x, registry.sources[x].code))); 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(graph.properties, graph.keywords, collector.properties, outputSlots, graph.unsupportedTargets); outputSlots.Dispose(); } static void GatherDescendentsFromGraph(GUID rootAssetGuid, out bool containsCircularDependency, out HashSet descendentGuids) { var dependencyMap = new Dictionary(); AssetCollection tempAssetCollection = new AssetCollection(); using (ListPool.Get(out var tempList)) { GatherDependencyMap(rootAssetGuid, dependencyMap, tempAssetCollection); containsCircularDependency = ContainsCircularDependency(rootAssetGuid, dependencyMap, tempList); } descendentGuids = new HashSet(); GatherDescendentsUsingDependencyMap(rootAssetGuid, descendentGuids, dependencyMap); } static void GatherDependencyMap(GUID rootAssetGUID, Dictionary 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 descendentGuids, Dictionary dependencyMap) { var dependencies = dependencyMap[rootAssetGUID]; foreach (GUID dependency in dependencies) { if (descendentGuids.Add(dependency)) { GatherDescendentsUsingDependencyMap(dependency, descendentGuids, dependencyMap); } } } static bool ContainsCircularDependency(GUID assetGUID, Dictionary dependencyMap, List 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; } } }