3
0

MaterialGraphCompiler.cpp 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <Atom/RHI.Reflect/SamplerState.h>
  9. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  10. #include <Atom/RPI.Edit/Common/JsonUtils.h>
  11. #include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
  12. #include <Atom/RPI.Edit/Material/MaterialUtils.h>
  13. #include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
  14. #include <AtomToolsFramework/Graph/DynamicNode/DynamicNode.h>
  15. #include <AtomToolsFramework/Graph/DynamicNode/DynamicNodeUtil.h>
  16. #include <AtomToolsFramework/Graph/GraphTemplateFileDataCacheRequestBus.h>
  17. #include <AtomToolsFramework/Graph/GraphUtil.h>
  18. #include <AtomToolsFramework/Util/MaterialPropertyUtil.h>
  19. #include <AtomToolsFramework/Util/Util.h>
  20. #include <AzCore/Jobs/Algorithms.h>
  21. #include <AzCore/Math/Color.h>
  22. #include <AzCore/Math/Vector2.h>
  23. #include <AzCore/Math/Vector3.h>
  24. #include <AzCore/Math/Vector4.h>
  25. #include <AzCore/RTTI/BehaviorContext.h>
  26. #include <AzCore/RTTI/RTTI.h>
  27. #include <AzCore/Serialization/EditContext.h>
  28. #include <AzCore/Serialization/ObjectStream.h>
  29. #include <AzCore/Serialization/SerializeContext.h>
  30. #include <AzCore/Serialization/Utils.h>
  31. #include <AzCore/Utils/Utils.h>
  32. #include <AzCore/std/containers/vector.h>
  33. #include <AzCore/std/sort.h>
  34. #include <AzCore/std/string/regex.h>
  35. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  36. #include <Document/MaterialGraphCompiler.h>
  37. #include <GraphModel/Model/Connection.h>
  38. namespace MaterialCanvas
  39. {
  40. void MaterialGraphCompiler::Reflect(AZ::ReflectContext* context)
  41. {
  42. if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
  43. {
  44. serialize->Class<MaterialGraphCompiler, AtomToolsFramework::GraphCompiler>()
  45. ->Version(0)
  46. ;
  47. }
  48. }
  49. MaterialGraphCompiler::MaterialGraphCompiler(const AZ::Crc32& toolId)
  50. : AtomToolsFramework::GraphCompiler(toolId)
  51. {
  52. }
  53. MaterialGraphCompiler::~MaterialGraphCompiler()
  54. {
  55. }
  56. AZStd::string MaterialGraphCompiler::GetGraphPath() const
  57. {
  58. if (const auto& graphPath = AtomToolsFramework::GraphCompiler::GetGraphPath(); graphPath.ends_with(".materialgraph"))
  59. {
  60. return graphPath;
  61. }
  62. return AZStd::string::format("%s/Assets/Materials/Generated/untitled.materialgraph", AZ::Utils::GetProjectPath().c_str());
  63. }
  64. bool MaterialGraphCompiler::CompileGraph(GraphModel::GraphPtr graph, const AZStd::string& graphName, const AZStd::string& graphPath)
  65. {
  66. if (!AtomToolsFramework::GraphCompiler::CompileGraph(graph, graphName, graphPath))
  67. {
  68. return false;
  69. }
  70. m_includePaths.clear();
  71. m_classDefinitions.clear();
  72. m_functionDefinitions.clear();
  73. m_configIdsVisited.clear();
  74. m_slotValueTable.clear();
  75. m_templateNodeCount = 0;
  76. m_templatePathsForCurrentNode.clear();
  77. m_templateFileDataVecForCurrentNode.clear();
  78. m_instructionNodesForCurrentNode.clear();
  79. BuildSlotValueTable();
  80. BuildDependencyTables();
  81. // Traverse all graph nodes and slots searching for settings to generate files from templates
  82. for (const auto& currentNode : GetAllNodesInExecutionOrder())
  83. {
  84. // Search this node for any template path settings that describe files that need to be generated from the graph.
  85. BuildTemplatePathsForCurrentNode(currentNode);
  86. // If no template files were specified for this node then skip additional processing and continue to the next one.
  87. if (m_templatePathsForCurrentNode.empty())
  88. {
  89. continue;
  90. }
  91. // Attempt to load all of the template files referenced by this node. All of the template data will be tokenized into individual
  92. // lines and stored in a container so then multiple passes can be made on each file, substituting tokens and filling in
  93. // details provided by the graph. None of the files generated from this node will be saved until they have all been processed.
  94. // Template files for material types will be processed in their own pass Because they require special handling and need to be
  95. // saved before material file templates to not trigger asset processor dependency errors.
  96. if (!LoadTemplatesForCurrentNode())
  97. {
  98. SetState(State::Failed);
  99. return false;
  100. }
  101. // Force delete prior versions of files to be generated if settings are configured to do so.
  102. DeleteExistingFilesForCurrentNode();
  103. // Reset the asset processor fingerprint for material and material type files to force them to recompile even if nothing
  104. // changed. Source material files, generated by material canvas graph templates, never change after they're first generated.
  105. // Material type and shader source files change constantly based on the configuration of the graph. The AP is not
  106. // reprocessing or triggering asset notifications for unmodified material assets even though the dependencies are changing.
  107. // AssetSystemRequestBus::Events::ClearFingerprintForAsset is rarely used but specifically documented to resolve this problem.
  108. // The consequence is that reflecting some changes in the preview may take more time because all generated assets will be
  109. // reprocessed including material types where only a material input might have changed.
  110. ClearFingerprintsForCurrentNode();
  111. // Perform an initial pass over all template files, injecting include files, class definitions, function definitions, simple
  112. // things that don't require much processing.
  113. PreprocessTemplatesForCurrentNode();
  114. // The next phase injects shader code instructions assembled by traversing the graph from each of the input slots on the current
  115. // node. The O3DE_GENERATED_INSTRUCTIONS_BEGIN marker will be followed by a list of input slot names corresponding to required
  116. // variables in the shader. Instructions will only be generated for the current node and nodes connected to the specified
  117. // inputs. This will allow multiple O3DE_GENERATED_INSTRUCTIONS blocks with different inputs to be specified in multiple
  118. // locations across multiple files from a single graph.
  119. // This will also keep track of nodes with instructions and data that contribute to the final shader code. The list of
  120. // contributing nodes will be used to exclude unused material inputs from generated SRGs and material types.
  121. BuildInstructionsForCurrentNode(currentNode);
  122. // At this point, all of the instructions have been generated for all of the template files used by this node. We now also have
  123. // a complete list of all nodes that contributed instructions to the final shader code across all of the files. Now, we can
  124. // safely generate the material SRG and material type that only contain variables referenced in the shaders. Without tracking
  125. // this, all variables would be included in the SRG and material type. The shader compiler would eliminate unused variables from
  126. // the compiled shader code. The material type would fail to build if it referenced any of the eliminated variables.
  127. BuildMaterialSrgForCurrentNode();
  128. // Save all of the generated files except for materials and material types. Generated material type files must be saved after
  129. // generated shader files to prevent AP errors because of missing dependencies.
  130. if (!ExportTemplatesMatchingRegex(".*\\.lua\\b") ||
  131. !ExportTemplatesMatchingRegex(".*\\.azsli\\b") ||
  132. !ExportTemplatesMatchingRegex(".*\\.azsl\\b") ||
  133. !ExportTemplatesMatchingRegex(".*\\.shader\\b"))
  134. {
  135. SetState(State::Failed);
  136. return false;
  137. }
  138. // Process material type template files, injecting properties from material input nodes.
  139. if (!BuildMaterialTypeForCurrentNode(currentNode))
  140. {
  141. SetState(State::Failed);
  142. return false;
  143. }
  144. // After the material types have been processed and saved, save the materials that reference them.
  145. if (!ExportTemplatesMatchingRegex(".*\\.material\\b"))
  146. {
  147. SetState(State::Failed);
  148. return false;
  149. }
  150. // Increment the template node counter in case we encounter another template node and need to uniquely identify it.
  151. ++m_templateNodeCount;
  152. }
  153. if (!ReportGeneratedFileStatus())
  154. {
  155. SetState(State::Failed);
  156. return false;
  157. }
  158. SetState(State::Complete);
  159. return true;
  160. }
  161. void MaterialGraphCompiler::BuildSlotValueTable()
  162. {
  163. // Build a table of all values for every slot in the graph.
  164. m_slotValueTable.clear();
  165. for (const auto& currentNode : GetAllNodesInExecutionOrder())
  166. {
  167. for (const auto& currentSlotPair : currentNode->GetSlots())
  168. {
  169. const auto& currentSlot = currentSlotPair.second;
  170. m_slotValueTable[currentSlot] = currentSlot->GetValue();
  171. }
  172. // If this is a dynamic node with slot data type groups, we will search for the largest vector or other data type and convert
  173. // all of the values in the group to the same type.
  174. if (auto dynamicNode = azrtti_cast<const AtomToolsFramework::DynamicNode*>(currentNode.get()))
  175. {
  176. const auto& nodeConfig = dynamicNode->GetConfig();
  177. for (const auto& slotDataTypeGroup : nodeConfig.m_slotDataTypeGroups)
  178. {
  179. // The slot data group string is separated by vertical bars and can be treated like a regular expression to compare
  180. // against slot names. The largest vector size is recorded for each slot group.
  181. const AZStd::regex slotDataTypeGroupRegex(slotDataTypeGroup, AZStd::regex::flag_type::icase);
  182. // Some nodes might specify a minimum vector size needed to up convert slot values for azsl snippet or output slot
  183. // requirements. Search all slots in the data type group for the minimum vector size setting to update the default
  184. // minimum vector size.
  185. unsigned int vectorSize = 0;
  186. AtomToolsFramework::VisitDynamicNodeSlotConfigs(
  187. nodeConfig,
  188. [&](const AtomToolsFramework::DynamicNodeSlotConfig& slotConfig)
  189. {
  190. if (AZStd::regex_match(slotConfig.m_name, slotDataTypeGroupRegex))
  191. {
  192. const AZStd::string vectorSizeStr =
  193. AtomToolsFramework::GetSettingValueByName(slotConfig.m_settings, "materialPropertyMinVectorSize");
  194. if (!vectorSizeStr.empty())
  195. {
  196. vectorSize = AZStd::max(vectorSize, static_cast<unsigned int>(AZStd::stoi(vectorSizeStr)));
  197. }
  198. }
  199. });
  200. for (const auto& currentSlotPair : currentNode->GetSlots())
  201. {
  202. const auto& currentSlot = currentSlotPair.second;
  203. if (currentSlot->GetSlotDirection() == GraphModel::SlotDirection::Input &&
  204. AZStd::regex_match(currentSlot->GetName(), slotDataTypeGroupRegex))
  205. {
  206. const auto& currentSlotValue = GetValueFromSlotOrConnection(currentSlot);
  207. vectorSize = AZStd::max(vectorSize, GetVectorSize(currentSlotValue));
  208. }
  209. }
  210. // Once all of the container sizes have been recorded for each slot data group, iterate over all of these slot values
  211. // and upgrade entries in the map to the bigger type.
  212. for (const auto& currentSlotPair : currentNode->GetSlots())
  213. {
  214. const auto& currentSlot = currentSlotPair.second;
  215. if (AZStd::regex_match(currentSlot->GetName(), slotDataTypeGroupRegex))
  216. {
  217. const auto& currentSlotValue = GetValueFromSlot(currentSlot);
  218. m_slotValueTable[currentSlot] = ConvertToVector(currentSlotValue, vectorSize);
  219. }
  220. }
  221. }
  222. }
  223. }
  224. }
  225. void MaterialGraphCompiler::BuildDependencyTables()
  226. {
  227. if (!m_graph)
  228. {
  229. AZ_Error("MaterialGraphCompiler", false, "Attempting to generate data from invalid graph object.");
  230. return;
  231. }
  232. for (const auto& nodePair : m_graph->GetNodes())
  233. {
  234. const auto& currentNode = nodePair.second;
  235. if (auto dynamicNode = azrtti_cast<const AtomToolsFramework::DynamicNode*>(currentNode.get()))
  236. {
  237. if (!m_configIdsVisited.contains(dynamicNode->GetConfig().m_id))
  238. {
  239. m_configIdsVisited.insert(dynamicNode->GetConfig().m_id);
  240. AtomToolsFramework::VisitDynamicNodeSettings(
  241. dynamicNode->GetConfig(),
  242. [&](const AtomToolsFramework::DynamicNodeSettingsMap& settings)
  243. {
  244. AtomToolsFramework::CollectDynamicNodeSettings(settings, "includePaths", m_includePaths);
  245. AtomToolsFramework::CollectDynamicNodeSettings(settings, "classDefinitions", m_classDefinitions);
  246. AtomToolsFramework::CollectDynamicNodeSettings(settings, "functionDefinitions", m_functionDefinitions);
  247. });
  248. }
  249. }
  250. }
  251. }
  252. void MaterialGraphCompiler::BuildTemplatePathsForCurrentNode(const GraphModel::ConstNodePtr& currentNode)
  253. {
  254. m_templatePathsForCurrentNode.clear();
  255. if (auto dynamicNode = azrtti_cast<const AtomToolsFramework::DynamicNode*>(currentNode.get()))
  256. {
  257. AtomToolsFramework::VisitDynamicNodeSettings(
  258. dynamicNode->GetConfig(),
  259. [&](const AtomToolsFramework::DynamicNodeSettingsMap& settings)
  260. {
  261. AtomToolsFramework::CollectDynamicNodeSettings(settings, "templatePaths", m_templatePathsForCurrentNode);
  262. });
  263. }
  264. }
  265. bool MaterialGraphCompiler::LoadTemplatesForCurrentNode()
  266. {
  267. m_templateFileDataVecForCurrentNode.clear();
  268. for (const auto& templatePath : m_templatePathsForCurrentNode)
  269. {
  270. if (!templatePath.ends_with(".materialtype"))
  271. {
  272. // Load the unmodified, template source file data, which will be copied and used for insertions, substitutions, and
  273. // code generation.
  274. AtomToolsFramework::GraphTemplateFileData templateFileData;
  275. AtomToolsFramework::GraphTemplateFileDataCacheRequestBus::EventResult(
  276. templateFileData,
  277. m_toolId,
  278. &AtomToolsFramework::GraphTemplateFileDataCacheRequestBus::Events::Load,
  279. AtomToolsFramework::GetPathWithoutAlias(templatePath));
  280. if (!templateFileData.IsLoaded())
  281. {
  282. m_templateFileDataVecForCurrentNode.clear();
  283. return false;
  284. }
  285. m_templateFileDataVecForCurrentNode.emplace_back(AZStd::move(templateFileData));
  286. }
  287. }
  288. return true;
  289. }
  290. void MaterialGraphCompiler::DeleteExistingFilesForCurrentNode()
  291. {
  292. if (AtomToolsFramework::GetSettingsValue("/O3DE/Atom/MaterialCanvas/ForceDeleteGeneratedFiles", false))
  293. {
  294. AZ::parallel_for_each(
  295. m_templateFileDataVecForCurrentNode.begin(),
  296. m_templateFileDataVecForCurrentNode.end(),
  297. [this](const auto& templateFileData)
  298. {
  299. const auto& templateInputPath = AtomToolsFramework::GetPathWithoutAlias(templateFileData.GetPath());
  300. const auto& templateOutputPath = GetOutputPathFromTemplatePath(templateInputPath);
  301. auto fileIO = AZ::IO::FileIOBase::GetInstance();
  302. fileIO->Remove(templateOutputPath.c_str());
  303. });
  304. }
  305. }
  306. void MaterialGraphCompiler::ClearFingerprintsForCurrentNode()
  307. {
  308. if (AtomToolsFramework::GetSettingsValue("/O3DE/Atom/MaterialCanvas/ForceClearAssetFingerprints", false))
  309. {
  310. for (const auto& templatePath : m_templatePathsForCurrentNode)
  311. {
  312. if (templatePath.ends_with(".material") || templatePath.ends_with(".materialtype"))
  313. {
  314. const auto& templateInputPath = AtomToolsFramework::GetPathWithoutAlias(templatePath);
  315. const auto& templateOutputPath = GetOutputPathFromTemplatePath(templateInputPath);
  316. AzToolsFramework::AssetSystemRequestBus::Broadcast(
  317. &AzToolsFramework::AssetSystemRequestBus::Events::ClearFingerprintForAsset, templateOutputPath);
  318. }
  319. }
  320. }
  321. }
  322. void MaterialGraphCompiler::PreprocessTemplatesForCurrentNode()
  323. {
  324. AZ::parallel_for_each(
  325. m_templateFileDataVecForCurrentNode.begin(),
  326. m_templateFileDataVecForCurrentNode.end(),
  327. [&](auto& templateFileData)
  328. {
  329. // Substitute all references to the placeholder graph name with one generated from the document name
  330. templateFileData.ReplaceSymbol("MaterialGraphName", GetUniqueGraphName());
  331. // Inject include files found while traversing the graph into any include file blocks in the template.
  332. templateFileData.ReplaceLinesInBlock(
  333. "O3DE_GENERATED_INCLUDES_BEGIN",
  334. "O3DE_GENERATED_INCLUDES_END",
  335. [&, this]([[maybe_unused]] const AZStd::string& blockHeader)
  336. {
  337. // Include file paths will need to be converted to include statements.
  338. AZStd::vector<AZStd::string> includeStatements;
  339. includeStatements.reserve(m_includePaths.size());
  340. for (const auto& path : m_includePaths)
  341. {
  342. bool relativePathFound = false;
  343. AZStd::string relativePath;
  344. AZStd::string relativePathFolder;
  345. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  346. relativePathFound,
  347. &AzToolsFramework::AssetSystem::AssetSystemRequest::GenerateRelativeSourcePath,
  348. AtomToolsFramework::GetPathWithoutAlias(path),
  349. relativePath,
  350. relativePathFolder);
  351. if (relativePathFound)
  352. {
  353. includeStatements.push_back(AZStd::string::format("#include <%s>", relativePath.c_str()));
  354. }
  355. }
  356. return includeStatements;
  357. });
  358. // Inject class definitions found while traversing the graph.
  359. templateFileData.ReplaceLinesInBlock(
  360. "O3DE_GENERATED_CLASSES_BEGIN",
  361. "O3DE_GENERATED_CLASSES_END",
  362. [&]([[maybe_unused]] const AZStd::string& blockHeader)
  363. {
  364. return m_classDefinitions;
  365. });
  366. // Inject function definitions found while traversing the graph.
  367. templateFileData.ReplaceLinesInBlock(
  368. "O3DE_GENERATED_FUNCTIONS_BEGIN",
  369. "O3DE_GENERATED_FUNCTIONS_END",
  370. [&]([[maybe_unused]] const AZStd::string& blockHeader)
  371. {
  372. return m_functionDefinitions;
  373. });
  374. });
  375. }
  376. void MaterialGraphCompiler::BuildInstructionsForCurrentNode(const GraphModel::ConstNodePtr& currentNode)
  377. {
  378. if (!m_graph)
  379. {
  380. AZ_Error("MaterialGraphCompiler", false, "Attempting to generate data from invalid graph object.");
  381. return;
  382. }
  383. m_instructionNodesForCurrentNode.clear();
  384. m_instructionNodesForCurrentNode.reserve(m_graph->GetNodeCount());
  385. AZ::parallel_for_each(
  386. m_templateFileDataVecForCurrentNode.begin(),
  387. m_templateFileDataVecForCurrentNode.end(),
  388. [&](auto& templateFileData)
  389. {
  390. templateFileData.ReplaceLinesInBlock(
  391. "O3DE_GENERATED_INSTRUCTIONS_BEGIN",
  392. "O3DE_GENERATED_INSTRUCTIONS_END",
  393. [&]([[maybe_unused]] const AZStd::string& blockHeader)
  394. {
  395. AZStd::vector<AZStd::string> inputSlotNames;
  396. AZ::StringFunc::Tokenize(blockHeader, inputSlotNames, ";:, \t\r\n\\/", false, false);
  397. AZStd::vector<GraphModel::ConstNodePtr> instructionNodesForBlock;
  398. instructionNodesForBlock.reserve(m_graph->GetNodeCount());
  399. const auto& lines = GetInstructionsFromConnectedNodes(currentNode, inputSlotNames, instructionNodesForBlock);
  400. // Adding all of the contributing notes from this blog to the set of all nodes for all blocks.
  401. AZStd::scoped_lock lock(m_instructionNodesForCurrentNodeMutex);
  402. m_instructionNodesForCurrentNode.insert(
  403. m_instructionNodesForCurrentNode.end(), instructionNodesForBlock.begin(), instructionNodesForBlock.end());
  404. return lines;
  405. });
  406. });
  407. // All of the instruction nodes are gathered in temporary vectors and the results concatenated. The vector needs to be reduced
  408. // to only contain unique nodes and then resorted by depth.
  409. AZStd::sort(m_instructionNodesForCurrentNode.begin(), m_instructionNodesForCurrentNode.end());
  410. m_instructionNodesForCurrentNode.erase(
  411. AZStd::unique(m_instructionNodesForCurrentNode.begin(), m_instructionNodesForCurrentNode.end()),
  412. m_instructionNodesForCurrentNode.end());
  413. AtomToolsFramework::SortNodesInExecutionOrder(m_instructionNodesForCurrentNode);
  414. }
  415. void MaterialGraphCompiler::BuildMaterialSrgForCurrentNode()
  416. {
  417. AZ::parallel_for_each(
  418. m_templateFileDataVecForCurrentNode.begin(),
  419. m_templateFileDataVecForCurrentNode.end(),
  420. [&](auto& templateFileData)
  421. {
  422. templateFileData.ReplaceLinesInBlock(
  423. "O3DE_GENERATED_MATERIAL_SRG_BEGIN",
  424. "O3DE_GENERATED_MATERIAL_SRG_END",
  425. [&]([[maybe_unused]] const AZStd::string& blockHeader)
  426. {
  427. return GetMaterialPropertySrgMemberFromNodes(m_instructionNodesForCurrentNode);
  428. });
  429. });
  430. }
  431. bool MaterialGraphCompiler::BuildMaterialTypeForCurrentNode(const GraphModel::ConstNodePtr& currentNode)
  432. {
  433. for (const auto& templatePath : m_templatePathsForCurrentNode)
  434. {
  435. if (!templatePath.ends_with(".materialtype"))
  436. {
  437. continue;
  438. }
  439. // Remove any aliases to resolve the absolute path to the template file
  440. const auto& templateInputPath = AtomToolsFramework::GetPathWithoutAlias(templatePath);
  441. const auto& templateOutputPath = GetOutputPathFromTemplatePath(templateInputPath);
  442. if (!BuildMaterialTypeFromTemplate(currentNode, m_instructionNodesForCurrentNode, templateInputPath, templateOutputPath))
  443. {
  444. return false;
  445. }
  446. AzFramework::AssetSystemRequestBus::Broadcast(
  447. &AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, templateOutputPath);
  448. m_generatedFiles.push_back(templateOutputPath);
  449. }
  450. return true;
  451. }
  452. bool MaterialGraphCompiler::ExportTemplatesMatchingRegex(const AZStd::string& pattern)
  453. {
  454. const AZStd::regex patternRegex(pattern, AZStd::regex::flag_type::icase);
  455. for (const auto& templateFileData : m_templateFileDataVecForCurrentNode)
  456. {
  457. if (AZStd::regex_match(templateFileData.GetPath(), patternRegex))
  458. {
  459. const auto& templateOutputPath = GetOutputPathFromTemplatePath(templateFileData.GetPath());
  460. if (!templateFileData.Save(templateOutputPath))
  461. {
  462. return false;
  463. }
  464. AzFramework::AssetSystemRequestBus::Broadcast(
  465. &AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, templateOutputPath);
  466. m_generatedFiles.push_back(templateOutputPath);
  467. }
  468. }
  469. return true;
  470. }
  471. AZStd::string MaterialGraphCompiler::GetOutputPathFromTemplatePath(const AZStd::string& templateInputPath) const
  472. {
  473. AZStd::string templateInputFileName;
  474. AZ::StringFunc::Path::GetFullFileName(templateInputPath.c_str(), templateInputFileName);
  475. AZStd::string templateOutputPath = GetGraphPath();
  476. AZ::StringFunc::Path::ReplaceFullName(templateOutputPath, templateInputFileName.c_str());
  477. AZ::StringFunc::Replace(templateOutputPath, "MaterialGraphName", GetUniqueGraphName().c_str());
  478. return templateOutputPath;
  479. }
  480. unsigned int MaterialGraphCompiler::GetVectorSize(const AZStd::any& slotValue) const
  481. {
  482. if (slotValue.is<AZ::Color>())
  483. {
  484. return 4;
  485. }
  486. if (slotValue.is<AZ::Vector4>())
  487. {
  488. return 4;
  489. }
  490. if (slotValue.is<AZ::Vector3>())
  491. {
  492. return 3;
  493. }
  494. if (slotValue.is<AZ::Vector2>())
  495. {
  496. return 2;
  497. }
  498. if (slotValue.is<bool>() || slotValue.is<int>() || slotValue.is<unsigned int>() || slotValue.is<float>())
  499. {
  500. return 1;
  501. }
  502. return 0;
  503. }
  504. AZStd::any MaterialGraphCompiler::ConvertToScalar(const AZStd::any& slotValue) const
  505. {
  506. if (auto v = AZStd::any_cast<const AZ::Color>(&slotValue))
  507. {
  508. return AZStd::any(v->GetR());
  509. }
  510. if (auto v = AZStd::any_cast<const AZ::Vector4>(&slotValue))
  511. {
  512. return AZStd::any(v->GetX());
  513. }
  514. if (auto v = AZStd::any_cast<const AZ::Vector3>(&slotValue))
  515. {
  516. return AZStd::any(v->GetX());
  517. }
  518. if (auto v = AZStd::any_cast<const AZ::Vector2>(&slotValue))
  519. {
  520. return AZStd::any(v->GetX());
  521. }
  522. return slotValue;
  523. }
  524. template<typename T>
  525. AZStd::any MaterialGraphCompiler::ConvertToVector(const AZStd::any& slotValue) const
  526. {
  527. if (auto v = AZStd::any_cast<const AZ::Color>(&slotValue))
  528. {
  529. return AZStd::any(T(v->GetAsVector4()));
  530. }
  531. if (auto v = AZStd::any_cast<const AZ::Vector4>(&slotValue))
  532. {
  533. return AZStd::any(T(*v));
  534. }
  535. if (auto v = AZStd::any_cast<const AZ::Vector3>(&slotValue))
  536. {
  537. return AZStd::any(T(*v));
  538. }
  539. if (auto v = AZStd::any_cast<const AZ::Vector2>(&slotValue))
  540. {
  541. return AZStd::any(T(*v));
  542. }
  543. return slotValue;
  544. }
  545. AZStd::any MaterialGraphCompiler::ConvertToVector(const AZStd::any& slotValue, unsigned int score) const
  546. {
  547. switch (score)
  548. {
  549. case 4:
  550. // Skipping color to vector conversions so that they export as the correct type with the material type.
  551. return slotValue.is<AZ::Color>() ? slotValue : ConvertToVector<AZ::Vector4>(slotValue);
  552. case 3:
  553. // Skipping color to vector conversions so that they export as the correct type with the material type.
  554. return slotValue.is<AZ::Color>() ? slotValue : ConvertToVector<AZ::Vector3>(slotValue);
  555. case 2:
  556. return ConvertToVector<AZ::Vector2>(slotValue);
  557. case 1:
  558. return ConvertToScalar(slotValue);
  559. default:
  560. return slotValue;
  561. }
  562. }
  563. AZStd::any MaterialGraphCompiler::GetValueFromSlot(GraphModel::ConstSlotPtr slot) const
  564. {
  565. const auto& slotItr = m_slotValueTable.find(slot);
  566. return slotItr != m_slotValueTable.end() ? slotItr->second : slot->GetValue();
  567. }
  568. AZStd::any MaterialGraphCompiler::GetValueFromSlotOrConnection(GraphModel::ConstSlotPtr slot) const
  569. {
  570. for (const auto& connection : slot->GetConnections())
  571. {
  572. auto sourceSlot = connection->GetSourceSlot();
  573. auto targetSlot = connection->GetTargetSlot();
  574. if (targetSlot == slot)
  575. {
  576. return GetValueFromSlotOrConnection(sourceSlot);
  577. }
  578. }
  579. return GetValueFromSlot(slot);
  580. }
  581. AZStd::string MaterialGraphCompiler::GetAzslTypeFromSlot(GraphModel::ConstSlotPtr slot) const
  582. {
  583. const auto& slotValue = GetValueFromSlot(slot);
  584. const auto& slotDataType = slot->GetGraphContext()->GetDataTypeForValue(slotValue);
  585. const auto& slotDataTypeName = slotDataType ? slotDataType->GetDisplayName() : AZStd::string{};
  586. if (AZ::StringFunc::Equal(slotDataTypeName, "color"))
  587. {
  588. return "float4";
  589. }
  590. return slotDataTypeName;
  591. }
  592. AZStd::string MaterialGraphCompiler::GetAzslValueFromSlot(GraphModel::ConstSlotPtr slot) const
  593. {
  594. const auto& slotValue = GetValueFromSlot(slot);
  595. // This code and some of these rules will be refactored and generalized after splitting this class into a document and builder or
  596. // compiler class. Once that is done, it will be easier to register types, conversions, substitutions with the system.
  597. for (const auto& connection : slot->GetConnections())
  598. {
  599. auto sourceSlot = connection->GetSourceSlot();
  600. auto targetSlot = connection->GetTargetSlot();
  601. if (targetSlot == slot)
  602. {
  603. // If there is an incoming connection to this slot, the name of the source slot from the incoming connection will be used as
  604. // part of the value for the slot. It must be cast to the correct vector type for generated code. These conversions will be
  605. // extended once the code generator is separated from the document class.
  606. const auto& sourceSlotValue = GetValueFromSlot(sourceSlot);
  607. const auto& sourceSlotSymbolName = GetSymbolNameFromSlot(sourceSlot);
  608. if (slotValue.is<AZ::Vector2>())
  609. {
  610. if (sourceSlotValue.is<AZ::Vector3>() ||
  611. sourceSlotValue.is<AZ::Vector4>() ||
  612. sourceSlotValue.is<AZ::Color>())
  613. {
  614. return AZStd::string::format("(float2)%s", sourceSlotSymbolName.c_str());
  615. }
  616. }
  617. if (slotValue.is<AZ::Vector3>())
  618. {
  619. if (sourceSlotValue.is<AZ::Vector2>())
  620. {
  621. return AZStd::string::format("float3(%s, 0)", sourceSlotSymbolName.c_str());
  622. }
  623. if (sourceSlotValue.is<AZ::Vector4>() ||
  624. sourceSlotValue.is<AZ::Color>())
  625. {
  626. return AZStd::string::format("(float3)%s", sourceSlotSymbolName.c_str());
  627. }
  628. }
  629. if (slotValue.is<AZ::Vector4>() ||
  630. slotValue.is<AZ::Color>())
  631. {
  632. if (sourceSlotValue.is<AZ::Vector2>())
  633. {
  634. return AZStd::string::format("float4(%s, 0, 1)", sourceSlotSymbolName.c_str());
  635. }
  636. if (sourceSlotValue.is<AZ::Vector3>())
  637. {
  638. return AZStd::string::format("float4(%s, 1)", sourceSlotSymbolName.c_str());
  639. }
  640. }
  641. return sourceSlotSymbolName;
  642. }
  643. }
  644. // If the slot's embedded value is being used then generate shader code to represent it. More generic options will be explored to
  645. // clean this code up, possibly storing numeric values in a two-dimensional floating point array with the layout corresponding to
  646. // most vector and matrix types.
  647. if (auto v = AZStd::any_cast<const AZ::Color>(&slotValue))
  648. {
  649. return AZStd::string::format("{%g, %g, %g, %g}", v->GetR(), v->GetG(), v->GetB(), v->GetA());
  650. }
  651. if (auto v = AZStd::any_cast<const AZ::Vector4>(&slotValue))
  652. {
  653. return AZStd::string::format("{%g, %g, %g, %g}", v->GetX(), v->GetY(), v->GetZ(), v->GetW());
  654. }
  655. if (auto v = AZStd::any_cast<const AZ::Vector3>(&slotValue))
  656. {
  657. return AZStd::string::format("{%g, %g, %g}", v->GetX(), v->GetY(), v->GetZ());
  658. }
  659. if (auto v = AZStd::any_cast<const AZ::Vector2>(&slotValue))
  660. {
  661. return AZStd::string::format("{%g, %g}", v->GetX(), v->GetY());
  662. }
  663. if (auto v = AZStd::any_cast<const AZStd::array<AZ::Vector2, 2>>(&slotValue))
  664. {
  665. const auto& value = *v;
  666. return AZStd::string::format(
  667. "{%g, %g, %g, %g}",
  668. value[0].GetX(), value[0].GetY(),
  669. value[1].GetX(), value[1].GetY());
  670. }
  671. if (auto v = AZStd::any_cast<const AZStd::array<AZ::Vector3, 3>>(&slotValue))
  672. {
  673. const auto& value = *v;
  674. return AZStd::string::format(
  675. "{%g, %g, %g, %g, %g, %g, %g, %g, %g}",
  676. value[0].GetX(), value[0].GetY(), value[0].GetZ(),
  677. value[1].GetX(), value[1].GetY(), value[1].GetZ(),
  678. value[2].GetX(), value[2].GetY(), value[2].GetZ());
  679. }
  680. if (auto v = AZStd::any_cast<const AZStd::array<AZ::Vector4, 3>>(&slotValue))
  681. {
  682. const auto& value = *v;
  683. return AZStd::string::format(
  684. "{%g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g}",
  685. value[0].GetX(), value[0].GetY(), value[0].GetZ(), value[0].GetW(),
  686. value[1].GetX(), value[1].GetY(), value[1].GetZ(), value[1].GetW(),
  687. value[2].GetX(), value[2].GetY(), value[2].GetZ(), value[2].GetW());
  688. }
  689. if (auto v = AZStd::any_cast<const AZStd::array<AZ::Vector4, 4>>(&slotValue))
  690. {
  691. const auto& value = *v;
  692. return AZStd::string::format(
  693. "{%g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g}",
  694. value[0].GetX(), value[0].GetY(), value[0].GetZ(), value[0].GetW(),
  695. value[1].GetX(), value[1].GetY(), value[1].GetZ(), value[1].GetW(),
  696. value[2].GetX(), value[2].GetY(), value[2].GetZ(), value[2].GetW(),
  697. value[3].GetX(), value[3].GetY(), value[3].GetZ(), value[3].GetW());
  698. }
  699. if (auto v = AZStd::any_cast<const float>(&slotValue))
  700. {
  701. return AZStd::string::format("%g", *v);
  702. }
  703. if (auto v = AZStd::any_cast<const int>(&slotValue))
  704. {
  705. return AZStd::string::format("%i", *v);
  706. }
  707. if (auto v = AZStd::any_cast<const unsigned int>(&slotValue))
  708. {
  709. return AZStd::string::format("%u", *v);
  710. }
  711. if (auto v = AZStd::any_cast<const bool>(&slotValue))
  712. {
  713. return AZStd::string::format("%u", *v ? 1 : 0);
  714. }
  715. if (auto v = AZStd::any_cast<const AZStd::string>(&slotValue))
  716. {
  717. return *v;
  718. }
  719. return AZStd::string();
  720. }
  721. AZStd::string MaterialGraphCompiler::GetAzslSrgMemberFromSlot(
  722. GraphModel::ConstNodePtr node, const AtomToolsFramework::DynamicNodeSlotConfig& slotConfig) const
  723. {
  724. if (const auto& slot = node->GetSlot(slotConfig.m_name))
  725. {
  726. const auto& slotValue = GetValueFromSlot(slot);
  727. if (auto v = AZStd::any_cast<const AZ::RHI::SamplerState>(&slotValue))
  728. {
  729. // The fields commented out below either cause errors or are not recognized by the shader compiler.
  730. AZStd::string srgMember;
  731. srgMember += AZStd::string::format("Sampler SLOTNAME\n");
  732. srgMember += AZStd::string::format("{\n");
  733. srgMember += AZStd::string::format("MaxAnisotropy = %u;\n", AZStd::max<uint32_t>(v->m_anisotropyMax, 1));
  734. //srgMember += AZStd::string::format("AnisotropyEnable = %u;\n", AZStd::clamp<uint32_t>(v->m_anisotropyEnable, 0, 1);
  735. srgMember += AZStd::string::format("MinFilter = %s;\n", AZ::RHI::FilterModeNamespace::ToString(v->m_filterMin).data());
  736. srgMember += AZStd::string::format("MagFilter = %s;\n", AZ::RHI::FilterModeNamespace::ToString(v->m_filterMag).data());
  737. srgMember += AZStd::string::format("MipFilter = %s;\n", AZ::RHI::FilterModeNamespace::ToString(v->m_filterMip).data());
  738. srgMember += AZStd::string::format("ReductionType = %s;\n", AZ::RHI::ReductionTypeNamespace::ToString(v->m_reductionType).data());
  739. //srgMember += AZStd::string::format("ComparisonFunc = %s;\n", AZ::RHI::ComparisonFuncNamespace::ToString(v->m_comparisonFunc).data());
  740. srgMember += AZStd::string::format("AddressU = %s;\n", AZ::RHI::AddressModeNamespace::ToString(v->m_addressU).data());
  741. srgMember += AZStd::string::format("AddressV = %s;\n", AZ::RHI::AddressModeNamespace::ToString(v->m_addressV).data());
  742. srgMember += AZStd::string::format("AddressW = %s;\n", AZ::RHI::AddressModeNamespace::ToString(v->m_addressW).data());
  743. srgMember += AZStd::string::format("MinLOD = %f;\n", AZStd::max(v->m_mipLodMin, 0.0f));
  744. srgMember += AZStd::string::format("MaxLOD = %f;\n", AZStd::max(v->m_mipLodMax, 0.0f));
  745. srgMember += AZStd::string::format("MipLODBias = %f;\n", AZStd::max(v->m_mipLodBias, 0.0f));
  746. srgMember += AZStd::string::format("BorderColor = %s;\n", AZ::RHI::BorderColorNamespace::ToString(v->m_borderColor).data());
  747. srgMember += "};\n";
  748. return srgMember;
  749. }
  750. if (AZStd::any_cast<const AZ::Data::Asset<AZ::RPI::StreamingImageAsset>>(&slotValue))
  751. {
  752. return AZStd::string::format("Texture2D SLOTNAME;\n");
  753. }
  754. return AZStd::string::format("SLOTTYPE SLOTNAME;\n");
  755. }
  756. return AZStd::string();
  757. }
  758. AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>> MaterialGraphCompiler::GetSubstitutionSymbolsFromNode(
  759. GraphModel::ConstNodePtr node) const
  760. {
  761. AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>> substitutionSymbols;
  762. // Reserving space for the number of elements added in this function.
  763. substitutionSymbols.reserve(node->GetSlots().size() * 4 + 1);
  764. substitutionSymbols.emplace_back("NODEID", GetSymbolNameFromNode(node));
  765. for (const auto& slotPair : node->GetSlots())
  766. {
  767. const auto& slot = slotPair.second;
  768. // These substitutions will allow accessing the slot ID, type, value from anywhere in the node's shader code.
  769. substitutionSymbols.emplace_back(AZStd::string::format("SLOTTYPE\\(%s\\)", slot->GetName().c_str()), GetAzslTypeFromSlot(slot));
  770. substitutionSymbols.emplace_back(AZStd::string::format("SLOTVALUE\\(%s\\)", slot->GetName().c_str()), GetAzslValueFromSlot(slot));
  771. substitutionSymbols.emplace_back(AZStd::string::format("SLOTNAME\\(%s\\)", slot->GetName().c_str()), GetSymbolNameFromSlot(slot));
  772. // This expression will allow direct substitution of node variable names in node configurations with the decorated symbol name.
  773. // It will match whole words only. No additional decoration should be required on the node configuration side. However, support
  774. // for the older slot type, name, value substitutions are still supported as a convenience.
  775. substitutionSymbols.emplace_back(AZStd::string::format("\\b%s\\b", slot->GetName().c_str()), GetSymbolNameFromSlot(slot));
  776. }
  777. return substitutionSymbols;
  778. }
  779. AZStd::vector<AZStd::string> MaterialGraphCompiler::GetInstructionsFromSlot(
  780. GraphModel::ConstNodePtr node,
  781. const AtomToolsFramework::DynamicNodeSlotConfig& slotConfig,
  782. const AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>>& substitutionSymbols) const
  783. {
  784. AZStd::vector<AZStd::string> instructionsForSlot;
  785. auto slot = node->GetSlot(slotConfig.m_name);
  786. if (slot && (slot->GetSlotDirection() != GraphModel::SlotDirection::Output || !slot->GetConnections().empty()))
  787. {
  788. AtomToolsFramework::CollectDynamicNodeSettings(slotConfig.m_settings, "instructions", instructionsForSlot);
  789. AtomToolsFramework::ReplaceSymbolsInContainer(substitutionSymbols, instructionsForSlot);
  790. AtomToolsFramework::ReplaceSymbolsInContainer("SLOTNAME", GetSymbolNameFromSlot(slot), instructionsForSlot);
  791. AtomToolsFramework::ReplaceSymbolsInContainer("SLOTTYPE", GetAzslTypeFromSlot(slot), instructionsForSlot);
  792. AtomToolsFramework::ReplaceSymbolsInContainer("SLOTVALUE", GetAzslValueFromSlot(slot), instructionsForSlot);
  793. }
  794. return instructionsForSlot;
  795. }
  796. bool MaterialGraphCompiler::ShouldUseInstructionsFromInputNode(
  797. GraphModel::ConstNodePtr outputNode, GraphModel::ConstNodePtr inputNode, const AZStd::vector<AZStd::string>& inputSlotNames) const
  798. {
  799. if (inputNode == outputNode)
  800. {
  801. return true;
  802. }
  803. for (const auto& inputSlotName : inputSlotNames)
  804. {
  805. if (const auto slot = outputNode->GetSlot(inputSlotName))
  806. {
  807. if (slot->GetSlotDirection() == GraphModel::SlotDirection::Input)
  808. {
  809. for (const auto& connection : slot->GetConnections())
  810. {
  811. AZ_Assert(connection->GetSourceNode() != outputNode, "This should never be the source node on an input connection.");
  812. AZ_Assert(connection->GetTargetNode() == outputNode, "This should always be the target node on an input connection.");
  813. if (connection->GetSourceNode() == inputNode || connection->GetSourceNode()->HasInputConnectionFromNode(inputNode))
  814. {
  815. return true;
  816. }
  817. }
  818. }
  819. }
  820. }
  821. return false;
  822. }
  823. AZStd::vector<GraphModel::ConstNodePtr> MaterialGraphCompiler::GetAllNodesInExecutionOrder() const
  824. {
  825. AZStd::vector<GraphModel::ConstNodePtr> nodes;
  826. if (!m_graph)
  827. {
  828. AZ_Error("MaterialGraphCompiler", false, "Attempting to generate data from invalid graph object.");
  829. return nodes;
  830. }
  831. nodes.reserve(m_graph->GetNodes().size());
  832. for (const auto& nodePair : m_graph->GetNodes())
  833. {
  834. nodes.push_back(nodePair.second);
  835. }
  836. AtomToolsFramework::SortNodesInExecutionOrder(nodes);
  837. return nodes;
  838. }
  839. AZStd::vector<GraphModel::ConstNodePtr> MaterialGraphCompiler::GetInstructionNodesInExecutionOrder(
  840. GraphModel::ConstNodePtr outputNode, const AZStd::vector<AZStd::string>& inputSlotNames) const
  841. {
  842. AZStd::vector<GraphModel::ConstNodePtr> nodes = GetAllNodesInExecutionOrder();
  843. AZStd::erase_if(nodes, [this, &outputNode, &inputSlotNames](const auto& node) {
  844. return !ShouldUseInstructionsFromInputNode(outputNode, node, inputSlotNames);
  845. });
  846. return nodes;
  847. }
  848. AZStd::vector<AZStd::string> MaterialGraphCompiler::GetInstructionsFromConnectedNodes(
  849. GraphModel::ConstNodePtr outputNode,
  850. const AZStd::vector<AZStd::string>& inputSlotNames,
  851. AZStd::vector<GraphModel::ConstNodePtr>& instructionNodes) const
  852. {
  853. AZStd::vector<AZStd::string> instructions;
  854. for (const auto& inputNode : GetInstructionNodesInExecutionOrder(outputNode, inputSlotNames))
  855. {
  856. // Build a list of all nodes that will contribute instructions for the output node
  857. if (AZStd::find(instructionNodes.begin(), instructionNodes.end(), inputNode) == instructionNodes.end())
  858. {
  859. instructionNodes.push_back(inputNode);
  860. }
  861. auto dynamicNode = azrtti_cast<const AtomToolsFramework::DynamicNode*>(inputNode.get());
  862. if (dynamicNode)
  863. {
  864. const auto& nodeConfig = dynamicNode->GetConfig();
  865. const auto& substitutionSymbols = GetSubstitutionSymbolsFromNode(inputNode);
  866. // Instructions are gathered separately for all of the slot categories because they need to be added in a specific order.
  867. // Gather and perform substitutions on instructions embedded directly in the node.
  868. AZStd::vector<AZStd::string> instructionsForNode;
  869. AtomToolsFramework::CollectDynamicNodeSettings(nodeConfig.m_settings, "instructions", instructionsForNode);
  870. AtomToolsFramework::ReplaceSymbolsInContainer(substitutionSymbols, instructionsForNode);
  871. // Gather and perform substitutions on instructions contained in property slots.
  872. AZStd::vector<AZStd::string> instructionsForPropertySlots;
  873. for (const auto& slotConfig : nodeConfig.m_propertySlots)
  874. {
  875. const auto& instructionsForSlot = GetInstructionsFromSlot(inputNode, slotConfig, substitutionSymbols);
  876. instructionsForPropertySlots.insert(instructionsForPropertySlots.end(), instructionsForSlot.begin(), instructionsForSlot.end());
  877. }
  878. // Gather and perform substitutions on instructions contained in input slots.
  879. AZStd::vector<AZStd::string> instructionsForInputSlots;
  880. for (const auto& slotConfig : nodeConfig.m_inputSlots)
  881. {
  882. // If this is the output node, only gather instructions for requested input slots.
  883. if (inputNode == outputNode &&
  884. AZStd::find(inputSlotNames.begin(), inputSlotNames.end(), slotConfig.m_name) == inputSlotNames.end())
  885. {
  886. continue;
  887. }
  888. const auto& instructionsForSlot = GetInstructionsFromSlot(inputNode, slotConfig, substitutionSymbols);
  889. instructionsForInputSlots.insert(instructionsForInputSlots.end(), instructionsForSlot.begin(), instructionsForSlot.end());
  890. }
  891. // Gather and perform substitutions on instructions contained in output slots.
  892. AZStd::vector<AZStd::string> instructionsForOutputSlots;
  893. for (const auto& slotConfig : nodeConfig.m_outputSlots)
  894. {
  895. const auto& instructionsForSlot = GetInstructionsFromSlot(inputNode, slotConfig, substitutionSymbols);
  896. instructionsForOutputSlots.insert(instructionsForOutputSlots.end(), instructionsForSlot.begin(), instructionsForSlot.end());
  897. }
  898. instructions.insert(instructions.end(), instructionsForPropertySlots.begin(), instructionsForPropertySlots.end());
  899. instructions.insert(instructions.end(), instructionsForInputSlots.begin(), instructionsForInputSlots.end());
  900. instructions.insert(instructions.end(), instructionsForNode.begin(), instructionsForNode.end());
  901. instructions.insert(instructions.end(), instructionsForOutputSlots.begin(), instructionsForOutputSlots.end());
  902. }
  903. }
  904. return instructions;
  905. }
  906. AZStd::string MaterialGraphCompiler::GetSymbolNameFromNode(GraphModel::ConstNodePtr node) const
  907. {
  908. return AtomToolsFramework::GetSymbolNameFromText(AZStd::string::format("node%u_%s", node->GetId(), node->GetTitle()));
  909. }
  910. AZStd::string MaterialGraphCompiler::GetSymbolNameFromSlot(GraphModel::ConstSlotPtr slot) const
  911. {
  912. bool allowNameSubstitution = true;
  913. if (auto dynamicNode = azrtti_cast<const AtomToolsFramework::DynamicNode*>(slot->GetParentNode().get()))
  914. {
  915. const auto& nodeConfig = dynamicNode->GetConfig();
  916. AtomToolsFramework::VisitDynamicNodeSlotConfigs(
  917. nodeConfig,
  918. [&](const AtomToolsFramework::DynamicNodeSlotConfig& slotConfig)
  919. {
  920. if (slot->GetName() == slotConfig.m_name)
  921. {
  922. allowNameSubstitution = slotConfig.m_allowNameSubstitution;
  923. }
  924. });
  925. }
  926. if (!allowNameSubstitution)
  927. {
  928. return slot->GetName();
  929. }
  930. if (slot->SupportsExtendability())
  931. {
  932. return AZStd::string::format(
  933. "%s_%s_%d", GetSymbolNameFromNode(slot->GetParentNode()).c_str(), slot->GetName().c_str(), slot->GetSlotSubId());
  934. }
  935. return AZStd::string::format("%s_%s", GetSymbolNameFromNode(slot->GetParentNode()).c_str(), slot->GetName().c_str());
  936. }
  937. AZStd::vector<AZStd::string> MaterialGraphCompiler::GetMaterialPropertySrgMemberFromSlot(
  938. GraphModel::ConstNodePtr node,
  939. const AtomToolsFramework::DynamicNodeSlotConfig& slotConfig,
  940. const AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>>& substitutionSymbols) const
  941. {
  942. AZStd::vector<AZStd::string> materialPropertySrgMemberForSlot;
  943. if (auto slot = node->GetSlot(slotConfig.m_name))
  944. {
  945. AtomToolsFramework::CollectDynamicNodeSettings(slotConfig.m_settings, "materialPropertySrgMember", materialPropertySrgMemberForSlot);
  946. AtomToolsFramework::ReplaceSymbolsInContainer(substitutionSymbols, materialPropertySrgMemberForSlot);
  947. AtomToolsFramework::ReplaceSymbolsInContainer("STANDARD_SRG_MEMBER", GetAzslSrgMemberFromSlot(node, slotConfig), materialPropertySrgMemberForSlot);
  948. AtomToolsFramework::ReplaceSymbolsInContainer("SLOTNAME", GetSymbolNameFromSlot(slot), materialPropertySrgMemberForSlot);
  949. AtomToolsFramework::ReplaceSymbolsInContainer("SLOTTYPE", GetAzslTypeFromSlot(slot), materialPropertySrgMemberForSlot);
  950. AtomToolsFramework::ReplaceSymbolsInContainer("SLOTVALUE", GetAzslValueFromSlot(slot), materialPropertySrgMemberForSlot);
  951. }
  952. return materialPropertySrgMemberForSlot;
  953. }
  954. AZStd::vector<AZStd::string> MaterialGraphCompiler::GetMaterialPropertySrgMemberFromNodes(
  955. const AZStd::vector<GraphModel::ConstNodePtr>& instructionNodes) const
  956. {
  957. if (!m_graph)
  958. {
  959. AZ_Error("MaterialGraphCompiler", false, "Attempting to generate data from invalid graph object.");
  960. return {};
  961. }
  962. AZStd::vector<AZStd::string> materialPropertySrgMember;
  963. for (const auto& inputNode : instructionNodes)
  964. {
  965. auto dynamicNode = azrtti_cast<const AtomToolsFramework::DynamicNode*>(inputNode.get());
  966. if (dynamicNode)
  967. {
  968. const auto& nodeConfig = dynamicNode->GetConfig();
  969. const auto& substitutionSymbols = GetSubstitutionSymbolsFromNode(inputNode);
  970. AZStd::vector<AZStd::string> materialPropertySrgMembersForNode;
  971. AtomToolsFramework::CollectDynamicNodeSettings(
  972. nodeConfig.m_settings, "materialPropertySrgMember", materialPropertySrgMembersForNode);
  973. AtomToolsFramework::ReplaceSymbolsInContainer(substitutionSymbols, materialPropertySrgMembersForNode);
  974. AtomToolsFramework::VisitDynamicNodeSlotConfigs(
  975. nodeConfig,
  976. [&](const AtomToolsFramework::DynamicNodeSlotConfig& slotConfig)
  977. {
  978. const auto& materialPropertySrgMemberForSlot =
  979. GetMaterialPropertySrgMemberFromSlot(inputNode, slotConfig, substitutionSymbols);
  980. materialPropertySrgMembersForNode.insert(
  981. materialPropertySrgMembersForNode.end(),
  982. materialPropertySrgMemberForSlot.begin(),
  983. materialPropertySrgMemberForSlot.end());
  984. });
  985. materialPropertySrgMember.insert(
  986. materialPropertySrgMember.end(), materialPropertySrgMembersForNode.begin(), materialPropertySrgMembersForNode.end());
  987. }
  988. }
  989. return materialPropertySrgMember;
  990. }
  991. bool MaterialGraphCompiler::BuildMaterialTypeFromTemplate(
  992. GraphModel::ConstNodePtr templateNode,
  993. const AZStd::vector<GraphModel::ConstNodePtr>& instructionNodes,
  994. const AZStd::string& templateInputPath,
  995. const AZStd::string& templateOutputPath) const
  996. {
  997. using namespace AtomToolsFramework;
  998. if (!m_graph)
  999. {
  1000. AZ_Error("MaterialGraphCompiler", false, "Attempting to generate data from invalid graph object.");
  1001. return false;
  1002. }
  1003. if (!templateNode)
  1004. {
  1005. AZ_Error("MaterialGraphCompiler", false, "Attempting to generate data from invalid template node.");
  1006. return false;
  1007. }
  1008. // Load the material type template file, which is the same format as MaterialTypeSourceData with a different extension
  1009. auto materialTypeOutcome = AZ::RPI::MaterialUtils::LoadMaterialTypeSourceData(templateInputPath);
  1010. if (!materialTypeOutcome.IsSuccess())
  1011. {
  1012. AZ_Error("MaterialGraphCompiler", false, "Material type template could not be loaded: '%s'.", templateInputPath.c_str());
  1013. return false;
  1014. }
  1015. // Copy the material type source data from the template and begin populating it.
  1016. AZ::RPI::MaterialTypeSourceData materialTypeSourceData = materialTypeOutcome.TakeValue();
  1017. // If the node providing all the template information has a description then assign it to the material type source data.
  1018. materialTypeSourceData.m_description = GetStringValueFromSlot(templateNode->GetSlot("inDescription"));
  1019. // Search the graph for nodes defining material input properties that should be added to the material type and material SRG
  1020. for (const auto& inputNode : instructionNodes)
  1021. {
  1022. // Search for all slots with settings indicating that material type properties should be generated. The settings can correspond
  1023. // to shader inputs, shader options, and other material property values that may or may not have matching entries in the
  1024. // material SRG.
  1025. AZStd::vector<AZStd::pair<GraphModel::ConstSlotPtr, DynamicNodeSlotConfig>> materialPropertyValueSlots;
  1026. if (auto dynamicNode = azrtti_cast<const DynamicNode*>(inputNode.get()))
  1027. {
  1028. VisitDynamicNodeSlotConfigs(
  1029. dynamicNode->GetConfig(),
  1030. [&](const DynamicNodeSlotConfig& slotConfig)
  1031. {
  1032. if (slotConfig.m_settings.contains("materialPropertyName") ||
  1033. slotConfig.m_settings.contains("materialPropertyDisplayName") ||
  1034. slotConfig.m_settings.contains("materialPropertyConnectionType") ||
  1035. slotConfig.m_settings.contains("materialPropertyConnectionName") ||
  1036. slotConfig.m_settings.contains("materialPropertyGroupName") ||
  1037. slotConfig.m_settings.contains("materialPropertyGroup"))
  1038. {
  1039. const auto materialPropertyValueSlot = inputNode->GetSlot(slotConfig.m_name);
  1040. materialPropertyValueSlots.emplace_back(materialPropertyValueSlot, slotConfig);
  1041. }
  1042. });
  1043. }
  1044. // Register all the properties that were parsed out of the slots with the material type.
  1045. for (const auto& [materialPropertyValueSlot, materialPropertyValueSlotConfig] : materialPropertyValueSlots)
  1046. {
  1047. // Sampler states are currently not configurable and will not be added added to the material type, just the material SRG.
  1048. if (!materialPropertyValueSlot || materialPropertyValueSlot->GetValue().empty() ||
  1049. materialPropertyValueSlot->GetValue().is<AZ::RHI::SamplerState>())
  1050. {
  1051. continue;
  1052. }
  1053. const auto& materialPropertyValueSlotSymbolName = GetSymbolNameFromSlot(materialPropertyValueSlot);
  1054. // If the property represents a shader option, the connection name will be defined in a static setting. Otherwise, it will
  1055. // be the slot symbol name which is the same as the variable name added to the SRG and referenced in code.
  1056. const auto& materialPropertyConnectionName = GetFirstNonEmptyString({
  1057. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyConnectionName"),
  1058. materialPropertyValueSlotSymbolName
  1059. });
  1060. // The material property connection type determines if the connection represents a shader option, shader input, internal
  1061. // value, or just a placeholder property.
  1062. const auto& materialPropertyConnectionType = GetFirstNonEmptyString({
  1063. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyConnectionType")
  1064. });
  1065. // While this might change, material properties representing shader inputs generally have their name, display name,
  1066. // description, and other details spread across multiple, user configurable slots on the same node. Shader options don't
  1067. // need a user configurable name or description because they refer to a predefined option name that will always be used the
  1068. // same way. Several shader options can be exposed on the same node. Because of that, shader options must specify their
  1069. // connection name and copy the name and description directly from the slot instead of having the users enter one.
  1070. const auto& materialPropertyUseSlotConfig = !AZ::StringFunc::Equal(materialPropertyConnectionType, "ShaderInput");
  1071. // The material property name must be unique relative to its group. Material property names are used to read and write
  1072. // property values through the material system API. These will be stored with default values in the material type and
  1073. // overridden values per material. In material canvas, rather than overwhelming the user with Learning and managing the
  1074. // differences between IDs, names, and display names, we will generate the values for symbol and display names Based on a
  1075. // single user specified material input name, slot settings, or the symbol name generated from the node and slot IDs.
  1076. // Find the most appropriate name to use for this property, prioritizing static settings for shader options first.
  1077. const auto& materialPropertyName = GetFirstNonEmptyString({
  1078. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyName"),
  1079. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyDisplayName"),
  1080. materialPropertyUseSlotConfig ? materialPropertyValueSlotConfig.m_displayName : GetStringValueFromSlot(inputNode->GetSlot("inDisplayName")),
  1081. materialPropertyUseSlotConfig ? materialPropertyValueSlotConfig.m_name : GetStringValueFromSlot(inputNode->GetSlot("inName")),
  1082. materialPropertyValueSlotSymbolName
  1083. });
  1084. // The symbol name used to uniquely identify the property in its group will be generated by transforming the above name to
  1085. // lowercase and replacing all non word characters with underscores.
  1086. const auto& materialPropertySymbolName = GetSymbolNameFromText(materialPropertyName);
  1087. // The display name slot was removed from the original, experimental material output nodes but we are handling it for
  1088. // backwards compatibility. The display name will otherwise be generated by sanitizing and camel casing the property name.
  1089. const auto& materialPropertyDisplayName = GetDisplayNameFromText(GetFirstNonEmptyString({
  1090. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyDisplayName"),
  1091. materialPropertyUseSlotConfig ? materialPropertyValueSlotConfig.m_displayName : GetStringValueFromSlot(inputNode->GetSlot("inDisplayName")),
  1092. materialPropertyName
  1093. }));
  1094. if (materialPropertyName.empty() || materialPropertySymbolName.empty() || materialPropertyDisplayName.empty())
  1095. {
  1096. AZ_Error(
  1097. "MaterialGraphCompiler",
  1098. false,
  1099. "Material property name could not be resolved for slot '%s' and template '%s'.",
  1100. materialPropertyValueSlotSymbolName.c_str(),
  1101. templateOutputPath.c_str());
  1102. return false;
  1103. }
  1104. // The group name can be specified in a static setting for shader options or configured for material inputs. Properties that
  1105. // do not explicitly define a group will fall back to the general group.
  1106. const auto& materialPropertyGroupName = GetFirstNonEmptyString({
  1107. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyGroup"),
  1108. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyGroupName"),
  1109. GetStringValueFromSlot(inputNode->GetSlot("inGroup")),
  1110. "general"
  1111. });
  1112. // Sanitize the symbol and display names for the group to force casing, spacing, and eliminate any potential erroneous input.
  1113. const auto& materialPropertyGroupSymbolName = GetSymbolNameFromText(materialPropertyGroupName);
  1114. const auto& materialPropertyGroupDisplayName = GetDisplayNameFromText(materialPropertyGroupName);
  1115. if (materialPropertyGroupName.empty() || materialPropertyGroupDisplayName.empty())
  1116. {
  1117. AZ_Error(
  1118. "MaterialGraphCompiler",
  1119. false,
  1120. "Material property group could not be resolved for slot '%s' and template '%s'.",
  1121. materialPropertyValueSlotSymbolName.c_str(),
  1122. templateOutputPath.c_str());
  1123. return false;
  1124. }
  1125. // The property description can also be read from static settings for shader options or a user configurable slot
  1126. // for material inputs. If no description is specified, it will fall back to using the material property display name.
  1127. const auto& materialPropertyDescription = GetFirstNonEmptyString({
  1128. GetSettingValueByName(materialPropertyValueSlotConfig.m_settings, "materialPropertyDescription"),
  1129. materialPropertyUseSlotConfig ? materialPropertyValueSlotConfig.m_description : GetStringValueFromSlot(inputNode->GetSlot("inDescription")),
  1130. materialPropertyDisplayName
  1131. });
  1132. // Find or create a property group with the specified name
  1133. auto propertyGroup = materialTypeSourceData.FindPropertyGroup(materialPropertyGroupSymbolName);
  1134. if (!propertyGroup)
  1135. {
  1136. // Add the property group to the material type if it was not already registered
  1137. propertyGroup = materialTypeSourceData.AddPropertyGroup(materialPropertyGroupSymbolName);
  1138. if (!propertyGroup)
  1139. {
  1140. AZ_Error(
  1141. "MaterialGraphCompiler",
  1142. false,
  1143. "Material property group '%s' could not be added for slot '%s' and template '%s'.",
  1144. materialPropertyGroupSymbolName.c_str(),
  1145. materialPropertyValueSlotSymbolName.c_str(),
  1146. templateOutputPath.c_str());
  1147. return false;
  1148. }
  1149. // The unmodified text value will be used as the display name and description for now
  1150. propertyGroup->SetDisplayName(materialPropertyGroupDisplayName);
  1151. propertyGroup->SetDescription(materialPropertyGroupDisplayName);
  1152. }
  1153. // Force material properties to be added with a unique names to prevent collisions that can occur if duplicating
  1154. unsigned int uniqueNameIndex = 0;
  1155. AZStd::string materialPropertySymbolNameUnique = materialPropertySymbolName;
  1156. AZStd::string materialPropertyDisplayNameUnique = materialPropertyDisplayName;
  1157. const auto& existingProperties = propertyGroup->GetProperties();
  1158. while (AZStd::find_if(
  1159. existingProperties.begin(),
  1160. existingProperties.end(),
  1161. [&materialPropertySymbolNameUnique](const auto& existingProperty)
  1162. {
  1163. return materialPropertySymbolNameUnique == existingProperty->GetName();
  1164. }) != existingProperties.end())
  1165. {
  1166. ++uniqueNameIndex;
  1167. materialPropertySymbolNameUnique = AZStd::string::format("%s_%u", materialPropertySymbolName.c_str(), uniqueNameIndex);
  1168. materialPropertyDisplayNameUnique = AZStd::string::format("%s (%u)", materialPropertyDisplayName.c_str(), uniqueNameIndex);
  1169. }
  1170. if (uniqueNameIndex > 0)
  1171. {
  1172. AZ_Warning(
  1173. "MaterialGraphCompiler",
  1174. false,
  1175. "Material property '%s' Was exported with a unique name '%s' in group '%s' for slot '%s' and template '%s'.",
  1176. materialPropertySymbolName.c_str(),
  1177. materialPropertySymbolNameUnique.c_str(),
  1178. materialPropertyGroupSymbolName.c_str(),
  1179. materialPropertyValueSlotSymbolName.c_str(),
  1180. templateOutputPath.c_str());
  1181. }
  1182. auto property = propertyGroup->AddProperty(materialPropertySymbolNameUnique);
  1183. if (!property)
  1184. {
  1185. AZ_Error(
  1186. "MaterialGraphCompiler",
  1187. false,
  1188. "Material property '%s' could not be added to group '%s' for slot '%s' and template '%s'.",
  1189. materialPropertySymbolNameUnique.c_str(),
  1190. materialPropertyGroupSymbolName.c_str(),
  1191. materialPropertyValueSlotSymbolName.c_str(),
  1192. templateOutputPath.c_str());
  1193. return false;
  1194. }
  1195. // Lastly, the property value is read from the slot.
  1196. const auto& materialPropertyValue = GetValueFromSlot(materialPropertyValueSlot);
  1197. // The complete property ID is a combination of the group name and the property name.
  1198. const AZ::Name materialPropertyId(materialPropertyGroupSymbolName + "." + materialPropertySymbolNameUnique);
  1199. property->m_displayName = materialPropertyDisplayNameUnique;
  1200. property->m_description = materialPropertyDescription;
  1201. property->m_enumValues = materialPropertyValueSlotConfig.m_enumValues;
  1202. property->m_value = AZ::RPI::MaterialPropertyValue::FromAny(materialPropertyValue);
  1203. // The property definition requires an explicit type enum that's converted from the actual data type.
  1204. property->m_dataType = GetMaterialPropertyDataTypeFromValue(property->m_value, !property->m_enumValues.empty());
  1205. // Images and enums need additional conversion prior to being saved.
  1206. ConvertToExportFormat(templateOutputPath, materialPropertyId, *property, property->m_value);
  1207. // This property connects to the material SRG member with the same name. Shader options are not yet supported.
  1208. if (!materialPropertyConnectionName.empty())
  1209. {
  1210. if (AZ::StringFunc::Equal(materialPropertyConnectionType, "ShaderInput"))
  1211. {
  1212. property->m_outputConnections.emplace_back(
  1213. AZ::RPI::MaterialPropertyOutputType::ShaderInput, materialPropertyConnectionName);
  1214. }
  1215. else if (AZ::StringFunc::Equal(materialPropertyConnectionType, "ShaderOption"))
  1216. {
  1217. property->m_outputConnections.emplace_back(
  1218. AZ::RPI::MaterialPropertyOutputType::ShaderOption, materialPropertyConnectionName);
  1219. }
  1220. else if (AZ::StringFunc::Equal(materialPropertyConnectionType, "InternalProperty"))
  1221. {
  1222. property->m_outputConnections.emplace_back(
  1223. AZ::RPI::MaterialPropertyOutputType::InternalProperty, materialPropertyConnectionName);
  1224. }
  1225. }
  1226. }
  1227. }
  1228. // Sorting groups and properties in the source data layout to force consistent ordering of the generated material type.
  1229. materialTypeSourceData.SortProperties();
  1230. // The file is written to an in memory buffer before saving to facilitate string substitutions.
  1231. AZStd::string templateOutputText;
  1232. if (!AZ::RPI::JsonUtils::SaveObjectToString(templateOutputText, materialTypeSourceData))
  1233. {
  1234. AZ_Error("MaterialGraphCompiler", false, "Material type template could not be saved: '%s'.", templateOutputPath.c_str());
  1235. return false;
  1236. }
  1237. // Substitute the material graph name and any other Material Canvas specific tokens
  1238. AZ::StringFunc::Replace(templateOutputText, "MaterialGraphName", GetUniqueGraphName().c_str());
  1239. AZ_TracePrintf_IfTrue(
  1240. "MaterialGraphCompiler", IsCompileLoggingEnabled(), "Saving generated file: %s\n", templateOutputPath.c_str());
  1241. // The material type is complete and can be saved to disk.
  1242. const auto writeOutcome = AZ::Utils::WriteFile(templateOutputText, templateOutputPath);
  1243. if (!writeOutcome)
  1244. {
  1245. AZ_Error("MaterialGraphCompiler", false, "Material type template could not be saved: '%s'.", templateOutputPath.c_str());
  1246. return false;
  1247. }
  1248. return true;
  1249. }
  1250. AZStd::string MaterialGraphCompiler::GetUniqueGraphName() const
  1251. {
  1252. return m_templateNodeCount <= 0 ? m_graphName : AZStd::string::format("%s_%03i", m_graphName.c_str(), m_templateNodeCount);
  1253. }
  1254. } // namespace MaterialCanvas