/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT #include #include #include #include #include #include #include #include #include #include AZ_POP_DISABLE_WARNING namespace AZ { namespace Render { namespace EditorMaterialComponentExporter { AZStd::string GetLabelByAssetId(const AZ::Data::AssetId& assetId) { AZStd::string label; if (assetId.IsValid()) { // Material assets that are exported through the FBX pipeline have their filenames generated by adding // the DCC material name as a prefix and a unique number to the end of the source file name. // Rather than storing the DCC material name inside of the material asset we can reproduce it by removing // the prefix and suffix from the product file name. // We need the material product path as the initial string that will be stripped down const AZStd::string& productPath = AZ::RPI::AssetUtils::GetProductPathByAssetId(assetId); if (!productPath.empty() && AzFramework::StringFunc::Path::GetFileName(productPath.c_str(), label)) { // If there is a source file, typically an FBX or other model file, we must get its filename to remove the prefix from the label AZStd::string prefix; const AZStd::string& sourcePath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(assetId); if (!sourcePath.empty() && AZ::StringFunc::Path::GetFileName(sourcePath.c_str(), prefix)) { if (!prefix.empty() && prefix.size() < label.size()) { if (AZ::StringFunc::StartsWith(label, prefix, false)) { // All of the product filename's tokens are separated by underscores so we must also remove the first underscore after the prefix label = label.substr(prefix.size() + 1); } } } // We can remove the numeric suffix by stripping the label of everything after the last underscore const auto iter = label.find_last_of("_"); if (iter != AZStd::string::npos) { label = label.substr(0, iter); } } } return label; } AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId) { AZStd::string exportPath; if (assetId.IsValid()) { exportPath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(assetId); AZ::StringFunc::Path::StripExtension(exportPath); exportPath += "_"; exportPath += GetLabelByAssetId(assetId); exportPath += "."; exportPath += AZ::RPI::MaterialSourceData::Extension; AZ::StringFunc::Path::Normalize(exportPath); } return exportPath; } // Returns message text based on the item state QString GetExportItemStatusMessage(const ExportItem& exportItem) { QFileInfo fileInfo(exportItem.m_exportPath.c_str()); if (!exportItem.m_enabled) { return QString("Do not generate a new material."); } if (fileInfo == QFileInfo()) { return QString("A valid material file path is required."); } if (fileInfo.exists()) { return QString("\"%1\" will be replaced in the designated folder.").arg(fileInfo.fileName()); } return QString("\"%1\" will be generated in the designated folder.").arg(fileInfo.fileName()); } bool OpenExportDialog(ExportItemsContainer& exportItems) { QWidget* activeWindow = nullptr; AzToolsFramework::EditorWindowRequestBus::BroadcastResult(activeWindow, &AzToolsFramework::EditorWindowRequests::GetAppMainWindow); // Constructing a dialog with a table to display all configurable material export items QDialog dialog(activeWindow); dialog.setWindowTitle("Generate Source Materials"); const QStringList headerLabels = { "Enable", "Material Slot", "Material Filename", "Status" }; const int EnableColumn = 0; const int MaterialColumn = 1; const int FileColumn = 2; const int StatusColumn = 3; // Create a table widget that will be filled with all of the data and options for each exported material QTableWidget* tableWidget = new QTableWidget(&dialog); tableWidget->setColumnCount(headerLabels.size()); tableWidget->setRowCount((int)exportItems.size()); tableWidget->setHorizontalHeaderLabels(headerLabels); tableWidget->setSortingEnabled(false); tableWidget->setAlternatingRowColors(true); tableWidget->setCornerButtonEnabled(false); tableWidget->setContextMenuPolicy(Qt::DefaultContextMenu); tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); tableWidget->setSelectionMode(QAbstractItemView::SingleSelection); tableWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); // Force the table to stretch its header to fill the entire width of the dialog tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); tableWidget->horizontalHeader()->setStretchLastSection(true); // Hide row numbers tableWidget->verticalHeader()->setVisible(false); int row = 0; for (ExportItem& exportItem : exportItems) { QFileInfo fileInfo(GetExportPathByAssetId(exportItem.m_assetId).c_str()); // Configuring initial settings based on whether or not the target file already exists exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData(); // Populate the table with data for every column tableWidget->setItem(row, EnableColumn, new QTableWidgetItem(exportItem.m_enabled)); tableWidget->setItem(row, MaterialColumn, new QTableWidgetItem(GetLabelByAssetId(exportItem.m_assetId).c_str())); tableWidget->setItem(row, FileColumn, new QTableWidgetItem(fileInfo.fileName())); // Create a check box for toggling the enabled state of this item QWidget* enableCheckBoxParent = new QWidget(tableWidget); QCheckBox* enableCheckBox = new QCheckBox(enableCheckBoxParent); enableCheckBox->setChecked(exportItem.m_enabled); // Center checkbox in cell QHBoxLayout* enableCheckBoxLayout = new QHBoxLayout(enableCheckBoxParent); enableCheckBoxLayout->setAlignment(Qt::AlignCenter); enableCheckBoxLayout->addWidget(enableCheckBox); enableCheckBoxParent->setLayout(enableCheckBoxLayout); tableWidget->setCellWidget(row, EnableColumn, enableCheckBoxParent); // Create a file picker widget for selecting the save path for the exported material AzQtComponents::BrowseEdit* fileWidget = new AzQtComponents::BrowseEdit(tableWidget); fileWidget->setLineEditReadOnly(true); fileWidget->setClearButtonEnabled(false); fileWidget->setText(fileInfo.fileName()); tableWidget->setCellWidget(row, FileColumn, fileWidget); // The status widget will be used to inform the user of issues and outcomes from selected settings QLabel* statusWidget = new QLabel(tableWidget); statusWidget->setText(GetExportItemStatusMessage(exportItem)); tableWidget->setCellWidget(row, StatusColumn, statusWidget); // Whenever the selection is updated, automatically apply the change to the export item QObject::connect(enableCheckBox, &QCheckBox::stateChanged, enableCheckBox, [&dialog, &exportItem, enableCheckBox, fileWidget, statusWidget]([[maybe_unused]] int state) { exportItem.m_enabled = enableCheckBox->isChecked(); fileWidget->setEnabled(exportItem.m_enabled); statusWidget->setText(GetExportItemStatusMessage(exportItem)); }); // Whenever the browse button is clicked, open a save file dialog in the same location as the current export file setting QObject::connect(fileWidget, &AzQtComponents::BrowseEdit::attachedButtonTriggered, fileWidget, [&dialog, &exportItem, enableCheckBox, fileWidget, statusWidget]() { QFileInfo fileInfo = QFileDialog::getSaveFileName(&dialog, QString("Select Material Filename"), exportItem.m_exportPath.c_str(), QString("Material (*.material)"), nullptr, QFileDialog::DontConfirmOverwrite); if (fileInfo != QFileInfo()) { // Only update the export data if a valid path and filename was selected exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData(); // Update the controls to display the new state fileWidget->setText(fileInfo.fileName()); statusWidget->setText(GetExportItemStatusMessage(exportItem)); } }); ++row; } tableWidget->sortItems(MaterialColumn); // Create the bottom row of the dialog with action buttons for exporting or canceling the operation QWidget* buttonRow = new QWidget(&dialog); buttonRow->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); QPushButton* confirmButton = new QPushButton("Confirm", buttonRow); QObject::connect(confirmButton, &QPushButton::clicked, confirmButton, [&dialog] { dialog.accept(); }); QPushButton* cancelButton = new QPushButton("Cancel", buttonRow); QObject::connect(cancelButton, &QPushButton::clicked, cancelButton, [&dialog] { dialog.reject(); }); QHBoxLayout* buttonLayout = new QHBoxLayout(buttonRow); buttonLayout->addStretch(); buttonLayout->addWidget(confirmButton); buttonLayout->addWidget(cancelButton); // Create a heading label for the top of the dialog QLabel* labelWidget = new QLabel("Select the material slots that you want to generate new source materials for. Edit the material file name and location using the file picker.", &dialog); QVBoxLayout* dialogLayout = new QVBoxLayout(&dialog); dialogLayout->addWidget(labelWidget); dialogLayout->addWidget(tableWidget); dialogLayout->addWidget(buttonRow); dialog.setLayout(dialogLayout); // Forcing the initial dialog size to accomodate typical content. // Temporarily settng fixed size because dialog.show/exec invokes WindowDecorationWrapper::showEvent. // This forces the dialog to be centered and sized based on the layout of content. // Resizing the dialog after show will not be centered and moving the dialog programatically doesn't m0ve the custmk frame. dialog.setFixedSize(1000, 200); dialog.show(); // Removing fixed size to allow drag resizing dialog.setMinimumSize(0, 0); dialog.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); // Return true if the user press the export button return dialog.exec() == QDialog::Accepted; } bool ExportMaterialSourceData(const ExportItem& exportItem) { if (!exportItem.m_enabled || exportItem.m_exportPath.empty()) { return false; } // Load the originating product asset from which the new source has set will be generated auto materialAssetOutcome = AZ::RPI::AssetUtils::LoadAsset(exportItem.m_assetId); if (!materialAssetOutcome) { AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, "Failed to load initial material asset while attempting to export: %s", exportItem.m_exportPath.c_str()); return false; } AZ::Data::Asset materialAsset = materialAssetOutcome.GetValue(); AZ::Data::Asset materialTypeAsset = materialAsset->GetMaterialTypeAsset(); // We need a valid path to the material type source data because it's required for to get the property layout and assign to the new material const AZStd::string& materialTypePath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(materialTypeAsset.GetId()); if (materialTypePath.empty()) { AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, "Failed to locate source material type asset while attempting to export: %s", exportItem.m_exportPath.c_str()); return false; } // Getting the source info for the material type file to make sure that it exists // We also need to watch folder to generate a relative asset path for the material type bool result = false; AZ::Data::AssetInfo info; AZStd::string watchFolder; AzToolsFramework::AssetSystemRequestBus::BroadcastResult(result, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath, materialTypePath.c_str(), info, watchFolder); if (!result) { AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, "Failed to get source file info and asset path while attempting to export: %s", exportItem.m_exportPath.c_str()); return false; } // At this point, we should be ready to attempt to load the material type data auto materialTypeOutcome = AZ::RPI::MaterialUtils::LoadMaterialTypeSourceData(materialTypePath); if (!materialTypeOutcome.IsSuccess()) { AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, "Failed to load material type source data: %s", materialTypePath.c_str()); return false; } AZ::RPI::MaterialTypeSourceData materialTypeSourceData = materialTypeOutcome.GetValue(); // Construct the material source data object that will be exported AZ::RPI::MaterialSourceData exportData; exportData.m_propertyLayoutVersion = materialTypeSourceData.m_propertyLayout.m_version; // Converting the absolute material type app to an asset relative path exportData.m_materialType = materialTypePath; AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::Bus::Events::MakePathRelative, exportData.m_materialType, watchFolder.c_str()); // Copy all of the properties from the material asset to the source data that will be exported result = true; materialTypeSourceData.EnumerateProperties([&materialAsset, &materialTypeSourceData, &exportData, &exportItem, &result](const AZStd::string& groupNameId, const AZStd::string& propertyNameId, const auto& propertyDefinition) { const AZ::RPI::MaterialPropertyId propertyId(groupNameId, propertyNameId); const AZ::RPI::MaterialPropertyIndex propertyIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyId.GetFullName()); AZ::RPI::MaterialPropertyValue propertyValue = materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]; if (!materialTypeSourceData.ConvertPropertyValueToSourceDataFormat(propertyDefinition, propertyValue)) { AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, "Failed to export: %s", exportItem.m_exportPath.c_str()); result = false; return false; } if (propertyDefinition.m_value == propertyValue) { return true; } exportData.m_properties[groupNameId][propertyDefinition.m_nameId].m_value = propertyValue; return true; }); return result && AZ::RPI::JsonUtils::SaveObjectToFile(exportItem.m_exportPath, exportData); } } // namespace EditorMaterialComponentExporter } // namespace Render } // namespace AZ