using UnityEngine; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using UnityEditor.AssetImporters; using UnityEditor.Graphing; using UnityEditor.Graphing.Util; using UnityEditor.ShaderGraph.Internal; using UnityEditor.ShaderGraph.Serialization; using Object = System.Object; namespace UnityEditor.ShaderGraph { [ExcludeFromPreset] [ScriptedImporter(130, Extension, -902)] class ShaderGraphImporter : ScriptedImporter { public const string Extension = "shadergraph"; public const string LegacyExtension = "ShaderGraph"; public const string k_ErrorShader = @" Shader ""Hidden/GraphErrorShader2"" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile _ UNITY_SINGLE_PASS_STEREO STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON #include ""UnityCG.cginc"" struct appdata_t { float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; UNITY_VERTEX_OUTPUT_STEREO }; v2f vert (appdata_t v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return fixed4(1,0,1,1); } ENDCG } } Fallback Off }"; [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]; } } Shader BuildAllShaders( AssetImportContext importContext, AssetImportErrorLog importErrorLog, AssetCollection allImportAssetDependencies, GraphData graph) { Shader primaryShader = null; string path = importContext.assetPath; var primaryShaderName = Path.GetFileNameWithoutExtension(path); try { // this will also add Target dependencies into the asset collection Generator generator; generator = new Generator(graph, graph.outputNode, GenerationMode.ForReals, primaryShaderName, assetCollection: allImportAssetDependencies); bool first = true; foreach (var generatedShader in generator.allGeneratedShaders) { var shaderString = generatedShader.codeString; // we only care if an error was reported for a node that we actually used if (graph.messageManager.AnyError((nodeId) => NodeWasUsedByGraph(nodeId, graph)) || shaderString == null) { shaderString = k_ErrorShader.Replace("Hidden/GraphErrorShader2", generatedShader.shaderName); } var shader = ShaderUtil.CreateShaderAsset(importContext, shaderString, false); ReportErrors(graph, shader, path, importErrorLog); EditorMaterialUtility.SetShaderDefaults( shader, generatedShader.assignedTextures.Where(x => x.modifiable).Select(x => x.name).ToArray(), generatedShader.assignedTextures.Where(x => x.modifiable).Select(x => EditorUtility.InstanceIDToObject(x.textureId) as Texture).ToArray()); EditorMaterialUtility.SetShaderNonModifiableDefaults( shader, generatedShader.assignedTextures.Where(x => !x.modifiable).Select(x => x.name).ToArray(), generatedShader.assignedTextures.Where(x => !x.modifiable).Select(x => EditorUtility.InstanceIDToObject(x.textureId) as Texture).ToArray()); if (first) { // first shader is always the primary shader // we return the primary shader so it can be attached to the import context at the outer level // allowing it to bind a custom icon as well primaryShader = shader; // only the main shader gets a material created Material material = new Material(shader) { name = primaryShaderName }; importContext.AddObjectToAsset("Material", material); first = false; } else { importContext.AddObjectToAsset($"Shader-{generatedShader.shaderName}", shader); } } foreach (var generatedComputeShader in generator.allGeneratedComputeShaders) { // Create the compute asset. var computeShader = ShaderUtil.CreateComputeShaderAsset(importContext, generatedComputeShader.codeString); // TODO: ReportErrors for Compute Shader. Will require ShaderUtil.GetComputeShaderMessages. computeShader.name = $"ComputeShader-{generatedComputeShader.shaderName}"; importContext.AddObjectToAsset(computeShader.name, computeShader); } } catch (Exception e) { Debug.LogException(e); // ignored } return primaryShader; } internal static bool subtargetNotFoundError = false; public override void OnImportAsset(AssetImportContext ctx) { var importLog = new AssetImportErrorLog(ctx); string path = ctx.assetPath; AssetCollection assetCollection = new AssetCollection(); MinimalGraphData.GatherMinimalDependenciesFromFile(assetPath, assetCollection); var textGraph = File.ReadAllText(path, Encoding.UTF8); var graph = new GraphData { messageManager = new MessageManager(), assetGuid = AssetDatabase.AssetPathToGUID(path) }; MultiJson.Deserialize(graph, textGraph); if (subtargetNotFoundError) { Debug.LogError($"{ctx.assetPath}: Import Error: Expected active subtarget not found, defaulting to first available."); subtargetNotFoundError = false; } graph.OnEnable(); graph.ValidateGraph(); UnityEngine.Object mainObject = null; #if VFX_GRAPH_10_0_0_OR_NEWER if (!graph.isOnlyVFXTarget) #endif { // build shaders mainObject = BuildAllShaders(ctx, importLog, assetCollection, graph); } #if VFX_GRAPH_10_0_0_OR_NEWER ShaderGraphVfxAsset vfxAsset = null; if (graph.hasVFXTarget) { vfxAsset = GenerateVfxShaderGraphAsset(graph); if (mainObject == null) { mainObject = vfxAsset; } else { //Correct main object if we have a shader and ShaderGraphVfxAsset : save as sub asset vfxAsset.name = Path.GetFileNameWithoutExtension(path); ctx.AddObjectToAsset("VFXShaderGraph", vfxAsset); } } #endif Texture2D texture = Resources.Load("Icons/sg_graph_icon"); ctx.AddObjectToAsset("MainAsset", mainObject, texture); ctx.SetMainObject(mainObject); var graphDataReadOnly = new GraphDataReadOnly(graph); foreach (var target in graph.activeTargets) { if (target is IHasMetadata iHasMetadata) { var metadata = iHasMetadata.GetMetadataObject(graphDataReadOnly); if (metadata == null) continue; metadata.hideFlags = HideFlags.HideInHierarchy; ctx.AddObjectToAsset($"{iHasMetadata.identifier}:Metadata", metadata); } } var sgMetadata = ScriptableObject.CreateInstance(); sgMetadata.hideFlags = HideFlags.HideInHierarchy; sgMetadata.assetDependencies = new List(); 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)) { sgMetadata.assetDependencies.Add( AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(UnityEngine.Object))); } } } List inputInspectorDataList = new List(); foreach (AbstractShaderProperty property in graph.properties) { // Don't write out data for non-exposed blackboard items if (!property.isExposed) continue; // VTs are treated differently if (property is VirtualTextureShaderProperty virtualTextureShaderProperty) inputInspectorDataList.Add(MinimalCategoryData.ProcessVirtualTextureProperty(virtualTextureShaderProperty)); else inputInspectorDataList.Add(new GraphInputData() { referenceName = property.referenceName, propertyType = property.propertyType, isKeyword = false }); } foreach (ShaderKeyword keyword in graph.keywords) { // Don't write out data for non-exposed blackboard items if (!keyword.isExposed) continue; var sanitizedReferenceName = keyword.referenceName; if (keyword.keywordType == KeywordType.Boolean && keyword.referenceName.Contains("_ON")) sanitizedReferenceName = sanitizedReferenceName.Replace("_ON", String.Empty); inputInspectorDataList.Add(new GraphInputData() { referenceName = sanitizedReferenceName, keywordType = keyword.keywordType, isKeyword = true }); } sgMetadata.categoryDatas = new List(); foreach (CategoryData categoryData in graph.categories) { // Don't write out empty categories if (categoryData.childCount == 0) continue; MinimalCategoryData mcd = new MinimalCategoryData() { categoryName = categoryData.name, propertyDatas = new List() }; foreach (var input in categoryData.Children) { GraphInputData propData; // Only write out data for exposed blackboard items if (input.isExposed == false) continue; // VTs are treated differently if (input is VirtualTextureShaderProperty virtualTextureShaderProperty) { propData = MinimalCategoryData.ProcessVirtualTextureProperty(virtualTextureShaderProperty); inputInspectorDataList.RemoveAll(inputData => inputData.referenceName == propData.referenceName); mcd.propertyDatas.Add(propData); continue; } else if (input is ShaderKeyword keyword) { var sanitizedReferenceName = keyword.referenceName; if (keyword.keywordType == KeywordType.Boolean && keyword.referenceName.Contains("_ON")) sanitizedReferenceName = sanitizedReferenceName.Replace("_ON", String.Empty); propData = new GraphInputData() { referenceName = sanitizedReferenceName, keywordType = keyword.keywordType, isKeyword = true }; } else { var prop = input as AbstractShaderProperty; propData = new GraphInputData() { referenceName = input.referenceName, propertyType = prop.propertyType, isKeyword = false }; } mcd.propertyDatas.Add(propData); inputInspectorDataList.Remove(propData); } sgMetadata.categoryDatas.Add(mcd); } // Any uncategorized elements get tossed into an un-named category at the top as a fallback if (inputInspectorDataList.Count > 0) { sgMetadata.categoryDatas.Insert(0, new MinimalCategoryData() { categoryName = "", propertyDatas = inputInspectorDataList }); } ctx.AddObjectToAsset("SGInternal:Metadata", sgMetadata); // 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}", mainObject); } } // 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); } } } internal class AssetImportErrorLog : MessageManager.IErrorLog { AssetImportContext ctx; public AssetImportErrorLog(AssetImportContext ctx) { this.ctx = ctx; } public void LogError(string message, UnityEngine.Object context = null) { // Note: if you get sent here by clicking on a ShaderGraph error message, // this is a bug in the scripted importer system, not being able to link import error messages to the imported asset ctx.LogImportError(message, context); } public void LogWarning(string message, UnityEngine.Object context = null) { ctx.LogImportWarning(message, context); } } static bool NodeWasUsedByGraph(string nodeId, GraphData graphData) { var node = graphData.GetNodeFromId(nodeId); return node?.wasUsedByGenerator ?? false; } // error messages should be reported through the asset import context, so that object references are translated properly (in the future), and the error is associated with the import static void ReportErrors(GraphData graph, Shader shader, string path, AssetImportErrorLog importLog) { // Grab any messages from the shader compiler var messages = ShaderUtil.GetShaderMessages(shader); var errors = graph.messageManager.ErrorStrings((nodeId) => NodeWasUsedByGraph(nodeId, graph)); int errCount = errors.Count(); // Find the first compiler message that's an error int firstShaderUtilErrorIndex = -1; if (messages != null) firstShaderUtilErrorIndex = Array.FindIndex(messages, m => (m.severity == Rendering.ShaderCompilerMessageSeverity.Error)); // Display only one message. Bias towards shader compiler messages over node messages and within that bias errors over warnings. if (firstShaderUtilErrorIndex != -1) { // if shader compiler reported an error, show that MessageManager.Log(path, messages[firstShaderUtilErrorIndex], shader, importLog); } else if (errCount > 0) { // otherwise show node errors var firstError = errors.FirstOrDefault(); importLog.LogError($"Shader Graph at {path} has {errCount} error(s), the first is: {firstError}", shader); } else if (messages.Length != 0) { // otherwise show shader compiler warnings MessageManager.Log(path, messages[0], shader, importLog); } else if (graph.messageManager.nodeMessagesChanged) { // otherwise show node warnings var warnings = graph.messageManager.ErrorStrings((nodeId) => NodeWasUsedByGraph(nodeId, graph), Rendering.ShaderCompilerMessageSeverity.Warning); var warnCount = warnings.Count(); var firstWarning = warnings.FirstOrDefault(); if (warnCount > 0) importLog.LogWarning($"Shader Graph at {path} has {warnCount} warning(s), the first is: {firstWarning}", shader); } } // this old path is still used by the old VFX path, so keeping it around for now internal static string GetShaderText(string path, out List configuredTextures, AssetCollection assetCollection, GraphData graph, GenerationMode mode = GenerationMode.ForReals, Target[] targets = null) { string shaderString = null; var shaderName = Path.GetFileNameWithoutExtension(path); try { Generator generator; generator = new Generator(graph, graph.outputNode, mode, shaderName, targets, assetCollection); shaderString = generator.generatedShader; configuredTextures = generator.configuredTextures; // we only care if an error was reported for a node that we actually used if (graph.messageManager.AnyError((nodeId) => NodeWasUsedByGraph(nodeId, graph))) { shaderString = null; } } catch (Exception e) { Debug.LogException(e); configuredTextures = new List(); // ignored } if (shaderString == null) { shaderString = k_ErrorShader.Replace("Hidden/GraphErrorShader2", shaderName); } return shaderString; } // this function is used by tests internal static string GetShaderText(string path, out List configuredTextures, AssetCollection assetCollection, out GraphData graph) { var textGraph = File.ReadAllText(path, Encoding.UTF8); graph = new GraphData { messageManager = new MessageManager(), assetGuid = AssetDatabase.AssetPathToGUID(path) }; MultiJson.Deserialize(graph, textGraph); graph.OnEnable(); graph.ValidateGraph(); return GetShaderText(path, out configuredTextures, assetCollection, graph); } /* internal static string GetShaderText(string path, out List configuredTextures) { var textGraph = File.ReadAllText(path, Encoding.UTF8); GraphData graph = new GraphData { messageManager = new MessageManager(), assetGuid = AssetDatabase.AssetPathToGUID(path) }; MultiJson.Deserialize(graph, textGraph); graph.OnEnable(); graph.ValidateGraph(); return GetShaderText(path, out configuredTextures, null, graph); } */ #if VFX_GRAPH_10_0_0_OR_NEWER // TODO: Fix this - VFX Graph can now use ShaderGraph as a code generation path. However, currently, the new // generation path still slightly depends on this container (The implementation of it was tightly coupled in VFXShaderGraphParticleOutput, // and we keep it now as there is no migration path for users yet). This will need to be decoupled so that we can eventually // remove this container. static ShaderGraphVfxAsset GenerateVfxShaderGraphAsset(GraphData graph) { var target = graph.activeTargets.FirstOrDefault(x => x.SupportsVFX()); if (target == null) return null; var nl = Environment.NewLine; var indent = new string(' ', 4); var asset = ScriptableObject.CreateInstance(); var result = asset.compilationResult = new GraphCompilationResult(); var mode = GenerationMode.ForReals; if (target is VFXTarget vfxTarget) { asset.lit = vfxTarget.lit; asset.alphaClipping = vfxTarget.alphaTest; asset.generatesWithShaderGraph = false; } else { asset.lit = true; asset.alphaClipping = false; asset.generatesWithShaderGraph = true; } var assetGuid = graph.assetGuid; var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid); var hlslName = NodeUtils.GetHLSLSafeName(Path.GetFileNameWithoutExtension(assetPath)); var ports = new List(); var nodes = new List(); foreach (var vertexBlock in graph.vertexContext.blocks) { vertexBlock.value.GetInputSlots(ports); NodeUtils.DepthFirstCollectNodesFromNode(nodes, vertexBlock); } foreach (var fragmentBlock in graph.fragmentContext.blocks) { fragmentBlock.value.GetInputSlots(ports); NodeUtils.DepthFirstCollectNodesFromNode(nodes, fragmentBlock); } //Remove inactive blocks from legacy generation if (!asset.generatesWithShaderGraph) { var tmpCtx = new TargetActiveBlockContext(new List(), null); // NOTE: For whatever reason, this call fails for custom interpolator ports (ie, active ones are not detected as active). // For the sake of compatibility with custom interpolator with shadergraph generation, skip the removal of inactive blocks. target.GetActiveBlocks(ref tmpCtx); ports.RemoveAll(materialSlot => { return !tmpCtx.activeBlocks.Any(o => materialSlot.RawDisplayName() == o.displayName); }); } var bodySb = new ShaderStringBuilder(1); var graphIncludes = new IncludeCollection(); var registry = new FunctionRegistry(new ShaderStringBuilder(), graphIncludes, true); foreach (var properties in graph.properties) { properties.SetupConcretePrecision(graph.graphDefaultConcretePrecision); } foreach (var node in nodes) { if (node is IGeneratesBodyCode bodyGenerator) { bodySb.currentNode = node; bodyGenerator.GenerateNodeCode(bodySb, mode); bodySb.ReplaceInCurrentMapping(PrecisionUtil.Token, node.concretePrecision.ToShaderString()); } if (node is IGeneratesFunction generatesFunction) { registry.builder.currentNode = node; generatesFunction.GenerateNodeFunction(registry, mode); } } bodySb.currentNode = null; var portNodeSets = new HashSet[ports.Count]; for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var port = ports[portIndex]; var nodeSet = new HashSet(); NodeUtils.CollectNodeSet(nodeSet, port); portNodeSets[portIndex] = nodeSet; } var portPropertySets = new HashSet[ports.Count]; for (var portIndex = 0; portIndex < ports.Count; portIndex++) { portPropertySets[portIndex] = new HashSet(); } foreach (var node in nodes) { if (!(node is PropertyNode propertyNode)) { continue; } for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var portNodeSet = portNodeSets[portIndex]; if (portNodeSet.Contains(node)) { portPropertySets[portIndex].Add(propertyNode.property.objectId); } } } var shaderProperties = new PropertyCollector(); foreach (var node in nodes) { node.CollectShaderProperties(shaderProperties, GenerationMode.ForReals); } asset.SetTextureInfos(shaderProperties.GetConfiguredTextures()); var codeSnippets = new List(); var portCodeIndices = new List[ports.Count]; var sharedCodeIndices = new List(); for (var i = 0; i < portCodeIndices.Length; i++) { portCodeIndices[i] = new List(); } sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"#include \"Packages/com.unity.shadergraph/ShaderGraphLibrary/Functions.hlsl\"{nl}"); foreach (var include in graphIncludes) { sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add(include.value + nl); } for (var registryIndex = 0; registryIndex < registry.names.Count; registryIndex++) { var name = registry.names[registryIndex]; var source = registry.sources[name]; var precision = source.nodes.First().concretePrecision; var hasPrecisionMismatch = false; var nodeNames = new HashSet(); foreach (var node in source.nodes) { nodeNames.Add(node.name); if (node.concretePrecision != precision) { hasPrecisionMismatch = true; break; } } if (hasPrecisionMismatch) { var message = new StringBuilder($"Precision mismatch for function {name}:"); foreach (var node in source.nodes) { message.AppendLine($"{node.name} ({node.objectId}): {node.concretePrecision}"); } throw new InvalidOperationException(message.ToString()); } var code = source.code.Replace(PrecisionUtil.Token, precision.ToShaderString()); code = $"// Node: {string.Join(", ", nodeNames)}{nl}{code}"; var codeIndex = codeSnippets.Count; codeSnippets.Add(code + nl); for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var portNodeSet = portNodeSets[portIndex]; foreach (var node in source.nodes) { if (portNodeSet.Contains(node)) { portCodeIndices[portIndex].Add(codeIndex); break; } } } } foreach (var property in graph.properties) { if (property.isExposed) { continue; } for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var portPropertySet = portPropertySets[portIndex]; if (portPropertySet.Contains(property.objectId)) { portCodeIndices[portIndex].Add(codeSnippets.Count); } } ShaderStringBuilder builder = new ShaderStringBuilder(); property.ForeachHLSLProperty(h => h.AppendTo(builder)); codeSnippets.Add($"// Property: {property.displayName}{nl}{builder.ToCodeBlock()}{nl}{nl}"); } foreach (var prop in shaderProperties.properties) { if (!graph.properties.Contains(prop) && (prop is SamplerStateShaderProperty)) { sharedCodeIndices.Add(codeSnippets.Count); ShaderStringBuilder builder = new ShaderStringBuilder(); prop.ForeachHLSLProperty(h => h.AppendTo(builder)); codeSnippets.Add($"// Property: {prop.displayName}{nl}{builder.ToCodeBlock()}{nl}{nl}"); } } var inputStructName = $"SG_Input_{assetGuid}"; var outputStructName = $"SG_Output_{assetGuid}"; var evaluationFunctionName = $"SG_Evaluate_{assetGuid}"; #region Input Struct sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"struct {inputStructName}{nl}{{{nl}"); #region Requirements var portRequirements = new ShaderGraphRequirements[ports.Count]; for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var requirementsNodes = portNodeSets[portIndex].ToList(); requirementsNodes.Add(ports[portIndex].owner); portRequirements[portIndex] = ShaderGraphRequirements.FromNodes(requirementsNodes, ports[portIndex].stageCapability); } var portIndices = new List(); portIndices.Capacity = ports.Count; void AddRequirementsSnippet(Func predicate, string snippet) { portIndices.Clear(); for (var portIndex = 0; portIndex < ports.Count; portIndex++) { if (predicate(portRequirements[portIndex])) { portIndices.Add(portIndex); } } if (portIndices.Count > 0) { foreach (var portIndex in portIndices) { portCodeIndices[portIndex].Add(codeSnippets.Count); } codeSnippets.Add($"{indent}{snippet};{nl}"); } } void AddCoordinateSpaceSnippets(InterpolatorType interpolatorType, Func selector) { foreach (var space in EnumInfo.values) { var neededSpace = space.ToNeededCoordinateSpace(); AddRequirementsSnippet(r => (selector(r) & neededSpace) > 0, $"float3 {space.ToVariableName(interpolatorType)}"); } } // TODO: Rework requirements system to make this better AddCoordinateSpaceSnippets(InterpolatorType.Normal, r => r.requiresNormal); AddCoordinateSpaceSnippets(InterpolatorType.Tangent, r => r.requiresTangent); AddCoordinateSpaceSnippets(InterpolatorType.BiTangent, r => r.requiresBitangent); AddCoordinateSpaceSnippets(InterpolatorType.ViewDirection, r => r.requiresViewDir); AddCoordinateSpaceSnippets(InterpolatorType.Position, r => r.requiresPosition); AddCoordinateSpaceSnippets(InterpolatorType.PositionPredisplacement, r => r.requiresPositionPredisplacement); AddRequirementsSnippet(r => r.requiresVertexColor, $"float4 {ShaderGeneratorNames.VertexColor}"); AddRequirementsSnippet(r => r.requiresScreenPosition, $"float4 {ShaderGeneratorNames.ScreenPosition}"); AddRequirementsSnippet(r => r.requiresNDCPosition, $"float2 {ShaderGeneratorNames.NDCPosition}"); AddRequirementsSnippet(r => r.requiresPixelPosition, $"float2 {ShaderGeneratorNames.PixelPosition}"); AddRequirementsSnippet(r => r.requiresFaceSign, $"float4 {ShaderGeneratorNames.FaceSign}"); foreach (var uvChannel in EnumInfo.values) { AddRequirementsSnippet(r => r.requiresMeshUVs.Contains(uvChannel), $"half4 {uvChannel.GetUVName()}"); } AddRequirementsSnippet(r => r.requiresTime, $"float3 {ShaderGeneratorNames.TimeParameters}"); #endregion sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"}};{nl}{nl}"); #endregion // VFX Code heavily relies on the slotId from the original MasterNodes // Since we keep these around for upgrades anyway, for now it is simpler to use them // Therefore we remap the output blocks back to the original Ids here var originialPortIds = new int[ports.Count]; for (int i = 0; i < originialPortIds.Length; i++) { if (!VFXTarget.s_BlockMap.TryGetValue((ports[i].owner as BlockNode).descriptor, out var originalId)) continue; // In Master Nodes we had a different BaseColor/Color slot id between Unlit/Lit // In the stack we use BaseColor for both cases. Catch this here. if (asset.lit && originalId == ShaderGraphVfxAsset.ColorSlotId) { originalId = ShaderGraphVfxAsset.BaseColorSlotId; } originialPortIds[i] = originalId; } #region Output Struct sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"struct {outputStructName}{nl}{{"); for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var port = ports[portIndex]; portCodeIndices[portIndex].Add(codeSnippets.Count); codeSnippets.Add($"{nl}{indent}{port.concreteValueType.ToShaderString(graph.graphDefaultConcretePrecision)} {port.shaderOutputName}_{originialPortIds[portIndex]};"); } sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"{nl}}};{nl}{nl}"); #endregion #region Graph Function sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"{outputStructName} {evaluationFunctionName}({nl}{indent}{inputStructName} IN"); var inputProperties = new List(); var portPropertyIndices = new List[ports.Count]; var propertiesStages = new List(); for (var portIndex = 0; portIndex < ports.Count; portIndex++) { portPropertyIndices[portIndex] = new List(); } // Fetch properties from the categories to keep the same order as in the shader graph blackboard // Union with the flat properties collection because previous shader graph version could store properties without category var sortedProperties = graph.categories .SelectMany(x => x.Children) .OfType() .Union(graph.properties) .Where(x => x.isExposed); foreach (var property in sortedProperties) { var propertyIndex = inputProperties.Count; var codeIndex = codeSnippets.Count; ShaderStageCapability stageCapability = 0; for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var portPropertySet = portPropertySets[portIndex]; if (portPropertySet.Contains(property.objectId)) { portCodeIndices[portIndex].Add(codeIndex); portPropertyIndices[portIndex].Add(propertyIndex); stageCapability |= ports[portIndex].stageCapability; } } propertiesStages.Add(stageCapability); inputProperties.Add(property); codeSnippets.Add($",{nl}{indent}/* Property: {property.displayName} */ {property.GetPropertyAsArgumentStringForVFX(property.concretePrecision.ToShaderString())}"); } sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"){nl}{{"); #region Node Code for (var mappingIndex = 0; mappingIndex < bodySb.mappings.Count; mappingIndex++) { var mapping = bodySb.mappings[mappingIndex]; var code = bodySb.ToString(mapping.startIndex, mapping.count); if (string.IsNullOrWhiteSpace(code)) { continue; } code = $"{nl}{indent}// Node: {mapping.node.name}{nl}{code}"; var codeIndex = codeSnippets.Count; codeSnippets.Add(code); for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var portNodeSet = portNodeSets[portIndex]; if (portNodeSet.Contains(mapping.node)) { portCodeIndices[portIndex].Add(codeIndex); } } } #endregion #region Output Mapping sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"{nl}{indent}// VFXMasterNode{nl}{indent}{outputStructName} OUT;{nl}"); // Output mapping for (var portIndex = 0; portIndex < ports.Count; portIndex++) { var port = ports[portIndex]; portCodeIndices[portIndex].Add(codeSnippets.Count); codeSnippets.Add($"{indent}OUT.{port.shaderOutputName}_{originialPortIds[portIndex]} = {port.owner.GetSlotValue(port.id, GenerationMode.ForReals, graph.graphDefaultConcretePrecision)};{nl}"); } #endregion // Function end sharedCodeIndices.Add(codeSnippets.Count); codeSnippets.Add($"{indent}return OUT;{nl}}}{nl}"); #endregion result.codeSnippets = codeSnippets.ToArray(); result.sharedCodeIndices = sharedCodeIndices.ToArray(); result.outputCodeIndices = new IntArray[ports.Count]; for (var i = 0; i < ports.Count; i++) { result.outputCodeIndices[i] = portCodeIndices[i].ToArray(); } var outputMetadatas = new OutputMetadata[ports.Count]; for (int portIndex = 0; portIndex < outputMetadatas.Length; portIndex++) { outputMetadatas[portIndex] = new OutputMetadata(portIndex, ports[portIndex].shaderOutputName, originialPortIds[portIndex]); } asset.SetOutputs(outputMetadatas); asset.evaluationFunctionName = evaluationFunctionName; asset.inputStructName = inputStructName; asset.outputStructName = outputStructName; asset.portRequirements = portRequirements; asset.m_PropertiesStages = propertiesStages.ToArray(); asset.concretePrecision = graph.graphDefaultConcretePrecision; asset.SetProperties(inputProperties); asset.outputPropertyIndices = new IntArray[ports.Count]; for (var portIndex = 0; portIndex < ports.Count; portIndex++) { asset.outputPropertyIndices[portIndex] = portPropertyIndices[portIndex].ToArray(); } return asset; } #endif } }