Refactored how model material slots work in preparation to support more flexible material conversion options for the scene asset pipeline. The material slot IDs are based on the MaterialUid that come from SceneAPI. Since these IDs are also used as the AssetId sub-ID for the converted material assets, the system was just checking the material asset sub-ID to determine the material slot ID. But in order to support certain FBX material conversion options, we needed to break this tie, so the slot ID is separate from the AssetId of the material in that slot. This will allow some other material to be used in the slot, instead of being forced to use one that was generated from the FBX.
Here we inttroduce a new struct ModelMaterialSlot which formalizes the concept of material slot, with an ID, display name, and default material assignment. The ID still comes from the MaterialUid like before. The display name is built-in, rather than being parsed out from the asset file name. And the default material assignment can be any material asset, it doesn't have to come from the FBX (or other scene file). This commit is just the preliminary set of changes. Cursory testing shows that it works pretty well but more testing is needed (and likely some fixes) before merging. Here is what's left to do... Add serialization version converters to preserve prior prefab data. See if we can get rid of GetLabelByAssetId function only rely on the display name inside ModelMaterialSlot. I'm not sure if the condition for enabling the "Edit Material Instance..." context menu item is correct. Test actors Lots more testing in general Signed-off-by: santorac <55155825+santorac@users.noreply.github.com>
This commit is contained in:
+67
-69
@@ -44,57 +44,8 @@ namespace AZ
|
||||
|
||||
if (classElement.GetVersion() < 3)
|
||||
{
|
||||
// The default material was changed from an asset to an EditorMaterialComponentSlot and old data must be converted
|
||||
constexpr AZ::u32 defaultMaterialAssetDataCrc = AZ_CRC("defaultMaterialAsset", 0x736fc071);
|
||||
|
||||
Data::Asset<RPI::MaterialAsset> oldDefaultMaterialData;
|
||||
if (!classElement.GetChildData(defaultMaterialAssetDataCrc, oldDefaultMaterialData))
|
||||
{
|
||||
AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to get defaultMaterialAsset element");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!classElement.RemoveElementByName(defaultMaterialAssetDataCrc))
|
||||
{
|
||||
AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to remove defaultMaterialAsset element");
|
||||
return false;
|
||||
}
|
||||
|
||||
EditorMaterialComponentSlot newDefaultMaterialData;
|
||||
newDefaultMaterialData.m_id = DefaultMaterialAssignmentId;
|
||||
newDefaultMaterialData.m_materialAsset = oldDefaultMaterialData;
|
||||
classElement.AddElementWithData(context, "defaultMaterialSlot", newDefaultMaterialData);
|
||||
|
||||
// Slots now support and display the default material asset when empty
|
||||
// The old placeholder assignments are irrelevant and must be cleared
|
||||
constexpr AZ::u32 materialSlotsByLodDataCrc = AZ_CRC("materialSlotsByLod", 0xb1498db6);
|
||||
|
||||
EditorMaterialComponentSlotsByLodContainer lodSlotData;
|
||||
if (!classElement.GetChildData(materialSlotsByLodDataCrc, lodSlotData))
|
||||
{
|
||||
AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to get materialSlotsByLod element");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!classElement.RemoveElementByName(materialSlotsByLodDataCrc))
|
||||
{
|
||||
AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to remove materialSlotsByLod element");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find and clear all slots that are assigned to the slot's default value
|
||||
for (auto& lodSlots : lodSlotData)
|
||||
{
|
||||
for (auto& slot : lodSlots)
|
||||
{
|
||||
if (slot.m_materialAsset.GetId() == slot.m_id.m_materialAssetId)
|
||||
{
|
||||
slot.m_materialAsset = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
classElement.AddElementWithData(context, "materialSlotsByLod", lodSlotData);
|
||||
AZ_Error("EditorMaterialComponent", false, "Material Component version < 3 is no longer supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (classElement.GetVersion() < 4)
|
||||
@@ -238,7 +189,7 @@ namespace AZ
|
||||
for (auto& materialSlotPair : GetMaterialSlots())
|
||||
{
|
||||
EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
|
||||
if (materialSlot->m_id.IsAssetOnly())
|
||||
if (materialSlot->m_id.IsSlotIdOnly())
|
||||
{
|
||||
materialSlot->Clear();
|
||||
}
|
||||
@@ -251,7 +202,7 @@ namespace AZ
|
||||
for (auto& materialSlotPair : GetMaterialSlots())
|
||||
{
|
||||
EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
|
||||
if (materialSlot->m_id.IsLodAndAsset())
|
||||
if (materialSlot->m_id.IsLodAndSlotId())
|
||||
{
|
||||
materialSlot->Clear();
|
||||
}
|
||||
@@ -318,6 +269,9 @@ namespace AZ
|
||||
// Build the controller configuration from the editor configuration
|
||||
MaterialComponentConfig config = m_controller.GetConfiguration();
|
||||
config.m_materials.clear();
|
||||
|
||||
RPI::ModelMaterialSlotMap modelMaterialSlots;
|
||||
MaterialReceiverRequestBus::EventResult(modelMaterialSlots, GetEntityId(), &MaterialReceiverRequestBus::Events::GetModelMaterialSlots);
|
||||
|
||||
for (const auto& materialSlotPair : GetMaterialSlots())
|
||||
{
|
||||
@@ -340,10 +294,15 @@ namespace AZ
|
||||
}
|
||||
else if (!materialSlot->m_propertyOverrides.empty() || !materialSlot->m_matModUvOverrides.empty())
|
||||
{
|
||||
MaterialAssignment& materialAssignment = config.m_materials[materialSlot->m_id];
|
||||
materialAssignment.m_materialAsset.Create(materialSlot->m_id.m_materialAssetId);
|
||||
materialAssignment.m_propertyOverrides = materialSlot->m_propertyOverrides;
|
||||
materialAssignment.m_matModUvOverrides = materialSlot->m_matModUvOverrides;
|
||||
auto materialSlotIter = modelMaterialSlots.find(materialSlot->m_id.m_materialSlotStableId);
|
||||
|
||||
if (materialSlotIter != modelMaterialSlots.end())
|
||||
{
|
||||
MaterialAssignment& materialAssignment = config.m_materials[materialSlot->m_id];
|
||||
materialAssignment.m_materialAsset = materialSlotIter->second.m_defaultMaterialAsset;
|
||||
materialAssignment.m_propertyOverrides = materialSlot->m_propertyOverrides;
|
||||
materialAssignment.m_matModUvOverrides = materialSlot->m_matModUvOverrides;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +321,9 @@ namespace AZ
|
||||
// Get the known material assignment slots from the associated model or other source
|
||||
MaterialAssignmentMap materialsFromSource;
|
||||
MaterialReceiverRequestBus::EventResult(materialsFromSource, GetEntityId(), &MaterialReceiverRequestBus::Events::GetMaterialAssignments);
|
||||
|
||||
RPI::ModelMaterialSlotMap modelMaterialSlots;
|
||||
MaterialReceiverRequestBus::EventResult(modelMaterialSlots, GetEntityId(), &MaterialReceiverRequestBus::Events::GetModelMaterialSlots);
|
||||
|
||||
// Generate the table of editable materials using the source data to define number of groups, elements, and initial values
|
||||
for (const auto& materialPair : materialsFromSource)
|
||||
@@ -385,6 +347,29 @@ namespace AZ
|
||||
OnConfigurationChanged();
|
||||
};
|
||||
|
||||
const char* UnknownSlotName = "<unknown>";
|
||||
|
||||
// If this is the default material assignment ID then it represents the default slot which is not contained in any other group
|
||||
if (slot.m_id == DefaultMaterialAssignmentId)
|
||||
{
|
||||
slot.m_label = "Default Material";
|
||||
}
|
||||
else
|
||||
{
|
||||
auto slotIter = modelMaterialSlots.find(slot.m_id.m_materialSlotStableId);
|
||||
if (slotIter != modelMaterialSlots.end())
|
||||
{
|
||||
const Name& displayName = slotIter->second.m_displayName;
|
||||
slot.m_label = !displayName.IsEmpty() ? displayName.GetStringView() : UnknownSlotName;
|
||||
|
||||
slot.m_defaultMaterialAsset = slotIter->second.m_defaultMaterialAsset;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.m_label = UnknownSlotName;
|
||||
}
|
||||
}
|
||||
|
||||
// if material is present in controller configuration, assign its data
|
||||
const MaterialAssignment& materialFromController = GetMaterialAssignmentFromMap(config.m_materials, slot.m_id);
|
||||
slot.m_materialAsset = materialFromController.m_materialAsset;
|
||||
@@ -400,13 +385,13 @@ namespace AZ
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slot.m_id.IsAssetOnly())
|
||||
if (slot.m_id.IsSlotIdOnly())
|
||||
{
|
||||
m_materialSlots.push_back(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slot.m_id.IsLodAndAsset())
|
||||
if (slot.m_id.IsLodAndSlotId())
|
||||
{
|
||||
// Resize the containers to fit all elements
|
||||
m_materialSlotsByLod.resize(AZ::GetMax<size_t>(m_materialSlotsByLod.size(), aznumeric_cast<size_t>(slot.m_id.m_lodIndex + 1)));
|
||||
@@ -452,17 +437,19 @@ namespace AZ
|
||||
{
|
||||
AzToolsFramework::ScopedUndoBatch undoBatch("Generating materials.");
|
||||
SetDirty();
|
||||
|
||||
RPI::ModelMaterialSlotMap modelMaterialSlots;
|
||||
MaterialReceiverRequestBus::EventResult(modelMaterialSlots, GetEntityId(), &MaterialReceiverRequestBus::Events::GetModelMaterialSlots);
|
||||
|
||||
// First generating a unique set of all material asset IDs that will be used for source data generation
|
||||
AZStd::unordered_set<AZ::Data::AssetId> assetIds;
|
||||
|
||||
auto materialSlots = GetMaterialSlots();
|
||||
for (auto& materialSlotPair : materialSlots)
|
||||
for (auto& materialSlot : modelMaterialSlots)
|
||||
{
|
||||
EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
|
||||
if (materialSlot->m_id.m_materialAssetId.IsValid())
|
||||
Data::AssetId defaultMaterialAssetId = materialSlot.second.m_defaultMaterialAsset.GetId();
|
||||
if (defaultMaterialAssetId.IsValid())
|
||||
{
|
||||
assetIds.insert(materialSlot->m_id.m_materialAssetId);
|
||||
assetIds.insert(defaultMaterialAssetId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,7 +459,7 @@ namespace AZ
|
||||
for (const AZ::Data::AssetId& assetId : assetIds)
|
||||
{
|
||||
EditorMaterialComponentExporter::ExportItem exportItem;
|
||||
exportItem.m_assetId = assetId;
|
||||
exportItem.m_originalAssetId = assetId;
|
||||
exportItems.push_back(exportItem);
|
||||
}
|
||||
|
||||
@@ -489,12 +476,23 @@ namespace AZ
|
||||
const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.m_exportPath, 0);
|
||||
if (assetIdOutcome)
|
||||
{
|
||||
for (auto& materialSlotPair : materialSlots)
|
||||
for (auto& materialSlotPair : GetMaterialSlots())
|
||||
{
|
||||
EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
|
||||
if (materialSlot && materialSlot->m_id.m_materialAssetId == exportItem.m_assetId)
|
||||
EditorMaterialComponentSlot* editorMaterialSlot = materialSlotPair.second;
|
||||
|
||||
if (editorMaterialSlot)
|
||||
{
|
||||
materialSlot->m_materialAsset.Create(assetIdOutcome.GetValue());
|
||||
// Only update the slot of it was originally empty, having no override material.
|
||||
// We need to check whether replaced material corresponds to this slot's default material.
|
||||
if (!editorMaterialSlot->m_materialAsset.GetId().IsValid())
|
||||
{
|
||||
auto materialSlot = modelMaterialSlots.find(editorMaterialSlot->m_id.m_materialSlotStableId);
|
||||
if (materialSlot != modelMaterialSlots.end() &&
|
||||
materialSlot->second.m_defaultMaterialAsset.GetId() == exportItem.m_originalAssetId)
|
||||
{
|
||||
editorMaterialSlot->m_materialAsset.Create(assetIdOutcome.GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -132,7 +132,7 @@ namespace AZ
|
||||
int row = 0;
|
||||
for (ExportItem& exportItem : exportItems)
|
||||
{
|
||||
QFileInfo fileInfo(GetExportPathByAssetId(exportItem.m_assetId).c_str());
|
||||
QFileInfo fileInfo(GetExportPathByAssetId(exportItem.m_originalAssetId).c_str());
|
||||
|
||||
// Configuring initial settings based on whether or not the target file already exists
|
||||
exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData();
|
||||
@@ -147,7 +147,7 @@ namespace AZ
|
||||
// Create a check box for toggling the enabled state of this item
|
||||
QCheckBox* materialSlotCheckBox = new QCheckBox(tableWidget);
|
||||
materialSlotCheckBox->setChecked(exportItem.m_enabled);
|
||||
materialSlotCheckBox->setText(GetLabelByAssetId(exportItem.m_assetId).c_str());
|
||||
materialSlotCheckBox->setText(GetLabelByAssetId(exportItem.m_originalAssetId).c_str());
|
||||
tableWidget->setCellWidget(row, MaterialSlotColumn, materialSlotCheckBox);
|
||||
|
||||
// Create a file picker widget for selecting the save path for the exported material
|
||||
@@ -256,7 +256,7 @@ namespace AZ
|
||||
}
|
||||
|
||||
EditorMaterialComponentUtil::MaterialEditData editData;
|
||||
if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.m_assetId, editData))
|
||||
if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.m_originalAssetId, editData))
|
||||
{
|
||||
AZ_Warning("AZ::Render::EditorMaterialComponentExporter", false, "Failed to load material data.");
|
||||
return false;
|
||||
|
||||
+6
-5
@@ -19,10 +19,10 @@ namespace AZ
|
||||
{
|
||||
namespace EditorMaterialComponentExporter
|
||||
{
|
||||
// Attemts to generate a display label for a material slot by parsing its file name
|
||||
//! Attemts to generate a display label for a material slot by parsing its file name
|
||||
AZStd::string GetLabelByAssetId(const AZ::Data::AssetId& assetId);
|
||||
|
||||
// Generates a destination file path for exporting material source data
|
||||
//! Generates a destination file path for exporting material source data
|
||||
AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId);
|
||||
|
||||
struct ExportItem
|
||||
@@ -30,16 +30,17 @@ namespace AZ
|
||||
bool m_enabled = true;
|
||||
bool m_exists = false;
|
||||
bool m_overwrite = false;
|
||||
AZ::Data::AssetId m_assetId;
|
||||
AZ::Data::AssetId m_originalAssetId; //!< AssetId of the original built-in material, which will be exported.
|
||||
AZStd::string m_exportPath;
|
||||
};
|
||||
|
||||
using ExportItemsContainer = AZStd::vector<ExportItem>;
|
||||
|
||||
// Generates and opens a dialog for configuring material data export paths and actions
|
||||
//! Generates and opens a dialog for configuring material data export paths and actions.
|
||||
//! Note this will not modify the m_originalAssetId field in each ExportItem.
|
||||
bool OpenExportDialog(ExportItemsContainer& exportItems);
|
||||
|
||||
// Attemts to construct and save material source data from a product asset
|
||||
//! Attemts to construct and save material source data from a product asset
|
||||
bool ExportMaterialSourceData(const ExportItem& exportItem);
|
||||
} // namespace EditorMaterialComponentExporter
|
||||
} // namespace Render
|
||||
|
||||
+10
-23
@@ -20,7 +20,7 @@
|
||||
|
||||
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QAction>
|
||||
#include <QCursor>
|
||||
AZ_POP_DISABLE_WARNING
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace AZ
|
||||
return false;
|
||||
}
|
||||
|
||||
const MaterialAssignmentId newId(oldId.first, oldId.second);
|
||||
const MaterialAssignmentId newId(oldId.first, oldId.second.m_subId);
|
||||
classElement.AddElementWithData(context, "id", newId);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace AZ
|
||||
->Version(5, &EditorMaterialComponentSlot::ConvertVersion)
|
||||
->Field("id", &EditorMaterialComponentSlot::m_id)
|
||||
->Field("materialAsset", &EditorMaterialComponentSlot::m_materialAsset)
|
||||
->Field("defaultMaterialAsset", &EditorMaterialComponentSlot::m_defaultMaterialAsset)
|
||||
;
|
||||
|
||||
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
|
||||
@@ -121,21 +122,12 @@ namespace AZ
|
||||
|
||||
AZ::Data::AssetId EditorMaterialComponentSlot::GetDefaultAssetId() const
|
||||
{
|
||||
return m_id.m_materialAssetId;
|
||||
return m_defaultMaterialAsset.GetId();
|
||||
}
|
||||
|
||||
AZStd::string EditorMaterialComponentSlot::GetLabel() const
|
||||
{
|
||||
// Generate the label for the material slot based on the assignment ID
|
||||
// If this is the default material assignment ID then it represents the default slot which is not contained in any other group
|
||||
if (m_id == DefaultMaterialAssignmentId)
|
||||
{
|
||||
return "Default Material";
|
||||
}
|
||||
|
||||
// Otherwise the label can be generated by parsing the source file name associated with the asset ID
|
||||
const AZStd::string& label = EditorMaterialComponentExporter::GetLabelByAssetId(m_id.m_materialAssetId);
|
||||
return !label.empty() ? label : "<unknown>";
|
||||
return m_label;
|
||||
}
|
||||
|
||||
bool EditorMaterialComponentSlot::HasSourceData() const
|
||||
@@ -183,27 +175,22 @@ namespace AZ
|
||||
OnMaterialChanged();
|
||||
}
|
||||
|
||||
void EditorMaterialComponentSlot::SetDefaultAsset()
|
||||
void EditorMaterialComponentSlot::ResetToDefaultAsset()
|
||||
{
|
||||
m_materialAsset = {};
|
||||
m_materialAsset = m_defaultMaterialAsset;
|
||||
m_propertyOverrides = {};
|
||||
m_matModUvOverrides = {};
|
||||
if (m_id.m_materialAssetId.IsValid())
|
||||
{
|
||||
// If no material is assigned to this slot, assign the default material from the slot id to edit its properties
|
||||
m_materialAsset.Create(m_id.m_materialAssetId);
|
||||
}
|
||||
OnMaterialChanged();
|
||||
}
|
||||
|
||||
void EditorMaterialComponentSlot::OpenMaterialExporter()
|
||||
{
|
||||
// Because we are generating a source material from this specific slot there is only one entry
|
||||
// But we still need to allow the user to reconfigure it using the dialogue
|
||||
// But we still need to allow the user to reconfigure it using the dialog
|
||||
EditorMaterialComponentExporter::ExportItemsContainer exportItems;
|
||||
{
|
||||
EditorMaterialComponentExporter::ExportItem exportItem;
|
||||
exportItem.m_assetId = m_id.m_materialAssetId;
|
||||
exportItem.m_originalAssetId = m_defaultMaterialAsset.GetId();
|
||||
exportItems.push_back(exportItem);
|
||||
}
|
||||
|
||||
@@ -275,7 +262,7 @@ namespace AZ
|
||||
QAction* action = nullptr;
|
||||
|
||||
action = menu.addAction("Generate/Manage Source Material...", [this]() { OpenMaterialExporter(); });
|
||||
action->setEnabled(m_id.m_materialAssetId.IsValid());
|
||||
action->setEnabled(m_defaultMaterialAsset.GetId().IsValid());
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
|
||||
+3
-1
@@ -34,7 +34,7 @@ namespace AZ
|
||||
AZStd::string GetLabel() const;
|
||||
bool HasSourceData() const;
|
||||
void OpenMaterialEditor() const;
|
||||
void SetDefaultAsset();
|
||||
void ResetToDefaultAsset();
|
||||
void Clear();
|
||||
void ClearOverrides();
|
||||
void OpenMaterialExporter();
|
||||
@@ -42,7 +42,9 @@ namespace AZ
|
||||
void OpenUvNameMapInspector();
|
||||
|
||||
MaterialAssignmentId m_id;
|
||||
AZStd::string m_label;
|
||||
Data::Asset<RPI::MaterialAsset> m_materialAsset;
|
||||
Data::Asset<RPI::MaterialAsset> m_defaultMaterialAsset;
|
||||
MaterialPropertyOverrideMap m_propertyOverrides;
|
||||
AZStd::function<void()> m_materialChangedCallback;
|
||||
AZStd::function<void()> m_propertyChangedCallback;
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ namespace AZ
|
||||
for (const auto& oldPair : oldMaterials)
|
||||
{
|
||||
const DeprecatedMaterialAssignmentId& oldId = oldPair.first;
|
||||
const MaterialAssignmentId newId(oldId.first, oldId.second);
|
||||
const MaterialAssignmentId newId(oldId.first, oldId.second.m_subId);
|
||||
newMaterials[newId] = oldPair.second;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user