using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; using UnityEditor.Graphing; using UnityEditor.Graphing.Util; using UnityEditor.ShaderGraph.Drawing.Inspector; using Object = UnityEngine.Object; using UnityEditor.Experimental.GraphView; using UnityEditor.ShaderGraph.Drawing.Colors; using UnityEngine.UIElements; using Edge = UnityEditor.Experimental.GraphView.Edge; using UnityEditor.VersionControl; using UnityEditor.Searcher; using Unity.Profiling; using UnityEditor.ShaderGraph.Internal; using UnityEditor.Experimental; using UnityEditor.PackageManager.UI; namespace UnityEditor.ShaderGraph.Drawing { [Serializable] class FloatingWindowsLayout { public WindowDockingLayout previewLayout = new WindowDockingLayout { dockingTop = false, dockingLeft = false, verticalOffset = 8, horizontalOffset = 8 }; } [Serializable] class UserViewSettings { public bool isBlackboardVisible = true; public bool isPreviewVisible = true; public bool isInspectorVisible = true; public string colorProvider = NoColors.Title; } class GraphEditorView : VisualElement, IDisposable { MaterialGraphView m_GraphView; MasterPreviewView m_MasterPreviewView; InspectorView m_InspectorView; GraphData m_Graph; PreviewManager m_PreviewManager; MessageManager m_MessageManager; SearchWindowProvider m_SearchWindowProvider; EdgeConnectorListener m_EdgeConnectorListener; VisualElement m_HoveredContextView; BlackboardController m_BlackboardController; internal BlackboardController blackboardController { get => m_BlackboardController; set { if (value != null) m_BlackboardController = value; } } ColorManager m_ColorManager; EditorWindow m_EditorWindow; const string k_UserViewSettings = "UnityEditor.ShaderGraph.ToggleSettings"; UserViewSettings m_UserViewSettings; internal UserViewSettings viewSettings { get => m_UserViewSettings; } const string k_FloatingWindowsLayoutKey = "UnityEditor.ShaderGraph.FloatingWindowsLayout2"; FloatingWindowsLayout m_FloatingWindowsLayout = new FloatingWindowsLayout(); public Action saveRequested { get; set; } public Action saveAsRequested { get; set; } public Func isCheckedOut { get; set; } public Action checkOut { get; set; } public Action convertToSubgraphRequested { get { return m_GraphView.onConvertToSubgraphClick; } set { m_GraphView.onConvertToSubgraphClick = value; } } public Action showInProjectRequested { get; set; } public MaterialGraphView graphView { get { return m_GraphView; } } public InspectorView inspectorView { get { return m_InspectorView; } } internal PreviewManager previewManager { get { return m_PreviewManager; } set { m_PreviewManager = value; } } public string assetName { get => m_AssetName; set { m_AssetName = value; // Also update blackboard title m_BlackboardController.UpdateBlackboardTitle(m_AssetName); } } public ColorManager colorManager { get => m_ColorManager; } void InstallSample(string sampleName) { var sample = Sample.FindByPackage("com.unity.shadergraph", null).SingleOrDefault(x => x.displayName == sampleName); if (!string.IsNullOrEmpty(sample.displayName)) { if (!sample.isImported) { sample.Import(); } else { var reinstall = EditorUtility.DisplayDialog("Warning", "This sample package is already installed.\nDo you want to reinstall it?", "Yes", "No"); if (reinstall) { sample.Import(Sample.ImportOptions.OverridePreviousImports); } } } else { Debug.LogWarning($"Could not find sample package {sampleName}"); } } private static readonly ProfilerMarker AddGroupsMarker = new ProfilerMarker("AddGroups"); private static readonly ProfilerMarker AddStickyNotesMarker = new ProfilerMarker("AddStickyNotes"); public GraphEditorView(EditorWindow editorWindow, GraphData graph, MessageManager messageManager, string graphName) { m_GraphViewGroupTitleChanged = OnGroupTitleChanged; m_GraphViewElementsAddedToGroup = OnElementsAddedToGroup; m_GraphViewElementsRemovedFromGroup = OnElementsRemovedFromGroup; ShaderGraphPreferences.onZoomStepSizeChanged += ResetZoom; m_EditorWindow = editorWindow; m_Graph = graph; m_AssetName = graphName; m_MessageManager = messageManager; previewManager = new PreviewManager(graph, messageManager); previewManager.RenderPreviews(m_EditorWindow, false); styleSheets.Add(Resources.Load("Styles/GraphEditorView")); var serializedSettings = EditorUserSettings.GetConfigValue(k_UserViewSettings); m_UserViewSettings = JsonUtility.FromJson(serializedSettings) ?? new UserViewSettings(); m_ColorManager = new ColorManager(m_UserViewSettings.colorProvider); List toolbarExtensions = new(); foreach (var type in TypeCache.GetTypesDerivedFrom(typeof(IShaderGraphToolbarExtension)).Where(e => !e.IsGenericType)) { toolbarExtensions.Add((IShaderGraphToolbarExtension)Activator.CreateInstance(type)); } var colorProviders = m_ColorManager.providerNames.ToArray(); var toolbar = new IMGUIContainer(() => { GUILayout.BeginHorizontal(EditorStyles.toolbar); if (GUILayout.Button(new GUIContent(EditorGUIUtility.FindTexture("SaveActive"), "Save"), EditorStyles.toolbarButton)) { if (saveRequested != null) saveRequested(); } if (GUILayout.Button(EditorResources.Load("d_dropdown"), EditorStyles.toolbarButton)) { GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent("Save As..."), false, () => saveAsRequested()); menu.AddItem(new GUIContent("Show In Project"), false, () => showInProjectRequested()); if (!isCheckedOut() && Provider.enabled && Provider.isActive) { menu.AddItem(new GUIContent("Check Out"), false, () => { if (checkOut != null) checkOut(); }); } else { menu.AddDisabledItem(new GUIContent("Check Out"), false); } menu.ShowAsContext(); } if (graphView != null) foreach (var ext in toolbarExtensions) ext.OnGUI(graphView); GUILayout.FlexibleSpace(); EditorGUI.BeginChangeCheck(); GUILayout.Label("Color Mode"); var newColorIndex = EditorGUILayout.Popup(m_ColorManager.activeIndex, colorProviders, GUILayout.Width(100f)); GUILayout.Space(4); m_UserViewSettings.isBlackboardVisible = GUILayout.Toggle(m_UserViewSettings.isBlackboardVisible, new GUIContent(Resources.Load("Icons/blackboard"), "Blackboard"), EditorStyles.toolbarButton); GUILayout.Space(6); m_UserViewSettings.isInspectorVisible = GUILayout.Toggle(m_UserViewSettings.isInspectorVisible, new GUIContent(EditorGUIUtility.TrIconContent("d_UnityEditor.InspectorWindow").image, "Graph Inspector"), EditorStyles.toolbarButton); GUILayout.Space(6); m_UserViewSettings.isPreviewVisible = GUILayout.Toggle(m_UserViewSettings.isPreviewVisible, new GUIContent(EditorGUIUtility.FindTexture("PreMatSphere"), "Main Preview"), EditorStyles.toolbarButton); if (GUILayout.Button(new GUIContent(EditorGUIUtility.TrIconContent("_Help").image, "Open Shader Graph User Manual"), EditorStyles.toolbarButton)) { Application.OpenURL(UnityEngine.Rendering.ShaderGraph.Documentation.GetPageLink("index")); //Application.OpenURL("https://docs.unity3d.com/Packages/com.unity.shadergraph@17.0/manual/index.html"); // TODO : point to latest? } if (GUILayout.Button(EditorResources.Load("d_dropdown"), EditorStyles.toolbarButton)) { GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent("Shader Graph Samples"), false, () => { PackageManager.UI.Window.Open("com.unity.shadergraph"); }); menu.AddItem(new GUIContent("Install Node Reference Sample"), false, () => { InstallSample("Node Reference"); }); menu.AddItem(new GUIContent("Install Procedural Patterns Sample"), false, () => { InstallSample("Procedural Patterns"); }); menu.AddSeparator(""); menu.AddItem(new GUIContent("Shader Graph Feature Page"), false, () => { Application.OpenURL("https://unity.com/features/shader-graph"); }); menu.AddItem(new GUIContent("Shader Graph Forums"), false, () => { Application.OpenURL("https://forum.unity.com/forums/shader-graph.346/"); }); menu.AddItem(new GUIContent("Shader Graph Roadmap"), false, () => { Application.OpenURL("https://portal.productboard.com/unity/1-unity-platform-rendering-visual-effects/tabs/7-shader-graph"); }); menu.ShowAsContext(); } if (EditorGUI.EndChangeCheck()) { UserViewSettingsChangeCheck(newColorIndex); } GUILayout.EndHorizontal(); }); Add(toolbar); var content = new VisualElement { name = "content" }; { m_GraphView = new MaterialGraphView(graph, () => m_PreviewManager.UpdateMasterPreview(ModificationScope.Topological)) { name = "GraphView", viewDataKey = "MaterialGraphView" }; ResetZoom(); m_GraphView.AddManipulator(new ContentDragger()); m_GraphView.AddManipulator(new SelectionDragger()); m_GraphView.AddManipulator(new RectangleSelector()); m_GraphView.AddManipulator(new ClickSelector()); // Bugfix 1312222. Running 'ResetSelectedBlockNodes' on all mouse up interactions will break selection // after changing tabs. This was originally added to fix a bug with middle-mouse clicking while dragging a block node. m_GraphView.RegisterCallback(evt => { if (evt.button == (int)MouseButton.MiddleMouse) m_GraphView.ResetSelectedBlockNodes(); }); // This takes care of when a property is dragged from BB and then the drag is ended by the Escape key, hides the scroll boundary regions and drag indicator if so m_GraphView.RegisterCallback(evt => { blackboardController.blackboard.OnDragExitedEvent(evt); blackboardController.blackboard.hideDragIndicatorAction?.Invoke(); }); RegisterGraphViewCallbacks(); content.Add(m_GraphView); string serializedWindowLayout = EditorUserSettings.GetConfigValue(k_FloatingWindowsLayoutKey); if (!string.IsNullOrEmpty(serializedWindowLayout)) { m_FloatingWindowsLayout = JsonUtility.FromJson(serializedWindowLayout); } CreateMasterPreview(); CreateInspector(); CreateBlackboard(); UpdateSubWindowsVisibility(); m_GraphView.graphViewChanged = GraphViewChanged; RegisterCallback(ApplySerializedWindowLayouts); if (m_Graph.isSubGraph) { m_GraphView.AddToClassList("subgraph"); } } m_SearchWindowProvider = new SearcherProvider(); m_SearchWindowProvider.Initialize(editorWindow, m_Graph, m_GraphView); m_GraphView.nodeCreationRequest = NodeCreationRequest; //regenerate entries when graph view is refocused, to propogate subgraph changes m_GraphView.RegisterCallback(evt => { m_SearchWindowProvider.regenerateEntries = true; }); m_EdgeConnectorListener = new EdgeConnectorListener(m_Graph, m_SearchWindowProvider, editorWindow); if (!m_Graph.isSubGraph) { AddContexts(); } using (AddGroupsMarker.Auto()) { foreach (var graphGroup in graph.groups) AddGroup(graphGroup); } using (AddStickyNotesMarker.Auto()) { foreach (var stickyNote in graph.stickyNotes) AddStickyNote(stickyNote); } AddNodes(graph.GetNodes()); AddBlocks(graph.GetNodes()); AddEdges(graph.edges); Add(content); // Active block lists need to be initialized on window start up // Do this here to as we cant do this inside GraphData // This is due to targets not being deserialized yet var context = new TargetSetupContext(); foreach (var target in m_Graph.activeTargets) { target.Setup(ref context); } var activeBlocks = m_Graph.GetActiveBlocksForAllActiveTargets(); m_Graph.UpdateActiveBlocks(activeBlocks); // Graph settings need to be initialized after the target setup m_InspectorView.InitializeGraphSettings(); } private void CreateBlackboard() { var blackboardViewModel = new BlackboardViewModel() { parentView = graphView, model = m_Graph, title = assetName }; m_BlackboardController = new BlackboardController(m_Graph, blackboardViewModel, m_Graph.owner.graphDataStore); } void AddContexts() { ContextView AddContext(string name, ContextData contextData, Direction portDirection) { //need to eventually remove this reference to editor window in context views var contextView = new ContextView(name, contextData, m_EditorWindow); // GraphView marks ContextViews' stacks, but not the actual root elements, as insertable. We want the // contextual searcher menu to come up when *any* part of the ContextView is hovered. As a workaround, // we keep track of the hovered ContextView and offer it if no targets are found. contextView.RegisterCallback((MouseOverEvent _) => m_HoveredContextView = contextView); contextView.RegisterCallback((MouseOutEvent _) => { if (m_HoveredContextView == contextView) m_HoveredContextView = null; }); contextView.SetPosition(new Rect(contextData.position, Vector2.zero)); contextView.AddPort(portDirection); m_GraphView.AddElement(contextView); return contextView; } // Add Contexts // As Contexts are hardcoded and contain a single port we can just give the direction var vertexContext = AddContext("Vertex", m_Graph.vertexContext, Direction.Output); var fragmentContext = AddContext("Fragment", m_Graph.fragmentContext, Direction.Input); // Connect Contexts // Vertical Edges have no representation in Model // Therefore just draw it and dont allow interaction var contextEdge = new Edge() { output = vertexContext.port, input = fragmentContext.port, pickingMode = PickingMode.Ignore, }; m_GraphView.AddElement(contextEdge); // Update the Context list on MaterialGraphView m_GraphView.UpdateContextList(); } internal void UserViewSettingsChangeCheck(int newColorIndex) { if (newColorIndex != m_ColorManager.activeIndex) { m_ColorManager.SetActiveProvider(newColorIndex, m_GraphView.Query().ToList()); m_UserViewSettings.colorProvider = m_ColorManager.activeProviderName; } var serializedViewSettings = JsonUtility.ToJson(m_UserViewSettings); EditorUserSettings.SetConfigValue(k_UserViewSettings, serializedViewSettings); UpdateSubWindowsVisibility(); } void NodeCreationRequest(NodeCreationContext c) { if (EditorWindow.focusedWindow == m_EditorWindow) //only display the search window when current graph view is focused { m_SearchWindowProvider.connectedPort = null; m_SearchWindowProvider.target = c.target ?? m_HoveredContextView; var displayPosition = graphView.cachedMousePosition; SearcherWindow.Show(m_EditorWindow, (m_SearchWindowProvider as SearcherProvider).LoadSearchWindow(), item => (m_SearchWindowProvider as SearcherProvider).OnSearcherSelectEntry(item, displayPosition), displayPosition, null, new SearcherWindow.Alignment(SearcherWindow.Alignment.Vertical.Center, SearcherWindow.Alignment.Horizontal.Left)); } } // Master Preview, Inspector and Blackboard all need to keep their layouts when hidden in order to restore user preferences. // Because of their differences we do this is different ways, for now. void UpdateSubWindowsVisibility() { // Blackboard needs to be effectively removed when hidden to avoid bugs. if (m_UserViewSettings.isBlackboardVisible) blackboardController.blackboard.ShowWindow(); else blackboardController.blackboard.HideWindow(); // Same for the inspector if (m_UserViewSettings.isInspectorVisible) m_InspectorView.ShowWindow(); else m_InspectorView.HideWindow(); m_MasterPreviewView.visible = m_UserViewSettings.isPreviewVisible; } Action m_GraphViewGroupTitleChanged; Action> m_GraphViewElementsAddedToGroup; Action> m_GraphViewElementsRemovedFromGroup; void RegisterGraphViewCallbacks() { m_GraphView.groupTitleChanged = m_GraphViewGroupTitleChanged; m_GraphView.elementsAddedToGroup = m_GraphViewElementsAddedToGroup; m_GraphView.elementsRemovedFromGroup = m_GraphViewElementsRemovedFromGroup; } void UnregisterGraphViewCallbacks() { m_GraphView.groupTitleChanged = null; m_GraphView.elementsAddedToGroup = null; m_GraphView.elementsRemovedFromGroup = null; } void CreateMasterPreview() { m_MasterPreviewView = new MasterPreviewView(previewManager, m_Graph) { name = "masterPreview" }; var masterPreviewViewDraggable = new WindowDraggable(null, this); m_MasterPreviewView.AddManipulator(masterPreviewViewDraggable); m_GraphView.Add(m_MasterPreviewView); masterPreviewViewDraggable.OnDragFinished += UpdateSerializedWindowLayout; m_MasterPreviewView.previewResizeBorderFrame.OnResizeFinished += UpdateSerializedWindowLayout; } void CreateInspector() { var inspectorViewModel = new InspectorViewModel() { parentView = this.graphView }; m_InspectorView = new InspectorView(inspectorViewModel); graphView.OnSelectionChange += m_InspectorView.TriggerInspectorUpdate; // Undo/redo actions that only affect selection don't trigger the above callback for some reason, so we also have to do this Undo.undoRedoPerformed += (() => { m_InspectorView?.TriggerInspectorUpdate(graphView?.selection); }); } // a nice curve that scales well for various HID (touchpad and mice). static float WeightStepSize(float x) => Mathf.Clamp(2 * Mathf.Pow(x, 7f / 2f), 0.001f, 2.0f); void ResetZoom() { var weightedStepSize = WeightStepSize(ShaderGraphPreferences.zoomStepSize); m_GraphView?.SetupZoom(0.05f, 8.0f, weightedStepSize, 1.0f); } GraphViewChange GraphViewChanged(GraphViewChange graphViewChange) { if (graphViewChange.edgesToCreate != null) { foreach (var edge in graphViewChange.edgesToCreate) { var leftSlot = edge.output.GetSlot(); var rightSlot = edge.input.GetSlot(); if (leftSlot != null && rightSlot != null) { m_Graph.owner.RegisterCompleteObjectUndo("Connect Edge"); m_Graph.Connect(leftSlot.slotReference, rightSlot.slotReference); } } graphViewChange.edgesToCreate.Clear(); } if (graphViewChange.movedElements != null) { m_Graph.owner.RegisterCompleteObjectUndo("Move Elements"); List nodesInsideGroup = new List(); foreach (var element in graphViewChange.movedElements) { var groupNode = element as ShaderGroup; if (groupNode == null) continue; foreach (GraphElement graphElement in groupNode.containedElements) { nodesInsideGroup.Add(graphElement); } SetGroupPosition(groupNode); } if (nodesInsideGroup.Any()) graphViewChange.movedElements.AddRange(nodesInsideGroup); foreach (var element in graphViewChange.movedElements) { if (element.userData is AbstractMaterialNode node) { var drawState = node.drawState; drawState.position = element.parent.ChangeCoordinatesTo(m_GraphView.contentViewContainer, element.GetPosition()); node.drawState = drawState; // BlockNode moved outside a Context // This isnt allowed but there is no way to disallow it on the GraphView if (node is BlockNode blockNode && element.GetFirstAncestorOfType() == null) { var context = graphView.GetContext(blockNode.contextData); // isDragging ensures we arent calling this when moving // the BlockNode into the GraphView during dragging if (context.isDragging) continue; // Remove from GraphView and add back to Context m_GraphView.RemoveElement(element); context.InsertBlock(element as MaterialNodeView); } } if (element is StickyNote stickyNote) { SetStickyNotePosition(stickyNote); } if (element is ContextView contextView) { var rect = element.parent.ChangeCoordinatesTo(m_GraphView.contentViewContainer, element.GetPosition()); contextView.contextData.position = rect.position; } } } var nodesToUpdate = m_NodeViewHashSet; nodesToUpdate.Clear(); if (graphViewChange.elementsToRemove != null) { m_Graph.owner.RegisterCompleteObjectUndo("Remove Elements"); m_Graph.RemoveElements( graphViewChange.elementsToRemove.OfType().Select(v => v.node).ToArray(), graphViewChange.elementsToRemove.OfType().Select(e => (IEdge)e.userData).ToArray(), graphViewChange.elementsToRemove.OfType().Select(g => g.userData).ToArray(), graphViewChange.elementsToRemove.OfType().Select(n => n.userData).ToArray(), graphViewChange.elementsToRemove.OfType().Select(f => (ShaderInput)f.userData).ToArray() ); foreach (var edge in graphViewChange.elementsToRemove.OfType()) { if (edge.input != null) { if (edge.input.node is IShaderNodeView materialNodeView) nodesToUpdate.Add(materialNodeView); } if (edge.output != null) { if (edge.output.node is IShaderNodeView materialNodeView) nodesToUpdate.Add(materialNodeView); } } } foreach (var node in nodesToUpdate) { node.OnModified(ModificationScope.Topological); } UpdateEdgeColors(nodesToUpdate); return graphViewChange; } void SetGroupPosition(ShaderGroup groupNode) { var pos = groupNode.GetPosition(); groupNode.userData.position = new Vector2(pos.x, pos.y); } void SetStickyNotePosition(StickyNote stickyNote) { var pos = stickyNote.GetPosition(); stickyNote.userData.position = new Rect(pos); } void OnGroupTitleChanged(Group graphGroup, string title) { var groupData = graphGroup.userData as GroupData; if (groupData != null) { groupData.title = graphGroup.title; } } void OnElementsAddedToGroup(Group graphGroup, IEnumerable elements) { if (graphGroup.userData is GroupData groupData) { var anyChanged = false; foreach (var element in elements) { if (element.userData is IGroupItem groupItem && groupItem.group != groupData) { anyChanged = true; break; } } if (!anyChanged) return; m_Graph.owner.RegisterCompleteObjectUndo(groupData.title); foreach (var element in elements) { if (element.userData is IGroupItem groupItem) { m_Graph.SetGroup(groupItem, groupData); } } } } void OnElementsRemovedFromGroup(Group graphGroup, IEnumerable elements) { if (graphGroup.userData is GroupData groupData) { var anyChanged = false; foreach (var element in elements) { if (element.userData is IGroupItem groupItem && groupItem.group == groupData) { anyChanged = true; break; } } if (!anyChanged) return; m_Graph.owner.RegisterCompleteObjectUndo("Ungroup Node(s)"); foreach (var element in elements) { if (element.userData is IGroupItem groupItem) { m_Graph.SetGroup(groupItem, null); SetGroupPosition((ShaderGroup)graphGroup); //, (GraphElement)nodeView); } } } } void OnNodeChanged(AbstractMaterialNode inNode, ModificationScope scope) { if (m_GraphView == null) return; var dependentNodes = new List(); if (!inNode.owner.graphIsConcretizing && !inNode.owner.replaceInProgress) NodeUtils.CollectNodesNodeFeedsInto(dependentNodes, inNode); else dependentNodes.Add(inNode); foreach (var node in dependentNodes) { var nodeView = m_GraphView.GetNodeByGuid(node.objectId) as IShaderNodeView; if (nodeView != null) nodeView.OnModified(scope); } } HashSet m_NodeViewHashSet = new HashSet(); HashSet m_GroupHashSet = new HashSet(); float lastUpdate = 0f; public void HandleGraphChanges(bool wasUndoRedoPerformed) { UnregisterGraphViewCallbacks(); // anything that gets a new view needs to be updated. throughout this call. // while it's a little expensive to build this every graph change, it's a huge // improvement in overall performance to do so. Dictionary lookupTable = new(); m_GraphView.graphElements.ForEach(e => { if (e.userData != null) lookupTable.Add(e.userData, e); }); previewManager.HandleGraphChanges(); var windowReceivesUpdates = (m_EditorWindow as MaterialGraphEditWindow)?.isVisible ?? false; if (Time.realtimeSinceStartup - lastUpdate >= 0.03f && windowReceivesUpdates && m_UserViewSettings.isPreviewVisible) { lastUpdate = Time.realtimeSinceStartup; previewManager.UpdateMasterPreview(ModificationScope.Node); } m_InspectorView.HandleGraphChanges(); if (m_Graph.addedEdges.Any() || m_Graph.removedEdges.Any()) { // Precision color provider is the only one that needs to update node colors on connection. if (m_ColorManager.activeProviderName == "Precision") { var nodeList = m_GraphView.Query().ToList(); m_ColorManager.SetNodesDirty(nodeList); m_ColorManager.UpdateNodeViews(nodeList); } } previewManager.RenderPreviews(m_EditorWindow); m_GraphView.wasUndoRedoPerformed = wasUndoRedoPerformed; if (wasUndoRedoPerformed || m_InspectorView.doesInspectorNeedUpdate) m_InspectorView.Update(); if (wasUndoRedoPerformed) m_GraphView.RestorePersistentSelectionAfterUndoRedo(); m_GroupHashSet.Clear(); HandleRemovedNodes(lookupTable); foreach (var noteData in m_Graph.removedNotes) { if (lookupTable.TryGetValue(noteData, out var note)) m_GraphView.RemoveElement(note); } foreach (GroupData groupData in m_Graph.removedGroups) { if (lookupTable.TryGetValue(groupData, out var group)) m_GraphView.RemoveElement(group); } foreach (var groupData in m_Graph.addedGroups) { AddGroup(groupData, lookupTable: lookupTable); } foreach (var stickyNote in m_Graph.addedStickyNotes) { AddStickyNote(stickyNote, lookupTable: lookupTable); } foreach (var node in m_Graph.addedNodes) { AddNode(node, lookupTable: lookupTable); } foreach (var groupChange in m_Graph.parentGroupChanges) { GraphElement graphElement = null; if (groupChange.groupItem is AbstractMaterialNode node) { lookupTable.TryGetValue(node, out graphElement); } else if (groupChange.groupItem is StickyNoteData stickyNote) { lookupTable.TryGetValue(stickyNote, out graphElement); } else { throw new InvalidOperationException("Unknown group item type."); } if (graphElement != null) { var groupView = graphElement.GetContainingScope() as ShaderGroup; if (groupView?.userData != groupChange.newGroup) { groupView?.RemoveElement(graphElement); if (groupChange.newGroup != null) { lookupTable.TryGetValue(groupChange.newGroup, out var newGroupView); ((ShaderGroup)newGroupView).AddElement(graphElement); } } } } foreach (var groupData in m_Graph.pastedGroups) { if (lookupTable.TryGetValue(groupData, out var group)) m_GraphView.AddToSelection(group); } foreach (var stickyNoteData in m_Graph.pastedStickyNotes) { if (lookupTable.TryGetValue(stickyNoteData, out var stickyNote)) m_GraphView.AddToSelection(stickyNote); } foreach (var node in m_Graph.pastedNodes) { if (lookupTable.TryGetValue(node.objectId, out var nodeView) && nodeView is IShaderNodeView) m_GraphView.AddToSelection((Node)nodeView); } foreach (var shaderGroup in m_GroupHashSet) { SetGroupPosition(shaderGroup); } var nodesToUpdate = m_NodeViewHashSet; nodesToUpdate.Clear(); foreach (var edge in m_Graph.removedEdges) { if (lookupTable.TryGetValue(edge, out var obj) && obj is Edge edgeView) { var nodeView = (IShaderNodeView)edgeView.input.node; if (nodeView?.node != null) { nodesToUpdate.Add(nodeView); } edgeView.output.Disconnect(edgeView); edgeView.input.Disconnect(edgeView); edgeView.output = null; edgeView.input = null; m_GraphView.RemoveElement(edgeView); } } foreach (var edge in m_Graph.addedEdges) { var edgeView = AddEdge(edge, lookupTable: lookupTable); if (edgeView != null) nodesToUpdate.Add((IShaderNodeView)edgeView.input.node); } foreach (var node in nodesToUpdate) { node.OnModified(ModificationScope.Topological); } UpdateEdgeColors(nodesToUpdate); if (m_Graph.movedContexts) { foreach (var context in m_GraphView.contexts) { context.SetPosition(new Rect(context.contextData.position, Vector2.zero)); } } // Checking if any new Group Nodes just got added if (m_Graph.mostRecentlyCreatedGroup != null) { var groups = m_GraphView.graphElements.ToList().OfType(); foreach (ShaderGroup shaderGroup in groups) { if (shaderGroup.userData == m_Graph.mostRecentlyCreatedGroup) { shaderGroup.FocusTitleTextField(); break; } } } // If we auto-remove blocks and something has happened to trigger a check (don't re-check constantly) if (m_Graph.checkAutoAddRemoveBlocks && ShaderGraphPreferences.autoAddRemoveBlocks) { var activeBlocks = m_Graph.GetActiveBlocksForAllActiveTargets(); m_Graph.AddRemoveBlocksFromActiveList(activeBlocks); m_Graph.checkAutoAddRemoveBlocks = false; // We have to re-check any nodes views that need to be removed since we already handled this above. After leaving this function the states on m_Graph will be cleared so we'll lose track of removed blocks. HandleRemovedNodes(lookupTable); } UpdateBadges(); RegisterGraphViewCallbacks(); } void HandleRemovedNodes(Dictionary lookupTable = null) { foreach (var node in m_Graph.removedNodes) { node.UnregisterCallback(OnNodeChanged); IShaderNodeView nodeView = null; if (lookupTable != null && lookupTable.TryGetValue(node, out var nodeElement)) { nodeView = nodeElement as IShaderNodeView; } else { nodeView = m_GraphView.nodes.ToList().OfType().FirstOrDefault(p => p.node != null && p.node == node); } // When deleting a node make sure to clear any input observers switch (node) { case PropertyNode propertyNode: propertyNode.property.RemoveObserver(propertyNode); propertyNode.property.RemoveObserver(nodeView as IShaderInputObserver); break; case KeywordNode keywordNode: keywordNode.keyword.RemoveObserver(keywordNode); break; case DropdownNode dropdownNode: dropdownNode.dropdown.RemoveObserver(dropdownNode); break; } if (nodeView != null) { nodeView.Dispose(); if (node is BlockNode blockNode) { var context = m_GraphView.GetContext(blockNode.contextData); // blocknode may be floating and not actually in the stacknode's visual hierarchy. if (context.Contains(nodeView as Node)) { context.RemoveElement(nodeView as Node); } else { m_GraphView.RemoveElement((Node)nodeView); } } else { m_GraphView.RemoveElement((Node)nodeView); } if (node.group != null) { if (lookupTable.TryGetValue(node.group, out var shaderGroup)) m_GroupHashSet.Add((ShaderGroup)shaderGroup); } } } } void UpdateBadges() { if (!m_MessageManager.nodeMessagesChanged) return; foreach (var messageData in m_MessageManager.GetNodeMessages()) { var node = m_Graph.GetNodeFromId(messageData.Key); if (node == null || !(m_GraphView.GetNodeByGuid(node.objectId) is IShaderNodeView nodeView)) continue; if (messageData.Value.Count == 0) { nodeView.ClearMessage(); } else { var foundMessage = messageData.Value.First(); string messageString; if (foundMessage.line > 0) messageString = foundMessage.message + " at line " + foundMessage.line; else messageString = foundMessage.message; nodeView.AttachMessage(messageString, foundMessage.severity); } } } List m_GraphElementsTemp = new List(); void AddNode(AbstractMaterialNode node, bool usePrebuiltVisualGroupMap = false, Dictionary lookupTable = null) { var materialNode = node; Node nodeView; if (node is PropertyNode propertyNode) { var tokenNode = new PropertyNodeView(propertyNode, m_EdgeConnectorListener); m_GraphView.AddElement(tokenNode); nodeView = tokenNode; // Register node model and node view as observer of property propertyNode.property.AddObserver(propertyNode); propertyNode.property.AddObserver(tokenNode); } else if (node is BlockNode blockNode) { var blockNodeView = new MaterialNodeView { userData = blockNode }; blockNodeView.Initialize(blockNode, m_PreviewManager, m_EdgeConnectorListener, graphView); blockNodeView.MarkDirtyRepaint(); nodeView = blockNodeView; var context = m_GraphView.GetContext(blockNode.contextData); context.InsertBlock(blockNodeView); } else if (node is RedirectNodeData redirectNodeData) { var redirectNodeView = new RedirectNodeView { userData = redirectNodeData }; m_GraphView.AddElement(redirectNodeView); redirectNodeView.ConnectToData(materialNode, m_EdgeConnectorListener); nodeView = redirectNodeView; } else { var materialNodeView = new MaterialNodeView { userData = materialNode }; // For keywords and dropdowns, we only register the node model itself as an observer, // the material node view redraws completely on changes so it doesn't need to be an observer switch (node) { case KeywordNode keywordNode: keywordNode.keyword.AddObserver(keywordNode); break; case DropdownNode dropdownNode: dropdownNode.dropdown.AddObserver(dropdownNode); break; } m_GraphView.AddElement(materialNodeView); materialNodeView.Initialize(materialNode, m_PreviewManager, m_EdgeConnectorListener, graphView); m_ColorManager.UpdateNodeView(materialNodeView); nodeView = materialNodeView; } node.RegisterCallback(OnNodeChanged); nodeView.MarkDirtyRepaint(); if (m_SearchWindowProvider.nodeNeedsRepositioning && m_SearchWindowProvider.targetSlotReference.node == node) { m_SearchWindowProvider.nodeNeedsRepositioning = false; if (nodeView is IShaderNodeView shaderView && shaderView.FindPort(m_SearchWindowProvider.targetSlotReference, out var port)) { port.RegisterCallback(RepositionNode); return; } } if (materialNode.group != null) { if (usePrebuiltVisualGroupMap) { // cheaper way to add the node to groups it is in ShaderGroup groupView; visualGroupMap.TryGetValue(materialNode.group, out groupView); if (groupView != null) groupView.AddElement(nodeView); } else { // This should also work for sticky notes m_GraphElementsTemp.Clear(); m_GraphView.graphElements.ToList(m_GraphElementsTemp); foreach (var element in m_GraphElementsTemp) { if (element is ShaderGroup groupView && groupView.userData == materialNode.group) { groupView.AddElement(nodeView); } } } } lookupTable?.Add(node, nodeView); } private static Dictionary visualGroupMap = new Dictionary(); private static void AddToVisualGroupMap(GraphElement e) { if (e is ShaderGroup sg) { visualGroupMap.Add(sg.userData, sg); } } private static Action AddToVisualGroupMapAction = AddToVisualGroupMap; void BuildVisualGroupMap() { visualGroupMap.Clear(); m_GraphView.graphElements.ForEach(AddToVisualGroupMapAction); } private static readonly ProfilerMarker AddNodesMarker = new ProfilerMarker("AddNodes"); void AddNodes(IEnumerable nodes) { using (AddNodesMarker.Auto()) { BuildVisualGroupMap(); foreach (var node in nodes) { // Skip BlockNodes as we need to order them if (node is BlockNode) continue; AddNode(node, true); } visualGroupMap.Clear(); } } private static readonly ProfilerMarker AddBlocksMarker = new ProfilerMarker("AddBlocks"); void AddBlocks(IEnumerable blocks) { using (AddBlocksMarker.Auto()) { // As they can be reordered, we cannot be sure BlockNodes are deserialized in the same order as their stack position // To handle this we reorder the BlockNodes here to avoid having to reorder them on the fly as they are added foreach (var node in blocks.OrderBy(s => s.index)) { AddNode(node); } } } void AddGroup(GroupData groupData, Dictionary lookupTable = null) { ShaderGroup graphGroup = new ShaderGroup(); graphGroup.userData = groupData; graphGroup.title = groupData.title; graphGroup.SetPosition(new Rect(graphGroup.userData.position, Vector2.zero)); m_GraphView.AddElement(graphGroup); lookupTable?.Add(groupData, graphGroup); } void AddStickyNote(StickyNoteData stickyNoteData, Dictionary lookupTable = null) { var stickyNote = new StickyNote(stickyNoteData.position, m_Graph); stickyNote.userData = stickyNoteData; stickyNote.viewDataKey = stickyNoteData.objectId; stickyNote.title = stickyNoteData.title; stickyNote.contents = stickyNoteData.content; stickyNote.textSize = (StickyNote.TextSize)stickyNoteData.textSize; stickyNote.theme = (StickyNote.Theme)stickyNoteData.theme; stickyNote.userData.group = stickyNoteData.group; stickyNote.SetPosition(new Rect(stickyNote.userData.position)); m_GraphView.AddElement(stickyNote); lookupTable?.Add(stickyNoteData, stickyNote); // Add Sticky Note to group m_GraphElementsTemp.Clear(); m_GraphView.graphElements.ToList(m_GraphElementsTemp); if (stickyNoteData.group != null) { foreach (var element in m_GraphElementsTemp) { if (element is ShaderGroup groupView && groupView.userData == stickyNoteData.group) { groupView.AddElement(stickyNote); } } } } static void RepositionNode(GeometryChangedEvent evt) { var port = evt.target as ShaderPort; if (port == null) return; port.UnregisterCallback(RepositionNode); var nodeView = port.node as IShaderNodeView; if (nodeView == null) return; var offset = nodeView.gvNode.mainContainer.WorldToLocal(port.GetGlobalCenter() + new Vector3(3f, 3f, 0f)); var position = nodeView.gvNode.GetPosition(); position.position -= offset; nodeView.gvNode.SetPosition(position); var drawState = nodeView.node.drawState; drawState.position = position; nodeView.node.drawState = drawState; nodeView.gvNode.MarkDirtyRepaint(); port.MarkDirtyRepaint(); } private static Dictionary visualNodeMap = new Dictionary(); private static void AddToVisualNodeMap(Node n) { IShaderNodeView snv = n as IShaderNodeView; if (snv != null) visualNodeMap.Add(snv.node, snv); } private static Action AddToVisualNodeMapAction = AddToVisualNodeMap; void BuildVisualNodeMap() { visualNodeMap.Clear(); m_GraphView.nodes.ForEach(AddToVisualNodeMapAction); } private static readonly ProfilerMarker AddEdgesMarker = new ProfilerMarker("AddEdges"); void AddEdges(IEnumerable edges) { using (AddEdgesMarker.Auto()) { // fast way BuildVisualNodeMap(); foreach (IEdge edge in edges) { AddEdge(edge, true, false); } // apply the port update on every node foreach (IShaderNodeView nodeView in visualNodeMap.Values) { nodeView.gvNode.RefreshPorts(); nodeView.UpdatePortInputTypes(); } // cleanup temp data visualNodeMap.Clear(); } } Edge AddEdge(IEdge edge, bool useVisualNodeMap = false, bool updateNodePorts = true, Dictionary lookupTable = null) { var sourceNode = edge.outputSlot.node; if (sourceNode == null) { Debug.LogWarning("Source node is null"); return null; } var sourceSlot = sourceNode.FindOutputSlot(edge.outputSlot.slotId); var targetNode = edge.inputSlot.node; if (targetNode == null) { Debug.LogWarning("Target node is null"); return null; } var targetSlot = targetNode.FindInputSlot(edge.inputSlot.slotId); IShaderNodeView sourceNodeView = null; if (lookupTable != null) { lookupTable.TryGetValue(sourceNode, out var graphElement); sourceNodeView = (IShaderNodeView)graphElement; } else if (useVisualNodeMap) visualNodeMap.TryGetValue(sourceNode, out sourceNodeView); if (sourceNodeView == null) sourceNodeView = m_GraphView.nodes.ToList().OfType().FirstOrDefault(x => x.node == sourceNode); if (sourceNodeView != null) { sourceNodeView.FindPort(sourceSlot.slotReference, out var sourceAnchor); IShaderNodeView targetNodeView = null; if (lookupTable != null) { lookupTable.TryGetValue(targetNode, out var graphElement); targetNodeView = (IShaderNodeView)graphElement; } else if (useVisualNodeMap) visualNodeMap.TryGetValue(targetNode, out targetNodeView); if (targetNodeView == null) targetNodeView = m_GraphView.nodes.ToList().OfType().First(x => x.node == targetNode); targetNodeView.FindPort(targetSlot.slotReference, out var targetAnchor); var edgeView = new Edge { userData = edge, output = sourceAnchor, input = targetAnchor }; edgeView.RegisterCallback(OnMouseDown); edgeView.output.Connect(edgeView); edgeView.input.Connect(edgeView); m_GraphView.AddElement(edgeView); if (updateNodePorts) { sourceNodeView.gvNode.RefreshPorts(); targetNodeView.gvNode.RefreshPorts(); sourceNodeView.UpdatePortInputTypes(); targetNodeView.UpdatePortInputTypes(); } return edgeView; } return null; } void OnMouseDown(MouseDownEvent evt) { if (evt.button == (int)MouseButton.LeftMouse && evt.clickCount == 2) { if (evt.target is Edge edgeTarget) { Vector2 pos = evt.mousePosition; m_GraphView.CreateRedirectNode(pos, edgeTarget); } } } Stack m_NodeStack = new Stack(); string m_AssetName; void UpdateEdgeColors(HashSet nodeViews) { var nodeStack = m_NodeStack; nodeStack.Clear(); foreach (var nodeView in nodeViews) nodeStack.Push((Node)nodeView); PooledList edgesToUpdate = PooledList.Get(); while (nodeStack.Any()) { var nodeView = nodeStack.Pop(); if (nodeView is IShaderNodeView shaderNodeView) { shaderNodeView.UpdatePortInputTypes(); } foreach (var anchorView in nodeView.outputContainer.Children().OfType()) { foreach (var edgeView in anchorView.connections) { //update edges based on the active state of any modified nodes if (edgeView.input.node is MaterialNodeView inputNode && edgeView.output.node is MaterialNodeView outputNode) { //force redraw on update to prevent visual lag in the graph //Now has to be delayed a frame because setting port styles wont update colors till next frame edgesToUpdate.Add(edgeView); } //update edges based on dynamic vector length of any modified nodes var targetSlot = edgeView.input.GetSlot(); if (targetSlot.valueType == SlotValueType.DynamicVector || targetSlot.valueType == SlotValueType.DynamicMatrix || targetSlot.valueType == SlotValueType.Dynamic) { var connectedNodeView = edgeView.input.node; if (connectedNodeView != null && !nodeViews.Contains((IShaderNodeView)connectedNodeView)) { nodeStack.Push(connectedNodeView); nodeViews.Add((IShaderNodeView)connectedNodeView); } } } } foreach (var anchorView in nodeView.inputContainer.Query().ToList()) { var targetSlot = anchorView.GetSlot(); if (targetSlot.valueType != SlotValueType.DynamicVector) continue; foreach (var edgeView in anchorView.connections) { //update edges based on the active state of any modified nodes if (edgeView.input.node is MaterialNodeView inputNode && edgeView.output.node is MaterialNodeView outputNode) { //force redraw on update to prevent visual lag in the graph //Now has to be delayed a frame because setting port styles wont update colors till next frame edgesToUpdate.Add(edgeView); } //update edge color for upstream dynamic vector types var connectedNodeView = edgeView.output.node; if (connectedNodeView != null && !nodeViews.Contains((IShaderNodeView)connectedNodeView)) { nodeStack.Push(connectedNodeView); nodeViews.Add((IShaderNodeView)connectedNodeView); } } } } schedule.Execute(() => { foreach (Edge e in edgesToUpdate) { e.UpdateEdgeControl(); } edgesToUpdate.Dispose(); }).StartingIn(0); } void ApplySerializedWindowLayouts(GeometryChangedEvent evt) { UnregisterCallback(ApplySerializedWindowLayouts); ApplyMasterPreviewLayout(); m_BlackboardController.blackboard.DeserializeLayout(); m_InspectorView.DeserializeLayout(); } void ApplyMasterPreviewLayout() { // If a preview size was loaded in from saved user settings use that if (m_FloatingWindowsLayout.previewLayout.size.x > 0f && m_FloatingWindowsLayout.previewLayout.size.y > 0f) { previewManager.ResizeMasterPreview(m_FloatingWindowsLayout.previewLayout.size); } else // Use default specified in the stylesheet for master preview { m_FloatingWindowsLayout.previewLayout.size = m_MasterPreviewView.layout.size; } m_FloatingWindowsLayout.previewLayout.ApplyPosition(m_MasterPreviewView); m_MasterPreviewView.style.width = m_FloatingWindowsLayout.previewLayout.size.x; m_MasterPreviewView.style.height = m_FloatingWindowsLayout.previewLayout.size.y; m_MasterPreviewView.RegisterCallback(SerializeMasterPreviewLayout); } void SerializeMasterPreviewLayout(GeometryChangedEvent evt) { UpdateSerializedWindowLayout(); } void UpdateSerializedWindowLayout() { m_FloatingWindowsLayout.previewLayout.CalculateDockingCornerAndOffset(m_MasterPreviewView.layout, m_GraphView.layout); m_FloatingWindowsLayout.previewLayout.ClampToParentWindow(); blackboardController.blackboard.ClampToParentLayout(m_GraphView.layout); m_InspectorView.ClampToParentLayout(m_GraphView.layout); if (m_MasterPreviewView.visible) { m_FloatingWindowsLayout.previewLayout.size = m_MasterPreviewView.layout.size; } string serializedWindowLayout = JsonUtility.ToJson(m_FloatingWindowsLayout); EditorUserSettings.SetConfigValue(k_FloatingWindowsLayoutKey, serializedWindowLayout); } public void Dispose() { ShaderGraphPreferences.onZoomStepSizeChanged -= ResetZoom; if (m_GraphView != null) { saveRequested = null; saveAsRequested = null; convertToSubgraphRequested = null; showInProjectRequested = null; isCheckedOut = null; checkOut = null; foreach (var materialNodeView in m_GraphView.Query().ToList()) materialNodeView.Dispose(); foreach (var propertyNodeView in m_GraphView.Query().ToList()) propertyNodeView.Dispose(); foreach (var redirectNodeView in m_GraphView.Query().ToList()) redirectNodeView.Dispose(); foreach (var contextView in m_GraphView.Query().ToList()) contextView.Dispose(); foreach (var edge in m_GraphView.Query().ToList()) { edge.output = null; edge.input = null; } m_GraphView.nodeCreationRequest = null; m_GraphView = null; } m_BlackboardController?.Dispose(); m_BlackboardController = null; m_InspectorView?.Dispose(); m_InspectorView = null; if (previewManager != null) { previewManager.Dispose(); previewManager = null; } // Unload any static resources here Resources.UnloadAsset(ShaderPort.styleSheet); if (m_SearchWindowProvider != null) { m_SearchWindowProvider.Dispose(); m_SearchWindowProvider = null; } } } }