Files
o3de/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp
T
2021-03-08 14:30:57 -08:00

346 lines
19 KiB
C++

/*
* 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 <Material/EditorMaterialComponentExporter.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <Atom/RPI.Edit/Common/AssetUtils.h>
#include <Atom/RPI.Edit/Common/JsonUtils.h>
#include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
#include <Atom/RPI.Edit/Material/MaterialSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialUtils.h>
#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
#include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
#include <Atom/RPI.Reflect/Material/MaterialTypeAsset.h>
#include <AzQtComponents/Components/Widgets/BrowseEdit.h>
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
#include <QApplication>
#include <QTableWidget>
#include <QHeaderView>
#include <QFileDialog>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#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 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());
switch (exportItem.m_exportAction)
{
case ExportAction::Nothing:
return QString("Do not generate a new material. Use the existing one.");
case ExportAction::UseExisting:
if (fileInfo == QFileInfo())
{
return QString("A valid material file path is required.");
}
if (!fileInfo.exists())
{
return QString("\"%1\" does not exist.").arg(fileInfo.fileName());
}
return QString("Use existing \"%1\".").arg(fileInfo.fileName());
case ExportAction::GenerateNew:
if (fileInfo == QFileInfo())
{
return QString("A valid material file path is required.");
}
if (!fileInfo.exists())
{
return QString("\"%1\" will be generated in the designated folder.").arg(fileInfo.fileName());
}
return QString("\"%1\" will be replaced in the designated folder.").arg(fileInfo.fileName());
}
QString errorMessage = QString("Unable to determine status for \"%1\".").arg(fileInfo.fileName());
AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, errorMessage.toUtf8().constData());
return errorMessage;
}
bool OpenExportDialog(ExportItemsContainer& exportItems)
{
// Constructing a dialog with a table to display all configurable material export items
QDialog dialog(QApplication::activeWindow());
dialog.setWindowTitle("Generate Editable Materials");
dialog.setFixedSize(1000, 200);
const QStringList actionLabels = { "Do Nothing", "Use Existing", "Generate New" };
const QStringList headerLabels = { "Material", "Action", "Editable source file", "Status" };
const int MaterialColumn = 0;
const int ActionColumn = 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);
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();
exportItem.m_exportAction = fileInfo.exists() ? ExportAction::UseExisting : ExportAction::GenerateNew;
// Populate the table with data for every column
tableWidget->setItem(row, MaterialColumn, new QTableWidgetItem(GetLabelByAssetId(exportItem.m_assetId).c_str()));
tableWidget->setItem(row, ActionColumn, new QTableWidgetItem(actionLabels[(int)exportItem.m_exportAction]));
tableWidget->setItem(row, FileColumn, new QTableWidgetItem(fileInfo.fileName()));
// Create a combo box that will be used for displaying and selecting which action will be performed for each material
QComboBox* actionComboBox = new QComboBox(tableWidget);
actionComboBox->setEditable(false);
actionComboBox->addItems(actionLabels);
actionComboBox->setCurrentIndex((int)exportItem.m_exportAction);
tableWidget->setCellWidget(row, ActionColumn, actionComboBox);
// Create a file picker widget that will be used 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(actionComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), actionComboBox, [&dialog, &exportItem, actionComboBox, fileWidget, statusWidget]([[maybe_unused]] int index) {
exportItem.m_exportAction = (ExportAction)actionComboBox->currentIndex();
fileWidget->setEnabled(exportItem.m_exportAction != ExportAction::Nothing);
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, actionComboBox, fileWidget, statusWidget]() {
QFileInfo fileInfo = QFileDialog::getSaveFileName(&dialog,
QString("Choose Export Filename"),
exportItem.m_exportPath.c_str(),
QString("Material (*.material)"));
if (fileInfo != QFileInfo())
{
// Only update the export data if a valid path and filename was selected
exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData();
// We will automatically update the action based on whether or not the file exists
exportItem.m_exportAction = fileInfo.exists() ? ExportAction::UseExisting : ExportAction::GenerateNew;
// Update the controls to display the new state
fileWidget->setText(fileInfo.fileName());
actionComboBox->setCurrentIndex((int)exportItem.m_exportAction);
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.done(1); });
QPushButton* cancelButton = new QPushButton("Cancel", buttonRow);
QObject::connect(cancelButton, &QPushButton::clicked, cancelButton, [&dialog] { dialog.done(0); });
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 actions to generate the editable material source files", &dialog);
QVBoxLayout* dialogLayout = new QVBoxLayout(&dialog);
dialogLayout->addWidget(labelWidget);
dialogLayout->addWidget(tableWidget);
dialogLayout->addWidget(buttonRow);
dialog.setLayout(dialogLayout);
// Return true if the user press the export button
return dialog.exec() == 1;
}
bool ExportMaterialSourceData(const ExportItem& exportItem)
{
// Load the originating product asset from which the new source has set will be generated
auto materialAssetOutcome = AZ::RPI::AssetUtils::LoadAsset<AZ::RPI::MaterialAsset>(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<AZ::RPI::MaterialAsset> materialAsset = materialAssetOutcome.GetValue();
AZ::Data::Asset<AZ::RPI::MaterialTypeAsset> 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
AZ::RPI::MaterialTypeSourceData materialTypeSourceData;
if (!AZ::RPI::JsonUtils::LoadObjectFromFile(materialTypePath, materialTypeSourceData))
{
AZ_Error("AZ::Render::EditorMaterialComponentExporter", false, "Failed to get load material type source data: %s", materialTypePath.c_str());
return false;
}
// 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