using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEditor.Experimental.UIElements; using UnityEditor.Experimental.UIElements.GraphView; using UnityEditor.Graphing.Util; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Experimental.UIElements; using UnityEditor.Graphing; using UnityEditor.ShaderGraph; using Object = UnityEngine.Object; using Edge = UnityEditor.Experimental.UIElements.GraphView.Edge; namespace UnityEditor.ShaderGraph.Drawing { public class MaterialGraphEditWindow : EditorWindow { [SerializeField] string m_Selected; [SerializeField] SerializableGraphObject m_GraphObject; GraphEditorView m_GraphEditorView; GraphEditorView graphEditorView { get { return m_GraphEditorView; } set { if (m_GraphEditorView != null) { m_GraphEditorView.RemoveFromHierarchy(); m_GraphEditorView.Dispose(); } m_GraphEditorView = value; if (m_GraphEditorView != null) { m_GraphEditorView.onUpdateAssetClick += UpdateAsset; m_GraphEditorView.onConvertToSubgraphClick += ToSubGraph; m_GraphEditorView.onShowInProjectClick += PingAsset; this.GetRootVisualContainer().Add(graphEditorView); } } } SerializableGraphObject 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; } } void Update() { if (graphObject == null && selectedGuid != null) { var guid = selectedGuid; selectedGuid = null; ChangeSelection(guid); } if (graphObject == null) { Close(); return; } var materialGraph = graphObject.graph as AbstractMaterialGraph; if (materialGraph == null) return; if (graphEditorView == null) { var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(selectedGuid)); graphEditorView = new GraphEditorView(this, materialGraph, asset.name) { persistenceKey = AssetDatabase.AssetPathToGUID(AssetDatabase.GUIDToAssetPath(selectedGuid)) }; } graphEditorView.HandleGraphChanges(); graphObject.graph.ClearChanges(); } void OnDisable() { graphEditorView = null; } void OnDestroy() { if (graphObject.isDirty && EditorUtility.DisplayDialog("Shader Graph Might Have Been Modified", "Do you want to save the changes you made in the shader graph?", "Save", "Don't Save")) UpdateAsset(); Undo.ClearUndo(graphObject); DestroyImmediate(graphObject); graphEditorView = null; } public void PingAsset() { if (selectedGuid != null) { var path = AssetDatabase.GUIDToAssetPath(selectedGuid); var asset = AssetDatabase.LoadAssetAtPath(path); EditorGUIUtility.PingObject(asset); } } public void UpdateAsset() { if (selectedGuid != null && graphObject != null) { var path = AssetDatabase.GUIDToAssetPath(selectedGuid); if (string.IsNullOrEmpty(path) || graphObject == null) return; if (m_GraphObject.graph.GetType() == typeof(MaterialGraph)) UpdateShaderGraphOnDisk(path); if (m_GraphObject.graph.GetType() == typeof(SubGraph)) UpdateAbstractSubgraphOnDisk(path); graphObject.isDirty = false; var windows = Resources.FindObjectsOfTypeAll(); foreach (var materialGraphEditWindow in windows) { materialGraphEditWindow.Rebuild(); } } } public void ToSubGraph() { var path = EditorUtility.SaveFilePanelInProject("Save subgraph", "New SubGraph", "ShaderSubGraph", ""); path = path.Replace(Application.dataPath, "Assets"); if (path.Length == 0) return; graphObject.RegisterCompleteObjectUndo("Convert To Subgraph"); var graphView = graphEditorView.graphView; var nodes = graphView.selection.OfType().Where(x => !(x.node is PropertyNode)).Select(x => x.node as INode).ToArray(); 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; var copyPasteGraph = new CopyPasteGraph( graphView.selection.OfType().Where(x => !(x.node is PropertyNode)).Select(x => x.node as INode), graphView.selection.OfType().Select(x => x.userData as IEdge)); var deserialized = CopyPasteGraph.FromJson(JsonUtility.ToJson(copyPasteGraph, false)); if (deserialized == null) return; var subGraph = new SubGraph(); 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); var nodeGuidMap = new Dictionary(); foreach (var node in deserialized.GetNodes()) { var oldGuid = node.guid; var newGuid = node.RewriteGuid(); nodeGuidMap[oldGuid] = newGuid; var drawState = node.drawState; drawState.position = new Rect(drawState.position.position - middle, drawState.position.size); node.drawState = drawState; subGraph.AddNode(node); } // figure out what needs remapping var externalOutputSlots = new List(); var externalInputSlots = new List(); foreach (var edge in deserialized.edges) { var outputSlot = edge.outputSlot; var inputSlot = edge.inputSlot; Guid remappedOutputNodeGuid; Guid remappedInputNodeGuid; var outputSlotExistsInSubgraph = nodeGuidMap.TryGetValue(outputSlot.nodeGuid, out remappedOutputNodeGuid); var inputSlotExistsInSubgraph = nodeGuidMap.TryGetValue(inputSlot.nodeGuid, out remappedInputNodeGuid); // pasting nice internal links! if (outputSlotExistsInSubgraph && inputSlotExistsInSubgraph) { var outputSlotRef = new SlotReference(remappedOutputNodeGuid, outputSlot.slotId); var inputSlotRef = new SlotReference(remappedInputNodeGuid, inputSlot.slotId); subGraph.Connect(outputSlotRef, inputSlotRef); } // one edge needs to go to outside world else if (outputSlotExistsInSubgraph) { externalInputSlots.Add(edge); } else if (inputSlotExistsInSubgraph) { externalOutputSlots.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>(); foreach (var group in uniqueIncomingEdges) { var sr = group.slotRef; var fromNode = graphObject.graph.GetNodeFromGuid(sr.nodeGuid); var fromSlot = fromNode.FindOutputSlot(sr.slotId); IShaderProperty prop; switch (fromSlot.concreteValueType) { case ConcreteSlotValueType.Texture2D: prop = new TextureShaderProperty(); 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 FloatShaderProperty(); break; default: throw new ArgumentOutOfRangeException(); } if (prop != null) { subGraph.AddShaderProperty(prop); var propNode = new PropertyNode(); { var drawState = propNode.drawState; drawState.position = new Rect(new Vector2(bounds.xMin - 300f, 0f), drawState.position.size); propNode.drawState = drawState; } subGraph.AddNode(propNode); propNode.propertyGuid = prop.guid; foreach (var edge in group.edges) { subGraph.Connect( new SlotReference(propNode.guid, PropertyNode.OutputSlotId), new SlotReference(nodeGuidMap[edge.inputSlot.nodeGuid], edge.inputSlot.slotId)); externalInputNeedingConnection.Add(new KeyValuePair(edge, prop)); } } } var uniqueOutgoingEdges = externalInputSlots.GroupBy( edge => edge.inputSlot, edge => edge, (key, edges) => new { slot = key, edges = edges.ToList() }); var externalOutputsNeedingConnection = new List>(); foreach (var group in uniqueOutgoingEdges) { var outputNode = subGraph.outputNode; var slotId = outputNode.AddSlot(); var inputSlotRef = new SlotReference(outputNode.guid, slotId); foreach (var edge in group.edges) { var newEdge = subGraph.Connect(new SlotReference(nodeGuidMap[edge.outputSlot.nodeGuid], edge.outputSlot.slotId), inputSlotRef); externalOutputsNeedingConnection.Add(new KeyValuePair(edge, newEdge)); } } File.WriteAllText(path, EditorJsonUtility.ToJson(subGraph)); AssetDatabase.ImportAsset(path); var loadedSubGraph = AssetDatabase.LoadAssetAtPath(path, typeof(MaterialSubGraphAsset)) as MaterialSubGraphAsset; 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; graphObject.graph.AddNode(subGraphNode); subGraphNode.subGraphAsset = loadedSubGraph; foreach (var edgeMap in externalInputNeedingConnection) { graphObject.graph.Connect(edgeMap.Key.outputSlot, new SlotReference(subGraphNode.guid, edgeMap.Value.guid.GetHashCode())); } foreach (var edgeMap in externalOutputsNeedingConnection) { graphObject.graph.Connect(new SlotReference(subGraphNode.guid, edgeMap.Value.inputSlot.slotId), edgeMap.Key.inputSlot); } graphObject.graph.RemoveElements( graphView.selection.OfType().Select(x => x.node as INode), Enumerable.Empty()); graphObject.graph.ValidateGraph(); } void UpdateAbstractSubgraphOnDisk(string path) where T : AbstractSubGraph { var graph = graphObject.graph as T; if (graph == null) return; File.WriteAllText(path, EditorJsonUtility.ToJson(graph, true)); AssetDatabase.ImportAsset(path); } void UpdateShaderGraphOnDisk(string path) { var graph = graphObject.graph as IShaderGraph; if (graph == null) return; List configuredTextures; graph.GetShader(Path.GetFileNameWithoutExtension(path), GenerationMode.ForReals, out configuredTextures); var shaderImporter = AssetImporter.GetAtPath(path) as ShaderGraphImporter; if (shaderImporter == null) return; File.WriteAllText(path, EditorJsonUtility.ToJson(graph, true)); shaderImporter.SaveAndReimport(); AssetDatabase.ImportAsset(path); } private void Rebuild() { if (graphObject != null && graphObject.graph != null) { var subNodes = graphObject.graph.GetNodes(); foreach (var node in subNodes) node.UpdateSlots(); } } public void ChangeSelection(string newSelectionGuid) { var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(newSelectionGuid)); if (asset == null) return; if (!EditorUtility.IsPersistent(asset)) return; if (selectedGuid == newSelectionGuid) return; var path = AssetDatabase.GetAssetPath(asset); var extension = Path.GetExtension(path); Type graphType; switch (extension) { case ".ShaderGraph": graphType = typeof(MaterialGraph); break; case ".ShaderSubGraph": graphType = typeof(SubGraph); break; default: return; } selectedGuid = newSelectionGuid; var textGraph = File.ReadAllText(path, Encoding.UTF8); graphObject = CreateInstance(); graphObject.hideFlags = HideFlags.HideAndDontSave; graphObject.graph = JsonUtility.FromJson(textGraph, graphType) as IGraph; graphObject.graph.OnEnable(); graphObject.graph.ValidateGraph(); graphEditorView = new GraphEditorView(this, m_GraphObject.graph as AbstractMaterialGraph, asset.name) { persistenceKey = AssetDatabase.GUIDToAssetPath(selectedGuid) }; graphEditorView.RegisterCallback(OnPostLayout); titleContent = new GUIContent(asset.name); Repaint(); } void OnPostLayout(PostLayoutEvent evt) { graphEditorView.UnregisterCallback(OnPostLayout); graphEditorView.graphView.FrameAll(); } } }