CustomFunctionNode.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. using System;
  2. using System.IO;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEngine;
  6. using UnityEditor.Graphing;
  7. using UnityEditor.Rendering;
  8. using UnityEngine.UIElements;
  9. using UnityEditor.ShaderGraph.Drawing;
  10. namespace UnityEditor.ShaderGraph
  11. {
  12. [HasDependencies(typeof(MinimalCustomFunctionNode))]
  13. [Title("Utility", "Custom Function")]
  14. class CustomFunctionNode : AbstractMaterialNode, IGeneratesBodyCode, IGeneratesFunction, IHasSettings
  15. {
  16. [Serializable]
  17. public class MinimalCustomFunctionNode : IHasDependencies
  18. {
  19. [SerializeField]
  20. HlslSourceType m_SourceType = HlslSourceType.File;
  21. [SerializeField]
  22. string m_FunctionName = k_DefaultFunctionName;
  23. [SerializeField]
  24. string m_FunctionSource = null;
  25. public void GetSourceAssetDependencies(List<string> paths)
  26. {
  27. if (m_SourceType == HlslSourceType.File)
  28. {
  29. m_FunctionSource = UpgradeFunctionSource(m_FunctionSource);
  30. if (IsValidFunction(m_SourceType, m_FunctionName, m_FunctionSource, null))
  31. {
  32. paths.Add(AssetDatabase.GUIDToAssetPath(m_FunctionSource));
  33. }
  34. }
  35. }
  36. }
  37. public static string[] s_ValidExtensions = { ".hlsl", ".cginc" };
  38. const string k_InvalidFileType = "Source file is not a valid file type. Valid file extensions are .hlsl and .cginc";
  39. const string k_MissingOutputSlot = "A Custom Function Node must have at least one output slot";
  40. public CustomFunctionNode()
  41. {
  42. name = "Custom Function";
  43. }
  44. public override bool hasPreview => true;
  45. [SerializeField]
  46. HlslSourceType m_SourceType = HlslSourceType.File;
  47. public HlslSourceType sourceType
  48. {
  49. get => m_SourceType;
  50. set => m_SourceType = value;
  51. }
  52. [SerializeField]
  53. string m_FunctionName = k_DefaultFunctionName;
  54. const string k_DefaultFunctionName = "Enter function name here...";
  55. public string functionName
  56. {
  57. get => m_FunctionName;
  58. set => m_FunctionName = value;
  59. }
  60. public static string defaultFunctionName => k_DefaultFunctionName;
  61. [SerializeField]
  62. string m_FunctionSource;
  63. const string k_DefaultFunctionSource = "Enter function source file path here...";
  64. public string functionSource
  65. {
  66. get => m_FunctionSource;
  67. set => m_FunctionSource = value;
  68. }
  69. [SerializeField]
  70. string m_FunctionBody = k_DefaultFunctionBody;
  71. const string k_DefaultFunctionBody = "Enter function body here...";
  72. public string functionBody
  73. {
  74. get => m_FunctionBody;
  75. set => m_FunctionBody = value;
  76. }
  77. public static string defaultFunctionBody => k_DefaultFunctionBody;
  78. public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode)
  79. {
  80. List<MaterialSlot> slots = new List<MaterialSlot>();
  81. GetOutputSlots<MaterialSlot>(slots);
  82. if(!IsValidFunction())
  83. {
  84. if(generationMode == GenerationMode.Preview && slots.Count != 0)
  85. {
  86. slots.OrderBy(s => s.id);
  87. sb.AppendLine("{0} {1};",
  88. slots[0].concreteValueType.ToShaderString(),
  89. GetVariableNameForSlot(slots[0].id));
  90. }
  91. return;
  92. }
  93. foreach (var argument in slots)
  94. sb.AppendLine("{0} {1};",
  95. argument.concreteValueType.ToShaderString(),
  96. GetVariableNameForSlot(argument.id));
  97. string call = $"{functionName}_$precision(";
  98. bool first = true;
  99. slots.Clear();
  100. GetInputSlots<MaterialSlot>(slots);
  101. foreach (var argument in slots)
  102. {
  103. if (!first)
  104. call += ", ";
  105. first = false;
  106. call += SlotInputValue(argument, generationMode);
  107. }
  108. slots.Clear();
  109. GetOutputSlots<MaterialSlot>(slots);
  110. foreach (var argument in slots)
  111. {
  112. if (!first)
  113. call += ", ";
  114. first = false;
  115. call += GetVariableNameForSlot(argument.id);
  116. }
  117. call += ");";
  118. sb.AppendLine(call);
  119. }
  120. public void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode)
  121. {
  122. if(!IsValidFunction())
  123. return;
  124. switch (sourceType)
  125. {
  126. case HlslSourceType.File:
  127. registry.ProvideFunction(functionSource, builder =>
  128. {
  129. string path = AssetDatabase.GUIDToAssetPath(functionSource);
  130. // This is required for upgrading without console errors
  131. if(string.IsNullOrEmpty(path))
  132. path = functionSource;
  133. string hash;
  134. try
  135. {
  136. hash = AssetDatabase.GetAssetDependencyHash(path).ToString();
  137. }
  138. catch
  139. {
  140. hash = "Failed to compute hash for include";
  141. }
  142. builder.AppendLine($"// {hash}");
  143. builder.AppendLine($"#include \"{path}\"");
  144. });
  145. break;
  146. case HlslSourceType.String:
  147. registry.ProvideFunction(functionName, builder =>
  148. {
  149. builder.AppendLine(GetFunctionHeader());
  150. using (builder.BlockScope())
  151. {
  152. builder.AppendLines(functionBody);
  153. }
  154. });
  155. break;
  156. default:
  157. throw new ArgumentOutOfRangeException();
  158. }
  159. }
  160. string GetFunctionHeader()
  161. {
  162. string header = $"void {functionName}_$precision(";
  163. var first = true;
  164. List<MaterialSlot> slots = new List<MaterialSlot>();
  165. GetInputSlots<MaterialSlot>(slots);
  166. foreach (var argument in slots)
  167. {
  168. if (!first)
  169. header += ", ";
  170. first = false;
  171. header += $"{argument.concreteValueType.ToShaderString()} {argument.shaderOutputName}";
  172. }
  173. slots.Clear();
  174. GetOutputSlots<MaterialSlot>(slots);
  175. foreach (var argument in slots)
  176. {
  177. if (!first)
  178. header += ", ";
  179. first = false;
  180. header += $"out {argument.concreteValueType.ToShaderString()} {argument.shaderOutputName}";
  181. }
  182. header += ")";
  183. return header;
  184. }
  185. string SlotInputValue(MaterialSlot port, GenerationMode generationMode)
  186. {
  187. IEdge[] edges = port.owner.owner.GetEdges(port.slotReference).ToArray();
  188. if (edges.Any())
  189. {
  190. var fromSocketRef = edges[0].outputSlot;
  191. var fromNode = owner.GetNodeFromGuid<AbstractMaterialNode>(fromSocketRef.nodeGuid);
  192. if (fromNode == null)
  193. return string.Empty;
  194. var slot = fromNode.FindOutputSlot<MaterialSlot>(fromSocketRef.slotId);
  195. if (slot == null)
  196. return string.Empty;
  197. return ShaderGenerator.AdaptNodeOutput(fromNode, slot.id, port.concreteValueType);
  198. }
  199. return port.GetDefaultValue(generationMode);
  200. }
  201. bool IsValidFunction()
  202. {
  203. return IsValidFunction(sourceType, functionName, functionSource, functionBody);
  204. }
  205. static bool IsValidFunction(HlslSourceType sourceType, string functionName, string functionSource, string functionBody)
  206. {
  207. bool validFunctionName = !string.IsNullOrEmpty(functionName) && functionName != k_DefaultFunctionName;
  208. if(sourceType == HlslSourceType.String)
  209. {
  210. bool validFunctionBody = !string.IsNullOrEmpty(functionBody) && functionBody != k_DefaultFunctionBody;
  211. return validFunctionName & validFunctionBody;
  212. }
  213. else
  214. {
  215. if(!validFunctionName || string.IsNullOrEmpty(functionSource) || functionSource == k_DefaultFunctionSource)
  216. return false;
  217. string path = AssetDatabase.GUIDToAssetPath(functionSource);
  218. if(string.IsNullOrEmpty(path))
  219. path = functionSource;
  220. string extension = Path.GetExtension(path);
  221. return s_ValidExtensions.Contains(extension);
  222. }
  223. }
  224. void ValidateSlotName()
  225. {
  226. List<MaterialSlot> slots = new List<MaterialSlot>();
  227. GetSlots(slots);
  228. foreach (var slot in slots)
  229. {
  230. var error = NodeUtils.ValidateSlotName(slot.RawDisplayName(), out string errorMessage);
  231. if (error)
  232. {
  233. owner.AddValidationError(tempId, errorMessage);
  234. break;
  235. }
  236. }
  237. }
  238. public override void ValidateNode()
  239. {
  240. if (!this.GetOutputSlots<MaterialSlot>().Any())
  241. {
  242. owner.AddValidationError(tempId, k_MissingOutputSlot, ShaderCompilerMessageSeverity.Warning);
  243. }
  244. if(sourceType == HlslSourceType.File)
  245. {
  246. if(!string.IsNullOrEmpty(functionSource))
  247. {
  248. string path = AssetDatabase.GUIDToAssetPath(functionSource);
  249. if(!string.IsNullOrEmpty(path))
  250. {
  251. string extension = path.Substring(path.LastIndexOf('.'));
  252. if(!s_ValidExtensions.Contains(extension))
  253. {
  254. owner.AddValidationError(tempId, k_InvalidFileType, ShaderCompilerMessageSeverity.Error);
  255. }
  256. }
  257. }
  258. }
  259. ValidateSlotName();
  260. base.ValidateNode();
  261. }
  262. public void Reload(HashSet<string> changedFileDependencies)
  263. {
  264. if (changedFileDependencies.Contains(m_FunctionSource))
  265. {
  266. owner.ClearErrorsForNode(this);
  267. ValidateNode();
  268. Dirty(ModificationScope.Graph);
  269. }
  270. }
  271. public VisualElement CreateSettingsElement()
  272. {
  273. PropertySheet ps = new PropertySheet();
  274. ps.Add(new ReorderableSlotListView(this, SlotType.Input));
  275. ps.Add(new ReorderableSlotListView(this, SlotType.Output));
  276. ps.Add(new HlslFunctionView(this));
  277. return ps;
  278. }
  279. public static string UpgradeFunctionSource(string functionSource)
  280. {
  281. // Handle upgrade from legacy asset path version
  282. // If functionSource is not empty or a guid then assume it is legacy version
  283. // If asset can be loaded from path then get its guid
  284. // Otherwise it was the default string so set to empty
  285. Guid guid;
  286. if(!string.IsNullOrEmpty(functionSource) && !Guid.TryParse(functionSource, out guid))
  287. {
  288. string guidString = string.Empty;
  289. TextAsset textAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(functionSource);
  290. if(textAsset != null)
  291. {
  292. long localId;
  293. AssetDatabase.TryGetGUIDAndLocalFileIdentifier(textAsset, out guidString, out localId);
  294. }
  295. functionSource = guidString;
  296. }
  297. return functionSource;
  298. }
  299. public override void OnAfterDeserialize()
  300. {
  301. base.OnAfterDeserialize();
  302. functionSource = UpgradeFunctionSource(functionSource);
  303. }
  304. }
  305. }