Removing material builder and related tests

Signed-off-by: Guthrie Adams <guthadam@amazon.com>
monroegm-disable-blank-issue-2
Guthrie Adams 4 years ago
parent 92c0e598d5
commit 44f2cbae47

@ -1,612 +0,0 @@
/*
* 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 "MaterialBuilderComponent.h"
#include <AzCore/IO/Path/Path.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContextConstants.inl>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzCore/Debug/Trace.h>
#include <AzCore/XML/rapidxml.h>
#include <cctype>
namespace MaterialBuilder
{
[[maybe_unused]] const char s_materialBuilder[] = "MaterialBuilder";
namespace Internal
{
const char g_nodeNameMaterial[] = "Material";
const char g_nodeNameSubmaterial[] = "SubMaterials";
const char g_nodeNameTexture[] = "Texture";
const char g_nodeNameTextures[] = "Textures";
const char g_attributeFileName[] = "File";
const int g_numSourceImageFormats = 9;
const char* g_sourceImageFormats[g_numSourceImageFormats] = { ".tif", ".tiff", ".bmp", ".gif", ".jpg", ".jpeg", ".tga", ".png", ".dds" };
bool IsSupportedImageExtension(const AZStd::string& extension)
{
for (const char* format : g_sourceImageFormats)
{
if (extension == format)
{
return true;
}
}
return false;
}
// Cleans up legacy pathing from older materials
const char* CleanLegacyPathingFromTexturePath(const char* texturePath)
{
// Copied from MaterialHelpers::SetTexturesFromXml, line 459
// legacy. Some textures used to be referenced using "engine\\" or "engine/" - this is no longer valid
if (
(strlen(texturePath) > 7) &&
(azstrnicmp(texturePath, "engine", 6) == 0) &&
((texturePath[6] == '\\') || (texturePath[6] == '/'))
)
{
texturePath = texturePath + 7;
}
// legacy: Files were saved into a mtl with many leading forward or back slashes, we eat them all here. We want it to start with a relative path.
const char* actualFileName = texturePath;
while ((actualFileName[0]) && ((actualFileName[0] == '\\') || (actualFileName[0] == '/')))
{
++actualFileName;
}
return actualFileName;
}
// Parses the material XML for all texture paths
AZ::Outcome<AZStd::string, AZStd::string> GetTexturePathsFromMaterial(AZ::rapidxml::xml_node<char>* materialNode, AZStd::vector<AZStd::string>& paths)
{
AZ::Outcome<AZStd::string, AZStd::string> resultOutcome = AZ::Failure(AZStd::string(""));
AZStd::string success_with_warning_message;
// check if this material has a set of textures defined, and if so, grab all the paths from the textures
AZ::rapidxml::xml_node<char>* texturesNode = materialNode->first_node(g_nodeNameTextures);
if (texturesNode)
{
AZ::rapidxml::xml_node<char>* textureNode = texturesNode->first_node(g_nodeNameTexture);
// it is possible for an empty <Textures> node to exist for things like collision materials, so check
// to make sure that there is at least one child <Texture> node before starting to iterate.
if (textureNode)
{
do
{
AZ::rapidxml::xml_attribute<char>* fileAttribute = textureNode->first_attribute(g_attributeFileName);
if (!fileAttribute)
{
success_with_warning_message = "Texture node exists but does not have a file attribute defined";
}
else
{
const char* rawTexturePath = fileAttribute->value();
// do an initial clean-up of the path taken from the file, similar to MaterialHelpers::SetTexturesFromXml
AZStd::string texturePath = CleanLegacyPathingFromTexturePath(rawTexturePath);
paths.emplace_back(AZStd::move(texturePath));
}
textureNode = textureNode->next_sibling(g_nodeNameTexture);
} while (textureNode);
}
}
// check to see if this material has sub materials defined. If so, recurse into this function for each sub material
AZ::rapidxml::xml_node<char>* subMaterialsNode = materialNode->first_node(g_nodeNameSubmaterial);
if (subMaterialsNode)
{
AZ::rapidxml::xml_node<char>* subMaterialNode = subMaterialsNode->first_node(g_nodeNameMaterial);
if (subMaterialNode == nullptr)
{
// this is a malformed material as there is no material node child in the SubMaterials node, so error out
return AZ::Failure(AZStd::string("SubMaterials node exists but does not have any child Material nodes."));
}
do
{
// grab the texture paths from the submaterial, or error out if necessary
AZ::Outcome<AZStd::string, AZStd::string> subMaterialTexturePathsResult = GetTexturePathsFromMaterial(subMaterialNode, paths);
if (!subMaterialTexturePathsResult.IsSuccess())
{
return subMaterialTexturePathsResult;
}
else if (!subMaterialTexturePathsResult.GetValue().empty())
{
success_with_warning_message = subMaterialTexturePathsResult.GetValue();
}
subMaterialNode = subMaterialNode->next_sibling(g_nodeNameMaterial);
} while (subMaterialNode);
}
if (texturesNode == nullptr && subMaterialsNode == nullptr)
{
return AZ::Failure(AZStd::string("Failed to find a Textures node or SubMaterials node in this material. At least one of these must exist to be able to gather texture dependencies."));
}
if (!success_with_warning_message.empty())
{
return AZ::Success(success_with_warning_message);
}
return AZ::Success(AZStd::string());
}
// find a sequence of digits with a string starting from lastDigitIndex, and try to parse that sequence to and int
// and store it in outAnimIndex.
bool ParseFilePathForCompleteNumber(const AZStd::string& filePath, int& lastDigitIndex, int& outAnimIndex)
{
int firstAnimIndexDigit = lastDigitIndex;
while (isdigit(static_cast<unsigned char>(filePath[lastDigitIndex])))
{
++lastDigitIndex;
}
if (!AzFramework::StringFunc::LooksLikeInt(filePath.substr(firstAnimIndexDigit, lastDigitIndex - firstAnimIndexDigit).c_str(), &outAnimIndex))
{
return false;
}
return true;
}
// Parse the texture path for a texture animation to determine the actual names of the textures to resolve that
// make up the entire sequence.
AZ::Outcome<void, AZStd::string> GetAllTexturesInTextureSequence(const AZStd::string& path, AZStd::vector<AZStd::string>& texturesInSequence)
{
// Taken from CShaderMan::mfReadTexSequence
// All comments next to variable declarations in this function are the original variable names in
// CShaderMan::mfReadTexSequence, to help keep track of how these variables relate to the original function
AZStd::string prefix;
AZStd::string postfix;
AZStd::string filePath = path; // name
AZStd::string extension; // ext
AzFramework::StringFunc::Path::GetExtension(filePath.c_str(), extension);
AzFramework::StringFunc::Path::StripExtension(filePath);
// unsure if it is actually possible to enter here or the original version with '$' as the indicator
// for texture sequences, but they check for both just in case, so this will match the behavior.
char separator = '#'; // chSep
int firstSeparatorIndex = static_cast<int>(filePath.find(separator));
if (firstSeparatorIndex == AZStd::string::npos)
{
firstSeparatorIndex = static_cast<int>(filePath.find('$'));
if (firstSeparatorIndex == AZStd::string::npos)
{
return AZ::Failure(AZStd::string("Failed to find separator '#' or '$' in texture path."));
}
separator = '$';
}
// we don't actually care about getting the speed of the animation, so just remove everything from the
// end of the string starting with the last open parenthesis
size_t speedStartIndex = filePath.find_last_of('(');
if (speedStartIndex != AZStd::string::npos)
{
AzFramework::StringFunc::LKeep(filePath, speedStartIndex);
AzFramework::StringFunc::Append(filePath, '\0');
}
// try to find where the digits start after the separator (there can be any number of separators
// between the texture name prefix and where the digit range starts)
int firstAnimIndexDigit = -1; // m
int numSeparators = 0; // j
for (int stringIndex = firstSeparatorIndex; stringIndex < filePath.length(); ++stringIndex)
{
if (filePath[stringIndex] == separator)
{
++numSeparators;
if (firstSeparatorIndex == -1)
{
firstSeparatorIndex = stringIndex;
}
}
else if (firstSeparatorIndex > 0 && firstAnimIndexDigit < 0)
{
firstAnimIndexDigit = stringIndex;
break;
}
}
if (numSeparators == 0)
{
return AZ::Failure(AZStd::string("Failed to find separator '#' or '$' in texture path."));
}
// store off everything before the separator
prefix = AZStd::move(filePath.substr(0, firstSeparatorIndex));
int startAnimIndex = 0; // startn
int endAnimIndex = 0; // endn
// we only found the separator, but no indexes, so just assume its 0 - 999
if (firstAnimIndexDigit < 0)
{
startAnimIndex = 0;
endAnimIndex = 999;
}
else
{
// find the length of the first index, then parse that to an int
int lastDigitIndex = firstAnimIndexDigit;
if (!ParseFilePathForCompleteNumber(filePath, lastDigitIndex, startAnimIndex))
{
return AZ::Failure(AZStd::string("Failed to determine first index of the sequence after the separators in texture path."));
}
// reset to the start of the next index
++lastDigitIndex;
// find the length of the end index, then parse that to an int
if (!ParseFilePathForCompleteNumber(filePath, lastDigitIndex, endAnimIndex))
{
return AZ::Failure(AZStd::string("Failed to determine last index of the sequence after the first index of the sequence in texture path."));
}
// save off the rest of the string
postfix = AZStd::move(filePath.substr(lastDigitIndex));
}
int numTextures = endAnimIndex - startAnimIndex + 1;
const char* textureNameFormat = "%s%.*d%s%s"; // prefix, num separators (number of digits), sequence index, postfix, extension)
for (int sequenceIndex = 0; sequenceIndex < numTextures; ++sequenceIndex)
{
texturesInSequence.emplace_back(AZStd::move(AZStd::string::format(textureNameFormat, prefix.c_str(), numSeparators, startAnimIndex + sequenceIndex, postfix.c_str(), extension.c_str())));
}
return AZ::Success();
}
// Determine which product path to use based on the path stored in the texture, and make it relative to
// the cache.
bool ResolveMaterialTexturePath(const AZStd::string& path, AZStd::string& outPath)
{
AZStd::string aliasedPath = path;
//if its a source image format try to load the dds
AZStd::string extension;
bool hasExtension = AzFramework::StringFunc::Path::GetExtension(path.c_str(), extension);
// Replace all supported extensions with DDS if it has an extension. If the extension exists but is not supported, fail out.
if (hasExtension && IsSupportedImageExtension(extension))
{
AzFramework::StringFunc::Path::ReplaceExtension(aliasedPath, ".dds");
}
else if (hasExtension)
{
AZ_Warning(s_materialBuilder, false, "Failed to resolve texture path %s as the path is not to a supported texture format. Please make sure that textures in materials are formats supported by Open 3D Engine.", aliasedPath.c_str());
return false;
}
AZStd::to_lower(aliasedPath.begin(), aliasedPath.end());
AzFramework::StringFunc::Path::Normalize(aliasedPath);
AZStd::string currentFolderSpecifier = AZStd::string::format(".%c", AZ_CORRECT_FILESYSTEM_SEPARATOR);
if (AzFramework::StringFunc::StartsWith(aliasedPath, currentFolderSpecifier))
{
AzFramework::StringFunc::Strip(aliasedPath, currentFolderSpecifier.c_str(), false, true);
}
AZStd::string resolvedPath;
char fullPathBuffer[AZ_MAX_PATH_LEN] = {};
// if there is an alias already at the front of the path, resolve it, and try to make it relative to the
// cache (@products@). If it can't, then error out.
// This case handles the possibility of aliases existing in texture paths in materials that is still supported
// by the legacy loading code, however it is not currently used, so the else path is always taken.
if (aliasedPath[0] == '@')
{
if (!AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(aliasedPath.c_str(), fullPathBuffer, AZ_MAX_PATH_LEN))
{
AZ_Warning(s_materialBuilder, false, "Failed to resolve the alias in texture path %s. Please make sure all aliases are registered with the engine.", aliasedPath.c_str());
return false;
}
resolvedPath = fullPathBuffer;
AzFramework::StringFunc::Path::Normalize(resolvedPath);
if (!AzFramework::StringFunc::Replace(resolvedPath, AZ::IO::FileIOBase::GetDirectInstance()->GetAlias("@products@"), ""))
{
AZ_Warning(s_materialBuilder, false, "Failed to resolve aliased texture path %s to be relative to the asset cache. Please make sure this alias resolves to a path within the asset cache.", aliasedPath.c_str());
return false;
}
}
else
{
resolvedPath = AZStd::move(aliasedPath);
}
// AP deferred path resolution requires UNIX separators and no leading separators, so clean up and convert here
if (AzFramework::StringFunc::StartsWith(resolvedPath, AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING))
{
AzFramework::StringFunc::Strip(resolvedPath, AZ_CORRECT_FILESYSTEM_SEPARATOR, false, true);
}
AzFramework::StringFunc::Replace(resolvedPath, AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING, "/");
outPath = AZStd::move(resolvedPath);
return true;
}
}
BuilderPluginComponent::BuilderPluginComponent()
{
}
BuilderPluginComponent::~BuilderPluginComponent()
{
}
void BuilderPluginComponent::Init()
{
}
void BuilderPluginComponent::Activate()
{
// Register material builder
AssetBuilderSDK::AssetBuilderDesc builderDescriptor;
builderDescriptor.m_name = "MaterialBuilderWorker";
builderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.mtl", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
builderDescriptor.m_busId = MaterialBuilderWorker::GetUUID();
builderDescriptor.m_version = 5;
builderDescriptor.m_createJobFunction = AZStd::bind(&MaterialBuilderWorker::CreateJobs, &m_materialBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2);
builderDescriptor.m_processJobFunction = AZStd::bind(&MaterialBuilderWorker::ProcessJob, &m_materialBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2);
// (optimization) this builder does not emit source dependencies:
builderDescriptor.m_flags |= AssetBuilderSDK::AssetBuilderDesc::BF_EmitsNoDependencies;
m_materialBuilder.BusConnect(builderDescriptor.m_busId);
EBUS_EVENT(AssetBuilderSDK::AssetBuilderBus, RegisterBuilderInformation, builderDescriptor);
}
void BuilderPluginComponent::Deactivate()
{
m_materialBuilder.BusDisconnect();
}
void BuilderPluginComponent::Reflect([[maybe_unused]] AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<BuilderPluginComponent, AZ::Component>()
->Version(1)
->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>({ AssetBuilderSDK::ComponentTags::AssetBuilder }));
}
}
MaterialBuilderWorker::MaterialBuilderWorker()
{
}
MaterialBuilderWorker::~MaterialBuilderWorker()
{
}
void MaterialBuilderWorker::ShutDown()
{
// This will be called on a different thread than the process job thread
m_isShuttingDown = true;
}
// This happens early on in the file scanning pass.
// This function should always create the same jobs and not do any checking whether the job is up to date.
void MaterialBuilderWorker::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
if (m_isShuttingDown)
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
return;
}
for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
{
AssetBuilderSDK::JobDescriptor descriptor;
descriptor.m_jobKey = "Material Builder Job";
descriptor.SetPlatformIdentifier(info.m_identifier.c_str());
descriptor.m_priority = 8; // meshes are more important (at 10) but mats are still pretty important.
response.m_createJobOutputs.push_back(descriptor);
}
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
// The request will contain the CreateJobResponse you constructed earlier, including any keys and
// values you placed into the hash table
void MaterialBuilderWorker::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
{
AZ_TracePrintf(AssetBuilderSDK::InfoWindow, "Starting Job.\n");
AZStd::string fileName;
AzFramework::StringFunc::Path::GetFullFileName(request.m_fullPath.c_str(), fileName);
AZStd::string destPath;
// Do all work inside the tempDirPath.
AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), fileName.c_str(), destPath, true);
AZ::IO::LocalFileIO fileIO;
if (!m_isShuttingDown && fileIO.Copy(request.m_fullPath.c_str(), destPath.c_str()) == AZ::IO::ResultCode::Success)
{
// Push assets back into the response's product list
// Assets you created in your temp path can be specified using paths relative to the temp path
// since that is assumed where you're writing stuff.
AZStd::string relPath = destPath;
AssetBuilderSDK::ProductPathDependencySet dependencyPaths;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
AssetBuilderSDK::JobProduct jobProduct(fileName);
bool dependencyResult = GatherProductDependencies(request.m_fullPath, dependencyPaths);
if (dependencyResult)
{
jobProduct.m_pathDependencies = AZStd::move(dependencyPaths);
jobProduct.m_dependenciesHandled = true; // We've output the dependencies immediately above so it's OK to tell the AP we've handled dependencies
}
else
{
AZ_Error(s_materialBuilder, false, "Dependency gathering for %s failed.", request.m_fullPath.c_str());
}
response.m_outputProducts.push_back(jobProduct);
}
else
{
if (m_isShuttingDown)
{
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Cancelled job %s because shutdown was requested.\n", request.m_fullPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
}
else
{
AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Error during processing job %s.\n", request.m_fullPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
}
}
}
bool MaterialBuilderWorker::GetResolvedTexturePathsFromMaterial(const AZStd::string& path, AZStd::vector<AZStd::string>& resolvedPaths)
{
if (!AZ::IO::SystemFile::Exists(path.c_str()))
{
AZ_Error(s_materialBuilder, false, "Failed to find material at path %s. Please make sure this material exists on disk.", path.c_str());
return false;
}
uint64_t fileSize = AZ::IO::SystemFile::Length(path.c_str());
if (fileSize == 0)
{
AZ_Error(s_materialBuilder, false, "Material at path %s is an empty file. Please make sure this material was properly saved to disk.", path.c_str());
return false;
}
AZStd::vector<char> buffer(fileSize + 1);
buffer[fileSize] = 0;
if (!AZ::IO::SystemFile::Read(path.c_str(), buffer.data()))
{
AZ_Error(s_materialBuilder, false, "Failed to read material at path %s. Please make sure the file is not open or being edited by another program.", path.c_str());
return false;
}
AZ::rapidxml::xml_document<char>* xmlDoc = azcreate(AZ::rapidxml::xml_document<char>, (), AZ::SystemAllocator, "Mtl builder temp XML Reader");
if (!xmlDoc->parse<AZ::rapidxml::parse_no_data_nodes>(buffer.data()))
{
azdestroy(xmlDoc, AZ::SystemAllocator, AZ::rapidxml::xml_document<char>);
AZ_Error(s_materialBuilder, false, "Failed to parse material at path %s into XML. Please make sure that the material was properly saved to disk.", path.c_str());
return false;
}
// if the first node in this file isn't a material, this must not actually be a material so it can't have deps
AZ::rapidxml::xml_node<char>* rootNode = xmlDoc->first_node(Internal::g_nodeNameMaterial);
if (!rootNode)
{
azdestroy(xmlDoc, AZ::SystemAllocator, AZ::rapidxml::xml_document<char>);
AZ_Error(s_materialBuilder, false, "Failed to find root material node for material at path %s. Please make sure that the material was properly saved to disk.", path.c_str());
return false;
}
AZStd::vector<AZStd::string> texturePaths;
// gather all textures in the material file
AZ::Outcome<AZStd::string, AZStd::string> texturePathsResult = Internal::GetTexturePathsFromMaterial(rootNode, texturePaths);
if (!texturePathsResult.IsSuccess())
{
azdestroy(xmlDoc, AZ::SystemAllocator, AZ::rapidxml::xml_document<char>);
AZ_Error(s_materialBuilder, false, "Failed to gather dependencies for %s as the material file is malformed. %s", path.c_str(), texturePathsResult.GetError().c_str());
return false;
}
else if (!texturePathsResult.GetValue().empty())
{
AZ_Warning(s_materialBuilder, false, "Some nodes in material %s could not be read as the material is malformed. %s. Some dependencies might not be reported correctly. Please make sure that the material was properly saved to disk.", path.c_str(), texturePathsResult.GetValue().c_str());
}
azdestroy(xmlDoc, AZ::SystemAllocator, AZ::rapidxml::xml_document<char>);
// fail this if there are absolute paths.
for (const AZStd::string& texPath : texturePaths)
{
if (AZ::IO::PathView(texPath).IsAbsolute())
{
AZ_Warning(s_materialBuilder, false, "Skipping resolving of texture path %s in material %s as the texture path is an absolute path. Please update the texture path to be relative to the asset cache.", texPath.c_str(), path.c_str());
texturePaths.erase(AZStd::find(texturePaths.begin(), texturePaths.end(), texPath));
}
}
// for each path in the array, split any texture animation entry up into the individual files and add each to the list.
for (const AZStd::string& texPath : texturePaths)
{
if (texPath.find('#') != AZStd::string::npos)
{
AZStd::vector<AZStd::string> actualTexturePaths;
AZ::Outcome<void, AZStd::string> parseTextureSequenceResult = Internal::GetAllTexturesInTextureSequence(texPath, actualTexturePaths);
if (parseTextureSequenceResult.IsSuccess())
{
texturePaths.erase(AZStd::find(texturePaths.begin(), texturePaths.end(), texPath));
texturePaths.insert(texturePaths.end(), actualTexturePaths.begin(), actualTexturePaths.end());
}
else
{
texturePaths.erase(AZStd::find(texturePaths.begin(), texturePaths.end(), texPath));
AZ_Warning(s_materialBuilder, false, "Failed to parse texture sequence %s when trying to gather dependencies for %s. %s Please make sure the texture sequence path is formatted correctly. Registering dependencies for the texture sequence will be skipped.", texPath.c_str(), path.c_str(), parseTextureSequenceResult.GetError().c_str());
}
}
}
// for each texture in the file
for (const AZStd::string& texPath : texturePaths)
{
// if the texture path starts with a '$' then it is a special runtime defined texture, so it it doesn't have
// an actual asset on disk to depend on. If the texture path doesn't have an extension, then it is a texture
// that is determined at runtime (such as 'nearest_cubemap'), so also ignore those, as other things pull in
// those dependencies.
if (AzFramework::StringFunc::StartsWith(texPath, "$") || !AzFramework::StringFunc::Path::HasExtension(texPath.c_str()))
{
continue;
}
// resolve the path in the file.
AZStd::string resolvedPath;
if (!Internal::ResolveMaterialTexturePath(texPath, resolvedPath))
{
AZ_Warning(s_materialBuilder, false, "Failed to resolve texture path %s to a product path when gathering dependencies for %s. Registering dependencies on this texture path will be skipped.", texPath.c_str(), path.c_str());
continue;
}
resolvedPaths.emplace_back(AZStd::move(resolvedPath));
}
return true;
}
bool MaterialBuilderWorker::PopulateProductDependencyList(AZStd::vector<AZStd::string>& resolvedPaths, AssetBuilderSDK::ProductPathDependencySet& dependencies)
{
for (const AZStd::string& texturePath : resolvedPaths)
{
if (texturePath.empty())
{
AZ_Warning(s_materialBuilder, false, "Resolved path is empty.\n");
return false;
}
dependencies.emplace(texturePath, AssetBuilderSDK::ProductPathDependencyType::ProductFile);
}
return true;
}
bool MaterialBuilderWorker::GatherProductDependencies(const AZStd::string& path, AssetBuilderSDK::ProductPathDependencySet& dependencies)
{
AZStd::vector<AZStd::string> resolvedTexturePaths;
if (!GetResolvedTexturePathsFromMaterial(path, resolvedTexturePaths))
{
return false;
}
if (!PopulateProductDependencyList(resolvedTexturePaths, dependencies))
{
AZ_Warning(s_materialBuilder, false, "Failed to populate dependency list for material %s with possible variants for textures.", path.c_str());
}
return true;
}
AZ::Uuid MaterialBuilderWorker::GetUUID()
{
return AZ::Uuid::CreateString("{258D34AC-12F8-4196-B535-3206D8E7287B}");
}
}

@ -1,65 +0,0 @@
/*
* 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
*
*/
#pragma once
#include <AzCore/Component/Component.h>
#include <AssetBuilderSDK/AssetBuilderBusses.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
namespace MaterialBuilder
{
//! Material builder is responsible for building material files
class MaterialBuilderWorker
: public AssetBuilderSDK::AssetBuilderCommandBus::Handler
{
public:
MaterialBuilderWorker();
~MaterialBuilderWorker();
//! Asset Builder Callback Functions
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
//!AssetBuilderSDK::AssetBuilderCommandBus interface
void ShutDown() override;
//! Returns the UUID for this builder
static AZ::Uuid GetUUID();
bool GetResolvedTexturePathsFromMaterial(const AZStd::string& path, AZStd::vector<AZStd::string>& resolvedPaths);
bool PopulateProductDependencyList(AZStd::vector<AZStd::string>& resolvedPaths, AssetBuilderSDK::ProductPathDependencySet& dependencies);
private:
bool GatherProductDependencies(const AZStd::string& path, AssetBuilderSDK::ProductPathDependencySet& dependencies);
bool m_isShuttingDown = false;
};
class BuilderPluginComponent
: public AZ::Component
{
public:
AZ_COMPONENT(BuilderPluginComponent, "{4D1A4B0C-54CE-4397-B8AE-ADD08898C2CD}")
static void Reflect(AZ::ReflectContext* context);
BuilderPluginComponent();
//////////////////////////////////////////////////////////////////////////
// AZ::Component
virtual void Init(); // create objects, allocate memory and initialize yourself without reaching out to the outside world
virtual void Activate(); // reach out to the outside world and connect up to what you need to, register things, etc.
virtual void Deactivate(); // unregister things, disconnect from the outside world
//////////////////////////////////////////////////////////////////////////
virtual ~BuilderPluginComponent(); // free memory an uninitialize yourself.
private:
MaterialBuilderWorker m_materialBuilder;
};
}

@ -43,7 +43,6 @@
#include <Builders/BenchmarkAssetBuilder/BenchmarkAssetBuilderComponent.h> #include <Builders/BenchmarkAssetBuilder/BenchmarkAssetBuilderComponent.h>
#include <Builders/LevelBuilder/LevelBuilderComponent.h> #include <Builders/LevelBuilder/LevelBuilderComponent.h>
#include <Builders/LuaBuilder/LuaBuilderComponent.h> #include <Builders/LuaBuilder/LuaBuilderComponent.h>
#include <Builders/MaterialBuilder/MaterialBuilderComponent.h>
#include <Builders/SliceBuilder/SliceBuilderComponent.h> #include <Builders/SliceBuilder/SliceBuilderComponent.h>
#include <Builders/TranslationBuilder/TranslationBuilderComponent.h> #include <Builders/TranslationBuilder/TranslationBuilderComponent.h>
#include "Builders/CopyDependencyBuilder/CopyDependencyBuilderComponent.h" #include "Builders/CopyDependencyBuilder/CopyDependencyBuilderComponent.h"
@ -84,7 +83,6 @@ namespace LmbrCentral
CopyDependencyBuilder::CopyDependencyBuilderComponent::CreateDescriptor(), CopyDependencyBuilder::CopyDependencyBuilderComponent::CreateDescriptor(),
DependencyBuilder::DependencyBuilderComponent::CreateDescriptor(), DependencyBuilder::DependencyBuilderComponent::CreateDescriptor(),
LevelBuilder::LevelBuilderComponent::CreateDescriptor(), LevelBuilder::LevelBuilderComponent::CreateDescriptor(),
MaterialBuilder::BuilderPluginComponent::CreateDescriptor(),
SliceBuilder::BuilderPluginComponent::CreateDescriptor(), SliceBuilder::BuilderPluginComponent::CreateDescriptor(),
TranslationBuilder::BuilderPluginComponent::CreateDescriptor(), TranslationBuilder::BuilderPluginComponent::CreateDescriptor(),
LuaBuilder::BuilderPluginComponent::CreateDescriptor(), LuaBuilder::BuilderPluginComponent::CreateDescriptor(),

@ -1,261 +0,0 @@
/*
* 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 <AzTest/AzTest.h>
#include <Builders/MaterialBuilder/MaterialBuilderComponent.h>
#include <AzTest/Utils.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/IO/FileIO.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Utils/Utils.h>
namespace UnitTest
{
using namespace MaterialBuilder;
using namespace AZ;
class MaterialBuilderTests
: public UnitTest::AllocatorsTestFixture
, public UnitTest::TraceBusRedirector
{
protected:
void SetUp() override
{
UnitTest::AllocatorsTestFixture::SetUp();
m_app.reset(aznew AzToolsFramework::ToolsApplication);
m_app->Start(AZ::ComponentApplication::Descriptor());
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests.
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
AZ::Debug::TraceMessageBus::Handler::BusConnect();
const AZStd::string engineRoot = AZ::Test::GetEngineRootPath();
AZ::IO::FileIOBase::GetInstance()->SetAlias("@engroot@", engineRoot.c_str());
AZ::IO::Path assetRoot(AZ::Utils::GetProjectPath());
assetRoot /= "Cache";
AZ::IO::FileIOBase::GetInstance()->SetAlias("@products@", assetRoot.c_str());
}
void TearDown() override
{
AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
m_app->Stop();
m_app.reset();
UnitTest::AllocatorsTestFixture::TearDown();
}
AZStd::string GetTestFileAliasedPath(AZStd::string_view fileName)
{
constexpr char testFileFolder[] = "@engroot@/Gems/LmbrCentral/Code/Tests/Materials/";
return AZStd::string::format("%s%.*s", testFileFolder, aznumeric_cast<int>(fileName.size()), fileName.data());
}
AZStd::string GetTestFileFullPath(AZStd::string_view fileName)
{
AZStd::string aliasedPath = GetTestFileAliasedPath(fileName);
char resolvedPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath(aliasedPath.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
return AZStd::string(resolvedPath);
}
void TestFailureCase(AZStd::string_view fileName, [[maybe_unused]] int expectedErrorCount)
{
MaterialBuilderWorker worker;
AZStd::vector<AZStd::string> resolvedPaths;
AZStd::string absoluteMatPath = GetTestFileFullPath(fileName);
AZ_TEST_START_ASSERTTEST;
ASSERT_FALSE(worker.GetResolvedTexturePathsFromMaterial(absoluteMatPath, resolvedPaths));
AZ_TEST_STOP_ASSERTTEST(expectedErrorCount * 2); // The assert tests double count AZ errors, so just multiply expected count by 2
ASSERT_EQ(resolvedPaths.size(), 0);
}
void TestSuccessCase(AZStd::string_view fileName, AZStd::vector<const char*>& expectedTextures)
{
MaterialBuilderWorker worker;
AZStd::vector<AZStd::string> resolvedPaths;
size_t texturesInMaterialFile = expectedTextures.size();
AZStd::string absoluteMatPath = GetTestFileFullPath(fileName);
ASSERT_TRUE(worker.GetResolvedTexturePathsFromMaterial(absoluteMatPath, resolvedPaths));
ASSERT_EQ(resolvedPaths.size(), texturesInMaterialFile);
if (texturesInMaterialFile > 0)
{
ASSERT_THAT(resolvedPaths, testing::ElementsAreArray(expectedTextures));
AssetBuilderSDK::ProductPathDependencySet dependencies;
ASSERT_TRUE(worker.PopulateProductDependencyList(resolvedPaths, dependencies));
ASSERT_EQ(dependencies.size(), texturesInMaterialFile);
}
}
void TestSuccessCase(AZStd::string_view fileName, const char* expectedTexture)
{
AZStd::vector<const char*> expectedTextures;
expectedTextures.push_back(expectedTexture);
TestSuccessCase(fileName, expectedTextures);
}
void TestSuccessCaseNoDependencies(AZStd::string_view fileName)
{
AZStd::vector<const char*> expectedTextures;
TestSuccessCase(fileName, expectedTextures);
}
AZStd::unique_ptr<AzToolsFramework::ToolsApplication> m_app;
};
TEST_F(MaterialBuilderTests, MaterialBuilder_EmptyFile_ExpectFailure)
{
// Should fail in MaterialBuilderWorker::GetResolvedTexturePathsFromMaterial, when checking for the size of the file.
TestFailureCase("test_mat1.mtl", 1);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_NoChildren_ExpectFailure)
{
// Should fail in MaterialBuilderWorker::GetResolvedTexturePathsFromMaterial after calling
// Internal::GetTexturePathsFromMaterial, which should return an AZ::Failure when both a Textures node and a
// SubMaterials node are not found. No other AZ_Errors should be generated.
TestFailureCase("test_mat2.mtl", 1);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_EmptyTexturesNode_NoDependencies)
{
TestSuccessCaseNoDependencies("test_mat3.mtl");
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_EmptySubMaterialNode_ExpectFailure)
{
// Should fail in MaterialBuilderWorker::GetResolvedTexturePathsFromMaterial after calling
// Internal::GetTexturePathsFromMaterial, which should return an AZ::Failure when a SubMaterials node is present,
// but has no children Material node. No other AZ_Errors should be generated.
TestFailureCase("test_mat4.mtl", 1);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_EmptyTextureNode_NoDependencies)
{
TestSuccessCaseNoDependencies("test_mat5.mtl");
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_EmptyMaterialInSubMaterial_ExpectFailure)
{
// Should fail in MaterialBuilderWorker::GetResolvedTexturePathsFromMaterial after calling
// Internal::GetTexturePathsFromMaterial, which should return an AZ::Failure when a SubMaterials node is present,
// but a child Material node has no child Textures node and no child SubMaterials node. No other AZ_Errors should
// be generated.
TestFailureCase("test_mat6.mtl", 1);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_EmptyTextureNodeInSubMaterial_NoDependencies)
{
TestSuccessCaseNoDependencies("test_mat7.mtl");
}
#if AZ_TRAIT_OS_USE_WINDOWS_FILE_PATHS // The following test file 'test_mat8.mtl' has a windows-specific absolute path, so this test is only valid on windows
TEST_F(MaterialBuilderTests, MaterialBuilder_TextureAbsolutePath_NoDependencies)
{
TestSuccessCaseNoDependencies("test_mat8.mtl");
}
#endif
TEST_F(MaterialBuilderTests, MaterialBuilder_TextureRuntimeAlias_NoDependencies)
{
TestSuccessCaseNoDependencies("test_mat9.mtl");
}
TEST_F(MaterialBuilderTests, MaterialBuilder_TextureRuntimeTexture_NoDependencies)
{
TestSuccessCaseNoDependencies("test_mat10.mtl");
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SingleMaterialSingleTexture_ValidSourceFormat)
{
// texture referenced is textures/natural/terrain/am_floor_tile_ddn.png
const char* expectedPath = "textures/natural/terrain/am_floor_tile_ddn.dds";
TestSuccessCase("test_mat11.mtl", expectedPath);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SingleMaterialSingleTexture_ValidProductFormat)
{
// texture referenced is textures/natural/terrain/am_floor_tile_ddn.dds
const char* expectedPath = "textures/natural/terrain/am_floor_tile_ddn.dds";
TestSuccessCase("test_mat12.mtl", expectedPath);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SingleMaterialSingleTexture_InvalidSourceFormat_NoDependenices)
{
// texture referenced is textures/natural/terrain/am_floor_tile_ddn.txt
TestSuccessCaseNoDependencies("test_mat13.mtl");
}
TEST_F(MaterialBuilderTests, MaterialBuilder_TextureAnimSequence)
{
AZStd::vector<const char*> expectedPaths = {
"path/to/my/textures/test_anim_sequence_01_texture000.dds",
"path/to/my/textures/test_anim_sequence_01_texture001.dds",
"path/to/my/textures/test_anim_sequence_01_texture002.dds",
"path/to/my/textures/test_anim_sequence_01_texture003.dds",
"path/to/my/textures/test_anim_sequence_01_texture004.dds",
"path/to/my/textures/test_anim_sequence_01_texture005.dds"
};
TestSuccessCase("test_mat14.mtl", expectedPaths);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SingleMaterialMultipleTexture)
{
AZStd::vector<const char*> expectedPaths = {
"engineassets/textures/hex.dds",
"engineassets/textures/hex_ddn.dds"
};
TestSuccessCase("test_mat15.mtl", expectedPaths);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_MalformedMaterial_MultipleTextures_OneEmptyTexture)
{
TestSuccessCase("test_mat16.mtl", "engineassets/textures/hex_ddn.dds");
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SingleMaterialMultipleTexture_ResolveLeadingSeparatorsAndAliases)
{
AZStd::vector<const char*> expectedPaths = {
"engineassets/textures/hex.dds", // resolved from "/engineassets/textures/hex.dds"
"engineassets/textures/hex_ddn.dds", // resolved from "./engineassets/textures/hex_ddn.dds"
"engineassets/textures/hex_spec.dds" // resolved from "@products@/engineassets/textures/hex_spec.dds"
};
TestSuccessCase("test_mat17.mtl", expectedPaths);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SubMaterialSingleTexture)
{
AZStd::vector<const char*> expectedPaths = {
"engineassets/textures/scratch.dds",
"engineassets/textures/perlinnoise2d.dds"
};
TestSuccessCase("test_mat18.mtl", expectedPaths);
}
TEST_F(MaterialBuilderTests, MaterialBuilder_SubMaterialMultipleTexture)
{
AZStd::vector<const char*> expectedPaths = {
"engineassets/textures/scratch.dds",
"engineassets/textures/scratch_ddn.dds",
"engineassets/textures/perlinnoise2d.dds",
"engineassets/textures/perlinnoisenormal_ddn.dds"
};
TestSuccessCase("test_mat19.mtl", expectedPaths);
}
}

@ -116,8 +116,6 @@ set(FILES
Source/Builders/LevelBuilder/LevelBuilderComponent.h Source/Builders/LevelBuilder/LevelBuilderComponent.h
Source/Builders/LevelBuilder/LevelBuilderWorker.cpp Source/Builders/LevelBuilder/LevelBuilderWorker.cpp
Source/Builders/LevelBuilder/LevelBuilderWorker.h Source/Builders/LevelBuilder/LevelBuilderWorker.h
Source/Builders/MaterialBuilder/MaterialBuilderComponent.cpp
Source/Builders/MaterialBuilder/MaterialBuilderComponent.h
Source/Builders/SliceBuilder/SliceBuilderComponent.cpp Source/Builders/SliceBuilder/SliceBuilderComponent.cpp
Source/Builders/SliceBuilder/SliceBuilderComponent.h Source/Builders/SliceBuilder/SliceBuilderComponent.h
Source/Builders/SliceBuilder/SliceBuilderWorker.cpp Source/Builders/SliceBuilder/SliceBuilderWorker.cpp

@ -21,7 +21,6 @@ set(FILES
Tests/Builders/CopyDependencyBuilderTest.cpp Tests/Builders/CopyDependencyBuilderTest.cpp
Tests/Builders/SliceBuilderTests.cpp Tests/Builders/SliceBuilderTests.cpp
Tests/Builders/LevelBuilderTest.cpp Tests/Builders/LevelBuilderTest.cpp
Tests/Builders/MaterialBuilderTests.cpp
Tests/Builders/LuaBuilderTests.cpp Tests/Builders/LuaBuilderTests.cpp
Tests/Builders/SeedBuilderTests.cpp Tests/Builders/SeedBuilderTests.cpp
Source/LmbrCentral.cpp Source/LmbrCentral.cpp

Loading…
Cancel
Save