You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
17 KiB
437 lines
17 KiB
using System; |
|
using System.Text; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.IO.IsolatedStorage; |
|
using System.Linq; |
|
using System.Text.RegularExpressions; |
|
using UnityEditor.Graphing; |
|
using UnityEditor.Graphing.Util; |
|
using UnityEditorInternal; |
|
using Debug = UnityEngine.Debug; |
|
using System.Reflection; |
|
using System.Runtime.Remoting.Metadata.W3cXsd2001; |
|
using UnityEditor.ProjectWindowCallback; |
|
using UnityEditor.ShaderGraph.Internal; |
|
using UnityEngine; |
|
using UnityEngine.Rendering; |
|
using Object = System.Object; |
|
|
|
namespace UnityEditor.ShaderGraph |
|
{ |
|
// a structure used to track active variable dependencies in the shader code |
|
// (i.e. the use of uv0 in the pixel shader means we need a uv0 interpolator, etc.) |
|
struct Dependency |
|
{ |
|
public string name; // the name of the thing |
|
public string dependsOn; // the thing above depends on this -- it reads it / calls it / requires it to be defined |
|
|
|
public Dependency(string name, string dependsOn) |
|
{ |
|
this.name = name; |
|
this.dependsOn = dependsOn; |
|
} |
|
}; |
|
|
|
[System.AttributeUsage(System.AttributeTargets.Struct)] |
|
class InterpolatorPack : System.Attribute |
|
{ |
|
public InterpolatorPack() |
|
{ |
|
} |
|
} |
|
|
|
// attribute used to flag a field as needing an HLSL semantic applied |
|
// i.e. float3 position : POSITION; |
|
// ^ semantic |
|
[System.AttributeUsage(System.AttributeTargets.Field)] |
|
class Semantic : System.Attribute |
|
{ |
|
public string semantic; |
|
|
|
public Semantic(string semantic) |
|
{ |
|
this.semantic = semantic; |
|
} |
|
} |
|
|
|
// attribute used to flag a field as being optional |
|
// i.e. if it is not active, then we can omit it from the struct |
|
[System.AttributeUsage(System.AttributeTargets.Field)] |
|
class Optional : System.Attribute |
|
{ |
|
public Optional() |
|
{ |
|
} |
|
} |
|
|
|
// attribute used to override the HLSL type of a field with a custom type string |
|
[System.AttributeUsage(System.AttributeTargets.Field)] |
|
class OverrideType : System.Attribute |
|
{ |
|
public string typeName; |
|
|
|
public OverrideType(string typeName) |
|
{ |
|
this.typeName = typeName; |
|
} |
|
} |
|
|
|
// attribute used to force system generated fields to bottom of structs |
|
[System.AttributeUsage(System.AttributeTargets.Field)] |
|
class SystemGenerated : System.Attribute |
|
{ |
|
public SystemGenerated() |
|
{ |
|
} |
|
} |
|
|
|
// attribute used to disable a field using a preprocessor #if |
|
[System.AttributeUsage(System.AttributeTargets.Field)] |
|
class PreprocessorIf : System.Attribute |
|
{ |
|
public string conditional; |
|
|
|
public PreprocessorIf(string conditional) |
|
{ |
|
this.conditional = conditional; |
|
} |
|
} |
|
|
|
class NewGraphAction : EndNameEditAction |
|
{ |
|
Target[] m_Targets; |
|
public Target[] targets |
|
{ |
|
get => m_Targets; |
|
set => m_Targets = value; |
|
} |
|
|
|
BlockFieldDescriptor[] m_Blocks; |
|
public BlockFieldDescriptor[] blocks |
|
{ |
|
get => m_Blocks; |
|
set => m_Blocks = value; |
|
} |
|
|
|
public override void Action(int instanceId, string pathName, string resourceFile) |
|
{ |
|
var graph = new GraphData(); |
|
graph.AddContexts(); |
|
graph.InitializeOutputs(m_Targets, m_Blocks); |
|
|
|
graph.path = "Shader Graphs"; |
|
FileUtilities.WriteShaderGraphToDisk(pathName, graph); |
|
AssetDatabase.Refresh(); |
|
|
|
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<Shader>(pathName); |
|
Selection.activeObject = obj; |
|
} |
|
} |
|
|
|
static class GraphUtil |
|
{ |
|
internal static bool CheckForRecursiveDependencyOnPendingSave(string saveFilePath, IEnumerable<SubGraphNode> subGraphNodes, string context = null) |
|
{ |
|
var overwriteGUID = AssetDatabase.AssetPathToGUID(saveFilePath); |
|
if (!string.IsNullOrEmpty(overwriteGUID)) |
|
{ |
|
foreach (var sgNode in subGraphNodes) |
|
{ |
|
var asset = sgNode?.asset; |
|
if (asset == null) |
|
{ |
|
// cannot read the asset; might be recursive but we can't tell... should we return "maybe"? |
|
// I think to be minimally intrusive to the user we can assume "No" in this case, |
|
// even though this may miss recursions in extraordinary cases. |
|
// it's more important to allow the user to save their files than to catch 100% of recursions |
|
continue; |
|
} |
|
else if ((asset.assetGuid == overwriteGUID) || asset.descendents.Contains(overwriteGUID)) |
|
{ |
|
if (context != null) |
|
{ |
|
Debug.LogWarning(context + " CANCELLED to avoid a generating a reference loop: the SubGraph '" + sgNode.asset.name + "' references the target file '" + saveFilePath + "'"); |
|
EditorUtility.DisplayDialog( |
|
context + " CANCELLED", |
|
"Saving the file would generate a reference loop, because the SubGraph '" + sgNode.asset.name + "' references the target file '" + saveFilePath + "'", "Cancel"); |
|
} |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
internal static string ConvertCamelCase(string text, bool preserveAcronyms) |
|
{ |
|
if (string.IsNullOrEmpty(text)) |
|
return string.Empty; |
|
StringBuilder newText = new StringBuilder(text.Length * 2); |
|
newText.Append(text[0]); |
|
for (int i = 1; i < text.Length; i++) |
|
{ |
|
if (char.IsUpper(text[i])) |
|
if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) || |
|
(preserveAcronyms && char.IsUpper(text[i - 1]) && |
|
i < text.Length - 1 && !char.IsUpper(text[i + 1]))) |
|
newText.Append(' '); |
|
newText.Append(text[i]); |
|
} |
|
return newText.ToString(); |
|
} |
|
|
|
public static void CreateNewGraph() |
|
{ |
|
var graphItem = ScriptableObject.CreateInstance<NewGraphAction>(); |
|
graphItem.targets = null; |
|
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, graphItem, |
|
string.Format("New Shader Graph.{0}", ShaderGraphImporter.Extension), null, null); |
|
} |
|
|
|
public static void CreateNewGraphWithOutputs(Target[] targets, BlockFieldDescriptor[] blockDescriptors) |
|
{ |
|
var graphItem = ScriptableObject.CreateInstance<NewGraphAction>(); |
|
graphItem.targets = targets; |
|
graphItem.blocks = blockDescriptors; |
|
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, graphItem, |
|
string.Format("New Shader Graph.{0}", ShaderGraphImporter.Extension), null, null); |
|
} |
|
|
|
public static bool TryGetMetadataOfType<T>(this Shader shader, out T obj) where T : ScriptableObject |
|
{ |
|
obj = null; |
|
if (!shader.IsShaderGraphAsset()) |
|
return false; |
|
|
|
var path = AssetDatabase.GetAssetPath(shader); |
|
foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(path)) |
|
{ |
|
if (asset is T metadataAsset) |
|
{ |
|
obj = metadataAsset; |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// this will work on ALL shadergraph-built shaders, in memory or asset based |
|
public static bool IsShaderGraph(this Material material) |
|
{ |
|
var shaderGraphTag = material.GetTag("ShaderGraphShader", false, null); |
|
return !string.IsNullOrEmpty(shaderGraphTag); |
|
} |
|
|
|
// NOTE: this ONLY works for ASSET based Shaders, if you created a temporary shader in memory, it won't work |
|
public static bool IsShaderGraphAsset(this Shader shader) |
|
{ |
|
var path = AssetDatabase.GetAssetPath(shader); |
|
var importer = AssetImporter.GetAtPath(path); |
|
return importer is ShaderGraphImporter; |
|
} |
|
|
|
[Obsolete("Use IsShaderGraphAsset instead", false)] |
|
public static bool IsShaderGraph(this Shader shader) => shader.IsShaderGraphAsset(); |
|
|
|
static void Visit(List<AbstractMaterialNode> outputList, Dictionary<string, AbstractMaterialNode> unmarkedNodes, AbstractMaterialNode node) |
|
{ |
|
if (!unmarkedNodes.ContainsKey(node.objectId)) |
|
return; |
|
foreach (var slot in node.GetInputSlots<MaterialSlot>()) |
|
{ |
|
foreach (var edge in node.owner.GetEdges(slot.slotReference)) |
|
{ |
|
var inputNode = edge.outputSlot.node; |
|
Visit(outputList, unmarkedNodes, inputNode); |
|
} |
|
} |
|
unmarkedNodes.Remove(node.objectId); |
|
outputList.Add(node); |
|
} |
|
|
|
static Dictionary<SerializationHelper.TypeSerializationInfo, SerializationHelper.TypeSerializationInfo> s_LegacyTypeRemapping; |
|
|
|
public static Dictionary<SerializationHelper.TypeSerializationInfo, SerializationHelper.TypeSerializationInfo> GetLegacyTypeRemapping() |
|
{ |
|
if (s_LegacyTypeRemapping == null) |
|
{ |
|
s_LegacyTypeRemapping = new Dictionary<SerializationHelper.TypeSerializationInfo, SerializationHelper.TypeSerializationInfo>(); |
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) |
|
{ |
|
foreach (var type in assembly.GetTypesOrNothing()) |
|
{ |
|
if (type.IsAbstract) |
|
continue; |
|
foreach (var attribute in type.GetCustomAttributes(typeof(FormerNameAttribute), false)) |
|
{ |
|
var legacyAttribute = (FormerNameAttribute)attribute; |
|
var serializationInfo = new SerializationHelper.TypeSerializationInfo { fullName = legacyAttribute.fullName }; |
|
s_LegacyTypeRemapping[serializationInfo] = SerializationHelper.GetTypeSerializableAsString(type); |
|
} |
|
} |
|
} |
|
} |
|
|
|
return s_LegacyTypeRemapping; |
|
} |
|
|
|
/// <summary> |
|
/// Sanitizes a supplied string such that it does not collide |
|
/// with any other name in a collection. |
|
/// </summary> |
|
/// <param name="existingNames"> |
|
/// A collection of names that the new name should not collide with. |
|
/// </param> |
|
/// <param name="duplicateFormat"> |
|
/// The format applied to the name if a duplicate exists. |
|
/// This must be a format string that contains `{0}` and `{1}` |
|
/// once each. An example could be `{0} ({1})`, which will append ` (n)` |
|
/// to the name for the n`th duplicate. |
|
/// </param> |
|
/// <param name="name"> |
|
/// The name to be sanitized. |
|
/// </param> |
|
/// <returns> |
|
/// A name that is distinct form any name in `existingNames`. |
|
/// </returns> |
|
internal static string SanitizeName(IEnumerable<string> existingNames, string duplicateFormat, string name, string disallowedPatternRegex = "\"") |
|
{ |
|
name = Regex.Replace(name, disallowedPatternRegex, "_"); |
|
return DeduplicateName(existingNames, duplicateFormat, name); |
|
} |
|
|
|
internal static string SanitizeCategoryName(string categoryName, string disallowedPatternRegex = "\"") |
|
{ |
|
return Regex.Replace(categoryName, disallowedPatternRegex, "_"); |
|
} |
|
|
|
internal static string DeduplicateName(IEnumerable<string> existingNames, string duplicateFormat, string name) |
|
{ |
|
if (!existingNames.Contains(name)) |
|
return name; |
|
|
|
string escapedDuplicateFormat = Regex.Escape(duplicateFormat); |
|
|
|
// Escaped format will escape string interpolation, so the escape characters must be removed for these. |
|
escapedDuplicateFormat = escapedDuplicateFormat.Replace(@"\{0}", @"{0}"); |
|
escapedDuplicateFormat = escapedDuplicateFormat.Replace(@"\{1}", @"{1}"); |
|
|
|
var baseRegex = new Regex(string.Format(escapedDuplicateFormat, @"^(.*)", @"(\d+)")); |
|
|
|
var baseMatch = baseRegex.Match(name); |
|
if (baseMatch.Success) |
|
name = baseMatch.Groups[1].Value; |
|
|
|
string baseNameExpression = string.Format(@"^{0}", Regex.Escape(name)); |
|
var regex = new Regex(string.Format(escapedDuplicateFormat, baseNameExpression, @"(\d+)") + "$"); |
|
|
|
var existingDuplicateNumbers = existingNames.Select(existingName => regex.Match(existingName)).Where(m => m.Success).Select(m => int.Parse(m.Groups[1].Value)).Where(n => n > 0).Distinct().ToList(); |
|
|
|
var duplicateNumber = 1; |
|
existingDuplicateNumbers.Sort(); |
|
if (existingDuplicateNumbers.Any() && existingDuplicateNumbers.First() == 1) |
|
{ |
|
duplicateNumber = existingDuplicateNumbers.Last() + 1; |
|
for (var i = 1; i < existingDuplicateNumbers.Count; i++) |
|
{ |
|
if (existingDuplicateNumbers[i - 1] != existingDuplicateNumbers[i] - 1) |
|
{ |
|
duplicateNumber = existingDuplicateNumbers[i - 1] + 1; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
return string.Format(duplicateFormat, name, duplicateNumber); |
|
} |
|
|
|
public static bool WriteToFile(string path, string content) |
|
{ |
|
try |
|
{ |
|
File.WriteAllText(path, content); |
|
return true; |
|
} |
|
catch (Exception e) |
|
{ |
|
Debug.LogError(e); |
|
return false; |
|
} |
|
} |
|
|
|
public static void OpenFile(string path) |
|
{ |
|
string filePath = Path.GetFullPath(path); |
|
if (!File.Exists(filePath)) |
|
{ |
|
Debug.LogError(string.Format("Path {0} doesn't exists", path)); |
|
return; |
|
} |
|
|
|
string externalScriptEditor = ScriptEditorUtility.GetExternalScriptEditor(); |
|
if (externalScriptEditor != "internal") |
|
{ |
|
InternalEditorUtility.OpenFileAtLineExternal(filePath, 0); |
|
} |
|
else |
|
{ |
|
Process p = new Process(); |
|
p.StartInfo.FileName = filePath; |
|
p.EnableRaisingEvents = true; |
|
p.Exited += (Object obj, EventArgs args) => |
|
{ |
|
if (p.ExitCode != 0) |
|
Debug.LogWarningFormat("Unable to open {0}: Check external editor in preferences", filePath); |
|
}; |
|
p.Start(); |
|
} |
|
} |
|
|
|
// |
|
// Find all nodes of the given type downstream from the given node |
|
// Returns a unique list. So even if a node can be reached through different paths it will be present only once. |
|
// |
|
public static List<NodeType> FindDownStreamNodesOfType<NodeType>(AbstractMaterialNode node) where NodeType : AbstractMaterialNode |
|
{ |
|
// Should never be called without a node |
|
Debug.Assert(node != null); |
|
|
|
HashSet<AbstractMaterialNode> visitedNodes = new HashSet<AbstractMaterialNode>(); |
|
List<NodeType> vtNodes = new List<NodeType>(); |
|
Queue<AbstractMaterialNode> nodeStack = new Queue<AbstractMaterialNode>(); |
|
nodeStack.Enqueue(node); |
|
visitedNodes.Add(node); |
|
|
|
while (nodeStack.Count > 0) |
|
{ |
|
AbstractMaterialNode visit = nodeStack.Dequeue(); |
|
|
|
// Flood fill through all the nodes |
|
foreach (var slot in visit.GetInputSlots<MaterialSlot>()) |
|
{ |
|
foreach (var edge in visit.owner.GetEdges(slot.slotReference)) |
|
{ |
|
var inputNode = edge.outputSlot.node; |
|
if (!visitedNodes.Contains(inputNode)) |
|
{ |
|
nodeStack.Enqueue(inputNode); |
|
visitedNodes.Add(inputNode); |
|
} |
|
} |
|
} |
|
|
|
// Extract vt node |
|
if (visit is NodeType) |
|
{ |
|
NodeType vtNode = visit as NodeType; |
|
vtNodes.Add(vtNode); |
|
} |
|
} |
|
|
|
return vtNodes; |
|
} |
|
} |
|
}
|
|
|