3
0

ImageAssetProducer.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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 <Processing/ImageAssetProducer.h>
  9. #include <Processing/ImageObjectImpl.h>
  10. #include <Processing/PixelFormatInfo.h>
  11. #include <Processing/Utils.h>
  12. #include <Processing/ImageFlags.h>
  13. #include <Atom/RHI.Reflect/Format.h>
  14. #include <Atom/RHI.Reflect/ImageSubresource.h>
  15. #include <Atom/RPI.Reflect/Image/StreamingImageAssetCreator.h>
  16. #include <Atom/RPI.Reflect/Image/ImageMipChainAssetCreator.h>
  17. #include <Atom/RPI.Reflect/Image/ImageAsset.h>
  18. #include <AzCore/Asset/AssetManager.h>
  19. #include <AzCore/Serialization/Json/JsonUtils.h>
  20. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  21. namespace ImageProcessingAtom
  22. {
  23. using namespace AZ;
  24. namespace
  25. {
  26. // Trying to fit as many as mips to 64k memory for one mip chain
  27. const uint32_t s_mininumMipBlockSize = 64 * 1024;
  28. }
  29. ImageAssetProducer::ImageAssetProducer(
  30. const IImageObjectPtr imageObject,
  31. AZStd::string_view saveFolder,
  32. const Data::AssetId& sourceAssetId,
  33. AZStd::string_view fileName,
  34. uint8_t numResidentMips,
  35. uint32_t subId,
  36. AZStd::set<AZStd::string> tags)
  37. : m_imageObject(imageObject)
  38. , m_productFolder(saveFolder)
  39. , m_sourceAssetId(sourceAssetId)
  40. , m_fileName(fileName)
  41. , m_numResidentMips(numResidentMips)
  42. , m_subId(subId)
  43. , m_tags(AZStd::move(tags))
  44. {
  45. AZ_Assert(imageObject, "Input imageObject can't be empty");
  46. AZ_Assert(sourceAssetId.IsValid(), "The source asset Id is not valid");
  47. AZ_Assert(saveFolder.size() != 0, "saveFolder shouldn't be empty");
  48. AZ_Assert(fileName.size() != 0, "fileName shouldn't be empty");
  49. }
  50. AZStd::string ImageAssetProducer::GenerateAssetFullPath(ImageAssetType assetType, uint32_t assetSubId)
  51. {
  52. // Note: m_fileName is the filename contains original extension to avoid file name collision if there are files with same name but with different extension.
  53. // For example, test.jpg's image asset full path will be outputFolder/test.jpg.streamingimage.
  54. // And test.png's image asset full path will be outputFolder/test.png.streamingimage.
  55. AZStd::string fileName;
  56. if (assetType == ImageAssetType::MipChain)
  57. {
  58. fileName = AZStd::string::format("%s.%d.%s", m_fileName.c_str(), assetSubId, RPI::ImageMipChainAsset::Extension);
  59. }
  60. else if (assetType == ImageAssetType::Image)
  61. {
  62. fileName = AZStd::string::format("%s.%s", m_fileName.c_str(), RPI::StreamingImageAsset::Extension);
  63. }
  64. else
  65. {
  66. AZ_Assert(false, "Unknown image asset type");
  67. }
  68. AZStd::string outPath;
  69. AzFramework::StringFunc::Path::Join(m_productFolder.c_str(), fileName.c_str(), outPath, true, true);
  70. return outPath;
  71. }
  72. const AZStd::vector<AssetBuilderSDK::JobProduct>& ImageAssetProducer::GetJobProducts() const
  73. {
  74. return m_jobProducts;
  75. }
  76. bool ImageAssetProducer::BuildImageAssets()
  77. {
  78. Data::AssetId imageAssetId(m_sourceAssetId.m_guid, m_subId);
  79. AssetBuilderSDK::JobProduct product;
  80. RPI::StreamingImageAssetCreator builder;
  81. builder.Begin(imageAssetId);
  82. uint32_t imageDepth = m_imageObject->GetDepth(0);
  83. const bool isVolumeTexture = m_imageObject->HasImageFlags(EIF_Volumetexture) || (imageDepth > 1);
  84. if (m_imageObject->HasImageFlags(EIF_Cubemap) && isVolumeTexture)
  85. {
  86. AZ_Assert(false, "An image can not be a cubemap and a volume texture at the same time!");
  87. return false;
  88. }
  89. int32_t arraySize = m_imageObject->HasImageFlags(EIF_Cubemap) ? 6 : 1;
  90. uint32_t imageWidth = m_imageObject->GetWidth(0);
  91. // The current cubemap faces are vertically aligned in the same buffer of image object. So the height should be divided by the array size.
  92. uint32_t imageHeight = m_imageObject->GetHeight(0) / arraySize;
  93. RHI::Format format = Utils::PixelFormatToRHIFormat(m_imageObject->GetPixelFormat(), m_imageObject->HasImageFlags(EIF_SRGBRead));
  94. RHI::ImageBindFlags bindFlag = RHI::ImageBindFlags::ShaderRead;
  95. RHI::ImageDescriptor imageDesc = isVolumeTexture
  96. ? RHI::ImageDescriptor::Create3D(bindFlag, imageWidth, imageHeight, imageDepth, format)
  97. : RHI::ImageDescriptor::Create2DArray(bindFlag, imageWidth, imageHeight, static_cast<uint16_t>(arraySize), format);
  98. imageDesc.m_mipLevels = static_cast<uint16_t>(m_imageObject->GetMipCount());
  99. if (m_imageObject->HasImageFlags(EIF_Cubemap))
  100. {
  101. imageDesc.m_isCubemap = true;
  102. }
  103. builder.SetImageDescriptor(imageDesc);
  104. // Set ImageViewDescriptor for cubemap. The regular 2d images can use the default one.
  105. if (m_imageObject->HasImageFlags(EIF_Cubemap))
  106. {
  107. builder.SetImageViewDescriptor(RHI::ImageViewDescriptor::CreateCubemap());
  108. }
  109. // Build mip chain assets.
  110. // Start from smallest mips so the mip chain asset for the lowest resolutions may contain more high level mips
  111. uint32_t lastMip = imageDesc.m_mipLevels;
  112. uint32_t totalSize = 0;
  113. AZStd::vector<Data::Asset<RPI::ImageMipChainAsset>> mipChains;
  114. uint32_t subid = m_subId + 1;
  115. bool saveProduct = false; // used to skip exporting the tail mip chain to job product
  116. // Merge m_residentMipCount amount of mips into a single mip chain, and add it to the StreamingImageAsset's tail mip chain
  117. if (m_numResidentMips != 0)
  118. {
  119. const uint32_t boundedResidentMipCount = AZStd::min(static_cast<uint32_t>(m_numResidentMips), lastMip);
  120. const uint32_t residentStartMip = lastMip - boundedResidentMipCount;
  121. // Build the StreamingImageAsset's tail mip chain
  122. Data::Asset<RPI::ImageMipChainAsset> chainAsset;
  123. const bool isSuccess = BuildMipChainAsset(Data::AssetId(m_sourceAssetId.m_guid, subid), residentStartMip, boundedResidentMipCount, chainAsset, saveProduct);
  124. if (!isSuccess)
  125. {
  126. AZ_Warning("Image Processing", false, "Failed to build the StreamingImageAsset's m_tailMipChain");
  127. return false;
  128. }
  129. mipChains.emplace_back(chainAsset);
  130. // Set the state for the remaining mips
  131. saveProduct = true;
  132. lastMip = residentStartMip;
  133. subid++;
  134. }
  135. // Create MipChainAssets for the remaining mips
  136. for (int32_t mip = lastMip - 1; mip >= 0; mip--)
  137. {
  138. totalSize += m_imageObject->GetMipBufSize(mip);
  139. // Trying to fit as many as mips to 64k memory for one mip chain
  140. if (mip == 0 || totalSize + m_imageObject->GetMipBufSize(mip - 1) > s_mininumMipBlockSize)
  141. {
  142. // Build a chain
  143. Data::Asset<RPI::ImageMipChainAsset> chainAsset;
  144. bool isSuccess = BuildMipChainAsset(Data::AssetId(m_sourceAssetId.m_guid, subid), mip, lastMip - mip, chainAsset, saveProduct);
  145. saveProduct = true;
  146. if (!isSuccess)
  147. {
  148. AZ_Warning("Image Processing", false, "Failed to build mip chain asset");
  149. return false;
  150. }
  151. mipChains.emplace_back(chainAsset);
  152. subid++;
  153. lastMip = mip;
  154. totalSize = 0;
  155. }
  156. }
  157. // Set the builder's state to non-streaming if it only has a single mip chain, which will be stored in the
  158. // StreamingImage's tail mip chain
  159. if (mipChains.size() == 1)
  160. {
  161. builder.SetFlags(AZ::RPI::StreamingImageFlags::NotStreamable);
  162. }
  163. // Add mip chains to the builder from mip level 0 to highest
  164. for (auto it = mipChains.rbegin(); it != mipChains.rend(); it++)
  165. {
  166. builder.AddMipChainAsset(*it->GetAs<RPI::ImageMipChainAsset>());
  167. // Add all the mip chain assets as dependencies except the tail mip chain since its embedded in the StreamingImageAsset
  168. if (it->Get() != mipChains.begin()->Get())
  169. {
  170. // Note: we don't want to preload the mipchain assets here to reduce loading time and memory footprint.
  171. // The mipchain assets will be loaded by StreamingImage automatically or loaded when user is accessing the mipmap data via
  172. // StreamingImageAsset::GetSubImageData() function
  173. product.m_dependencies.push_back(AssetBuilderSDK::ProductDependency(it->GetId(), AZ::Data::ProductDependencyInfo::CreateFlags(AZ::Data::AssetLoadBehavior::NoLoad)));
  174. }
  175. }
  176. builder.SetAverageColor(m_imageObject->GetAverageColor());
  177. for (const AZStd::string& tag : m_tags)
  178. {
  179. builder.AddTag(AZ::Name{ tag });
  180. }
  181. product.m_dependenciesHandled = true; // We've output the dependencies immediately above so it's OK to tell the AP we've handled dependencies
  182. bool result = false;
  183. Data::Asset<RPI::StreamingImageAsset> imageAsset;
  184. if (builder.End(imageAsset))
  185. {
  186. AZStd::string destPath = GenerateAssetFullPath(ImageAssetType::Image, imageAsset.GetId().m_subId);
  187. result = AZ::Utils::SaveObjectToFile(destPath, AZ::DataStream::ST_BINARY, imageAsset.GetData(), imageAsset.GetData()->GetType(), nullptr);
  188. if (!result)
  189. {
  190. AZ_Warning("Image Processing", false, "Failed to save image asset to file %s", destPath.c_str());
  191. }
  192. else
  193. {
  194. product.m_productAssetType = imageAsset.GetData()->GetType();
  195. product.m_productSubID = imageAsset.GetId().m_subId;
  196. product.m_productFileName = destPath;
  197. auto& imageDescriptor = imageAsset->GetImageDescriptor();
  198. AZStd::string folder;
  199. AZStd::string jsonName;
  200. folder = AZStd::string::format("%s/%s.abdata.json", m_productFolder.c_str(), m_fileName.c_str());
  201. AssetBuilderSDK::CreateABDataFile(folder,
  202. [imageDescriptor](rapidjson::PrettyWriter<rapidjson::StringBuffer>& writer)
  203. {
  204. writer.Key("dimension");
  205. writer.StartArray();
  206. writer.Double(imageDescriptor.m_size.m_width);
  207. writer.Double(imageDescriptor.m_size.m_height);
  208. writer.Double(imageDescriptor.m_size.m_depth);
  209. writer.EndArray();
  210. });
  211. AssetBuilderSDK::JobProduct jsonProduct(folder);
  212. jsonProduct.m_productSubID |= product.m_productSubID;
  213. m_jobProducts.emplace_back(AZStd::move(jsonProduct));
  214. // The StreamingImageAsset is added to end of product list on purpose.
  215. // This is in case a new mip chain file is generated, for example when updating the original image's resolution,
  216. // the mip chain asset hasn't be registered by the AssetCatalog when processing the asset change notification for
  217. // StreamingImageAsset reload and it leads to an unknown asset error.
  218. // The Asset system can be modified to solve the problem so the order doesn't matter.
  219. // The task is tracked in ATOM-242
  220. m_jobProducts.emplace_back(AZStd::move(product));
  221. }
  222. }
  223. return result;
  224. }
  225. bool ImageAssetProducer::BuildMipChainAsset(const Data::AssetId& chainAssetId, uint32_t startMip, uint32_t mipLevels,
  226. Data::Asset<RPI::ImageMipChainAsset>& outAsset, bool saveAsProduct)
  227. {
  228. RPI::ImageMipChainAssetCreator builder;
  229. uint32_t arraySize = m_imageObject->HasImageFlags(EIF_Cubemap) ? 6 : 1;
  230. builder.Begin(chainAssetId, static_cast<uint16_t>(mipLevels), static_cast<uint16_t>(arraySize));
  231. for (uint32_t mip = startMip; mip < startMip + mipLevels; mip++)
  232. {
  233. uint8_t* mipBuffer;
  234. uint32_t pitch;
  235. m_imageObject->GetImagePointer(mip, mipBuffer, pitch);
  236. RHI::Format format = Utils::PixelFormatToRHIFormat(m_imageObject->GetPixelFormat(), m_imageObject->HasImageFlags(EIF_SRGBRead));
  237. const auto mipSize = (m_imageObject->GetDepth(0) == 1)
  238. ? RHI::Size(m_imageObject->GetWidth(mip), m_imageObject->GetHeight(mip) / arraySize, 1)
  239. : RHI::Size(m_imageObject->GetWidth(mip), m_imageObject->GetHeight(mip), m_imageObject->GetDepth(mip));
  240. RHI::ImageSubresourceLayout layout = RHI::GetImageSubresourceLayout(mipSize, format);
  241. const auto mipSizeInBytes = layout.m_bytesPerImage * mipSize.m_depth;
  242. builder.BeginMip(layout);
  243. for (uint32_t arrayIndex = 0; arrayIndex < arraySize; ++arrayIndex)
  244. {
  245. builder.AddSubImage(mipBuffer + arrayIndex * mipSizeInBytes, mipSizeInBytes);
  246. }
  247. builder.EndMip();
  248. }
  249. bool result = false;
  250. if (builder.End(outAsset))
  251. {
  252. AZStd::string destPath = GenerateAssetFullPath(ImageAssetType::MipChain, outAsset.GetId().m_subId);
  253. if (saveAsProduct)
  254. {
  255. result = AZ::Utils::SaveObjectToFile(destPath, AZ::DataStream::ST_BINARY, outAsset.GetData(), outAsset.GetData()->GetType(), nullptr);
  256. if (result)
  257. {
  258. // Save job product
  259. AssetBuilderSDK::JobProduct product;
  260. product.m_productAssetType = outAsset.GetData()->GetType();
  261. product.m_productSubID = outAsset.GetId().m_subId;
  262. product.m_productFileName = destPath;
  263. m_jobProducts.emplace_back(AZStd::move(product));
  264. }
  265. }
  266. else
  267. {
  268. result = true;
  269. }
  270. }
  271. return result;
  272. }
  273. } // namespace ImageProcessingAtom