EditorMaterialComponentInspector.cpp 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  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 <Material/EditorMaterialComponentInspector.h>
  9. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  10. #include <Atom/RPI.Edit/Common/JsonUtils.h>
  11. #include <Atom/RPI.Edit/Material/MaterialFunctorSourceData.h>
  12. #include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
  13. #include <Atom/RPI.Edit/Material/MaterialUtils.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 <AtomLyIntegration/CommonFeatures/Material/EditorMaterialSystemComponentRequestBus.h>
  18. #include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h>
  19. #include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentConfig.h>
  20. #include <AtomToolsFramework/Inspector/InspectorPropertyGroupWidget.h>
  21. #include <AtomToolsFramework/Util/MaterialPropertyUtil.h>
  22. #include <AtomToolsFramework/Util/Util.h>
  23. #include <AzCore/Utils/Utils.h>
  24. #include <AzFramework/API/ApplicationAPI.h>
  25. #include <AzQtComponents/Components/Widgets/Text.h>
  26. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  27. #include <AzToolsFramework/API/EditorWindowRequestBus.h>
  28. #include <AzToolsFramework/API/ToolsApplicationAPI.h>
  29. AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
  30. #include <QApplication>
  31. #include <QFileInfo>
  32. #include <QLabel>
  33. #include <QMenu>
  34. #include <QToolButton>
  35. #include <QToolTip>
  36. #include <QWidget>
  37. AZ_POP_DISABLE_WARNING
  38. namespace AZ
  39. {
  40. namespace Render
  41. {
  42. namespace EditorMaterialComponentInspector
  43. {
  44. MaterialPropertyInspector::MaterialPropertyInspector(QWidget* parent)
  45. : AtomToolsFramework::InspectorWidget(parent)
  46. {
  47. CreateHeading();
  48. AZ::SystemTickBus::Handler::BusConnect();
  49. AZ::EntitySystemBus::Handler::BusConnect();
  50. EditorMaterialSystemComponentNotificationBus::Handler::BusConnect();
  51. }
  52. MaterialPropertyInspector::~MaterialPropertyInspector()
  53. {
  54. AZ::SystemTickBus::Handler::BusDisconnect();
  55. AZ::EntitySystemBus::Handler::BusDisconnect();
  56. EditorMaterialSystemComponentNotificationBus::Handler::BusDisconnect();
  57. MaterialComponentNotificationBus::MultiHandler::BusDisconnect();
  58. }
  59. bool MaterialPropertyInspector::LoadMaterial(
  60. const AZ::EntityId& primaryEntityId,
  61. const AzToolsFramework::EntityIdSet& entityIdsToEdit,
  62. const AZ::Render::MaterialAssignmentId& materialAssignmentId)
  63. {
  64. UnloadMaterial();
  65. // Only allow the load to succeed if all of the affected entities have matching material types to guarantee that the
  66. // inspector configuration matches all of the entities
  67. if (!EditorMaterialComponentUtil::DoEntitiesHaveMatchingMaterialTypes(primaryEntityId, entityIdsToEdit, materialAssignmentId))
  68. {
  69. UnloadMaterial();
  70. return false;
  71. }
  72. m_primaryEntityId = primaryEntityId;
  73. m_entityIdsToEdit = entityIdsToEdit;
  74. m_materialAssignmentId = materialAssignmentId;
  75. // Connect all of the affected entities to the material component notification bus so that the UI can be updated or
  76. // invalidated whenever any of their configurations change in a way that may not be compatible with the other entities
  77. MaterialComponentNotificationBus::MultiHandler::BusDisconnect();
  78. MaterialComponentNotificationBus::MultiHandler::BusConnect(m_primaryEntityId);
  79. for (const AZ::EntityId& entityId : m_entityIdsToEdit)
  80. {
  81. MaterialComponentNotificationBus::MultiHandler::BusConnect(entityId);
  82. }
  83. const AZ::Data::AssetId materialAssetId = GetActiveMaterialAssetIdFromEntity();
  84. if (!materialAssetId.IsValid())
  85. {
  86. UnloadMaterial();
  87. return false;
  88. }
  89. if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(materialAssetId, m_editData))
  90. {
  91. AZ_Warning("AZ::Render::EditorMaterialComponentInspector", false, "Failed to load material data.");
  92. UnloadMaterial();
  93. return false;
  94. }
  95. // The material instance is still needed for functor execution
  96. m_materialInstance = AZ::RPI::Material::Create(m_editData.m_materialAsset);
  97. if (!m_materialInstance)
  98. {
  99. AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Material instance could not be created.");
  100. UnloadMaterial();
  101. return false;
  102. }
  103. // Add material functors that are in the top-level functors list. Other functors are also added per-property-group elsewhere.
  104. AddEditorMaterialFunctors(m_editData.m_materialTypeSourceData.m_materialFunctorSourceData, AZ::RPI::MaterialNameContext{});
  105. Populate();
  106. LoadOverridesFromEntity();
  107. return true;
  108. }
  109. void MaterialPropertyInspector::UnloadMaterial()
  110. {
  111. Reset();
  112. m_editData = EditorMaterialComponentUtil::MaterialEditData();
  113. m_materialInstance = {};
  114. m_dirtyPropertyFlags.set();
  115. m_editorFunctors = {};
  116. m_internalEditNotification = {};
  117. m_updateUI = {};
  118. m_updatePreview = {};
  119. UpdateHeading();
  120. }
  121. bool MaterialPropertyInspector::IsLoaded() const
  122. {
  123. // The inspector only has a valid configuration if the entity ID, material assignment ID, and material asset are all valid
  124. // and match what is on the selected entity. If there is a mismatch, the content must be reloaded.
  125. const AZ::Data::AssetId materialAssetId = GetActiveMaterialAssetIdFromEntity();
  126. return m_primaryEntityId.IsValid() && m_materialInstance && m_editData.m_materialAsset.IsReady() &&
  127. m_editData.m_materialAsset.GetId() == materialAssetId && m_editData.m_materialAssetId == materialAssetId &&
  128. EditorMaterialComponentUtil::DoEntitiesHaveMatchingMaterialTypes(m_primaryEntityId, m_entityIdsToEdit, m_materialAssignmentId);
  129. }
  130. void MaterialPropertyInspector::Reset()
  131. {
  132. m_groups = {};
  133. m_dirtyPropertyFlags.set();
  134. m_internalEditNotification = {};
  135. AtomToolsFramework::InspectorWidget::Reset();
  136. }
  137. void MaterialPropertyInspector::CreateHeading()
  138. {
  139. // Create the menu button
  140. QToolButton* menuButton = new QToolButton(this);
  141. menuButton->setAutoRaise(true);
  142. menuButton->setIcon(QIcon(":/Cards/img/UI20/Cards/menu_ico.svg"));
  143. menuButton->setVisible(true);
  144. QObject::connect(menuButton, &QToolButton::clicked, this, [this]() { OpenMenu(); });
  145. AddHeading(menuButton);
  146. m_overviewImage = new QLabel(this);
  147. m_overviewImage->setFixedSize(QSize(120, 120));
  148. m_overviewImage->setScaledContents(true);
  149. m_overviewImage->setVisible(false);
  150. m_overviewText = new QLabel(this);
  151. QSizePolicy sizePolicy1(QSizePolicy::Ignored, QSizePolicy::Preferred);
  152. sizePolicy1.setHorizontalStretch(0);
  153. sizePolicy1.setVerticalStretch(0);
  154. sizePolicy1.setHeightForWidth(m_overviewText->sizePolicy().hasHeightForWidth());
  155. m_overviewText->setSizePolicy(sizePolicy1);
  156. m_overviewText->setMinimumSize(QSize(0, 0));
  157. m_overviewText->setMaximumSize(QSize(16777215, 16777215));
  158. m_overviewText->setTextFormat(Qt::AutoText);
  159. m_overviewText->setScaledContents(false);
  160. m_overviewText->setWordWrap(true);
  161. m_overviewText->setVisible(true);
  162. auto overviewContainer = new QWidget(this);
  163. overviewContainer->setLayout(new QHBoxLayout());
  164. overviewContainer->layout()->addWidget(m_overviewImage);
  165. overviewContainer->layout()->addWidget(m_overviewText);
  166. AddHeading(overviewContainer);
  167. }
  168. void MaterialPropertyInspector::UpdateHeading()
  169. {
  170. if (!IsLoaded())
  171. {
  172. if (m_entityIdsToEdit.size() > 1)
  173. {
  174. m_overviewText->setText(tr("The selected entities and materials cannot be edited.\n"
  175. "Multiple entities and materials have been selected for editing.\n"
  176. "All of the selected entities must be valid, active, and have a material component.\n"
  177. "Each material component must provide the selected material slot.\n"
  178. "The active material on each slot must have the same material type."));
  179. }
  180. else
  181. {
  182. m_overviewText->setText(tr("The selected entities and materials cannot be edited.\n"
  183. "The selected entity must be valid, active, and have a material component.\n"
  184. "The material component must provide the selected material slot."));
  185. }
  186. m_overviewText->setAlignment(Qt::AlignCenter);
  187. m_overviewImage->setVisible(false);
  188. return;
  189. }
  190. AZStd::string entityName;
  191. AZ::ComponentApplicationBus::BroadcastResult(
  192. entityName, &AZ::ComponentApplicationBus::Events::GetEntityName, m_primaryEntityId);
  193. AZStd::string slotName;
  194. MaterialComponentRequestBus::EventResult(
  195. slotName, m_primaryEntityId, &MaterialComponentRequestBus::Events::GetMaterialLabel, m_materialAssignmentId);
  196. QString materialInfo;
  197. materialInfo += tr("<table>");
  198. materialInfo += tr("<tr><td><b>Entity Name&emsp;</b></td><td>%1</td></tr>").arg(entityName.c_str());
  199. materialInfo += tr("<tr><td><b>Entity Count&emsp;</b></td><td>%1</td></tr>").arg(m_entityIdsToEdit.size());
  200. materialInfo += tr("<tr><td><b>Material Slot Name&emsp;</b></td><td>%1</td></tr>").arg(slotName.c_str());
  201. materialInfo += m_materialAssignmentId.IsDefault() || m_materialAssignmentId.IsSlotIdOnly()
  202. ? tr("<tr><td><b>Material Slot LOD&emsp;</b></td><td>%1</td></tr>").arg(-1)
  203. : tr("<tr><td><b>Material Slot LOD&emsp;</b></td><td>%1</td></tr>").arg(m_materialAssignmentId.m_lodIndex);
  204. if (m_editData.m_materialAsset.GetId().IsValid())
  205. {
  206. AZ::Data::AssetInfo assetInfo;
  207. AZ::Data::AssetCatalogRequestBus::BroadcastResult(
  208. assetInfo, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, m_editData.m_materialAsset.GetId());
  209. materialInfo += tr("<tr><td><b>Material Asset&emsp;</b></td><td>%1</td></tr>").arg(assetInfo.m_relativePath.c_str());
  210. }
  211. if (!m_editData.m_materialSourcePath.empty())
  212. {
  213. // Inserting links that will be used to open the material/type in the material editor
  214. const auto& materialSourceFileName = GetFileName(m_editData.m_materialSourcePath);
  215. if (IsSourceMaterial(m_editData.m_materialSourcePath))
  216. {
  217. materialInfo += tr("<tr><td><b>Material Source&emsp;</b></td><td><a href=\"%1\">%2</a></td></tr>")
  218. .arg(m_editData.m_materialSourcePath.c_str())
  219. .arg(materialSourceFileName.c_str());
  220. }
  221. else
  222. {
  223. // Materials that come from other sources like FBX files will not have the link
  224. materialInfo += tr("<tr><td><b>Material Source&emsp;</b></td><td>%1</td></tr>")
  225. .arg(materialSourceFileName.c_str());
  226. }
  227. }
  228. if (IsSourceMaterial(m_editData.m_materialParentSourcePath))
  229. {
  230. // Inserting links that will be used to open the material/type in the material editor
  231. const auto& materialParentSourceFileName = GetFileName(m_editData.m_materialParentSourcePath);
  232. materialInfo += tr("<tr><td><b>Material Parent&emsp;</b></td><td><a href=\"%1\">%2</a></td></tr>")
  233. .arg(m_editData.m_materialParentSourcePath.c_str())
  234. .arg(materialParentSourceFileName.c_str());
  235. }
  236. if (!m_editData.m_originalMaterialTypeSourcePath.empty())
  237. {
  238. // Inserting links that will be used to open the material/type in the material editor
  239. const auto& materialTypeSourceFileName = GetFileName(m_editData.m_originalMaterialTypeSourcePath);
  240. materialInfo += tr("<tr><td><b>Material Type&emsp;</b></td><td><a href=\"%1\">%2</a></td></tr>")
  241. .arg(m_editData.m_originalMaterialTypeSourcePath.c_str())
  242. .arg(materialTypeSourceFileName.c_str());
  243. }
  244. materialInfo += tr("</table>");
  245. m_overviewText->setText(materialInfo);
  246. m_overviewText->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop);
  247. m_overviewText->setOpenExternalLinks(false);
  248. connect(m_overviewText, &QLabel::linkActivated, this, [](const QString& link) {
  249. EditorMaterialSystemComponentRequestBus::Broadcast(
  250. &EditorMaterialSystemComponentRequestBus::Events::OpenMaterialEditor, link.toUtf8().constData());
  251. });
  252. connect(m_overviewText, &QLabel::linkHovered, this, [](const QString& link) {
  253. QToolTip::showText(QCursor::pos(), link);
  254. });
  255. // Update the overview image with the last rendered preview of the primary entity's material.
  256. QPixmap pixmap;
  257. EditorMaterialSystemComponentRequestBus::BroadcastResult(
  258. pixmap, &EditorMaterialSystemComponentRequestBus::Events::GetRenderedMaterialPreview, m_primaryEntityId,
  259. m_materialAssignmentId);
  260. m_overviewImage->setPixmap(pixmap);
  261. // If more than one entity is selected for editing in this inspector then the image will be hidden.
  262. // This will eliminate any confusion if editing multiple materials and they do not hold match the primary entities preview.
  263. m_overviewImage->setVisible(m_entityIdsToEdit.size() == 1);
  264. // If the image was not found then request that the preview be updated again at a later time
  265. m_updatePreview |= pixmap.isNull();
  266. }
  267. void MaterialPropertyInspector::AddUvNamesGroup()
  268. {
  269. const AZStd::string groupName = AZ::RPI::UvGroupName;
  270. const AZStd::string groupDisplayName = "UV Sets";
  271. const AZStd::string groupDescription = "UV set names in this material, which can be renamed to match those in the model.";
  272. auto& group = m_groups[groupName];
  273. const RPI::MaterialUvNameMap& uvNameMap = m_editData.m_materialAsset->GetMaterialTypeAsset()->GetUvNameMap();
  274. group.m_properties.reserve(uvNameMap.size());
  275. for (const RPI::UvNamePair& uvNamePair : uvNameMap)
  276. {
  277. AtomToolsFramework::DynamicPropertyConfig propertyConfig;
  278. const AZStd::string shaderInputStr = uvNamePair.m_shaderInput.ToString();
  279. const AZStd::string uvName = uvNamePair.m_uvName.GetStringView();
  280. propertyConfig = {};
  281. propertyConfig.m_id = AZ::RPI::MaterialPropertyId(groupName, shaderInputStr).GetCStr();
  282. propertyConfig.m_name = shaderInputStr;
  283. propertyConfig.m_displayName = shaderInputStr;
  284. propertyConfig.m_groupName = groupDisplayName;
  285. propertyConfig.m_description = shaderInputStr;
  286. propertyConfig.m_defaultValue = uvName;
  287. propertyConfig.m_originalValue = uvName;
  288. propertyConfig.m_parentValue = uvName;
  289. propertyConfig.m_readOnly = true;
  290. group.m_properties.emplace_back(propertyConfig);
  291. }
  292. // Passing in same group as main and comparison instance to enable custom value comparison for highlighting modified properties
  293. auto propertyGroupWidget = new AtomToolsFramework::InspectorPropertyGroupWidget(
  294. &group, &group, group.TYPEINFO_Uuid(), this, this, GetGroupSaveStateKey(groupName), {},
  295. [this](const auto node) { return GetInstanceNodePropertyIndicator(node); }, 0);
  296. AddGroup(groupName, groupDisplayName, groupDescription, propertyGroupWidget);
  297. }
  298. void MaterialPropertyInspector::AddPropertiesGroup()
  299. {
  300. // Copy all of the properties from the material asset to the populate the inspector
  301. m_editData.m_materialTypeSourceData.EnumeratePropertyGroups(
  302. [this](const AZ::RPI::MaterialTypeSourceData::PropertyGroupStack& propertyGroupStack)
  303. {
  304. using namespace AZ::RPI;
  305. const MaterialTypeSourceData::PropertyGroup* propertyGroupDefinition = propertyGroupStack.back();
  306. MaterialNameContext groupNameContext = MaterialTypeSourceData::MakeMaterialNameContext(propertyGroupStack);
  307. AddEditorMaterialFunctors(propertyGroupDefinition->GetFunctors(), groupNameContext);
  308. AZStd::vector<AZStd::string> groupNameVector;
  309. AZStd::vector<AZStd::string> groupDisplayNameVector;
  310. groupNameVector.reserve(propertyGroupStack.size());
  311. groupDisplayNameVector.reserve(propertyGroupStack.size());
  312. for (auto& nextGroup : propertyGroupStack)
  313. {
  314. groupNameVector.push_back(nextGroup->GetName());
  315. groupDisplayNameVector.push_back(!nextGroup->GetDisplayName().empty() ? nextGroup->GetDisplayName() : nextGroup->GetName());
  316. }
  317. AZStd::string groupId;
  318. AzFramework::StringFunc::Join(groupId, groupNameVector.begin(), groupNameVector.end(), ".");
  319. auto& group = m_groups[groupId];
  320. group.m_name = groupId;
  321. AzFramework::StringFunc::Join(group.m_displayName, groupDisplayNameVector.begin(), groupDisplayNameVector.end(), " | ");
  322. group.m_description = !propertyGroupDefinition->GetDescription().empty() ? propertyGroupDefinition->GetDescription() : group.m_displayName;
  323. group.m_properties.reserve(propertyGroupDefinition->GetProperties().size());
  324. for (const auto& propertyDefinition : propertyGroupDefinition->GetProperties())
  325. {
  326. AtomToolsFramework::DynamicPropertyConfig propertyConfig;
  327. // Assign id before conversion so it can be used in dynamic description
  328. propertyConfig.m_id = propertyDefinition->GetName();
  329. groupNameContext.ContextualizeProperty(propertyConfig.m_id);
  330. AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, *propertyDefinition);
  331. propertyConfig.m_description +=
  332. "\n\n<img src=\':/Icons/changed_property.svg\'> An indicator icon will be shown to the left of properties "
  333. "with overridden values that are different from the assigned material.\n";
  334. const auto& propertyIndex =
  335. m_editData.m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id);
  336. // (Does DynamicPropertyConfig really even need m_groupName? It doesn't seem to be used anywhere)
  337. propertyConfig.m_groupName = group.m_name;
  338. propertyConfig.m_groupDisplayName = group.m_displayName;
  339. propertyConfig.m_showThumbnail = true;
  340. propertyConfig.m_defaultValue = AtomToolsFramework::ConvertToEditableType(
  341. m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]);
  342. // There is no explicit parent material here. Material instance property overrides replace the values from the
  343. // assigned material asset. Its values should be treated as parent, for comparison, in this case.
  344. propertyConfig.m_parentValue = AtomToolsFramework::ConvertToEditableType(
  345. m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]);
  346. propertyConfig.m_originalValue = AtomToolsFramework::ConvertToEditableType(
  347. m_editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]);
  348. group.m_properties.emplace_back(propertyConfig);
  349. }
  350. // Passing in same group as main and comparison instance to enable custom value comparison for highlighting modified properties
  351. auto propertyGroupWidget = new AtomToolsFramework::InspectorPropertyGroupWidget(
  352. &group, &group, group.TYPEINFO_Uuid(), this, this, GetGroupSaveStateKey(group.m_name), {},
  353. [this](const auto node) { return GetInstanceNodePropertyIndicator(node); }, 0);
  354. AddGroup(group.m_name, group.m_displayName, group.m_description, propertyGroupWidget);
  355. return true;
  356. });
  357. }
  358. void MaterialPropertyInspector::Populate()
  359. {
  360. AddGroupsBegin();
  361. AddUvNamesGroup();
  362. AddPropertiesGroup();
  363. AddGroupsEnd();
  364. }
  365. void MaterialPropertyInspector::LoadOverridesFromEntity()
  366. {
  367. if (!IsLoaded())
  368. {
  369. return;
  370. }
  371. m_editData.m_materialPropertyOverrideMap.clear();
  372. MaterialComponentRequestBus::EventResult(
  373. m_editData.m_materialPropertyOverrideMap, m_primaryEntityId, &MaterialComponentRequestBus::Events::GetPropertyValues,
  374. m_materialAssignmentId);
  375. // Apply any automatic property renames so that the material inspector will be properly initialized with the right values
  376. // for properties that have new names.
  377. {
  378. AZStd::vector<AZStd::pair<Name, Name>> renamedProperties;
  379. for (auto& propertyOverridePair : m_editData.m_materialPropertyOverrideMap)
  380. {
  381. Name name = propertyOverridePair.first;
  382. if (m_materialInstance->GetAsset()->GetMaterialTypeAsset()->ApplyPropertyRenames(name))
  383. {
  384. renamedProperties.emplace_back(propertyOverridePair.first, name);
  385. }
  386. }
  387. for (const auto& [oldName, newName] : renamedProperties)
  388. {
  389. m_editData.m_materialPropertyOverrideMap[newName] = m_editData.m_materialPropertyOverrideMap[oldName];
  390. m_editData.m_materialPropertyOverrideMap.erase(oldName);
  391. }
  392. }
  393. for (auto& group : m_groups)
  394. {
  395. for (auto& property : group.second.m_properties)
  396. {
  397. const AtomToolsFramework::DynamicPropertyConfig& propertyConfig = property.GetConfig();
  398. const auto overrideItr = m_editData.m_materialPropertyOverrideMap.find(propertyConfig.m_id);
  399. const auto& editValue = overrideItr != m_editData.m_materialPropertyOverrideMap.end() ? overrideItr->second : propertyConfig.m_originalValue;
  400. // This first converts to an acceptable runtime type in case the value came from script
  401. const auto propertyIndex = m_materialInstance->FindPropertyIndex(property.GetId());
  402. if (propertyIndex.IsValid())
  403. {
  404. const auto runtimeValue = AtomToolsFramework::ConvertToRuntimeType(editValue);
  405. if (runtimeValue.IsValid())
  406. {
  407. property.SetValue(AtomToolsFramework::ConvertToEditableType(runtimeValue));
  408. }
  409. }
  410. else
  411. {
  412. property.SetValue(editValue);
  413. }
  414. UpdateMaterialInstanceProperty(property);
  415. }
  416. }
  417. m_dirtyPropertyFlags.set();
  418. RunEditorMaterialFunctors();
  419. RebuildAll();
  420. UpdateHeading();
  421. }
  422. void MaterialPropertyInspector::SaveOverrideToEntities(const AtomToolsFramework::DynamicProperty& property, bool commitChanges)
  423. {
  424. if (!IsLoaded())
  425. {
  426. return;
  427. }
  428. // Apply the incoming property override to all pinned entities
  429. for (const AZ::EntityId& entityId : m_entityIdsToEdit)
  430. {
  431. MaterialComponentRequestBus::Event(
  432. entityId, &MaterialComponentRequestBus::Events::SetPropertyValue, m_materialAssignmentId,
  433. property.GetId().GetStringView(), property.GetValue());
  434. }
  435. if (commitChanges)
  436. {
  437. // If editing is complete and these changes are being committed we must mark all of the entities dirty for undo redo
  438. AzToolsFramework::ScopedUndoBatch undoBatch("Material slot changed.");
  439. m_internalEditNotification = true;
  440. for (const AZ::EntityId& entityId : m_entityIdsToEdit)
  441. {
  442. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(
  443. &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entityId);
  444. MaterialComponentNotificationBus::Event(entityId, &MaterialComponentNotifications::OnMaterialsEdited);
  445. }
  446. m_internalEditNotification = false;
  447. }
  448. // m_updatePreview should be set to true here for continuous preview updates as slider/color properties change but needs
  449. // throttling
  450. }
  451. bool MaterialPropertyInspector::AddEditorMaterialFunctors(
  452. const AZStd::vector<AZ::RPI::Ptr<AZ::RPI::MaterialFunctorSourceDataHolder>>& functorSourceDataHolders,
  453. const AZ::RPI::MaterialNameContext& nameContext)
  454. {
  455. // Copied from MaterialDocument::AddEditorMaterialFunctors, should be refactored at some point
  456. const AZ::RPI::MaterialFunctorSourceData::EditorContext editorContext = AZ::RPI::MaterialFunctorSourceData::EditorContext(
  457. m_editData.m_materialTypeSourcePath, m_editData.m_materialAsset->GetMaterialPropertiesLayout(), &nameContext);
  458. for (AZ::RPI::Ptr<AZ::RPI::MaterialFunctorSourceDataHolder> functorData : functorSourceDataHolders)
  459. {
  460. AZ::RPI::MaterialFunctorSourceData::FunctorResult result = functorData->CreateFunctor(editorContext);
  461. if (result.IsSuccess())
  462. {
  463. AZ::RPI::Ptr<AZ::RPI::MaterialFunctor>& functor = result.GetValue();
  464. if (functor != nullptr)
  465. {
  466. m_editorFunctors.push_back(functor);
  467. }
  468. }
  469. else
  470. {
  471. AZ_Error("MaterialDocument", false, "Material functors were not created: '%s'.", m_editData.m_materialTypeSourcePath.c_str());
  472. return false;
  473. }
  474. }
  475. return true;
  476. }
  477. void MaterialPropertyInspector::RunEditorMaterialFunctors()
  478. {
  479. if (!IsLoaded())
  480. {
  481. return;
  482. }
  483. AZStd::unordered_set<AZ::Name> changedPropertyNames;
  484. AZStd::unordered_set<AZ::Name> changedPropertyGroupNames;
  485. // Convert editor property configuration data into material property meta data so that it can be used to execute functors
  486. AZStd::unordered_map<AZ::Name, AZ::RPI::MaterialPropertyDynamicMetadata> propertyDynamicMetadata;
  487. AZStd::unordered_map<AZ::Name, AZ::RPI::MaterialPropertyGroupDynamicMetadata> propertyGroupDynamicMetadata;
  488. for (auto& groupPair : m_groups)
  489. {
  490. AZ::RPI::MaterialPropertyGroupDynamicMetadata& metadata = propertyGroupDynamicMetadata[AZ::Name{groupPair.first}];
  491. for (auto& property : groupPair.second.m_properties)
  492. {
  493. AtomToolsFramework::ConvertToPropertyMetaData(propertyDynamicMetadata[property.GetId()], property.GetConfig());
  494. }
  495. // It's significant that we check IsGroupHidden rather than IsGroupVisisble, because it follows the same rules as QWidget::isHidden().
  496. // We don't care whether the widget and all its parents are visible, we only care about whether the group was hidden within the context
  497. // of the Material Instance Editor.
  498. metadata.m_visibility = IsGroupHidden(groupPair.first) ?
  499. AZ::RPI::MaterialPropertyGroupVisibility::Hidden : AZ::RPI::MaterialPropertyGroupVisibility::Enabled;
  500. }
  501. for (AZ::RPI::Ptr<AZ::RPI::MaterialFunctor>& functor : m_editorFunctors)
  502. {
  503. const AZ::RPI::MaterialPropertyFlags& materialPropertyDependencies = functor->GetMaterialPropertyDependencies();
  504. // None also covers case that the client code doesn't register material properties to dependencies,
  505. // which will later get caught in Process() when trying to access a property.
  506. if (materialPropertyDependencies.none() || functor->NeedsProcess(m_dirtyPropertyFlags))
  507. {
  508. AZ::RPI::MaterialFunctorAPI::EditorContext context = AZ::RPI::MaterialFunctorAPI::EditorContext(
  509. m_materialInstance->GetPropertyCollection(),
  510. propertyDynamicMetadata,
  511. propertyGroupDynamicMetadata,
  512. changedPropertyNames,
  513. changedPropertyGroupNames,
  514. &materialPropertyDependencies
  515. );
  516. functor->Process(context);
  517. }
  518. }
  519. m_dirtyPropertyFlags.reset();
  520. // Apply any changes to material property meta data back to the editor property configurations
  521. for (auto& groupPair : m_groups)
  522. {
  523. AZ::Name groupName{ groupPair.first };
  524. if (changedPropertyGroupNames.find(groupName) != changedPropertyGroupNames.end())
  525. {
  526. SetGroupVisible(
  527. groupPair.first,
  528. propertyGroupDynamicMetadata[groupName].m_visibility == AZ::RPI::MaterialPropertyGroupVisibility::Enabled);
  529. }
  530. for (auto& property : groupPair.second.m_properties)
  531. {
  532. AtomToolsFramework::DynamicPropertyConfig propertyConfig = property.GetConfig();
  533. const auto oldVisible = propertyConfig.m_visible;
  534. const auto oldReadOnly = propertyConfig.m_readOnly;
  535. AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDynamicMetadata[property.GetId()]);
  536. property.SetConfig(propertyConfig);
  537. if (oldReadOnly != propertyConfig.m_readOnly)
  538. {
  539. RefreshGroup(groupPair.first);
  540. }
  541. if (oldVisible != propertyConfig.m_visible)
  542. {
  543. RebuildGroup(groupPair.first);
  544. }
  545. }
  546. }
  547. }
  548. void MaterialPropertyInspector::UpdateMaterialInstanceProperty(const AtomToolsFramework::DynamicProperty& property)
  549. {
  550. if (!IsLoaded())
  551. {
  552. return;
  553. }
  554. const auto propertyIndex = m_materialInstance->FindPropertyIndex(property.GetId());
  555. if (propertyIndex.IsValid())
  556. {
  557. m_dirtyPropertyFlags.set(propertyIndex.GetIndex());
  558. const auto runtimeValue = AtomToolsFramework::ConvertToRuntimeType(property.GetValue());
  559. if (runtimeValue.IsValid())
  560. {
  561. m_materialInstance->SetPropertyValue(propertyIndex, runtimeValue);
  562. }
  563. }
  564. }
  565. AZ::Crc32 MaterialPropertyInspector::GetGroupSaveStateKey(const AZStd::string& groupName) const
  566. {
  567. return AZ::Crc32(AZStd::string::format(
  568. "MaterialPropertyInspector::PropertyGroup::%s::%s", m_editData.m_materialAssetId.ToString<AZStd::string>().c_str(),
  569. groupName.c_str()));
  570. }
  571. bool MaterialPropertyInspector::IsInstanceNodePropertyModifed(const AzToolsFramework::InstanceDataNode* node) const
  572. {
  573. const auto property = AtomToolsFramework::FindAncestorInstanceDataNodeByType<AtomToolsFramework::DynamicProperty>(node);
  574. return property && !AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_originalValue);
  575. }
  576. const char* MaterialPropertyInspector::GetInstanceNodePropertyIndicator(const AzToolsFramework::InstanceDataNode* node) const
  577. {
  578. if (IsInstanceNodePropertyModifed(node))
  579. {
  580. return ":/Icons/changed_property.svg";
  581. }
  582. return ":/Icons/blank.png";
  583. }
  584. AZStd::string MaterialPropertyInspector::GetRelativePath(const AZStd::string& path) const
  585. {
  586. bool pathFound = false;
  587. AZStd::string rootFolder;
  588. AZStd::string relativePath;
  589. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  590. pathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GenerateRelativeSourcePath, path, relativePath,
  591. rootFolder);
  592. return relativePath;
  593. }
  594. AZStd::string MaterialPropertyInspector::GetFileName(const AZStd::string& path) const
  595. {
  596. AZStd::string fileName;
  597. AZ::StringFunc::Path::GetFullFileName(path.c_str(), fileName);
  598. return fileName;
  599. }
  600. bool MaterialPropertyInspector::IsSourceMaterial(const AZStd::string& path) const
  601. {
  602. return !path.empty() && AZ::StringFunc::Path::IsExtension(path.c_str(), AZ::RPI::MaterialSourceData::Extension);
  603. }
  604. bool MaterialPropertyInspector::SaveMaterial(const AZStd::string& path) const
  605. {
  606. const AZStd::string saveFilePath = AtomToolsFramework::GetSaveFilePathFromDialog(
  607. path, { { "Material", AZ::RPI::MaterialSourceData::Extension } }, "Material");
  608. if (saveFilePath.empty())
  609. {
  610. return false;
  611. }
  612. if (!EditorMaterialComponentUtil::SaveSourceMaterialFromEditData(saveFilePath, m_editData))
  613. {
  614. AZ_Warning("AZ::Render::EditorMaterialComponentInspector", false, "Failed to save material data.");
  615. return false;
  616. }
  617. return true;
  618. }
  619. void MaterialPropertyInspector::OpenMenu()
  620. {
  621. if (!IsLoaded())
  622. {
  623. return;
  624. }
  625. QMenu menu(this);
  626. menu.addAction(tr("Save As..."), [this] {
  627. const auto& defaultPath = AtomToolsFramework::GetUniqueFilePath(AZStd::string::format(
  628. "%s/Assets/untitled.%s", AZ::Utils::GetProjectPath().c_str(), AZ::RPI::MaterialSourceData::Extension));
  629. SaveMaterial(defaultPath);
  630. });
  631. if (IsSourceMaterial(m_editData.m_materialSourcePath))
  632. {
  633. const auto& materialSourceFileName = GetFileName(m_editData.m_materialSourcePath);
  634. menu.addAction(tr("Save Over \"%1\"...").arg(materialSourceFileName.c_str()), [this] {
  635. SaveMaterial(m_editData.m_materialSourcePath);
  636. });
  637. }
  638. menu.addSeparator();
  639. menu.addAction("Clear Overrides", [this] {
  640. AzToolsFramework::ScopedUndoBatch undoBatch("Clear material property overrides.");
  641. m_editData.m_materialPropertyOverrideMap.clear();
  642. for (const AZ::EntityId& entityId : m_entityIdsToEdit)
  643. {
  644. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(
  645. &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entityId);
  646. MaterialComponentRequestBus::Event(
  647. entityId, &MaterialComponentRequestBus::Events::SetPropertyValues, m_materialAssignmentId,
  648. m_editData.m_materialPropertyOverrideMap);
  649. MaterialComponentNotificationBus::Event(entityId, &MaterialComponentNotifications::OnMaterialsEdited);
  650. }
  651. m_updateUI = true;
  652. m_updatePreview = true;
  653. });
  654. menu.exec(QCursor::pos());
  655. }
  656. const EditorMaterialComponentUtil::MaterialEditData& MaterialPropertyInspector::GetEditData() const
  657. {
  658. return m_editData;
  659. }
  660. AZ::Data::AssetId MaterialPropertyInspector::GetActiveMaterialAssetIdFromEntity() const
  661. {
  662. AZ::Data::AssetId materialAssetId = {};
  663. MaterialComponentRequestBus::EventResult(
  664. materialAssetId, m_primaryEntityId, &MaterialComponentRequestBus::Events::GetMaterialAssetId, m_materialAssignmentId);
  665. return materialAssetId;
  666. }
  667. void MaterialPropertyInspector::AfterPropertyModified(AzToolsFramework::InstanceDataNode* pNode)
  668. {
  669. const auto property = AtomToolsFramework::FindAncestorInstanceDataNodeByType<AtomToolsFramework::DynamicProperty>(pNode);
  670. if (property)
  671. {
  672. m_editData.m_materialPropertyOverrideMap[property->GetId()] = property->GetValue();
  673. UpdateMaterialInstanceProperty(*property);
  674. SaveOverrideToEntities(*property, false);
  675. }
  676. }
  677. void MaterialPropertyInspector::SetPropertyEditingComplete(AzToolsFramework::InstanceDataNode* pNode)
  678. {
  679. // As above, there are symmetrical functions on the notification interface for when editing begins and ends and has been
  680. // completed but they are not being called following that pattern. when this function executes the changes to the property
  681. // are ready to be committed or reverted
  682. const auto property = AtomToolsFramework::FindAncestorInstanceDataNodeByType<AtomToolsFramework::DynamicProperty>(pNode);
  683. if (property)
  684. {
  685. m_editData.m_materialPropertyOverrideMap[property->GetId()] = property->GetValue();
  686. UpdateMaterialInstanceProperty(*property);
  687. SaveOverrideToEntities(*property, true);
  688. RunEditorMaterialFunctors();
  689. }
  690. }
  691. void MaterialPropertyInspector::OnEntityInitialized(const AZ::EntityId& entityId)
  692. {
  693. if (m_entityIdsToEdit.count(entityId) > 0)
  694. {
  695. UnloadMaterial();
  696. }
  697. }
  698. void MaterialPropertyInspector::OnEntityDestroyed(const AZ::EntityId& entityId)
  699. {
  700. if (m_entityIdsToEdit.count(entityId) > 0)
  701. {
  702. UnloadMaterial();
  703. }
  704. }
  705. void MaterialPropertyInspector::OnEntityActivated(const AZ::EntityId& entityId)
  706. {
  707. m_updateUI |= (m_entityIdsToEdit.count(entityId) > 0);
  708. }
  709. void MaterialPropertyInspector::OnEntityDeactivated(const AZ::EntityId& entityId)
  710. {
  711. if (m_entityIdsToEdit.count(entityId) > 0)
  712. {
  713. UnloadMaterial();
  714. }
  715. }
  716. void MaterialPropertyInspector::OnEntityNameChanged(const AZ::EntityId& entityId, const AZStd::string& name)
  717. {
  718. AZ_UNUSED(name);
  719. m_updateUI |= (m_primaryEntityId == entityId);
  720. }
  721. void MaterialPropertyInspector::OnSystemTick()
  722. {
  723. if (m_updateUI)
  724. {
  725. m_updateUI = false;
  726. UpdateUI();
  727. }
  728. if (m_updatePreview)
  729. {
  730. m_updatePreview = false;
  731. for (const AZ::EntityId& entityId : m_entityIdsToEdit)
  732. {
  733. EditorMaterialSystemComponentRequestBus::Broadcast(
  734. &EditorMaterialSystemComponentRequestBus::Events::RenderMaterialPreview, entityId, m_materialAssignmentId);
  735. }
  736. }
  737. }
  738. void MaterialPropertyInspector::OnMaterialsEdited()
  739. {
  740. m_updateUI |= !m_internalEditNotification;
  741. m_updatePreview = true;
  742. }
  743. void MaterialPropertyInspector::OnRenderMaterialPreviewReady(
  744. const AZ::EntityId& entityId, const AZ::Render::MaterialAssignmentId& materialAssignmentId, const QPixmap& pixmap)
  745. {
  746. if (m_overviewImage && m_primaryEntityId == entityId && m_materialAssignmentId == materialAssignmentId)
  747. {
  748. m_overviewImage->setPixmap(pixmap);
  749. }
  750. }
  751. void MaterialPropertyInspector::UpdateUI()
  752. {
  753. if (IsLoaded())
  754. {
  755. LoadOverridesFromEntity();
  756. }
  757. else
  758. {
  759. LoadMaterial(m_primaryEntityId, m_entityIdsToEdit, m_materialAssignmentId);
  760. }
  761. }
  762. } // namespace EditorMaterialComponentInspector
  763. } // namespace Render
  764. } // namespace AZ