835 lines
37 KiB
C#
835 lines
37 KiB
C#
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using UnityEngine;
|
||
|
using UnityEditor.Experimental.GraphView;
|
||
|
using UnityEngine.UIElements;
|
||
|
using System;
|
||
|
using UnityEditor.Graphing;
|
||
|
using UnityEditor.ShaderGraph.Internal;
|
||
|
using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;
|
||
|
using BlackboardItem = UnityEditor.ShaderGraph.Internal.ShaderInput;
|
||
|
|
||
|
namespace UnityEditor.ShaderGraph.Drawing
|
||
|
{
|
||
|
struct BlackboardShaderInputOrder
|
||
|
{
|
||
|
public bool isKeyword;
|
||
|
public bool isDropdown;
|
||
|
public KeywordType keywordType;
|
||
|
public ShaderKeyword builtInKeyword;
|
||
|
public string deprecatedPropertyName;
|
||
|
public int version;
|
||
|
}
|
||
|
class BlackboardShaderInputFactory
|
||
|
{
|
||
|
static public ShaderInput GetShaderInput(BlackboardShaderInputOrder order)
|
||
|
{
|
||
|
ShaderInput output;
|
||
|
if (order.isKeyword)
|
||
|
{
|
||
|
if (order.builtInKeyword == null)
|
||
|
{
|
||
|
output = new ShaderKeyword(order.keywordType);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
output = order.builtInKeyword;
|
||
|
}
|
||
|
}
|
||
|
else if (order.isDropdown)
|
||
|
{
|
||
|
output = new ShaderDropdown();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
switch (order.deprecatedPropertyName)
|
||
|
{
|
||
|
case "Color":
|
||
|
output = new ColorShaderProperty(order.version);
|
||
|
break;
|
||
|
default:
|
||
|
output = null;
|
||
|
AssertHelpers.Fail("BlackboardShaderInputFactory: Unknown deprecated property type.");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return output;
|
||
|
}
|
||
|
}
|
||
|
class AddShaderInputAction : IGraphDataAction
|
||
|
{
|
||
|
public enum AddActionSource
|
||
|
{
|
||
|
Default,
|
||
|
AddMenu
|
||
|
}
|
||
|
|
||
|
void AddShaderInput(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddShaderInputAction");
|
||
|
|
||
|
// If type property is valid, create instance of that type
|
||
|
if (blackboardItemType != null && blackboardItemType.IsSubclassOf(typeof(BlackboardItem)))
|
||
|
{
|
||
|
shaderInputReference = (BlackboardItem)Activator.CreateInstance(blackboardItemType, true);
|
||
|
}
|
||
|
else if (m_ShaderInputReferenceGetter != null)
|
||
|
{
|
||
|
shaderInputReference = m_ShaderInputReferenceGetter();
|
||
|
}
|
||
|
// If type is null a direct override object must have been provided or else we are in an error-state
|
||
|
else if (shaderInputReference == null)
|
||
|
{
|
||
|
AssertHelpers.Fail("BlackboardController: Unable to complete Add Shader Input action.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
shaderInputReference.generatePropertyBlock = shaderInputReference.isExposable;
|
||
|
|
||
|
if (graphData.owner != null)
|
||
|
graphData.owner.RegisterCompleteObjectUndo("Add Shader Input");
|
||
|
else
|
||
|
AssertHelpers.Fail("GraphObject is null while carrying out AddShaderInputAction");
|
||
|
|
||
|
graphData.AddGraphInput(shaderInputReference);
|
||
|
|
||
|
// If no categoryToAddItemToGuid is provided, add the input to the default category
|
||
|
if (categoryToAddItemToGuid == String.Empty)
|
||
|
{
|
||
|
var defaultCategory = graphData.categories.FirstOrDefault();
|
||
|
AssertHelpers.IsNotNull(defaultCategory, "Default category reference is null.");
|
||
|
if (defaultCategory != null)
|
||
|
{
|
||
|
var addItemToCategoryAction = new AddItemToCategoryAction();
|
||
|
addItemToCategoryAction.categoryGuid = defaultCategory.categoryGuid;
|
||
|
addItemToCategoryAction.itemToAdd = shaderInputReference;
|
||
|
graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var addItemToCategoryAction = new AddItemToCategoryAction();
|
||
|
addItemToCategoryAction.categoryGuid = categoryToAddItemToGuid;
|
||
|
addItemToCategoryAction.itemToAdd = shaderInputReference;
|
||
|
graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static AddShaderInputAction AddDeprecatedPropertyAction(BlackboardShaderInputOrder order)
|
||
|
{
|
||
|
return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
|
||
|
}
|
||
|
|
||
|
public static AddShaderInputAction AddDropdownAction(BlackboardShaderInputOrder order)
|
||
|
{
|
||
|
return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
|
||
|
}
|
||
|
|
||
|
public static AddShaderInputAction AddKeywordAction(BlackboardShaderInputOrder order)
|
||
|
{
|
||
|
return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
|
||
|
}
|
||
|
|
||
|
public static AddShaderInputAction AddPropertyAction(Type shaderInputType)
|
||
|
{
|
||
|
return new() { blackboardItemType = shaderInputType, addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
|
||
|
}
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => AddShaderInput;
|
||
|
// If this is a subclass of ShaderInput and is not null, then an object of this type is created to add to blackboard
|
||
|
// If the type field above is null and this is provided, then it is directly used as the item to add to blackboard
|
||
|
public BlackboardItem shaderInputReference { get; set; }
|
||
|
public AddActionSource addInputActionType { get; set; }
|
||
|
public string categoryToAddItemToGuid { get; set; } = String.Empty;
|
||
|
|
||
|
Type blackboardItemType { get; set; }
|
||
|
|
||
|
Func<BlackboardItem> m_ShaderInputReferenceGetter = null;
|
||
|
}
|
||
|
|
||
|
class ChangeGraphPathAction : IGraphDataAction
|
||
|
{
|
||
|
void ChangeGraphPath(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeGraphPathAction");
|
||
|
graphData.path = NewGraphPath;
|
||
|
}
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => ChangeGraphPath;
|
||
|
|
||
|
public string NewGraphPath { get; set; }
|
||
|
}
|
||
|
|
||
|
class CopyShaderInputAction : IGraphDataAction
|
||
|
{
|
||
|
void CopyShaderInput(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyShaderInputAction");
|
||
|
AssertHelpers.IsNotNull(shaderInputToCopy, "ShaderInputToCopy is null while carrying out CopyShaderInputAction");
|
||
|
|
||
|
// Don't handle undo here as there are different contexts in which this action is used, that define the undo action
|
||
|
// TODO: Perhaps a sign that each of those need to be made their own actions instead of conflating intent into a single action
|
||
|
|
||
|
switch (shaderInputToCopy)
|
||
|
{
|
||
|
case AbstractShaderProperty property:
|
||
|
|
||
|
insertIndex = Mathf.Clamp(insertIndex, -1, graphData.properties.Count() - 1);
|
||
|
var copiedProperty = (AbstractShaderProperty)graphData.AddCopyOfShaderInput(property, insertIndex);
|
||
|
if (copiedProperty != null) // some property types cannot be duplicated (unknown types)
|
||
|
{
|
||
|
// Update the property nodes that depends on the copied node
|
||
|
foreach (var node in dependentNodeList)
|
||
|
{
|
||
|
if (node is PropertyNode propertyNode)
|
||
|
{
|
||
|
propertyNode.owner = graphData;
|
||
|
propertyNode.property = copiedProperty;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
copiedShaderInput = copiedProperty;
|
||
|
break;
|
||
|
|
||
|
case ShaderKeyword shaderKeyword:
|
||
|
// InsertIndex gets passed in relative to the blackboard position of an item overall,
|
||
|
// and not relative to the array sizes of the properties/keywords/dropdowns
|
||
|
var keywordInsertIndex = insertIndex - graphData.properties.Count();
|
||
|
|
||
|
keywordInsertIndex = Mathf.Clamp(keywordInsertIndex, -1, graphData.keywords.Count() - 1);
|
||
|
|
||
|
// Don't duplicate built-in keywords within the same graph
|
||
|
if (shaderKeyword.isBuiltIn && graphData.keywords.Any(p => p.referenceName == shaderInputToCopy.referenceName))
|
||
|
return;
|
||
|
|
||
|
var copiedKeyword = (ShaderKeyword)graphData.AddCopyOfShaderInput(shaderKeyword, keywordInsertIndex);
|
||
|
|
||
|
// Update the keyword nodes that depends on the copied node
|
||
|
foreach (var node in dependentNodeList)
|
||
|
{
|
||
|
if (node is KeywordNode propertyNode)
|
||
|
{
|
||
|
propertyNode.owner = graphData;
|
||
|
propertyNode.keyword = copiedKeyword;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
copiedShaderInput = copiedKeyword;
|
||
|
break;
|
||
|
|
||
|
case ShaderDropdown shaderDropdown:
|
||
|
// InsertIndex gets passed in relative to the blackboard position of an item overall,
|
||
|
// and not relative to the array sizes of the properties/keywords/dropdowns
|
||
|
var dropdownInsertIndex = insertIndex - graphData.properties.Count() - graphData.keywords.Count();
|
||
|
|
||
|
dropdownInsertIndex = Mathf.Clamp(dropdownInsertIndex, -1, graphData.dropdowns.Count() - 1);
|
||
|
|
||
|
var copiedDropdown = (ShaderDropdown)graphData.AddCopyOfShaderInput(shaderDropdown, dropdownInsertIndex);
|
||
|
|
||
|
// Update the dropdown nodes that depends on the copied node
|
||
|
foreach (var node in dependentNodeList)
|
||
|
{
|
||
|
if (node is DropdownNode propertyNode)
|
||
|
{
|
||
|
propertyNode.owner = graphData;
|
||
|
propertyNode.dropdown = copiedDropdown;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
copiedShaderInput = copiedDropdown;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new ArgumentOutOfRangeException();
|
||
|
}
|
||
|
|
||
|
if (copiedShaderInput != null)
|
||
|
{
|
||
|
// If specific category to copy to is provided, find and use it
|
||
|
foreach (var category in graphData.categories)
|
||
|
{
|
||
|
if (category.categoryGuid == containingCategoryGuid)
|
||
|
{
|
||
|
// Ensures that the new item gets added after the item it was duplicated from
|
||
|
insertIndex += 1;
|
||
|
// If the source item was already the last item in list, just add to end of list
|
||
|
if (insertIndex >= category.childCount)
|
||
|
insertIndex = -1;
|
||
|
graphData.InsertItemIntoCategory(category.objectId, copiedShaderInput, insertIndex);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Else, add to default category
|
||
|
graphData.categories.First().InsertItemIntoCategory(copiedShaderInput);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => CopyShaderInput;
|
||
|
|
||
|
public IEnumerable<AbstractMaterialNode> dependentNodeList { get; set; } = new List<AbstractMaterialNode>();
|
||
|
|
||
|
public BlackboardItem shaderInputToCopy { get; set; }
|
||
|
|
||
|
public BlackboardItem copiedShaderInput { get; set; }
|
||
|
|
||
|
public string containingCategoryGuid { get; set; }
|
||
|
|
||
|
public int insertIndex { get; set; } = -1;
|
||
|
}
|
||
|
|
||
|
class AddCategoryAction : IGraphDataAction
|
||
|
{
|
||
|
void AddCategory(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddCategoryAction");
|
||
|
graphData.owner.RegisterCompleteObjectUndo("Add Category");
|
||
|
// If categoryDataReference is not null, directly add it to graphData
|
||
|
if (categoryDataReference == null)
|
||
|
categoryDataReference = new CategoryData(categoryName, childObjects);
|
||
|
graphData.AddCategory(categoryDataReference);
|
||
|
}
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => AddCategory;
|
||
|
|
||
|
// Direct reference to the categoryData to use if it is specified
|
||
|
public CategoryData categoryDataReference { get; set; }
|
||
|
public string categoryName { get; set; } = String.Empty;
|
||
|
public List<ShaderInput> childObjects { get; set; }
|
||
|
}
|
||
|
|
||
|
class MoveCategoryAction : IGraphDataAction
|
||
|
{
|
||
|
void MoveCategory(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out MoveCategoryAction");
|
||
|
graphData.owner.RegisterCompleteObjectUndo("Move Category");
|
||
|
// Handling for out of range moves is slightly different, but otherwise we need to reverse for insertion order.
|
||
|
var guids = newIndexValue >= graphData.categories.Count() ? categoryGuids : categoryGuids.Reverse<string>();
|
||
|
foreach (var guid in categoryGuids)
|
||
|
{
|
||
|
var cat = graphData.categories.FirstOrDefault(c => c.categoryGuid == guid);
|
||
|
graphData.MoveCategory(cat, newIndexValue);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => MoveCategory;
|
||
|
|
||
|
// Reference to the shader input being modified
|
||
|
internal List<string> categoryGuids { get; set; }
|
||
|
|
||
|
internal int newIndexValue { get; set; }
|
||
|
}
|
||
|
|
||
|
class AddItemToCategoryAction : IGraphDataAction
|
||
|
{
|
||
|
public enum AddActionSource
|
||
|
{
|
||
|
Default,
|
||
|
DragDrop
|
||
|
}
|
||
|
|
||
|
void AddItemsToCategory(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddItemToCategoryAction");
|
||
|
graphData.owner.RegisterCompleteObjectUndo("Add Item to Category");
|
||
|
graphData.InsertItemIntoCategory(categoryGuid, itemToAdd, indexToAddItemAt);
|
||
|
}
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => AddItemsToCategory;
|
||
|
|
||
|
public string categoryGuid { get; set; }
|
||
|
|
||
|
public ShaderInput itemToAdd { get; set; }
|
||
|
|
||
|
// By default an item is always added to the end of a category, if this value is set to something other than -1, will insert the item at that position within the category
|
||
|
public int indexToAddItemAt { get; set; } = -1;
|
||
|
|
||
|
public AddActionSource addActionSource { get; set; }
|
||
|
}
|
||
|
|
||
|
class CopyCategoryAction : IGraphDataAction
|
||
|
{
|
||
|
void CopyCategory(GraphData graphData)
|
||
|
{
|
||
|
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyCategoryAction");
|
||
|
AssertHelpers.IsNotNull(categoryToCopyReference, "CategoryToCopyReference is null while carrying out CopyCategoryAction");
|
||
|
|
||
|
// This is called by MaterialGraphView currently, no need to repeat it here, though ideally it would live here
|
||
|
//graphData.owner.RegisterCompleteObjectUndo("Copy Category");
|
||
|
|
||
|
newCategoryDataReference = graphData.CopyCategory(categoryToCopyReference);
|
||
|
}
|
||
|
|
||
|
// Reference to the new category created as a copy
|
||
|
public CategoryData newCategoryDataReference { get; set; }
|
||
|
|
||
|
// After category has been copied, store reference to it
|
||
|
public CategoryData categoryToCopyReference { get; set; }
|
||
|
|
||
|
public Action<GraphData> modifyGraphDataAction => CopyCategory;
|
||
|
}
|
||
|
|
||
|
class ShaderVariantLimitAction : IGraphDataAction
|
||
|
{
|
||
|
public int currentVariantCount { get; set; } = 0;
|
||
|
public int maxVariantCount { get; set; } = 0;
|
||
|
|
||
|
public ShaderVariantLimitAction(int currentVariantCount, int maxVariantCount)
|
||
|
{
|
||
|
this.maxVariantCount = maxVariantCount;
|
||
|
this.currentVariantCount = currentVariantCount;
|
||
|
}
|
||
|
|
||
|
// There's no action actually performed on the graph, but we need to implement this as a valid function
|
||
|
public Action<GraphData> modifyGraphDataAction => Empty;
|
||
|
|
||
|
void Empty(GraphData graphData)
|
||
|
{
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class BlackboardController : SGViewController<GraphData, BlackboardViewModel>
|
||
|
{
|
||
|
// Type changes (adds/removes of Types) only happen after a full assembly reload so its safe to make this static
|
||
|
static IList<Type> s_ShaderInputTypes;
|
||
|
|
||
|
static BlackboardController()
|
||
|
{
|
||
|
var shaderInputTypes = TypeCache.GetTypesWithAttribute<BlackboardInputInfo>().ToList();
|
||
|
// Sort the ShaderInput by priority using the BlackboardInputInfo attribute
|
||
|
shaderInputTypes.Sort((s1, s2) =>
|
||
|
{
|
||
|
var info1 = Attribute.GetCustomAttribute(s1, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
|
||
|
var info2 = Attribute.GetCustomAttribute(s2, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
|
||
|
|
||
|
if (info1.priority == info2.priority)
|
||
|
return (info1.name ?? s1.Name).CompareTo(info2.name ?? s2.Name);
|
||
|
else
|
||
|
return info1.priority.CompareTo(info2.priority);
|
||
|
});
|
||
|
|
||
|
s_ShaderInputTypes = shaderInputTypes.ToList();
|
||
|
}
|
||
|
|
||
|
BlackboardCategoryController m_DefaultCategoryController = null;
|
||
|
Dictionary<string, BlackboardCategoryController> m_BlackboardCategoryControllers = new Dictionary<string, BlackboardCategoryController>();
|
||
|
|
||
|
protected SGBlackboard m_Blackboard;
|
||
|
|
||
|
internal SGBlackboard blackboard
|
||
|
{
|
||
|
get => m_Blackboard;
|
||
|
private set => m_Blackboard = value;
|
||
|
}
|
||
|
public string GetFirstSelectedCategoryGuid()
|
||
|
{
|
||
|
if (m_Blackboard == null)
|
||
|
{
|
||
|
return string.Empty;
|
||
|
}
|
||
|
var copiedSelectionList = new List<ISelectable>(m_Blackboard.selection);
|
||
|
var selectedCategories = new List<SGBlackboardCategory>();
|
||
|
var selectedCategoryGuid = String.Empty;
|
||
|
for (int i = 0; i < copiedSelectionList.Count; i++)
|
||
|
{
|
||
|
var selectable = copiedSelectionList[i];
|
||
|
if (selectable is SGBlackboardCategory category)
|
||
|
{
|
||
|
selectedCategories.Add(selectable as SGBlackboardCategory);
|
||
|
}
|
||
|
}
|
||
|
if (selectedCategories.Any())
|
||
|
{
|
||
|
selectedCategoryGuid = selectedCategories[0].viewModel.associatedCategoryGuid;
|
||
|
}
|
||
|
return selectedCategoryGuid;
|
||
|
}
|
||
|
|
||
|
void InitializeViewModel(bool useDropdowns)
|
||
|
{
|
||
|
// Clear the view model
|
||
|
ViewModel.ResetViewModelData();
|
||
|
ViewModel.subtitle = BlackboardUtils.FormatPath(Model.path);
|
||
|
BlackboardShaderInputOrder propertyTypesOrder = new BlackboardShaderInputOrder();
|
||
|
|
||
|
// Property data first
|
||
|
foreach (var shaderInputType in s_ShaderInputTypes)
|
||
|
{
|
||
|
if (shaderInputType.IsAbstract)
|
||
|
continue;
|
||
|
|
||
|
var info = Attribute.GetCustomAttribute(shaderInputType, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
|
||
|
string name = info?.name ?? ObjectNames.NicifyVariableName(shaderInputType.Name.Replace("ShaderProperty", ""));
|
||
|
|
||
|
// QUICK FIX TO DEAL WITH DEPRECATED COLOR PROPERTY
|
||
|
if (name.Equals("Color", StringComparison.InvariantCultureIgnoreCase) && ShaderGraphPreferences.allowDeprecatedBehaviors)
|
||
|
{
|
||
|
propertyTypesOrder.isKeyword = false;
|
||
|
propertyTypesOrder.deprecatedPropertyName = name;
|
||
|
propertyTypesOrder.version = ColorShaderProperty.deprecatedVersion;
|
||
|
ViewModel.propertyNameToAddActionMap.Add($"Color (Legacy v0)", AddShaderInputAction.AddDeprecatedPropertyAction(propertyTypesOrder));
|
||
|
ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType));
|
||
|
}
|
||
|
else
|
||
|
ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType));
|
||
|
}
|
||
|
|
||
|
// Default Keywords next
|
||
|
BlackboardShaderInputOrder keywordTypesOrder = new BlackboardShaderInputOrder();
|
||
|
keywordTypesOrder.isKeyword = true;
|
||
|
keywordTypesOrder.keywordType = KeywordType.Boolean;
|
||
|
ViewModel.defaultKeywordNameToAddActionMap.Add("Boolean", AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
|
||
|
keywordTypesOrder.keywordType = KeywordType.Enum;
|
||
|
ViewModel.defaultKeywordNameToAddActionMap.Add("Enum", AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
|
||
|
|
||
|
// Built-In Keywords after that
|
||
|
foreach (var builtinKeywordDescriptor in KeywordUtil.GetBuiltinKeywordDescriptors())
|
||
|
{
|
||
|
var keyword = ShaderKeyword.CreateBuiltInKeyword(builtinKeywordDescriptor);
|
||
|
// Do not allow user to add built-in keywords that conflict with user-made keywords that have the same reference name or display name
|
||
|
if (Model.keywords.Any(x => x.referenceName == keyword.referenceName || x.displayName == keyword.displayName))
|
||
|
{
|
||
|
ViewModel.disabledKeywordNameList.Add(keyword.displayName);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
keywordTypesOrder.builtInKeyword = (ShaderKeyword)keyword.Copy();
|
||
|
ViewModel.builtInKeywordNameToAddActionMap.Add(keyword.displayName, AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (useDropdowns)
|
||
|
{
|
||
|
BlackboardShaderInputOrder dropdownsOrder = new BlackboardShaderInputOrder();
|
||
|
dropdownsOrder.isDropdown = true;
|
||
|
ViewModel.defaultDropdownNameToAdd = new Tuple<string, IGraphDataAction>("Dropdown", AddShaderInputAction.AddDropdownAction(dropdownsOrder));
|
||
|
}
|
||
|
|
||
|
// Category data last
|
||
|
var defaultNewCategoryReference = new CategoryData("Category");
|
||
|
ViewModel.addCategoryAction = new AddCategoryAction() { categoryDataReference = defaultNewCategoryReference };
|
||
|
|
||
|
ViewModel.requestModelChangeAction = this.RequestModelChange;
|
||
|
ViewModel.categoryInfoList.AddRange(DataStore.State.categories.ToList());
|
||
|
}
|
||
|
|
||
|
internal BlackboardController(GraphData model, BlackboardViewModel inViewModel, GraphDataStore graphDataStore)
|
||
|
: base(model, inViewModel, graphDataStore)
|
||
|
{
|
||
|
// TODO: hide this more generically for category types.
|
||
|
bool useDropdowns = model.isSubGraph;
|
||
|
InitializeViewModel(useDropdowns);
|
||
|
|
||
|
blackboard = new SGBlackboard(ViewModel, this);
|
||
|
|
||
|
// Add default category at the top of the blackboard (create it if it doesn't exist already)
|
||
|
var existingDefaultCategory = DataStore.State.categories.FirstOrDefault();
|
||
|
if (existingDefaultCategory != null && existingDefaultCategory.IsNamedCategory() == false)
|
||
|
{
|
||
|
AddBlackboardCategory(graphDataStore, existingDefaultCategory);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Any properties that don't already have a category (for example, if this graph is being loaded from an older version that doesn't have category data)
|
||
|
var uncategorizedBlackboardItems = new List<ShaderInput>();
|
||
|
foreach (var shaderProperty in DataStore.State.properties)
|
||
|
if (IsInputUncategorized(shaderProperty))
|
||
|
uncategorizedBlackboardItems.Add(shaderProperty);
|
||
|
|
||
|
foreach (var shaderKeyword in DataStore.State.keywords)
|
||
|
if (IsInputUncategorized(shaderKeyword))
|
||
|
uncategorizedBlackboardItems.Add(shaderKeyword);
|
||
|
|
||
|
if (useDropdowns)
|
||
|
{
|
||
|
foreach (var shaderDropdown in DataStore.State.dropdowns)
|
||
|
if (IsInputUncategorized(shaderDropdown))
|
||
|
uncategorizedBlackboardItems.Add(shaderDropdown);
|
||
|
}
|
||
|
|
||
|
var addCategoryAction = new AddCategoryAction();
|
||
|
addCategoryAction.categoryDataReference = CategoryData.DefaultCategory(uncategorizedBlackboardItems);
|
||
|
graphDataStore.Dispatch(addCategoryAction);
|
||
|
}
|
||
|
|
||
|
// Get the reference to default category controller after its been added
|
||
|
m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
|
||
|
AssertHelpers.IsNotNull(m_DefaultCategoryController, "Failed to instantiate default category.");
|
||
|
|
||
|
// Handle loaded-in categories from graph first, skipping the first/default category
|
||
|
foreach (var categoryData in ViewModel.categoryInfoList.Skip(1))
|
||
|
{
|
||
|
AddBlackboardCategory(graphDataStore, categoryData);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal string editorPrefsBaseKey => "unity.shadergraph." + DataStore.State.objectId;
|
||
|
|
||
|
BlackboardCategoryController AddBlackboardCategory(GraphDataStore graphDataStore, CategoryData categoryInfo)
|
||
|
{
|
||
|
var blackboardCategoryViewModel = new BlackboardCategoryViewModel();
|
||
|
blackboardCategoryViewModel.parentView = blackboard;
|
||
|
blackboardCategoryViewModel.requestModelChangeAction = ViewModel.requestModelChangeAction;
|
||
|
blackboardCategoryViewModel.name = categoryInfo.name;
|
||
|
blackboardCategoryViewModel.associatedCategoryGuid = categoryInfo.categoryGuid;
|
||
|
blackboardCategoryViewModel.isExpanded = EditorPrefs.GetBool($"{editorPrefsBaseKey}.{categoryInfo.categoryGuid}.{ChangeCategoryIsExpandedAction.kEditorPrefKey}", true);
|
||
|
|
||
|
var blackboardCategoryController = new BlackboardCategoryController(categoryInfo, blackboardCategoryViewModel, graphDataStore);
|
||
|
if (m_BlackboardCategoryControllers.ContainsKey(categoryInfo.categoryGuid) == false)
|
||
|
{
|
||
|
m_BlackboardCategoryControllers.Add(categoryInfo.categoryGuid, blackboardCategoryController);
|
||
|
m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
AssertHelpers.Fail("Failed to add category controller due to category with same GUID already having been added.");
|
||
|
return null;
|
||
|
}
|
||
|
return blackboardCategoryController;
|
||
|
}
|
||
|
|
||
|
// Creates controller, view and view model for a blackboard item and adds the view to the specified index in the category
|
||
|
SGBlackboardRow InsertBlackboardRow(BlackboardItem shaderInput, int insertionIndex = -1)
|
||
|
{
|
||
|
return m_DefaultCategoryController.InsertBlackboardRow(shaderInput, insertionIndex);
|
||
|
}
|
||
|
|
||
|
public void UpdateBlackboardTitle(string newTitle)
|
||
|
{
|
||
|
ViewModel.title = newTitle;
|
||
|
blackboard.title = ViewModel.title;
|
||
|
}
|
||
|
|
||
|
protected override void RequestModelChange(IGraphDataAction changeAction)
|
||
|
{
|
||
|
DataStore.Dispatch(changeAction);
|
||
|
}
|
||
|
|
||
|
// Called by GraphDataStore.Subscribe after the model has been changed
|
||
|
protected override void ModelChanged(GraphData graphData, IGraphDataAction changeAction)
|
||
|
{
|
||
|
// Reconstruct view-model first
|
||
|
// TODO: hide this more generically for category types.
|
||
|
bool useDropdowns = graphData.isSubGraph;
|
||
|
InitializeViewModel(useDropdowns);
|
||
|
|
||
|
var graphView = ViewModel.parentView as MaterialGraphView;
|
||
|
|
||
|
switch (changeAction)
|
||
|
{
|
||
|
// If newly added input doesn't belong to any of the user-made categories, add it to the default category at top of blackboard
|
||
|
case AddShaderInputAction addBlackboardItemAction:
|
||
|
if (IsInputUncategorized(addBlackboardItemAction.shaderInputReference))
|
||
|
{
|
||
|
var blackboardRow = InsertBlackboardRow(addBlackboardItemAction.shaderInputReference);
|
||
|
if (blackboardRow != null)
|
||
|
{
|
||
|
var propertyView = blackboardRow.Q<SGBlackboardField>();
|
||
|
if (addBlackboardItemAction.addInputActionType == AddShaderInputAction.AddActionSource.AddMenu)
|
||
|
propertyView.OpenTextEditor();
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
// Need to handle deletion of shader inputs here as opposed to BlackboardCategoryController, as currently,
|
||
|
// once removed from the categories there is no way to associate an input with the category that owns it
|
||
|
case DeleteShaderInputAction deleteShaderInputAction:
|
||
|
foreach (var shaderInput in deleteShaderInputAction.shaderInputsToDelete)
|
||
|
RemoveInputFromBlackboard(shaderInput);
|
||
|
break;
|
||
|
|
||
|
case HandleUndoRedoAction handleUndoRedoAction:
|
||
|
ClearBlackboardCategories();
|
||
|
|
||
|
foreach (var categoryData in graphData.addedCategories)
|
||
|
AddBlackboardCategory(DataStore, categoryData);
|
||
|
|
||
|
m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
|
||
|
|
||
|
break;
|
||
|
case CopyShaderInputAction copyShaderInputAction:
|
||
|
// In the specific case of only-one keywords like Material Quality and Raytracing, they can get copied, but because only one can exist, the output copied value is null
|
||
|
if (copyShaderInputAction.copiedShaderInput != null && IsInputUncategorized(copyShaderInputAction.copiedShaderInput))
|
||
|
{
|
||
|
var blackboardRow = InsertBlackboardRow(copyShaderInputAction.copiedShaderInput, copyShaderInputAction.insertIndex);
|
||
|
var propertyView = blackboardRow.Q<SGBlackboardField>();
|
||
|
graphView?.AddToSelectionNoUndoRecord(propertyView);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case AddCategoryAction addCategoryAction:
|
||
|
AddBlackboardCategory(DataStore, addCategoryAction.categoryDataReference);
|
||
|
// Iterate through anything that is selected currently
|
||
|
foreach (var selectedElement in blackboard.selection.ToList())
|
||
|
{
|
||
|
if (selectedElement is SGBlackboardField { userData: ShaderInput shaderInput })
|
||
|
{
|
||
|
// If a blackboard item is selected, first remove it from the blackboard
|
||
|
RemoveInputFromBlackboard(shaderInput);
|
||
|
|
||
|
// Then add input to the new category
|
||
|
var addItemToCategoryAction = new AddItemToCategoryAction();
|
||
|
addItemToCategoryAction.categoryGuid = addCategoryAction.categoryDataReference.categoryGuid;
|
||
|
addItemToCategoryAction.itemToAdd = shaderInput;
|
||
|
DataStore.Dispatch(addItemToCategoryAction);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case DeleteCategoryAction deleteCategoryAction:
|
||
|
// Clean up deleted categories
|
||
|
foreach (var categoryGUID in deleteCategoryAction.categoriesToRemoveGuids)
|
||
|
{
|
||
|
RemoveBlackboardCategory(categoryGUID);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case MoveCategoryAction moveCategoryAction:
|
||
|
ClearBlackboardCategories();
|
||
|
foreach (var categoryData in ViewModel.categoryInfoList)
|
||
|
AddBlackboardCategory(graphData.owner.graphDataStore, categoryData);
|
||
|
break;
|
||
|
|
||
|
case CopyCategoryAction copyCategoryAction:
|
||
|
var blackboardCategory = AddBlackboardCategory(graphData.owner.graphDataStore, copyCategoryAction.newCategoryDataReference);
|
||
|
if (blackboardCategory != null)
|
||
|
graphView?.AddToSelectionNoUndoRecord(blackboardCategory.blackboardCategoryView);
|
||
|
break;
|
||
|
case ShaderVariantLimitAction shaderVariantLimitAction:
|
||
|
blackboard.SetCurrentVariantUsage(shaderVariantLimitAction.currentVariantCount, shaderVariantLimitAction.maxVariantCount);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Lets all event handlers this controller owns/manages know that the model has changed
|
||
|
// Usually this is to update views and make them reconstruct themself from updated view-model
|
||
|
//NotifyChange(changeAction);
|
||
|
|
||
|
// Let child controllers know about changes to this controller so they may update themselves in turn
|
||
|
//ApplyChanges();
|
||
|
}
|
||
|
|
||
|
void RemoveInputFromBlackboard(ShaderInput shaderInput)
|
||
|
{
|
||
|
// Check if input is in one of the categories
|
||
|
foreach (var controller in m_BlackboardCategoryControllers.Values)
|
||
|
{
|
||
|
var blackboardRow = controller.FindBlackboardRow(shaderInput);
|
||
|
if (blackboardRow != null)
|
||
|
{
|
||
|
controller.RemoveBlackboardRow(shaderInput);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool IsInputUncategorized(ShaderInput shaderInput)
|
||
|
{
|
||
|
// Skip the first category controller as that is guaranteed to be the default category
|
||
|
foreach (var categoryController in m_BlackboardCategoryControllers.Values.Skip(1))
|
||
|
{
|
||
|
if (categoryController.IsInputInCategory(shaderInput))
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public SGBlackboardCategory GetBlackboardCategory(string inputGuid)
|
||
|
{
|
||
|
foreach (var categoryController in m_BlackboardCategoryControllers.Values)
|
||
|
{
|
||
|
if (categoryController.Model.categoryGuid == inputGuid)
|
||
|
return categoryController.blackboardCategoryView;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public SGBlackboardRow GetBlackboardRow(ShaderInput blackboardItem)
|
||
|
{
|
||
|
foreach (var categoryController in m_BlackboardCategoryControllers.Values)
|
||
|
{
|
||
|
var blackboardRow = categoryController.FindBlackboardRow(blackboardItem);
|
||
|
if (blackboardRow != null)
|
||
|
return blackboardRow;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
int numberOfCategories => m_BlackboardCategoryControllers.Count;
|
||
|
|
||
|
// Gets the index after the currently selected shader input for pasting properties into this graph
|
||
|
internal int GetInsertionIndexForPaste()
|
||
|
{
|
||
|
if (blackboard?.selection == null || blackboard.selection.Count == 0)
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
foreach (ISelectable selection in blackboard.selection)
|
||
|
{
|
||
|
if (selection is SGBlackboardField blackboardPropertyView)
|
||
|
{
|
||
|
SGBlackboardRow row = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardRow>();
|
||
|
SGBlackboardCategory category = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardCategory>();
|
||
|
if (row == null || category == null)
|
||
|
continue;
|
||
|
int blackboardFieldIndex = category.IndexOf(row);
|
||
|
|
||
|
return blackboardFieldIndex;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void RemoveBlackboardCategory(string categoryGUID)
|
||
|
{
|
||
|
m_BlackboardCategoryControllers.TryGetValue(categoryGUID, out var blackboardCategoryController);
|
||
|
if (blackboardCategoryController != null)
|
||
|
{
|
||
|
blackboardCategoryController.Dispose();
|
||
|
m_BlackboardCategoryControllers.Remove(categoryGUID);
|
||
|
}
|
||
|
else
|
||
|
AssertHelpers.Fail("Tried to remove a category that doesn't exist. ");
|
||
|
}
|
||
|
|
||
|
public override void Dispose()
|
||
|
{
|
||
|
if (m_Blackboard == null)
|
||
|
return;
|
||
|
|
||
|
base.Dispose();
|
||
|
m_DefaultCategoryController = null;
|
||
|
ClearBlackboardCategories();
|
||
|
|
||
|
m_Blackboard?.Dispose();
|
||
|
m_Blackboard = null;
|
||
|
}
|
||
|
|
||
|
void ClearBlackboardCategories()
|
||
|
{
|
||
|
foreach (var categoryController in m_BlackboardCategoryControllers.Values)
|
||
|
{
|
||
|
categoryController.Dispose();
|
||
|
}
|
||
|
m_BlackboardCategoryControllers.Clear();
|
||
|
}
|
||
|
|
||
|
// Meant to be used by UI testing in order to clear blackboard state
|
||
|
internal void ResetBlackboardState()
|
||
|
{
|
||
|
ClearBlackboardCategories();
|
||
|
var addCategoryAction = new AddCategoryAction();
|
||
|
addCategoryAction.categoryDataReference = CategoryData.DefaultCategory();
|
||
|
DataStore.Dispatch(addCategoryAction);
|
||
|
}
|
||
|
}
|
||
|
}
|