MaterialUtils.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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 <Atom/RPI.Edit/Material/MaterialUtils.h>
  9. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  10. #include <Atom/RPI.Reflect/Image/AttachmentImageAsset.h>
  11. #include <Atom/RPI.Reflect/Image/ImageAsset.h>
  12. #include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
  13. #include <Atom/RPI.Reflect/Material/MaterialAsset.h>
  14. #include <Atom/RPI.Reflect/Material/MaterialTypeAsset.h>
  15. #include <Atom/RPI.Edit/Material/MaterialSourceData.h>
  16. #include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
  17. #include <Atom/RPI.Edit/Material/MaterialPipelineSourceData.h>
  18. #include <Atom/RPI.Edit/Common/JsonReportingHelper.h>
  19. #include <Atom/RPI.Edit/Common/JsonUtils.h>
  20. #include <AzCore/Serialization/Json/JsonUtils.h>
  21. #include <AzCore/Serialization/Json/BaseJsonSerializer.h>
  22. #include <AzCore/Serialization/Json/JsonSerializationResult.h>
  23. #include <AzCore/Serialization/Json/JsonImporter.h>
  24. #include <AzCore/Settings/SettingsRegistry.h>
  25. #include <AzCore/Utils/Utils.h>
  26. #include <AzCore/std/string/regex.h>
  27. #include <AzCore/std/string/string.h>
  28. #include <AzFramework/IO/LocalFileIO.h>
  29. #include <AzFramework/StringFunc/StringFunc.h>
  30. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  31. namespace AZ
  32. {
  33. namespace RPI
  34. {
  35. namespace MaterialUtils
  36. {
  37. GetImageAssetResult GetImageAssetReference(Data::Asset<ImageAsset>& imageAsset, AZStd::string_view materialSourceFilePath, const AZStd::string imageFilePath)
  38. {
  39. imageAsset = {};
  40. if (imageFilePath.empty())
  41. {
  42. // The image value was present but specified an empty string, meaning the texture asset should be explicitly cleared.
  43. return GetImageAssetResult::Empty;
  44. }
  45. else
  46. {
  47. // We use TraceLevel::None because fallback textures are available and we'll return GetImageAssetResult::Missing below in that case.
  48. // Callers of GetImageAssetReference will be responsible for logging warnings or errors as needed.
  49. uint32_t subId = 0;
  50. auto typeId = azrtti_typeid<AttachmentImageAsset>();
  51. bool isAttachmentImageAsset = imageFilePath.ends_with(AttachmentImageAsset::Extension);
  52. if (!isAttachmentImageAsset)
  53. {
  54. subId = StreamingImageAsset::GetImageAssetSubId();
  55. typeId = azrtti_typeid<StreamingImageAsset>();
  56. }
  57. Outcome<Data::AssetId> imageAssetId = AssetUtils::MakeAssetId(materialSourceFilePath, imageFilePath, subId, AssetUtils::TraceLevel::None);
  58. if (!imageAssetId.IsSuccess())
  59. {
  60. // When the AssetId cannot be found, we don't want to outright fail, because the runtime has mechanisms for displaying fallback textures which gives the
  61. // user a better recovery workflow. On the other hand we can't just provide an empty/invalid Asset<ImageAsset> because that would be interpreted as simply
  62. // no value was present and result in using no texture, and this would amount to a silent failure.
  63. // So we use a UUID that is clearly invalid, which the runtime and tools will interpret as a missing asset and represent
  64. // it as such.
  65. static constexpr Uuid InvalidAssetPlaceholderId(Uuid::CreateInvalid());
  66. imageAsset = Data::Asset<ImageAsset>{InvalidAssetPlaceholderId, azrtti_typeid<StreamingImageAsset>(), imageFilePath};
  67. AZ_Error("MaterialUtils", false, "Material at path %.*s could not resolve image %.*s, using invalid UUID %.*s. "
  68. "To resolve this, verify the image exists at the relative path to a scan folder matching this reference. "
  69. "Verify a portion of the scan folder is not in the relative path, which is a common cause of this issue.",
  70. AZ_STRING_ARG(materialSourceFilePath),
  71. AZ_STRING_ARG(imageFilePath),
  72. AZ_STRING_ARG(InvalidAssetPlaceholderId.ToFixedString()));
  73. return GetImageAssetResult::Missing;
  74. }
  75. imageAsset = Data::Asset<ImageAsset>{imageAssetId.GetValue(), typeId, imageFilePath};
  76. imageAsset.SetAutoLoadBehavior(Data::AssetLoadBehavior::PreLoad);
  77. return GetImageAssetResult::Found;
  78. }
  79. }
  80. bool ResolveMaterialPropertyEnumValue(const MaterialPropertyDescriptor* propertyDescriptor, const AZ::Name& enumName, MaterialPropertyValue& outResolvedValue)
  81. {
  82. uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName);
  83. if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
  84. {
  85. AZ_Error("MaterialUtils", false, "Enum name \"%s\" can't be found in property \"%s\".", enumName.GetCStr(), propertyDescriptor->GetName().GetCStr());
  86. return false;
  87. }
  88. outResolvedValue = enumValue;
  89. return true;
  90. }
  91. template<typename SourceDataType>
  92. AZ::Outcome<SourceDataType> LoadJsonSourceDataWithImports(const AZStd::string& filePath, rapidjson::Document* document, ImportedJsonFiles* importedFiles)
  93. {
  94. rapidjson::Document localDocument;
  95. if (document == nullptr)
  96. {
  97. AZ::Outcome<rapidjson::Document, AZStd::string> loadOutcome = AZ::JsonSerializationUtils::ReadJsonFile(filePath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
  98. if (!loadOutcome.IsSuccess())
  99. {
  100. AZ_Error("MaterialUtils", false, "%s", loadOutcome.GetError().c_str());
  101. return AZ::Failure();
  102. }
  103. localDocument = loadOutcome.TakeValue();
  104. document = &localDocument;
  105. }
  106. AZ::BaseJsonImporter jsonImporter;
  107. AZ::JsonImportSettings importSettings;
  108. importSettings.m_importer = &jsonImporter;
  109. importSettings.m_loadedJsonPath = filePath;
  110. AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::ResolveImports(document->GetObject(), document->GetAllocator(), importSettings);
  111. if (result.GetProcessing() != AZ::JsonSerializationResult::Processing::Completed)
  112. {
  113. AZ_Error("MaterialUtils", false, "%s", result.ToString(filePath).c_str());
  114. return AZ::Failure();
  115. }
  116. if (importedFiles)
  117. {
  118. *importedFiles = importSettings.m_importer->GetImportedFiles();
  119. }
  120. SourceDataType sourceData;
  121. JsonDeserializerSettings settings;
  122. JsonReportingHelper reportingHelper;
  123. reportingHelper.Attach(settings);
  124. JsonSerialization::Load(sourceData, *document, settings);
  125. if (reportingHelper.ErrorsReported())
  126. {
  127. return AZ::Failure();
  128. }
  129. else
  130. {
  131. return AZ::Success(AZStd::move(sourceData));
  132. }
  133. }
  134. AZ::Outcome<MaterialPipelineSourceData> LoadMaterialPipelineSourceData(const AZStd::string& filePath, rapidjson::Document* document, ImportedJsonFiles* importedFiles)
  135. {
  136. return LoadJsonSourceDataWithImports<MaterialPipelineSourceData>(filePath, document, importedFiles);
  137. }
  138. AZ::Outcome<MaterialTypeSourceData> LoadMaterialTypeSourceData(const AZStd::string& filePath, rapidjson::Document* document, ImportedJsonFiles* importedFiles)
  139. {
  140. AZ::Outcome<MaterialTypeSourceData> outcome = LoadJsonSourceDataWithImports<MaterialTypeSourceData>(filePath, document, importedFiles);
  141. if (outcome.IsSuccess())
  142. {
  143. outcome.GetValue().UpgradeLegacyFormat();
  144. outcome.GetValue().ResolveUvEnums();
  145. }
  146. return outcome;
  147. }
  148. AZ::Outcome<MaterialSourceData> LoadMaterialSourceData(const AZStd::string& filePath, const rapidjson::Value* document, bool warningsAsErrors)
  149. {
  150. AZ::Outcome<rapidjson::Document, AZStd::string> loadOutcome;
  151. if (document == nullptr)
  152. {
  153. loadOutcome = AZ::JsonSerializationUtils::ReadJsonFile(filePath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
  154. if (!loadOutcome.IsSuccess())
  155. {
  156. AZ_Error("MaterialUtils", false, "%s", loadOutcome.GetError().c_str());
  157. return AZ::Failure();
  158. }
  159. document = &loadOutcome.GetValue();
  160. }
  161. MaterialSourceData material;
  162. JsonDeserializerSettings settings;
  163. JsonReportingHelper reportingHelper;
  164. reportingHelper.Attach(settings);
  165. JsonSerialization::Load(material, *document, settings);
  166. material.UpgradeLegacyFormat();
  167. if (reportingHelper.ErrorsReported())
  168. {
  169. return AZ::Failure();
  170. }
  171. else if (warningsAsErrors && reportingHelper.WarningsReported())
  172. {
  173. AZ_Error("MaterialUtils", false, "Warnings reported while loading '%s'", filePath.c_str());
  174. return AZ::Failure();
  175. }
  176. else
  177. {
  178. return AZ::Success(AZStd::move(material));
  179. }
  180. }
  181. void CheckForUnrecognizedJsonFields(const AZStd::string_view* acceptedFieldNames, uint32_t acceptedFieldNameCount, const rapidjson::Value& object, JsonDeserializerContext& context, JsonSerializationResult::ResultCode &result)
  182. {
  183. for (auto iter = object.MemberBegin(); iter != object.MemberEnd(); ++iter)
  184. {
  185. bool matched = false;
  186. for (uint32_t i = 0; i < acceptedFieldNameCount; ++i)
  187. {
  188. if (iter->name.GetString() == acceptedFieldNames[i])
  189. {
  190. matched = true;
  191. break;
  192. }
  193. }
  194. if (!matched)
  195. {
  196. ScopedContextPath subPath{context, iter->name.GetString()};
  197. result.Combine(context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Skipped, "Skipping unrecognized field"));
  198. }
  199. }
  200. }
  201. bool LooksLikeImageFileReference(const MaterialPropertyValue& value)
  202. {
  203. // If the source value type is a string, there are two possible property types: Image and Enum. If there is a "." in
  204. // the string (for the extension) we can assume it's an Image file path.
  205. return value.Is<AZStd::string>() && AzFramework::StringFunc::Contains(value.GetValue<AZStd::string>(), ".");
  206. }
  207. bool IsValidName(AZStd::string_view name)
  208. {
  209. // Checks for a c++ style identifier
  210. return AZStd::regex_match(name.begin(), name.end(), AZStd::regex("^[a-zA-Z_][a-zA-Z0-9_]*$"));
  211. }
  212. bool IsValidName(const AZ::Name& name)
  213. {
  214. return IsValidName(name.GetStringView());
  215. }
  216. bool CheckIsValidName(AZStd::string_view name, [[maybe_unused]] AZStd::string_view nameTypeForDebug)
  217. {
  218. if (IsValidName(name))
  219. {
  220. return true;
  221. }
  222. else
  223. {
  224. AZ_Error("MaterialUtils", false, "%.*s '%.*s' is not a valid identifier", AZ_STRING_ARG(nameTypeForDebug), AZ_STRING_ARG(name));
  225. return false;
  226. }
  227. }
  228. bool CheckIsValidPropertyName(AZStd::string_view name)
  229. {
  230. return CheckIsValidName(name, "Property name");
  231. }
  232. bool CheckIsValidGroupName(AZStd::string_view name)
  233. {
  234. return CheckIsValidName(name, "Group name");
  235. }
  236. AZStd::string PredictOriginalMaterialTypeSourcePath(const AZStd::string& materialTypeSourcePath)
  237. {
  238. constexpr const char* generatedMaterialTypeSuffix = "_generated.materialtype";
  239. if (materialTypeSourcePath.ends_with(generatedMaterialTypeSuffix))
  240. {
  241. // Separate the input material type path into a relative path and root folder. This should work for both intermediate,
  242. // generated material types and original material types.
  243. bool pathFound = false;
  244. AZStd::string relativePath;
  245. AZStd::string rootFolder;
  246. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  247. pathFound,
  248. &AzToolsFramework::AssetSystemRequestBus::Events::GenerateRelativeSourcePath,
  249. materialTypeSourcePath.c_str(),
  250. relativePath,
  251. rootFolder);
  252. if (pathFound)
  253. {
  254. // Replace the generated suffix if present
  255. AZ::StringFunc::Replace(relativePath, generatedMaterialTypeSuffix, ".materialtype");
  256. // Search for the original material type file using the stripped generated material type path.
  257. pathFound = false;
  258. AZ::Data::AssetInfo sourceInfo;
  259. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  260. pathFound,
  261. &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath,
  262. relativePath.c_str(),
  263. sourceInfo,
  264. rootFolder);
  265. if (pathFound)
  266. {
  267. const AZ::IO::Path result = AZ::IO::Path(rootFolder) / sourceInfo.m_relativePath;
  268. if (!result.empty())
  269. {
  270. return result.LexicallyNormal().String();
  271. }
  272. }
  273. }
  274. }
  275. // Conversion failed. Return the input path.
  276. return materialTypeSourcePath;
  277. }
  278. AZStd::string PredictIntermediateMaterialTypeSourcePath(const AZStd::string& originalMaterialTypeSourcePath)
  279. {
  280. // This just normalizes the original path into a relative path that can be easily converted into relative path
  281. // to the intermediate .materialtype file
  282. bool pathFound = false;
  283. AZ::Data::AssetInfo sourceInfo;
  284. AZStd::string rootFolder;
  285. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  286. pathFound,
  287. &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath,
  288. originalMaterialTypeSourcePath.c_str(),
  289. sourceInfo,
  290. rootFolder);
  291. if (!pathFound)
  292. {
  293. return {};
  294. }
  295. IO::Path intermediatePath = sourceInfo.m_relativePath;
  296. const AZStd::string materialTypeFilename =
  297. AZStd::string::format("%.*s_generated.materialtype", AZ_STRING_ARG(intermediatePath.Stem().Native()));
  298. intermediatePath.ReplaceFilename(materialTypeFilename.c_str());
  299. AZStd::string intermediatePathString = intermediatePath.Native();
  300. AZStd::to_lower(intermediatePathString.begin(), intermediatePathString.end());
  301. IO::Path intermediateMaterialTypePath = Utils::GetProjectPath().c_str();
  302. intermediateMaterialTypePath /= "Cache";
  303. intermediateMaterialTypePath /= "Intermediate Assets";
  304. intermediateMaterialTypePath /= intermediatePathString;
  305. return intermediateMaterialTypePath.LexicallyNormal().String();
  306. }
  307. AZStd::string PredictIntermediateMaterialTypeSourcePath(const AZStd::string& referencingFilePath, const AZStd::string& originalMaterialTypeSourcePath)
  308. {
  309. const AZStd::string resolvedPath = AssetUtils::ResolvePathReference(referencingFilePath, originalMaterialTypeSourcePath);
  310. return PredictIntermediateMaterialTypeSourcePath(resolvedPath);
  311. }
  312. AZStd::string GetIntermediateMaterialTypeSourcePath(const AZStd::string& forOriginalMaterialTypeSourcePath)
  313. {
  314. const AZStd::string intermediatePathString = PredictIntermediateMaterialTypeSourcePath(forOriginalMaterialTypeSourcePath);
  315. return IO::LocalFileIO::GetInstance()->Exists(intermediatePathString.c_str()) ? intermediatePathString : AZStd::string{};
  316. }
  317. Outcome<Data::AssetId> GetFinalMaterialTypeAssetId(const AZStd::string& referencingFilePath, const AZStd::string& originalMaterialTypeSourcePath)
  318. {
  319. const AZStd::string resolvedPath = AssetUtils::ResolvePathReference(referencingFilePath, originalMaterialTypeSourcePath);
  320. const AZStd::string intermediateMaterialTypePath = GetIntermediateMaterialTypeSourcePath(resolvedPath);
  321. if (!intermediateMaterialTypePath.empty())
  322. {
  323. return AssetUtils::MakeAssetId(intermediateMaterialTypePath, MaterialTypeSourceData::IntermediateMaterialTypeSubId);
  324. }
  325. return AssetUtils::MakeAssetId(resolvedPath, MaterialTypeAsset::SubId);
  326. }
  327. AZStd::string GetFinalMaterialTypeSourcePath(const AZStd::string& referencingFilePath, const AZStd::string& originalMaterialTypeSourcePath)
  328. {
  329. const AZStd::string resolvedPath = AssetUtils::ResolvePathReference(referencingFilePath, originalMaterialTypeSourcePath);
  330. const AZStd::string intermediateMaterialTypePath = GetIntermediateMaterialTypeSourcePath(resolvedPath);
  331. if (intermediateMaterialTypePath.empty())
  332. {
  333. return resolvedPath;
  334. }
  335. return intermediateMaterialTypePath;
  336. }
  337. }
  338. }
  339. }