A game about forced loneliness, made by TACStudios
1using System;
2using System.IO;
3using System.Collections.Generic;
4using System.Linq;
5using UnityEngine;
6using UnityEditor.Graphing;
7using UnityEditor.Rendering;
8using UnityEngine.UIElements;
9using UnityEditor.ShaderGraph.Drawing;
10using System.Text;
11
12namespace UnityEditor.ShaderGraph
13{
14 [HasDependencies(typeof(MinimalCustomFunctionNode))]
15 [Title("Utility", "Custom Function")]
16 class CustomFunctionNode : AbstractMaterialNode, IGeneratesBodyCode, IGeneratesFunction, IMayRequireTransform
17 {
18 // 0 original version
19 // 1 differentiate between struct-based UnityTexture2D and bare Texture2D resources (for all texture and samplerstate resources)
20 public override int latestVersion => 1;
21
22 public override IEnumerable<int> allowedNodeVersions => new int[] { 1 };
23
24 [Serializable]
25 public class MinimalCustomFunctionNode : IHasDependencies
26 {
27 [SerializeField]
28 HlslSourceType m_SourceType = HlslSourceType.File;
29
30 [SerializeField]
31 string m_FunctionName = k_DefaultFunctionName;
32
33 [SerializeField]
34 string m_FunctionSource = null;
35
36 public void GetSourceAssetDependencies(AssetCollection assetCollection)
37 {
38 if (m_SourceType == HlslSourceType.File)
39 {
40 m_FunctionSource = UpgradeFunctionSource(m_FunctionSource);
41 if (IsValidFunction(m_SourceType, m_FunctionName, m_FunctionSource, null))
42 {
43 if (GUID.TryParse(m_FunctionSource, out GUID guid))
44 {
45 // as this is just #included into the generated .shader file
46 // it doesn't actually need to be a dependency, other than for export package
47 assetCollection.AddAssetDependency(guid, AssetCollection.Flags.IncludeInExportPackage);
48 }
49 }
50 }
51 }
52 }
53
54 enum SourceFileStatus
55 {
56 Empty, // No File specified
57 DoesNotExist, // Either file doesn't exist (empty name) or guid points to a non-existant file
58 Invalid, // File exists but isn't of a valid type (such as wrong extension)
59 Valid
60 };
61
62 // With ShaderInclude asset type, it should no longer be necessary to soft-check the extension.
63 public static string[] s_ValidExtensions = { ".hlsl", ".cginc", ".cg" };
64 const string k_InvalidFileType = "Source file is not a valid file type. Valid file extensions are .hlsl, .cginc, and .cg";
65 const string k_MissingFile = "Source file does not exist. A valid .hlsl, .cginc, or .cg file must be referenced";
66 const string k_MissingOutputSlot = "A Custom Function Node must have at least one output slot";
67
68 public CustomFunctionNode()
69 {
70 UpdateNodeName();
71 synonyms = new string[] { "code", "HLSL" };
72 }
73
74 void UpdateNodeName()
75 {
76 if ((functionName == defaultFunctionName) || (functionName == null))
77 name = "Custom Function";
78 else
79 name = functionName + " (Custom Function)";
80 }
81
82 public override bool hasPreview => true;
83
84 [SerializeField]
85 HlslSourceType m_SourceType = HlslSourceType.File;
86
87 public HlslSourceType sourceType
88 {
89 get => m_SourceType;
90 set => m_SourceType = value;
91 }
92
93 [SerializeField]
94 string m_FunctionName = k_DefaultFunctionName;
95
96 const string k_DefaultFunctionName = "Enter function name here...";
97
98 public string functionName
99 {
100 get => m_FunctionName;
101 set
102 {
103 m_FunctionName = value;
104 UpdateNodeName();
105 }
106 }
107
108 public string hlslFunctionName
109 {
110 get => m_FunctionName + "_$precision";
111 }
112
113
114 public static string defaultFunctionName => k_DefaultFunctionName;
115
116 [SerializeField]
117 string m_FunctionSource;
118
119 const string k_DefaultFunctionSource = "Enter function source file path here...";
120
121 public string functionSource
122 {
123 get => m_FunctionSource;
124 set => m_FunctionSource = value;
125 }
126
127 [SerializeField]
128 private bool m_FunctionSourceUsePragmas = true;
129
130 public bool functionSourceUsePragmas
131 {
132 get => m_FunctionSourceUsePragmas;
133 set => m_FunctionSourceUsePragmas = value;
134 }
135
136 [SerializeField]
137 string m_FunctionBody = k_DefaultFunctionBody;
138
139 const string k_DefaultFunctionBody = "Enter function body here...";
140
141 public string functionBody
142 {
143 get => m_FunctionBody;
144 set => m_FunctionBody = value;
145 }
146
147 public static string defaultFunctionBody => k_DefaultFunctionBody;
148
149 public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode)
150 {
151 using (var inputSlots = PooledList<MaterialSlot>.Get())
152 using (var outputSlots = PooledList<MaterialSlot>.Get())
153 {
154 GetInputSlots<MaterialSlot>(inputSlots);
155 GetOutputSlots<MaterialSlot>(outputSlots);
156
157 if (!IsValidFunction())
158 {
159 // invalid functions generate special preview code.. (why?)
160 if (generationMode == GenerationMode.Preview && outputSlots.Count != 0)
161 {
162 outputSlots.OrderBy(s => s.id);
163 var hlslVariableType = outputSlots[0].concreteValueType.ToShaderString();
164 sb.AppendLine("{0} {1};",
165 hlslVariableType,
166 GetVariableNameForSlot(outputSlots[0].id));
167 }
168 return;
169 }
170
171 // declare output variables
172 foreach (var output in outputSlots)
173 {
174 sb.AppendLine("{0} {1};",
175 output.concreteValueType.ToShaderString(),
176 GetVariableNameForSlot(output.id));
177
178 if (output.bareResource)
179 AssignDefaultBareResource(output, sb);
180 }
181
182 // call function
183 sb.TryAppendIndentation();
184 sb.Append(hlslFunctionName);
185 sb.Append("(");
186 bool first = true;
187
188 foreach (var input in inputSlots)
189 {
190 if (!first)
191 sb.Append(", ");
192 first = false;
193
194 sb.Append(SlotInputValue(input, generationMode));
195
196 // fixup input for Bare types
197 if (input.bareResource)
198 {
199 if (input is SamplerStateMaterialSlot)
200 sb.Append(".samplerstate");
201 else
202 sb.Append(".tex");
203 }
204 }
205
206 foreach (var output in outputSlots)
207 {
208 if (!first)
209 sb.Append(", ");
210 first = false;
211 sb.Append(GetVariableNameForSlot(output.id));
212
213 // fixup output for Bare types
214 if (output.bareResource)
215 {
216 if (output is SamplerStateMaterialSlot)
217 sb.Append(".samplerstate");
218 else
219 sb.Append(".tex");
220 }
221 }
222 sb.Append(");");
223 sb.AppendNewLine();
224 }
225 }
226
227 void AssignDefaultBareResource(MaterialSlot slot, ShaderStringBuilder sb)
228 {
229 switch (slot.concreteValueType)
230 {
231 case ConcreteSlotValueType.Texture2D:
232 {
233 var slotVariable = GetVariableNameForSlot(slot.id);
234 sb.TryAppendIndentation();
235 sb.Append(slotVariable);
236 sb.Append(".samplerstate = default_sampler_Linear_Repeat;");
237 sb.AppendNewLine();
238 sb.TryAppendIndentation();
239 sb.Append(slotVariable);
240 sb.Append(".texelSize = float4(1.0f/128.0f, 1.0f/128.0f, 128.0f, 128.0f);");
241 sb.AppendNewLine();
242 sb.TryAppendIndentation();
243 sb.Append(slotVariable);
244 sb.Append(".scaleTranslate = float4(1.0f, 1.0f, 0.0f, 0.0f);");
245 sb.AppendNewLine();
246 }
247 break;
248 case ConcreteSlotValueType.Texture3D:
249 case ConcreteSlotValueType.Texture2DArray:
250 case ConcreteSlotValueType.Cubemap:
251 {
252 var slotVariable = GetVariableNameForSlot(slot.id);
253 sb.TryAppendIndentation();
254 sb.Append(slotVariable);
255 sb.Append(".samplerstate = default_sampler_Linear_Repeat;");
256 sb.AppendNewLine();
257 }
258 break;
259 }
260 }
261
262 public void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode)
263 {
264 if (!IsValidFunction())
265 return;
266
267 switch (sourceType)
268 {
269 case HlslSourceType.File:
270 string path = AssetDatabase.GUIDToAssetPath(functionSource);
271
272 // This is required for upgrading without console errors
273 if (string.IsNullOrEmpty(path))
274 path = functionSource;
275
276 registry.RequiresIncludePath(path, shouldIncludeWithPragmas: functionSourceUsePragmas);
277 break;
278 case HlslSourceType.String:
279 registry.ProvideFunction(hlslFunctionName, builder =>
280 {
281 // add a hint for the analytic derivative code to ignore user functions
282 builder.AddLine("// unity-custom-func-begin");
283 GetFunctionHeader(builder);
284 using (builder.BlockScope())
285 {
286 builder.AppendLines(functionBody);
287 }
288 builder.AddLine("// unity-custom-func-end");
289 });
290 break;
291 default:
292 throw new ArgumentOutOfRangeException();
293 }
294 }
295
296 void GetFunctionHeader(ShaderStringBuilder sb)
297 {
298 using (var inputSlots = PooledList<MaterialSlot>.Get())
299 using (var outputSlots = PooledList<MaterialSlot>.Get())
300 {
301 GetInputSlots(inputSlots);
302 GetOutputSlots(outputSlots);
303
304 sb.Append("void ");
305 sb.Append(hlslFunctionName);
306 sb.Append("(");
307
308 var first = true;
309
310 foreach (var argument in inputSlots)
311 {
312 if (!first)
313 sb.Append(", ");
314 first = false;
315 argument.AppendHLSLParameterDeclaration(sb, argument.shaderOutputName);
316 }
317
318 foreach (var argument in outputSlots)
319 {
320 if (!first)
321 sb.Append(", ");
322 first = false;
323 sb.Append("out ");
324 argument.AppendHLSLParameterDeclaration(sb, argument.shaderOutputName);
325 }
326
327 sb.Append(")");
328 }
329 }
330
331 string SlotInputValue(MaterialSlot port, GenerationMode generationMode)
332 {
333 IEdge[] edges = port.owner.owner.GetEdges(port.slotReference).ToArray();
334 if (edges.Any())
335 {
336 var fromSocketRef = edges[0].outputSlot;
337 var fromNode = fromSocketRef.node;
338 if (fromNode == null)
339 return string.Empty;
340
341 return fromNode.GetOutputForSlot(fromSocketRef, port.concreteValueType, generationMode);
342 }
343
344 return port.GetDefaultValue(generationMode);
345 }
346
347 bool IsValidFunction()
348 {
349 return IsValidFunction(sourceType, functionName, functionSource, functionBody);
350 }
351
352 static bool IsValidFunction(HlslSourceType sourceType, string functionName, string functionSource, string functionBody)
353 {
354 bool validFunctionName = !string.IsNullOrEmpty(functionName) && functionName != k_DefaultFunctionName;
355
356 if (sourceType == HlslSourceType.String)
357 {
358 bool validFunctionBody = !string.IsNullOrEmpty(functionBody) && functionBody != k_DefaultFunctionBody;
359 return validFunctionName & validFunctionBody;
360 }
361 else
362 {
363 if (!validFunctionName || string.IsNullOrEmpty(functionSource) || functionSource == k_DefaultFunctionSource)
364 return false;
365
366 string path = AssetDatabase.GUIDToAssetPath(functionSource);
367 if (string.IsNullOrEmpty(path))
368 path = functionSource;
369
370 string extension = Path.GetExtension(path);
371 return s_ValidExtensions.Contains(extension);
372 }
373 }
374
375 void ValidateSlotName()
376 {
377 using (var slots = PooledList<MaterialSlot>.Get())
378 {
379 GetSlots(slots);
380 foreach (var slot in slots)
381 {
382 // check for bad slot names
383 var error = NodeUtils.ValidateSlotName(slot.RawDisplayName(), out string errorMessage);
384 if (error)
385 {
386 owner.AddValidationError(objectId, errorMessage);
387 break;
388 }
389 }
390 }
391 }
392
393 void ValidateBareTextureSlots()
394 {
395 using (var outputSlots = PooledList<MaterialSlot>.Get())
396 {
397 GetOutputSlots(outputSlots);
398 foreach (var slot in outputSlots)
399 {
400 if (slot.bareResource)
401 {
402 owner.AddValidationError(objectId, "This node uses Bare Texture or SamplerState outputs, which may produce unexpected results when fed to other nodes. Please convert the node to use the non-Bare struct-based outputs (see the structs defined in com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl)", ShaderCompilerMessageSeverity.Warning);
403 break;
404 }
405 }
406 }
407 }
408
409 public override void ValidateNode()
410 {
411 bool hasAnyOutputs = this.GetOutputSlots<MaterialSlot>().Any();
412 if (sourceType == HlslSourceType.File)
413 {
414 SourceFileStatus fileStatus = SourceFileStatus.Empty;
415 if (!string.IsNullOrEmpty(functionSource))
416 {
417 string path = AssetDatabase.GUIDToAssetPath(functionSource);
418 if (!string.IsNullOrEmpty(path) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
419 {
420 string extension = path.Substring(path.LastIndexOf('.'));
421 if (!s_ValidExtensions.Contains(extension))
422 {
423 fileStatus = SourceFileStatus.Invalid;
424 }
425 else
426 {
427 fileStatus = SourceFileStatus.Valid;
428 }
429 }
430 else
431 fileStatus = SourceFileStatus.DoesNotExist;
432 }
433
434 if (fileStatus == SourceFileStatus.DoesNotExist || (fileStatus == SourceFileStatus.Empty && hasAnyOutputs))
435 owner.AddValidationError(objectId, k_MissingFile, ShaderCompilerMessageSeverity.Error);
436 else if (fileStatus == SourceFileStatus.Invalid)
437 owner.AddValidationError(objectId, k_InvalidFileType, ShaderCompilerMessageSeverity.Error);
438 else if (fileStatus == SourceFileStatus.Valid)
439 owner.ClearErrorsForNode(this);
440 }
441 if (!hasAnyOutputs)
442 {
443 owner.AddValidationError(objectId, k_MissingOutputSlot, ShaderCompilerMessageSeverity.Warning);
444 }
445 ValidateSlotName();
446 ValidateBareTextureSlots();
447
448 base.ValidateNode();
449 }
450
451 public bool Reload(HashSet<string> changedFileDependencyGUIDs)
452 {
453 if (changedFileDependencyGUIDs.Contains(m_FunctionSource))
454 {
455 owner.ClearErrorsForNode(this);
456 ValidateNode();
457 Dirty(ModificationScope.Graph);
458 return true;
459 }
460 return false;
461 }
462
463 public static string UpgradeFunctionSource(string functionSource)
464 {
465 // Handle upgrade from legacy asset path version
466 // If functionSource is not empty or a guid then assume it is legacy version
467 // If asset can be loaded from path then get its guid
468 // Otherwise it was the default string so set to empty
469 Guid guid;
470 if (!string.IsNullOrEmpty(functionSource) && !Guid.TryParse(functionSource, out guid))
471 {
472 // not sure why we don't use AssetDatabase.AssetPathToGUID...
473 // I guess we are testing that it actually exists and can be loaded here before converting?
474 string guidString = string.Empty;
475 ShaderInclude shaderInclude = AssetDatabase.LoadAssetAtPath<ShaderInclude>(functionSource);
476 if (shaderInclude != null)
477 {
478 long localId;
479 AssetDatabase.TryGetGUIDAndLocalFileIdentifier(shaderInclude, out guidString, out localId);
480 }
481 functionSource = guidString;
482 }
483
484 return functionSource;
485 }
486
487 public override void OnAfterDeserialize()
488 {
489 base.OnAfterDeserialize();
490 functionSource = UpgradeFunctionSource(functionSource);
491 UpdateNodeName();
492 }
493
494 public override void OnAfterMultiDeserialize(string json)
495 {
496 if (sgVersion < 1)
497 {
498 // any Texture2D slots used prior to version 1 should be flagged as "bare" so we can
499 // generate backwards compatible code
500 var slots = new List<MaterialSlot>();
501 GetSlots(slots);
502 foreach (var slot in slots)
503 {
504 slot.bareResource = true;
505 }
506 ChangeVersion(1);
507 }
508 }
509
510 public NeededTransform[] RequiresTransform(ShaderStageCapability stageCapability = ShaderStageCapability.All)
511 {
512 return new[]
513 {
514 NeededTransform.ObjectToWorld,
515 NeededTransform.WorldToObject
516 };
517 }
518 }
519}