A game about forced loneliness, made by TACStudios
at master 20 kB view raw
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}