14d2e38b90
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>
276 lines
15 KiB
C++
276 lines
15 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <Material/EditorMaterialComponentExporter.h>
|
|
#include <Material/EditorMaterialComponentUtil.h>
|
|
|
|
#include <AzFramework/API/ApplicationAPI.h>
|
|
#include <AzQtComponents/Components/Widgets/BrowseEdit.h>
|
|
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
|
#include <AzToolsFramework/API/EditorWindowRequestBus.h>
|
|
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
|
|
|
|
#include <Atom/RPI.Edit/Common/AssetUtils.h>
|
|
|
|
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
|
|
#include <QApplication>
|
|
#include <QCheckBox>
|
|
#include <QComboBox>
|
|
#include <QDialogButtonBox>
|
|
#include <QFileDialog>
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QTableWidget>
|
|
#include <QVBoxLayout>
|
|
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 scene 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;
|
|
}
|
|
|
|
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/Manage Source Materials");
|
|
|
|
const QStringList headerLabels = { "Material Slot", "Material Filename", "Overwrite" };
|
|
const int MaterialSlotColumn = 0;
|
|
const int MaterialFileColumn = 1;
|
|
const int OverwriteFileColumn = 2;
|
|
|
|
// 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(MaterialSlotColumn, QHeaderView::ResizeToContents);
|
|
tableWidget->horizontalHeader()->setSectionResizeMode(MaterialFileColumn, QHeaderView::Stretch);
|
|
tableWidget->horizontalHeader()->setSectionResizeMode(OverwriteFileColumn, QHeaderView::ResizeToContents);
|
|
tableWidget->horizontalHeader()->setStretchLastSection(false);
|
|
|
|
// Hide row numbers
|
|
tableWidget->verticalHeader()->setVisible(false);
|
|
|
|
int row = 0;
|
|
for (ExportItem& exportItem : exportItems)
|
|
{
|
|
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();
|
|
exportItem.m_exists = fileInfo.exists();
|
|
exportItem.m_overwrite = false;
|
|
|
|
// Populate the table with data for every column
|
|
tableWidget->setItem(row, MaterialSlotColumn, new QTableWidgetItem());
|
|
tableWidget->setItem(row, MaterialFileColumn, new QTableWidgetItem());
|
|
tableWidget->setItem(row, OverwriteFileColumn, new QTableWidgetItem());
|
|
|
|
// 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_originalAssetId).c_str());
|
|
tableWidget->setCellWidget(row, MaterialSlotColumn, materialSlotCheckBox);
|
|
|
|
// Create a file picker widget for selecting the save path for the exported material
|
|
AzQtComponents::BrowseEdit* materialFileWidget = new AzQtComponents::BrowseEdit(tableWidget);
|
|
materialFileWidget->setLineEditReadOnly(true);
|
|
materialFileWidget->setClearButtonEnabled(false);
|
|
materialFileWidget->setEnabled(exportItem.m_enabled);
|
|
materialFileWidget->setText(fileInfo.fileName());
|
|
tableWidget->setCellWidget(row, MaterialFileColumn, materialFileWidget);
|
|
|
|
// Create a check box for toggling the overwrite state of this item
|
|
QWidget* overwriteCheckBoxContainer = new QWidget(tableWidget);
|
|
QCheckBox* overwriteCheckBox = new QCheckBox(overwriteCheckBoxContainer);
|
|
overwriteCheckBox->setChecked(exportItem.m_overwrite);
|
|
overwriteCheckBox->setEnabled(exportItem.m_enabled && exportItem.m_exists);
|
|
|
|
overwriteCheckBoxContainer->setLayout(new QHBoxLayout(overwriteCheckBoxContainer));
|
|
overwriteCheckBoxContainer->layout()->addWidget(overwriteCheckBox);
|
|
overwriteCheckBoxContainer->layout()->setAlignment(Qt::AlignCenter);
|
|
overwriteCheckBoxContainer->layout()->setContentsMargins(0, 0, 0, 0);
|
|
|
|
tableWidget->setCellWidget(row, OverwriteFileColumn, overwriteCheckBoxContainer);
|
|
|
|
// Whenever the selection is updated, automatically apply the change to the export item
|
|
QObject::connect(materialSlotCheckBox, &QCheckBox::stateChanged, materialSlotCheckBox, [&exportItem, materialFileWidget, materialSlotCheckBox, overwriteCheckBox]([[maybe_unused]] int state) {
|
|
exportItem.m_enabled = materialSlotCheckBox->isChecked();
|
|
materialFileWidget->setEnabled(exportItem.m_enabled);
|
|
overwriteCheckBox->setEnabled(exportItem.m_enabled && exportItem.m_exists);
|
|
});
|
|
|
|
// Whenever the overwrite check box is updated, automatically apply the change to the export item
|
|
QObject::connect(overwriteCheckBox, &QCheckBox::stateChanged, overwriteCheckBox, [&exportItem, overwriteCheckBox]([[maybe_unused]] int state) {
|
|
exportItem.m_overwrite = overwriteCheckBox->isChecked();
|
|
});
|
|
|
|
// Whenever the browse button is clicked, open a save file dialog in the same location as the current export file setting
|
|
QObject::connect(materialFileWidget, &AzQtComponents::BrowseEdit::attachedButtonTriggered, materialFileWidget, [&dialog, &exportItem, materialFileWidget, overwriteCheckBox]() {
|
|
QFileInfo fileInfo = QFileDialog::getSaveFileName(&dialog,
|
|
QString("Select Material Filename"),
|
|
exportItem.m_exportPath.c_str(),
|
|
QString("Material (*.material)"),
|
|
nullptr,
|
|
QFileDialog::DontConfirmOverwrite);
|
|
|
|
// Only update the export data if a valid path and filename was selected
|
|
if (!fileInfo.absoluteFilePath().isEmpty())
|
|
{
|
|
exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData();
|
|
exportItem.m_exists = fileInfo.exists();
|
|
exportItem.m_overwrite = fileInfo.exists();
|
|
|
|
// Update the controls to display the new state
|
|
materialFileWidget->setText(fileInfo.fileName());
|
|
overwriteCheckBox->setChecked(exportItem.m_overwrite);
|
|
overwriteCheckBox->setEnabled(exportItem.m_enabled && exportItem.m_exists);
|
|
}
|
|
});
|
|
|
|
++row;
|
|
}
|
|
|
|
tableWidget->sortItems(MaterialSlotColumn);
|
|
|
|
// Create the bottom row of the dialog with action buttons for exporting or canceling the operation
|
|
QDialogButtonBox* buttonBox = new QDialogButtonBox(&dialog);
|
|
buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
|
|
QObject::connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
|
QObject::connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
|
|
|
// Create a heading label for the top of the dialog
|
|
QLabel* labelWidget = new QLabel("\nSelect the material slots that you want to generate new source materials for. Edit the material file name and location using the file picker.\n", &dialog);
|
|
labelWidget->setWordWrap(true);
|
|
|
|
QVBoxLayout* dialogLayout = new QVBoxLayout(&dialog);
|
|
dialogLayout->addWidget(labelWidget);
|
|
dialogLayout->addWidget(tableWidget);
|
|
dialogLayout->addWidget(buttonBox);
|
|
dialog.setLayout(dialogLayout);
|
|
dialog.setModal(true);
|
|
|
|
// 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(500, 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;
|
|
}
|
|
|
|
if (exportItem.m_exists && !exportItem.m_overwrite)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
EditorMaterialComponentUtil::MaterialEditData editData;
|
|
if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.m_originalAssetId, editData))
|
|
{
|
|
AZ_Warning("AZ::Render::EditorMaterialComponentExporter", false, "Failed to load material data.");
|
|
return false;
|
|
}
|
|
|
|
if (!EditorMaterialComponentUtil::SaveSourceMaterialFromEditData(exportItem.m_exportPath, editData))
|
|
{
|
|
AZ_Warning("AZ::Render::EditorMaterialComponentExporter", false, "Failed to save material data.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} // namespace EditorMaterialComponentExporter
|
|
} // namespace Render
|
|
} // namespace AZ
|