You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
16 KiB
C++
359 lines
16 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 "MaterialBuilder.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.Edit/Common/AssetUtils.h>
|
|
#include <Atom/RPI.Edit/Common/JsonFileLoadContext.h>
|
|
#include <Atom/RPI.Edit/Common/JsonReportingHelper.h>
|
|
#include <AtomCore/Serialization/Json/JsonUtils.h>
|
|
|
|
#include <Atom/RPI.Reflect/Shader/ShaderResourceGroupAsset.h>
|
|
#include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
|
|
#include <Atom/RPI.Reflect/Material/MaterialAssetCreator.h>
|
|
#include <Atom/RPI.Reflect/Material/MaterialFunctor.h>
|
|
|
|
#include <AzFramework/IO/LocalFileIO.h>
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
|
|
|
#include <AssetBuilderSDK/AssetBuilderSDK.h>
|
|
#include <AssetBuilderSDK/SerializationDependencies.h>
|
|
|
|
#include <AzCore/IO/IOUtils.h>
|
|
#include <AzCore/IO/Path/Path.h>
|
|
#include <AzCore/Serialization/Json/JsonSerialization.h>
|
|
|
|
namespace AZ
|
|
{
|
|
namespace RPI
|
|
{
|
|
namespace
|
|
{
|
|
static constexpr char const MaterialBuilderName[] = "MaterialBuilder";
|
|
}
|
|
|
|
const char* MaterialBuilder::JobKey = "Atom Material Builder";
|
|
|
|
void MaterialBuilder::RegisterBuilder()
|
|
{
|
|
AssetBuilderSDK::AssetBuilderDesc materialBuilderDescriptor;
|
|
materialBuilderDescriptor.m_name = JobKey;
|
|
materialBuilderDescriptor.m_version = 107; // ATOM-14918
|
|
materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.material", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
|
materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.materialtype", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
|
materialBuilderDescriptor.m_busId = azrtti_typeid<MaterialBuilder>();
|
|
materialBuilderDescriptor.m_createJobFunction = AZStd::bind(&MaterialBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
|
|
materialBuilderDescriptor.m_processJobFunction = AZStd::bind(&MaterialBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
|
|
|
|
BusConnect(materialBuilderDescriptor.m_busId);
|
|
|
|
AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, materialBuilderDescriptor);
|
|
}
|
|
|
|
MaterialBuilder::~MaterialBuilder()
|
|
{
|
|
BusDisconnect();
|
|
}
|
|
|
|
void AddPossibleJobDependencies(const char* jobKey, AZStd::string_view currentFilePath, AZStd::string_view referencedParentPath, AZStd::vector<AssetBuilderSDK::JobDependency>& jobDependencies)
|
|
{
|
|
AZStd::vector<AZStd::string> possibleDependencies = AssetUtils::GetPossibleDepenencyPaths(currentFilePath, referencedParentPath);
|
|
for (auto& file : possibleDependencies)
|
|
{
|
|
AssetBuilderSDK::JobDependency jobDependency;
|
|
jobDependency.m_jobKey = jobKey;
|
|
jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
|
|
jobDependency.m_sourceFile.m_sourceFileDependencyPath = file;
|
|
jobDependencies.push_back(jobDependency);
|
|
}
|
|
}
|
|
|
|
template<typename MaterialSourceDataT>
|
|
AZ::Outcome<MaterialSourceDataT> LoadSourceData(const rapidjson::Value& value, const AZStd::string& filePath)
|
|
{
|
|
MaterialSourceDataT material;
|
|
|
|
JsonDeserializerSettings settings;
|
|
|
|
JsonReportingHelper reportingHelper;
|
|
reportingHelper.Attach(settings);
|
|
|
|
// This is required by some custom material serializers to support relative path references.
|
|
JsonFileLoadContext fileLoadContext;
|
|
fileLoadContext.PushFilePath(filePath);
|
|
settings.m_metadata.Add(fileLoadContext);
|
|
|
|
JsonSerialization::Load(material, value, settings);
|
|
|
|
if (reportingHelper.ErrorsReported())
|
|
{
|
|
return AZ::Failure();
|
|
}
|
|
else if (reportingHelper.WarningsReported())
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "Warnings reported while loading '%s'", filePath.c_str());
|
|
return AZ::Failure();
|
|
}
|
|
else
|
|
{
|
|
return AZ::Success(AZStd::move(material));
|
|
}
|
|
}
|
|
|
|
void MaterialBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
|
|
{
|
|
if (m_isShuttingDown)
|
|
{
|
|
response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
|
|
return;
|
|
}
|
|
|
|
// We'll build up this one JobDescriptor and reuse it to register each of the platforms
|
|
AssetBuilderSDK::JobDescriptor outputJobDescriptor;
|
|
outputJobDescriptor.m_jobKey = JobKey;
|
|
|
|
// Load the file so we can detect and report dependencies.
|
|
// If the file is a .materialtype, report dependencies on the .shader files.
|
|
// If the file is a .material, report a dependency on the .materialtype and parent .material file
|
|
{
|
|
AZStd::string fullSourcePath;
|
|
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), fullSourcePath, true);
|
|
|
|
auto loadOutcome = JsonSerializationUtils::ReadJsonFile(fullSourcePath);
|
|
if (!loadOutcome.IsSuccess())
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "%s", loadOutcome.GetError().c_str());
|
|
return;
|
|
}
|
|
|
|
rapidjson::Document& document = loadOutcome.GetValue();
|
|
|
|
const bool isMaterialTypeFile = AzFramework::StringFunc::Path::IsExtension(request.m_sourceFile.c_str(), MaterialTypeSourceData::Extension);
|
|
if (isMaterialTypeFile)
|
|
{
|
|
auto materialTypeSourceData = MaterialUtils::LoadMaterialTypeSourceData(fullSourcePath, &document);
|
|
|
|
if (!materialTypeSourceData.IsSuccess())
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (auto& shader : materialTypeSourceData.GetValue().m_shaderCollection)
|
|
{
|
|
AddPossibleJobDependencies("Shader Asset", request.m_sourceFile, shader.m_shaderFilePath, outputJobDescriptor.m_jobDependencyList);
|
|
}
|
|
|
|
for (auto& functor : materialTypeSourceData.GetValue().m_materialFunctorSourceData)
|
|
{
|
|
auto dependencies = functor->GetActualSourceData()->GetAssetDependencies();
|
|
|
|
for (const MaterialFunctorSourceData::AssetDependency& dependency : dependencies)
|
|
{
|
|
AddPossibleJobDependencies(dependency.m_jobKey.c_str(), request.m_sourceFile, dependency.m_sourceFilePath, outputJobDescriptor.m_jobDependencyList);
|
|
}
|
|
}
|
|
}
|
|
else // it's a .material file
|
|
{
|
|
// Note we don't use the LoadMaterial() utility function or JsonSerializer here because we don't care about fully
|
|
// processing the material file at this point and reporting on the many things that could go wrong. We just want
|
|
// to report the parent material and material type dependencies. So using rapidjson directly is actually simpler.
|
|
|
|
AZStd::string materialTypePath;
|
|
AZStd::string parentMaterialPath;
|
|
|
|
auto& variantData = document;
|
|
|
|
const char* const materialTypeField = "materialType";
|
|
const char* const parentMaterialField = "parentMaterial";
|
|
|
|
if (variantData.IsObject() && variantData.HasMember(materialTypeField) && variantData[materialTypeField].IsString())
|
|
{
|
|
materialTypePath = variantData[materialTypeField].GetString();
|
|
}
|
|
|
|
if (variantData.IsObject() && variantData.HasMember(parentMaterialField) && variantData[parentMaterialField].IsString())
|
|
{
|
|
parentMaterialPath = variantData[parentMaterialField].GetString();
|
|
}
|
|
|
|
if (parentMaterialPath.empty())
|
|
{
|
|
parentMaterialPath = materialTypePath;
|
|
}
|
|
|
|
// Register dependency on the parent material source file so we can load it and use it's data to build this variant material.
|
|
// Note, we don't need a direct dependency on the material type because the parent material will depend on it.
|
|
AddPossibleJobDependencies(JobKey, request.m_sourceFile, parentMaterialPath, outputJobDescriptor.m_jobDependencyList);
|
|
}
|
|
}
|
|
|
|
// Create the output jobs for each platform
|
|
for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms)
|
|
{
|
|
outputJobDescriptor.SetPlatformIdentifier(platformInfo.m_identifier.c_str());
|
|
|
|
for (auto& jobDependency : outputJobDescriptor.m_jobDependencyList)
|
|
{
|
|
jobDependency.m_platformIdentifier = platformInfo.m_identifier;
|
|
}
|
|
|
|
response.m_createJobOutputs.push_back(outputJobDescriptor);
|
|
}
|
|
|
|
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
|
|
}
|
|
|
|
AZ::Data::Asset<MaterialTypeAsset> CreateMaterialTypeAsset(AZStd::string_view materialTypeSourceFilePath, const rapidjson::Value& json)
|
|
{
|
|
auto materialType = MaterialUtils::LoadMaterialTypeSourceData(materialTypeSourceFilePath, &json);
|
|
|
|
if (!materialType.IsSuccess())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
auto materialTypeAssetOutcome = materialType.GetValue().CreateMaterialTypeAsset(Uuid::CreateRandom(), materialTypeSourceFilePath, true);
|
|
if (!materialTypeAssetOutcome.IsSuccess())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
return materialTypeAssetOutcome.GetValue();
|
|
}
|
|
|
|
AZ::Data::Asset<MaterialAsset> CreateMaterialAsset(AZStd::string_view materialSourceFilePath, const rapidjson::Value& json)
|
|
{
|
|
auto material = LoadSourceData<MaterialSourceData>(json, materialSourceFilePath);
|
|
|
|
if (!material.IsSuccess())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
auto materialAssetOutcome = material.GetValue().CreateMaterialAsset(Uuid::CreateRandom(), materialSourceFilePath, true);
|
|
if (!materialAssetOutcome.IsSuccess())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
return materialAssetOutcome.GetValue();
|
|
}
|
|
|
|
void MaterialBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
|
|
{
|
|
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
|
|
|
|
if (jobCancelListener.IsCancelled())
|
|
{
|
|
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
|
|
return;
|
|
}
|
|
if (m_isShuttingDown)
|
|
{
|
|
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
|
|
return;
|
|
}
|
|
|
|
const bool isMaterialTypeFile = AzFramework::StringFunc::Path::IsExtension(request.m_sourceFile.c_str(), MaterialTypeSourceData::Extension);
|
|
|
|
AZStd::string fullSourcePath;
|
|
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), fullSourcePath, true);
|
|
|
|
auto loadOutcome = JsonSerializationUtils::ReadJsonFile(fullSourcePath);
|
|
if (!loadOutcome.IsSuccess())
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "Failed to load material file: %s", loadOutcome.GetError().c_str());
|
|
return;
|
|
}
|
|
|
|
rapidjson::Document& document = loadOutcome.GetValue();
|
|
|
|
AZStd::string materialProductPath;
|
|
AZStd::string fileNameNoExt;
|
|
AzFramework::StringFunc::Path::GetFileName(request.m_sourceFile.c_str(), fileNameNoExt);
|
|
|
|
AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), fileNameNoExt.c_str(), materialProductPath, true);
|
|
AzFramework::StringFunc::Path::ReplaceExtension(materialProductPath, isMaterialTypeFile ? MaterialTypeAsset::Extension : MaterialAsset::Extension);
|
|
|
|
if (isMaterialTypeFile)
|
|
{
|
|
// Load the material type file and create the MaterialTypeAsset object
|
|
AZ::Data::Asset<MaterialTypeAsset> materialTypeAsset;
|
|
materialTypeAsset = CreateMaterialTypeAsset(request.m_sourceFile, document);
|
|
|
|
if (!materialTypeAsset)
|
|
{
|
|
// Errors will have been reported above
|
|
return;
|
|
}
|
|
|
|
// [ATOM-13190] Change this back to ST_BINARY. It's ST_XML temporarily for debugging.
|
|
if (!AZ::Utils::SaveObjectToFile(materialProductPath, AZ::DataStream::ST_XML, materialTypeAsset.Get()))
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "Failed to save material type to file '%s'!", materialProductPath.c_str());
|
|
return;
|
|
}
|
|
|
|
AssetBuilderSDK::JobProduct jobProduct;
|
|
if (!AssetBuilderSDK::OutputObject(materialTypeAsset.Get(), materialProductPath, azrtti_typeid<RPI::MaterialTypeAsset>(), 0, jobProduct))
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "Failed to output product dependencies.");
|
|
return;
|
|
}
|
|
|
|
response.m_outputProducts.push_back(AZStd::move(jobProduct));
|
|
|
|
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
|
|
}
|
|
else
|
|
{
|
|
// Load the material file and create the MaterialAsset object
|
|
AZ::Data::Asset<MaterialAsset> materialAsset;
|
|
materialAsset = CreateMaterialAsset(request.m_sourceFile, document);
|
|
|
|
if (!materialAsset)
|
|
{
|
|
// Errors will have been reported above
|
|
return;
|
|
}
|
|
|
|
// [ATOM-13190] Change this back to ST_BINARY. It's ST_XML temporarily for debugging.
|
|
if (!AZ::Utils::SaveObjectToFile(materialProductPath, AZ::DataStream::ST_XML, materialAsset.Get()))
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "Failed to save material to file '%s'!", materialProductPath.c_str());
|
|
return;
|
|
}
|
|
|
|
AssetBuilderSDK::JobProduct jobProduct;
|
|
if (!AssetBuilderSDK::OutputObject(materialAsset.Get(), materialProductPath, azrtti_typeid<RPI::MaterialAsset>(), 0, jobProduct))
|
|
{
|
|
AZ_Error(MaterialBuilderName, false, "Failed to output product dependencies.");
|
|
return;
|
|
}
|
|
|
|
response.m_outputProducts.push_back(AZStd::move(jobProduct));
|
|
|
|
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
|
|
}
|
|
}
|
|
|
|
void MaterialBuilder::ShutDown()
|
|
{
|
|
m_isShuttingDown = true;
|
|
}
|
|
} // namespace RPI
|
|
} // namespace AZ
|