2962 lines
115 KiB
C#
2962 lines
115 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Text.RegularExpressions;
|
|
using UnityEngine;
|
|
using UnityEditor.Graphing;
|
|
using UnityEditor.Graphing.Util;
|
|
using UnityEditor.Rendering;
|
|
using UnityEditor.ShaderGraph.Internal;
|
|
using UnityEditor.ShaderGraph.Legacy;
|
|
using UnityEditor.ShaderGraph.Serialization;
|
|
using UnityEditor.ShaderGraph.Drawing;
|
|
using Edge = UnityEditor.Graphing.Edge;
|
|
|
|
using UnityEngine.UIElements;
|
|
using UnityEngine.Assertions;
|
|
using UnityEngine.Pool;
|
|
using UnityEngine.Serialization;
|
|
|
|
namespace UnityEditor.ShaderGraph
|
|
{
|
|
[Serializable]
|
|
[FormerName("UnityEditor.ShaderGraph.MaterialGraph")]
|
|
[FormerName("UnityEditor.ShaderGraph.SubGraph")]
|
|
[FormerName("UnityEditor.ShaderGraph.AbstractMaterialGraph")]
|
|
sealed partial class GraphData : JsonObject
|
|
{
|
|
public override int latestVersion => 3;
|
|
|
|
public GraphObject owner { get; set; }
|
|
|
|
#region Input data
|
|
|
|
[SerializeField]
|
|
List<JsonData<AbstractShaderProperty>> m_Properties = new List<JsonData<AbstractShaderProperty>>();
|
|
|
|
public DataValueEnumerable<AbstractShaderProperty> properties => m_Properties.SelectValue();
|
|
|
|
[SerializeField]
|
|
List<JsonData<ShaderKeyword>> m_Keywords = new List<JsonData<ShaderKeyword>>();
|
|
|
|
public DataValueEnumerable<ShaderKeyword> keywords => m_Keywords.SelectValue();
|
|
|
|
[SerializeField]
|
|
List<JsonData<ShaderDropdown>> m_Dropdowns = new List<JsonData<ShaderDropdown>>();
|
|
|
|
public DataValueEnumerable<ShaderDropdown> dropdowns => m_Dropdowns.SelectValue();
|
|
|
|
[NonSerialized]
|
|
List<ShaderInput> m_AddedInputs = new List<ShaderInput>();
|
|
|
|
public IEnumerable<ShaderInput> addedInputs
|
|
{
|
|
get { return m_AddedInputs; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<ShaderInput> m_RemovedInputs = new List<ShaderInput>();
|
|
|
|
public IEnumerable<ShaderInput> removedInputs
|
|
{
|
|
get { return m_RemovedInputs; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<ShaderInput> m_MovedInputs = new List<ShaderInput>();
|
|
|
|
public IEnumerable<ShaderInput> movedInputs
|
|
{
|
|
get { return m_MovedInputs; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<CategoryData> m_AddedCategories = new List<CategoryData>();
|
|
|
|
public IEnumerable<CategoryData> addedCategories
|
|
{
|
|
get { return m_AddedCategories; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<CategoryData> m_RemovedCategories = new List<CategoryData>();
|
|
|
|
public IEnumerable<CategoryData> removedCategories
|
|
{
|
|
get { return m_RemovedCategories; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<CategoryData> m_MovedCategories = new List<CategoryData>();
|
|
|
|
public IEnumerable<CategoryData> movedCategories
|
|
{
|
|
get { return m_MovedCategories; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
bool m_MovedContexts = false;
|
|
public bool movedContexts => m_MovedContexts;
|
|
|
|
public string assetGuid { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Category Data
|
|
|
|
[SerializeField]
|
|
List<JsonData<CategoryData>> m_CategoryData = new List<JsonData<CategoryData>>();
|
|
|
|
public DataValueEnumerable<CategoryData> categories => m_CategoryData.SelectValue();
|
|
|
|
#endregion
|
|
|
|
#region Node data
|
|
|
|
[SerializeField]
|
|
List<JsonData<AbstractMaterialNode>> m_Nodes = new List<JsonData<AbstractMaterialNode>>();
|
|
|
|
[NonSerialized]
|
|
Dictionary<string, AbstractMaterialNode> m_NodeDictionary = new Dictionary<string, AbstractMaterialNode>();
|
|
|
|
[NonSerialized]
|
|
Dictionary<string, AbstractMaterialNode> m_LegacyUpdateDictionary = new Dictionary<string, AbstractMaterialNode>();
|
|
|
|
public IEnumerable<T> GetNodes<T>()
|
|
{
|
|
return m_Nodes.SelectValue().OfType<T>();
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<AbstractMaterialNode> m_AddedNodes = new List<AbstractMaterialNode>();
|
|
|
|
public IEnumerable<AbstractMaterialNode> addedNodes
|
|
{
|
|
get { return m_AddedNodes; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<AbstractMaterialNode> m_RemovedNodes = new List<AbstractMaterialNode>();
|
|
|
|
public IEnumerable<AbstractMaterialNode> removedNodes
|
|
{
|
|
get { return m_RemovedNodes; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<AbstractMaterialNode> m_PastedNodes = new List<AbstractMaterialNode>();
|
|
|
|
public IEnumerable<AbstractMaterialNode> pastedNodes
|
|
{
|
|
get { return m_PastedNodes; }
|
|
}
|
|
#endregion
|
|
|
|
#region Group Data
|
|
|
|
[SerializeField]
|
|
List<JsonData<GroupData>> m_GroupDatas = new List<JsonData<GroupData>>();
|
|
|
|
public DataValueEnumerable<GroupData> groups
|
|
{
|
|
get { return m_GroupDatas.SelectValue(); }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<GroupData> m_AddedGroups = new List<GroupData>();
|
|
|
|
public IEnumerable<GroupData> addedGroups
|
|
{
|
|
get { return m_AddedGroups; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<GroupData> m_RemovedGroups = new List<GroupData>();
|
|
|
|
public IEnumerable<GroupData> removedGroups
|
|
{
|
|
get { return m_RemovedGroups; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<GroupData> m_PastedGroups = new List<GroupData>();
|
|
|
|
public IEnumerable<GroupData> pastedGroups
|
|
{
|
|
get { return m_PastedGroups; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<ParentGroupChange> m_ParentGroupChanges = new List<ParentGroupChange>();
|
|
|
|
public IEnumerable<ParentGroupChange> parentGroupChanges
|
|
{
|
|
get { return m_ParentGroupChanges; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
GroupData m_MostRecentlyCreatedGroup;
|
|
|
|
public GroupData mostRecentlyCreatedGroup => m_MostRecentlyCreatedGroup;
|
|
|
|
[NonSerialized]
|
|
Dictionary<JsonRef<GroupData>, List<IGroupItem>> m_GroupItems = new Dictionary<JsonRef<GroupData>, List<IGroupItem>>();
|
|
|
|
public IEnumerable<IGroupItem> GetItemsInGroup(GroupData groupData)
|
|
{
|
|
if (m_GroupItems.TryGetValue(groupData, out var nodes))
|
|
{
|
|
return nodes;
|
|
}
|
|
return Enumerable.Empty<IGroupItem>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region StickyNote Data
|
|
[SerializeField]
|
|
List<JsonData<StickyNoteData>> m_StickyNoteDatas = new List<JsonData<StickyNoteData>>();
|
|
|
|
public DataValueEnumerable<StickyNoteData> stickyNotes => m_StickyNoteDatas.SelectValue();
|
|
|
|
[NonSerialized]
|
|
List<StickyNoteData> m_AddedStickyNotes = new List<StickyNoteData>();
|
|
|
|
public List<StickyNoteData> addedStickyNotes => m_AddedStickyNotes;
|
|
|
|
[NonSerialized]
|
|
List<StickyNoteData> m_RemovedNotes = new List<StickyNoteData>();
|
|
|
|
public IEnumerable<StickyNoteData> removedNotes => m_RemovedNotes;
|
|
|
|
[NonSerialized]
|
|
List<StickyNoteData> m_PastedStickyNotes = new List<StickyNoteData>();
|
|
|
|
public IEnumerable<StickyNoteData> pastedStickyNotes => m_PastedStickyNotes;
|
|
|
|
#endregion
|
|
|
|
#region Edge data
|
|
|
|
[SerializeField]
|
|
List<Edge> m_Edges = new List<Edge>();
|
|
|
|
public IEnumerable<Edge> edges => m_Edges;
|
|
|
|
[NonSerialized]
|
|
Dictionary<string, List<IEdge>> m_NodeEdges = new Dictionary<string, List<IEdge>>();
|
|
|
|
[NonSerialized]
|
|
List<IEdge> m_AddedEdges = new List<IEdge>();
|
|
|
|
public IEnumerable<IEdge> addedEdges
|
|
{
|
|
get { return m_AddedEdges; }
|
|
}
|
|
|
|
[NonSerialized]
|
|
List<IEdge> m_RemovedEdges = new List<IEdge>();
|
|
|
|
public IEnumerable<IEdge> removedEdges
|
|
{
|
|
get { return m_RemovedEdges; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Context Data
|
|
|
|
[SerializeField]
|
|
ContextData m_VertexContext;
|
|
|
|
[SerializeField]
|
|
ContextData m_FragmentContext;
|
|
|
|
// We build this once and cache it as it uses reflection
|
|
// This list is used to build the Create Node menu entries for Blocks
|
|
// as well as when deserializing descriptor fields on serialized Blocks
|
|
[NonSerialized]
|
|
List<BlockFieldDescriptor> m_BlockFieldDescriptors;
|
|
|
|
public ContextData vertexContext => m_VertexContext;
|
|
public ContextData fragmentContext => m_FragmentContext;
|
|
public List<BlockFieldDescriptor> blockFieldDescriptors => m_BlockFieldDescriptors;
|
|
|
|
#endregion
|
|
|
|
[SerializeField]
|
|
InspectorPreviewData m_PreviewData = new InspectorPreviewData();
|
|
|
|
public InspectorPreviewData previewData
|
|
{
|
|
get { return m_PreviewData; }
|
|
set { m_PreviewData = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
string m_Path;
|
|
|
|
public string path
|
|
{
|
|
get { return m_Path; }
|
|
set
|
|
{
|
|
if (m_Path == value)
|
|
return;
|
|
m_Path = value;
|
|
if (owner != null)
|
|
owner.RegisterCompleteObjectUndo("Change Path");
|
|
}
|
|
}
|
|
|
|
public MessageManager messageManager { get; set; }
|
|
public bool isSubGraph { get; set; }
|
|
|
|
// we default this to Graph for subgraphs
|
|
// but for shadergraphs, this will get replaced with Single
|
|
[SerializeField]
|
|
private GraphPrecision m_GraphPrecision = GraphPrecision.Graph;
|
|
|
|
public GraphPrecision graphDefaultPrecision
|
|
{
|
|
get
|
|
{
|
|
// shader graphs are not allowed to have graph precision
|
|
// we force them to Single if they somehow get set to graph
|
|
if ((!isSubGraph) && (m_GraphPrecision == GraphPrecision.Graph))
|
|
return GraphPrecision.Single;
|
|
|
|
return m_GraphPrecision;
|
|
}
|
|
}
|
|
|
|
public ConcretePrecision graphDefaultConcretePrecision
|
|
{
|
|
get
|
|
{
|
|
// when in "Graph switchable" mode, we choose Half as the default concrete precision
|
|
// so you can visualize the worst-case
|
|
return graphDefaultPrecision.ToConcrete(ConcretePrecision.Half);
|
|
}
|
|
}
|
|
|
|
// Some state has been changed that requires checking for the auto add/removal of blocks.
|
|
// This needs to be checked at a later point in time so actions like replace (remove + add) don't remove blocks.
|
|
internal bool checkAutoAddRemoveBlocks { get; set; }
|
|
|
|
public void SetGraphDefaultPrecision(GraphPrecision newGraphDefaultPrecision)
|
|
{
|
|
if ((!isSubGraph) && (newGraphDefaultPrecision == GraphPrecision.Graph))
|
|
{
|
|
// shader graphs can't be set to "Graph", only subgraphs can
|
|
Debug.LogError("Cannot set ShaderGraph to a default precision of Graph");
|
|
}
|
|
else
|
|
{
|
|
m_GraphPrecision = newGraphDefaultPrecision;
|
|
}
|
|
}
|
|
|
|
// NOTE: having preview mode default to 3D preserves the old behavior of pre-existing subgraphs
|
|
// if we change this, we would have to introduce a versioning step if we want to maintain the old behavior
|
|
[SerializeField]
|
|
private PreviewMode m_PreviewMode = PreviewMode.Preview3D;
|
|
|
|
public PreviewMode previewMode
|
|
{
|
|
get => m_PreviewMode;
|
|
set => m_PreviewMode = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
JsonRef<AbstractMaterialNode> m_OutputNode;
|
|
|
|
public AbstractMaterialNode outputNode
|
|
{
|
|
get => m_OutputNode;
|
|
set => m_OutputNode = value;
|
|
}
|
|
|
|
internal delegate void SaveGraphDelegate(Shader shader, object context);
|
|
internal static SaveGraphDelegate onSaveGraph;
|
|
|
|
#region Targets
|
|
|
|
// Serialized list of user-selected active targets, sorted in displayName order (to maintain deterministic serialization order)
|
|
// some of these may be MultiJsonInternal.UnknownTargetType if we can't recognize the type of the target
|
|
[SerializeField]
|
|
internal List<JsonData<Target>> m_ActiveTargets = new List<JsonData<Target>>(); // After adding to this list, you MUST call SortActiveTargets()
|
|
public DataValueEnumerable<Target> activeTargets => m_ActiveTargets.SelectValue();
|
|
|
|
// this stores all of the current possible Target types (including any unknown target types we serialized in)
|
|
class PotentialTarget
|
|
{
|
|
// the potential Target
|
|
Target m_Target;
|
|
|
|
// a Target is either known (we know the Type) or unknown (can't find a matching definition of the Type)
|
|
// Targets of unknown type are stored in an UnknownTargetType
|
|
private Type m_KnownType;
|
|
private MultiJsonInternal.UnknownTargetType m_UnknownTarget;
|
|
|
|
public PotentialTarget(Target target)
|
|
{
|
|
m_Target = target;
|
|
|
|
if (target is MultiJsonInternal.UnknownTargetType)
|
|
{
|
|
m_UnknownTarget = (MultiJsonInternal.UnknownTargetType)target;
|
|
m_KnownType = null;
|
|
}
|
|
else
|
|
{
|
|
m_UnknownTarget = null;
|
|
m_KnownType = target.GetType();
|
|
}
|
|
}
|
|
|
|
public bool IsUnknown()
|
|
{
|
|
return m_UnknownTarget != null;
|
|
}
|
|
|
|
public MultiJsonInternal.UnknownTargetType GetUnknown()
|
|
{
|
|
return m_UnknownTarget;
|
|
}
|
|
|
|
public Type knownType { get { return m_KnownType; } }
|
|
|
|
public bool Is(Target t)
|
|
{
|
|
return t == m_Target;
|
|
}
|
|
|
|
public string GetDisplayName()
|
|
{
|
|
return m_Target.displayName;
|
|
}
|
|
|
|
public void ReplaceStoredTarget(Target t)
|
|
{
|
|
if (m_KnownType != null)
|
|
Assert.IsTrue(t.GetType() == m_KnownType);
|
|
m_Target = t;
|
|
}
|
|
|
|
public Target GetTarget()
|
|
{
|
|
return m_Target;
|
|
}
|
|
}
|
|
[NonSerialized]
|
|
List<PotentialTarget> m_AllPotentialTargets = new List<PotentialTarget>();
|
|
public IEnumerable<Target> allPotentialTargets => m_AllPotentialTargets.Select(x => x.GetTarget());
|
|
|
|
public int GetTargetIndexByKnownType(Type targetType)
|
|
{
|
|
return m_AllPotentialTargets.FindIndex(pt => pt.knownType == targetType);
|
|
}
|
|
|
|
public int GetTargetIndex(Target t)
|
|
{
|
|
int result = m_AllPotentialTargets.FindIndex(pt => pt.Is(t));
|
|
return result;
|
|
}
|
|
|
|
public List<string> GetPotentialTargetDisplayNames()
|
|
{
|
|
List<string> displayNames = new List<string>(m_AllPotentialTargets.Count);
|
|
for (int validIndex = 0; validIndex < m_AllPotentialTargets.Count; validIndex++)
|
|
{
|
|
displayNames.Add(m_AllPotentialTargets[validIndex].GetDisplayName());
|
|
}
|
|
return displayNames;
|
|
}
|
|
|
|
public void SetTargetActive(Target target, bool skipSortAndUpdate = false)
|
|
{
|
|
int activeIndex = m_ActiveTargets.IndexOf(target);
|
|
if (activeIndex < 0)
|
|
{
|
|
activeIndex = m_ActiveTargets.Count;
|
|
m_ActiveTargets.Add(target);
|
|
}
|
|
|
|
// active known targets should replace the stored Target in AllPotentialTargets
|
|
if (target is MultiJsonInternal.UnknownTargetType unknownTarget)
|
|
{
|
|
// find any existing potential target with the same unknown jsonData
|
|
int targetIndex = m_AllPotentialTargets.FindIndex(
|
|
pt => pt.IsUnknown() && (pt.GetUnknown().jsonData == unknownTarget.jsonData));
|
|
|
|
// replace existing target, or add it if there is none
|
|
if (targetIndex >= 0)
|
|
m_AllPotentialTargets[targetIndex] = new PotentialTarget(target);
|
|
else
|
|
m_AllPotentialTargets.Add(new PotentialTarget(target));
|
|
}
|
|
else
|
|
{
|
|
// known types should already have been registered
|
|
Type targetType = target.GetType();
|
|
int targetIndex = GetTargetIndexByKnownType(targetType);
|
|
Assert.IsTrue(targetIndex >= 0);
|
|
m_AllPotentialTargets[targetIndex].ReplaceStoredTarget(target);
|
|
}
|
|
|
|
if (!skipSortAndUpdate)
|
|
SortAndUpdateActiveTargets();
|
|
}
|
|
|
|
public void SetTargetActive(int targetIndex, bool skipSortAndUpdate = false)
|
|
{
|
|
Target target = m_AllPotentialTargets[targetIndex].GetTarget();
|
|
SetTargetActive(target, skipSortAndUpdate);
|
|
}
|
|
|
|
public void SetTargetInactive(Target target, bool skipSortAndUpdate = false)
|
|
{
|
|
int activeIndex = m_ActiveTargets.IndexOf(target);
|
|
if (activeIndex < 0)
|
|
return;
|
|
|
|
int targetIndex = GetTargetIndex(target);
|
|
|
|
// if a target was in the active targets, it should also have been in the potential targets list
|
|
Assert.IsTrue(targetIndex >= 0);
|
|
|
|
m_ActiveTargets.RemoveAt(activeIndex);
|
|
|
|
if (!skipSortAndUpdate)
|
|
SortAndUpdateActiveTargets();
|
|
}
|
|
|
|
// this list is populated by graph validation, and lists all of the targets that nodes did not like
|
|
[NonSerialized]
|
|
List<Target> m_UnsupportedTargets = new List<Target>();
|
|
public List<Target> unsupportedTargets { get => m_UnsupportedTargets; }
|
|
|
|
private Comparison<Target> targetComparison = new Comparison<Target>((a, b) => string.Compare(a.displayName, b.displayName));
|
|
public void SortActiveTargets()
|
|
{
|
|
activeTargets.Sort(targetComparison);
|
|
}
|
|
|
|
// TODO: Need a better way to handle this
|
|
#if VFX_GRAPH_10_0_0_OR_NEWER
|
|
public bool hasVFXCompatibleTarget => activeTargets.Any(o => o.SupportsVFX());
|
|
public bool hasVFXTarget
|
|
{
|
|
get
|
|
{
|
|
bool supports = true;
|
|
supports &= !isSubGraph;
|
|
supports &= activeTargets.Any();
|
|
// Maintain support for VFXTarget and VFX compatible targets.
|
|
supports &= activeTargets.OfType<VFXTarget>().Any() || hasVFXCompatibleTarget;
|
|
return supports;
|
|
}
|
|
}
|
|
|
|
public bool isOnlyVFXTarget => activeTargets.Count() == 1 &&
|
|
activeTargets.Count(t => t is VFXTarget) == 1;
|
|
#else
|
|
public bool isVFXTarget => false;
|
|
public bool isOnlyVFXTarget => false;
|
|
#endif
|
|
#endregion
|
|
|
|
public GraphData()
|
|
{
|
|
m_GroupItems[null] = new List<IGroupItem>();
|
|
GetBlockFieldDescriptors();
|
|
AddKnownTargetsToPotentialTargets();
|
|
}
|
|
|
|
// used to initialize the graph with targets, i.e. when creating new graphs via the popup menu
|
|
public void InitializeOutputs(Target[] targets, BlockFieldDescriptor[] blockDescriptors)
|
|
{
|
|
if (targets == null)
|
|
return;
|
|
|
|
foreach (var target in targets)
|
|
{
|
|
if (GetTargetIndexByKnownType(target.GetType()) >= 0)
|
|
{
|
|
SetTargetActive(target, true);
|
|
}
|
|
}
|
|
SortActiveTargets();
|
|
|
|
if (blockDescriptors != null)
|
|
{
|
|
foreach (var descriptor in blockDescriptors)
|
|
{
|
|
var contextData = descriptor.shaderStage == ShaderStage.Fragment ? m_FragmentContext : m_VertexContext;
|
|
var block = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
|
|
block.Init(descriptor);
|
|
AddBlockNoValidate(block, contextData, contextData.blocks.Count);
|
|
}
|
|
}
|
|
|
|
ValidateGraph();
|
|
var activeBlocks = GetActiveBlocksForAllActiveTargets();
|
|
UpdateActiveBlocks(activeBlocks);
|
|
}
|
|
|
|
void GetBlockFieldDescriptors()
|
|
{
|
|
m_BlockFieldDescriptors = new List<BlockFieldDescriptor>();
|
|
|
|
var asmTypes = TypeCache.GetTypesWithAttribute<GenerateBlocksAttribute>();
|
|
foreach (var type in asmTypes)
|
|
{
|
|
var attrs = type.GetCustomAttributes(typeof(GenerateBlocksAttribute), false);
|
|
if (attrs == null || attrs.Length <= 0)
|
|
continue;
|
|
|
|
var attribute = attrs[0] as GenerateBlocksAttribute;
|
|
|
|
// Get all fields that are BlockFieldDescriptor
|
|
// If field and context stages match add to list
|
|
foreach (var fieldInfo in type.GetFields())
|
|
{
|
|
if (fieldInfo.GetValue(type) is BlockFieldDescriptor blockFieldDescriptor)
|
|
{
|
|
blockFieldDescriptor.path = attribute.path;
|
|
m_BlockFieldDescriptors.Add(blockFieldDescriptor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddKnownTargetsToPotentialTargets()
|
|
{
|
|
Assert.AreEqual(m_AllPotentialTargets.Count, 0);
|
|
|
|
// Find all valid Targets by looking in the TypeCache
|
|
var targetTypes = TypeCache.GetTypesDerivedFrom<Target>();
|
|
foreach (var type in targetTypes)
|
|
{
|
|
if (type.IsAbstract || type.IsGenericType || !type.IsClass)
|
|
continue;
|
|
|
|
// create a new instance of the Target, to represent the potential Target
|
|
// NOTE: this instance may be replaced later if we serialize in an Active Target of that type
|
|
var target = (Target)Activator.CreateInstance(type);
|
|
if (!target.isHidden)
|
|
{
|
|
m_AllPotentialTargets.Add(new PotentialTarget(target));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SortAndUpdateActiveTargets()
|
|
{
|
|
SortActiveTargets();
|
|
ValidateGraph();
|
|
NodeUtils.ReevaluateActivityOfNodeList(m_Nodes.SelectValue());
|
|
}
|
|
|
|
public void ClearChanges()
|
|
{
|
|
m_AddedNodes.Clear();
|
|
m_RemovedNodes.Clear();
|
|
m_PastedNodes.Clear();
|
|
m_ParentGroupChanges.Clear();
|
|
m_AddedGroups.Clear();
|
|
m_RemovedGroups.Clear();
|
|
m_PastedGroups.Clear();
|
|
m_AddedEdges.Clear();
|
|
m_RemovedEdges.Clear();
|
|
m_AddedInputs.Clear();
|
|
m_RemovedInputs.Clear();
|
|
m_MovedInputs.Clear();
|
|
m_AddedCategories.Clear();
|
|
m_RemovedCategories.Clear();
|
|
m_MovedCategories.Clear();
|
|
m_AddedStickyNotes.Clear();
|
|
m_RemovedNotes.Clear();
|
|
m_PastedStickyNotes.Clear();
|
|
m_MostRecentlyCreatedGroup = null;
|
|
m_MovedContexts = false;
|
|
}
|
|
|
|
public void AddNode(AbstractMaterialNode node)
|
|
{
|
|
if (node is AbstractMaterialNode materialNode)
|
|
{
|
|
if (isSubGraph && !materialNode.allowedInSubGraph)
|
|
{
|
|
Debug.LogWarningFormat("Attempting to add {0} to Sub Graph. This is not allowed.", materialNode.GetType());
|
|
return;
|
|
}
|
|
|
|
AddNodeNoValidate(materialNode);
|
|
|
|
// If adding a Sub Graph node whose asset contains Keywords
|
|
// Need to restest Keywords against the variant limit
|
|
if (node is SubGraphNode subGraphNode &&
|
|
subGraphNode.asset != null &&
|
|
subGraphNode.asset.keywords.Any())
|
|
{
|
|
OnKeywordChangedNoValidate();
|
|
}
|
|
|
|
ValidateGraph();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarningFormat("Trying to add node {0} to Material graph, but it is not a {1}", node, typeof(AbstractMaterialNode));
|
|
}
|
|
}
|
|
|
|
public void CreateGroup(GroupData groupData)
|
|
{
|
|
if (AddGroup(groupData))
|
|
{
|
|
m_MostRecentlyCreatedGroup = groupData;
|
|
}
|
|
}
|
|
|
|
bool AddGroup(GroupData groupData)
|
|
{
|
|
if (m_GroupDatas.Contains(groupData))
|
|
return false;
|
|
|
|
m_GroupDatas.Add(groupData);
|
|
m_AddedGroups.Add(groupData);
|
|
m_GroupItems.Add(groupData, new List<IGroupItem>());
|
|
|
|
return true;
|
|
}
|
|
|
|
public void RemoveGroup(GroupData groupData)
|
|
{
|
|
RemoveGroupNoValidate(groupData);
|
|
ValidateGraph();
|
|
}
|
|
|
|
void RemoveGroupNoValidate(GroupData group)
|
|
{
|
|
if (!m_GroupDatas.Contains(group))
|
|
throw new InvalidOperationException("Cannot remove a group that doesn't exist.");
|
|
m_GroupDatas.Remove(group);
|
|
m_RemovedGroups.Add(group);
|
|
|
|
if (m_GroupItems.TryGetValue(group, out var items))
|
|
{
|
|
foreach (IGroupItem groupItem in items.ToList())
|
|
{
|
|
SetGroup(groupItem, null);
|
|
}
|
|
|
|
m_GroupItems.Remove(group);
|
|
}
|
|
}
|
|
|
|
public void AddStickyNote(StickyNoteData stickyNote)
|
|
{
|
|
if (m_StickyNoteDatas.Contains(stickyNote))
|
|
{
|
|
throw new InvalidOperationException("Sticky note has already been added to the graph.");
|
|
}
|
|
|
|
if (!m_GroupItems.ContainsKey(stickyNote.group))
|
|
{
|
|
throw new InvalidOperationException("Trying to add sticky note with group that doesn't exist.");
|
|
}
|
|
|
|
m_StickyNoteDatas.Add(stickyNote);
|
|
m_AddedStickyNotes.Add(stickyNote);
|
|
m_GroupItems[stickyNote.group].Add(stickyNote);
|
|
}
|
|
|
|
void RemoveNoteNoValidate(StickyNoteData stickyNote)
|
|
{
|
|
if (!m_StickyNoteDatas.Contains(stickyNote))
|
|
{
|
|
throw new InvalidOperationException("Cannot remove a note that doesn't exist.");
|
|
}
|
|
|
|
m_StickyNoteDatas.Remove(stickyNote);
|
|
m_RemovedNotes.Add(stickyNote);
|
|
|
|
if (m_GroupItems.TryGetValue(stickyNote.group, out var groupItems))
|
|
{
|
|
groupItems.Remove(stickyNote);
|
|
}
|
|
}
|
|
|
|
public void RemoveStickyNote(StickyNoteData stickyNote)
|
|
{
|
|
RemoveNoteNoValidate(stickyNote);
|
|
ValidateGraph();
|
|
}
|
|
|
|
public void SetGroup(IGroupItem node, GroupData group)
|
|
{
|
|
var groupChange = new ParentGroupChange()
|
|
{
|
|
groupItem = node,
|
|
oldGroup = node.group,
|
|
// Checking if the groupdata is null. If it is, then it means node has been removed out of a group.
|
|
// If the group data is null, then maybe the old group id should be removed
|
|
newGroup = group,
|
|
};
|
|
node.group = groupChange.newGroup;
|
|
|
|
var oldGroupNodes = m_GroupItems[groupChange.oldGroup];
|
|
oldGroupNodes.Remove(node);
|
|
|
|
m_GroupItems[groupChange.newGroup].Add(node);
|
|
m_ParentGroupChanges.Add(groupChange);
|
|
}
|
|
|
|
public void AddContexts()
|
|
{
|
|
m_VertexContext = new ContextData();
|
|
m_VertexContext.shaderStage = ShaderStage.Vertex;
|
|
m_VertexContext.position = new Vector2(0, 0);
|
|
m_FragmentContext = new ContextData();
|
|
m_FragmentContext.shaderStage = ShaderStage.Fragment;
|
|
m_FragmentContext.position = new Vector2(0, 200);
|
|
}
|
|
|
|
public void AddBlock(BlockNode blockNode, ContextData contextData, int index)
|
|
{
|
|
AddBlockNoValidate(blockNode, contextData, index);
|
|
ValidateGraph();
|
|
|
|
var activeBlocks = GetActiveBlocksForAllActiveTargets();
|
|
UpdateActiveBlocks(activeBlocks);
|
|
}
|
|
|
|
void AddBlockNoValidate(BlockNode blockNode, ContextData contextData, int index)
|
|
{
|
|
// Regular AddNode path
|
|
AddNodeNoValidate(blockNode);
|
|
|
|
// Set BlockNode properties
|
|
blockNode.contextData = contextData;
|
|
|
|
// Add to ContextData
|
|
if (index == -1 || index >= contextData.blocks.Count())
|
|
{
|
|
contextData.blocks.Add(blockNode);
|
|
}
|
|
else
|
|
{
|
|
contextData.blocks.Insert(index, blockNode);
|
|
}
|
|
}
|
|
|
|
public List<BlockFieldDescriptor> GetActiveBlocksForAllActiveTargets()
|
|
{
|
|
// Get list of active Block types
|
|
var currentBlocks = GetNodes<BlockNode>();
|
|
var context = new TargetActiveBlockContext(currentBlocks.Select(x => x.descriptor).ToList(), null);
|
|
foreach (var target in activeTargets)
|
|
{
|
|
target.GetActiveBlocks(ref context);
|
|
}
|
|
|
|
// custom blocks aren't going to exist in GetActiveBlocks, we need to ensure we grab those too.
|
|
foreach (var cibnode in currentBlocks.Where(bn => bn.isCustomBlock))
|
|
{
|
|
context.AddBlock(cibnode.descriptor);
|
|
}
|
|
return context.activeBlocks;
|
|
}
|
|
|
|
public void UpdateActiveBlocks(List<BlockFieldDescriptor> activeBlockDescriptors)
|
|
{
|
|
// Set Blocks as active based on supported Block list
|
|
//Note: we never want unknown blocks to be active, so explicitly set them to inactive always
|
|
bool disableCI = activeTargets.All(at => at.ignoreCustomInterpolators);
|
|
foreach (var vertexBlock in vertexContext.blocks)
|
|
{
|
|
if (vertexBlock.value?.isCustomBlock == true)
|
|
{
|
|
vertexBlock.value.SetOverrideActiveState(disableCI ? AbstractMaterialNode.ActiveState.ExplicitInactive : AbstractMaterialNode.ActiveState.ExplicitActive);
|
|
}
|
|
else if (vertexBlock.value?.descriptor?.isUnknown == true)
|
|
{
|
|
vertexBlock.value.SetOverrideActiveState(AbstractMaterialNode.ActiveState.ExplicitInactive);
|
|
}
|
|
else
|
|
{
|
|
vertexBlock.value.SetOverrideActiveState(activeBlockDescriptors.Contains(vertexBlock.value.descriptor) ? AbstractMaterialNode.ActiveState.ExplicitActive
|
|
: AbstractMaterialNode.ActiveState.ExplicitInactive);
|
|
}
|
|
}
|
|
foreach (var fragmentBlock in fragmentContext.blocks)
|
|
{
|
|
if (fragmentBlock.value?.descriptor?.isUnknown == true)
|
|
{
|
|
fragmentBlock.value.SetOverrideActiveState(AbstractMaterialNode.ActiveState.ExplicitInactive);
|
|
}
|
|
else
|
|
{
|
|
fragmentBlock.value.SetOverrideActiveState(activeBlockDescriptors.Contains(fragmentBlock.value.descriptor) ? AbstractMaterialNode.ActiveState.ExplicitActive
|
|
: AbstractMaterialNode.ActiveState.ExplicitInactive);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AddRemoveBlocksFromActiveList(List<BlockFieldDescriptor> activeBlockDescriptors)
|
|
{
|
|
var blocksToRemove = ListPool<BlockNode>.Get();
|
|
|
|
void GetBlocksToRemoveForContext(ContextData contextData)
|
|
{
|
|
for (int i = 0; i < contextData.blocks.Count; i++)
|
|
{
|
|
if (contextData.blocks[i].value?.isCustomBlock == true) // custom interpolators are fine.
|
|
continue;
|
|
|
|
var block = contextData.blocks[i];
|
|
if (!activeBlockDescriptors.Contains(block.value.descriptor))
|
|
{
|
|
var slot = block.value.FindSlot<MaterialSlot>(0);
|
|
//Need to check if a slot is not default value OR is an untracked unknown block type
|
|
if (slot.IsUsingDefaultValue() || block.value.descriptor.isUnknown) // TODO: How to check default value
|
|
{
|
|
blocksToRemove.Add(block);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TryAddBlockToContext(BlockFieldDescriptor descriptor, ContextData contextData)
|
|
{
|
|
if (descriptor.shaderStage != contextData.shaderStage)
|
|
return;
|
|
|
|
if (contextData.blocks.Any(x => x.value.descriptor.Equals(descriptor)))
|
|
return;
|
|
|
|
var node = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
|
|
node.Init(descriptor);
|
|
AddBlockNoValidate(node, contextData, contextData.blocks.Count);
|
|
}
|
|
|
|
// Get inactive Blocks to remove
|
|
GetBlocksToRemoveForContext(vertexContext);
|
|
GetBlocksToRemoveForContext(fragmentContext);
|
|
|
|
// Remove blocks
|
|
foreach (var block in blocksToRemove)
|
|
{
|
|
RemoveNodeNoValidate(block);
|
|
}
|
|
|
|
// Add active Blocks not currently in Contexts
|
|
foreach (var descriptor in activeBlockDescriptors)
|
|
{
|
|
TryAddBlockToContext(descriptor, vertexContext);
|
|
TryAddBlockToContext(descriptor, fragmentContext);
|
|
}
|
|
}
|
|
|
|
void AddNodeNoValidate(AbstractMaterialNode node)
|
|
{
|
|
if (node.group != null && !m_GroupItems.ContainsKey(node.group))
|
|
{
|
|
throw new InvalidOperationException("Cannot add a node whose group doesn't exist.");
|
|
}
|
|
node.owner = this;
|
|
m_Nodes.Add(node);
|
|
m_NodeDictionary.Add(node.objectId, node);
|
|
m_AddedNodes.Add(node);
|
|
m_GroupItems[node.group].Add(node);
|
|
}
|
|
|
|
public void RemoveNode(AbstractMaterialNode node)
|
|
{
|
|
if (!node.canDeleteNode)
|
|
{
|
|
throw new InvalidOperationException($"Node {node.name} ({node.objectId}) cannot be deleted.");
|
|
}
|
|
RemoveNodeNoValidate(node);
|
|
ValidateGraph();
|
|
|
|
if (node is BlockNode blockNode)
|
|
{
|
|
var activeBlocks = GetActiveBlocksForAllActiveTargets();
|
|
UpdateActiveBlocks(activeBlocks);
|
|
blockNode.Dirty(ModificationScope.Graph);
|
|
}
|
|
}
|
|
|
|
void RemoveNodeNoValidate(AbstractMaterialNode node)
|
|
{
|
|
if (!m_NodeDictionary.ContainsKey(node.objectId) && node.isActive && !m_RemovedNodes.Contains(node))
|
|
{
|
|
throw new InvalidOperationException("Cannot remove a node that doesn't exist.");
|
|
}
|
|
|
|
m_Nodes.Remove(node);
|
|
m_NodeDictionary.Remove(node.objectId);
|
|
messageManager?.RemoveNode(node.objectId);
|
|
m_RemovedNodes.Add(node);
|
|
|
|
if (m_GroupItems.TryGetValue(node.group, out var groupItems))
|
|
{
|
|
groupItems.Remove(node);
|
|
}
|
|
|
|
if (node is BlockNode blockNode && blockNode.contextData != null)
|
|
{
|
|
// Remove from ContextData
|
|
blockNode.contextData.blocks.Remove(blockNode);
|
|
}
|
|
}
|
|
|
|
void AddEdgeToNodeEdges(IEdge edge)
|
|
{
|
|
List<IEdge> inputEdges;
|
|
if (!m_NodeEdges.TryGetValue(edge.inputSlot.node.objectId, out inputEdges))
|
|
m_NodeEdges[edge.inputSlot.node.objectId] = inputEdges = new List<IEdge>();
|
|
inputEdges.Add(edge);
|
|
|
|
List<IEdge> outputEdges;
|
|
if (!m_NodeEdges.TryGetValue(edge.outputSlot.node.objectId, out outputEdges))
|
|
m_NodeEdges[edge.outputSlot.node.objectId] = outputEdges = new List<IEdge>();
|
|
outputEdges.Add(edge);
|
|
}
|
|
|
|
IEdge ConnectNoValidate(SlotReference fromSlotRef, SlotReference toSlotRef)
|
|
{
|
|
var fromNode = fromSlotRef.node;
|
|
var toNode = toSlotRef.node;
|
|
|
|
if (fromNode == null || toNode == null)
|
|
return null;
|
|
|
|
// both nodes must belong to this graph
|
|
if ((fromNode.owner != this) || (toNode.owner != this))
|
|
return null;
|
|
|
|
// if fromNode is already connected to toNode
|
|
// do now allow a connection as toNode will then
|
|
// have an edge to fromNode creating a cycle.
|
|
// if this is parsed it will lead to an infinite loop.
|
|
var dependentNodes = new List<AbstractMaterialNode>();
|
|
NodeUtils.CollectNodesNodeFeedsInto(dependentNodes, toNode);
|
|
if (dependentNodes.Contains(fromNode))
|
|
return null;
|
|
|
|
var fromSlot = fromNode.FindSlot<MaterialSlot>(fromSlotRef.slotId);
|
|
var toSlot = toNode.FindSlot<MaterialSlot>(toSlotRef.slotId);
|
|
|
|
if (fromSlot == null || toSlot == null)
|
|
return null;
|
|
|
|
if (fromSlot.isOutputSlot == toSlot.isOutputSlot)
|
|
return null;
|
|
|
|
var outputSlot = fromSlot.isOutputSlot ? fromSlotRef : toSlotRef;
|
|
var inputSlot = fromSlot.isInputSlot ? fromSlotRef : toSlotRef;
|
|
|
|
s_TempEdges.Clear();
|
|
GetEdges(inputSlot, s_TempEdges);
|
|
|
|
// remove any inputs that exits before adding
|
|
foreach (var edge in s_TempEdges)
|
|
{
|
|
RemoveEdgeNoValidate(edge);
|
|
}
|
|
|
|
var newEdge = new Edge(outputSlot, inputSlot);
|
|
m_Edges.Add(newEdge);
|
|
m_AddedEdges.Add(newEdge);
|
|
AddEdgeToNodeEdges(newEdge);
|
|
NodeUtils.ReevaluateActivityOfConnectedNodes(toNode);
|
|
|
|
//Debug.LogFormat("Connected edge: {0} -> {1} ({2} -> {3})\n{4}", newEdge.outputSlot.nodeGuid, newEdge.inputSlot.nodeGuid, fromNode.name, toNode.name, Environment.StackTrace);
|
|
return newEdge;
|
|
}
|
|
|
|
public IEdge Connect(SlotReference fromSlotRef, SlotReference toSlotRef)
|
|
{
|
|
var newEdge = ConnectNoValidate(fromSlotRef, toSlotRef);
|
|
ValidateGraph();
|
|
return newEdge;
|
|
}
|
|
|
|
internal void UnnotifyAddedEdge(IEdge edge)
|
|
{
|
|
m_AddedEdges.Remove(edge);
|
|
}
|
|
|
|
public void RemoveEdge(IEdge e)
|
|
{
|
|
RemoveEdgeNoValidate(e);
|
|
ValidateGraph();
|
|
}
|
|
|
|
public void RemoveElements(AbstractMaterialNode[] nodes, IEdge[] edges, GroupData[] groups, StickyNoteData[] notes)
|
|
{
|
|
foreach (var node in nodes)
|
|
{
|
|
if (!node.canDeleteNode)
|
|
{
|
|
throw new InvalidOperationException($"Node {node.name} ({node.objectId}) cannot be deleted.");
|
|
}
|
|
}
|
|
|
|
foreach (var edge in edges.ToArray())
|
|
{
|
|
RemoveEdgeNoValidate(edge);
|
|
}
|
|
|
|
foreach (var serializableNode in nodes)
|
|
{
|
|
// Check if it is a Redirect Node
|
|
// Get the edges and then re-create all Edges
|
|
// This only works if it has all the edges.
|
|
// If one edge is already deleted then we can not re-create.
|
|
if (serializableNode is RedirectNodeData redirectNode)
|
|
{
|
|
redirectNode.GetOutputAndInputSlots(out SlotReference outputSlotRef, out var inputSlotRefs);
|
|
|
|
foreach (SlotReference slot in inputSlotRefs)
|
|
{
|
|
ConnectNoValidate(outputSlotRef, slot);
|
|
}
|
|
}
|
|
|
|
RemoveNodeNoValidate(serializableNode);
|
|
}
|
|
|
|
foreach (var noteData in notes)
|
|
{
|
|
RemoveNoteNoValidate(noteData);
|
|
}
|
|
|
|
foreach (var groupData in groups)
|
|
{
|
|
RemoveGroupNoValidate(groupData);
|
|
}
|
|
|
|
ValidateGraph();
|
|
|
|
if (nodes.Any(x => x is BlockNode))
|
|
{
|
|
var activeBlocks = GetActiveBlocksForAllActiveTargets();
|
|
UpdateActiveBlocks(activeBlocks);
|
|
}
|
|
}
|
|
|
|
void RemoveEdgeNoValidate(IEdge e, bool reevaluateActivity = true)
|
|
{
|
|
e = m_Edges.FirstOrDefault(x => x.Equals(e));
|
|
if (e == null)
|
|
throw new ArgumentException("Trying to remove an edge that does not exist.", "e");
|
|
m_Edges.Remove(e as Edge);
|
|
|
|
AbstractMaterialNode input = e.inputSlot.node, output = e.outputSlot.node;
|
|
if (input != null && ShaderGraphPreferences.autoAddRemoveBlocks)
|
|
{
|
|
checkAutoAddRemoveBlocks = true;
|
|
}
|
|
|
|
List<IEdge> inputNodeEdges;
|
|
if (m_NodeEdges.TryGetValue(input.objectId, out inputNodeEdges))
|
|
inputNodeEdges.Remove(e);
|
|
|
|
List<IEdge> outputNodeEdges;
|
|
if (m_NodeEdges.TryGetValue(output.objectId, out outputNodeEdges))
|
|
outputNodeEdges.Remove(e);
|
|
|
|
m_AddedEdges.Remove(e);
|
|
m_RemovedEdges.Add(e);
|
|
|
|
if (reevaluateActivity)
|
|
{
|
|
if (input != null)
|
|
{
|
|
NodeUtils.ReevaluateActivityOfConnectedNodes(input);
|
|
}
|
|
|
|
if (output != null)
|
|
{
|
|
NodeUtils.ReevaluateActivityOfConnectedNodes(output);
|
|
}
|
|
}
|
|
}
|
|
|
|
public AbstractMaterialNode GetNodeFromId(string nodeId)
|
|
{
|
|
m_NodeDictionary.TryGetValue(nodeId, out var node);
|
|
return node;
|
|
}
|
|
|
|
public T GetNodeFromId<T>(string nodeId) where T : class
|
|
{
|
|
m_NodeDictionary.TryGetValue(nodeId, out var node);
|
|
return node as T;
|
|
}
|
|
|
|
internal Texture2DShaderProperty GetMainTexture()
|
|
{
|
|
foreach (var prop in properties)
|
|
{
|
|
if (prop is Texture2DShaderProperty tex)
|
|
{
|
|
if (tex.isMainTexture)
|
|
{
|
|
return tex;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
internal ColorShaderProperty GetMainColor()
|
|
{
|
|
foreach (var prop in properties)
|
|
{
|
|
if (prop is ColorShaderProperty col)
|
|
{
|
|
if (col.isMainColor)
|
|
{
|
|
return col;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public bool ContainsCategory(CategoryData categoryData)
|
|
{
|
|
return categories.Contains(categoryData);
|
|
}
|
|
|
|
public bool ContainsInput(ShaderInput shaderInput)
|
|
{
|
|
if (shaderInput == null)
|
|
return false;
|
|
|
|
return properties.Contains(shaderInput) || keywords.Contains(shaderInput) || dropdowns.Contains(shaderInput);
|
|
}
|
|
|
|
public bool ContainsNode(AbstractMaterialNode node)
|
|
{
|
|
if (node == null)
|
|
return false;
|
|
|
|
return m_NodeDictionary.TryGetValue(node.objectId, out var foundNode) && node == foundNode;
|
|
}
|
|
|
|
public void GetEdges(SlotReference s, List<IEdge> foundEdges)
|
|
{
|
|
MaterialSlot slot = s.slot;
|
|
|
|
List<IEdge> candidateEdges;
|
|
if (!m_NodeEdges.TryGetValue(s.node.objectId, out candidateEdges))
|
|
return;
|
|
|
|
foreach (var edge in candidateEdges)
|
|
{
|
|
var cs = slot.isInputSlot ? edge.inputSlot : edge.outputSlot;
|
|
if (cs.node == s.node && cs.slotId == s.slotId)
|
|
foundEdges.Add(edge);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<IEdge> GetEdges(SlotReference s)
|
|
{
|
|
var edges = new List<IEdge>();
|
|
GetEdges(s, edges);
|
|
return edges;
|
|
}
|
|
|
|
public void GetEdges(AbstractMaterialNode node, List<IEdge> foundEdges)
|
|
{
|
|
if (m_NodeEdges.TryGetValue(node.objectId, out var edges))
|
|
{
|
|
foundEdges.AddRange(edges);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<IEdge> GetEdges(AbstractMaterialNode node)
|
|
{
|
|
List<IEdge> edges = new List<IEdge>();
|
|
GetEdges(node, edges);
|
|
return edges;
|
|
}
|
|
|
|
public void ForeachHLSLProperty(Action<HLSLProperty> action)
|
|
{
|
|
foreach (var prop in properties)
|
|
prop.ForeachHLSLProperty(action);
|
|
}
|
|
|
|
public void CollectShaderProperties(PropertyCollector collector, GenerationMode generationMode)
|
|
{
|
|
foreach (var prop in properties)
|
|
{
|
|
// For VFX Shader generation, we must omit exposed properties from the Material CBuffer.
|
|
// This is because VFX computes properties on the fly in the vertex stage, and packed into interpolator.
|
|
if (generationMode == GenerationMode.VFX && prop.isExposed)
|
|
{
|
|
prop.overrideHLSLDeclaration = true;
|
|
prop.hlslDeclarationOverride = HLSLDeclaration.DoNotDeclare;
|
|
}
|
|
|
|
// ugh, this needs to be moved to the gradient property implementation
|
|
if (prop is GradientShaderProperty gradientProp && generationMode == GenerationMode.Preview)
|
|
{
|
|
GradientUtil.GetGradientPropertiesForPreview(collector, gradientProp.referenceName, gradientProp.value);
|
|
continue;
|
|
}
|
|
|
|
collector.AddShaderProperty(prop);
|
|
}
|
|
}
|
|
|
|
public void CollectShaderKeywords(KeywordCollector collector, GenerationMode generationMode)
|
|
{
|
|
foreach (var keyword in keywords)
|
|
{
|
|
collector.AddShaderKeyword(keyword);
|
|
}
|
|
|
|
// Alwways calculate permutations when collecting
|
|
collector.CalculateKeywordPermutations();
|
|
}
|
|
|
|
public bool IsInputAllowedInGraph(ShaderInput input)
|
|
{
|
|
return (isSubGraph && input.allowedInSubGraph) || (!isSubGraph && input.allowedInMainGraph);
|
|
}
|
|
|
|
public bool IsInputAllowedInGraph(AbstractMaterialNode node)
|
|
{
|
|
return (isSubGraph && node.allowedInSubGraph) || (!isSubGraph && node.allowedInMainGraph);
|
|
}
|
|
|
|
// adds the input to the graph, and sanitizes the names appropriately
|
|
public void AddGraphInput(ShaderInput input, int index = -1)
|
|
{
|
|
if (input == null)
|
|
return;
|
|
|
|
// sanitize the display name
|
|
input.SetDisplayNameAndSanitizeForGraph(this);
|
|
|
|
// sanitize the reference name
|
|
input.SetReferenceNameAndSanitizeForGraph(this);
|
|
|
|
AddGraphInputNoSanitization(input, index);
|
|
}
|
|
|
|
// just adds the input to the graph, does not fix colliding or illegal names
|
|
internal void AddGraphInputNoSanitization(ShaderInput input, int index = -1)
|
|
{
|
|
if (input == null)
|
|
return;
|
|
|
|
switch (input)
|
|
{
|
|
case AbstractShaderProperty property:
|
|
if (m_Properties.Contains(property))
|
|
return;
|
|
|
|
if (index < 0)
|
|
m_Properties.Add(property);
|
|
else
|
|
m_Properties.Insert(index, property);
|
|
|
|
break;
|
|
case ShaderKeyword keyword:
|
|
if (m_Keywords.Contains(keyword))
|
|
return;
|
|
|
|
if (index < 0)
|
|
m_Keywords.Add(keyword);
|
|
else
|
|
m_Keywords.Insert(index, keyword);
|
|
|
|
OnKeywordChangedNoValidate();
|
|
|
|
break;
|
|
case ShaderDropdown dropdown:
|
|
if (m_Dropdowns.Contains(dropdown))
|
|
return;
|
|
|
|
if (index < 0)
|
|
m_Dropdowns.Add(dropdown);
|
|
else
|
|
m_Dropdowns.Insert(index, dropdown);
|
|
|
|
OnDropdownChangedNoValidate();
|
|
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
|
|
m_AddedInputs.Add(input);
|
|
}
|
|
|
|
// only ignores names matching ignoreName on properties matching ignoreGuid
|
|
public List<string> BuildPropertyDisplayNameList(AbstractShaderProperty ignoreProperty, string ignoreName)
|
|
{
|
|
List<String> result = new List<String>();
|
|
foreach (var p in properties)
|
|
{
|
|
int before = result.Count;
|
|
p.GetPropertyDisplayNames(result);
|
|
|
|
if ((p == ignoreProperty) && (ignoreName != null))
|
|
{
|
|
// remove ignoreName, if it was just added
|
|
for (int i = before; i < result.Count; i++)
|
|
{
|
|
if (result[i] == ignoreName)
|
|
{
|
|
result.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// only ignores names matching ignoreName on properties matching ignoreGuid
|
|
public List<string> BuildPropertyReferenceNameList(AbstractShaderProperty ignoreProperty, string ignoreName)
|
|
{
|
|
List<String> result = new List<String>();
|
|
foreach (var p in properties)
|
|
{
|
|
int before = result.Count;
|
|
p.GetPropertyReferenceNames(result);
|
|
|
|
if ((p == ignoreProperty) && (ignoreName != null))
|
|
{
|
|
// remove ignoreName, if it was just added
|
|
for (int i = before; i < result.Count; i++)
|
|
{
|
|
if (result[i] == ignoreName)
|
|
{
|
|
result.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public string SanitizeGraphInputName(ShaderInput input, string desiredName)
|
|
{
|
|
string currentName = input.displayName;
|
|
string sanitizedName = desiredName.Trim();
|
|
switch (input)
|
|
{
|
|
case AbstractShaderProperty property:
|
|
sanitizedName = GraphUtil.SanitizeName(BuildPropertyDisplayNameList(property, currentName), "{0} ({1})", sanitizedName);
|
|
break;
|
|
case ShaderKeyword keyword:
|
|
sanitizedName = GraphUtil.SanitizeName(keywords.Where(p => p != input).Select(p => p.displayName), "{0} ({1})", sanitizedName);
|
|
break;
|
|
case ShaderDropdown dropdown:
|
|
sanitizedName = GraphUtil.SanitizeName(dropdowns.Where(p => p != input).Select(p => p.displayName), "{0} ({1})", sanitizedName);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
return sanitizedName;
|
|
}
|
|
|
|
public string SanitizeGraphInputReferenceName(ShaderInput input, string desiredName)
|
|
{
|
|
var sanitizedName = NodeUtils.ConvertToValidHLSLIdentifier(desiredName, (desiredName) => (NodeUtils.IsShaderLabKeyWord(desiredName) || NodeUtils.IsShaderGraphKeyWord(desiredName)));
|
|
|
|
switch (input)
|
|
{
|
|
case AbstractShaderProperty property:
|
|
{
|
|
// must deduplicate ref names against keywords, dropdowns, and properties, as they occupy the same name space
|
|
var existingNames = properties.Where(p => p != property).Select(p => p.referenceName).Union(keywords.Select(p => p.referenceName)).Union(dropdowns.Select(p => p.referenceName));
|
|
sanitizedName = GraphUtil.DeduplicateName(existingNames, "{0}_{1}", sanitizedName);
|
|
}
|
|
break;
|
|
case ShaderKeyword keyword:
|
|
{
|
|
// must deduplicate ref names against keywords, dropdowns, and properties, as they occupy the same name space
|
|
sanitizedName = sanitizedName.ToUpper();
|
|
var existingNames = properties.Select(p => p.referenceName).Union(keywords.Where(p => p != input).Select(p => p.referenceName)).Union(dropdowns.Select(p => p.referenceName));
|
|
sanitizedName = GraphUtil.DeduplicateName(existingNames, "{0}_{1}", sanitizedName);
|
|
}
|
|
break;
|
|
case ShaderDropdown dropdown:
|
|
{
|
|
// must deduplicate ref names against keywords, dropdowns, and properties, as they occupy the same name space
|
|
var existingNames = properties.Select(p => p.referenceName).Union(keywords.Select(p => p.referenceName)).Union(dropdowns.Where(p => p != input).Select(p => p.referenceName));
|
|
sanitizedName = GraphUtil.DeduplicateName(existingNames, "{0}_{1}", sanitizedName);
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
return sanitizedName;
|
|
}
|
|
|
|
// copies the ShaderInput, and adds it to the graph with proper name sanitization, returning the copy
|
|
public ShaderInput AddCopyOfShaderInput(ShaderInput source, int insertIndex = -1)
|
|
{
|
|
ShaderInput copy = source.Copy();
|
|
|
|
// some ShaderInputs cannot be copied (unknown types)
|
|
if (copy == null)
|
|
return null;
|
|
|
|
// copy common properties that should always be copied over
|
|
copy.generatePropertyBlock = source.generatePropertyBlock; // the exposed toggle
|
|
|
|
if ((source is AbstractShaderProperty sourceProp) && (copy is AbstractShaderProperty copyProp))
|
|
{
|
|
copyProp.hidden = sourceProp.hidden;
|
|
copyProp.precision = sourceProp.precision;
|
|
copyProp.overrideHLSLDeclaration = sourceProp.overrideHLSLDeclaration;
|
|
copyProp.hlslDeclarationOverride = sourceProp.hlslDeclarationOverride;
|
|
copyProp.useCustomSlotLabel = sourceProp.useCustomSlotLabel;
|
|
}
|
|
|
|
// sanitize the display name (we let the .Copy() function actually copy the display name over)
|
|
copy.SetDisplayNameAndSanitizeForGraph(this);
|
|
|
|
// copy and sanitize the reference name (must do this after the display name, so the default is correct)
|
|
if (source.IsUsingNewDefaultRefName())
|
|
{
|
|
// if source was using new default, we can just rely on the default for the copy we made.
|
|
// the code above has already handled collisions properly for the default,
|
|
// and it will assign the same name as the source if there are no collisions.
|
|
// Also it will result better names chosen when there are collisions.
|
|
}
|
|
else
|
|
{
|
|
// when the source is using an old default, we set it as an override
|
|
copy.SetReferenceNameAndSanitizeForGraph(this, source.referenceName);
|
|
}
|
|
|
|
copy.OnBeforePasteIntoGraph(this);
|
|
AddGraphInputNoSanitization(copy, insertIndex);
|
|
|
|
return copy;
|
|
}
|
|
|
|
public void RemoveGraphInput(ShaderInput input)
|
|
{
|
|
switch (input)
|
|
{
|
|
case AbstractShaderProperty property:
|
|
var propertyNodes = GetNodes<PropertyNode>().Where(x => x.property == input).ToList();
|
|
foreach (var propertyNode in propertyNodes)
|
|
ReplacePropertyNodeWithConcreteNodeNoValidate(propertyNode);
|
|
break;
|
|
}
|
|
|
|
// Also remove this input from any category it existed in
|
|
foreach (var categoryData in categories)
|
|
{
|
|
if (categoryData.IsItemInCategory(input))
|
|
{
|
|
categoryData.RemoveItemFromCategory(input);
|
|
break;
|
|
}
|
|
}
|
|
|
|
RemoveGraphInputNoValidate(input);
|
|
ValidateGraph();
|
|
}
|
|
|
|
public void MoveCategory(CategoryData category, int newIndex)
|
|
{
|
|
if (newIndex > m_CategoryData.Count || newIndex < 0)
|
|
{
|
|
AssertHelpers.Fail("New index is not within categories list.");
|
|
return;
|
|
}
|
|
var currentIndex = m_CategoryData.IndexOf(category);
|
|
if (currentIndex == -1)
|
|
{
|
|
AssertHelpers.Fail("Category is not in graph.");
|
|
return;
|
|
}
|
|
if (newIndex == currentIndex)
|
|
return;
|
|
m_CategoryData.RemoveAt(currentIndex);
|
|
if (newIndex > currentIndex)
|
|
newIndex--;
|
|
var isLast = newIndex == m_CategoryData.Count;
|
|
if (isLast)
|
|
m_CategoryData.Add(category);
|
|
else
|
|
m_CategoryData.Insert(newIndex, category);
|
|
if (!m_MovedCategories.Contains(category))
|
|
m_MovedCategories.Add(category);
|
|
}
|
|
|
|
public void MoveItemInCategory(ShaderInput itemToMove, int newIndex, string associatedCategoryGuid)
|
|
{
|
|
foreach (var categoryData in categories)
|
|
{
|
|
if (categoryData.categoryGuid == associatedCategoryGuid && categoryData.IsItemInCategory(itemToMove))
|
|
{
|
|
// Validate new index to move the item to
|
|
if (newIndex < -1 || newIndex >= categoryData.childCount)
|
|
{
|
|
AssertHelpers.Fail("Provided invalid index input to MoveItemInCategory.");
|
|
return;
|
|
}
|
|
|
|
categoryData.MoveItemInCategory(itemToMove, newIndex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public int GetGraphInputIndex(ShaderInput input)
|
|
{
|
|
switch (input)
|
|
{
|
|
case AbstractShaderProperty property:
|
|
return m_Properties.IndexOf(property);
|
|
case ShaderKeyword keyword:
|
|
return m_Keywords.IndexOf(keyword);
|
|
case ShaderDropdown dropdown:
|
|
return m_Dropdowns.IndexOf(dropdown);
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
void RemoveGraphInputNoValidate(ShaderInput shaderInput)
|
|
{
|
|
if (shaderInput is AbstractShaderProperty property && m_Properties.Remove(property) ||
|
|
shaderInput is ShaderKeyword keyword && m_Keywords.Remove(keyword) ||
|
|
shaderInput is ShaderDropdown dropdown && m_Dropdowns.Remove(dropdown))
|
|
{
|
|
m_RemovedInputs.Add(shaderInput);
|
|
m_AddedInputs.Remove(shaderInput);
|
|
m_MovedInputs.Remove(shaderInput);
|
|
}
|
|
}
|
|
|
|
static List<IEdge> s_TempEdges = new List<IEdge>();
|
|
|
|
public void ReplacePropertyNodeWithConcreteNode(PropertyNode propertyNode)
|
|
{
|
|
ReplacePropertyNodeWithConcreteNodeNoValidate(propertyNode);
|
|
ValidateGraph();
|
|
}
|
|
|
|
void ReplacePropertyNodeWithConcreteNodeNoValidate(PropertyNode propertyNode, bool deleteNodeIfNoConcreteFormExists = true)
|
|
{
|
|
var property = properties.FirstOrDefault(x => x == propertyNode.property);
|
|
if (property == null)
|
|
return;
|
|
|
|
var node = property.ToConcreteNode() as AbstractMaterialNode;
|
|
if (node == null) // Some nodes have no concrete form
|
|
{
|
|
if (deleteNodeIfNoConcreteFormExists)
|
|
RemoveNodeNoValidate(propertyNode);
|
|
return;
|
|
}
|
|
|
|
var slot = propertyNode.FindOutputSlot<MaterialSlot>(PropertyNode.OutputSlotId);
|
|
var newSlot = node.GetOutputSlots<MaterialSlot>().FirstOrDefault(s => s.valueType == slot.valueType);
|
|
if (newSlot == null)
|
|
return;
|
|
|
|
node.drawState = propertyNode.drawState;
|
|
node.group = propertyNode.group;
|
|
AddNodeNoValidate(node);
|
|
|
|
foreach (var edge in this.GetEdges(slot.slotReference))
|
|
ConnectNoValidate(newSlot.slotReference, edge.inputSlot);
|
|
|
|
RemoveNodeNoValidate(propertyNode);
|
|
}
|
|
|
|
public void AddCategory(CategoryData categoryDataReference)
|
|
{
|
|
m_CategoryData.Add(categoryDataReference);
|
|
m_AddedCategories.Add(categoryDataReference);
|
|
}
|
|
|
|
public string FindCategoryForInput(ShaderInput input)
|
|
{
|
|
foreach (var categoryData in categories)
|
|
{
|
|
if (categoryData.IsItemInCategory(input))
|
|
{
|
|
return categoryData.categoryGuid;
|
|
}
|
|
}
|
|
|
|
AssertHelpers.Fail("Attempted to find category for an input that doesn't exist in the graph.");
|
|
return String.Empty;
|
|
}
|
|
|
|
public void ChangeCategoryName(string categoryGUID, string newName)
|
|
{
|
|
foreach (var categoryData in categories)
|
|
{
|
|
if (categoryData.categoryGuid == categoryGUID)
|
|
{
|
|
var sanitizedCategoryName = GraphUtil.SanitizeCategoryName(newName);
|
|
categoryData.name = sanitizedCategoryName;
|
|
return;
|
|
}
|
|
}
|
|
|
|
AssertHelpers.Fail("Attempted to change name of a category that does not exist in the graph.");
|
|
}
|
|
|
|
public void InsertItemIntoCategory(string categoryGUID, ShaderInput itemToAdd, int insertionIndex = -1)
|
|
{
|
|
foreach (var categoryData in categories)
|
|
{
|
|
if (categoryData.categoryGuid == categoryGUID)
|
|
{
|
|
categoryData.InsertItemIntoCategory(itemToAdd, insertionIndex);
|
|
}
|
|
// Also make sure to remove this items guid from an existing category if it exists within one
|
|
else if (categoryData.IsItemInCategory(itemToAdd))
|
|
{
|
|
categoryData.RemoveItemFromCategory(itemToAdd);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RemoveItemFromCategory(string categoryGUID, ShaderInput itemToRemove)
|
|
{
|
|
foreach (var categoryData in categories)
|
|
{
|
|
if (categoryData.categoryGuid == categoryGUID)
|
|
{
|
|
categoryData.RemoveItemFromCategory(itemToRemove);
|
|
return;
|
|
}
|
|
}
|
|
|
|
AssertHelpers.Fail("Attempted to remove item from a category that does not exist in the graph.");
|
|
}
|
|
|
|
public void RemoveCategory(string categoryGUID)
|
|
{
|
|
var existingCategory = categories.FirstOrDefault(category => category.categoryGuid == categoryGUID);
|
|
if (existingCategory != null)
|
|
{
|
|
m_CategoryData.Remove(existingCategory);
|
|
m_RemovedCategories.Add(existingCategory);
|
|
|
|
// Whenever a category is removed, also remove any inputs within that category
|
|
foreach (var shaderInput in existingCategory.Children)
|
|
RemoveGraphInput(shaderInput);
|
|
}
|
|
else
|
|
AssertHelpers.Fail("Attempted to remove a category that does not exist in the graph.");
|
|
}
|
|
|
|
// This differs from the rest of the category handling functions due to how categories can be copied between graphs
|
|
// Since we have no guarantee of us owning the categories, we need a direct reference to the category to copy
|
|
public CategoryData CopyCategory(CategoryData categoryToCopy)
|
|
{
|
|
var copiedCategory = new CategoryData(categoryToCopy);
|
|
AddCategory(copiedCategory);
|
|
// Whenever a category is copied, also copy over all the inputs within that category
|
|
foreach (var childInputToCopy in categoryToCopy.Children)
|
|
{
|
|
var newShaderInput = AddCopyOfShaderInput(childInputToCopy);
|
|
copiedCategory.InsertItemIntoCategory(newShaderInput);
|
|
}
|
|
|
|
return copiedCategory;
|
|
}
|
|
|
|
public void OnKeywordChanged()
|
|
{
|
|
OnKeywordChangedNoValidate();
|
|
ValidateGraph();
|
|
}
|
|
|
|
public void OnKeywordChangedNoValidate()
|
|
{
|
|
var allNodes = GetNodes<AbstractMaterialNode>();
|
|
foreach (AbstractMaterialNode node in allNodes)
|
|
{
|
|
node.Dirty(ModificationScope.Topological);
|
|
node.ValidateNode();
|
|
}
|
|
}
|
|
|
|
public void OnDropdownChanged()
|
|
{
|
|
OnDropdownChangedNoValidate();
|
|
ValidateGraph();
|
|
}
|
|
|
|
public void OnDropdownChangedNoValidate()
|
|
{
|
|
var allNodes = GetNodes<AbstractMaterialNode>();
|
|
foreach (AbstractMaterialNode node in allNodes)
|
|
{
|
|
node.Dirty(ModificationScope.Topological);
|
|
node.ValidateNode();
|
|
}
|
|
}
|
|
|
|
public void CleanupGraph()
|
|
{
|
|
//First validate edges, remove any
|
|
//orphans. This can happen if a user
|
|
//manually modifies serialized data
|
|
//of if they delete a node in the inspector
|
|
//debug view.
|
|
foreach (var edge in edges.ToArray())
|
|
{
|
|
var outputNode = edge.outputSlot.node;
|
|
var inputNode = edge.inputSlot.node;
|
|
|
|
MaterialSlot outputSlot = null;
|
|
MaterialSlot inputSlot = null;
|
|
if (ContainsNode(outputNode) && ContainsNode(inputNode))
|
|
{
|
|
outputSlot = outputNode.FindOutputSlot<MaterialSlot>(edge.outputSlot.slotId);
|
|
inputSlot = inputNode.FindInputSlot<MaterialSlot>(edge.inputSlot.slotId);
|
|
}
|
|
|
|
if (outputNode == null
|
|
|| inputNode == null
|
|
|| outputSlot == null
|
|
|| inputSlot == null)
|
|
{
|
|
//orphaned edge
|
|
RemoveEdgeNoValidate(edge, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ValidateGraph()
|
|
{
|
|
messageManager?.ClearAllFromProvider(this);
|
|
CleanupGraph();
|
|
GraphSetup.SetupGraph(this);
|
|
GraphConcretization.ConcretizeGraph(this);
|
|
GraphValidation.ValidateGraph(this);
|
|
|
|
foreach (var edge in m_AddedEdges.ToList())
|
|
{
|
|
if (!ContainsNode(edge.outputSlot.node) || !ContainsNode(edge.inputSlot.node))
|
|
{
|
|
Debug.LogWarningFormat("Added edge is invalid: {0} -> {1}\n{2}", edge.outputSlot.node.objectId, edge.inputSlot.node.objectId, Environment.StackTrace);
|
|
m_AddedEdges.Remove(edge);
|
|
}
|
|
}
|
|
|
|
foreach (var groupChange in m_ParentGroupChanges.ToList())
|
|
{
|
|
if (groupChange.groupItem is AbstractMaterialNode node && !ContainsNode(node))
|
|
{
|
|
m_ParentGroupChanges.Remove(groupChange);
|
|
}
|
|
|
|
if (groupChange.groupItem is StickyNoteData stickyNote && !m_StickyNoteDatas.Contains(stickyNote))
|
|
{
|
|
m_ParentGroupChanges.Remove(groupChange);
|
|
}
|
|
}
|
|
|
|
var existingDefaultCategory = categories.FirstOrDefault();
|
|
if (existingDefaultCategory?.childCount == 0 && categories.Count() == 1 && (properties.Count() != 0 || keywords.Count() != 0 || dropdowns.Count() != 0))
|
|
{
|
|
// Have a graph with category data in invalid state
|
|
// there is only one category, the default category, and all shader inputs should belong to it
|
|
// Clear category data as it will get reconstructed in the BlackboardController constructor
|
|
m_CategoryData.Clear();
|
|
}
|
|
|
|
|
|
ValidateCustomBlockLimit();
|
|
ValidateContextBlocks();
|
|
}
|
|
|
|
public void AddValidationError(string id, string errorMessage,
|
|
ShaderCompilerMessageSeverity severity = ShaderCompilerMessageSeverity.Error)
|
|
{
|
|
messageManager?.AddOrAppendError(this, id, new ShaderMessage("Validation: " + errorMessage, severity));
|
|
}
|
|
|
|
public void AddSetupError(string id, string errorMessage,
|
|
ShaderCompilerMessageSeverity severity = ShaderCompilerMessageSeverity.Error)
|
|
{
|
|
messageManager?.AddOrAppendError(this, id, new ShaderMessage("Setup: " + errorMessage, severity));
|
|
}
|
|
|
|
public void AddConcretizationError(string id, string errorMessage,
|
|
ShaderCompilerMessageSeverity severity = ShaderCompilerMessageSeverity.Error)
|
|
{
|
|
messageManager?.AddOrAppendError(this, id, new ShaderMessage("Concretization: " + errorMessage, severity));
|
|
}
|
|
|
|
public void ClearErrorsForNode(AbstractMaterialNode node)
|
|
{
|
|
messageManager?.ClearNodesFromProvider(this, node.ToEnumerable());
|
|
}
|
|
|
|
public void ReplaceWith(GraphData other)
|
|
{
|
|
if (other == null)
|
|
throw new ArgumentException("Can only replace with another AbstractMaterialGraph", "other");
|
|
|
|
m_GraphPrecision = other.m_GraphPrecision;
|
|
m_PreviewMode = other.m_PreviewMode;
|
|
m_OutputNode = other.m_OutputNode;
|
|
|
|
if ((this.vertexContext.position != other.vertexContext.position) ||
|
|
(this.fragmentContext.position != other.fragmentContext.position))
|
|
{
|
|
this.vertexContext.position = other.vertexContext.position;
|
|
this.fragmentContext.position = other.fragmentContext.position;
|
|
m_MovedContexts = true;
|
|
}
|
|
|
|
using (var inputsToRemove = PooledList<ShaderInput>.Get())
|
|
{
|
|
foreach (var property in m_Properties.SelectValue())
|
|
inputsToRemove.Add(property);
|
|
foreach (var keyword in m_Keywords.SelectValue())
|
|
inputsToRemove.Add(keyword);
|
|
foreach (var dropdown in m_Dropdowns.SelectValue())
|
|
inputsToRemove.Add(dropdown);
|
|
foreach (var input in inputsToRemove)
|
|
RemoveGraphInputNoValidate(input);
|
|
}
|
|
foreach (var otherProperty in other.properties)
|
|
{
|
|
AddGraphInputNoSanitization(otherProperty);
|
|
}
|
|
foreach (var otherKeyword in other.keywords)
|
|
{
|
|
AddGraphInputNoSanitization(otherKeyword);
|
|
}
|
|
foreach (var otherDropdown in other.dropdowns)
|
|
{
|
|
AddGraphInputNoSanitization(otherDropdown);
|
|
}
|
|
|
|
other.ValidateGraph();
|
|
ValidateGraph();
|
|
|
|
// Current tactic is to remove all nodes and edges and then re-add them, such that depending systems
|
|
// will re-initialize with new references.
|
|
|
|
using (ListPool<GroupData>.Get(out var removedGroupDatas))
|
|
{
|
|
removedGroupDatas.AddRange(m_GroupDatas.SelectValue());
|
|
foreach (var groupData in removedGroupDatas)
|
|
{
|
|
RemoveGroupNoValidate(groupData);
|
|
}
|
|
}
|
|
|
|
using (ListPool<StickyNoteData>.Get(out var removedNoteDatas))
|
|
{
|
|
removedNoteDatas.AddRange(m_StickyNoteDatas.SelectValue());
|
|
foreach (var groupData in removedNoteDatas)
|
|
{
|
|
RemoveNoteNoValidate(groupData);
|
|
}
|
|
}
|
|
|
|
using (var pooledList = ListPool<IEdge>.Get(out var removedNodeEdges))
|
|
{
|
|
removedNodeEdges.AddRange(m_Edges);
|
|
foreach (var edge in removedNodeEdges)
|
|
RemoveEdgeNoValidate(edge);
|
|
}
|
|
|
|
using (var nodesToRemove = PooledList<AbstractMaterialNode>.Get())
|
|
{
|
|
nodesToRemove.AddRange(m_Nodes.SelectValue());
|
|
foreach (var node in nodesToRemove)
|
|
RemoveNodeNoValidate(node);
|
|
}
|
|
|
|
// Clear category data too before re-adding
|
|
m_CategoryData.Clear();
|
|
|
|
ValidateGraph();
|
|
|
|
foreach (GroupData groupData in other.groups)
|
|
AddGroup(groupData);
|
|
|
|
// If categories are ever removed completely, make sure there is always one default category that exists
|
|
if (!other.categories.Any())
|
|
{
|
|
AddCategory(CategoryData.DefaultCategory());
|
|
}
|
|
else
|
|
{
|
|
foreach (CategoryData categoryData in other.categories)
|
|
{
|
|
AddCategory(categoryData);
|
|
}
|
|
}
|
|
|
|
|
|
foreach (var stickyNote in other.stickyNotes)
|
|
{
|
|
AddStickyNote(stickyNote);
|
|
}
|
|
|
|
foreach (var node in other.GetNodes<AbstractMaterialNode>())
|
|
{
|
|
if (node is BlockNode blockNode)
|
|
{
|
|
var contextData = blockNode.descriptor.shaderStage == ShaderStage.Vertex ? vertexContext : fragmentContext;
|
|
AddBlockNoValidate(blockNode, contextData, blockNode.index);
|
|
}
|
|
else
|
|
{
|
|
AddNodeNoValidate(node);
|
|
}
|
|
}
|
|
|
|
foreach (var edge in other.edges)
|
|
{
|
|
ConnectNoValidate(edge.outputSlot, edge.inputSlot);
|
|
}
|
|
|
|
outputNode = other.outputNode;
|
|
|
|
// clear our local active targets and copy state from the other GraphData
|
|
|
|
// NOTE: we DO NOT clear or rebuild m_AllPotentialTargets, in order to
|
|
// retain the data from any inactive targets.
|
|
// this allows the user can add them back and keep the old settings
|
|
|
|
m_ActiveTargets.Clear();
|
|
foreach (var target in other.activeTargets)
|
|
{
|
|
// Ensure target inits correctly
|
|
var context = new TargetSetupContext();
|
|
target.Setup(ref context);
|
|
SetTargetActive(target, true);
|
|
}
|
|
SortActiveTargets();
|
|
|
|
// Active blocks
|
|
var activeBlocks = GetActiveBlocksForAllActiveTargets();
|
|
UpdateActiveBlocks(activeBlocks);
|
|
ValidateGraph();
|
|
}
|
|
|
|
internal void PasteGraph(CopyPasteGraph graphToPaste, List<AbstractMaterialNode> remappedNodes,
|
|
List<Edge> remappedEdges)
|
|
{
|
|
var groupMap = new Dictionary<GroupData, GroupData>();
|
|
foreach (var group in graphToPaste.groups)
|
|
{
|
|
var position = group.position;
|
|
position.x += 30;
|
|
position.y += 30;
|
|
|
|
GroupData newGroup = new GroupData(group.title, position);
|
|
|
|
groupMap[group] = newGroup;
|
|
|
|
AddGroup(newGroup);
|
|
m_PastedGroups.Add(newGroup);
|
|
}
|
|
|
|
foreach (var stickyNote in graphToPaste.stickyNotes)
|
|
{
|
|
var position = stickyNote.position;
|
|
position.x += 30;
|
|
position.y += 30;
|
|
|
|
StickyNoteData pastedStickyNote = new StickyNoteData(stickyNote.title, stickyNote.content, position);
|
|
pastedStickyNote.textSize = stickyNote.textSize;
|
|
pastedStickyNote.theme = stickyNote.theme;
|
|
if (stickyNote.group != null && groupMap.ContainsKey(stickyNote.group))
|
|
{
|
|
pastedStickyNote.group = groupMap[stickyNote.group];
|
|
}
|
|
|
|
AddStickyNote(pastedStickyNote);
|
|
m_PastedStickyNotes.Add(pastedStickyNote);
|
|
}
|
|
|
|
var edges = graphToPaste.edges.ToList();
|
|
var nodeList = graphToPaste.GetNodes<AbstractMaterialNode>();
|
|
foreach (var node in nodeList)
|
|
{
|
|
// cannot paste block nodes, or unknown node types
|
|
if ((node is BlockNode) || (node is MultiJsonInternal.UnknownNodeType))
|
|
continue;
|
|
|
|
if (!IsInputAllowedInGraph(node))
|
|
continue;
|
|
|
|
AbstractMaterialNode pastedNode = node;
|
|
|
|
// Check if the property nodes need to be made into a concrete node.
|
|
if (node is PropertyNode propertyNode)
|
|
{
|
|
// If the property is not in the current graph, do check if the
|
|
// property can be made into a concrete node.
|
|
var property = m_Properties.SelectValue().FirstOrDefault(x => x.objectId == propertyNode.property.objectId
|
|
|| (x.propertyType == propertyNode.property.propertyType && x.referenceName == propertyNode.property.referenceName));
|
|
if (property != null)
|
|
{
|
|
propertyNode.property = property;
|
|
}
|
|
else
|
|
{
|
|
pastedNode = propertyNode.property.ToConcreteNode();
|
|
// some property nodes cannot be concretized.. fail to paste them
|
|
if (pastedNode == null)
|
|
continue;
|
|
pastedNode.drawState = node.drawState;
|
|
for (var i = 0; i < edges.Count; i++)
|
|
{
|
|
var edge = edges[i];
|
|
if (edge.outputSlot.node == node)
|
|
{
|
|
edges[i] = new Edge(new SlotReference(pastedNode, edge.outputSlot.slotId), edge.inputSlot);
|
|
}
|
|
else if (edge.inputSlot.node == node)
|
|
{
|
|
edges[i] = new Edge(edge.outputSlot, new SlotReference(pastedNode, edge.inputSlot.slotId));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the node has a group guid and no group has been copied, reset the group guid.
|
|
// Check if the node is inside a group
|
|
if (node.group != null)
|
|
{
|
|
if (groupMap.ContainsKey(node.group))
|
|
{
|
|
var absNode = pastedNode;
|
|
absNode.group = groupMap[node.group];
|
|
pastedNode = absNode;
|
|
}
|
|
else
|
|
{
|
|
pastedNode.group = null;
|
|
}
|
|
}
|
|
|
|
remappedNodes.Add(pastedNode);
|
|
AddNode(pastedNode);
|
|
|
|
// add the node to the pasted node list
|
|
m_PastedNodes.Add(pastedNode);
|
|
|
|
// Check if the keyword nodes need to have their keywords copied.
|
|
if (node is KeywordNode keywordNode)
|
|
{
|
|
var keyword = m_Keywords.SelectValue().FirstOrDefault(x => x.objectId == keywordNode.keyword.objectId
|
|
|| (x.keywordType == keywordNode.keyword.keywordType && x.referenceName == keywordNode.keyword.referenceName));
|
|
if (keyword != null)
|
|
{
|
|
keywordNode.keyword = keyword;
|
|
}
|
|
else
|
|
{
|
|
owner.graphDataStore.Dispatch(new AddShaderInputAction() { shaderInputReference = keywordNode.keyword });
|
|
}
|
|
|
|
// Always update Keyword nodes to handle any collisions resolved on the Keyword
|
|
keywordNode.UpdateNode();
|
|
}
|
|
|
|
// Check if the dropdown nodes need to have their dropdowns copied.
|
|
if (node is DropdownNode dropdownNode)
|
|
{
|
|
var dropdown = m_Dropdowns.SelectValue().FirstOrDefault(x => x.objectId == dropdownNode.dropdown.objectId
|
|
|| x.referenceName == dropdownNode.dropdown.referenceName);
|
|
if (dropdown != null)
|
|
{
|
|
dropdownNode.dropdown = dropdown;
|
|
}
|
|
else
|
|
{
|
|
owner.graphDataStore.Dispatch(new AddShaderInputAction() { shaderInputReference = dropdownNode.dropdown });
|
|
}
|
|
|
|
// Always update Dropdown nodes to handle any collisions resolved on the Keyword
|
|
dropdownNode.UpdateNode();
|
|
}
|
|
}
|
|
|
|
foreach (var edge in edges)
|
|
{
|
|
var newEdge = (Edge)Connect(edge.outputSlot, edge.inputSlot);
|
|
if (newEdge != null)
|
|
{
|
|
remappedEdges.Add(newEdge);
|
|
}
|
|
}
|
|
|
|
ValidateGraph();
|
|
}
|
|
|
|
public override void OnBeforeSerialize()
|
|
{
|
|
m_Edges.Sort();
|
|
ChangeVersion(latestVersion);
|
|
}
|
|
|
|
static T DeserializeLegacy<T>(string typeString, string json, Guid? overrideObjectId = null) where T : JsonObject
|
|
{
|
|
var jsonObj = MultiJsonInternal.CreateInstanceForDeserialization(typeString);
|
|
var value = jsonObj as T;
|
|
if (value == null)
|
|
{
|
|
Debug.Log($"Cannot create instance for {typeString}");
|
|
return null;
|
|
}
|
|
|
|
// by default, MultiJsonInternal.CreateInstance will create a new objectID randomly..
|
|
// we need some created objects to have deterministic objectIDs, because they affect the generated shader.
|
|
// if the generated shader is not deterministic, it can create ripple effects (i.e. causing Materials to be modified randomly as properties are renamed)
|
|
// so we provide this path to allow the calling code to override the objectID with something deterministic
|
|
if (overrideObjectId.HasValue)
|
|
value.OverrideObjectId(overrideObjectId.Value.ToString("N"));
|
|
MultiJsonInternal.Enqueue(value, json);
|
|
return value as T;
|
|
}
|
|
|
|
static AbstractMaterialNode DeserializeLegacyNode(string typeString, string json, Guid? overrideObjectId = null)
|
|
{
|
|
var jsonObj = MultiJsonInternal.CreateInstanceForDeserialization(typeString);
|
|
var value = jsonObj as AbstractMaterialNode;
|
|
if (value == null)
|
|
{
|
|
//Special case - want to support nodes of unknwon type for cross pipeline compatability
|
|
value = new LegacyUnknownTypeNode(typeString, json);
|
|
if (overrideObjectId.HasValue)
|
|
value.OverrideObjectId(overrideObjectId.Value.ToString("N"));
|
|
MultiJsonInternal.Enqueue(value, json);
|
|
return value as AbstractMaterialNode;
|
|
}
|
|
else
|
|
{
|
|
if (overrideObjectId.HasValue)
|
|
value.OverrideObjectId(overrideObjectId.Value.ToString("N"));
|
|
MultiJsonInternal.Enqueue(value, json);
|
|
return value as AbstractMaterialNode;
|
|
}
|
|
}
|
|
|
|
public override void OnAfterDeserialize(string json)
|
|
{
|
|
if (sgVersion == 0)
|
|
{
|
|
var graphData0 = JsonUtility.FromJson<GraphData0>(json);
|
|
//If a graph was previously updated to V2, since we had to rename m_Version to m_SGVersion to avoid collision with an upgrade system from
|
|
//HDRP, we have to handle the case that our version might not be correct -
|
|
if (graphData0.m_Version > 0)
|
|
{
|
|
sgVersion = graphData0.m_Version;
|
|
}
|
|
else
|
|
{
|
|
// graphData.m_Version == 0 (matches current sgVersion)
|
|
Guid assetGuid;
|
|
if (!Guid.TryParse(this.assetGuid, out assetGuid))
|
|
assetGuid = JsonObject.GenerateNamespaceUUID(Guid.Empty, json);
|
|
|
|
var nodeGuidMap = new Dictionary<string, AbstractMaterialNode>();
|
|
var propertyGuidMap = new Dictionary<string, AbstractShaderProperty>();
|
|
var keywordGuidMap = new Dictionary<string, ShaderKeyword>();
|
|
var groupGuidMap = new Dictionary<string, GroupData>();
|
|
var slotsField = typeof(AbstractMaterialNode).GetField("m_Slots", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
var propertyField = typeof(PropertyNode).GetField("m_Property", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
var keywordField = typeof(KeywordNode).GetField("m_Keyword", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
var dropdownField = typeof(DropdownNode).GetField("m_Dropdown", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
var defaultReferenceNameField = typeof(ShaderInput).GetField("m_DefaultReferenceName", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
m_GroupDatas.Clear();
|
|
m_StickyNoteDatas.Clear();
|
|
|
|
foreach (var group0 in graphData0.m_Groups)
|
|
{
|
|
var group = new GroupData(group0.m_Title, group0.m_Position);
|
|
m_GroupDatas.Add(group);
|
|
if (!groupGuidMap.ContainsKey(group0.m_GuidSerialized))
|
|
{
|
|
groupGuidMap.Add(group0.m_GuidSerialized, group);
|
|
}
|
|
else if (!groupGuidMap[group0.m_GuidSerialized].Equals(group.objectId))
|
|
{
|
|
Debug.LogError("Group id mismatch");
|
|
}
|
|
}
|
|
|
|
foreach (var serializedProperty in graphData0.m_SerializedProperties)
|
|
{
|
|
var propObjectId = JsonObject.GenerateNamespaceUUID(assetGuid, serializedProperty.JSONnodeData);
|
|
var property = DeserializeLegacy<AbstractShaderProperty>(serializedProperty.typeInfo.fullName, serializedProperty.JSONnodeData, propObjectId);
|
|
if (property == null)
|
|
continue;
|
|
|
|
m_Properties.Add(property);
|
|
|
|
var input0 = JsonUtility.FromJson<ShaderInput0>(serializedProperty.JSONnodeData);
|
|
propertyGuidMap[input0.m_Guid.m_GuidSerialized] = property;
|
|
|
|
// Fix up missing reference names
|
|
// Properties on Sub Graphs in V0 never have reference names serialized
|
|
// To maintain Sub Graph node property mapping we force guid based reference names on upgrade
|
|
if (string.IsNullOrEmpty((string)defaultReferenceNameField.GetValue(property)))
|
|
{
|
|
// ColorShaderProperty is the only Property case where `GetDefaultReferenceName` was overriden
|
|
if (MultiJson.ParseType(serializedProperty.typeInfo.fullName) == typeof(ColorShaderProperty))
|
|
{
|
|
defaultReferenceNameField.SetValue(property, $"Color_{GuidEncoder.Encode(Guid.Parse(input0.m_Guid.m_GuidSerialized))}");
|
|
}
|
|
else
|
|
{
|
|
defaultReferenceNameField.SetValue(property, $"{property.concreteShaderValueType}_{GuidEncoder.Encode(Guid.Parse(input0.m_Guid.m_GuidSerialized))}");
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var serializedKeyword in graphData0.m_SerializedKeywords)
|
|
{
|
|
var keyword = DeserializeLegacy<ShaderKeyword>(serializedKeyword.typeInfo.fullName, serializedKeyword.JSONnodeData);
|
|
if (keyword == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
m_Keywords.Add(keyword);
|
|
|
|
var input0 = JsonUtility.FromJson<ShaderInput0>(serializedKeyword.JSONnodeData);
|
|
keywordGuidMap[input0.m_Guid.m_GuidSerialized] = keyword;
|
|
}
|
|
|
|
foreach (var serializedNode in graphData0.m_SerializableNodes)
|
|
{
|
|
var node0 = JsonUtility.FromJson<AbstractMaterialNode0>(serializedNode.JSONnodeData);
|
|
|
|
var nodeObjectId = JsonObject.GenerateNamespaceUUID(node0.m_GuidSerialized, "node");
|
|
var node = DeserializeLegacyNode(serializedNode.typeInfo.fullName, serializedNode.JSONnodeData, nodeObjectId);
|
|
if (node == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
nodeGuidMap.Add(node0.m_GuidSerialized, node);
|
|
m_Nodes.Add(node);
|
|
|
|
if (!string.IsNullOrEmpty(node0.m_PropertyGuidSerialized) && propertyGuidMap.TryGetValue(node0.m_PropertyGuidSerialized, out var property))
|
|
{
|
|
propertyField.SetValue(node, (JsonRef<AbstractShaderProperty>)property);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(node0.m_KeywordGuidSerialized) && keywordGuidMap.TryGetValue(node0.m_KeywordGuidSerialized, out var keyword))
|
|
{
|
|
keywordField.SetValue(node, (JsonRef<ShaderKeyword>)keyword);
|
|
}
|
|
|
|
var slots = (List<JsonData<MaterialSlot>>)slotsField.GetValue(node);
|
|
slots.Clear();
|
|
|
|
foreach (var serializedSlot in node0.m_SerializableSlots)
|
|
{
|
|
var slotObjectId = JsonObject.GenerateNamespaceUUID(node0.m_GuidSerialized, serializedSlot.JSONnodeData);
|
|
var slot = DeserializeLegacy<MaterialSlot>(serializedSlot.typeInfo.fullName, serializedSlot.JSONnodeData, slotObjectId);
|
|
if (slot == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
slots.Add(slot);
|
|
}
|
|
|
|
if (!String.IsNullOrEmpty(node0.m_GroupGuidSerialized))
|
|
{
|
|
if (groupGuidMap.TryGetValue(node0.m_GroupGuidSerialized, out GroupData foundGroup))
|
|
{
|
|
node.group = foundGroup;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var stickyNote0 in graphData0.m_StickyNotes)
|
|
{
|
|
var stickyNote = new StickyNoteData(stickyNote0.m_Title, stickyNote0.m_Content, stickyNote0.m_Position);
|
|
if (!String.IsNullOrEmpty(stickyNote0.m_GroupGuidSerialized))
|
|
{
|
|
if (groupGuidMap.TryGetValue(stickyNote0.m_GroupGuidSerialized, out GroupData foundGroup))
|
|
{
|
|
stickyNote.group = foundGroup;
|
|
}
|
|
}
|
|
stickyNote.theme = stickyNote0.m_Theme;
|
|
stickyNote.textSize = stickyNote0.m_TextSize;
|
|
m_StickyNoteDatas.Add(stickyNote);
|
|
}
|
|
|
|
var subgraphOuput = GetNodes<SubGraphOutputNode>();
|
|
isSubGraph = subgraphOuput.Any();
|
|
|
|
if (isSubGraph)
|
|
{
|
|
m_OutputNode = subgraphOuput.FirstOrDefault();
|
|
}
|
|
else if (!string.IsNullOrEmpty(graphData0.m_ActiveOutputNodeGuidSerialized))
|
|
{
|
|
m_OutputNode = nodeGuidMap[graphData0.m_ActiveOutputNodeGuidSerialized];
|
|
}
|
|
else
|
|
{
|
|
m_OutputNode = (AbstractMaterialNode)GetNodes<IMasterNode1>().FirstOrDefault();
|
|
}
|
|
|
|
foreach (var serializedElement in graphData0.m_SerializableEdges)
|
|
{
|
|
var edge0 = JsonUtility.FromJson<Edge0>(serializedElement.JSONnodeData);
|
|
m_Edges.Add(new Edge(
|
|
new SlotReference(
|
|
nodeGuidMap[edge0.m_OutputSlot.m_NodeGUIDSerialized],
|
|
edge0.m_OutputSlot.m_SlotId),
|
|
new SlotReference(
|
|
nodeGuidMap[edge0.m_InputSlot.m_NodeGUIDSerialized],
|
|
edge0.m_InputSlot.m_SlotId)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
class OldGraphDataReadConcretePrecision
|
|
{
|
|
// old value just for upgrade
|
|
[SerializeField]
|
|
public ConcretePrecision m_ConcretePrecision = ConcretePrecision.Single;
|
|
};
|
|
|
|
public override void OnAfterMultiDeserialize(string json)
|
|
{
|
|
// Deferred upgrades
|
|
if (sgVersion != latestVersion)
|
|
{
|
|
if (sgVersion < 2)
|
|
{
|
|
var addedBlocks = ListPool<BlockFieldDescriptor>.Get();
|
|
|
|
void UpgradeFromBlockMap(Dictionary<BlockFieldDescriptor, int> blockMap)
|
|
{
|
|
// Map master node ports to blocks
|
|
if (blockMap != null)
|
|
{
|
|
foreach (var blockMapping in blockMap)
|
|
{
|
|
// Create a new BlockNode for each unique map entry
|
|
var descriptor = blockMapping.Key;
|
|
if (addedBlocks.Contains(descriptor))
|
|
continue;
|
|
|
|
addedBlocks.Add(descriptor);
|
|
|
|
var contextData = descriptor.shaderStage == ShaderStage.Fragment ? m_FragmentContext : m_VertexContext;
|
|
var block = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
|
|
block.Init(descriptor);
|
|
AddBlockNoValidate(block, contextData, contextData.blocks.Count);
|
|
|
|
// To avoid having to go around the following deserialization code
|
|
// We simply run OnBeforeSerialization here to ensure m_SerializedDescriptor is set
|
|
block.OnBeforeSerialize();
|
|
|
|
// Now remap the incoming edges to blocks
|
|
var slotId = blockMapping.Value;
|
|
var oldSlot = m_OutputNode.value.FindSlot<MaterialSlot>(slotId);
|
|
var newSlot = block.FindSlot<MaterialSlot>(0);
|
|
if (oldSlot == null)
|
|
continue;
|
|
|
|
var oldInputSlotRef = m_OutputNode.value.GetSlotReference(slotId);
|
|
var newInputSlotRef = block.GetSlotReference(0);
|
|
|
|
// Always copy the value over for convenience
|
|
newSlot.CopyValuesFrom(oldSlot);
|
|
|
|
for (int i = 0; i < m_Edges.Count; i++)
|
|
{
|
|
// Find all edges connected to the master node using slot ID from the block map
|
|
// Remove them and replace them with new edges connected to the block nodes
|
|
var edge = m_Edges[i];
|
|
if (edge.inputSlot.Equals(oldInputSlotRef))
|
|
{
|
|
var outputSlot = edge.outputSlot;
|
|
m_Edges.Remove(edge);
|
|
m_Edges.Add(new Edge(outputSlot, newInputSlotRef));
|
|
}
|
|
}
|
|
|
|
// manually handle a bug where fragment normal slots could get out of sync of the master node's set fragment normal space
|
|
if (descriptor == BlockFields.SurfaceDescription.NormalOS)
|
|
{
|
|
NormalMaterialSlot norm = newSlot as NormalMaterialSlot;
|
|
if (norm.space != CoordinateSpace.Object)
|
|
{
|
|
norm.space = CoordinateSpace.Object;
|
|
}
|
|
}
|
|
else if (descriptor == BlockFields.SurfaceDescription.NormalTS)
|
|
{
|
|
NormalMaterialSlot norm = newSlot as NormalMaterialSlot;
|
|
if (norm.space != CoordinateSpace.Tangent)
|
|
{
|
|
norm.space = CoordinateSpace.Tangent;
|
|
}
|
|
}
|
|
else if (descriptor == BlockFields.SurfaceDescription.NormalWS)
|
|
{
|
|
NormalMaterialSlot norm = newSlot as NormalMaterialSlot;
|
|
if (norm.space != CoordinateSpace.World)
|
|
{
|
|
norm.space = CoordinateSpace.World;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We need to call AddBlockNoValidate but this adds to m_AddedNodes resulting in duplicates
|
|
// Therefore we need to clear this list before the view is created
|
|
m_AddedNodes.Clear();
|
|
}
|
|
}
|
|
|
|
var masterNode = m_OutputNode.value as IMasterNode1;
|
|
|
|
// This is required for edge lookup during Target upgrade
|
|
if (m_OutputNode.value != null)
|
|
{
|
|
m_OutputNode.value.owner = this;
|
|
}
|
|
foreach (var edge in m_Edges)
|
|
{
|
|
AddEdgeToNodeEdges(edge);
|
|
}
|
|
|
|
// Ensure correct initialization of Contexts
|
|
AddContexts();
|
|
|
|
// Position Contexts to the match master node
|
|
var oldPosition = Vector2.zero;
|
|
if (m_OutputNode.value != null)
|
|
{
|
|
oldPosition = m_OutputNode.value.drawState.position.position;
|
|
}
|
|
m_VertexContext.position = oldPosition;
|
|
m_FragmentContext.position = new Vector2(oldPosition.x, oldPosition.y + 200);
|
|
|
|
// Try to upgrade all potential targets from master node
|
|
if (masterNode != null)
|
|
{
|
|
foreach (var potentialTarget in m_AllPotentialTargets)
|
|
{
|
|
if (potentialTarget.IsUnknown())
|
|
continue;
|
|
|
|
var target = potentialTarget.GetTarget();
|
|
if (!(target is ILegacyTarget legacyTarget))
|
|
continue;
|
|
|
|
if (!legacyTarget.TryUpgradeFromMasterNode(masterNode, out var newBlockMap))
|
|
continue;
|
|
|
|
// upgrade succeeded! Activate it
|
|
SetTargetActive(target, true);
|
|
UpgradeFromBlockMap(newBlockMap);
|
|
}
|
|
SortActiveTargets();
|
|
}
|
|
|
|
// Clean up after upgrade
|
|
if (!isSubGraph)
|
|
{
|
|
m_OutputNode = null;
|
|
}
|
|
|
|
var masterNodes = GetNodes<IMasterNode1>().ToArray();
|
|
for (int i = 0; i < masterNodes.Length; i++)
|
|
{
|
|
var node = masterNodes.ElementAt(i) as AbstractMaterialNode;
|
|
m_Nodes.Remove(node);
|
|
}
|
|
|
|
m_NodeEdges.Clear();
|
|
}
|
|
|
|
if (sgVersion < 3)
|
|
{
|
|
var oldGraph = JsonUtility.FromJson<OldGraphDataReadConcretePrecision>(json);
|
|
|
|
// upgrade concrete precision to the new graph precision
|
|
switch (oldGraph.m_ConcretePrecision)
|
|
{
|
|
case ConcretePrecision.Half:
|
|
m_GraphPrecision = GraphPrecision.Half;
|
|
break;
|
|
case ConcretePrecision.Single:
|
|
m_GraphPrecision = GraphPrecision.Single;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ChangeVersion(latestVersion);
|
|
}
|
|
|
|
PooledList<(LegacyUnknownTypeNode, AbstractMaterialNode)> updatedNodes = PooledList<(LegacyUnknownTypeNode, AbstractMaterialNode)>.Get();
|
|
foreach (var node in m_Nodes.SelectValue())
|
|
{
|
|
if (node is LegacyUnknownTypeNode lNode && lNode.foundType != null)
|
|
{
|
|
AbstractMaterialNode legacyNode = (AbstractMaterialNode)Activator.CreateInstance(lNode.foundType);
|
|
JsonUtility.FromJsonOverwrite(lNode.serializedData, legacyNode);
|
|
legacyNode.group = lNode.group;
|
|
updatedNodes.Add((lNode, legacyNode));
|
|
}
|
|
}
|
|
foreach (var nodePair in updatedNodes)
|
|
{
|
|
m_Nodes.Add(nodePair.Item2);
|
|
ReplaceNodeWithNode(nodePair.Item1, nodePair.Item2);
|
|
}
|
|
updatedNodes.Dispose();
|
|
|
|
m_NodeDictionary = new Dictionary<string, AbstractMaterialNode>(m_Nodes.Count);
|
|
|
|
foreach (var group in m_GroupDatas.SelectValue())
|
|
{
|
|
m_GroupItems.Add(group, new List<IGroupItem>());
|
|
}
|
|
|
|
foreach (var node in m_Nodes.SelectValue())
|
|
{
|
|
node.owner = this;
|
|
node.UpdateNodeAfterDeserialization();
|
|
node.SetupSlots();
|
|
m_NodeDictionary.Add(node.objectId, node);
|
|
if (m_GroupItems.TryGetValue(node.group, out var groupItems))
|
|
{
|
|
groupItems.Add(node);
|
|
}
|
|
else
|
|
{
|
|
node.group = null;
|
|
}
|
|
}
|
|
|
|
foreach (var stickyNote in m_StickyNoteDatas.SelectValue())
|
|
{
|
|
if (m_GroupItems.TryGetValue(stickyNote.group, out var groupItems))
|
|
{
|
|
groupItems.Add(stickyNote);
|
|
}
|
|
else
|
|
{
|
|
stickyNote.group = null;
|
|
}
|
|
}
|
|
|
|
foreach (var edge in m_Edges)
|
|
AddEdgeToNodeEdges(edge);
|
|
|
|
// --------------------------------------------------
|
|
// Deserialize Contexts & Blocks
|
|
|
|
void DeserializeContextData(ContextData contextData, ShaderStage stage)
|
|
{
|
|
// Because Vertex/Fragment Contexts are serialized explicitly
|
|
// we do not need to serialize the Stage value on the ContextData
|
|
contextData.shaderStage = stage;
|
|
|
|
var blocks = contextData.blocks.SelectValue().ToList();
|
|
var blockCount = blocks.Count;
|
|
for (int i = 0; i < blockCount; i++)
|
|
{
|
|
// Update NonSerialized data on the BlockNode
|
|
var block = blocks[i];
|
|
// custom interpolators fully regenerate their own descriptor on deserialization
|
|
if (!block.isCustomBlock)
|
|
{
|
|
block.descriptor = m_BlockFieldDescriptors.FirstOrDefault(x => $"{x.tag}.{x.name}" == block.serializedDescriptor);
|
|
}
|
|
if (block.descriptor == null)
|
|
{
|
|
//Hit a descriptor that was not recognized from the assembly (likely from a different SRP)
|
|
//create a new entry for it and continue on
|
|
if (string.IsNullOrEmpty(block.serializedDescriptor))
|
|
{
|
|
throw new Exception($"Block {block} had no serialized descriptor");
|
|
}
|
|
|
|
var tmp = block.serializedDescriptor.Split('.');
|
|
if (tmp.Length != 2)
|
|
{
|
|
throw new Exception($"Block {block}'s serialized descriptor {block.serializedDescriptor} did not match expected format {{x.tag}}.{{x.name}}");
|
|
}
|
|
//right thing to do?
|
|
block.descriptor = new BlockFieldDescriptor(tmp[0], tmp[1], null, null, stage, true, true);
|
|
m_BlockFieldDescriptors.Add(block.descriptor);
|
|
}
|
|
block.contextData = contextData;
|
|
}
|
|
}
|
|
|
|
// First deserialize the ContextDatas
|
|
DeserializeContextData(m_VertexContext, ShaderStage.Vertex);
|
|
DeserializeContextData(m_FragmentContext, ShaderStage.Fragment);
|
|
|
|
// there should be no unknown potential targets at this point
|
|
Assert.IsFalse(m_AllPotentialTargets.Any(pt => pt.IsUnknown()));
|
|
|
|
foreach (var target in m_ActiveTargets.SelectValue())
|
|
{
|
|
var targetType = target.GetType();
|
|
if (targetType == typeof(MultiJsonInternal.UnknownTargetType))
|
|
{
|
|
// register any active UnknownTargetType as a potential target
|
|
m_AllPotentialTargets.Add(new PotentialTarget(target));
|
|
}
|
|
else
|
|
{
|
|
// active known targets should replace the stored Target in AllPotentialTargets
|
|
int targetIndex = m_AllPotentialTargets.FindIndex(pt => pt.knownType == targetType);
|
|
m_AllPotentialTargets[targetIndex].ReplaceStoredTarget(target);
|
|
}
|
|
}
|
|
|
|
SortActiveTargets();
|
|
}
|
|
|
|
private void ReplaceNodeWithNode(LegacyUnknownTypeNode nodeToReplace, AbstractMaterialNode nodeReplacement)
|
|
{
|
|
var oldSlots = new List<MaterialSlot>();
|
|
nodeToReplace.GetSlots(oldSlots);
|
|
var newSlots = new List<MaterialSlot>();
|
|
nodeReplacement.GetSlots(newSlots);
|
|
|
|
for (int i = 0; i < oldSlots.Count; i++)
|
|
{
|
|
newSlots[i].CopyValuesFrom(oldSlots[i]);
|
|
var oldSlotRef = nodeToReplace.GetSlotReference(oldSlots[i].id);
|
|
var newSlotRef = nodeReplacement.GetSlotReference(newSlots[i].id);
|
|
|
|
for (int x = 0; x < m_Edges.Count; x++)
|
|
{
|
|
var edge = m_Edges[x];
|
|
if (edge.inputSlot.Equals(oldSlotRef))
|
|
{
|
|
var outputSlot = edge.outputSlot;
|
|
m_Edges.Remove(edge);
|
|
m_Edges.Add(new Edge(outputSlot, newSlotRef));
|
|
}
|
|
else if (edge.outputSlot.Equals(oldSlotRef))
|
|
{
|
|
var inputSlot = edge.inputSlot;
|
|
m_Edges.Remove(edge);
|
|
m_Edges.Add(new Edge(newSlotRef, inputSlot));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnEnable()
|
|
{
|
|
foreach (var node in GetNodes<AbstractMaterialNode>().OfType<IOnAssetEnabled>())
|
|
{
|
|
node.OnEnable();
|
|
}
|
|
|
|
ShaderGraphPreferences.onVariantLimitChanged += OnKeywordChanged;
|
|
}
|
|
|
|
public void OnDisable()
|
|
{
|
|
ShaderGraphPreferences.onVariantLimitChanged -= OnKeywordChanged;
|
|
}
|
|
|
|
internal void ValidateCustomBlockLimit()
|
|
{
|
|
if (m_ActiveTargets.Count() == 0)
|
|
return;
|
|
|
|
int nonCustomUsage = 0;
|
|
foreach (var bnode in vertexContext.blocks.Where(jb => !jb.value.isCustomBlock).Select(b => b.value))
|
|
{
|
|
if (bnode == null || bnode.descriptor == null)
|
|
continue;
|
|
|
|
if (bnode.descriptor.HasPreprocessor() || bnode.descriptor.HasSemantic() || bnode.descriptor.vectorCount == 0) // not packable.
|
|
nonCustomUsage += 4;
|
|
else nonCustomUsage += bnode.descriptor.vectorCount;
|
|
}
|
|
int maxTargetUsage = m_ActiveTargets.Select(jt => jt.value.padCustomInterpolatorLimit).Max() * 4;
|
|
|
|
int padding = nonCustomUsage + maxTargetUsage;
|
|
|
|
int errRange = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold;
|
|
int warnRange = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold;
|
|
|
|
int errorLevel = errRange * 4 - padding;
|
|
int warnLevel = warnRange * 4 - padding;
|
|
|
|
int total = 0;
|
|
|
|
// warn based on the interpolator's location in the block list.
|
|
foreach (var cib in vertexContext.blocks.Where(jb => jb.value.isCustomBlock).Select(b => b.value))
|
|
{
|
|
ClearErrorsForNode(cib);
|
|
total += (int)cib.customWidth;
|
|
if (total > errorLevel)
|
|
{
|
|
AddValidationError(cib.objectId, $"{cib.customName} exceeds the interpolation channel error threshold: {errRange}. See ShaderGraph project settings.");
|
|
}
|
|
else if (total > warnLevel)
|
|
{
|
|
AddValidationError(cib.objectId, $"{cib.customName} exceeds the interpolation channel warning threshold: {warnRange}. See ShaderGraph project settings.", ShaderCompilerMessageSeverity.Warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ValidateContextBlocks()
|
|
{
|
|
void ValidateContext(ContextData contextData, ShaderStage expectedShaderStage)
|
|
{
|
|
if (contextData == null)
|
|
return;
|
|
|
|
foreach (var block in contextData.blocks)
|
|
{
|
|
var slots = block.value.GetInputSlots<MaterialSlot>();
|
|
foreach (var slot in slots)
|
|
FindAndReportSlotErrors(slot, expectedShaderStage);
|
|
}
|
|
};
|
|
|
|
ValidateContext(vertexContext, ShaderStage.Vertex);
|
|
ValidateContext(fragmentContext, ShaderStage.Fragment);
|
|
}
|
|
|
|
void FindAndReportSlotErrors(MaterialSlot initialSlot, ShaderStage expectedShaderStage)
|
|
{
|
|
var expectedCapability = expectedShaderStage.GetShaderStageCapability();
|
|
var errorSourceSlots = new HashSet<MaterialSlot>();
|
|
var visitedNodes = new HashSet<AbstractMaterialNode>();
|
|
|
|
var graph = initialSlot.owner.owner;
|
|
var slotStack = new Stack<MaterialSlot>();
|
|
slotStack.Clear();
|
|
slotStack.Push(initialSlot);
|
|
|
|
// Trace back and find any edges that introduce an error
|
|
while (slotStack.Any())
|
|
{
|
|
var slot = slotStack.Pop();
|
|
|
|
// If the slot is an input, jump across the connected edge to the output it's connected to
|
|
if (slot.isInputSlot)
|
|
{
|
|
foreach (var edge in graph.GetEdges(slot.slotReference))
|
|
{
|
|
var node = edge.outputSlot.node;
|
|
|
|
var outputSlot = node.FindOutputSlot<MaterialSlot>(edge.outputSlot.slotId);
|
|
// If the output slot this is connected to is invalid then this is a source of an error.
|
|
// Mark the slot and stop iterating, otherwise continue the recursion
|
|
if (!outputSlot.stageCapability.HasFlag(expectedCapability))
|
|
errorSourceSlots.Add(outputSlot);
|
|
else
|
|
slotStack.Push(outputSlot);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No need to double visit nodes
|
|
if (visitedNodes.Contains(slot.owner))
|
|
continue;
|
|
visitedNodes.Add(slot.owner);
|
|
|
|
var ownerSlots = slot.owner.GetInputSlots<MaterialSlot>(slot);
|
|
foreach (var ownerSlot in ownerSlots)
|
|
slotStack.Push(ownerSlot);
|
|
}
|
|
}
|
|
|
|
bool IsEntireNodeStageLocked(AbstractMaterialNode node, ShaderStageCapability expectedNodeCapability)
|
|
{
|
|
var slots = node.GetOutputSlots<MaterialSlot>();
|
|
foreach (var slot in slots)
|
|
{
|
|
if (expectedNodeCapability != slot.stageCapability)
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
foreach (var errorSourceSlot in errorSourceSlots)
|
|
{
|
|
var errorNode = errorSourceSlot.owner;
|
|
|
|
// Determine if only one slot or the entire node is at fault. Currently only slots are
|
|
// denoted with stage capabilities so deduce this by checking all outputs
|
|
string errorSource;
|
|
if (IsEntireNodeStageLocked(errorNode, errorSourceSlot.stageCapability))
|
|
errorSource = $"Node {errorNode.name}";
|
|
else
|
|
errorSource = $"Slot {errorSourceSlot.RawDisplayName()}";
|
|
|
|
// Determine what action they can take. If the stage capability is None then this can't be connected to anything.
|
|
string actionToTake;
|
|
if (errorSourceSlot.stageCapability != ShaderStageCapability.None)
|
|
{
|
|
var validStageName = errorSourceSlot.stageCapability.ToString().ToLower();
|
|
actionToTake = $"reconnect to a {validStageName} block or delete invalid connection";
|
|
}
|
|
else
|
|
actionToTake = "delete invalid connection";
|
|
|
|
var invalidStageName = expectedShaderStage.ToString().ToLower();
|
|
string message = $"{errorSource} is not compatible with {invalidStageName} block {initialSlot.RawDisplayName()}, {actionToTake}.";
|
|
AddValidationError(errorNode.objectId, message, ShaderCompilerMessageSeverity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
class InspectorPreviewData
|
|
{
|
|
public SerializableMesh serializedMesh = new SerializableMesh();
|
|
public bool preventRotation;
|
|
|
|
[NonSerialized]
|
|
public Quaternion rotation = Quaternion.identity;
|
|
|
|
[NonSerialized]
|
|
public float scale = 1f;
|
|
}
|
|
}
|