MaterialDocument.cpp 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  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/RPI.Edit/Common/AssetUtils.h>
  9. #include <Atom/RPI.Edit/Common/JsonUtils.h>
  10. #include <Atom/RPI.Edit/Material/MaterialFunctorSourceData.h>
  11. #include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
  12. #include <Atom/RPI.Edit/Material/MaterialUtils.h>
  13. #include <Atom/RPI.Public/Material/Material.h>
  14. #include <Atom/RPI.Reflect/Material/MaterialFunctor.h>
  15. #include <Atom/RPI.Reflect/Material/MaterialNameContext.h>
  16. #include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
  17. #include <AtomCore/Instance/Instance.h>
  18. #include <AtomToolsFramework/Document/AtomToolsDocumentNotificationBus.h>
  19. #include <AtomToolsFramework/Util/MaterialPropertyUtil.h>
  20. #include <AtomToolsFramework/Util/Util.h>
  21. #include <AtomToolsFramework/Util/Util.h>
  22. #include <AzCore/RTTI/BehaviorContext.h>
  23. #include <AzCore/Serialization/EditContext.h>
  24. #include <AzCore/Serialization/SerializeContext.h>
  25. #include <Document/MaterialDocument.h>
  26. namespace MaterialEditor
  27. {
  28. void MaterialDocument::Reflect(AZ::ReflectContext* context)
  29. {
  30. if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
  31. {
  32. serialize->Class<MaterialDocument, AtomToolsFramework::AtomToolsDocument>()
  33. ->Version(0);
  34. }
  35. if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  36. {
  37. behaviorContext->EBus<MaterialDocumentRequestBus>("MaterialDocumentRequestBus")
  38. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
  39. ->Attribute(AZ::Script::Attributes::Category, "Editor")
  40. ->Attribute(AZ::Script::Attributes::Module, "materialeditor")
  41. ->Event("SetPropertyValue", &MaterialDocumentRequestBus::Events::SetPropertyValue)
  42. ->Event("GetPropertyValue", &MaterialDocumentRequestBus::Events::GetPropertyValue);
  43. }
  44. }
  45. MaterialDocument::MaterialDocument(const AZ::Crc32& toolId, const AtomToolsFramework::DocumentTypeInfo& documentTypeInfo)
  46. : AtomToolsFramework::AtomToolsDocument(toolId, documentTypeInfo)
  47. {
  48. MaterialDocumentRequestBus::Handler::BusConnect(m_id);
  49. }
  50. MaterialDocument::~MaterialDocument()
  51. {
  52. MaterialDocumentRequestBus::Handler::BusDisconnect();
  53. AZ::SystemTickBus::Handler::BusDisconnect();
  54. }
  55. AZ::Data::Asset<AZ::RPI::MaterialAsset> MaterialDocument::GetAsset() const
  56. {
  57. return m_materialAsset;
  58. }
  59. AZ::Data::Instance<AZ::RPI::Material> MaterialDocument::GetInstance() const
  60. {
  61. return m_materialInstance;
  62. }
  63. const AZ::RPI::MaterialSourceData* MaterialDocument::GetMaterialSourceData() const
  64. {
  65. return &m_materialSourceData;
  66. }
  67. const AZ::RPI::MaterialTypeSourceData* MaterialDocument::GetMaterialTypeSourceData() const
  68. {
  69. return &m_materialTypeSourceData;
  70. }
  71. void MaterialDocument::SetPropertyValue(const AZStd::string& propertyId, const AZStd::any& value)
  72. {
  73. const AZ::Name propertyName(propertyId);
  74. AtomToolsFramework::DynamicProperty* foundProperty = {};
  75. TraverseGroups(m_groups, [&, this](auto& group) {
  76. for (auto& property : group->m_properties)
  77. {
  78. if (property.GetId() == propertyName)
  79. {
  80. foundProperty = &property;
  81. if (m_materialInstance)
  82. {
  83. // This first converts to an acceptable runtime type in case the value came from script
  84. const AZ::RPI::MaterialPropertyValue propertyValue = AtomToolsFramework::ConvertToRuntimeType(value);
  85. property.SetValue(AtomToolsFramework::ConvertToEditableType(propertyValue));
  86. const auto propertyIndex = m_materialInstance->FindPropertyIndex(propertyName);
  87. if (!propertyIndex.IsNull())
  88. {
  89. if (m_materialInstance->SetPropertyValue(propertyIndex, propertyValue))
  90. {
  91. AZ::RPI::MaterialPropertyFlags dirtyFlags = m_materialInstance->GetPropertyDirtyFlags();
  92. Recompile();
  93. RunEditorMaterialFunctors(dirtyFlags);
  94. }
  95. }
  96. }
  97. AtomToolsFramework::AtomToolsDocumentNotificationBus::Event(
  98. m_toolId, &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentObjectInfoChanged, m_id,
  99. GetObjectInfoFromDynamicPropertyGroup(group.get()), false);
  100. AtomToolsFramework::AtomToolsDocumentNotificationBus::Event(
  101. m_toolId, &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentModified, m_id);
  102. return false;
  103. }
  104. }
  105. return true;
  106. });
  107. if (!foundProperty)
  108. {
  109. AZ_Error("MaterialDocument", false, "Document property could not be found: '%s'.", propertyId.c_str());
  110. }
  111. }
  112. const AZStd::any& MaterialDocument::GetPropertyValue(const AZStd::string& propertyId) const
  113. {
  114. auto property = FindProperty(AZ::Name(propertyId));
  115. if (!property)
  116. {
  117. AZ_Error("MaterialDocument", false, "Document property could not be found: '%s'.", propertyId.c_str());
  118. return m_invalidValue;
  119. }
  120. return property->GetValue();
  121. }
  122. AtomToolsFramework::DocumentTypeInfo MaterialDocument::BuildDocumentTypeInfo()
  123. {
  124. AtomToolsFramework::DocumentTypeInfo documentType;
  125. documentType.m_documentTypeName = "Material";
  126. documentType.m_documentFactoryCallback = [](const AZ::Crc32& toolId, const AtomToolsFramework::DocumentTypeInfo& documentTypeInfo) {
  127. return aznew MaterialDocument(toolId, documentTypeInfo); };
  128. documentType.m_supportedExtensionsToCreate.push_back({ "Material Type", AZ::RPI::MaterialTypeSourceData::Extension });
  129. documentType.m_supportedExtensionsToCreate.push_back({ "Material", AZ::RPI::MaterialSourceData::Extension });
  130. documentType.m_supportedExtensionsToOpen.push_back({ "Material Type", AZ::RPI::MaterialTypeSourceData::Extension });
  131. documentType.m_supportedExtensionsToOpen.push_back({ "Material", AZ::RPI::MaterialSourceData::Extension });
  132. documentType.m_supportedExtensionsToSave.push_back({ "Material", AZ::RPI::MaterialSourceData::Extension });
  133. documentType.m_defaultDocumentTemplate =
  134. AtomToolsFramework::GetPathWithoutAlias(AtomToolsFramework::GetSettingsValue<AZStd::string>(
  135. "/O3DE/Atom/MaterialEditor/DefaultMaterialType",
  136. "@gemroot:Atom_Feature_Common@/Assets/Materials/Types/StandardPBR.materialtype"));
  137. return documentType;
  138. }
  139. AtomToolsFramework::DocumentObjectInfoVector MaterialDocument::GetObjectInfo() const
  140. {
  141. AtomToolsFramework::DocumentObjectInfoVector objects = AtomToolsDocument::GetObjectInfo();
  142. objects.reserve(objects.size() + m_groups.size());
  143. for (const auto& group : m_groups)
  144. {
  145. objects.push_back(AZStd::move(GetObjectInfoFromDynamicPropertyGroup(group.get())));
  146. }
  147. return objects;
  148. }
  149. bool MaterialDocument::Save()
  150. {
  151. if (!AtomToolsDocument::Save())
  152. {
  153. // SaveFailed has already been called so just forward the result without additional notifications.
  154. // TODO Replace bool return value with enum for open and save states.
  155. return false;
  156. }
  157. // populate sourceData with modified or overridden properties and save object
  158. AZ::RPI::MaterialSourceData sourceData;
  159. if (m_materialAsset.IsReady() &&
  160. m_materialAsset->GetMaterialTypeAsset().IsReady())
  161. {
  162. sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion();
  163. }
  164. sourceData.m_materialType = AtomToolsFramework::GetPathToExteralReference(m_absolutePath, m_materialSourceData.m_materialType);
  165. sourceData.m_parentMaterial = AtomToolsFramework::GetPathToExteralReference(m_absolutePath, m_materialSourceData.m_parentMaterial);
  166. auto propertyFilter = [](const AtomToolsFramework::DynamicProperty& property) {
  167. return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_parentValue);
  168. };
  169. if (!SaveSourceData(sourceData, propertyFilter))
  170. {
  171. return SaveFailed();
  172. }
  173. // after saving, reset to a clean state
  174. TraverseGroups(m_groups, [&](auto& group) {
  175. for (auto& property : group->m_properties)
  176. {
  177. auto propertyConfig = property.GetConfig();
  178. propertyConfig.m_originalValue = property.GetValue();
  179. property.SetConfig(propertyConfig);
  180. }
  181. return true;
  182. });
  183. return SaveSucceeded();
  184. }
  185. bool MaterialDocument::SaveAsCopy(const AZStd::string& savePath)
  186. {
  187. if (!AtomToolsDocument::SaveAsCopy(savePath))
  188. {
  189. // SaveFailed has already been called so just forward the result without additional notifications.
  190. // TODO Replace bool return value with enum for open and save states.
  191. return false;
  192. }
  193. // populate sourceData with modified or overridden properties and save object
  194. AZ::RPI::MaterialSourceData sourceData;
  195. if (m_materialAsset.IsReady() &&
  196. m_materialAsset->GetMaterialTypeAsset().IsReady())
  197. {
  198. sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion();
  199. }
  200. sourceData.m_materialType = AtomToolsFramework::GetPathToExteralReference(m_savePathNormalized, m_materialSourceData.m_materialType);
  201. sourceData.m_parentMaterial = AtomToolsFramework::GetPathToExteralReference(m_savePathNormalized, m_materialSourceData.m_parentMaterial);
  202. auto propertyFilter = [](const AtomToolsFramework::DynamicProperty& property) {
  203. return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_parentValue);
  204. };
  205. if (!SaveSourceData(sourceData, propertyFilter))
  206. {
  207. return SaveFailed();
  208. }
  209. // If the document is saved to a new file we need to reopen the new document to update assets, paths, property deltas.
  210. if (!Open(m_savePathNormalized))
  211. {
  212. return SaveFailed();
  213. }
  214. return SaveSucceeded();
  215. }
  216. bool MaterialDocument::SaveAsChild(const AZStd::string& savePath)
  217. {
  218. if (!AtomToolsDocument::SaveAsChild(savePath))
  219. {
  220. // SaveFailed has already been called so just forward the result without additional notifications.
  221. // TODO Replace bool return value with enum for open and save states.
  222. return false;
  223. }
  224. // populate sourceData with modified or overridden properties and save object
  225. AZ::RPI::MaterialSourceData sourceData;
  226. if (m_materialAsset.IsReady() &&
  227. m_materialAsset->GetMaterialTypeAsset().IsReady())
  228. {
  229. sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion();
  230. }
  231. sourceData.m_materialType = AtomToolsFramework::GetPathToExteralReference(m_savePathNormalized, m_materialSourceData.m_materialType);
  232. // Only assign a parent path if the source was a .material
  233. if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), AZ::RPI::MaterialSourceData::Extension))
  234. {
  235. sourceData.m_parentMaterial = AtomToolsFramework::GetPathToExteralReference(m_savePathNormalized, m_absolutePath);
  236. }
  237. auto propertyFilter = [](const AtomToolsFramework::DynamicProperty& property) {
  238. return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_originalValue);
  239. };
  240. if (!SaveSourceData(sourceData, propertyFilter))
  241. {
  242. return SaveFailed();
  243. }
  244. // If the document is saved to a new file we need to reopen the new document to update assets, paths, property deltas.
  245. if (!Open(m_savePathNormalized))
  246. {
  247. return SaveFailed();
  248. }
  249. return SaveSucceeded();
  250. }
  251. bool MaterialDocument::IsModified() const
  252. {
  253. bool result = false;
  254. TraverseGroups(m_groups, [&](auto& group) {
  255. for (auto& property : group->m_properties)
  256. {
  257. if (!AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_originalValue))
  258. {
  259. result = true;
  260. return false;
  261. }
  262. }
  263. return true;
  264. });
  265. return result;
  266. }
  267. bool MaterialDocument::CanSaveAsChild() const
  268. {
  269. return true;
  270. }
  271. bool MaterialDocument::BeginEdit()
  272. {
  273. // Save the current properties as a momento for undo before any changes are applied
  274. m_propertyValuesBeforeEdit.clear();
  275. TraverseGroups(m_groups, [this](auto& group) {
  276. for (auto& property : group->m_properties)
  277. {
  278. m_propertyValuesBeforeEdit[property.GetId()] = property.GetValue();
  279. }
  280. return true;
  281. });
  282. return true;
  283. }
  284. bool MaterialDocument::EndEdit()
  285. {
  286. PropertyValueMap propertyValuesForUndo;
  287. PropertyValueMap propertyValuesForRedo;
  288. // After editing has completed, check to see if properties have changed so the deltas can be recorded in the history
  289. for (const auto& propertyBeforeEditPair : m_propertyValuesBeforeEdit)
  290. {
  291. const auto& propertyName = propertyBeforeEditPair.first;
  292. const auto& propertyValueForUndo = propertyBeforeEditPair.second;
  293. const auto& propertyValueForRedo = GetPropertyValue(propertyName.GetStringView());
  294. if (!AtomToolsFramework::ArePropertyValuesEqual(propertyValueForUndo, propertyValueForRedo))
  295. {
  296. propertyValuesForUndo[propertyName] = propertyValueForUndo;
  297. propertyValuesForRedo[propertyName] = propertyValueForRedo;
  298. }
  299. }
  300. if (!propertyValuesForUndo.empty() && !propertyValuesForRedo.empty())
  301. {
  302. AddUndoRedoHistory(
  303. [this, propertyValuesForUndo]() { RestorePropertyValues(propertyValuesForUndo); },
  304. [this, propertyValuesForRedo]() { RestorePropertyValues(propertyValuesForRedo); });
  305. }
  306. m_propertyValuesBeforeEdit.clear();
  307. return true;
  308. }
  309. void MaterialDocument::OnSystemTick()
  310. {
  311. if (m_compilePending)
  312. {
  313. if (m_materialInstance && m_materialInstance->Compile())
  314. {
  315. m_compilePending = false;
  316. AZ::SystemTickBus::Handler::BusDisconnect();
  317. }
  318. }
  319. }
  320. bool MaterialDocument::SaveSourceData(AZ::RPI::MaterialSourceData& sourceData, PropertyFilterFunction propertyFilter) const
  321. {
  322. bool addPropertiesResult = true;
  323. // populate sourceData with properties that meet the filter
  324. m_materialTypeSourceData.EnumerateProperties([&](const auto& propertyDefinition, const AZ::RPI::MaterialNameContext& nameContext) {
  325. AZ::Name propertyId{propertyDefinition->GetName()};
  326. nameContext.ContextualizeProperty(propertyId);
  327. const auto property = FindProperty(propertyId);
  328. if (property && propertyFilter(*property))
  329. {
  330. AZ::RPI::MaterialPropertyValue propertyValue = AtomToolsFramework::ConvertToRuntimeType(property->GetValue());
  331. if (propertyValue.IsValid())
  332. {
  333. if (!AtomToolsFramework::ConvertToExportFormat(m_savePathNormalized, propertyId, *propertyDefinition, propertyValue))
  334. {
  335. AZ_Error("MaterialDocument", false, "Document property could not be converted: '%s' in '%s'.", propertyId.GetCStr(), m_absolutePath.c_str());
  336. addPropertiesResult = false;
  337. return false;
  338. }
  339. sourceData.SetPropertyValue(propertyId, propertyValue);
  340. }
  341. }
  342. return true;
  343. });
  344. if (!addPropertiesResult)
  345. {
  346. AZ_Error("MaterialDocument", false, "Document properties could not be saved: '%s'.", m_savePathNormalized.c_str());
  347. return false;
  348. }
  349. // Copy the description property to the outgoing source data
  350. if (const AZStd::any descriptionProperty = GetPropertyValue("overview.materialDescription");
  351. descriptionProperty.is<AZStd::string>())
  352. {
  353. sourceData.m_description = AZStd::any_cast<AZStd::string>(descriptionProperty);
  354. }
  355. if (!AZ::RPI::JsonUtils::SaveObjectToFile(m_savePathNormalized, sourceData))
  356. {
  357. AZ_Error("MaterialDocument", false, "Document could not be saved: '%s'.", m_savePathNormalized.c_str());
  358. return false;
  359. }
  360. return true;
  361. }
  362. bool MaterialDocument::Open(const AZStd::string& loadPath)
  363. {
  364. if (!AtomToolsDocument::Open(loadPath))
  365. {
  366. return false;
  367. }
  368. // The material document can load both material source data and material type source data files. Saving material type documents is
  369. // not supported but they can be used to save a child or create a new material from the material type. This could also be extended
  370. // to load material product assets, like the material instance editor on the material component. Those would also not be savable
  371. // but could be used to create material source file, like the material component UI.
  372. if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), AZ::RPI::MaterialSourceData::Extension))
  373. {
  374. if (!LoadMaterialSourceData())
  375. {
  376. return OpenFailed();
  377. }
  378. }
  379. else if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), AZ::RPI::MaterialTypeSourceData::Extension))
  380. {
  381. if (!LoadMaterialTypeSourceData())
  382. {
  383. return OpenFailed();
  384. }
  385. }
  386. else
  387. {
  388. AZ_Error("MaterialDocument", false, "Document extension not supported: '%s'.", m_absolutePath.c_str());
  389. return OpenFailed();
  390. }
  391. const bool elevateWarnings = false;
  392. // In order to support automation, general usability, and 'save as' functionality, the user must not have to wait
  393. // for their JSON file to be cooked by the asset processor before opening or editing it.
  394. // We need to reduce or remove dependency on the asset processor. In order to get around the bottleneck for now,
  395. // we can create the asset dynamically from the source data.
  396. // Long term, the material document should not be concerned with assets at all. The viewport window should be the
  397. // only thing concerned with assets or instances.
  398. auto materialAssetResult = m_materialSourceData.CreateMaterialAssetFromSourceData(
  399. AZ::Uuid::CreateRandom(), m_absolutePath, elevateWarnings, &m_sourceDependencies);
  400. if (!materialAssetResult)
  401. {
  402. AZ_Error("MaterialDocument", false, "Material asset could not be created from source data: '%s'.", m_absolutePath.c_str());
  403. return OpenFailed();
  404. }
  405. m_materialAsset = materialAssetResult.GetValue();
  406. if (!m_materialAsset.IsReady())
  407. {
  408. AZ_Error("MaterialDocument", false, "Material asset is not ready: '%s'.", m_absolutePath.c_str());
  409. return OpenFailed();
  410. }
  411. const auto& materialTypeAsset = m_materialAsset->GetMaterialTypeAsset();
  412. if (!materialTypeAsset.IsReady())
  413. {
  414. AZ_Error("MaterialDocument", false, "Material type asset is not ready: '%s'.", m_absolutePath.c_str());
  415. return OpenFailed();
  416. }
  417. // The parent material asset is only needed to retrieve property values for comparison.
  418. AZStd::span<const AZ::RPI::MaterialPropertyValue> parentPropertyValues = materialTypeAsset->GetDefaultPropertyValues();
  419. AZ::Data::Asset<AZ::RPI::MaterialAsset> parentMaterialAsset;
  420. if (!m_materialSourceData.m_parentMaterial.empty())
  421. {
  422. AZ::RPI::MaterialSourceData parentMaterialSourceData;
  423. auto loadResult = AZ::RPI::MaterialUtils::LoadMaterialSourceData(m_materialSourceData.m_parentMaterial);
  424. if (!loadResult)
  425. {
  426. AZ_Error("MaterialDocument", false, "Material parent source data could not be loaded for: '%s'.", m_materialSourceData.m_parentMaterial.c_str());
  427. return OpenFailed();
  428. }
  429. parentMaterialSourceData = loadResult.TakeValue();
  430. const auto parentMaterialAssetIdResult = AZ::RPI::AssetUtils::MakeAssetId(m_materialSourceData.m_parentMaterial, 0);
  431. if (!parentMaterialAssetIdResult)
  432. {
  433. AZ_Error("MaterialDocument", false, "Material parent asset ID could not be created: '%s'.", m_materialSourceData.m_parentMaterial.c_str());
  434. return OpenFailed();
  435. }
  436. // In order to avoid reliance on the asset processor, the material asset is generated in memory, directly from source files.
  437. auto parentMaterialAssetResult = parentMaterialSourceData.CreateMaterialAssetFromSourceData(
  438. parentMaterialAssetIdResult.GetValue(), m_materialSourceData.m_parentMaterial, true);
  439. if (!parentMaterialAssetResult)
  440. {
  441. AZ_Error("MaterialDocument", false, "Material parent asset could not be created from source data: '%s'.", m_materialSourceData.m_parentMaterial.c_str());
  442. return OpenFailed();
  443. }
  444. parentMaterialAsset = parentMaterialAssetResult.GetValue();
  445. parentPropertyValues = parentMaterialAsset->GetPropertyValues();
  446. }
  447. // A material instance needs to be created from the loaded asset to execute functors and be able to modify properties in real time
  448. // on the object in the viewport. Now that there is much better support for hot reloading, and material assets cook fairly
  449. // quickly, this direct connection to the viewport instance may not be required. It will still be required for functors. The
  450. // instance will fail to create a new document will not open if the material asset has bad texture or material type references.
  451. m_materialInstance = AZ::RPI::Material::Create(m_materialAsset);
  452. if (!m_materialInstance)
  453. {
  454. AZ_Error("MaterialDocument", false, "Material instance could not be created: '%s'.", m_absolutePath.c_str());
  455. return OpenFailed();
  456. }
  457. // Pipeline State Object changes are always allowed in the material editor because it only runs on developer systems where such
  458. // changes are supported at runtime.
  459. m_materialInstance->SetPsoHandlingOverride(AZ::RPI::MaterialPropertyPsoHandling::Allowed);
  460. // Inserting hardcoded properties to display material type, parent material, description, UV set names, and other information at the
  461. // top of the inspector. Dynamic properties were originally created to generically adapt and edit JSON and other non-standard
  462. // reflected data using the RPE. Most of these hardcoded properties are readonly. As that changes, it may be cleaner to add
  463. // explicit functions and reflection for things that are more complicated to edit like parent material and material type.
  464. auto createHeadingPropertyConfig = [](const AZStd::string& group, const AZStd::string& name, const AZStd::string& description,
  465. const AZStd::any& value, bool readOnly)
  466. {
  467. AtomToolsFramework::DynamicPropertyConfig propertyConfig;
  468. propertyConfig.m_name = name;
  469. propertyConfig.m_displayName = AtomToolsFramework::GetDisplayNameFromText(propertyConfig.m_name);
  470. propertyConfig.m_groupName = group;
  471. propertyConfig.m_groupDisplayName = AtomToolsFramework::GetDisplayNameFromText(propertyConfig.m_groupName);
  472. propertyConfig.m_id = propertyConfig.m_groupName + "." + name;
  473. propertyConfig.m_description = description;
  474. propertyConfig.m_parentValue = propertyConfig.m_originalValue = propertyConfig.m_defaultValue = value;
  475. propertyConfig.m_readOnly = readOnly;
  476. propertyConfig.m_showThumbnail = true;
  477. return propertyConfig;
  478. };
  479. m_groups.emplace_back(aznew AtomToolsFramework::DynamicPropertyGroup);
  480. m_groups.back()->m_name = "overview";
  481. m_groups.back()->m_displayName = "Overview";
  482. m_groups.back()->m_description = "Overview of the current material and its dependencies";
  483. m_groups.back()->m_properties.emplace_back(createHeadingPropertyConfig(
  484. "overview",
  485. "materialType",
  486. AZStd::string::format(
  487. "The material type defines the layout, properties, default values, shader connections, and other data needed to create and "
  488. "edit a material.\n\nDescription of %s:\n%s",
  489. AtomToolsFramework::GetDisplayNameFromPath(m_materialSourceData.m_materialType).c_str(),
  490. m_materialTypeSourceData.m_description.c_str()),
  491. AZStd::any(materialTypeAsset),
  492. true));
  493. m_groups.back()->m_properties.emplace_back(createHeadingPropertyConfig(
  494. "overview",
  495. "parentMaterial",
  496. "The parent material provides an initial configuration whose properties are inherited and overriden by a derived material.",
  497. AZStd::any(parentMaterialAsset),
  498. true));
  499. m_groups.back()->m_properties.emplace_back(createHeadingPropertyConfig(
  500. "overview",
  501. "materialDescription",
  502. "Description of the selected material.",
  503. AZStd::any(m_materialSourceData.m_description),
  504. false));
  505. // Inserting a hard coded property group to display UV channels specified in the material type.
  506. m_groups.emplace_back(aznew AtomToolsFramework::DynamicPropertyGroup);
  507. m_groups.back()->m_name = UvGroupName;
  508. m_groups.back()->m_displayName = "UV Sets";
  509. m_groups.back()->m_description = "UV set names in this material, which can be renamed to match those in the model.";
  510. const AZ::RPI::MaterialUvNameMap& uvNameMap = materialTypeAsset->GetUvNameMap();
  511. for (const AZ::RPI::UvNamePair& uvNamePair : uvNameMap)
  512. {
  513. const AZStd::string shaderInput = uvNamePair.m_shaderInput.ToString();
  514. const AZStd::string uvName = uvNamePair.m_uvName.GetStringView();
  515. m_groups.back()->m_properties.emplace_back(createHeadingPropertyConfig(UvGroupName, shaderInput, shaderInput, AZStd::any(uvName), true));
  516. }
  517. // Populate the property map from a combination of source data and assets
  518. // Assets must still be used for now because they contain the final accumulated value after all other materials
  519. // in the hierarchy are applied
  520. bool enumerateResult = m_materialTypeSourceData.EnumeratePropertyGroups(
  521. [this, &parentPropertyValues](const AZ::RPI::MaterialTypeSourceData::PropertyGroupStack& propertyGroupStack)
  522. {
  523. using namespace AZ::RPI;
  524. const MaterialTypeSourceData::PropertyGroup* propertyGroup = propertyGroupStack.back();
  525. MaterialNameContext groupNameContext = MaterialTypeSourceData::MakeMaterialNameContext(propertyGroupStack);
  526. if (!AddEditorMaterialFunctors(propertyGroup->GetFunctors(), groupNameContext))
  527. {
  528. return false;
  529. }
  530. // Build a container of all of the group and display names accumulated while enumerating the group hierarchy. These will be
  531. // joined together for assembling full property IDs and group display names.
  532. AZStd::vector<AZStd::string> groupNameVector;
  533. groupNameVector.reserve(propertyGroupStack.size());
  534. AZStd::vector<AZStd::string> groupDisplayNameVector;
  535. groupDisplayNameVector.reserve(propertyGroupStack.size());
  536. for (auto& propertyGroupStackItem : propertyGroupStack)
  537. {
  538. groupNameVector.push_back(propertyGroupStackItem->GetName());
  539. groupDisplayNameVector.push_back(propertyGroupStackItem->GetDisplayName());
  540. }
  541. // Create a dynamic property group that will be managed by the document and used to display the properties in the inspector.
  542. AZStd::shared_ptr<AtomToolsFramework::DynamicPropertyGroup> dynamicPropertyGroup;
  543. dynamicPropertyGroup.reset(aznew AtomToolsFramework::DynamicPropertyGroup);
  544. // Copy details about this property group from the material type property group definition. Recombine the group name and
  545. // display name vectors so that the complete hierarchy will be displayed in the UI and available for creating property IDs.
  546. AzFramework::StringFunc::Join(dynamicPropertyGroup->m_name, groupNameVector.begin(), groupNameVector.end(), ".");
  547. AzFramework::StringFunc::Join(dynamicPropertyGroup->m_displayName, groupDisplayNameVector.begin(), groupDisplayNameVector.end(), " | ");
  548. if (dynamicPropertyGroup->m_displayName.empty())
  549. {
  550. dynamicPropertyGroup->m_displayName =
  551. !propertyGroup->GetDisplayName().empty() ? propertyGroup->GetDisplayName() : propertyGroup->GetName();
  552. }
  553. dynamicPropertyGroup->m_description = propertyGroup->GetDescription();
  554. if (dynamicPropertyGroup->m_description.empty())
  555. {
  556. dynamicPropertyGroup->m_description = dynamicPropertyGroup->m_displayName;
  557. }
  558. // All of the material type properties must be adapted for display in the ui. This is done by converting them into a dynamic
  559. // property class that can be used to display and edit multiple types.
  560. for (const auto& propertyDefinition : propertyGroup->GetProperties())
  561. {
  562. AtomToolsFramework::DynamicPropertyConfig propertyConfig;
  563. // The property ID must be set up before calling the function to convert the rest of the material type property
  564. // definition into the dynamic property config. The dynamic property config will set up a description that includes the
  565. // ID.
  566. propertyConfig.m_id = propertyDefinition->GetName();
  567. groupNameContext.ContextualizeProperty(propertyConfig.m_id);
  568. // A valid property index is required to look up property values in the material type and material asset property vectors.
  569. const auto& propertyIndex = m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id);
  570. const bool propertyIndexInBounds =
  571. propertyIndex.IsValid() && propertyIndex.GetIndex() < m_materialAsset->GetPropertyValues().size();
  572. AZ_Warning(
  573. "MaterialDocument",
  574. propertyIndexInBounds,
  575. "Failed to add material property '%s' to document '%s'.",
  576. propertyConfig.m_id.GetCStr(),
  577. m_absolutePath.c_str());
  578. if (propertyIndexInBounds)
  579. {
  580. // Utility function converts most attributes from the property definition into a dynamic property config.
  581. AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, *propertyDefinition);
  582. // The utility function assigns a description from the property definition along with its name and display name.
  583. // This will be displayed as the tooltip when dragging over the property in the inspector UI. The description is
  584. // extended here so that the tooltip will display an image and additional information about the indicator that
  585. // appears when properties are modified. The tooltip will automatically interpret the embedded HTML and display the
  586. // image and formatting.
  587. propertyConfig.m_description +=
  588. "\n\n<img src=\':/Icons/changed_property.svg\'> An indicator icon will be shown to the left of properties with "
  589. "overridden values that are different from the parent material, or material type if there is no parent.\n";
  590. // The dynamic property uses the group name and display name to forward as attributes to the RPE and property asset
  591. // control. The control will then use the attributes to display a context sensitive title when opening the asset
  592. // picker for textures and other assets. Rather than using strings, this data could also be specified using
  593. // AZStd::function.
  594. propertyConfig.m_groupName = dynamicPropertyGroup->m_name;
  595. propertyConfig.m_groupDisplayName = dynamicPropertyGroup->m_displayName;
  596. // Enabling thumbnails will display a preview image next to an asset property in the RPE, if one is available.
  597. propertyConfig.m_showThumbnail = true;
  598. // Multiple values are recorded for the property, including the original value, default value, and parent value.
  599. // These values are compared against each other to determine if an indicator needs to be displayed in the property
  600. // inspector as well as which values get saved with the material.
  601. propertyConfig.m_originalValue =
  602. AtomToolsFramework::ConvertToEditableType(m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]);
  603. propertyConfig.m_parentValue =
  604. AtomToolsFramework::ConvertToEditableType(parentPropertyValues[propertyIndex.GetIndex()]);
  605. // The data change callback is invoked whenever the properties are modified in the inspector. The changes will be
  606. // stored in the dynamic property automatically but need to be processed and applied to the material instance that's
  607. // displayed in the viewport. This is also necessary to update and rerun functors.
  608. propertyConfig.m_dataChangeCallback = [documentId = m_id, propertyId = propertyConfig.m_id](const AZStd::any& value)
  609. {
  610. MaterialDocumentRequestBus::Event(
  611. documentId, &MaterialDocumentRequestBus::Events::SetPropertyValue, propertyId.GetStringView(), value);
  612. return AZ::Edit::PropertyRefreshLevels::AttributesAndValues;
  613. };
  614. dynamicPropertyGroup->m_properties.push_back(AtomToolsFramework::DynamicProperty(propertyConfig));
  615. }
  616. }
  617. // The group will not be added if no properties were added to it.
  618. if (!dynamicPropertyGroup->m_properties.empty())
  619. {
  620. m_groups.push_back(dynamicPropertyGroup);
  621. }
  622. return true;
  623. });
  624. if (!enumerateResult)
  625. {
  626. return OpenFailed();
  627. }
  628. // Add material functors that are in the top-level functors list.
  629. AZ::RPI::MaterialNameContext materialNameContext; // There is no name context for top-level functors, only functors inside PropertyGroups
  630. if (!AddEditorMaterialFunctors(m_materialTypeSourceData.m_materialFunctorSourceData, materialNameContext))
  631. {
  632. return OpenFailed();
  633. }
  634. AZ::RPI::MaterialPropertyFlags dirtyFlags;
  635. dirtyFlags.set(); // Mark all properties as dirty since we just loaded the material and need to initialize property visibility
  636. RunEditorMaterialFunctors(dirtyFlags);
  637. return OpenSucceeded();
  638. }
  639. void MaterialDocument::Clear()
  640. {
  641. AtomToolsFramework::AtomToolsDocument::Clear();
  642. AZ::SystemTickBus::Handler::BusDisconnect();
  643. m_materialAsset = {};
  644. m_materialInstance = {};
  645. m_compilePending = {};
  646. m_groups.clear();
  647. m_editorFunctors.clear();
  648. m_materialTypeSourceData = AZ::RPI::MaterialTypeSourceData();
  649. m_materialSourceData = AZ::RPI::MaterialSourceData();
  650. m_propertyValuesBeforeEdit.clear();
  651. }
  652. bool MaterialDocument::ReopenRecordState()
  653. {
  654. m_propertyValuesBeforeReopen.clear();
  655. TraverseGroups(m_groups, [this](auto& group) {
  656. for (auto& property : group->m_properties)
  657. {
  658. if (!AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_parentValue))
  659. {
  660. m_propertyValuesBeforeReopen[property.GetId()] = property.GetValue();
  661. }
  662. }
  663. return true;
  664. });
  665. return AtomToolsDocument::ReopenRecordState();
  666. }
  667. bool MaterialDocument::ReopenRestoreState()
  668. {
  669. RestorePropertyValues(m_propertyValuesBeforeReopen);
  670. m_propertyValuesBeforeReopen.clear();
  671. return AtomToolsDocument::ReopenRestoreState();
  672. }
  673. void MaterialDocument::Recompile()
  674. {
  675. if (!m_compilePending)
  676. {
  677. AZ::SystemTickBus::Handler::BusConnect();
  678. m_compilePending = true;
  679. }
  680. }
  681. bool MaterialDocument::LoadMaterialSourceData()
  682. {
  683. auto loadResult = AZ::RPI::MaterialUtils::LoadMaterialSourceData(m_absolutePath);
  684. if (!loadResult)
  685. {
  686. AZ_Error("MaterialDocument", false, "Material source data could not be loaded: '%s'.", m_absolutePath.c_str());
  687. return false;
  688. }
  689. m_materialSourceData = loadResult.TakeValue();
  690. // We always need the absolute path for the material type and parent material to load source data and resolving
  691. // relative paths when saving. This will convert and store them as absolute paths for use within the document.
  692. m_materialSourceData.m_parentMaterial =
  693. AZ::RPI::AssetUtils::ResolvePathReference(m_absolutePath, m_materialSourceData.m_parentMaterial);
  694. m_materialSourceData.m_materialType =
  695. AZ::RPI::AssetUtils::ResolvePathReference(m_absolutePath, m_materialSourceData.m_materialType);
  696. // If the material was previously saved with a reference to a material pipeline generated material type in the intermediate asset
  697. // folder, attempt to redirect to the original source material type.
  698. m_materialSourceData.m_materialType =
  699. AZ::RPI::MaterialUtils::PredictOriginalMaterialTypeSourcePath(m_materialSourceData.m_materialType);
  700. // Load the material type source data which provides the layout and default values of all of the properties
  701. auto materialTypeOutcome = AZ::RPI::MaterialUtils::LoadMaterialTypeSourceData(m_materialSourceData.m_materialType);
  702. if (!materialTypeOutcome.IsSuccess())
  703. {
  704. AZ_Error("MaterialDocument", false, "Material type source data could not be loaded: '%s'.", m_materialSourceData.m_materialType.c_str());
  705. return false;
  706. }
  707. m_materialTypeSourceData = materialTypeOutcome.TakeValue();
  708. return true;
  709. }
  710. bool MaterialDocument::LoadMaterialTypeSourceData()
  711. {
  712. // A material document can be created or loaded from material or material type source data. If we are attempting to load
  713. // material type source data then the material source data object can be created just by referencing the document path as the
  714. // material type path.
  715. auto materialTypeOutcome = AZ::RPI::MaterialUtils::LoadMaterialTypeSourceData(m_absolutePath);
  716. if (!materialTypeOutcome.IsSuccess())
  717. {
  718. AZ_Error("MaterialDocument", false, "Material type source data could not be loaded: '%s'.", m_absolutePath.c_str());
  719. return false;
  720. }
  721. m_materialTypeSourceData = materialTypeOutcome.TakeValue();
  722. // We are storing absolute paths in the loaded version of the source data so that the files can be resolved at all times.
  723. m_materialSourceData.m_materialType = m_absolutePath;
  724. m_materialSourceData.m_parentMaterial.clear();
  725. return true;
  726. }
  727. void MaterialDocument::RestorePropertyValues(const PropertyValueMap& propertyValues)
  728. {
  729. for (const auto& propertyValuePair : propertyValues)
  730. {
  731. const auto& propertyName = propertyValuePair.first;
  732. const auto& propertyValue = propertyValuePair.second;
  733. SetPropertyValue(propertyName.GetStringView(), propertyValue);
  734. }
  735. }
  736. bool MaterialDocument::AddEditorMaterialFunctors(
  737. const AZStd::vector<AZ::RPI::Ptr<AZ::RPI::MaterialFunctorSourceDataHolder>>& functorSourceDataHolders,
  738. const AZ::RPI::MaterialNameContext& nameContext)
  739. {
  740. const AZ::RPI::MaterialFunctorSourceData::EditorContext editorContext = AZ::RPI::MaterialFunctorSourceData::EditorContext(
  741. m_materialSourceData.m_materialType, m_materialAsset->GetMaterialPropertiesLayout(), &nameContext);
  742. for (AZ::RPI::Ptr<AZ::RPI::MaterialFunctorSourceDataHolder> functorData : functorSourceDataHolders)
  743. {
  744. AZ::RPI::MaterialFunctorSourceData::FunctorResult result = functorData->CreateFunctor(editorContext);
  745. if (result.IsSuccess())
  746. {
  747. AZ::RPI::Ptr<AZ::RPI::MaterialFunctor>& functor = result.GetValue();
  748. if (functor != nullptr)
  749. {
  750. m_editorFunctors.push_back(functor);
  751. }
  752. }
  753. else
  754. {
  755. AZ_Error("MaterialDocument", false, "Material functors were not created: '%s'.", m_absolutePath.c_str());
  756. return false;
  757. }
  758. }
  759. return true;
  760. }
  761. void MaterialDocument::RunEditorMaterialFunctors(AZ::RPI::MaterialPropertyFlags dirtyFlags)
  762. {
  763. if (!m_materialInstance)
  764. {
  765. return;
  766. }
  767. AZStd::unordered_map<AZ::Name, AZ::RPI::MaterialPropertyDynamicMetadata> propertyDynamicMetadata;
  768. AZStd::unordered_map<AZ::Name, AZ::RPI::MaterialPropertyGroupDynamicMetadata> propertyGroupDynamicMetadata;
  769. TraverseGroups(m_groups, [&](auto& group) {
  770. AZ::RPI::MaterialPropertyGroupDynamicMetadata& metadata = propertyGroupDynamicMetadata[AZ::Name{ group->m_name }];
  771. metadata.m_visibility = group->m_visible ? AZ::RPI::MaterialPropertyGroupVisibility::Enabled : AZ::RPI::MaterialPropertyGroupVisibility::Hidden;
  772. for (auto& property : group->m_properties)
  773. {
  774. AtomToolsFramework::ConvertToPropertyMetaData(propertyDynamicMetadata[property.GetId()], property.GetConfig());
  775. }
  776. return true;
  777. });
  778. AZStd::unordered_set<AZ::Name> updatedProperties;
  779. AZStd::unordered_set<AZ::Name> updatedPropertyGroups;
  780. for (AZ::RPI::Ptr<AZ::RPI::MaterialFunctor>& functor : m_editorFunctors)
  781. {
  782. const AZ::RPI::MaterialPropertyFlags& materialPropertyDependencies = functor->GetMaterialPropertyDependencies();
  783. // None also covers case that the client code doesn't register material properties to dependencies,
  784. // which will later get caught in Process() when trying to access a property.
  785. if (materialPropertyDependencies.none() || functor->NeedsProcess(dirtyFlags))
  786. {
  787. AZ::RPI::MaterialFunctorAPI::EditorContext context = AZ::RPI::MaterialFunctorAPI::EditorContext(
  788. m_materialInstance->GetPropertyCollection(), propertyDynamicMetadata,
  789. propertyGroupDynamicMetadata, updatedProperties, updatedPropertyGroups,
  790. &materialPropertyDependencies);
  791. functor->Process(context);
  792. }
  793. }
  794. TraverseGroups(m_groups, [&](auto& group) {
  795. bool groupChange = false;
  796. bool groupRebuilt = false;
  797. if (updatedPropertyGroups.find(AZ::Name(group->m_name)) != updatedPropertyGroups.end())
  798. {
  799. AZ::RPI::MaterialPropertyGroupDynamicMetadata& metadata = propertyGroupDynamicMetadata[AZ::Name{ group->m_name }];
  800. group->m_visible = metadata.m_visibility != AZ::RPI::MaterialPropertyGroupVisibility::Hidden;
  801. groupChange = true;
  802. }
  803. for (auto& property : group->m_properties)
  804. {
  805. if (updatedProperties.find(AZ::Name(property.GetId())) != updatedProperties.end())
  806. {
  807. const bool visibleBefore = property.GetConfig().m_visible;
  808. AtomToolsFramework::DynamicPropertyConfig propertyConfig = property.GetConfig();
  809. AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDynamicMetadata[property.GetId()]);
  810. property.SetConfig(propertyConfig);
  811. groupChange = true;
  812. groupRebuilt |= visibleBefore != property.GetConfig().m_visible;
  813. }
  814. }
  815. if (groupChange || groupRebuilt)
  816. {
  817. AtomToolsFramework::AtomToolsDocumentNotificationBus::Event(
  818. m_toolId, &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentObjectInfoChanged, m_id,
  819. GetObjectInfoFromDynamicPropertyGroup(group.get()), groupRebuilt);
  820. }
  821. return true;
  822. });
  823. }
  824. AtomToolsFramework::DocumentObjectInfo MaterialDocument::GetObjectInfoFromDynamicPropertyGroup(
  825. const AtomToolsFramework::DynamicPropertyGroup* group) const
  826. {
  827. AtomToolsFramework::DocumentObjectInfo objectInfo;
  828. objectInfo.m_visible = group->m_visible;
  829. objectInfo.m_name = group->m_name;
  830. objectInfo.m_displayName = group->m_displayName;
  831. objectInfo.m_description = group->m_description;
  832. objectInfo.m_objectType = azrtti_typeid<AtomToolsFramework::DynamicPropertyGroup>();
  833. objectInfo.m_objectPtr = const_cast<AtomToolsFramework::DynamicPropertyGroup*>(group);
  834. if (group->m_name == "overview")
  835. {
  836. // Properties in the overview category don't require special comparison or indicator icons. However, the blank icon is still
  837. // needed to keep everything aligned.
  838. objectInfo.m_nodeIndicatorFunction = []([[maybe_unused]] const AzToolsFramework::InstanceDataNode* node)
  839. {
  840. return ":/Icons/blank.png";
  841. };
  842. }
  843. else
  844. {
  845. objectInfo.m_nodeIndicatorFunction = [](const AzToolsFramework::InstanceDataNode* node)
  846. {
  847. const auto property = AtomToolsFramework::FindAncestorInstanceDataNodeByType<AtomToolsFramework::DynamicProperty>(node);
  848. return property && !AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_parentValue)
  849. ? ":/Icons/changed_property.svg"
  850. : ":/Icons/blank.png";
  851. };
  852. }
  853. return objectInfo;
  854. }
  855. bool MaterialDocument::TraverseGroups(
  856. AZStd::vector<AZStd::shared_ptr<AtomToolsFramework::DynamicPropertyGroup>>& groups,
  857. AZStd::function<bool(AZStd::shared_ptr<AtomToolsFramework::DynamicPropertyGroup>&)> callback)
  858. {
  859. if (!callback)
  860. {
  861. return false;
  862. }
  863. for (auto& group : groups)
  864. {
  865. if (!callback(group) || !TraverseGroups(group->m_groups, callback))
  866. {
  867. return false;
  868. }
  869. }
  870. return true;
  871. }
  872. bool MaterialDocument::TraverseGroups(
  873. const AZStd::vector<AZStd::shared_ptr<AtomToolsFramework::DynamicPropertyGroup>>& groups,
  874. AZStd::function<bool(const AZStd::shared_ptr<AtomToolsFramework::DynamicPropertyGroup>&)> callback) const
  875. {
  876. if (!callback)
  877. {
  878. return false;
  879. }
  880. for (auto& group : groups)
  881. {
  882. if (!callback(group) || !TraverseGroups(group->m_groups, callback))
  883. {
  884. return false;
  885. }
  886. }
  887. return true;
  888. }
  889. AtomToolsFramework::DynamicProperty* MaterialDocument::FindProperty(const AZ::Name& propertyId)
  890. {
  891. AtomToolsFramework::DynamicProperty* result = nullptr;
  892. TraverseGroups(m_groups, [&](auto& group) {
  893. for (auto& property : group->m_properties)
  894. {
  895. if (property.GetId() == propertyId)
  896. {
  897. result = &property;
  898. return false;
  899. }
  900. }
  901. return true;
  902. });
  903. return result;
  904. }
  905. const AtomToolsFramework::DynamicProperty* MaterialDocument::FindProperty(const AZ::Name& propertyId) const
  906. {
  907. AtomToolsFramework::DynamicProperty* result = nullptr;
  908. TraverseGroups(m_groups, [&](auto& group) {
  909. for (auto& property : group->m_properties)
  910. {
  911. if (property.GetId() == propertyId)
  912. {
  913. result = &property;
  914. return false;
  915. }
  916. }
  917. return true;
  918. });
  919. return result;
  920. }
  921. } // namespace MaterialEditor