VisualsMaker.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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 "RobotImporter/URDF/VisualsMaker.h"
  9. #include "RobotImporter/URDF/PrefabMakerUtils.h"
  10. #include "RobotImporter/Utils/TypeConversions.h"
  11. #include <Atom/RPI.Reflect/Material/MaterialAsset.h>
  12. #include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h>
  13. #include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentConfig.h>
  14. #include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentConstants.h>
  15. #include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h>
  16. #include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentConstants.h>
  17. #include <AzCore/Component/NonUniformScaleBus.h>
  18. #include <AzCore/Component/TransformBus.h>
  19. #include <AzToolsFramework/Entity/EditorEntityHelpers.h>
  20. #include <AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.h>
  21. #include <sdf/Material.hh>
  22. #include <sdf/Pbr.hh>
  23. namespace ROS2RobotImporter
  24. {
  25. VisualsMaker::VisualsMaker() = default;
  26. VisualsMaker::VisualsMaker(const AZStd::shared_ptr<Utils::UrdfAssetMap>& urdfAssetsMapping)
  27. : m_urdfAssetsMapping(urdfAssetsMapping)
  28. {
  29. }
  30. AZStd::vector<AZ::EntityId> VisualsMaker::AddVisuals(const sdf::Link* link, AZ::EntityId entityId) const
  31. {
  32. AZStd::vector<AZ::EntityId> createdEntities;
  33. const AZStd::string typeString = "visual";
  34. if (link->VisualCount() < 1)
  35. {
  36. // For zero visuals, element is used
  37. auto createdEntity = AddVisual(nullptr, entityId, PrefabMakerUtils::MakeEntityName(link->Name().c_str(), typeString));
  38. if (createdEntity.IsValid())
  39. {
  40. createdEntities.emplace_back(createdEntity);
  41. }
  42. }
  43. else
  44. {
  45. // For one or more visuals, an array is used
  46. size_t nameSuffixIndex = 0; // For disambiguation when multiple unnamed visuals are present. The order does not matter here
  47. for (uint64_t index = 0; index < link->VisualCount(); index++)
  48. {
  49. auto createdEntity = AddVisual(
  50. link->VisualByIndex(index),
  51. entityId,
  52. PrefabMakerUtils::MakeEntityName(link->Name().c_str(), typeString, nameSuffixIndex));
  53. if (createdEntity.IsValid())
  54. {
  55. createdEntities.emplace_back(createdEntity);
  56. }
  57. nameSuffixIndex++;
  58. }
  59. }
  60. return createdEntities;
  61. }
  62. AZ::EntityId VisualsMaker::AddVisual(const sdf::Visual* visual, AZ::EntityId entityId, const AZStd::string& generatedName) const
  63. {
  64. if (!visual)
  65. { // It is ok not to have a visual in a link
  66. return AZ::EntityId();
  67. }
  68. if (!visual->Geom())
  69. { // Non-empty visual should have a geometry. Warn if no geometry present
  70. AZ_Warning("AddVisual", false, "No Geometry for a visual");
  71. return AZ::EntityId();
  72. }
  73. AZ_Trace("AddVisual", "Processing visual for entity id:%s\n", entityId.ToString().c_str());
  74. // Use a name generated from the link unless specific name is defined for this visual
  75. AZStd::string subEntityName = visual->Name().empty() ? generatedName.c_str() : visual->Name().c_str();
  76. // Since O3DE does not allow origin for visuals, we need to create a sub-entity and store visual there
  77. auto createEntityResult = PrefabMakerUtils::CreateEntity(entityId, subEntityName);
  78. if (!createEntityResult.IsSuccess())
  79. {
  80. AZ_Error("AddVisual", false, "Unable to create a sub-entity for visual element %s\n", subEntityName.c_str());
  81. return AZ::EntityId();
  82. }
  83. auto visualEntityId = createEntityResult.GetValue();
  84. auto visualAssetId = AddVisualToEntity(visual, visualEntityId);
  85. AddMaterialForVisual(visual, visualEntityId, visualAssetId);
  86. return visualEntityId;
  87. }
  88. AZ::Data::AssetId VisualsMaker::AddVisualToEntity(const sdf::Visual* visual, AZ::EntityId entityId) const
  89. {
  90. // Asset ID for the asset added to the visual entity, if any.
  91. AZ::Data::AssetId assetId;
  92. // Apply transform as per origin
  93. PrefabMakerUtils::SetEntityTransformLocal(visual->RawPose(), entityId);
  94. auto geometry = visual->Geom();
  95. switch (geometry->Type())
  96. {
  97. case sdf::GeometryType::SPHERE:
  98. {
  99. auto sphereGeometry = geometry->SphereShape();
  100. AZ_Assert(sphereGeometry, "geometry is not Sphere");
  101. // Convert radius to diameter: the `_sphere_1x1.fbx.azmodel` model has a diameter of 1
  102. const AZ::Vector3 sphereDimensions(sphereGeometry->Radius() * 2.0f);
  103. // The `_sphere_1x1.fbx.azmodel` is created by Asset Processor based on O3DE `PrimitiveAssets` Gem source.
  104. const char* sphereAssetRelPath = "objects/_primitives/_sphere_1x1.fbx.azmodel"; // relative path to cache folder.
  105. AZ::Data::AssetCatalogRequestBus::BroadcastResult(
  106. assetId,
  107. &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
  108. sphereAssetRelPath,
  109. AZ::Data::s_invalidAssetType,
  110. false);
  111. AZ_Warning("AddVisual", assetId.IsValid(), "There is no product asset for %s.", sphereAssetRelPath);
  112. AddVisualAssetToEntity(entityId, assetId, sphereDimensions);
  113. }
  114. break;
  115. case sdf::GeometryType::CYLINDER:
  116. {
  117. auto cylinderGeometry = geometry->CylinderShape();
  118. AZ_Assert(cylinderGeometry, "geometry is not Cylinder");
  119. // Convert radius to diameter: the `_cylinder_1x1.fbx.azmodel` model has a diameter of 1
  120. const AZ::Vector3 cylinderDimensions(
  121. cylinderGeometry->Radius() * 2.0f, cylinderGeometry->Radius() * 2.0f, cylinderGeometry->Length());
  122. // The `_cylinder_1x1.fbx.azmodel` is created by Asset Processor based on O3DE `PrimitiveAssets` Gem source.
  123. const char* cylinderAssetRelPath = "objects/_primitives/_cylinder_1x1.fbx.azmodel"; // relative path to cache folder.
  124. AZ::Data::AssetCatalogRequestBus::BroadcastResult(
  125. assetId,
  126. &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
  127. cylinderAssetRelPath,
  128. AZ::Data::s_invalidAssetType,
  129. false);
  130. AZ_Warning("AddVisual", assetId.IsValid(), "There is no product asset for %s.", cylinderAssetRelPath);
  131. AddVisualAssetToEntity(entityId, assetId, cylinderDimensions);
  132. }
  133. break;
  134. case sdf::GeometryType::BOX:
  135. {
  136. auto boxGeometry = geometry->BoxShape();
  137. AZ_Assert(boxGeometry, "geometry is not Box");
  138. const AZ::Vector3 boxDimensions = URDF::TypeConversions::ConvertVector3(boxGeometry->Size());
  139. // The `_box_1x1.fbx.azmodel` is created by Asset Processor based on O3DE `PrimitiveAssets` Gem source.
  140. const char* boxAssetRelPath = "objects/_primitives/_box_1x1.fbx.azmodel"; // relative path to cache folder.
  141. AZ::Data::AssetCatalogRequestBus::BroadcastResult(
  142. assetId,
  143. &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
  144. boxAssetRelPath,
  145. AZ::Data::s_invalidAssetType,
  146. false);
  147. AZ_Warning("AddVisual", assetId.IsValid(), "There is no product asset for %s.", boxAssetRelPath);
  148. AddVisualAssetToEntity(entityId, assetId, boxDimensions);
  149. }
  150. break;
  151. case sdf::GeometryType::MESH:
  152. {
  153. auto meshGeometry = geometry->MeshShape();
  154. AZ_Assert(meshGeometry, "geometry is not Mesh");
  155. const AZ::Vector3 scaleVector = URDF::TypeConversions::ConvertVector3(meshGeometry->Scale());
  156. const auto asset = PrefabMakerUtils::GetAssetFromPath(*m_urdfAssetsMapping, AZStd::string(meshGeometry->Uri().c_str()));
  157. AZ_Warning("AddVisual", asset, "There is no source asset for %s.", meshGeometry->Uri().c_str());
  158. if (asset)
  159. {
  160. assetId = Utils::GetModelProductAssetId(asset->m_sourceGuid);
  161. AZ_Warning(
  162. "AddVisual", assetId.IsValid(), "There is no product asset for %s.", asset->m_sourceAssetRelativePath.c_str());
  163. }
  164. AddVisualAssetToEntity(entityId, assetId, scaleVector);
  165. }
  166. break;
  167. default:
  168. AZ_Warning("AddVisual", false, "Unsupported visual geometry type, %d", (int)geometry->Type());
  169. break;
  170. }
  171. return assetId;
  172. }
  173. void VisualsMaker::AddVisualAssetToEntity(AZ::EntityId entityId, const AZ::Data::AssetId& assetId, const AZ::Vector3& scale) const
  174. {
  175. if (!assetId.IsValid())
  176. {
  177. return;
  178. }
  179. AZ::Entity* entity = AzToolsFramework::GetEntityById(entityId);
  180. bool isUniformScale = AZ::IsClose(scale.GetMaxElement(), scale.GetMinElement(), AZ::Constants::FloatEpsilon);
  181. if (isUniformScale)
  182. {
  183. auto* transformComponent = entity->FindComponent(AZ::EditorTransformComponentTypeId);
  184. AZ_Assert(transformComponent, "Entity doesn't have a transform component.");
  185. auto* transformInterface = azrtti_cast<AZ::TransformInterface*>(transformComponent);
  186. AZ_Assert(transformInterface, "Found component has no transformInterface");
  187. transformInterface->SetLocalUniformScale(scale.GetX());
  188. }
  189. else
  190. {
  191. auto component = entity->CreateComponent<AzToolsFramework::Components::EditorNonUniformScaleComponent>();
  192. AZ_Assert(component, "EditorNonUniformScaleComponent was not created");
  193. component->SetScale(scale);
  194. }
  195. auto editorMeshComponent = entity->CreateComponent(AZ::Render::EditorMeshComponentTypeId);
  196. if (editorMeshComponent)
  197. {
  198. auto editorBaseComponent = azrtti_cast<AzToolsFramework::Components::EditorComponentBase*>(editorMeshComponent);
  199. AZ_Assert(editorBaseComponent, "EditorMeshComponent didn't derive from EditorComponentBase.");
  200. editorBaseComponent->SetPrimaryAsset(assetId);
  201. }
  202. }
  203. static void OverrideScriptMaterial(const sdf::Material* material, AZ::Render::MaterialAssignmentMap& overrides)
  204. {
  205. AZStd::string materialName(material->ScriptName().c_str(), material->ScriptName().size());
  206. if (materialName.empty())
  207. {
  208. return;
  209. }
  210. // Make sure the material name is lowercased before checking the path in the Asset Cache
  211. AZStd::to_lower(materialName);
  212. // If a material has a <script> element we'll treat the name as a path and name to an O3DE material.
  213. // For example, "Gazebo/Wood" will look for a product material in "<cache>/gazebo/wood.azmaterial"
  214. AZ::IO::Path materialProductPath(materialName);
  215. materialProductPath.ReplaceExtension(".azmaterial");
  216. // Try getting an asset ID for the given name.
  217. constexpr bool AutoGenerateId = false;
  218. AZ::Data::AssetId assetId;
  219. AZ::Data::AssetCatalogRequestBus::BroadcastResult(
  220. assetId,
  221. &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
  222. materialProductPath.String().c_str(),
  223. azrtti_typeid<AZ::RPI::MaterialAsset>(),
  224. AutoGenerateId);
  225. // No asset was found, we can't convert the material script.
  226. if (!assetId.IsValid())
  227. {
  228. AZ_Warning("AddMaterial", false, "Failed to find product material for %s", materialProductPath.c_str());
  229. return;
  230. }
  231. // The asset was found, so replace all the material assets in the given material assignment map.
  232. AZ::Data::Asset<AZ::RPI::MaterialAsset> materialAsset(assetId, azrtti_typeid<AZ::RPI::MaterialAsset>());
  233. for (auto& [id, override] : overrides)
  234. {
  235. if (id == AZ::Render::DefaultMaterialAssignmentId)
  236. {
  237. override.m_defaultMaterialAsset = materialAsset;
  238. }
  239. else
  240. {
  241. override.m_materialAsset = materialAsset;
  242. }
  243. }
  244. AZ_Info("AddMaterial", "Added product material %s\n", materialProductPath.c_str());
  245. }
  246. static void OverrideMaterialPbrSettings(
  247. const sdf::Material* material,
  248. const AZStd::shared_ptr<Utils::UrdfAssetMap>& assetMapping,
  249. AZ::Render::MaterialAssignmentMap& overrides)
  250. {
  251. if (auto pbr = material->PbrMaterial(); pbr)
  252. {
  253. // Start by trying to get the Metal workflow, since this is the workflow that O3DE uses.
  254. auto pbrWorkflow = pbr->Workflow(sdf::PbrWorkflowType::METAL);
  255. if (!pbrWorkflow)
  256. {
  257. // If the Metal workflow doesn't exist, try to get the Specular workflow.
  258. // Even though O3DE uses a Metal workflow, the vast majority of the Specular workflow data can still be used.
  259. // It's only the specular/glossiness maps that won't be converted.
  260. pbrWorkflow = pbr->Workflow(sdf::PbrWorkflowType::SPECULAR);
  261. if (!pbrWorkflow)
  262. {
  263. AZ_Error(
  264. "AddMaterial",
  265. false,
  266. "Material has a PBR definition, but it is neither a Metal nor a Specular workflow. Cannot convert.");
  267. return;
  268. }
  269. }
  270. for (auto& [id, materialAssignment] : overrides)
  271. {
  272. auto GetImageAssetIdFromPath = [&assetMapping](const std::string& uri) -> AZ::Data::AssetId
  273. {
  274. AZ::Data::AssetId assetId;
  275. const auto asset = PrefabMakerUtils::GetAssetFromPath(*assetMapping, uri);
  276. AZ_Warning("AddVisual", asset, "There is no source image asset for %s.", uri.c_str());
  277. if (asset)
  278. {
  279. assetId = Utils::GetImageProductAssetId(asset->m_sourceGuid);
  280. AZ_Warning(
  281. "AddVisual",
  282. assetId.IsValid(),
  283. "There is no product image asset for %s.",
  284. asset->m_sourceAssetRelativePath.c_str());
  285. }
  286. return assetId;
  287. };
  288. if (auto texture = pbrWorkflow->AlbedoMap(); !texture.empty())
  289. {
  290. materialAssignment.m_propertyOverrides.emplace(
  291. AZ::Name("baseColor.textureMap"), AZStd::any(GetImageAssetIdFromPath(texture)));
  292. }
  293. if (auto texture = pbrWorkflow->NormalMap(); !texture.empty())
  294. {
  295. materialAssignment.m_propertyOverrides.emplace(
  296. AZ::Name("normal.textureMap"), AZStd::any(GetImageAssetIdFromPath(texture)));
  297. }
  298. if (auto texture = pbrWorkflow->AmbientOcclusionMap(); !texture.empty())
  299. {
  300. materialAssignment.m_propertyOverrides.emplace(
  301. AZ::Name("occlusion.diffuseTextureMap"), AZStd::any(GetImageAssetIdFromPath(texture)));
  302. }
  303. if (auto texture = pbrWorkflow->EmissiveMap(); !texture.empty())
  304. {
  305. materialAssignment.m_propertyOverrides.emplace(AZ::Name("emissive.enable"), AZStd::any(true));
  306. materialAssignment.m_propertyOverrides.emplace(
  307. AZ::Name("emissive.textureMap"), AZStd::any(GetImageAssetIdFromPath(texture)));
  308. }
  309. if (pbrWorkflow->Type() == sdf::PbrWorkflowType::METAL)
  310. {
  311. if (auto texture = pbrWorkflow->RoughnessMap(); !texture.empty())
  312. {
  313. materialAssignment.m_propertyOverrides.emplace(
  314. AZ::Name("roughness.textureMap"), AZStd::any(GetImageAssetIdFromPath(texture)));
  315. }
  316. if (auto texture = pbrWorkflow->MetalnessMap(); !texture.empty())
  317. {
  318. materialAssignment.m_propertyOverrides.emplace(
  319. AZ::Name("metallic.textureMap"), AZStd::any(GetImageAssetIdFromPath(texture)));
  320. }
  321. if (pbrWorkflow->Element()->HasElement("roughness"))
  322. {
  323. materialAssignment.m_propertyOverrides.emplace(
  324. AZ::Name("roughness.factor"), AZStd::any(static_cast<float>(pbrWorkflow->Roughness())));
  325. }
  326. if (pbrWorkflow->Element()->HasElement("metalness"))
  327. {
  328. materialAssignment.m_propertyOverrides.emplace(
  329. AZ::Name("metallic.factor"), AZStd::any(static_cast<float>(pbrWorkflow->Metalness())));
  330. }
  331. }
  332. else
  333. {
  334. AZ_Warning(
  335. "AddMaterial",
  336. pbrWorkflow->GlossinessMap().empty(),
  337. "PBR material has a Glossiness map (%s), which is a Specular PBR workflow, not a Metal PBR workflow. It will not "
  338. "be converted.",
  339. pbrWorkflow->GlossinessMap().c_str());
  340. AZ_Warning(
  341. "AddMaterial",
  342. pbrWorkflow->SpecularMap().empty(),
  343. "PBR material has a Specular map (%s), which is a Specular PBR workflow, not a Metal PBR workflow. It will not be "
  344. "converted.",
  345. pbrWorkflow->SpecularMap().c_str());
  346. }
  347. }
  348. }
  349. }
  350. static void OverrideMaterialBaseColor(const sdf::Material* material, AZ::Render::MaterialAssignmentMap& overrides)
  351. {
  352. // Base Color: Try to use the diffuse color if the material has one, or fall back to using the ambient color as a backup option.
  353. if (material->Element()->HasElement("diffuse") || material->Element()->HasElement("ambient"))
  354. {
  355. // Get the material's diffuse color as the preferred option for the PBR material base color.
  356. // If a diffuse color didn't exist but ambient does, try to use that instead as the base color.
  357. // It will likely be too dark, but that's still probably better than not setting the color at all.
  358. // Convert from gamma to linear to try and account for the different color spaces between phong and PBR rendering.
  359. const auto materialColor = material->Element()->HasElement("diffuse") ? material->Diffuse() : material->Ambient();
  360. const AZ::Color baseColor = URDF::TypeConversions::ConvertColor(materialColor).GammaToLinear();
  361. for (auto& [id, materialAssignment] : overrides)
  362. {
  363. materialAssignment.m_propertyOverrides.emplace(AZ::Name("baseColor.color"), AZStd::any(baseColor));
  364. }
  365. }
  366. }
  367. static void OverrideMaterialTransparency(const sdf::Visual* visual, AZ::Render::MaterialAssignmentMap& overrides)
  368. {
  369. // Opacity: Use visual->transparency to set the material's opacity.
  370. if (visual->Element()->HasElement("transparency"))
  371. {
  372. const auto transparency = visual->Transparency();
  373. if (transparency > 0.0f)
  374. {
  375. // Override the material properties for every material used by the model.
  376. for (auto& [id, materialAssignment] : overrides)
  377. {
  378. materialAssignment.m_propertyOverrides.emplace(AZ::Name("opacity.mode"), AZStd::any(AZStd::string("Blended")));
  379. materialAssignment.m_propertyOverrides.emplace(AZ::Name("opacity.alphaSource"), AZStd::any(AZStd::string("None")));
  380. materialAssignment.m_propertyOverrides.emplace(AZ::Name("opacity.factor"), AZStd::any(1.0f - transparency));
  381. }
  382. }
  383. }
  384. }
  385. static void OverrideMaterialEmissiveSettings(const sdf::Material* material, AZ::Render::MaterialAssignmentMap& overrides)
  386. {
  387. // Emissive: If an emissive color has been specified, enable emissive on the material and set the emissive color to the provided
  388. // one.
  389. if (material->Element()->HasElement("emissive"))
  390. {
  391. // Get the color and convert from gamma to linear to try and account for the different color spaces between phong and PBR
  392. // rendering.
  393. const auto materialColor = material->Emissive();
  394. const AZ::Color emissiveColor = URDF::TypeConversions::ConvertColor(materialColor).GammaToLinear();
  395. // It seems to be fairly common to have an emissive entry of black, which isn't emissive at all.
  396. // Only enable the emissive color if it's a non-black value.
  397. if ((emissiveColor.GetR() > 0.0f) || (emissiveColor.GetG() > 0.0f) || (emissiveColor.GetB() > 0.0f))
  398. {
  399. for (auto& [id, materialAssignment] : overrides)
  400. {
  401. materialAssignment.m_propertyOverrides.emplace(AZ::Name("emissive.enable"), AZStd::any(true));
  402. materialAssignment.m_propertyOverrides.emplace(AZ::Name("emissive.color"), AZStd::any(emissiveColor));
  403. // The URDF/SDF file doesn't specify an emissive intensity, just a color.
  404. // We're arbitrarily using a value slightly higher than the default emissive intensity.
  405. // This value was picked based on observations of emissive color behaviors in Gazebo.
  406. // This intensity mostly preserves the color (though it lightens it a little) and
  407. // potentially adds a little bit of lighting to the scene if Bloom or Diffuse Probe Grid also exist in the world.
  408. materialAssignment.m_propertyOverrides.emplace(AZ::Name("emissive.intensity"), AZStd::any(5.5f));
  409. }
  410. }
  411. }
  412. }
  413. static void OverrideMaterialRoughness(const sdf::Material* material, AZ::Render::MaterialAssignmentMap& overrides)
  414. {
  415. // Metallic/Roughness: Try to use the shininess value for roughness if we have one, otherwise fall back to using the specular
  416. // brightness.
  417. if (material->Element()->HasElement("shininess") || material->Element()->HasElement("specular"))
  418. {
  419. float shininess = 0.0f;
  420. float roughness = 0.0f;
  421. if (material->Element()->HasElement("shininess"))
  422. {
  423. // If we have a shininess value, we'll use it to set both metallic and roughness.
  424. // The shinier it is, the more metallic and less rough we'll make the result.
  425. shininess = material->Shininess();
  426. roughness = 1.0f - shininess;
  427. }
  428. else
  429. {
  430. // We don't have shininess, so use the specular color to estimate metallic/roughness values.
  431. // Convert the specular color into a brightness value. To get the brightness, we'll use the average of the three
  432. // color values. This isn't the most perceptually accurate choice, but specular brightness isn't the same as roughness
  433. // anyways, so it's hard to say what perceptual brightness model would cause any better or worse results here.
  434. // Another possibility to consider would be taking the max of the RGB values.
  435. const auto materialColor = material->Specular();
  436. const AZ::Color specularColor = URDF::TypeConversions::ConvertColor(materialColor);
  437. const float specularBrightness = (specularColor.GetR() + specularColor.GetG() + specularColor.GetB()) / 3.0f;
  438. roughness = 1.0f - specularBrightness;
  439. // Since specular color doesn't really speak to shininess, we'll arbitrarily scale down the specular brightness to
  440. // 1/4 of the total brightness to modulate the metallic reflectiveness a little, but not too much. Without this scaling,
  441. // a white specular color would always become fully metallic, perfectly smooth, and therefore fully reflective.
  442. // With the scaling, a white specular color will be perfectly smooth but only 25% metallic, so it will have some
  443. // reflectivity but not a lot.
  444. shininess = specularBrightness * 0.25f;
  445. }
  446. for (auto& [id, materialAssignment] : overrides)
  447. {
  448. materialAssignment.m_propertyOverrides.emplace(AZ::Name("metallic.factor"), AZStd::any(shininess));
  449. materialAssignment.m_propertyOverrides.emplace(AZ::Name("roughness.factor"), AZStd::any(roughness));
  450. }
  451. }
  452. }
  453. static void OverrideMaterialDoubleSided(const sdf::Material* material, AZ::Render::MaterialAssignmentMap& overrides)
  454. {
  455. // DoubleSided: The double_sided element converts directly over to the O3DE doubleSided material attribute.
  456. if (material->Element()->HasElement("double_sided"))
  457. {
  458. // The default material property value is one-sided, so only override the value if it should be double-sided.
  459. if (material->DoubleSided())
  460. {
  461. for (auto& [id, materialAssignment] : overrides)
  462. {
  463. materialAssignment.m_propertyOverrides.emplace(AZ::Name("general.doubleSided"), AZStd::any(true));
  464. }
  465. }
  466. }
  467. }
  468. void VisualsMaker::AddMaterialForVisual(const sdf::Visual* visual, AZ::EntityId entityId, const AZ::Data::AssetId& assetId) const
  469. {
  470. auto material = visual->Material();
  471. if (!material)
  472. {
  473. // Material is optional, it might not appear on all visuals.
  474. return;
  475. }
  476. AZ_Assert(material->Element(), "Material has data but no Element pointer. Something unexpected has happened with the SDF parsing.");
  477. // Conversions from <material> in the file to O3DE are extremely imprecise because the data is going from a Phong model to PBR,
  478. // and there are no direct translations from one type of lighting model to the other. All of the conversions will create some
  479. // rough approximations of the source lighting data, but should hopefully provide a reasonable starting point for tuning the look.
  480. // Also, URDF/SDF files don't have a concept of overriding specific materials, so every material override generated
  481. // below will get applied to *all* materials for a mesh file.
  482. // First, force the model asset to get loaded into memory before adding the material component.
  483. // This is required so that we can get the default material map that will be used to override properties for each material.
  484. auto modelAsset = AZ::Data::AssetManager::Instance().GetAsset<AZ::RPI::ModelAsset>(assetId, AZ::Data::AssetLoadBehavior::Default);
  485. modelAsset.BlockUntilLoadComplete();
  486. AZ_Error(
  487. "AddMaterial",
  488. modelAsset.IsReady(),
  489. "Trying to create materials for a model that couldn't load. The generated material overrides may not work correctly.");
  490. // Initialize the material component configuration to contain all of the material mappings from the model.
  491. AZ::Render::MaterialComponentConfig config;
  492. config.m_materials = AZ::Render::GetDefaultMaterialMapFromModelAsset(modelAsset);
  493. // Try to override all of the various material settings based on what's contained in the <material> and <visual> elements in the
  494. // source file.
  495. OverrideScriptMaterial(material, config.m_materials);
  496. OverrideMaterialPbrSettings(material, m_urdfAssetsMapping, config.m_materials);
  497. OverrideMaterialBaseColor(material, config.m_materials);
  498. OverrideMaterialTransparency(visual, config.m_materials);
  499. OverrideMaterialEmissiveSettings(material, config.m_materials);
  500. OverrideMaterialRoughness(material, config.m_materials);
  501. OverrideMaterialDoubleSided(material, config.m_materials);
  502. // All the material overrides are in place, so get the entity, add the material component, and set its configuration to use the
  503. // material overrides.
  504. AZ::Entity* entity = AzToolsFramework::GetEntityById(entityId);
  505. AZ_Assert(entity, "Entity ID for visual %s couldn't be found.", visual->Name().c_str());
  506. auto component = entity->CreateComponent(AZ::Render::EditorMaterialComponentTypeId);
  507. component->SetConfiguration(config);
  508. }
  509. } // namespace ROS2RobotImporter