mathe/Library/PackageCache/com.unity.shadergraph@14.0.8/Editor/Data/Nodes/Utility/SubGraphNode.cs
2024-09-20 20:30:10 +02:00

909 lines
33 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor.Graphing;
using UnityEditor.ShaderGraph.Internal;
namespace UnityEditor.ShaderGraph
{
[HasDependencies(typeof(MinimalSubGraphNode))]
[Title("Utility", "Sub-graph")]
class SubGraphNode : AbstractMaterialNode
, IGeneratesBodyCode
, IOnAssetEnabled
, IGeneratesFunction
, IMayRequireNormal
, IMayRequireTangent
, IMayRequireBitangent
, IMayRequireMeshUV
, IMayRequireScreenPosition
, IMayRequireNDCPosition
, IMayRequirePixelPosition
, IMayRequireViewDirection
, IMayRequirePosition
, IMayRequirePositionPredisplacement
, IMayRequireVertexColor
, IMayRequireTime
, IMayRequireFaceSign
, IMayRequireCameraOpaqueTexture
, IMayRequireDepthTexture
, IMayRequireVertexSkinning
, IMayRequireVertexID
, IDisposable
{
[Serializable]
public class MinimalSubGraphNode : IHasDependencies
{
[SerializeField]
string m_SerializedSubGraph = string.Empty;
public void GetSourceAssetDependencies(AssetCollection assetCollection)
{
var assetReference = JsonUtility.FromJson<SubGraphAssetReference>(m_SerializedSubGraph);
string guidString = assetReference?.subGraph?.guid;
if (!string.IsNullOrEmpty(guidString) && GUID.TryParse(guidString, out GUID guid))
{
// subgraphs are read as artifacts
// they also should be pulled into .unitypackages
assetCollection.AddAssetDependency(
guid,
AssetCollection.Flags.ArtifactDependency |
AssetCollection.Flags.IsSubGraph |
AssetCollection.Flags.IncludeInExportPackage);
}
}
}
[Serializable]
class SubGraphHelper
{
public SubGraphAsset subGraph;
}
[Serializable]
class SubGraphAssetReference
{
public AssetReference subGraph = default;
public override string ToString()
{
return $"subGraph={subGraph}";
}
}
[Serializable]
class AssetReference
{
public long fileID = default;
public string guid = default;
public int type = default;
public override string ToString()
{
return $"fileID={fileID}, guid={guid}, type={type}";
}
}
[SerializeField]
string m_SerializedSubGraph = string.Empty;
[NonSerialized]
SubGraphAsset m_SubGraph; // This should not be accessed directly by most code -- use the asset property instead, and check for NULL! :)
[SerializeField]
List<string> m_PropertyGuids = new List<string>();
[SerializeField]
List<int> m_PropertyIds = new List<int>();
[SerializeField]
List<string> m_Dropdowns = new List<string>();
[SerializeField]
List<string> m_DropdownSelectedEntries = new List<string>();
public string subGraphGuid
{
get
{
var assetReference = JsonUtility.FromJson<SubGraphAssetReference>(m_SerializedSubGraph);
return assetReference?.subGraph?.guid;
}
}
void LoadSubGraph()
{
if (m_SubGraph == null)
{
if (string.IsNullOrEmpty(m_SerializedSubGraph))
{
return;
}
var graphGuid = subGraphGuid;
var assetPath = AssetDatabase.GUIDToAssetPath(graphGuid);
if (string.IsNullOrEmpty(assetPath))
{
// this happens if the editor has never seen the GUID
// error will be printed by validation code in this case
return;
}
m_SubGraph = AssetDatabase.LoadAssetAtPath<SubGraphAsset>(assetPath);
if (m_SubGraph == null)
{
// this happens if the editor has seen the GUID, but the file has been deleted since then
// error will be printed by validation code in this case
return;
}
m_SubGraph.LoadGraphData();
m_SubGraph.LoadDependencyData();
name = m_SubGraph.name;
}
}
public SubGraphAsset asset
{
get
{
LoadSubGraph();
return m_SubGraph;
}
set
{
if (asset == value)
return;
var helper = new SubGraphHelper();
helper.subGraph = value;
m_SerializedSubGraph = EditorJsonUtility.ToJson(helper, true);
m_SubGraph = null;
UpdateSlots();
Dirty(ModificationScope.Topological);
}
}
public override bool hasPreview
{
get { return true; }
}
public override PreviewMode previewMode
{
get
{
PreviewMode mode = m_PreviewMode;
if ((mode == PreviewMode.Inherit) && (asset != null))
mode = asset.previewMode;
return mode;
}
}
public SubGraphNode()
{
name = "Sub Graph";
}
public override bool allowedInSubGraph
{
get { return true; }
}
public override bool canSetPrecision
{
get { return asset?.subGraphGraphPrecision == GraphPrecision.Graph; }
}
public override void GetInputSlots<T>(MaterialSlot startingSlot, List<T> foundSlots)
{
var allSlots = new List<T>();
GetInputSlots<T>(allSlots);
var info = asset?.GetOutputDependencies(startingSlot.RawDisplayName());
if (info != null)
{
foreach (var slot in allSlots)
{
if (info.ContainsSlot(slot))
foundSlots.Add(slot);
}
}
}
public override void GetOutputSlots<T>(MaterialSlot startingSlot, List<T> foundSlots)
{
var allSlots = new List<T>();
GetOutputSlots<T>(allSlots);
var info = asset?.GetInputDependencies(startingSlot.RawDisplayName());
if (info != null)
{
foreach (var slot in allSlots)
{
if (info.ContainsSlot(slot))
foundSlots.Add(slot);
}
}
}
ShaderStageCapability GetSlotCapability(MaterialSlot slot)
{
SlotDependencyInfo dependencyInfo;
if (slot.isInputSlot)
dependencyInfo = asset?.GetInputDependencies(slot.RawDisplayName());
else
dependencyInfo = asset?.GetOutputDependencies(slot.RawDisplayName());
if (dependencyInfo != null)
return dependencyInfo.capabilities;
return ShaderStageCapability.All;
}
public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode)
{
var outputGraphPrecision = asset?.outputGraphPrecision ?? GraphPrecision.Single;
var outputPrecision = outputGraphPrecision.ToConcrete(concretePrecision);
if (asset == null || hasError)
{
var outputSlots = new List<MaterialSlot>();
GetOutputSlots(outputSlots);
foreach (var slot in outputSlots)
{
sb.AppendLine($"{slot.concreteValueType.ToShaderString(outputPrecision)} {GetVariableNameForSlot(slot.id)} = {slot.GetDefaultValue(GenerationMode.ForReals)};");
}
return;
}
var inputVariableName = $"_{GetVariableNameForNode()}";
GenerationUtils.GenerateSurfaceInputTransferCode(sb, asset.requirements, asset.inputStructName, inputVariableName);
// declare output variables
foreach (var outSlot in asset.outputs)
sb.AppendLine("{0} {1};", outSlot.concreteValueType.ToShaderString(outputPrecision), GetVariableNameForSlot(outSlot.id));
var arguments = new List<string>();
foreach (AbstractShaderProperty prop in asset.inputs)
{
// setup the property concrete precision (fallback to node concrete precision when it's switchable)
prop.SetupConcretePrecision(this.concretePrecision);
var inSlotId = m_PropertyIds[m_PropertyGuids.IndexOf(prop.guid.ToString())];
arguments.Add(GetSlotValue(inSlotId, generationMode, prop.concretePrecision));
if (prop.isConnectionTestable)
arguments.Add(IsSlotConnected(inSlotId) ? "true" : "false");
}
var dropdowns = asset.dropdowns;
foreach (var dropdown in dropdowns)
{
var name = GetDropdownEntryName(dropdown.referenceName);
if (dropdown.ContainsEntry(name))
arguments.Add(dropdown.IndexOfName(name).ToString());
else
arguments.Add(dropdown.value.ToString());
}
// pass surface inputs through
arguments.Add(inputVariableName);
foreach (var outSlot in asset.outputs)
arguments.Add(GetVariableNameForSlot(outSlot.id));
foreach (var feedbackSlot in asset.vtFeedbackVariables)
{
string feedbackVar = GetVariableNameForNode() + "_" + feedbackSlot;
sb.AppendLine("{0} {1};", ConcreteSlotValueType.Vector4.ToShaderString(ConcretePrecision.Single), feedbackVar);
arguments.Add(feedbackVar);
}
sb.TryAppendIndentation();
sb.Append(asset.functionName);
sb.Append("(");
bool firstArg = true;
foreach (var arg in arguments)
{
if (!firstArg)
sb.Append(", ");
firstArg = false;
sb.Append(arg);
}
sb.Append(");");
sb.AppendNewLine();
}
public void OnEnable()
{
UpdateSlots();
}
public bool Reload(HashSet<string> changedFileDependencyGUIDs)
{
if (!changedFileDependencyGUIDs.Contains(subGraphGuid))
{
return false;
}
if (asset == null)
{
// asset missing or deleted
return true;
}
if (changedFileDependencyGUIDs.Contains(asset.assetGuid) || asset.descendents.Any(changedFileDependencyGUIDs.Contains))
{
m_SubGraph = null;
UpdateSlots();
if (hasError)
{
return true;
}
owner.ClearErrorsForNode(this);
ValidateNode();
Dirty(ModificationScope.Graph);
}
return true;
}
public override void UpdatePrecision(List<MaterialSlot> inputSlots)
{
if (asset != null)
{
if (asset.subGraphGraphPrecision == GraphPrecision.Graph)
{
// subgraph is defined to be switchable, so use the default behavior to determine precision
base.UpdatePrecision(inputSlots);
}
else
{
// subgraph sets a specific precision, force that
graphPrecision = asset.subGraphGraphPrecision;
concretePrecision = graphPrecision.ToConcrete(owner.graphDefaultConcretePrecision);
}
}
else
{
// no subgraph asset; use default behavior
base.UpdatePrecision(inputSlots);
}
}
public virtual void UpdateSlots()
{
var validNames = new List<int>();
if (asset == null)
{
return;
}
var props = asset.inputs;
var toFix = new HashSet<(SlotReference from, SlotReference to)>();
foreach (var prop in props)
{
SlotValueType valueType = prop.concreteShaderValueType.ToSlotValueType();
var propertyString = prop.guid.ToString();
var propertyIndex = m_PropertyGuids.IndexOf(propertyString);
if (propertyIndex < 0)
{
propertyIndex = m_PropertyGuids.Count;
m_PropertyGuids.Add(propertyString);
m_PropertyIds.Add(prop.guid.GetHashCode());
}
var id = m_PropertyIds[propertyIndex];
//for whatever reason, it seems like shader property ids changed between 21.2a17 and 21.2b1
//tried tracking it down, couldnt find any reason for it, so we gotta fix it in post (after we deserialize)
List<MaterialSlot> inputs = new List<MaterialSlot>();
MaterialSlot found = null;
GetInputSlots(inputs);
foreach (var input in inputs)
{
if (input.shaderOutputName == prop.referenceName && input.id != id)
{
found = input;
break;
}
}
MaterialSlot slot = MaterialSlot.CreateMaterialSlot(valueType, id, prop.displayName, prop.referenceName, SlotType.Input, Vector4.zero, ShaderStageCapability.All);
// Copy defaults
switch (prop.concreteShaderValueType)
{
case ConcreteSlotValueType.SamplerState:
{
var tSlot = slot as SamplerStateMaterialSlot;
var tProp = prop as SamplerStateShaderProperty;
if (tSlot != null && tProp != null)
tSlot.defaultSamplerState = tProp.value;
}
break;
case ConcreteSlotValueType.Matrix4:
{
var tSlot = slot as Matrix4MaterialSlot;
var tProp = prop as Matrix4ShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Matrix3:
{
var tSlot = slot as Matrix3MaterialSlot;
var tProp = prop as Matrix3ShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Matrix2:
{
var tSlot = slot as Matrix2MaterialSlot;
var tProp = prop as Matrix2ShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Texture2D:
{
var tSlot = slot as Texture2DInputMaterialSlot;
var tProp = prop as Texture2DShaderProperty;
if (tSlot != null && tProp != null)
tSlot.texture = tProp.value.texture;
}
break;
case ConcreteSlotValueType.Texture2DArray:
{
var tSlot = slot as Texture2DArrayInputMaterialSlot;
var tProp = prop as Texture2DArrayShaderProperty;
if (tSlot != null && tProp != null)
tSlot.textureArray = tProp.value.textureArray;
}
break;
case ConcreteSlotValueType.Texture3D:
{
var tSlot = slot as Texture3DInputMaterialSlot;
var tProp = prop as Texture3DShaderProperty;
if (tSlot != null && tProp != null)
tSlot.texture = tProp.value.texture;
}
break;
case ConcreteSlotValueType.Cubemap:
{
var tSlot = slot as CubemapInputMaterialSlot;
var tProp = prop as CubemapShaderProperty;
if (tSlot != null && tProp != null)
tSlot.cubemap = tProp.value.cubemap;
}
break;
case ConcreteSlotValueType.Gradient:
{
var tSlot = slot as GradientInputMaterialSlot;
var tProp = prop as GradientShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Vector4:
{
var tSlot = slot as Vector4MaterialSlot;
var vector4Prop = prop as Vector4ShaderProperty;
var colorProp = prop as ColorShaderProperty;
if (tSlot != null && vector4Prop != null)
tSlot.value = vector4Prop.value;
else if (tSlot != null && colorProp != null)
tSlot.value = colorProp.value;
}
break;
case ConcreteSlotValueType.Vector3:
{
var tSlot = slot as Vector3MaterialSlot;
var tProp = prop as Vector3ShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Vector2:
{
var tSlot = slot as Vector2MaterialSlot;
var tProp = prop as Vector2ShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Vector1:
{
var tSlot = slot as Vector1MaterialSlot;
var tProp = prop as Vector1ShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
case ConcreteSlotValueType.Boolean:
{
var tSlot = slot as BooleanMaterialSlot;
var tProp = prop as BooleanShaderProperty;
if (tSlot != null && tProp != null)
tSlot.value = tProp.value;
}
break;
}
AddSlot(slot);
validNames.Add(id);
if (found != null)
{
List<IEdge> edges = new List<IEdge>();
owner.GetEdges(found.slotReference, edges);
foreach (var edge in edges)
{
toFix.Add((edge.outputSlot, slot.slotReference));
}
}
}
foreach (var slot in asset.outputs)
{
var outputStage = GetSlotCapability(slot);
var newSlot = MaterialSlot.CreateMaterialSlot(slot.valueType, slot.id, slot.RawDisplayName(),
slot.shaderOutputName, SlotType.Output, Vector4.zero, outputStage, slot.hidden);
AddSlot(newSlot);
validNames.Add(slot.id);
}
RemoveSlotsNameNotMatching(validNames, true);
// sort slot order to match subgraph property order
SetSlotOrder(validNames);
foreach (var (from, to) in toFix)
{
//for whatever reason, in this particular error fix, GraphView will incorrectly either add two edgeViews or none
//but it does work correctly if we dont notify GraphView of this added edge. Gross.
owner.UnnotifyAddedEdge(owner.Connect(from, to));
}
}
void ValidateShaderStage()
{
if (asset != null)
{
List<MaterialSlot> slots = new List<MaterialSlot>();
GetInputSlots(slots);
GetOutputSlots(slots);
foreach (MaterialSlot slot in slots)
slot.stageCapability = GetSlotCapability(slot);
}
}
public override void ValidateNode()
{
base.ValidateNode();
if (asset == null)
{
hasError = true;
var assetGuid = subGraphGuid;
var assetPath = string.IsNullOrEmpty(subGraphGuid) ? null : AssetDatabase.GUIDToAssetPath(assetGuid);
if (string.IsNullOrEmpty(assetPath))
{
owner.AddValidationError(objectId, $"Could not find Sub Graph asset with GUID {assetGuid}.");
}
else
{
owner.AddValidationError(objectId, $"Could not load Sub Graph asset at \"{assetPath}\" with GUID {assetGuid}.");
}
return;
}
if (owner.isSubGraph && (asset.descendents.Contains(owner.assetGuid) || asset.assetGuid == owner.assetGuid))
{
hasError = true;
owner.AddValidationError(objectId, $"Detected a recursion in Sub Graph asset at \"{AssetDatabase.GUIDToAssetPath(subGraphGuid)}\" with GUID {subGraphGuid}.");
}
else if (!asset.isValid)
{
hasError = true;
owner.AddValidationError(objectId, $"Sub Graph has errors, asset at \"{AssetDatabase.GUIDToAssetPath(subGraphGuid)}\" with GUID {subGraphGuid}.");
}
else if (!owner.isSubGraph && owner.activeTargets.Any(x => asset.unsupportedTargets.Contains(x)))
{
SetOverrideActiveState(ActiveState.ExplicitInactive);
owner.AddValidationError(objectId, $"Sub Graph contains nodes that are unsupported by the current active targets, asset at \"{AssetDatabase.GUIDToAssetPath(subGraphGuid)}\" with GUID {subGraphGuid}.");
}
// detect disconnected VT properties, and VT layer count mismatches
foreach (var paramProp in asset.inputs)
{
if (paramProp is VirtualTextureShaderProperty vtProp)
{
int paramLayerCount = vtProp.value.layers.Count;
var argSlotId = m_PropertyIds[m_PropertyGuids.IndexOf(paramProp.guid.ToString())]; // yikes
if (!IsSlotConnected(argSlotId))
{
owner.AddValidationError(objectId, $"A VirtualTexture property must be connected to the input slot \"{paramProp.displayName}\"");
}
else
{
var argProp = GetSlotProperty(argSlotId) as VirtualTextureShaderProperty;
if (argProp != null)
{
int argLayerCount = argProp.value.layers.Count;
if (argLayerCount != paramLayerCount)
owner.AddValidationError(objectId, $"Input \"{paramProp.displayName}\" has different number of layers from the connected property \"{argProp.displayName}\"");
}
else
{
owner.AddValidationError(objectId, $"Input \"{paramProp.displayName}\" is not connected to a valid VirtualTexture property");
}
}
break;
}
}
ValidateShaderStage();
}
public override void CollectShaderProperties(PropertyCollector visitor, GenerationMode generationMode)
{
base.CollectShaderProperties(visitor, generationMode);
if (asset == null)
return;
foreach (var property in asset.nodeProperties)
{
visitor.AddShaderProperty(property);
}
}
public AbstractShaderProperty GetShaderProperty(int id)
{
var index = m_PropertyIds.IndexOf(id);
if (index >= 0)
{
var guid = m_PropertyGuids[index];
return asset?.inputs.Where(x => x.guid.ToString().Equals(guid)).FirstOrDefault();
}
return null;
}
public void CollectShaderKeywords(KeywordCollector keywords, GenerationMode generationMode)
{
if (asset == null)
return;
foreach (var keyword in asset.keywords)
{
keywords.AddShaderKeyword(keyword as ShaderKeyword);
}
}
public override void CollectPreviewMaterialProperties(List<PreviewProperty> properties)
{
base.CollectPreviewMaterialProperties(properties);
if (asset == null)
return;
foreach (var property in asset.nodeProperties)
{
properties.Add(property.GetPreviewMaterialProperty());
}
}
public virtual void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode)
{
if (asset == null || hasError)
return;
registry.RequiresIncludes(asset.includes);
var graphData = registry.builder.currentNode.owner;
var graphDefaultConcretePrecision = graphData.graphDefaultConcretePrecision;
foreach (var function in asset.functions)
{
var name = function.key;
var source = function.value;
var graphPrecisionFlags = function.graphPrecisionFlags;
// the subgraph may use multiple precision variants of this function internally
// here we iterate through all the requested precisions and forward those requests out to the graph
for (int requestedGraphPrecision = 0; requestedGraphPrecision <= (int)GraphPrecision.Half; requestedGraphPrecision++)
{
// only provide requested precisions
if ((graphPrecisionFlags & (1 << requestedGraphPrecision)) != 0)
{
// when a function coming from a subgraph asset has a graph precision of "Graph",
// that means it is up to the subgraph NODE to decide (i.e. us!)
GraphPrecision actualGraphPrecision = (GraphPrecision)requestedGraphPrecision;
// subgraph asset setting falls back to this node setting (when switchable)
actualGraphPrecision = actualGraphPrecision.GraphFallback(this.graphPrecision);
// which falls back to the graph default concrete precision
ConcretePrecision actualConcretePrecision = actualGraphPrecision.ToConcrete(graphDefaultConcretePrecision);
// forward the function into the current graph
registry.ProvideFunction(name, actualGraphPrecision, actualConcretePrecision, sb => sb.AppendLines(source));
}
}
}
}
public NeededCoordinateSpace RequiresNormal(ShaderStageCapability stageCapability)
{
if (asset == null)
return NeededCoordinateSpace.None;
return asset.requirements.requiresNormal;
}
public bool RequiresMeshUV(UVChannel channel, ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresMeshUVs.Contains(channel);
}
public bool RequiresScreenPosition(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresScreenPosition;
}
public bool RequiresNDCPosition(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresNDCPosition;
}
public bool RequiresPixelPosition(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresPixelPosition;
}
public NeededCoordinateSpace RequiresViewDirection(ShaderStageCapability stageCapability)
{
if (asset == null)
return NeededCoordinateSpace.None;
return asset.requirements.requiresViewDir;
}
public NeededCoordinateSpace RequiresPosition(ShaderStageCapability stageCapability)
{
if (asset == null)
return NeededCoordinateSpace.None;
return asset.requirements.requiresPosition;
}
public NeededCoordinateSpace RequiresPositionPredisplacement(ShaderStageCapability stageCapability = ShaderStageCapability.All)
{
if (asset == null)
return NeededCoordinateSpace.None;
return asset.requirements.requiresPositionPredisplacement;
}
public NeededCoordinateSpace RequiresTangent(ShaderStageCapability stageCapability)
{
if (asset == null)
return NeededCoordinateSpace.None;
return asset.requirements.requiresTangent;
}
public bool RequiresTime()
{
if (asset == null)
return false;
return asset.requirements.requiresTime;
}
public bool RequiresFaceSign(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresFaceSign;
}
public NeededCoordinateSpace RequiresBitangent(ShaderStageCapability stageCapability)
{
if (asset == null)
return NeededCoordinateSpace.None;
return asset.requirements.requiresBitangent;
}
public bool RequiresVertexColor(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresVertexColor;
}
public bool RequiresCameraOpaqueTexture(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresCameraOpaqueTexture;
}
public bool RequiresDepthTexture(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresDepthTexture;
}
public bool RequiresVertexSkinning(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresVertexSkinning;
}
public bool RequiresVertexID(ShaderStageCapability stageCapability)
{
if (asset == null)
return false;
return asset.requirements.requiresVertexID;
}
public string GetDropdownEntryName(string referenceName)
{
var index = m_Dropdowns.IndexOf(referenceName);
return index >= 0 ? m_DropdownSelectedEntries[index] : string.Empty;
}
public void SetDropdownEntryName(string referenceName, string value)
{
var index = m_Dropdowns.IndexOf(referenceName);
if (index >= 0)
{
m_DropdownSelectedEntries[index] = value;
}
else
{
m_Dropdowns.Add(referenceName);
m_DropdownSelectedEntries.Add(value);
}
}
public override void Dispose()
{
base.Dispose();
m_SubGraph = null;
}
}
}