MaterialBuilder.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. // Assign dependencies from image properties
  161. for (const auto& [propertyId, propertyValue] : materialSourceData.GetPropertyValues())
  162. {
  163. AZ_UNUSED(propertyId);
  164. if (MaterialUtils::LooksLikeImageFileReference(propertyValue))
  165. {
  166. MaterialBuilderUtils::AddPossibleImageDependencies(
  167. materialSourcePath, propertyValue.GetValue<AZStd::string>(), outputJobDescriptor);
  168. }
  169. }
  170. // Create the output jobs for each platform
  171. for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms)
  172. {
  173. outputJobDescriptor.SetPlatformIdentifier(platformInfo.m_identifier.c_str());
  174. for (auto& jobDependency : outputJobDescriptor.m_jobDependencyList)
  175. {
  176. if (jobDependency.m_platformIdentifier.empty())
  177. {
  178. jobDependency.m_platformIdentifier = platformInfo.m_identifier;
  179. }
  180. }
  181. response.m_createJobOutputs.push_back(outputJobDescriptor);
  182. }
  183. response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
  184. }
  185. void MaterialBuilder::ProcessJob(
  186. const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
  187. {
  188. AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
  189. if (jobCancelListener.IsCancelled())
  190. {
  191. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  192. return;
  193. }
  194. if (m_isShuttingDown)
  195. {
  196. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  197. return;
  198. }
  199. AZStd::string materialSourcePath;
  200. AzFramework::StringFunc::Path::ConstructFull(
  201. request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), materialSourcePath, true);
  202. const auto& materialSourceDataOutcome = MaterialUtils::LoadMaterialSourceData(materialSourcePath);
  203. if (!materialSourceDataOutcome)
  204. {
  205. AZ_Error(MaterialBuilderName, false, "Failed to load material source data: %s", materialSourcePath.c_str());
  206. return;
  207. }
  208. const auto& materialSourceData = materialSourceDataOutcome.GetValue();
  209. // Load the material file and create the MaterialAsset object
  210. const auto& materialAssetOutcome = materialSourceData.CreateMaterialAsset(
  211. Uuid::CreateRandom(), materialSourcePath, ShouldReportMaterialAssetWarningsAsErrors());
  212. if (!materialAssetOutcome)
  213. {
  214. AZ_Error(MaterialBuilderName, false, "Failed to create material asset from source data: %s", materialSourcePath.c_str());
  215. return;
  216. }
  217. const auto& materialAsset = materialAssetOutcome.GetValue();
  218. if (!materialAsset)
  219. {
  220. // Errors will have been reported above
  221. return;
  222. }
  223. AZStd::string materialProductPath;
  224. AZStd::string fileName;
  225. AzFramework::StringFunc::Path::GetFileName(materialSourcePath.c_str(), fileName);
  226. AzFramework::StringFunc::Path::ReplaceExtension(fileName, MaterialAsset::Extension);
  227. AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), fileName.c_str(), materialProductPath, true);
  228. if (!AZ::Utils::SaveObjectToFile(materialProductPath, AZ::DataStream::ST_BINARY, materialAsset.Get()))
  229. {
  230. AZ_Error(MaterialBuilderName, false, "Failed to save material to file '%s'!", materialProductPath.c_str());
  231. return;
  232. }
  233. AssetBuilderSDK::JobProduct jobProduct;
  234. if (!AssetBuilderSDK::OutputObject(
  235. materialAsset.Get(), materialProductPath, azrtti_typeid<RPI::MaterialAsset>(), 0, jobProduct))
  236. {
  237. AZ_Error(MaterialBuilderName, false, "Failed to output product dependencies.");
  238. return;
  239. }
  240. response.m_outputProducts.emplace_back(AZStd::move(jobProduct));
  241. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  242. }
  243. void MaterialBuilder::ShutDown()
  244. {
  245. m_isShuttingDown = true;
  246. }
  247. } // namespace RPI
  248. } // namespace AZ