UnityGame/Library/PackageCache/com.unity.shadergraph/Editor/Drawing/MaterialGraphEditWindow.cs

1365 lines
55 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEditor.Graphing;
using UnityEditor.Graphing.Util;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using UnityEngine.Rendering;
using UnityEditor.UIElements;
using Edge = UnityEditor.Experimental.GraphView.Edge;
using UnityEditor.Experimental.GraphView;
using UnityEditor.ShaderGraph.Internal;
using UnityEditor.ShaderGraph.Serialization;
using UnityEngine.UIElements;
using UnityEditor.VersionControl;
using Unity.Profiling;
using UnityEngine.Assertions;
namespace UnityEditor.ShaderGraph.Drawing
{
class MaterialGraphEditWindow : EditorWindow
{
// For conversion to Sub Graph: keys for remembering the user's desired path
const string k_PrevSubGraphPathKey = "SHADER_GRAPH_CONVERT_TO_SUB_GRAPH_PATH";
const string k_PrevSubGraphPathDefaultValue = "?"; // Special character that NTFS does not allow, so that no directory could match it.
[SerializeField]
string m_Selected;
[SerializeField]
GraphObject m_GraphObject;
// this stores the contents of the file on disk, as of the last time we saved or loaded it from disk
[SerializeField]
string m_LastSerializedFileContents;
[NonSerialized]
HashSet<string> m_ChangedFileDependencyGUIDs = new HashSet<string>();
ColorSpace m_ColorSpace;
RenderPipelineAsset m_RenderPipelineAsset;
[NonSerialized]
bool m_FrameAllAfterLayout;
[NonSerialized]
bool m_HasError;
[NonSerialized]
bool m_ProTheme;
[NonSerialized]
int m_customInterpWarn;
[NonSerialized]
int m_customInterpErr;
[SerializeField]
bool m_AssetMaybeChangedOnDisk;
[SerializeField]
bool m_AssetMaybeDeleted;
internal bool WereWindowResourcesDisposed { get; private set; }
MessageManager m_MessageManager;
MessageManager messageManager
{
get { return m_MessageManager ?? (m_MessageManager = new MessageManager()); }
}
GraphEditorView m_GraphEditorView;
internal GraphEditorView graphEditorView
{
get { return m_GraphEditorView; }
private set
{
if (m_GraphEditorView != null)
{
m_GraphEditorView.RemoveFromHierarchy();
m_GraphEditorView.Dispose();
}
m_GraphEditorView = value;
if (m_GraphEditorView != null)
{
m_GraphEditorView.saveRequested += () => SaveAsset();
m_GraphEditorView.saveAsRequested += SaveAs;
m_GraphEditorView.convertToSubgraphRequested += ToSubGraph;
m_GraphEditorView.showInProjectRequested += PingAsset;
m_GraphEditorView.isCheckedOut += IsGraphAssetCheckedOut;
m_GraphEditorView.checkOut += CheckoutAsset;
m_GraphEditorView.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
m_FrameAllAfterLayout = true;
this.rootVisualElement.Add(graphEditorView);
}
}
}
internal GraphObject graphObject
{
get { return m_GraphObject; }
set
{
if (m_GraphObject != null)
DestroyImmediate(m_GraphObject);
m_GraphObject = value;
}
}
public string selectedGuid
{
get { return m_Selected; }
private set
{
m_Selected = value;
}
}
public string assetName
{
get { return titleContent.text; }
}
[field: NonSerialized]
internal bool isVisible { get; private set; }
bool AssetFileExists()
{
var assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
return File.Exists(assetPath);
}
// returns true when the graph has been successfully saved, or the user has indicated they are ok with discarding the local graph
// returns false when saving has failed
bool DisplayDeletedFromDiskDialog(bool reopen = true)
{
// first double check if we've actually been deleted
bool saved = false;
bool okToClose = false;
string originalAssetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
while (true)
{
int option = EditorUtility.DisplayDialogComplex(
"Graph removed from project",
"The file has been deleted or removed from the project folder.\n\n" +
originalAssetPath +
"\n\nWould you like to save your Graph Asset?",
"Save As...", "Cancel", "Discard Graph and Close Window");
if (option == 0)
{
string savedPath = SaveAsImplementation(false);
if (savedPath != null)
{
saved = true;
// either close or reopen the local window editor
graphObject = null;
selectedGuid = (reopen ? AssetDatabase.AssetPathToGUID(savedPath) : null);
break;
}
}
else if (option == 2)
{
okToClose = true;
graphObject = null;
selectedGuid = null;
break;
}
else if (option == 1)
{
// continue in deleted state...
break;
}
}
return (saved || okToClose);
}
void Update()
{
if (m_HasError)
return;
bool updateTitle = false;
if (m_AssetMaybeDeleted)
{
m_AssetMaybeDeleted = false;
if (AssetFileExists())
{
// it exists... just to be sure, let's check if it changed
m_AssetMaybeChangedOnDisk = true;
}
else
{
// it was really deleted, ask the user what to do
bool handled = DisplayDeletedFromDiskDialog(true);
}
updateTitle = true;
}
if (PlayerSettings.colorSpace != m_ColorSpace)
{
graphEditorView = null;
m_ColorSpace = PlayerSettings.colorSpace;
}
if (GraphicsSettings.currentRenderPipeline != m_RenderPipelineAsset)
{
graphEditorView = null;
m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
}
if (EditorGUIUtility.isProSkin != m_ProTheme)
{
if (graphObject != null && graphObject.graph != null)
{
updateTitle = true; // trigger icon swap
m_ProTheme = EditorGUIUtility.isProSkin;
}
}
bool revalidate = false;
if (m_customInterpWarn != ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold)
{
m_customInterpWarn = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold;
revalidate = true;
}
if (m_customInterpErr != ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold)
{
m_customInterpErr = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold;
revalidate = true;
}
if (revalidate)
{
graphEditorView?.graphView?.graph?.ValidateGraph();
}
if (m_AssetMaybeChangedOnDisk)
{
m_AssetMaybeChangedOnDisk = false;
// if we don't have any graph, then it doesn't really matter if the file on disk changed or not
// as we're going to reload it below anyways
if (graphObject?.graph != null)
{
// check if it actually did change on disk
if (FileOnDiskHasChanged())
{
// don't worry people about "losing changes" unless there are changes to lose
bool graphChanged = GraphHasChangedSinceLastSerialization();
if (EditorUtility.DisplayDialog(
"Graph has changed on disk",
AssetDatabase.GUIDToAssetPath(selectedGuid) + "\n\n" +
(graphChanged ? "Do you want to reload it and lose the changes made in the graph?" : "Do you want to reload it?"),
graphChanged ? "Discard Changes And Reload" : "Reload",
"Don't Reload"))
{
// clear graph, trigger reload
graphObject = null;
}
}
}
updateTitle = true;
}
try
{
if (graphObject == null && selectedGuid != null)
{
var guid = selectedGuid;
selectedGuid = null;
Initialize(guid);
}
if (graphObject == null)
{
Close();
return;
}
var materialGraph = graphObject.graph as GraphData;
if (materialGraph == null)
return;
if (graphEditorView == null)
{
messageManager.ClearAll();
materialGraph.messageManager = messageManager;
string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
string graphName = Path.GetFileNameWithoutExtension(assetPath);
graphEditorView = new GraphEditorView(this, materialGraph, messageManager, graphName)
{
viewDataKey = selectedGuid,
};
m_ColorSpace = PlayerSettings.colorSpace;
m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
graphObject.Validate();
// update blackboard title for the new graphEditorView
updateTitle = true;
}
if (m_ChangedFileDependencyGUIDs.Count > 0 && graphObject != null && graphObject.graph != null)
{
bool reloadedSomething = false;
foreach (var guid in m_ChangedFileDependencyGUIDs)
{
if (AssetDatabase.GUIDToAssetPath(guid) != null)
{
// update preview for changed textures
graphEditorView?.previewManager?.ReloadChangedFiles(guid);
}
}
var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>();
foreach (var subGraphNode in subGraphNodes)
{
var reloaded = subGraphNode.Reload(m_ChangedFileDependencyGUIDs);
reloadedSomething |= reloaded;
}
if (subGraphNodes.Count() > 0)
{
// Keywords always need to be updated to test against variant limit
// No Keywords may indicate removal and this may have now made the Graph valid again
// Need to validate Graph to clear errors in this case
materialGraph.OnKeywordChanged();
UpdateDropdownEntries();
materialGraph.OnDropdownChanged();
}
foreach (var customFunctionNode in graphObject.graph.GetNodes<CustomFunctionNode>())
{
var reloaded = customFunctionNode.Reload(m_ChangedFileDependencyGUIDs);
reloadedSomething |= reloaded;
}
// reloading files may change serialization
if (reloadedSomething)
{
updateTitle = true;
// may also need to re-run validation/concretization
graphObject.Validate();
}
m_ChangedFileDependencyGUIDs.Clear();
}
var wasUndoRedoPerformed = graphObject.wasUndoRedoPerformed;
if (wasUndoRedoPerformed)
{
graphEditorView.HandleGraphChanges(true);
graphObject.graph.ClearChanges();
graphObject.HandleUndoRedo();
}
if (graphObject.isDirty || wasUndoRedoPerformed)
{
updateTitle = true;
graphObject.isDirty = false;
hasUnsavedChanges = false;
}
// Called again to handle changes from deserialization in case an undo/redo was performed
graphEditorView.HandleGraphChanges(wasUndoRedoPerformed);
graphObject.graph.ClearChanges();
if (wasUndoRedoPerformed)
{
graphEditorView.inspectorView.RefreshInspectables();
}
if (updateTitle)
UpdateTitle();
}
catch (Exception e)
{
m_HasError = true;
m_GraphEditorView = null;
graphObject = null;
Debug.LogException(e);
throw;
}
}
public void ReloadSubGraphsOnNextUpdate(List<string> changedFileGUIDs)
{
foreach (var changedFileGUID in changedFileGUIDs)
{
m_ChangedFileDependencyGUIDs.Add(changedFileGUID);
}
}
void UpdateDropdownEntries()
{
var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>();
foreach (var subGraphNode in subGraphNodes)
{
var nodeView = graphEditorView.graphView.nodes.ToList().OfType<IShaderNodeView>()
.FirstOrDefault(p => p.node != null && p.node == subGraphNode);
if (nodeView != null)
{
nodeView.UpdateDropdownEntries();
}
}
}
void OnEnable()
{
this.SetAntiAliasing(4);
}
void OnDisable()
{
m_GraphEditorView?.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
m_GraphEditorView?.Dispose();
messageManager.ClearAll();
m_GraphEditorView = null;
m_GraphObject = null;
m_MessageManager = null;
m_RenderPipelineAsset = null;
Resources.UnloadUnusedAssets();
WereWindowResourcesDisposed = true;
}
// 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)
internal bool FileOnDiskHasChanged()
{
var currentFileJson = ReadAssetFile();
return !string.Equals(currentFileJson, m_LastSerializedFileContents, StringComparison.Ordinal);
}
// returns true only when the graph in this window would serialize different from the last time we loaded or saved it
internal bool GraphHasChangedSinceLastSerialization()
{
Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code
var currentGraphJson = MultiJson.Serialize(graphObject.graph);
return !string.Equals(currentGraphJson, m_LastSerializedFileContents, StringComparison.Ordinal);
}
// returns true only when saving the graph in this window would serialize different from the file on disk
internal bool GraphIsDifferentFromFileOnDisk()
{
Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code
var currentGraphJson = MultiJson.Serialize(graphObject.graph);
var currentFileJson = ReadAssetFile();
return !string.Equals(currentGraphJson, currentFileJson, StringComparison.Ordinal);
}
// NOTE: we're using the AssetPostprocessor Asset Import and Deleted callbacks as a proxy for detecting file changes
// We could probably replace both m_AssetMaybeDeleted and m_AssetMaybeChangedOnDisk with a combined "need to check the real status of the file" flag
public void CheckForChanges()
{
if (!m_AssetMaybeDeleted && graphObject?.graph != null)
{
m_AssetMaybeChangedOnDisk = true;
UpdateTitle();
}
}
public void AssetWasDeleted()
{
m_AssetMaybeDeleted = true;
UpdateTitle();
}
public void UpdateTitle()
{
string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
string shaderName = Path.GetFileNameWithoutExtension(assetPath);
// update blackboard title (before we add suffixes)
if (graphEditorView != null)
graphEditorView.assetName = shaderName;
// build the window title (with suffixes)
string title = shaderName;
if (graphObject?.graph == null)
title = title + " (nothing loaded)";
else
{
if (GraphHasChangedSinceLastSerialization())
{
hasUnsavedChanges = true;
// This is the message EditorWindow will show when prompting to close while dirty
saveChangesMessage = GetSaveChangesMessage();
}
else
{
hasUnsavedChanges = false;
saveChangesMessage = "";
}
if (!AssetFileExists())
title = title + " (deleted)";
}
// get window icon
bool isSubGraph = graphObject?.graph?.isSubGraph ?? false;
Texture2D icon;
{
string theme = EditorGUIUtility.isProSkin ? "_dark" : "_light";
if (isSubGraph)
icon = Resources.Load<Texture2D>("Icons/sg_subgraph_icon_gray" + theme);
else
icon = Resources.Load<Texture2D>("Icons/sg_graph_icon_gray" + theme);
}
titleContent = new GUIContent(title, icon);
}
void OnDestroy()
{
// Prompting the user if they want to close is mostly handled via the EditorWindow's system (hasUnsavedChanges).
// There's unfortunately a code path (Reload Window) that doesn't go through this path. The old logic is left
// here as a fallback to catch this. This has one edge case with "Reload Window" -> "Cancel" which will produce
// two shader graph windows: one unmodified (that the editor opens) and one modified (that we open below).
// we are closing the shadergraph window
MaterialGraphEditWindow newWindow = null;
if (!PromptSaveIfDirtyOnQuit())
{
// user does not want to close the window.
// we can't stop the close from this code path though..
// all we can do is open a new window and transfer our data to the new one to avoid losing it
// newWin = Instantiate<MaterialGraphEditWindow>(this);
newWindow = EditorWindow.CreateWindow<MaterialGraphEditWindow>(typeof(MaterialGraphEditWindow), typeof(SceneView));
newWindow.Initialize(this);
}
else
{
// the window is closing for good.. cleanup undo history for the graph object
Undo.ClearUndo(graphObject);
}
// Discard any unsaved modification on the generated material
if (graphObject && graphObject.materialArtifact && EditorUtility.IsDirty(graphObject.materialArtifact))
{
var material = new Material(AssetDatabase.LoadAssetAtPath<Shader>(AssetDatabase.GUIDToAssetPath(graphObject.AssetGuid)));
graphObject.materialArtifact.CopyPropertiesFromMaterial(material);
CoreUtils.Destroy(material);
}
graphObject = null;
graphEditorView = null;
// show new window if we have one
if (newWindow != null)
{
newWindow.Show();
newWindow.Focus();
}
}
public void PingAsset()
{
if (selectedGuid != null)
{
var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
EditorGUIUtility.PingObject(asset);
}
}
public bool IsGraphAssetCheckedOut()
{
if (selectedGuid != null)
{
var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (!AssetDatabase.IsOpenForEdit(asset, StatusQueryOptions.UseCachedIfPossible))
return false;
return true;
}
return false;
}
public void CheckoutAsset()
{
if (selectedGuid != null)
{
var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
Task task = Provider.Checkout(asset, CheckoutMode.Both);
task.Wait();
}
}
// returns true if the asset was succesfully saved
public bool SaveAsset()
{
bool saved = false;
if (selectedGuid != null && graphObject != null)
{
var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
if (string.IsNullOrEmpty(path) || graphObject == null)
return false;
if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, graphObject.graph.GetNodes<SubGraphNode>(), "Save"))
return false;
ShaderGraphAnalytics.SendShaderGraphEvent(selectedGuid, graphObject.graph);
var oldShader = AssetDatabase.LoadAssetAtPath<Shader>(path);
if (oldShader != null)
ShaderUtil.ClearShaderMessages(oldShader);
var newFileContents = FileUtilities.WriteShaderGraphToDisk(path, graphObject.graph);
if (newFileContents != null)
{
saved = true;
m_LastSerializedFileContents = newFileContents;
AssetDatabase.ImportAsset(path);
}
OnSaveGraph(path);
hasUnsavedChanges = false;
}
UpdateTitle();
return saved;
}
void OnSaveGraph(string path)
{
if (GraphData.onSaveGraph == null)
return;
if (graphObject.graph.isSubGraph)
return;
var shader = AssetDatabase.LoadAssetAtPath<Shader>(path);
if (shader == null)
return;
foreach (var target in graphObject.graph.activeTargets)
{
GraphData.onSaveGraph(shader, target.saveContext);
}
}
public void SaveAs()
{
SaveAsImplementation(true);
}
// returns the asset path the file was saved to, or NULL if nothing was saved
string SaveAsImplementation(bool openWhenSaved)
{
string savedFilePath = null;
if (selectedGuid != null && graphObject?.graph != null)
{
var oldFilePath = AssetDatabase.GUIDToAssetPath(selectedGuid);
if (string.IsNullOrEmpty(oldFilePath) || graphObject == null)
return null;
// The asset's name needs to be removed from the path, otherwise SaveFilePanel assumes it's a folder
string oldDirectory = Path.GetDirectoryName(oldFilePath);
var extension = graphObject.graph.isSubGraph ? ShaderSubGraphImporter.Extension : ShaderGraphImporter.Extension;
var newFilePath = EditorUtility.SaveFilePanelInProject("Save Graph As...", Path.GetFileNameWithoutExtension(oldFilePath), extension, "", oldDirectory);
newFilePath = newFilePath.Replace(Application.dataPath, "Assets");
if (newFilePath != oldFilePath)
{
if (!string.IsNullOrEmpty(newFilePath))
{
// If the newPath already exists, we are overwriting an existing file, and could be creating recursions. Let's check.
if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(newFilePath, graphObject.graph.GetNodes<SubGraphNode>(), "Save As"))
return null;
bool success = (FileUtilities.WriteShaderGraphToDisk(newFilePath, graphObject.graph) != null);
AssetDatabase.ImportAsset(newFilePath);
if (success)
{
if (openWhenSaved)
ShaderGraphImporterEditor.ShowGraphEditWindow(newFilePath);
OnSaveGraph(newFilePath);
savedFilePath = newFilePath;
}
}
}
else
{
// saving to the current path
if (SaveAsset())
{
graphObject.isDirty = false;
savedFilePath = oldFilePath;
}
}
}
return savedFilePath;
}
public void ToSubGraph()
{
var graphView = graphEditorView.graphView;
string path;
string sessionStateResult = SessionState.GetString(k_PrevSubGraphPathKey, k_PrevSubGraphPathDefaultValue);
string pathToOriginSG = Path.GetDirectoryName(AssetDatabase.GUIDToAssetPath(selectedGuid));
if (!sessionStateResult.Equals(k_PrevSubGraphPathDefaultValue))
{
path = sessionStateResult;
}
else
{
path = pathToOriginSG;
}
path = EditorUtility.SaveFilePanelInProject("Save Sub Graph", "New Shader Sub Graph", ShaderSubGraphImporter.Extension, "", path);
path = path.Replace(Application.dataPath, "Assets");
// Friendly warning that the user is generating a subgraph that would overwrite the one they are currently working on.
if (AssetDatabase.AssetPathToGUID(path) == selectedGuid)
{
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"))
{
path = "";
}
}
if (path.Length == 0)
return;
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();
// Convert To Subgraph could create recursive reference loops if the target path already exists
// Let's check for that here
if (!string.IsNullOrEmpty(path))
{
if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, nodes.OfType<SubGraphNode>(), "Convert To SubGraph"))
return;
}
graphObject.RegisterCompleteObjectUndo("Convert To Subgraph");
var bounds = Rect.MinMaxRect(float.PositiveInfinity, float.PositiveInfinity, float.NegativeInfinity, float.NegativeInfinity);
foreach (var node in nodes)
{
var center = node.drawState.position.center;
bounds = Rect.MinMaxRect(
Mathf.Min(bounds.xMin, center.x),
Mathf.Min(bounds.yMin, center.y),
Mathf.Max(bounds.xMax, center.x),
Mathf.Max(bounds.yMax, center.y));
}
var middle = bounds.center;
bounds.center = Vector2.zero;
// Collect graph inputs
var graphInputs = graphView.selection.OfType<SGBlackboardField>().Select(x => x.userData as ShaderInput);
var categories = graphView.selection.OfType<SGBlackboardCategory>().Select(x => x.userData as CategoryData);
// Collect the property nodes and get the corresponding properties
var propertyNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is PropertyNode)).Select(x => ((PropertyNode)x.node).property);
var metaProperties = graphView.graph.properties.Where(x => propertyNodes.Contains(x));
// Collect the keyword nodes and get the corresponding keywords
var keywordNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is KeywordNode)).Select(x => ((KeywordNode)x.node).keyword);
var dropdownNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is DropdownNode)).Select(x => ((DropdownNode)x.node).dropdown);
var metaKeywords = graphView.graph.keywords.Where(x => keywordNodes.Contains(x));
var metaDropdowns = graphView.graph.dropdowns.Where(x => dropdownNodes.Contains(x));
var copyPasteGraph = new CopyPasteGraph(graphView.selection.OfType<ShaderGroup>().Select(x => x.userData),
nodes,
graphView.selection.OfType<Edge>().Select(x => x.userData as Graphing.Edge),
graphInputs,
categories,
metaProperties,
metaKeywords,
metaDropdowns,
graphView.selection.OfType<StickyNote>().Select(x => x.userData),
true,
false);
// why do we serialize and deserialize only to make copies of everything in the steps below?
// is this just to clear out all non-serialized data?
var deserialized = CopyPasteGraph.FromJson(MultiJson.Serialize(copyPasteGraph), graphView.graph);
if (deserialized == null)
return;
var subGraph = new GraphData { isSubGraph = true, path = "Sub Graphs" };
var subGraphOutputNode = new SubGraphOutputNode();
{
var drawState = subGraphOutputNode.drawState;
drawState.position = new Rect(new Vector2(bounds.xMax + 200f, 0f), drawState.position.size);
subGraphOutputNode.drawState = drawState;
}
subGraph.AddNode(subGraphOutputNode);
subGraph.outputNode = subGraphOutputNode;
// Always copy deserialized keyword inputs
foreach (ShaderKeyword keyword in deserialized.metaKeywords)
{
var copiedInput = (ShaderKeyword)subGraph.AddCopyOfShaderInput(keyword);
// Update the keyword nodes that depends on the copied keyword
var dependentKeywordNodes = deserialized.GetNodes<KeywordNode>().Where(x => x.keyword == keyword);
foreach (var node in dependentKeywordNodes)
{
node.owner = graphView.graph;
node.keyword = copiedInput;
}
}
// Always copy deserialized dropdown inputs
foreach (ShaderDropdown dropdown in deserialized.metaDropdowns)
{
var copiedInput = (ShaderDropdown)subGraph.AddCopyOfShaderInput(dropdown);
// Update the dropdown nodes that depends on the copied dropdown
var dependentDropdownNodes = deserialized.GetNodes<DropdownNode>().Where(x => x.dropdown == dropdown);
foreach (var node in dependentDropdownNodes)
{
node.owner = graphView.graph;
node.dropdown = copiedInput;
}
}
foreach (GroupData groupData in deserialized.groups)
{
subGraph.CreateGroup(groupData);
}
foreach (var node in deserialized.GetNodes<AbstractMaterialNode>())
{
var drawState = node.drawState;
drawState.position = new Rect(drawState.position.position - middle, drawState.position.size);
node.drawState = drawState;
// Checking if the group guid is also being copied.
// If not then nullify that guid
if (node.group != null && !subGraph.groups.Contains(node.group))
{
node.group = null;
}
subGraph.AddNode(node);
}
foreach (var note in deserialized.stickyNotes)
{
if (note.group != null && !subGraph.groups.Contains(note.group))
{
note.group = null;
}
subGraph.AddStickyNote(note);
}
// figure out what needs remapping
var externalOutputSlots = new List<Graphing.Edge>();
var externalInputSlots = new List<Graphing.Edge>();
var passthroughSlots = new List<Graphing.Edge>();
foreach (var edge in deserialized.edges)
{
var outputSlot = edge.outputSlot;
var inputSlot = edge.inputSlot;
var outputSlotExistsInSubgraph = subGraph.ContainsNode(outputSlot.node);
var inputSlotExistsInSubgraph = subGraph.ContainsNode(inputSlot.node);
// pasting nice internal links!
if (outputSlotExistsInSubgraph && inputSlotExistsInSubgraph)
{
subGraph.Connect(outputSlot, inputSlot);
}
// one edge needs to go to outside world
else if (outputSlotExistsInSubgraph)
{
externalInputSlots.Add(edge);
}
else if (inputSlotExistsInSubgraph)
{
externalOutputSlots.Add(edge);
}
else
{
externalInputSlots.Add(edge);
externalOutputSlots.Add(edge);
passthroughSlots.Add(edge);
}
}
// Find the unique edges coming INTO the graph
var uniqueIncomingEdges = externalOutputSlots.GroupBy(
edge => edge.outputSlot,
edge => edge,
(key, edges) => new { slotRef = key, edges = edges.ToList() });
var externalInputNeedingConnection = new List<KeyValuePair<IEdge, AbstractShaderProperty>>();
var amountOfProps = uniqueIncomingEdges.Count();
const int height = 40;
const int subtractHeight = 20;
var propPos = new Vector2(0, -((amountOfProps / 2) + height) - subtractHeight);
var passthroughSlotRefLookup = new Dictionary<SlotReference, SlotReference>();
var passedInProperties = new Dictionary<AbstractShaderProperty, AbstractShaderProperty>();
foreach (var group in uniqueIncomingEdges)
{
var sr = group.slotRef;
var fromNode = sr.node;
var fromSlot = sr.slot;
var materialGraph = graphObject.graph;
var fromProperty = fromNode is PropertyNode fromPropertyNode
? materialGraph.properties.FirstOrDefault(p => p == fromPropertyNode.property)
: null;
AbstractShaderProperty prop;
if (fromProperty != null && passedInProperties.TryGetValue(fromProperty, out prop))
{
}
else
{
switch (fromSlot.concreteValueType)
{
case ConcreteSlotValueType.Texture2D:
prop = new Texture2DShaderProperty();
break;
case ConcreteSlotValueType.Texture2DArray:
prop = new Texture2DArrayShaderProperty();
break;
case ConcreteSlotValueType.Texture3D:
prop = new Texture3DShaderProperty();
break;
case ConcreteSlotValueType.Cubemap:
prop = new CubemapShaderProperty();
break;
case ConcreteSlotValueType.Vector4:
prop = new Vector4ShaderProperty();
break;
case ConcreteSlotValueType.Vector3:
prop = new Vector3ShaderProperty();
break;
case ConcreteSlotValueType.Vector2:
prop = new Vector2ShaderProperty();
break;
case ConcreteSlotValueType.Vector1:
prop = new Vector1ShaderProperty();
break;
case ConcreteSlotValueType.Boolean:
prop = new BooleanShaderProperty();
break;
case ConcreteSlotValueType.Matrix2:
prop = new Matrix2ShaderProperty();
break;
case ConcreteSlotValueType.Matrix3:
prop = new Matrix3ShaderProperty();
break;
case ConcreteSlotValueType.Matrix4:
prop = new Matrix4ShaderProperty();
break;
case ConcreteSlotValueType.SamplerState:
prop = new SamplerStateShaderProperty();
break;
case ConcreteSlotValueType.Gradient:
prop = new GradientShaderProperty();
break;
case ConcreteSlotValueType.VirtualTexture:
prop = new VirtualTextureShaderProperty()
{
// also copy the VT settings over from the original property (if there is one)
value = (fromProperty as VirtualTextureShaderProperty)?.value ?? new SerializableVirtualTexture()
};
break;
default:
throw new ArgumentOutOfRangeException();
}
var propName = fromProperty != null
? fromProperty.displayName
: fromSlot.concreteValueType.ToString();
prop.SetDisplayNameAndSanitizeForGraph(subGraph, propName);
if (fromProperty?.useCustomSlotLabel ?? false)
{
prop.useCustomSlotLabel = true;
prop.customSlotLabel = fromProperty.customSlotLabel;
}
subGraph.AddGraphInput(prop);
if (fromProperty != null)
{
passedInProperties.Add(fromProperty, prop);
}
}
var propNode = new PropertyNode();
{
var drawState = propNode.drawState;
drawState.position = new Rect(new Vector2(bounds.xMin - 300f, 0f) + propPos,
drawState.position.size);
propPos += new Vector2(0, height);
propNode.drawState = drawState;
}
subGraph.AddNode(propNode);
propNode.property = prop;
Vector2 avg = Vector2.zero;
foreach (var edge in group.edges)
{
if (passthroughSlots.Contains(edge) && !passthroughSlotRefLookup.ContainsKey(sr))
{
passthroughSlotRefLookup.Add(sr, new SlotReference(propNode, PropertyNode.OutputSlotId));
}
else
{
subGraph.Connect(
new SlotReference(propNode, PropertyNode.OutputSlotId),
edge.inputSlot);
int i;
var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList();
for (i = 0; i < inputs.Count; ++i)
{
if (inputs[i].slotReference.slotId == edge.inputSlot.slotId)
{
break;
}
}
avg += new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y + 30f * i);
}
//we collapse input properties so dont add edges that are already being added
if (!externalInputNeedingConnection.Any(x => x.Key.outputSlot.slot == edge.outputSlot.slot && x.Value == prop))
{
externalInputNeedingConnection.Add(new KeyValuePair<IEdge, AbstractShaderProperty>(edge, prop));
}
}
avg /= group.edges.Count;
var pos = avg - new Vector2(150f, 0f);
propNode.drawState = new DrawState()
{
position = new Rect(pos, propNode.drawState.position.size),
expanded = propNode.drawState.expanded
};
}
var uniqueOutgoingEdges = externalInputSlots.GroupBy(
edge => edge.outputSlot,
edge => edge,
(key, edges) => new { slot = key, edges = edges.ToList() });
var externalOutputsNeedingConnection = new List<KeyValuePair<IEdge, IEdge>>();
foreach (var group in uniqueOutgoingEdges)
{
var outputNode = subGraph.outputNode as SubGraphOutputNode;
AbstractMaterialNode node = group.edges[0].outputSlot.node;
MaterialSlot slot = node.FindSlot<MaterialSlot>(group.edges[0].outputSlot.slotId);
var slotId = outputNode.AddSlot(slot.concreteValueType);
var inputSlotRef = new SlotReference(outputNode, slotId);
foreach (var edge in group.edges)
{
var newEdge = subGraph.Connect(passthroughSlotRefLookup.TryGetValue(edge.outputSlot, out SlotReference remap) ? remap : edge.outputSlot, inputSlotRef);
externalOutputsNeedingConnection.Add(new KeyValuePair<IEdge, IEdge>(edge, newEdge));
}
}
if (FileUtilities.WriteShaderGraphToDisk(path, subGraph) != null)
AssetDatabase.ImportAsset(path);
// Store path for next time
if (!pathToOriginSG.Equals(Path.GetDirectoryName(path)))
{
SessionState.SetString(k_PrevSubGraphPathKey, Path.GetDirectoryName(path));
}
else
{
// Or continue to make it so that next time it will open up in the converted-from SG's directory
SessionState.EraseString(k_PrevSubGraphPathKey);
}
var loadedSubGraph = AssetDatabase.LoadAssetAtPath(path, typeof(SubGraphAsset)) as SubGraphAsset;
if (loadedSubGraph == null)
return;
var subGraphNode = new SubGraphNode();
var ds = subGraphNode.drawState;
ds.position = new Rect(middle - new Vector2(100f, 150f), Vector2.zero);
subGraphNode.drawState = ds;
// Add the subgraph into the group if the nodes was all in the same group group
var firstNode = copyPasteGraph.GetNodes<AbstractMaterialNode>().FirstOrDefault();
if (firstNode != null && copyPasteGraph.GetNodes<AbstractMaterialNode>().All(x => x.group == firstNode.group))
{
subGraphNode.group = firstNode.group;
}
subGraphNode.asset = loadedSubGraph;
graphObject.graph.AddNode(subGraphNode);
foreach (var edgeMap in externalInputNeedingConnection)
{
graphObject.graph.Connect(edgeMap.Key.outputSlot, new SlotReference(subGraphNode, edgeMap.Value.guid.GetHashCode()));
}
foreach (var edgeMap in externalOutputsNeedingConnection)
{
graphObject.graph.Connect(new SlotReference(subGraphNode, edgeMap.Value.inputSlot.slotId), edgeMap.Key.inputSlot);
}
graphObject.graph.RemoveElements(
graphView.selection.OfType<IShaderNodeView>().Select(x => x.node).Where(x => !(x is PropertyNode || x is SubGraphOutputNode) && x.allowedInSubGraph).ToArray(),
new IEdge[] { },
new GroupData[] { },
graphView.selection.OfType<StickyNote>().Select(x => x.userData).ToArray());
List<GraphElement> moved = new List<GraphElement>();
foreach (var nodeView in graphView.selection.OfType<IShaderNodeView>())
{
var node = nodeView.node;
if (graphView.graph.removedNodes.Contains(node) || node is SubGraphOutputNode)
{
continue;
}
var edges = graphView.graph.GetEdges(node);
int numEdges = edges.Count();
if (numEdges == 0)
{
graphView.graph.RemoveNode(node);
}
else if (numEdges == 1 && edges.First().inputSlot.node != node) //its an output edge
{
var edge = edges.First();
int i;
var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList();
for (i = 0; i < inputs.Count; ++i)
{
if (inputs[i].slotReference.slotId == edge.inputSlot.slotId)
{
break;
}
}
node.drawState = new DrawState()
{
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),
expanded = node.drawState.expanded
};
(nodeView as GraphElement).SetPosition(node.drawState.position);
}
}
graphObject.graph.ValidateGraph();
}
public void Initialize(MaterialGraphEditWindow other)
{
// create a new window that copies the entire editor state of an existing window
// this function is used to "reopen" an editor window that is closing, but where the user has canceled the close
// for example, if the graph of a closing window was dirty, but could not be saved
try
{
selectedGuid = other.selectedGuid;
graphObject = CreateInstance<GraphObject>();
graphObject.hideFlags = HideFlags.HideAndDontSave;
graphObject.graph = other.graphObject.graph;
graphObject.graph.messageManager = this.messageManager;
UpdateTitle();
Repaint();
}
catch (Exception e)
{
Debug.LogException(e);
m_HasError = true;
m_GraphEditorView = null;
graphObject = null;
throw;
}
}
private static readonly ProfilerMarker GraphLoadMarker = new ProfilerMarker("GraphLoad");
private static readonly ProfilerMarker CreateGraphEditorViewMarker = new ProfilerMarker("CreateGraphEditorView");
public void Initialize(string assetGuid)
{
try
{
m_ColorSpace = PlayerSettings.colorSpace;
m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
var asset = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(assetGuid));
if (asset == null)
return;
if (!EditorUtility.IsPersistent(asset))
return;
if (selectedGuid == assetGuid)
return;
var path = AssetDatabase.GetAssetPath(asset);
var extension = Path.GetExtension(path);
if (extension == null)
return;
// Path.GetExtension returns the extension prefixed with ".", so we remove it. We force lower case such that
// the comparison will be case-insensitive.
extension = extension.Substring(1).ToLowerInvariant();
bool isSubGraph;
switch (extension)
{
case ShaderGraphImporter.Extension:
isSubGraph = false;
break;
case ShaderSubGraphImporter.Extension:
isSubGraph = true;
break;
default:
return;
}
selectedGuid = assetGuid;
string graphName = Path.GetFileNameWithoutExtension(path);
using (GraphLoadMarker.Auto())
{
m_LastSerializedFileContents = File.ReadAllText(path, Encoding.UTF8);
graphObject = CreateInstance<GraphObject>();
graphObject.hideFlags = HideFlags.HideAndDontSave;
graphObject.graph = new GraphData
{
assetGuid = assetGuid,
isSubGraph = isSubGraph,
messageManager = messageManager
};
MultiJson.Deserialize(graphObject.graph, m_LastSerializedFileContents);
graphObject.graph.OnEnable();
graphObject.graph.ValidateGraph();
}
using (CreateGraphEditorViewMarker.Auto())
{
graphEditorView = new GraphEditorView(this, m_GraphObject.graph, messageManager, graphName)
{
viewDataKey = selectedGuid,
};
}
UpdateTitle();
Repaint();
}
catch (Exception)
{
m_HasError = true;
m_GraphEditorView = null;
graphObject = null;
throw;
}
}
// returns contents of the asset file, or null if any exception occurred
private string ReadAssetFile()
{
var filePath = AssetDatabase.GUIDToAssetPath(selectedGuid);
return FileUtilities.SafeReadAllText(filePath);
}
// 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)
// returns false when the user wants to cancel closing the window or application
internal bool PromptSaveIfDirtyOnQuit()
{
// only bother unless we've actually got data to preserve
if (graphObject?.graph != null)
{
// if the asset has been deleted, ask the user what to do
if (!AssetFileExists())
return DisplayDeletedFromDiskDialog(false);
// If there are unsaved modifications, ask the user what to do.
// If the editor has already handled this check we'll no longer have unsaved changes
// (either they saved or they discarded, both of which will set hasUnsavedChanges to false).
if (hasUnsavedChanges)
{
int option = EditorUtility.DisplayDialogComplex(
"Shader Graph Has Been Modified",
GetSaveChangesMessage(),
"Save", "Cancel", "Discard Changes");
if (option == 0) // save
{
return SaveAsset();
}
else if (option == 1) // cancel (or escape/close dialog)
{
return false;
}
else if (option == 2) // discard
{
return true;
}
}
}
return true;
}
private string GetSaveChangesMessage()
{
return "Do you want to save the changes you made in the Shader Graph?\n\n" +
AssetDatabase.GUIDToAssetPath(selectedGuid) +
"\n\nYour changes will be lost if you don't save them.";
}
public override void SaveChanges()
{
base.SaveChanges();
SaveAsset();
}
void OnGeometryChanged(GeometryChangedEvent evt)
{
if (graphEditorView == null)
return;
// this callback is only so we can run post-layout behaviors after the graph loads for the first time
// we immediately unregister it so it doesn't get called again
graphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
if (m_FrameAllAfterLayout)
graphEditorView.graphView.FrameAll();
m_FrameAllAfterLayout = false;
}
private void OnBecameVisible()
{
isVisible = true;
}
private void OnBecameInvisible()
{
isVisible = false;
}
}
}