diff --git a/AutomatedTesting/Assets/TestAnim/parent_tops.fbx b/AutomatedTesting/Assets/TestAnim/parent_tops.fbx new file mode 100644 index 0000000000..f13323ae8b --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/parent_tops.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3789abdf439a6d70438fd4bb1e06881ae6686a4699209c6bc371d22d161e5347 +size 26476 diff --git a/AutomatedTesting/Assets/TestAnim/parenting_test_default.fbx b/AutomatedTesting/Assets/TestAnim/parenting_test_default.fbx new file mode 100644 index 0000000000..d3e189bdc5 --- /dev/null +++ b/AutomatedTesting/Assets/TestAnim/parenting_test_default.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c987d7d79685fda83efcffb7e1afbcd356c37fc68ec5c663a89b02d4df10caea +size 46412 diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp index 6ccc82652b..631c6a1596 100644 --- a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp @@ -18,13 +18,57 @@ #include #include #include +#include +#include +#include +#include #include #include #include +#include #include +#include +#include #include +// a mock AZ::Render::EditorMeshComponent +namespace AZ::Render +{ + struct EditorMeshComponent + : public AZ::Component + { + AZ_COMPONENT(EditorMeshComponent, "{DCE68F6E-2E16-4CB4-A834-B6C2F900A7E9}"); + void Activate() override {} + void Deactivate() override {} + + static void Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) + { + serializeContext->Class(); + } + + if (BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("AZ::Render::EditorMeshComponent"); + } + + } + }; + + struct EditorMeshComponentHelper + : public AZ::ComponentDescriptorHelper + { + AZ_CLASS_ALLOCATOR(EditorMeshComponentHelper, AZ::SystemAllocator, 0); + + void Reflect(AZ::ReflectContext* reflection) const override + { + AZ::Render::EditorMeshComponent::Reflect(reflection); + } + }; +} + namespace UnitTest { class PrefabBehaviorTests @@ -39,10 +83,12 @@ namespace UnitTest AZ::AllocatorInstance().Create(); } AZ::SceneAPI::SceneCoreStandaloneAllocator::Initialize(AZ::Environment::GetInstance()); + AZ::SceneAPI::SceneDataStandaloneAllocator::Initialize(AZ::Environment::GetInstance()); } static void TearDownTestCase() { + AZ::SceneAPI::SceneDataStandaloneAllocator::TearDown(); AZ::SceneAPI::SceneCoreStandaloneAllocator::TearDown(); AZ::AllocatorInstance().Destroy(); } @@ -62,10 +108,16 @@ namespace UnitTest return PrefabBehaviorTests::OnGetSourceInfoBySourcePath(path, info); }); m_assetSystemRequestMock.BusConnect(); + + m_componentDescriptorHelperEditorMeshComponent = AZStd::make_unique(); + m_componentDescriptorHelperEditorMeshComponent->Reflect(m_app.GetSerializeContext()); + m_componentDescriptorHelperEditorMeshComponent->Reflect(m_app.GetBehaviorContext()); } void TearDown() override { + m_componentDescriptorHelperEditorMeshComponent.reset(); + m_assetSystemRequestMock.BusDisconnect(); m_prefabGroupBehavior->Deactivate(); @@ -86,6 +138,72 @@ namespace UnitTest return true; } + AZStd::shared_ptr CreateMockScene() + { + using namespace AZ::SceneAPI; + + auto scene = AZStd::make_shared("mock_scene"); + scene->SetManifestFilename("ManifestFilename"); + scene->SetSource("Source", AZ::Uuid::CreateRandom()); + scene->SetWatchFolder("WatchFolder"); + + /*---------------------------------------\ + Root + | + 1 + | + 2 + / \ + ------3m 7 + / / / \ + 6 5 4t 8m------- + \ \ \ + 9t 10 11 + \---------------------------------------*/ + + // Build up the graph + auto root = scene->GetGraph().GetRoot(); + scene->GetGraph().SetContent(root, AZStd::make_shared(0)); + auto index1 = scene->GetGraph().AddChild(root, "1", AZStd::make_shared(1)); + auto index2 = scene->GetGraph().AddChild(index1, "2", AZStd::make_shared(2)); + auto index3 = scene->GetGraph().AddChild(index2, "3", AZStd::make_shared()); + auto index4 = scene->GetGraph().AddChild(index3, "4", AZStd::make_shared()); + auto index5 = scene->GetGraph().AddChild(index3, "5", AZStd::make_shared(5)); + auto index6 = scene->GetGraph().AddChild(index3, "6", AZStd::make_shared(6)); + auto index7 = scene->GetGraph().AddChild(index2, "7", AZStd::make_shared(7)); + auto index8 = scene->GetGraph().AddChild(index7, "8", AZStd::make_shared()); + auto index9 = scene->GetGraph().AddChild(index8, "9", AZStd::make_shared()); + auto index10 = scene->GetGraph().AddChild(index8, "10", AZStd::make_shared(10)); + auto index11 = scene->GetGraph().AddChild(index8, "11", AZStd::make_shared(11)); + + scene->GetGraph().MakeEndPoint(index4); + scene->GetGraph().MakeEndPoint(index5); + scene->GetGraph().MakeEndPoint(index6); + scene->GetGraph().MakeEndPoint(index9); + scene->GetGraph().MakeEndPoint(index10); + scene->GetGraph().MakeEndPoint(index11); + + return scene; + } + + // mock classes and structures + + struct MockTransform + : public AZ::SceneAPI::DataTypes::ITransform + { + AZ::Matrix3x4 m_matrix = AZ::Matrix3x4::CreateIdentity(); + + AZ::Matrix3x4& GetMatrix() override + { + return m_matrix; + } + + const AZ::Matrix3x4& GetMatrix() const + { + return m_matrix; + } + }; + struct TestPreExportEventContext { TestPreExportEventContext() @@ -110,6 +228,7 @@ namespace UnitTest AZStd::unique_ptr m_prefabGroupBehavior; testing::NiceMock m_assetSystemRequestMock; + AZStd::unique_ptr m_componentDescriptorHelperEditorMeshComponent; }; TEST_F(PrefabBehaviorTests, PrefabBehavior_EmptyContextIgnored_Works) @@ -164,4 +283,72 @@ namespace UnitTest AZ_Warning("testing", false, "The product asset (%s) is missing", pathStr.c_str()); } } + + TEST_F(PrefabBehaviorTests, PrefabBehavior_UpdateManifestWithEmptyScene_DoesNotFail) + { + using namespace AZ::SceneAPI; + using namespace AZ::SceneAPI::Events; + + Containers::Scene scene("empty_scene"); + AssetImportRequest::ManifestAction action = AssetImportRequest::ManifestAction::ConstructDefault; + AssetImportRequest::RequestingApplication requester = {}; + + Behaviors::PrefabGroupBehavior prefabGroupBehavior; + ProcessingResult result = ProcessingResult::Failure; + AssetImportRequestBus::BroadcastResult(result, &AssetImportRequestBus::Events::UpdateManifest, scene, action, requester); + EXPECT_NE(result, ProcessingResult::Failure); + } + + TEST_F(PrefabBehaviorTests, PrefabBehavior_UpdateManifestWithEmptyScene_Ignored) + { + using namespace AZ::SceneAPI; + using namespace AZ::SceneAPI::Events; + + Containers::Scene scene("empty_scene"); + AssetImportRequest::ManifestAction action = AssetImportRequest::ManifestAction::Update; + AssetImportRequest::RequestingApplication requester = {}; + + Behaviors::PrefabGroupBehavior prefabGroupBehavior; + ProcessingResult result = ProcessingResult::Failure; + AssetImportRequestBus::BroadcastResult(result, &AssetImportRequestBus::Events::UpdateManifest, scene, action, requester); + EXPECT_EQ(result, ProcessingResult::Ignored); + } + + TEST_F(PrefabBehaviorTests, PrefabBehavior_UpdateManifestMockScene_CreatesPrefab) + { + using namespace AZ::SceneAPI; + using namespace AZ::SceneAPI::Events; + + auto scene = CreateMockScene(); + AssetImportRequest::ManifestAction action = AssetImportRequest::ManifestAction::ConstructDefault; + AssetImportRequest::RequestingApplication requester = {}; + + Behaviors::PrefabGroupBehavior prefabGroupBehavior; + ProcessingResult result = ProcessingResult::Failure; + AssetImportRequestBus::BroadcastResult(result, &AssetImportRequestBus::Events::UpdateManifest, *scene, action, requester); + EXPECT_EQ(result, ProcessingResult::Success); + EXPECT_EQ(scene->GetManifest().GetEntryCount(), 3); + EXPECT_TRUE(azrtti_istypeof(scene->GetManifest().GetValue(0).get())); + EXPECT_TRUE(azrtti_istypeof(scene->GetManifest().GetValue(1).get())); + EXPECT_TRUE(azrtti_istypeof(scene->GetManifest().GetValue(2).get())); + } + + TEST_F(PrefabBehaviorTests, PrefabBehavior_UpdateManifest_ToggleWorks) + { + using namespace AZ::SceneAPI; + using namespace AZ::SceneAPI::Events; + + ASSERT_TRUE(AZ::SettingsRegistry::Get()); + AZ::SettingsRegistry::Get()->Set("/O3DE/Preferences/Prefabs/CreateDefaults", false); + + auto scene = CreateMockScene(); + AssetImportRequest::ManifestAction action = AssetImportRequest::ManifestAction::ConstructDefault; + AssetImportRequest::RequestingApplication requester = {}; + + Behaviors::PrefabGroupBehavior prefabGroupBehavior; + ProcessingResult result = ProcessingResult::Failure; + AssetImportRequestBus::BroadcastResult(result, &AssetImportRequestBus::Events::UpdateManifest, *scene, action, requester); + EXPECT_EQ(result, ProcessingResult::Ignored); + EXPECT_EQ(scene->GetManifest().GetEntryCount(), 0); + } } diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp index af2a54f7a5..292d7a2822 100644 --- a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -20,16 +21,42 @@ #include #include #include +#include +#include +#include #include +#include #include -#include +#include #include -#include -#include -#include -#include -#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AZStd +{ + template<> struct hash + { + inline size_t operator()(const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex) const + { + size_t hashValue{ 0 }; + hash_combine(hashValue, nodeIndex.AsNumber()); + return hashValue; + } + }; +} namespace AZ::SceneAPI::Behaviors { @@ -37,8 +64,11 @@ namespace AZ::SceneAPI::Behaviors // ExportEventHandler // + static constexpr const char s_PrefabGroupBehaviorCreateDefaultKey[] = "/O3DE/Preferences/Prefabs/CreateDefaults"; + struct PrefabGroupBehavior::ExportEventHandler final : public AZ::SceneAPI::SceneCore::ExportingComponent + , public Events::AssetImportRequestBus::Handler { using PreExportEventContextFunction = AZStd::function; PreExportEventContextFunction m_preExportEventContextFunction; @@ -51,10 +81,12 @@ namespace AZ::SceneAPI::Behaviors { BindToCall(&ExportEventHandler::PrepareForExport); AZ::SceneAPI::SceneCore::ExportingComponent::Activate(); + Events::AssetImportRequestBus::Handler::BusConnect(); } ~ExportEventHandler() { + Events::AssetImportRequestBus::Handler::BusDisconnect(); AZ::SceneAPI::SceneCore::ExportingComponent::Deactivate(); } @@ -62,8 +94,338 @@ namespace AZ::SceneAPI::Behaviors { return m_preExportEventContextFunction(context); } + + using MeshTransformPair = AZStd::pair; + using MeshTransformEntry = AZStd::pair; + using MeshTransformMap = AZStd::unordered_map; + + MeshTransformMap CalculateMeshTransformMap(const Containers::Scene& scene) + { + auto graph = scene.GetGraph(); + const auto view = Containers::Views::MakeSceneGraphDownwardsView( + graph, + graph.GetRoot(), + graph.GetContentStorage().cbegin(), + true); + + if (view.begin() == view.end()) + { + return {}; + } + + MeshTransformMap meshTransformMap; + for (auto it = view.begin(); it != view.end(); ++it) + { + Containers::SceneGraph::NodeIndex currentIndex = graph.ConvertToNodeIndex(it.GetHierarchyIterator()); + AZStd::string currentNodeName = graph.GetNodeName(currentIndex).GetPath(); + auto currentContent = graph.GetNodeContent(currentIndex); + if (currentContent) + { + if (azrtti_istypeof(currentContent.get())) + { + auto parentIndex = graph.GetNodeParent(currentIndex); + if (parentIndex.IsValid() == false) + { + continue; + } + auto parentContent = graph.GetNodeContent(parentIndex); + if (parentContent && azrtti_istypeof(parentContent.get())) + { + // map the node parent to the ITransform + MeshTransformPair pair{ parentIndex, currentIndex }; + meshTransformMap.emplace(MeshTransformEntry{ graph.GetNodeParent(parentIndex), AZStd::move(pair) }); + } + } + } + } + return meshTransformMap; + } + + using ManifestUpdates = AZStd::vector>; + using NodeEntityMap = AZStd::unordered_map; + + NodeEntityMap CreateMeshGroups( + ManifestUpdates& manifestUpdates, + const MeshTransformMap& meshTransformMap, + const Containers::Scene& scene, + AZStd::string& relativeSourcePath) + { + NodeEntityMap nodeEntityMap; + const auto& graph = scene.GetGraph(); + + for (const auto& entry : meshTransformMap) + { + const auto thisNodeIndex = entry.first; + const auto meshNodeIndex = entry.second.first; + const auto meshNodeName = graph.GetNodeName(meshNodeIndex); + AZStd::string meshNodePath{ meshNodeName.GetPath() }; + + AZStd::string meshNodeFullName; + meshNodeFullName = relativeSourcePath; + meshNodeFullName.append("_"); + meshNodeFullName.append(meshNodeName.GetName()); + + auto meshGroup = AZStd::make_shared(); + meshGroup->SetName(meshNodeFullName.c_str()); + meshGroup->GetSceneNodeSelectionList().AddSelectedNode(AZStd::move(meshNodePath)); + for (const auto& meshGoupNamePair : meshTransformMap) + { + if (meshGoupNamePair.first != thisNodeIndex) + { + const auto nodeName = graph.GetNodeName(meshGoupNamePair.second.first); + meshGroup->GetSceneNodeSelectionList().RemoveSelectedNode(nodeName.GetPath()); + } + } + meshGroup->OverrideId(DataTypes::Utilities::CreateStableUuid( + scene, + azrtti_typeid(), + meshNodeName.GetPath())); + + // this clears out the mesh coordinates each mesh group will be rotated and translated + // using the attached scene graph node + auto coordinateSystemRule = AZStd::make_shared(); + coordinateSystemRule->SetUseAdvancedData(true); + coordinateSystemRule->SetRotation(AZ::Quaternion::CreateIdentity()); + coordinateSystemRule->SetTranslation(AZ::Vector3::CreateZero()); + coordinateSystemRule->SetScale(1.0f); + meshGroup->GetRuleContainer().AddRule(coordinateSystemRule); + + manifestUpdates.emplace_back(meshGroup); + + // create an entity for each MeshGroup + AZ::EntityId entityId; + AzToolsFramework::EntityUtilityBus::BroadcastResult( + entityId, + &AzToolsFramework::EntityUtilityBus::Events::CreateEditorReadyEntity, + meshNodeName.GetName()); + + if (entityId.IsValid() == false) + { + return {}; + } + + // Since the mesh component lives in a gem, then create it by name + AzFramework::BehaviorComponentId editorMeshComponent; + AzToolsFramework::EntityUtilityBus::BroadcastResult( + editorMeshComponent, + &AzToolsFramework::EntityUtilityBus::Events::GetOrAddComponentByTypeName, + entityId, + "{DCE68F6E-2E16-4CB4-A834-B6C2F900A7E9} AZ::Render::EditorMeshComponent"); + + if (editorMeshComponent.IsValid() == false) + { + AZ_Warning("prefab", false, "Could not add the EditorMeshComponent component; project needs Atom enabled."); + return {}; + } + + // assign mesh asset id hint using JSON + auto meshAssetJson = AZStd::string::format( + R"JSON( + {"Controller": {"Configuration": {"ModelAsset": { "assetHint": "%s.azmodel"}}}} + )JSON", meshNodeFullName.c_str()); + + bool result = false; + AzToolsFramework::EntityUtilityBus::BroadcastResult( + result, + &AzToolsFramework::EntityUtilityBus::Events::UpdateComponentForEntity, + entityId, + editorMeshComponent, + meshAssetJson); + + if (result == false) + { + AZ_Error("prefab", false, "UpdateComponentForEntity failed for EditorMeshComponent component"); + return {}; + } + + nodeEntityMap.insert({ thisNodeIndex, entityId }); + } + + return nodeEntityMap; + } + + using EntityIdList = AZStd::vector; + + EntityIdList FixUpEntityParenting( + const NodeEntityMap& nodeEntityMap, + const Containers::SceneGraph& graph, + const MeshTransformMap& meshTransformMap) + { + EntityIdList entities; + entities.reserve(nodeEntityMap.size()); + + for (const auto& nodeEntity : nodeEntityMap) + { + entities.emplace_back(nodeEntity.second); + + // find matching parent EntityId (if any) + AZ::EntityId parentEntityId; + const auto thisNodeIndex = nodeEntity.first; + auto parentNodeIndex = graph.GetNodeParent(thisNodeIndex); + while (parentNodeIndex.IsValid()) + { + auto parentNodeIterator = meshTransformMap.find(parentNodeIndex); + if (meshTransformMap.end() != parentNodeIterator) + { + auto parentEntiyIterator = nodeEntityMap.find(parentNodeIterator->first); + if (nodeEntityMap.end() != parentEntiyIterator) + { + parentEntityId = parentEntiyIterator->second; + break; + } + } + else if (graph.HasNodeParent(parentNodeIndex)) + { + parentNodeIndex = graph.GetNodeParent(parentNodeIndex); + } + else + { + parentNodeIndex = {}; + } + } + + AZ::Entity* entity = AZ::Interface::Get()->FindEntity(nodeEntity.second); + auto* entityTransform = entity->FindComponent(); + if (!entityTransform) + { + return {}; + } + + // parent entities + if (parentEntityId.IsValid()) + { + entityTransform->SetParent(parentEntityId); + } + + auto thisNodeIterator = meshTransformMap.find(thisNodeIndex); + AZ_Assert(thisNodeIterator != meshTransformMap.end(), "This node index missing."); + auto thisTransformIndex = thisNodeIterator->second.second; + + // get node matrix data to set the entity's local transform + const auto nodeTransform = azrtti_cast(graph.GetNodeContent(thisTransformIndex)); + entityTransform->SetLocalTM(AZ::Transform::CreateFromMatrix3x4(nodeTransform->GetMatrix())); + } + + return entities; + } + + bool CreatePrefabGroup( + ManifestUpdates& manifestUpdates, + Containers::Scene& scene, + const EntityIdList& entities, + const AZStd::string& filenameOnly, + const AZStd::string& relativeSourcePath) + { + AZ::Interface::Get()->RemoveAllTemplates(); + + // create prefab group for entire stack + AzToolsFramework::Prefab::TemplateId prefabTemplateId; + AzToolsFramework::Prefab::PrefabSystemScriptingBus::BroadcastResult( + prefabTemplateId, + &AzToolsFramework::Prefab::PrefabSystemScriptingBus::Events::CreatePrefabTemplate, + entities, + filenameOnly); + + if (prefabTemplateId == AzToolsFramework::Prefab::InvalidTemplateId) + { + AZ_Error("prefab", false, "Could not create a prefab template for entites."); + return false; + } + + // Convert the prefab to a JSON string + AZ::Outcome outcome; + AzToolsFramework::Prefab::PrefabLoaderScriptingBus::BroadcastResult( + outcome, + &AzToolsFramework::Prefab::PrefabLoaderScriptingBus::Events::SaveTemplateToString, + prefabTemplateId); + + if (outcome.IsSuccess() == false) + { + AZ_Error("prefab", false, "Could not create JSON string for template; maybe NaN values in the template?"); + return false; + } + + AzToolsFramework::Prefab::PrefabDom prefabDom; + prefabDom.Parse(outcome.GetValue().c_str()); + + auto prefabGroup = AZStd::make_shared(); + prefabGroup->SetName(relativeSourcePath); + prefabGroup->SetPrefabDom(AZStd::move(prefabDom)); + prefabGroup->SetId(DataTypes::Utilities::CreateStableUuid( + scene, + azrtti_typeid(), + relativeSourcePath)); + + manifestUpdates.emplace_back(prefabGroup); + + // update manifest if there where no errors + for (auto update : manifestUpdates) + { + scene.GetManifest().AddEntry(update); + } + + return true; + } + + // AssetImportRequest + Events::ProcessingResult UpdateManifest(Containers::Scene& scene, ManifestAction action, RequestingApplication requester) override; }; + Events::ProcessingResult PrefabGroupBehavior::ExportEventHandler::UpdateManifest( + Containers::Scene& scene, + ManifestAction action, + [[maybe_unused]] RequestingApplication requester) + { + if (action != Events::AssetImportRequest::ConstructDefault) + { + return Events::ProcessingResult::Ignored; + } + + // this toggle makes constructing default mesh groups and a prefab optional + if (AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry) + { + bool createDefaultPrefab = true; + settingsRegistry->Get(createDefaultPrefab, s_PrefabGroupBehaviorCreateDefaultKey); + if (createDefaultPrefab == false) + { + return Events::ProcessingResult::Ignored; + } + } + + auto meshTransformMap = CalculateMeshTransformMap(scene); + if (meshTransformMap.empty()) + { + return Events::ProcessingResult::Ignored; + } + + // compute the filenames of the scene file + AZStd::string relativeSourcePath = scene.GetSourceFilename(); + AZ::StringFunc::Replace(relativeSourcePath, ".", "_"); + // the watch folder and forward slash is used to in the asset hint path of the file + AZStd::string watchFolder = scene.GetWatchFolder() + "/"; + AZ::StringFunc::Replace(relativeSourcePath, watchFolder.c_str(), ""); + AZStd::string filenameOnly{ relativeSourcePath }; + AZ::StringFunc::Path::GetFileName(filenameOnly.c_str(), filenameOnly); + AZ::StringFunc::Path::ReplaceExtension(filenameOnly, "prefab"); + + ManifestUpdates manifestUpdates; + + auto nodeEntityMap = CreateMeshGroups(manifestUpdates, meshTransformMap, scene, relativeSourcePath); + if(nodeEntityMap.empty()) + { + return Events::ProcessingResult::Ignored; + } + + auto entities = FixUpEntityParenting(nodeEntityMap, scene.GetGraph(), meshTransformMap); + if(entities.empty()) + { + return Events::ProcessingResult::Ignored; + } + + bool success = CreatePrefabGroup(manifestUpdates, scene, entities, filenameOnly, relativeSourcePath); + return success ? Events::ProcessingResult::Success : Events::ProcessingResult::Ignored; + } + // // PrefabGroupBehavior //