MaterialBuilder.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "MaterialBuilder.h"
  9. #include "MaterialTypeBuilder.h"
  10. #include <Material/MaterialBuilderUtils.h>
  11. #include <Atom/RPI.Edit/Material/MaterialUtils.h>
  12. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  13. #include <Atom/RPI.Edit/Common/JsonUtils.h>
  14. #include <AzCore/Serialization/Json/JsonUtils.h>
  15. #include <AssetBuilderSDK/SerializationDependencies.h>
  16. #include <AzCore/Settings/SettingsRegistry.h>
  17. namespace AZ
  18. {
  19. namespace RPI
  20. {
  21. namespace
  22. {
  23. [[maybe_unused]] static constexpr char const MaterialBuilderName[] = "MaterialBuilder";
  24. }
  25. const char* MaterialBuilder::JobKey = "Material Builder";
  26. AZStd::string MaterialBuilder::GetBuilderSettingsFingerprint() const
  27. {
  28. return AZStd::string::format(
  29. "[%s %s]", MaterialBuilderName, ShouldReportMaterialAssetWarningsAsErrors() ? "WarningsAsErrorsOn" : "WarningsAsErrorsOff");
  30. }
  31. void MaterialBuilder::RegisterBuilder()
  32. {
  33. AssetBuilderSDK::AssetBuilderDesc materialBuilderDescriptor;
  34. materialBuilderDescriptor.m_name = JobKey;
  35. materialBuilderDescriptor.m_version = 141; // Replaced possible dependency utility function with explicit and wildcard job dependencies
  36. materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.material", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
  37. materialBuilderDescriptor.m_busId = azrtti_typeid<MaterialBuilder>();
  38. materialBuilderDescriptor.m_createJobFunction = AZStd::bind(&MaterialBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
  39. materialBuilderDescriptor.m_processJobFunction = AZStd::bind(&MaterialBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
  40. materialBuilderDescriptor.m_analysisFingerprint = GetBuilderSettingsFingerprint();
  41. BusConnect(materialBuilderDescriptor.m_busId);
  42. AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, materialBuilderDescriptor);
  43. }
  44. MaterialBuilder::~MaterialBuilder()
  45. {
  46. BusDisconnect();
  47. }
  48. bool MaterialBuilder::ShouldReportMaterialAssetWarningsAsErrors() const
  49. {
  50. bool warningsAsErrors = false;
  51. if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
  52. {
  53. settingsRegistry->Get(warningsAsErrors, "/O3DE/Atom/RPI/MaterialBuilder/WarningsAsErrors");
  54. }
  55. return warningsAsErrors;
  56. }
  57. void MaterialBuilder::CreateJobs(
  58. const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
  59. {
  60. if (m_isShuttingDown)
  61. {
  62. response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
  63. return;
  64. }
  65. // We'll build up this one JobDescriptor and reuse it to register each of the platforms
  66. AssetBuilderSDK::JobDescriptor outputJobDescriptor;
  67. outputJobDescriptor.m_jobKey = JobKey;
  68. outputJobDescriptor.m_additionalFingerprintInfo = GetBuilderSettingsFingerprint();
  69. AZStd::string materialSourcePath;
  70. AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), materialSourcePath, true);
  71. // Rather than just reading the JSON document, we read the material source data structure because we need access
  72. // to material type, parent material, and all of the properties to enumerate images and other dependencies.
  73. const auto materialSourceDataOutcome = MaterialUtils::LoadMaterialSourceData(materialSourcePath);
  74. if (!materialSourceDataOutcome)
  75. {
  76. AZ_Error(MaterialBuilderName, false, "Failed to load material source data: %s", materialSourcePath.c_str());
  77. return;
  78. }
  79. MaterialBuilderUtils::AddFingerprintForDependency(materialSourcePath, outputJobDescriptor);
  80. const auto& materialSourceData = materialSourceDataOutcome.GetValue();
  81. if (!materialSourceData.m_parentMaterial.empty())
  82. {
  83. // Register dependency on the parent material source file so we can load and use its data to build this material.
  84. MaterialBuilderUtils::AddJobDependency(
  85. outputJobDescriptor,
  86. AssetUtils::ResolvePathReference(materialSourcePath, materialSourceData.m_parentMaterial),
  87. JobKey,
  88. {},
  89. { 0 });
  90. }
  91. // Note that parentMaterialPath may have registered a dependency above, and the parent material reports dependency on the
  92. // material type as well, so there is a chain that propagates automatically, at least in some cases. However, that isn't
  93. // sufficient for all cases and a direct dependency on the material type is needed, because ProcessJob loads the parent material
  94. // and the material type independent of each other. Otherwise, edge cases are possible, where the material type changes in some
  95. // way that does not impact the parent material asset's final data, yet it does impact the child material. See
  96. // https://github.com/o3de/o3de/issues/13766
  97. if (!materialSourceData.m_materialType.empty())
  98. {
  99. // We usually won't load file during CreateJob since we want to keep the function fast. But here we have to load the
  100. // material type data to find the exact material type format so we could create an accurate source dependency.
  101. const auto materialResolvedPath = AssetUtils::ResolvePathReference(materialSourcePath, materialSourceData.m_materialType);
  102. const auto resolvedMaterialTypePath = MaterialUtils::PredictOriginalMaterialTypeSourcePath(materialResolvedPath);
  103. AZ_Warning(
  104. MaterialBuilderName,
  105. AZ::StringFunc::Equal(materialResolvedPath, resolvedMaterialTypePath),
  106. "Material type is referencing an asset in the intermediate or cache folder. Please update it with the proper path %s",
  107. resolvedMaterialTypePath.c_str());
  108. const auto& materialTypeSourceDataOutcome = MaterialUtils::LoadMaterialTypeSourceData(resolvedMaterialTypePath);
  109. if (!materialTypeSourceDataOutcome)
  110. {
  111. AZ_Error(MaterialBuilderName, false, "Failed to load material type source data: %s", resolvedMaterialTypePath.c_str());
  112. return;
  113. }
  114. const auto& materialTypeSourceData = materialTypeSourceDataOutcome.GetValue();
  115. const MaterialTypeSourceData::Format materialTypeFormat = materialTypeSourceData.GetFormat();
  116. // If the material uses the "Direct" format, then there will need to be a dependency on that file. If it uses the "Abstract"
  117. // format, then there will be an intermediate .materialtype and there needs to be a dependency on that file instead.
  118. if (materialTypeFormat == MaterialTypeSourceData::Format::Direct)
  119. {
  120. MaterialBuilderUtils::AddJobDependency(
  121. outputJobDescriptor, resolvedMaterialTypePath, MaterialTypeBuilder::FinalStageJobKey, {}, { 0 });
  122. for (const auto& shader : materialTypeSourceData.GetShaderReferences())
  123. {
  124. MaterialBuilderUtils::AddJobDependency(
  125. outputJobDescriptor,
  126. AssetUtils::ResolvePathReference(resolvedMaterialTypePath, shader.m_shaderFilePath),
  127. "Shader Asset");
  128. }
  129. }
  130. else if (materialTypeFormat == MaterialTypeSourceData::Format::Abstract)
  131. {
  132. // Create a dependency on the abstract, pipeline, version of the material type and its products. The pipeline based
  133. // material type builder uses the 'common' asset platform ID because it produces immediate assets. The sub ID filter
  134. // should remain empty to observe all produced intermediate assets.
  135. MaterialBuilderUtils::AddJobDependency(
  136. outputJobDescriptor,
  137. resolvedMaterialTypePath,
  138. MaterialTypeBuilder::PipelineStageJobKey,
  139. AssetBuilderSDK::CommonPlatformName);
  140. // The abstract, pipeline material type will generate a direct material type as an intermediate source asset. This
  141. // attempts to predict where that source asset will be located in the intermediate asset folder then maps it as a
  142. // product dependency if it exists or a source dependency if it is to be created in the future.
  143. const auto& intermediateMaterialTypePath =
  144. MaterialUtils::PredictIntermediateMaterialTypeSourcePath(resolvedMaterialTypePath);
  145. if (!intermediateMaterialTypePath.empty())
  146. {
  147. // Add the ordered product dependency for the intermediate material type source file so that the material cannot be
  148. // processed before it's complete
  149. MaterialBuilderUtils::AddJobDependency(
  150. outputJobDescriptor, intermediateMaterialTypePath, MaterialTypeBuilder::FinalStageJobKey, {}, { 0 });
  151. // Add a wild card job dependency for any of the shaders generated with the material type so the material will only
  152. // be processed after they are complete
  153. auto& jobDependency = MaterialBuilderUtils::AddJobDependency(
  154. outputJobDescriptor, intermediateMaterialTypePath, "Shader Asset", {}, {}, false);
  155. jobDependency.m_sourceFile.m_sourceDependencyType = AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards;
  156. AZ::StringFunc::Replace(jobDependency.m_sourceFile.m_sourceFileDependencyPath, "_generated.materialtype", "*.shader");
  157. }
  158. }
  159. }
  160. // Create the output jobs for each platform
  161. for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms)
  162. {
  163. outputJobDescriptor.SetPlatformIdentifier(platformInfo.m_identifier.c_str());
  164. for (auto& jobDependency : outputJobDescriptor.m_jobDependencyList)
  165. {
  166. if (jobDependency.m_platformIdentifier.empty())
  167. {
  168. jobDependency.m_platformIdentifier = platformInfo.m_identifier;
  169. }
  170. }
  171. response.m_createJobOutputs.push_back(outputJobDescriptor);
  172. }
  173. response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
  174. }
  175. void MaterialBuilder::ProcessJob(
  176. const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
  177. {
  178. AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
  179. if (jobCancelListener.IsCancelled())
  180. {
  181. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  182. return;
  183. }
  184. if (m_isShuttingDown)
  185. {
  186. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  187. return;
  188. }
  189. AZStd::string materialSourcePath;
  190. AzFramework::StringFunc::Path::ConstructFull(
  191. request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), materialSourcePath, true);
  192. const auto& materialSourceDataOutcome = MaterialUtils::LoadMaterialSourceData(materialSourcePath);
  193. if (!materialSourceDataOutcome)
  194. {
  195. AZ_Error(MaterialBuilderName, false, "Failed to load material source data: %s", materialSourcePath.c_str());
  196. return;
  197. }
  198. const auto& materialSourceData = materialSourceDataOutcome.GetValue();
  199. // Load the material file and create the MaterialAsset object
  200. const auto& materialAssetOutcome = materialSourceData.CreateMaterialAsset(
  201. Uuid::CreateRandom(), materialSourcePath, ShouldReportMaterialAssetWarningsAsErrors());
  202. if (!materialAssetOutcome)
  203. {
  204. AZ_Error(MaterialBuilderName, false, "Failed to create material asset from source data: %s", materialSourcePath.c_str());
  205. return;
  206. }
  207. const auto& materialAsset = materialAssetOutcome.GetValue();
  208. if (!materialAsset)
  209. {
  210. // Errors will have been reported above
  211. return;
  212. }
  213. AZStd::string materialProductPath;
  214. AZStd::string fileName;
  215. AzFramework::StringFunc::Path::GetFileName(materialSourcePath.c_str(), fileName);
  216. // REMARK: The reason we shouldn't call StringFunc::Path::ReplaceExtension(fileName, MaterialAsset::Extension);
  217. // is because if materialSourcePath == "<folder>/bed_frame.001.material", then GetFileName (called above) returns
  218. // "bed_frame.001", and calling ReplaceExtension would result in "bed_frame.azmaterial" and we'd lose
  219. // the original material name. Instead, by using the append operator, the fileName results in "bed_frame.001.azmaterial".
  220. fileName += ".";
  221. fileName += MaterialAsset::Extension;
  222. AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), fileName.c_str(), materialProductPath, true);
  223. if (!AZ::Utils::SaveObjectToFile(materialProductPath, AZ::DataStream::ST_BINARY, materialAsset.Get()))
  224. {
  225. AZ_Error(MaterialBuilderName, false, "Failed to save material to file '%s'!", materialProductPath.c_str());
  226. return;
  227. }
  228. AssetBuilderSDK::JobProduct jobProduct;
  229. if (!AssetBuilderSDK::OutputObject(
  230. materialAsset.Get(), materialProductPath, azrtti_typeid<RPI::MaterialAsset>(), 0, jobProduct))
  231. {
  232. AZ_Error(MaterialBuilderName, false, "Failed to output product dependencies.");
  233. return;
  234. }
  235. MaterialBuilderUtils::AddImageAssetDependenciesToProduct(materialAsset.Get(), jobProduct);
  236. response.m_outputProducts.emplace_back(AZStd::move(jobProduct));
  237. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  238. }
  239. void MaterialBuilder::ShutDown()
  240. {
  241. m_isShuttingDown = true;
  242. }
  243. } // namespace RPI
  244. } // namespace AZ