UnityGame/Library/PackageCache/com.unity.shadergraph/Editor/Drawing/Views/MaterialNodeView.cs

850 lines
31 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEditor.Graphing;
using UnityEditor.Graphing.Util;
using UnityEditor.ShaderGraph.Drawing.Controls;
using UnityEngine.Rendering;
using UnityEditor.Experimental.GraphView;
using UnityEditor.Rendering;
using UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using Node = UnityEditor.Experimental.GraphView.Node;
namespace UnityEditor.ShaderGraph.Drawing
{
sealed class MaterialNodeView : Node, IShaderNodeView, IInspectable
{
PreviewRenderData m_PreviewRenderData;
Image m_PreviewImage;
// Remove this after updated to the correct API call has landed in trunk. ------------
VisualElement m_TitleContainer;
VisualElement m_PreviewContainer;
VisualElement m_PreviewFiller;
VisualElement m_ControlItems;
VisualElement m_ControlsDivider;
VisualElement m_DropdownItems;
VisualElement m_DropdownsDivider;
Action m_UnregisterAll;
IEdgeConnectorListener m_ConnectorListener;
MaterialGraphView m_GraphView;
public string inspectorTitle => $"{node.name} (Node)";
public void Initialize(AbstractMaterialNode inNode, PreviewManager previewManager, IEdgeConnectorListener connectorListener, MaterialGraphView graphView)
{
styleSheets.Add(Resources.Load<StyleSheet>("Styles/MaterialNodeView"));
styleSheets.Add(Resources.Load<StyleSheet>($"Styles/ColorMode"));
AddToClassList("MaterialNode");
if (inNode == null)
return;
var contents = this.Q("contents");
m_GraphView = graphView;
mainContainer.style.overflow = StyleKeyword.None; // Override explicit style set in base class
m_ConnectorListener = connectorListener;
node = inNode;
viewDataKey = node.objectId;
UpdateTitle();
// Add disabled overlay
Add(new VisualElement() { name = "disabledOverlay", pickingMode = PickingMode.Ignore });
// Add controls container
var controlsContainer = new VisualElement { name = "controls" };
{
m_ControlsDivider = new VisualElement { name = "divider" };
m_ControlsDivider.AddToClassList("horizontal");
controlsContainer.Add(m_ControlsDivider);
m_ControlItems = new VisualElement { name = "items" };
controlsContainer.Add(m_ControlItems);
// Instantiate control views from node
foreach (var propertyInfo in node.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
foreach (IControlAttribute attribute in propertyInfo.GetCustomAttributes(typeof(IControlAttribute), false))
m_ControlItems.Add(attribute.InstantiateControl(node, propertyInfo));
}
if (m_ControlItems.childCount > 0)
contents.Add(controlsContainer);
// Add dropdowns container
if (inNode is SubGraphNode)
{
var dropdownContainer = new VisualElement { name = "dropdowns" };
{
m_DropdownsDivider = new VisualElement { name = "divider" };
m_DropdownsDivider.AddToClassList("horizontal");
dropdownContainer.Add(m_DropdownsDivider);
m_DropdownItems = new VisualElement { name = "items" };
dropdownContainer.Add(m_DropdownItems);
UpdateDropdownEntries();
}
contents.Add(dropdownContainer);
}
if (node.hasPreview)
{
// Add actual preview which floats on top of the node
m_PreviewContainer = new VisualElement
{
name = "previewContainer",
style = { overflow = Overflow.Hidden },
pickingMode = PickingMode.Ignore
};
m_PreviewImage = new Image
{
name = "preview",
pickingMode = PickingMode.Ignore,
image = Texture2D.whiteTexture,
};
{
// Add preview collapse button on top of preview
var collapsePreviewButton = new VisualElement { name = "collapse" };
collapsePreviewButton.Add(new VisualElement { name = "icon" });
collapsePreviewButton.AddManipulator(new Clickable(() =>
{
SetPreviewExpandedStateOnSelection(false);
}));
m_PreviewImage.Add(collapsePreviewButton);
}
m_PreviewContainer.Add(m_PreviewImage);
// Hook up preview image to preview manager
m_PreviewRenderData = previewManager.GetPreviewRenderData(inNode);
m_PreviewRenderData.onPreviewChanged += UpdatePreviewTexture;
UpdatePreviewTexture();
// Add fake preview which pads out the node to provide space for the floating preview
m_PreviewFiller = new VisualElement { name = "previewFiller" };
m_PreviewFiller.AddToClassList("expanded");
{
var previewDivider = new VisualElement { name = "divider" };
previewDivider.AddToClassList("horizontal");
m_PreviewFiller.Add(previewDivider);
var expandPreviewButton = new VisualElement { name = "expand" };
expandPreviewButton.Add(new VisualElement { name = "icon" });
expandPreviewButton.AddManipulator(new Clickable(() =>
{
SetPreviewExpandedStateOnSelection(true);
}));
m_PreviewFiller.Add(expandPreviewButton);
}
contents.Add(m_PreviewFiller);
UpdatePreviewExpandedState(node.previewExpanded);
}
base.expanded = node.drawState.expanded;
AddSlots(node.GetSlots<MaterialSlot>());
switch (node)
{
case SubGraphNode:
RegisterCallback<MouseDownEvent>(OnSubGraphDoubleClick);
m_UnregisterAll += () => { UnregisterCallback<MouseDownEvent>(OnSubGraphDoubleClick); };
break;
}
m_TitleContainer = this.Q("title");
if (node is BlockNode blockData)
{
AddToClassList("blockData");
m_TitleContainer.RemoveFromHierarchy();
}
else
{
SetPosition(new Rect(node.drawState.position.x, node.drawState.position.y, 0, 0));
}
// Update active state
SetActive(node.isActive);
// Register OnMouseHover callbacks for node highlighting
RegisterCallback<MouseEnterEvent>(OnMouseHover);
m_UnregisterAll += () => { UnregisterCallback<MouseEnterEvent>(OnMouseHover); };
RegisterCallback<MouseLeaveEvent>(OnMouseHover);
m_UnregisterAll += () => { UnregisterCallback<MouseLeaveEvent>(OnMouseHover); };
ShaderGraphPreferences.onAllowDeprecatedChanged += UpdateTitle;
}
public bool FindPort(SlotReference slotRef, out ShaderPort port)
{
port = inputContainer.Query<ShaderPort>().ToList()
.Concat(outputContainer.Query<ShaderPort>().ToList())
.First(p => p.slot.slotReference.Equals(slotRef));
return port != null;
}
public void AttachMessage(string errString, ShaderCompilerMessageSeverity severity)
{
ClearMessage();
IconBadge badge;
if (severity == ShaderCompilerMessageSeverity.Error)
{
badge = IconBadge.CreateError(errString);
}
else
{
badge = IconBadge.CreateComment(errString);
}
Add(badge);
if (node is BlockNode)
{
FindPort(node.GetSlotReference(0), out var port);
badge.AttachTo(port.parent, SpriteAlignment.RightCenter);
}
else
{
badge.AttachTo(m_TitleContainer, SpriteAlignment.RightCenter);
}
}
public void SetActive(bool state)
{
// Setup
var disabledString = "disabled";
var portDisabledString = "inactive";
if (!state)
{
// Add elements to disabled class list
AddToClassList(disabledString);
var inputPorts = inputContainer.Query<ShaderPort>().ToList();
foreach (var port in inputPorts)
{
port.AddToClassList(portDisabledString);
}
var outputPorts = outputContainer.Query<ShaderPort>().ToList();
foreach (var port in outputPorts)
{
port.AddToClassList(portDisabledString);
}
}
else
{
// Remove elements from disabled class list
RemoveFromClassList(disabledString);
var inputPorts = inputContainer.Query<ShaderPort>().ToList();
foreach (var port in inputPorts)
{
port.RemoveFromClassList(portDisabledString);
}
var outputPorts = outputContainer.Query<ShaderPort>().ToList();
foreach (var port in outputPorts)
{
port.RemoveFromClassList(portDisabledString);
}
}
}
public void ClearMessage()
{
var badge = this.Q<IconBadge>();
badge?.Detach();
badge?.RemoveFromHierarchy();
}
public void UpdateDropdownEntries()
{
if (node is SubGraphNode subGraphNode && subGraphNode.asset != null)
{
m_DropdownItems.Clear();
var dropdowns = subGraphNode.asset.dropdowns;
foreach (var dropdown in dropdowns)
{
if (dropdown.isExposed)
{
var name = subGraphNode.GetDropdownEntryName(dropdown.referenceName);
if (!dropdown.ContainsEntry(name))
{
name = dropdown.entryName;
subGraphNode.SetDropdownEntryName(dropdown.referenceName, name);
}
var field = new PopupField<string>(dropdown.entries.Select(x => x.displayName).ToList(), name);
// Create anonymous lambda
EventCallback<ChangeEvent<string>> eventCallback = (evt) =>
{
subGraphNode.owner.owner.RegisterCompleteObjectUndo("Change Dropdown Value");
subGraphNode.SetDropdownEntryName(dropdown.referenceName, field.value);
subGraphNode.Dirty(ModificationScope.Topological);
};
field.RegisterValueChangedCallback(eventCallback);
// Setup so we can unregister this callback later
m_UnregisterAll += () => field.UnregisterValueChangedCallback(eventCallback);
m_DropdownItems.Add(new PropertyRow(new Label(dropdown.displayName)), (row) =>
{
row.styleSheets.Add(Resources.Load<StyleSheet>("Styles/PropertyRow"));
row.Add(field);
});
}
}
}
}
public VisualElement colorElement
{
get { return this; }
}
static readonly StyleColor noColor = new StyleColor(StyleKeyword.Null);
public void SetColor(Color color)
{
m_TitleContainer.style.borderBottomColor = color;
}
public void ResetColor()
{
m_TitleContainer.style.borderBottomColor = noColor;
}
public Color GetColor()
{
return m_TitleContainer.resolvedStyle.borderBottomColor;
}
void OnSubGraphDoubleClick(MouseDownEvent evt)
{
if (evt.clickCount == 2 && evt.button == 0)
{
SubGraphNode subgraphNode = node as SubGraphNode;
var path = AssetDatabase.GUIDToAssetPath(subgraphNode.subGraphGuid);
ShaderGraphImporterEditor.ShowGraphEditWindow(path);
}
}
public Node gvNode => this;
[Inspectable("Node", null)]
public AbstractMaterialNode node { get; private set; }
public override bool expanded
{
get => base.expanded;
set
{
if (base.expanded == value)
return;
base.expanded = value;
if (node.drawState.expanded != value)
{
var ds = node.drawState;
ds.expanded = value;
node.drawState = ds;
}
foreach (var inputPort in inputContainer.Query<ShaderPort>().ToList())
{
inputPort.parent.style.visibility = inputPort.style.visibility;
}
RefreshExpandedState(); // Necessary b/c we can't override enough Node.cs functions to update only what's needed
}
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
if (evt.target is Node)
{
var canViewShader = node.hasPreview || node is SubGraphOutputNode;
evt.menu.AppendAction("Copy Shader", CopyToClipboard,
_ => canViewShader ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden,
GenerationMode.ForReals);
evt.menu.AppendAction("Show Generated Code", ShowGeneratedCode,
_ => canViewShader ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden,
GenerationMode.ForReals);
if (Unsupported.IsDeveloperMode())
{
evt.menu.AppendAction("Show Preview Code", ShowGeneratedCode,
_ => canViewShader ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden,
GenerationMode.Preview);
}
}
base.BuildContextualMenu(evt);
}
void CopyToClipboard(DropdownMenuAction action)
{
GUIUtility.systemCopyBuffer = ConvertToShader((GenerationMode)action.userData);
}
public string SanitizeName(string name)
{
return new string(name.Where(c => !Char.IsWhiteSpace(c)).ToArray());
}
public void ShowGeneratedCode(DropdownMenuAction action)
{
string name = GetFirstAncestorOfType<GraphEditorView>().assetName;
var mode = (GenerationMode)action.userData;
string path = String.Format("Temp/GeneratedFromGraph-{0}-{1}-{2}{3}.shader", SanitizeName(name),
SanitizeName(node.name), node.objectId, mode == GenerationMode.Preview ? "-Preview" : "");
if (GraphUtil.WriteToFile(path, ConvertToShader(mode)))
GraphUtil.OpenFile(path);
}
string ConvertToShader(GenerationMode mode)
{
var generator = new Generator(node.owner, node, mode, node.name);
return generator.generatedShader;
}
void SetNodesAsDirty()
{
var editorView = GetFirstAncestorOfType<GraphEditorView>();
var nodeList = m_GraphView.Query<MaterialNodeView>().ToList();
editorView.colorManager.SetNodesDirty(nodeList);
}
void UpdateNodeViews()
{
var editorView = GetFirstAncestorOfType<GraphEditorView>();
var nodeList = m_GraphView.Query<MaterialNodeView>().ToList();
editorView.colorManager.UpdateNodeViews(nodeList);
}
public object GetObjectToInspect()
{
return node;
}
public void SupplyDataToPropertyDrawer(IPropertyDrawer propertyDrawer, Action inspectorUpdateDelegate)
{
if (propertyDrawer is IGetNodePropertyDrawerPropertyData nodePropertyDrawer)
{
nodePropertyDrawer.GetPropertyData(SetNodesAsDirty, UpdateNodeViews);
}
}
private void SetSelfSelected()
{
m_GraphView.ClearSelection();
m_GraphView.AddToSelection(this);
}
protected override void ToggleCollapse()
{
node.owner.owner.RegisterCompleteObjectUndo(!expanded ? "Expand Nodes" : "Collapse Nodes");
expanded = !expanded;
// If selected, expand/collapse the other applicable nodes that are also selected
if (selected)
{
m_GraphView.SetNodeExpandedForSelectedNodes(expanded, false);
}
}
void SetPreviewExpandedStateOnSelection(bool state)
{
// If selected, expand/collapse the other applicable nodes that are also selected
if (selected)
{
m_GraphView.SetPreviewExpandedForSelectedNodes(state);
}
else
{
node.owner.owner.RegisterCompleteObjectUndo(state ? "Expand Previews" : "Collapse Previews");
node.previewExpanded = state;
}
}
public bool CanToggleNodeExpanded()
{
return !(node is BlockNode) && m_CollapseButton.enabledInHierarchy;
}
void UpdatePreviewExpandedState(bool expanded)
{
node.previewExpanded = expanded;
if (m_PreviewFiller == null)
return;
if (expanded)
{
if (m_PreviewContainer.parent != this)
{
Add(m_PreviewContainer);
m_PreviewContainer.PlaceBehind(this.Q("selection-border"));
}
m_PreviewFiller.AddToClassList("expanded");
m_PreviewFiller.RemoveFromClassList("collapsed");
}
else
{
if (m_PreviewContainer.parent == m_PreviewFiller)
{
m_PreviewContainer.RemoveFromHierarchy();
}
m_PreviewFiller.RemoveFromClassList("expanded");
m_PreviewFiller.AddToClassList("collapsed");
}
UpdatePreviewTexture();
}
void UpdateTitle()
{
if (node is SubGraphNode subGraphNode && subGraphNode.asset != null)
title = subGraphNode.asset.name;
else
{
if (node.sgVersion < node.latestVersion)
{
if (node is IHasCustomDeprecationMessage customDeprecationMessage)
{
title = customDeprecationMessage.GetCustomDeprecationLabel();
}
else
{
title = node.name + $" (Legacy v{node.sgVersion})";
}
}
else
{
title = node.name;
}
}
}
void UpdateShaderPortsForSlots(bool inputSlots, List<MaterialSlot> allSlots, ShaderPort[] slotShaderPorts)
{
VisualElement portContainer = inputSlots ? inputContainer : outputContainer;
var existingPorts = portContainer.Query<ShaderPort>().ToList();
foreach (ShaderPort shaderPort in existingPorts)
{
var currentSlotId = shaderPort.slot.id;
int newSlotIndex = allSlots.FindIndex(s => s.id == currentSlotId);
if (newSlotIndex < 0)
{
// slot doesn't exist anymore, remove it
if (inputSlots)
portContainer.Remove(shaderPort.parent); // remove parent (includes the InputView)
else
portContainer.Remove(shaderPort);
}
else
{
var newSlot = allSlots[newSlotIndex];
slotShaderPorts[newSlotIndex] = shaderPort;
// these should probably be in an UpdateShaderPort(shaderPort, newSlot) function
shaderPort.slot = newSlot;
shaderPort.portName = newSlot.displayName;
if (inputSlots) // input slots also have to update the InputView
UpdatePortInputView(shaderPort);
}
}
}
public void OnModified(ModificationScope scope)
{
UpdateTitle();
SetActive(node.isActive);
if (node.hasPreview)
UpdatePreviewExpandedState(node.previewExpanded);
base.expanded = node.drawState.expanded;
switch (scope)
{
// Update slots to match node modification
case ModificationScope.Topological:
{
var slots = node.GetSlots<MaterialSlot>().ToList();
// going to record the corresponding ShaderPort to each slot, so we can order them later
ShaderPort[] slotShaderPorts = new ShaderPort[slots.Count];
// update existing input and output ports
UpdateShaderPortsForSlots(true, slots, slotShaderPorts);
UpdateShaderPortsForSlots(false, slots, slotShaderPorts);
// check if there are any new slots that must create new ports
for (int i = 0; i < slots.Count; i++)
{
if (slotShaderPorts[i] == null)
slotShaderPorts[i] = AddShaderPortForSlot(slots[i]);
}
// make sure they are in the right order
// by bringing each port to front in declaration order
// note that this sorts input and output containers at the same time
foreach (var shaderPort in slotShaderPorts)
{
if (shaderPort != null)
{
if (shaderPort.slot.isInputSlot)
shaderPort.parent.BringToFront();
else
shaderPort.BringToFront();
}
}
break;
}
}
RefreshExpandedState(); // Necessary b/c we can't override enough Node.cs functions to update only what's needed
foreach (var listener in m_ControlItems.Children().OfType<AbstractMaterialNodeModificationListener>())
{
if (listener != null)
listener.OnNodeModified(scope);
}
}
ShaderPort AddShaderPortForSlot(MaterialSlot slot)
{
if (slot.hidden)
return null;
ShaderPort port = ShaderPort.Create(slot, m_ConnectorListener);
if (slot.isOutputSlot)
{
outputContainer.Add(port);
}
else
{
var portContainer = new VisualElement();
portContainer.style.flexDirection = FlexDirection.Row;
var portInputView = new PortInputView(slot) { style = { position = Position.Absolute } };
portContainer.Add(portInputView);
portContainer.Add(port);
inputContainer.Add(portContainer);
// Update active state
if (node.isActive)
{
portInputView.RemoveFromClassList("disabled");
}
else
{
portInputView.AddToClassList("disabled");
}
}
port.OnDisconnect = OnEdgeDisconnected;
return port;
}
void AddSlots(IEnumerable<MaterialSlot> slots)
{
foreach (var slot in slots)
AddShaderPortForSlot(slot);
// Make sure the visuals are properly updated to reflect port list
RefreshPorts();
}
void OnEdgeDisconnected(Port obj)
{
RefreshExpandedState();
}
static bool GetPortInputView(ShaderPort port, out PortInputView view)
{
view = port.parent.Q<PortInputView>();
return view != null;
}
public void UpdatePortInputTypes()
{
var portList = inputContainer.Query<ShaderPort>().ToList();
portList.AddRange(outputContainer.Query<ShaderPort>().ToList());
foreach (var anchor in portList)
{
var slot = anchor.slot;
anchor.portName = slot.displayName;
anchor.visualClass = slot.concreteValueType.ToClassName();
if (GetPortInputView(anchor, out var portInputView))
{
portInputView.UpdateSlotType();
UpdatePortInputVisibility(portInputView, anchor);
}
}
foreach (var control in m_ControlItems.Children())
{
if (control is AbstractMaterialNodeModificationListener listener)
listener.OnNodeModified(ModificationScope.Graph);
}
}
void UpdatePortInputView(ShaderPort port)
{
if (GetPortInputView(port, out var portInputView))
{
portInputView.UpdateSlot(port.slot);
UpdatePortInputVisibility(portInputView, port);
}
}
void UpdatePortInputVisibility(PortInputView portInputView, ShaderPort port)
{
SetElementVisible(portInputView, !port.slot.isConnected);
port.parent.style.visibility = port.style.visibility;
portInputView.MarkDirtyRepaint();
}
void SetElementVisible(VisualElement element, bool isVisible)
{
const string k_HiddenClassList = "hidden";
if (isVisible)
{
// Restore default value for visibility by setting it to StyleKeyword.Null.
// Setting it to Visibility.Visible would make it visible even if parent is hidden.
element.style.visibility = StyleKeyword.Null;
element.RemoveFromClassList(k_HiddenClassList);
}
else
{
element.style.visibility = Visibility.Hidden;
element.AddToClassList(k_HiddenClassList);
}
}
SGBlackboardRow GetAssociatedBlackboardRow()
{
var graphEditorView = GetFirstAncestorOfType<GraphEditorView>();
if (graphEditorView == null)
return null;
var blackboardController = graphEditorView.blackboardController;
if (blackboardController == null)
return null;
if (node is KeywordNode keywordNode)
{
return blackboardController.GetBlackboardRow(keywordNode.keyword);
}
if (node is DropdownNode dropdownNode)
{
return blackboardController.GetBlackboardRow(dropdownNode.dropdown);
}
return null;
}
void OnMouseHover(EventBase evt)
{
// Keyword/Dropdown nodes should be highlighted when Blackboard entry is hovered
// TODO: Move to new NodeView type when keyword node has unique style
var blackboardRow = GetAssociatedBlackboardRow();
if (blackboardRow != null)
{
if (evt.eventTypeId == MouseEnterEvent.TypeId())
{
blackboardRow.AddToClassList("hovered");
}
else
{
blackboardRow.RemoveFromClassList("hovered");
}
}
}
void UpdatePreviewTexture()
{
if (m_PreviewRenderData.texture == null || !node.previewExpanded)
{
m_PreviewImage.visible = false;
m_PreviewImage.image = Texture2D.blackTexture;
}
else
{
m_PreviewImage.visible = true;
m_PreviewImage.AddToClassList("visible");
m_PreviewImage.RemoveFromClassList("hidden");
if (m_PreviewImage.image != m_PreviewRenderData.texture)
m_PreviewImage.image = m_PreviewRenderData.texture;
else
m_PreviewImage.MarkDirtyRepaint();
if (m_PreviewRenderData.shaderData.isOutOfDate)
m_PreviewImage.tintColor = new Color(1.0f, 1.0f, 1.0f, 0.3f);
else
m_PreviewImage.tintColor = Color.white;
}
}
public void Dispose()
{
ClearMessage();
foreach (var portInputView in inputContainer.Query<PortInputView>().ToList())
portInputView.Dispose();
foreach (var shaderPort in outputContainer.Query<ShaderPort>().ToList())
shaderPort.Dispose();
var propRow = GetAssociatedBlackboardRow();
// If this node view is deleted, remove highlighting from associated blackboard row
if (propRow != null)
{
propRow.RemoveFromClassList("hovered");
}
styleSheets.Clear();
inputContainer?.Clear();
outputContainer?.Clear();
m_DropdownsDivider?.Clear();
m_ControlsDivider?.Clear();
m_PreviewContainer?.Clear();
m_ControlItems?.Clear();
m_ConnectorListener = null;
m_GraphView = null;
m_DropdownItems = null;
m_ControlItems = null;
m_ControlsDivider = null;
m_PreviewContainer = null;
m_PreviewFiller = null;
m_PreviewImage = null;
m_DropdownsDivider = null;
m_TitleContainer = null;
node = null;
userData = null;
// Unregister callback
m_UnregisterAll?.Invoke();
m_UnregisterAll = null;
if (m_PreviewRenderData != null)
{
m_PreviewRenderData.onPreviewChanged -= UpdatePreviewTexture;
m_PreviewRenderData = null;
}
ShaderGraphPreferences.onAllowDeprecatedChanged -= UpdateTitle;
Clear();
}
}
}