A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Text;
6using UnityEngine;
7using UnityEditor.Graphing;
8using UnityEditor.Graphing.Util;
9using Debug = UnityEngine.Debug;
10using Object = UnityEngine.Object;
11using UnityEngine.Rendering;
12
13using UnityEditor.UIElements;
14using Edge = UnityEditor.Experimental.GraphView.Edge;
15using UnityEditor.Experimental.GraphView;
16using UnityEditor.ShaderGraph.Internal;
17using UnityEditor.ShaderGraph.Serialization;
18using UnityEngine.UIElements;
19using UnityEditor.VersionControl;
20
21using Unity.Profiling;
22using UnityEngine.Assertions;
23
24namespace UnityEditor.ShaderGraph.Drawing
25{
26 class MaterialGraphEditWindow : EditorWindow
27 {
28 // For conversion to Sub Graph: keys for remembering the user's desired path
29 const string k_PrevSubGraphPathKey = "SHADER_GRAPH_CONVERT_TO_SUB_GRAPH_PATH";
30 const string k_PrevSubGraphPathDefaultValue = "?"; // Special character that NTFS does not allow, so that no directory could match it.
31
32 [SerializeField]
33 string m_Selected;
34
35 [SerializeField]
36 GraphObject m_GraphObject;
37
38 // this stores the contents of the file on disk, as of the last time we saved or loaded it from disk
39 [SerializeField]
40 string m_LastSerializedFileContents;
41
42 [NonSerialized]
43 HashSet<string> m_ChangedFileDependencyGUIDs = new HashSet<string>();
44
45 ColorSpace m_ColorSpace;
46 RenderPipelineAsset m_RenderPipelineAsset;
47
48 [NonSerialized]
49 bool m_FrameAllAfterLayout;
50 [NonSerialized]
51 bool m_HasError;
52 [NonSerialized]
53 bool m_ProTheme;
54 [NonSerialized]
55 int m_customInterpWarn;
56 [NonSerialized]
57 int m_customInterpErr;
58
59 [SerializeField]
60 bool m_AssetMaybeChangedOnDisk;
61
62 [SerializeField]
63 bool m_AssetMaybeDeleted;
64
65 internal bool WereWindowResourcesDisposed { get; private set; }
66
67 MessageManager m_MessageManager;
68 MessageManager messageManager
69 {
70 get { return m_MessageManager ?? (m_MessageManager = new MessageManager()); }
71 }
72
73 GraphEditorView m_GraphEditorView;
74 internal GraphEditorView graphEditorView
75 {
76 get { return m_GraphEditorView; }
77 private set
78 {
79 if (m_GraphEditorView != null)
80 {
81 m_GraphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
82 m_GraphEditorView.RemoveFromHierarchy();
83 m_GraphEditorView.Dispose();
84 }
85
86 m_GraphEditorView = value;
87
88 if (m_GraphEditorView != null)
89 {
90 m_GraphEditorView.saveRequested += () => SaveAsset();
91 m_GraphEditorView.saveAsRequested += SaveAs;
92 m_GraphEditorView.convertToSubgraphRequested += ToSubGraph;
93 m_GraphEditorView.showInProjectRequested += PingAsset;
94 m_GraphEditorView.isCheckedOut += IsGraphAssetCheckedOut;
95 m_GraphEditorView.checkOut += CheckoutAsset;
96 m_GraphEditorView.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
97 m_FrameAllAfterLayout = true;
98 this.rootVisualElement.Add(graphEditorView);
99 }
100 }
101 }
102
103 internal GraphObject graphObject
104 {
105 get { return m_GraphObject; }
106 set
107 {
108 if (m_GraphObject != null)
109 DestroyImmediate(m_GraphObject);
110 m_GraphObject = value;
111 }
112 }
113
114 public string selectedGuid
115 {
116 get { return m_Selected; }
117 private set
118 {
119 m_Selected = value;
120 }
121 }
122
123 public string assetName
124 {
125 get { return titleContent.text; }
126 }
127
128 [field: NonSerialized]
129 internal bool isVisible { get; private set; }
130
131 bool AssetFileExists()
132 {
133 var assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
134 return File.Exists(assetPath);
135 }
136
137 // returns true when the graph has been successfully saved, or the user has indicated they are ok with discarding the local graph
138 // returns false when saving has failed
139 bool DisplayDeletedFromDiskDialog(bool reopen = true)
140 {
141 // first double check if we've actually been deleted
142 bool saved = false;
143 bool okToClose = false;
144 string originalAssetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
145
146 while (true)
147 {
148 int option = EditorUtility.DisplayDialogComplex(
149 "Graph removed from project",
150 "The file has been deleted or removed from the project folder.\n\n" +
151 originalAssetPath +
152 "\n\nWould you like to save your Graph Asset?",
153 "Save As...", "Cancel", "Discard Graph and Close Window");
154
155 if (option == 0)
156 {
157 string savedPath = SaveAsImplementation(false);
158 if (savedPath != null)
159 {
160 saved = true;
161
162 // either close or reopen the local window editor
163 graphObject = null;
164 selectedGuid = (reopen ? AssetDatabase.AssetPathToGUID(savedPath) : null);
165
166 break;
167 }
168 }
169 else if (option == 2)
170 {
171 okToClose = true;
172 graphObject = null;
173 selectedGuid = null;
174 break;
175 }
176 else if (option == 1)
177 {
178 // continue in deleted state...
179 break;
180 }
181 }
182
183 return (saved || okToClose);
184 }
185
186 void Update()
187 {
188 if (m_HasError)
189 return;
190
191 bool updateTitle = false;
192
193 if (m_AssetMaybeDeleted)
194 {
195 m_AssetMaybeDeleted = false;
196 if (AssetFileExists())
197 {
198 // it exists... just to be sure, let's check if it changed
199 m_AssetMaybeChangedOnDisk = true;
200 }
201 else
202 {
203 // it was really deleted, ask the user what to do
204 bool handled = DisplayDeletedFromDiskDialog(true);
205 }
206 updateTitle = true;
207 }
208
209 if (PlayerSettings.colorSpace != m_ColorSpace)
210 {
211 graphEditorView = null;
212 m_ColorSpace = PlayerSettings.colorSpace;
213 }
214
215 if (GraphicsSettings.currentRenderPipeline != m_RenderPipelineAsset)
216 {
217 graphEditorView = null;
218 m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
219 }
220
221 if (EditorGUIUtility.isProSkin != m_ProTheme)
222 {
223 if (graphObject != null && graphObject.graph != null)
224 {
225 updateTitle = true; // trigger icon swap
226 m_ProTheme = EditorGUIUtility.isProSkin;
227 }
228 }
229
230 bool revalidate = false;
231 if (m_customInterpWarn != ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold)
232 {
233 m_customInterpWarn = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold;
234 revalidate = true;
235 }
236 if (m_customInterpErr != ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold)
237 {
238 m_customInterpErr = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold;
239 revalidate = true;
240 }
241 if (revalidate)
242 {
243 graphEditorView?.graphView?.graph?.ValidateGraph();
244 }
245
246 if (m_AssetMaybeChangedOnDisk)
247 {
248 m_AssetMaybeChangedOnDisk = false;
249
250 // if we don't have any graph, then it doesn't really matter if the file on disk changed or not
251 // as we're going to reload it below anyways
252 if (graphObject?.graph != null)
253 {
254 // check if it actually did change on disk
255 if (FileOnDiskHasChanged())
256 {
257 // don't worry people about "losing changes" unless there are changes to lose
258 bool graphChanged = GraphHasChangedSinceLastSerialization();
259
260 if (EditorUtility.DisplayDialog(
261 "Graph has changed on disk",
262 AssetDatabase.GUIDToAssetPath(selectedGuid) + "\n\n" +
263 (graphChanged ? "Do you want to reload it and lose the changes made in the graph?" : "Do you want to reload it?"),
264 graphChanged ? "Discard Changes And Reload" : "Reload",
265 "Don't Reload"))
266 {
267 // clear graph, trigger reload
268 graphObject = null;
269 }
270 }
271 }
272 updateTitle = true;
273 }
274
275 try
276 {
277 if (graphObject == null && selectedGuid != null)
278 {
279 var guid = selectedGuid;
280 selectedGuid = null;
281 Initialize(guid);
282 }
283
284 if (graphObject == null)
285 {
286 Close();
287 return;
288 }
289
290 var materialGraph = graphObject.graph as GraphData;
291 if (materialGraph == null)
292 return;
293
294 if (graphEditorView == null)
295 {
296 messageManager.ClearAll();
297 materialGraph.messageManager = messageManager;
298 string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
299 string graphName = Path.GetFileNameWithoutExtension(assetPath);
300
301 graphEditorView = new GraphEditorView(this, materialGraph, messageManager, graphName)
302 {
303 viewDataKey = selectedGuid,
304 };
305 m_ColorSpace = PlayerSettings.colorSpace;
306 m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
307 graphObject.Validate();
308
309 // update blackboard title for the new graphEditorView
310 updateTitle = true;
311 }
312
313 if (m_ChangedFileDependencyGUIDs.Count > 0 && graphObject != null && graphObject.graph != null)
314 {
315 bool reloadedSomething = false;
316 foreach (var guid in m_ChangedFileDependencyGUIDs)
317 {
318 if (AssetDatabase.GUIDToAssetPath(guid) != null)
319 {
320 // update preview for changed textures
321 graphEditorView?.previewManager?.ReloadChangedFiles(guid);
322 }
323 }
324
325 var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>();
326 foreach (var subGraphNode in subGraphNodes)
327 {
328 var reloaded = subGraphNode.Reload(m_ChangedFileDependencyGUIDs);
329 reloadedSomething |= reloaded;
330 }
331 if (subGraphNodes.Count() > 0)
332 {
333 // Keywords always need to be updated to test against variant limit
334 // No Keywords may indicate removal and this may have now made the Graph valid again
335 // Need to validate Graph to clear errors in this case
336 materialGraph.OnKeywordChanged();
337
338 UpdateDropdownEntries();
339 materialGraph.OnDropdownChanged();
340 }
341 foreach (var customFunctionNode in graphObject.graph.GetNodes<CustomFunctionNode>())
342 {
343 var reloaded = customFunctionNode.Reload(m_ChangedFileDependencyGUIDs);
344 reloadedSomething |= reloaded;
345 }
346
347 // reloading files may change serialization
348 if (reloadedSomething)
349 {
350 updateTitle = true;
351
352 // may also need to re-run validation/concretization
353 graphObject.Validate();
354 }
355
356 m_ChangedFileDependencyGUIDs.Clear();
357 }
358
359 var wasUndoRedoPerformed = graphObject.wasUndoRedoPerformed;
360
361 if (wasUndoRedoPerformed)
362 {
363 graphEditorView.HandleGraphChanges(true);
364 graphObject.graph.ClearChanges();
365 graphObject.HandleUndoRedo();
366 }
367
368 if (graphObject.isDirty || wasUndoRedoPerformed)
369 {
370 updateTitle = true;
371 graphObject.isDirty = false;
372 hasUnsavedChanges = false;
373 }
374
375 // Called again to handle changes from deserialization in case an undo/redo was performed
376 graphEditorView.HandleGraphChanges(wasUndoRedoPerformed);
377 graphObject.graph.ClearChanges();
378
379 if (wasUndoRedoPerformed)
380 {
381 graphEditorView.inspectorView.RefreshInspectables();
382 }
383
384 if (updateTitle)
385 UpdateTitle();
386 }
387 catch (Exception e)
388 {
389 m_HasError = true;
390 m_GraphEditorView = null;
391 graphObject = null;
392 Debug.LogException(e);
393 throw;
394 }
395 }
396
397 public void ReloadSubGraphsOnNextUpdate(List<string> changedFileGUIDs)
398 {
399 foreach (var changedFileGUID in changedFileGUIDs)
400 {
401 m_ChangedFileDependencyGUIDs.Add(changedFileGUID);
402 }
403 }
404
405 void UpdateDropdownEntries()
406 {
407 var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>();
408 foreach (var subGraphNode in subGraphNodes)
409 {
410 var nodeView = graphEditorView.graphView.nodes.ToList().OfType<IShaderNodeView>()
411 .FirstOrDefault(p => p.node != null && p.node == subGraphNode);
412 if (nodeView != null)
413 {
414 nodeView.UpdateDropdownEntries();
415 }
416 }
417 }
418
419 void OnEnable()
420 {
421 this.SetAntiAliasing(4);
422 }
423
424 void OnDisable()
425 {
426 messageManager.ClearAll();
427
428 m_MessageManager = null;
429 m_RenderPipelineAsset = null;
430
431 Resources.UnloadUnusedAssets();
432
433 WereWindowResourcesDisposed = true;
434 }
435
436 // returns true only when the file on disk doesn't match the graph we last loaded or saved to disk (i.e. someone else changed it)
437 internal bool FileOnDiskHasChanged()
438 {
439 var currentFileJson = ReadAssetFile();
440 return !string.Equals(currentFileJson, m_LastSerializedFileContents, StringComparison.Ordinal);
441 }
442
443 // returns true only when the graph in this window would serialize different from the last time we loaded or saved it
444 internal bool GraphHasChangedSinceLastSerialization()
445 {
446 Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code
447 var currentGraphJson = MultiJson.Serialize(graphObject.graph);
448 return !string.Equals(currentGraphJson, m_LastSerializedFileContents, StringComparison.Ordinal);
449 }
450
451 // returns true only when saving the graph in this window would serialize different from the file on disk
452 internal bool GraphIsDifferentFromFileOnDisk()
453 {
454 Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code
455 var currentGraphJson = MultiJson.Serialize(graphObject.graph);
456 var currentFileJson = ReadAssetFile();
457 return !string.Equals(currentGraphJson, currentFileJson, StringComparison.Ordinal);
458 }
459
460 // NOTE: we're using the AssetPostprocessor Asset Import and Deleted callbacks as a proxy for detecting file changes
461 // We could probably replace both m_AssetMaybeDeleted and m_AssetMaybeChangedOnDisk with a combined "need to check the real status of the file" flag
462 public void CheckForChanges()
463 {
464 if (!m_AssetMaybeDeleted && graphObject?.graph != null)
465 {
466 m_AssetMaybeChangedOnDisk = true;
467 UpdateTitle();
468 }
469 }
470
471 public void AssetWasDeleted()
472 {
473 m_AssetMaybeDeleted = true;
474 UpdateTitle();
475 }
476
477 public void UpdateTitle()
478 {
479 string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
480 string shaderName = Path.GetFileNameWithoutExtension(assetPath);
481
482 // update blackboard title (before we add suffixes)
483 if (graphEditorView != null)
484 graphEditorView.assetName = shaderName;
485
486 // build the window title (with suffixes)
487 string title = shaderName;
488 if (graphObject?.graph == null)
489 title = title + " (nothing loaded)";
490 else
491 {
492 if (GraphHasChangedSinceLastSerialization())
493 {
494 hasUnsavedChanges = true;
495 // This is the message EditorWindow will show when prompting to close while dirty
496 saveChangesMessage = GetSaveChangesMessage();
497 }
498 else
499 {
500 hasUnsavedChanges = false;
501 saveChangesMessage = "";
502 }
503 if (!AssetFileExists())
504 title = title + " (deleted)";
505 }
506
507 // get window icon
508 bool isSubGraph = graphObject?.graph?.isSubGraph ?? false;
509 Texture2D icon;
510 {
511 string theme = EditorGUIUtility.isProSkin ? "_dark" : "_light";
512 if (isSubGraph)
513 icon = Resources.Load<Texture2D>("Icons/sg_subgraph_icon_gray" + theme);
514 else
515 icon = Resources.Load<Texture2D>("Icons/sg_graph_icon_gray" + theme);
516 }
517
518 titleContent = new GUIContent(title, icon);
519 }
520
521 void OnDestroy()
522 {
523 // Prompting the user if they want to close is mostly handled via the EditorWindow's system (hasUnsavedChanges).
524 // There's unfortunately a code path (Reload Window) that doesn't go through this path. The old logic is left
525 // here as a fallback to catch this. This has one edge case with "Reload Window" -> "Cancel" which will produce
526 // two shader graph windows: one unmodified (that the editor opens) and one modified (that we open below).
527
528 // we are closing the shadergraph window
529 MaterialGraphEditWindow newWindow = null;
530 if (!PromptSaveIfDirtyOnQuit())
531 {
532 // user does not want to close the window.
533 // we can't stop the close from this code path though..
534 // all we can do is open a new window and transfer our data to the new one to avoid losing it
535 // newWin = Instantiate<MaterialGraphEditWindow>(this);
536 newWindow = EditorWindow.CreateWindow<MaterialGraphEditWindow>(typeof(MaterialGraphEditWindow), typeof(SceneView));
537 newWindow.Initialize(this);
538 }
539 else
540 {
541 // the window is closing for good.. cleanup undo history for the graph object
542 Undo.ClearUndo(graphObject);
543 }
544
545 // Discard any unsaved modification on the generated material
546 if (graphObject && graphObject.materialArtifact && EditorUtility.IsDirty(graphObject.materialArtifact))
547 {
548 var material = new Material(AssetDatabase.LoadAssetAtPath<Shader>(AssetDatabase.GUIDToAssetPath(graphObject.AssetGuid)));
549 graphObject.materialArtifact.CopyPropertiesFromMaterial(material);
550 CoreUtils.Destroy(material);
551 }
552
553 graphObject = null;
554 graphEditorView = null;
555
556 // show new window if we have one
557 if (newWindow != null)
558 {
559 newWindow.Show();
560 newWindow.Focus();
561 }
562 }
563
564 public void PingAsset()
565 {
566 if (selectedGuid != null)
567 {
568 var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
569 var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
570 EditorGUIUtility.PingObject(asset);
571 }
572 }
573
574 public bool IsGraphAssetCheckedOut()
575 {
576 if (selectedGuid != null)
577 {
578 var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
579 var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
580 if (!AssetDatabase.IsOpenForEdit(asset, StatusQueryOptions.UseCachedIfPossible))
581 return false;
582
583 return true;
584 }
585
586 return false;
587 }
588
589 public void CheckoutAsset()
590 {
591 if (selectedGuid != null)
592 {
593 var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
594 var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
595 Task task = Provider.Checkout(asset, CheckoutMode.Both);
596 task.Wait();
597 }
598 }
599
600 // returns true if the asset was succesfully saved
601 public bool SaveAsset()
602 {
603 bool saved = false;
604
605 if (selectedGuid != null && graphObject != null)
606 {
607 var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
608 if (string.IsNullOrEmpty(path) || graphObject == null)
609 return false;
610
611 if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, graphObject.graph.GetNodes<SubGraphNode>(), "Save"))
612 return false;
613
614 ShaderGraphAnalytics.SendShaderGraphEvent(selectedGuid, graphObject.graph);
615
616 var oldShader = AssetDatabase.LoadAssetAtPath<Shader>(path);
617 if (oldShader != null)
618 ShaderUtil.ClearShaderMessages(oldShader);
619
620 var newFileContents = FileUtilities.WriteShaderGraphToDisk(path, graphObject.graph);
621 if (newFileContents != null)
622 {
623 saved = true;
624 m_LastSerializedFileContents = newFileContents;
625 AssetDatabase.ImportAsset(path);
626 }
627
628 OnSaveGraph(path);
629 hasUnsavedChanges = false;
630 }
631
632 UpdateTitle();
633
634 return saved;
635 }
636
637 void OnSaveGraph(string path)
638 {
639 if (GraphData.onSaveGraph == null)
640 return;
641
642 if (graphObject.graph.isSubGraph)
643 return;
644
645 var shader = AssetDatabase.LoadAssetAtPath<Shader>(path);
646 if (shader == null)
647 return;
648
649 foreach (var target in graphObject.graph.activeTargets)
650 {
651 GraphData.onSaveGraph(shader, target.saveContext);
652 }
653 }
654
655 public void SaveAs()
656 {
657 SaveAsImplementation(true);
658 }
659
660 // returns the asset path the file was saved to, or NULL if nothing was saved
661 string SaveAsImplementation(bool openWhenSaved)
662 {
663 string savedFilePath = null;
664
665 if (selectedGuid != null && graphObject?.graph != null)
666 {
667 var oldFilePath = AssetDatabase.GUIDToAssetPath(selectedGuid);
668 if (string.IsNullOrEmpty(oldFilePath) || graphObject == null)
669 return null;
670
671 // The asset's name needs to be removed from the path, otherwise SaveFilePanel assumes it's a folder
672 string oldDirectory = Path.GetDirectoryName(oldFilePath);
673
674 var extension = graphObject.graph.isSubGraph ? ShaderSubGraphImporter.Extension : ShaderGraphImporter.Extension;
675 var newFilePath = EditorUtility.SaveFilePanelInProject("Save Graph As...", Path.GetFileNameWithoutExtension(oldFilePath), extension, "", oldDirectory);
676 newFilePath = newFilePath.Replace(Application.dataPath, "Assets");
677
678 if (newFilePath != oldFilePath)
679 {
680 if (!string.IsNullOrEmpty(newFilePath))
681 {
682 // If the newPath already exists, we are overwriting an existing file, and could be creating recursions. Let's check.
683 if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(newFilePath, graphObject.graph.GetNodes<SubGraphNode>(), "Save As"))
684 return null;
685
686 bool success = (FileUtilities.WriteShaderGraphToDisk(newFilePath, graphObject.graph) != null);
687 AssetDatabase.ImportAsset(newFilePath);
688 if (success)
689 {
690 if (openWhenSaved)
691 ShaderGraphImporterEditor.ShowGraphEditWindow(newFilePath);
692 OnSaveGraph(newFilePath);
693 savedFilePath = newFilePath;
694 }
695 }
696 }
697 else
698 {
699 // saving to the current path
700 if (SaveAsset())
701 {
702 graphObject.isDirty = false;
703 savedFilePath = oldFilePath;
704 }
705 }
706 }
707 return savedFilePath;
708 }
709
710 public void ToSubGraph()
711 {
712 var graphView = graphEditorView.graphView;
713
714 string path;
715 string sessionStateResult = SessionState.GetString(k_PrevSubGraphPathKey, k_PrevSubGraphPathDefaultValue);
716 string pathToOriginSG = Path.GetDirectoryName(AssetDatabase.GUIDToAssetPath(selectedGuid));
717
718 if (!sessionStateResult.Equals(k_PrevSubGraphPathDefaultValue))
719 {
720 path = sessionStateResult;
721 }
722 else
723 {
724 path = pathToOriginSG;
725 }
726
727 path = EditorUtility.SaveFilePanelInProject("Save Sub Graph", "New Shader Sub Graph", ShaderSubGraphImporter.Extension, "", path);
728 path = path.Replace(Application.dataPath, "Assets");
729
730 // Friendly warning that the user is generating a subgraph that would overwrite the one they are currently working on.
731 if (AssetDatabase.AssetPathToGUID(path) == selectedGuid)
732 {
733 if (!EditorUtility.DisplayDialog("Overwrite Current Subgraph", "Do you want to overwrite this Sub Graph that you are currently working on? You cannot undo this operation.", "Yes", "Cancel"))
734 {
735 path = "";
736 }
737 }
738
739 if (path.Length == 0)
740 return;
741
742 var nodes = graphView.selection.OfType<IShaderNodeView>().Where(x => !(x.node is PropertyNode || x.node is SubGraphOutputNode)).Select(x => x.node).Where(x => x.allowedInSubGraph).ToArray();
743
744 // Convert To Subgraph could create recursive reference loops if the target path already exists
745 // Let's check for that here
746 if (!string.IsNullOrEmpty(path))
747 {
748 if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, nodes.OfType<SubGraphNode>(), "Convert To SubGraph"))
749 return;
750 }
751
752 graphObject.RegisterCompleteObjectUndo("Convert To Subgraph");
753
754 var bounds = Rect.MinMaxRect(float.PositiveInfinity, float.PositiveInfinity, float.NegativeInfinity, float.NegativeInfinity);
755 foreach (var node in nodes)
756 {
757 var center = node.drawState.position.center;
758 bounds = Rect.MinMaxRect(
759 Mathf.Min(bounds.xMin, center.x),
760 Mathf.Min(bounds.yMin, center.y),
761 Mathf.Max(bounds.xMax, center.x),
762 Mathf.Max(bounds.yMax, center.y));
763 }
764 var middle = bounds.center;
765 bounds.center = Vector2.zero;
766
767 // Collect graph inputs
768 var graphInputs = graphView.selection.OfType<SGBlackboardField>().Select(x => x.userData as ShaderInput);
769 var categories = graphView.selection.OfType<SGBlackboardCategory>().Select(x => x.userData as CategoryData);
770
771 // Collect the property nodes and get the corresponding properties
772 var propertyNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is PropertyNode)).Select(x => ((PropertyNode)x.node).property);
773 var metaProperties = graphView.graph.properties.Where(x => propertyNodes.Contains(x));
774
775 // Collect the keyword nodes and get the corresponding keywords
776 var keywordNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is KeywordNode)).Select(x => ((KeywordNode)x.node).keyword);
777 var dropdownNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is DropdownNode)).Select(x => ((DropdownNode)x.node).dropdown);
778
779 var metaKeywords = graphView.graph.keywords.Where(x => keywordNodes.Contains(x));
780 var metaDropdowns = graphView.graph.dropdowns.Where(x => dropdownNodes.Contains(x));
781
782 var copyPasteGraph = new CopyPasteGraph(graphView.selection.OfType<ShaderGroup>().Select(x => x.userData),
783 nodes,
784 graphView.selection.OfType<Edge>().Select(x => x.userData as Graphing.Edge),
785 graphInputs,
786 categories,
787 metaProperties,
788 metaKeywords,
789 metaDropdowns,
790 graphView.selection.OfType<StickyNote>().Select(x => x.userData),
791 true,
792 false);
793
794 // why do we serialize and deserialize only to make copies of everything in the steps below?
795 // is this just to clear out all non-serialized data?
796 var deserialized = CopyPasteGraph.FromJson(MultiJson.Serialize(copyPasteGraph), graphView.graph);
797 if (deserialized == null)
798 return;
799
800 var subGraph = new GraphData { isSubGraph = true, path = "Sub Graphs" };
801 var subGraphOutputNode = new SubGraphOutputNode();
802 {
803 var drawState = subGraphOutputNode.drawState;
804 drawState.position = new Rect(new Vector2(bounds.xMax + 200f, 0f), drawState.position.size);
805 subGraphOutputNode.drawState = drawState;
806 }
807 subGraph.AddNode(subGraphOutputNode);
808 subGraph.outputNode = subGraphOutputNode;
809
810 // Always copy deserialized keyword inputs
811 foreach (ShaderKeyword keyword in deserialized.metaKeywords)
812 {
813 var copiedInput = (ShaderKeyword)subGraph.AddCopyOfShaderInput(keyword);
814
815 // Update the keyword nodes that depends on the copied keyword
816 var dependentKeywordNodes = deserialized.GetNodes<KeywordNode>().Where(x => x.keyword == keyword);
817 foreach (var node in dependentKeywordNodes)
818 {
819 node.owner = graphView.graph;
820 node.keyword = copiedInput;
821 }
822 }
823
824 // Always copy deserialized dropdown inputs
825 foreach (ShaderDropdown dropdown in deserialized.metaDropdowns)
826 {
827 var copiedInput = (ShaderDropdown)subGraph.AddCopyOfShaderInput(dropdown);
828
829 // Update the dropdown nodes that depends on the copied dropdown
830 var dependentDropdownNodes = deserialized.GetNodes<DropdownNode>().Where(x => x.dropdown == dropdown);
831 foreach (var node in dependentDropdownNodes)
832 {
833 node.owner = graphView.graph;
834 node.dropdown = copiedInput;
835 }
836 }
837
838 foreach (GroupData groupData in deserialized.groups)
839 {
840 subGraph.CreateGroup(groupData);
841 }
842
843 foreach (var node in deserialized.GetNodes<AbstractMaterialNode>())
844 {
845 var drawState = node.drawState;
846 drawState.position = new Rect(drawState.position.position - middle, drawState.position.size);
847 node.drawState = drawState;
848
849 // Checking if the group guid is also being copied.
850 // If not then nullify that guid
851 if (node.group != null && !subGraph.groups.Contains(node.group))
852 {
853 node.group = null;
854 }
855
856 subGraph.AddNode(node);
857 }
858
859 foreach (var note in deserialized.stickyNotes)
860 {
861 if (note.group != null && !subGraph.groups.Contains(note.group))
862 {
863 note.group = null;
864 }
865
866 subGraph.AddStickyNote(note);
867 }
868
869 // figure out what needs remapping
870 var externalOutputSlots = new List<Graphing.Edge>();
871 var externalInputSlots = new List<Graphing.Edge>();
872 var passthroughSlots = new List<Graphing.Edge>();
873 foreach (var edge in deserialized.edges)
874 {
875 var outputSlot = edge.outputSlot;
876 var inputSlot = edge.inputSlot;
877
878 var outputSlotExistsInSubgraph = subGraph.ContainsNode(outputSlot.node);
879 var inputSlotExistsInSubgraph = subGraph.ContainsNode(inputSlot.node);
880
881 // pasting nice internal links!
882 if (outputSlotExistsInSubgraph && inputSlotExistsInSubgraph)
883 {
884 subGraph.Connect(outputSlot, inputSlot);
885 }
886 // one edge needs to go to outside world
887 else if (outputSlotExistsInSubgraph)
888 {
889 externalInputSlots.Add(edge);
890 }
891 else if (inputSlotExistsInSubgraph)
892 {
893 externalOutputSlots.Add(edge);
894 }
895 else
896 {
897 externalInputSlots.Add(edge);
898 externalOutputSlots.Add(edge);
899 passthroughSlots.Add(edge);
900 }
901 }
902
903 // Find the unique edges coming INTO the graph
904 var uniqueIncomingEdges = externalOutputSlots.GroupBy(
905 edge => edge.outputSlot,
906 edge => edge,
907 (key, edges) => new { slotRef = key, edges = edges.ToList() });
908
909 var externalInputNeedingConnection = new List<KeyValuePair<IEdge, AbstractShaderProperty>>();
910
911 var amountOfProps = uniqueIncomingEdges.Count();
912 const int height = 40;
913 const int subtractHeight = 20;
914 var propPos = new Vector2(0, -((amountOfProps / 2) + height) - subtractHeight);
915
916 var passthroughSlotRefLookup = new Dictionary<SlotReference, SlotReference>();
917
918 var passedInProperties = new Dictionary<AbstractShaderProperty, AbstractShaderProperty>();
919 foreach (var group in uniqueIncomingEdges)
920 {
921 var sr = group.slotRef;
922 var fromNode = sr.node;
923 var fromSlot = sr.slot;
924
925 var materialGraph = graphObject.graph;
926 var fromProperty = fromNode is PropertyNode fromPropertyNode
927 ? materialGraph.properties.FirstOrDefault(p => p == fromPropertyNode.property)
928 : null;
929
930 AbstractShaderProperty prop;
931 if (fromProperty != null && passedInProperties.TryGetValue(fromProperty, out prop))
932 {
933 }
934 else
935 {
936 switch (fromSlot.concreteValueType)
937 {
938 case ConcreteSlotValueType.Texture2D:
939 prop = new Texture2DShaderProperty();
940 break;
941 case ConcreteSlotValueType.Texture2DArray:
942 prop = new Texture2DArrayShaderProperty();
943 break;
944 case ConcreteSlotValueType.Texture3D:
945 prop = new Texture3DShaderProperty();
946 break;
947 case ConcreteSlotValueType.Cubemap:
948 prop = new CubemapShaderProperty();
949 break;
950 case ConcreteSlotValueType.Vector4:
951 prop = new Vector4ShaderProperty();
952 break;
953 case ConcreteSlotValueType.Vector3:
954 prop = new Vector3ShaderProperty();
955 break;
956 case ConcreteSlotValueType.Vector2:
957 prop = new Vector2ShaderProperty();
958 break;
959 case ConcreteSlotValueType.Vector1:
960 prop = new Vector1ShaderProperty();
961 break;
962 case ConcreteSlotValueType.Boolean:
963 prop = new BooleanShaderProperty();
964 break;
965 case ConcreteSlotValueType.Matrix2:
966 prop = new Matrix2ShaderProperty();
967 break;
968 case ConcreteSlotValueType.Matrix3:
969 prop = new Matrix3ShaderProperty();
970 break;
971 case ConcreteSlotValueType.Matrix4:
972 prop = new Matrix4ShaderProperty();
973 break;
974 case ConcreteSlotValueType.SamplerState:
975 prop = new SamplerStateShaderProperty();
976 break;
977 case ConcreteSlotValueType.Gradient:
978 prop = new GradientShaderProperty();
979 break;
980 case ConcreteSlotValueType.VirtualTexture:
981 prop = new VirtualTextureShaderProperty()
982 {
983 // also copy the VT settings over from the original property (if there is one)
984 value = (fromProperty as VirtualTextureShaderProperty)?.value ?? new SerializableVirtualTexture()
985 };
986 break;
987 default:
988 throw new ArgumentOutOfRangeException();
989 }
990
991 var propName = fromProperty != null
992 ? fromProperty.displayName
993 : fromSlot.concreteValueType.ToString();
994 prop.SetDisplayNameAndSanitizeForGraph(subGraph, propName);
995
996 if (fromProperty?.useCustomSlotLabel ?? false)
997 {
998 prop.useCustomSlotLabel = true;
999 prop.customSlotLabel = fromProperty.customSlotLabel;
1000 }
1001
1002 subGraph.AddGraphInput(prop);
1003 if (fromProperty != null)
1004 {
1005 passedInProperties.Add(fromProperty, prop);
1006 }
1007 }
1008
1009 var propNode = new PropertyNode();
1010 {
1011 var drawState = propNode.drawState;
1012 drawState.position = new Rect(new Vector2(bounds.xMin - 300f, 0f) + propPos,
1013 drawState.position.size);
1014 propPos += new Vector2(0, height);
1015 propNode.drawState = drawState;
1016 }
1017 subGraph.AddNode(propNode);
1018 propNode.property = prop;
1019
1020
1021 Vector2 avg = Vector2.zero;
1022 foreach (var edge in group.edges)
1023 {
1024 if (passthroughSlots.Contains(edge) && !passthroughSlotRefLookup.ContainsKey(sr))
1025 {
1026 passthroughSlotRefLookup.Add(sr, new SlotReference(propNode, PropertyNode.OutputSlotId));
1027 }
1028 else
1029 {
1030 subGraph.Connect(
1031 new SlotReference(propNode, PropertyNode.OutputSlotId),
1032 edge.inputSlot);
1033
1034 int i;
1035 var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList();
1036
1037 for (i = 0; i < inputs.Count; ++i)
1038 {
1039 if (inputs[i].slotReference.slotId == edge.inputSlot.slotId)
1040 {
1041 break;
1042 }
1043 }
1044 avg += new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y + 30f * i);
1045 }
1046 //we collapse input properties so dont add edges that are already being added
1047 if (!externalInputNeedingConnection.Any(x => x.Key.outputSlot.slot == edge.outputSlot.slot && x.Value == prop))
1048 {
1049 externalInputNeedingConnection.Add(new KeyValuePair<IEdge, AbstractShaderProperty>(edge, prop));
1050 }
1051 }
1052 avg /= group.edges.Count;
1053 var pos = avg - new Vector2(150f, 0f);
1054 propNode.drawState = new DrawState()
1055 {
1056 position = new Rect(pos, propNode.drawState.position.size),
1057 expanded = propNode.drawState.expanded
1058 };
1059 }
1060
1061 var uniqueOutgoingEdges = externalInputSlots.GroupBy(
1062 edge => edge.outputSlot,
1063 edge => edge,
1064 (key, edges) => new { slot = key, edges = edges.ToList() });
1065
1066 var externalOutputsNeedingConnection = new List<KeyValuePair<IEdge, IEdge>>();
1067 foreach (var group in uniqueOutgoingEdges)
1068 {
1069 var outputNode = subGraph.outputNode as SubGraphOutputNode;
1070
1071 AbstractMaterialNode node = group.edges[0].outputSlot.node;
1072 MaterialSlot slot = node.FindSlot<MaterialSlot>(group.edges[0].outputSlot.slotId);
1073 var slotId = outputNode.AddSlot(slot.concreteValueType);
1074
1075 var inputSlotRef = new SlotReference(outputNode, slotId);
1076
1077 foreach (var edge in group.edges)
1078 {
1079 var newEdge = subGraph.Connect(passthroughSlotRefLookup.TryGetValue(edge.outputSlot, out SlotReference remap) ? remap : edge.outputSlot, inputSlotRef);
1080 externalOutputsNeedingConnection.Add(new KeyValuePair<IEdge, IEdge>(edge, newEdge));
1081 }
1082 }
1083
1084 if (FileUtilities.WriteShaderGraphToDisk(path, subGraph) != null)
1085 AssetDatabase.ImportAsset(path);
1086
1087 // Store path for next time
1088 if (!pathToOriginSG.Equals(Path.GetDirectoryName(path)))
1089 {
1090 SessionState.SetString(k_PrevSubGraphPathKey, Path.GetDirectoryName(path));
1091 }
1092 else
1093 {
1094 // Or continue to make it so that next time it will open up in the converted-from SG's directory
1095 SessionState.EraseString(k_PrevSubGraphPathKey);
1096 }
1097
1098 var loadedSubGraph = AssetDatabase.LoadAssetAtPath(path, typeof(SubGraphAsset)) as SubGraphAsset;
1099 if (loadedSubGraph == null)
1100 return;
1101
1102 var subGraphNode = new SubGraphNode();
1103 var ds = subGraphNode.drawState;
1104 ds.position = new Rect(middle - new Vector2(100f, 150f), Vector2.zero);
1105 subGraphNode.drawState = ds;
1106
1107 // Add the subgraph into the group if the nodes was all in the same group group
1108 var firstNode = copyPasteGraph.GetNodes<AbstractMaterialNode>().FirstOrDefault();
1109 if (firstNode != null && copyPasteGraph.GetNodes<AbstractMaterialNode>().All(x => x.group == firstNode.group))
1110 {
1111 subGraphNode.group = firstNode.group;
1112 }
1113
1114 subGraphNode.asset = loadedSubGraph;
1115 graphObject.graph.AddNode(subGraphNode);
1116
1117 foreach (var edgeMap in externalInputNeedingConnection)
1118 {
1119 graphObject.graph.Connect(edgeMap.Key.outputSlot, new SlotReference(subGraphNode, edgeMap.Value.guid.GetHashCode()));
1120 }
1121
1122 foreach (var edgeMap in externalOutputsNeedingConnection)
1123 {
1124 graphObject.graph.Connect(new SlotReference(subGraphNode, edgeMap.Value.inputSlot.slotId), edgeMap.Key.inputSlot);
1125 }
1126
1127 graphObject.graph.RemoveElements(
1128 graphView.selection.OfType<IShaderNodeView>().Select(x => x.node).Where(x => !(x is PropertyNode || x is SubGraphOutputNode) && x.allowedInSubGraph).ToArray(),
1129 new IEdge[] { },
1130 new GroupData[] { },
1131 graphView.selection.OfType<StickyNote>().Select(x => x.userData).ToArray());
1132
1133 List<GraphElement> moved = new List<GraphElement>();
1134 foreach (var nodeView in graphView.selection.OfType<IShaderNodeView>())
1135 {
1136 var node = nodeView.node;
1137 if (graphView.graph.removedNodes.Contains(node) || node is SubGraphOutputNode)
1138 {
1139 continue;
1140 }
1141
1142 var edges = graphView.graph.GetEdges(node);
1143 int numEdges = edges.Count();
1144 if (numEdges == 0)
1145 {
1146 graphView.graph.RemoveNode(node);
1147 }
1148 else if (numEdges == 1 && edges.First().inputSlot.node != node) //its an output edge
1149 {
1150 var edge = edges.First();
1151 int i;
1152 var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList();
1153 for (i = 0; i < inputs.Count; ++i)
1154 {
1155 if (inputs[i].slotReference.slotId == edge.inputSlot.slotId)
1156 {
1157 break;
1158 }
1159 }
1160 node.drawState = new DrawState()
1161 {
1162 position = new Rect(new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y) - new Vector2(150f, -30f * i), node.drawState.position.size),
1163 expanded = node.drawState.expanded
1164 };
1165 (nodeView as GraphElement).SetPosition(node.drawState.position);
1166 }
1167 }
1168 graphObject.graph.ValidateGraph();
1169 }
1170
1171 public void Initialize(MaterialGraphEditWindow other)
1172 {
1173 // create a new window that copies the entire editor state of an existing window
1174 // this function is used to "reopen" an editor window that is closing, but where the user has canceled the close
1175 // for example, if the graph of a closing window was dirty, but could not be saved
1176 try
1177 {
1178 selectedGuid = other.selectedGuid;
1179
1180 graphObject = CreateInstance<GraphObject>();
1181 graphObject.hideFlags = HideFlags.HideAndDontSave;
1182 graphObject.graph = other.graphObject.graph;
1183
1184 graphObject.graph.messageManager = this.messageManager;
1185
1186 UpdateTitle();
1187
1188 Repaint();
1189 }
1190 catch (Exception e)
1191 {
1192 Debug.LogException(e);
1193 m_HasError = true;
1194 m_GraphEditorView = null;
1195 graphObject = null;
1196 throw;
1197 }
1198 }
1199
1200 private static readonly ProfilerMarker GraphLoadMarker = new ProfilerMarker("GraphLoad");
1201 private static readonly ProfilerMarker CreateGraphEditorViewMarker = new ProfilerMarker("CreateGraphEditorView");
1202 public void Initialize(string assetGuid)
1203 {
1204 try
1205 {
1206 m_ColorSpace = PlayerSettings.colorSpace;
1207 m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
1208
1209 var asset = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(assetGuid));
1210 if (asset == null)
1211 return;
1212
1213 if (!EditorUtility.IsPersistent(asset))
1214 return;
1215
1216 if (selectedGuid == assetGuid)
1217 return;
1218
1219
1220 var path = AssetDatabase.GetAssetPath(asset);
1221 var extension = Path.GetExtension(path);
1222 if (extension == null)
1223 return;
1224 // Path.GetExtension returns the extension prefixed with ".", so we remove it. We force lower case such that
1225 // the comparison will be case-insensitive.
1226 extension = extension.Substring(1).ToLowerInvariant();
1227 bool isSubGraph;
1228 switch (extension)
1229 {
1230 case ShaderGraphImporter.Extension:
1231 isSubGraph = false;
1232 break;
1233 case ShaderSubGraphImporter.Extension:
1234 isSubGraph = true;
1235 break;
1236 default:
1237 return;
1238 }
1239
1240 selectedGuid = assetGuid;
1241 string graphName = Path.GetFileNameWithoutExtension(path);
1242
1243 using (GraphLoadMarker.Auto())
1244 {
1245 m_LastSerializedFileContents = File.ReadAllText(path, Encoding.UTF8);
1246 graphObject = CreateInstance<GraphObject>();
1247 graphObject.hideFlags = HideFlags.HideAndDontSave;
1248 graphObject.graph = new GraphData
1249 {
1250 assetGuid = assetGuid,
1251 isSubGraph = isSubGraph,
1252 messageManager = messageManager
1253 };
1254 MultiJson.Deserialize(graphObject.graph, m_LastSerializedFileContents);
1255 graphObject.graph.OnEnable();
1256 graphObject.graph.ValidateGraph();
1257 }
1258
1259 using (CreateGraphEditorViewMarker.Auto())
1260 {
1261 graphEditorView = new GraphEditorView(this, m_GraphObject.graph, messageManager, graphName)
1262 {
1263 viewDataKey = selectedGuid,
1264 };
1265 }
1266
1267 UpdateTitle();
1268
1269 Repaint();
1270 }
1271 catch (Exception)
1272 {
1273 m_HasError = true;
1274 m_GraphEditorView = null;
1275 graphObject = null;
1276 throw;
1277 }
1278 }
1279
1280 // returns contents of the asset file, or null if any exception occurred
1281 private string ReadAssetFile()
1282 {
1283 var filePath = AssetDatabase.GUIDToAssetPath(selectedGuid);
1284 return FileUtilities.SafeReadAllText(filePath);
1285 }
1286
1287 // returns true when the user is OK with closing the window or application (either they've saved dirty content, or are ok with losing it)
1288 // returns false when the user wants to cancel closing the window or application
1289 internal bool PromptSaveIfDirtyOnQuit()
1290 {
1291 // only bother unless we've actually got data to preserve
1292 if (graphObject?.graph != null)
1293 {
1294 // if the asset has been deleted, ask the user what to do
1295 if (!AssetFileExists())
1296 return DisplayDeletedFromDiskDialog(false);
1297
1298 // If there are unsaved modifications, ask the user what to do.
1299 // If the editor has already handled this check we'll no longer have unsaved changes
1300 // (either they saved or they discarded, both of which will set hasUnsavedChanges to false).
1301 if (hasUnsavedChanges)
1302 {
1303 int option = EditorUtility.DisplayDialogComplex(
1304 "Shader Graph Has Been Modified",
1305 GetSaveChangesMessage(),
1306 "Save", "Cancel", "Discard Changes");
1307
1308 if (option == 0) // save
1309 {
1310 return SaveAsset();
1311 }
1312 else if (option == 1) // cancel (or escape/close dialog)
1313 {
1314 return false;
1315 }
1316 else if (option == 2) // discard
1317 {
1318 return true;
1319 }
1320 }
1321 }
1322 return true;
1323 }
1324
1325 private string GetSaveChangesMessage()
1326 {
1327 return "Do you want to save the changes you made in the Shader Graph?\n\n" +
1328 AssetDatabase.GUIDToAssetPath(selectedGuid) +
1329 "\n\nYour changes will be lost if you don't save them.";
1330 }
1331
1332 public override void SaveChanges()
1333 {
1334 base.SaveChanges();
1335 SaveAsset();
1336 }
1337
1338 void OnGeometryChanged(GeometryChangedEvent evt)
1339 {
1340 if (graphEditorView == null)
1341 return;
1342
1343 // this callback is only so we can run post-layout behaviors after the graph loads for the first time
1344 // we immediately unregister it so it doesn't get called again
1345 graphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
1346 if (m_FrameAllAfterLayout)
1347 graphEditorView.graphView.FrameAll();
1348 m_FrameAllAfterLayout = false;
1349 }
1350
1351 private void OnBecameVisible()
1352 {
1353 isVisible = true;
1354 }
1355
1356 private void OnBecameInvisible()
1357 {
1358 isVisible = false;
1359 }
1360 }
1361}