#include #include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportSelectionUtils.h" #include #include #include #include #include #include #include #include #include #include namespace { using XCEngine::Components::GameObject; using XCEngine::Components::CameraComponent; using XCEngine::Components::CameraProjectionType; using XCEngine::Components::MeshFilterComponent; using XCEngine::Components::MeshRendererComponent; using XCEngine::Components::Scene; using XCEngine::Editor::BuildSceneViewportCameraData; using XCEngine::Editor::BuildSceneViewportProjectionMatrix; using XCEngine::Editor::BuildSceneViewportViewMatrix; using XCEngine::Editor::CollectSceneViewportSelectionRenderables; using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Math::Matrix4x4; using XCEngine::Math::Vector3; using XCEngine::Resources::Mesh; using XCEngine::Resources::MeshSection; using XCEngine::Resources::StaticMeshVertex; bool NearlyEqual(float lhs, float rhs, float epsilon = 1e-4f) { return std::abs(lhs - rhs) <= epsilon; } bool NearlyEqual(const Matrix4x4& lhs, const Matrix4x4& rhs, float epsilon = 1e-4f) { for (int row = 0; row < 4; ++row) { for (int column = 0; column < 4; ++column) { if (!NearlyEqual(lhs.m[row][column], rhs.m[row][column], epsilon)) { return false; } } } return true; } std::unique_ptr CreateSelectionMesh(uint64_t guid, bool splitIntoTwoSections) { auto mesh = std::make_unique(); XCEngine::Resources::IResource::ConstructParams params = {}; params.name = "SelectionMesh"; params.path = "memory://selection_mesh"; params.guid = XCEngine::Resources::ResourceGUID(guid); mesh->Initialize(params); const StaticMeshVertex vertices[] = { { Vector3(-1.0f, -1.0f, 0.0f) }, { Vector3(1.0f, -1.0f, 0.0f) }, { Vector3(0.0f, 1.0f, 0.0f) }, { Vector3(-1.0f, -1.0f, 0.0f) }, { Vector3(0.0f, 1.0f, 0.0f) }, { Vector3(-2.0f, 1.0f, 0.0f) } }; const uint16_t indices[] = { 0, 1, 2, 3, 4, 5 }; mesh->SetVertexData( vertices, sizeof(vertices), static_cast(std::size(vertices)), sizeof(StaticMeshVertex), XCEngine::Resources::VertexAttribute::Position); mesh->SetIndexData(indices, sizeof(indices), static_cast(std::size(indices)), false); mesh->SetBounds(XCEngine::Math::Bounds(Vector3(-0.5f, 0.0f, 0.0f), Vector3(3.0f, 2.0f, 0.0f))); MeshSection firstSection = {}; firstSection.baseVertex = 0; firstSection.vertexCount = 3; firstSection.startIndex = 0; firstSection.indexCount = 3; firstSection.materialID = 0; firstSection.bounds = mesh->GetBounds(); mesh->AddSection(firstSection); if (splitIntoTwoSections) { MeshSection secondSection = {}; secondSection.baseVertex = 3; secondSection.vertexCount = 3; secondSection.startIndex = 3; secondSection.indexCount = 3; secondSection.materialID = 1; secondSection.bounds = mesh->GetBounds(); mesh->AddSection(secondSection); } return mesh; } class SceneViewportSelectionUtilsTest : public ::testing::Test { protected: SceneViewportOverlayData CreateOverlay() const { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3(2.0f, 3.0f, -8.0f); overlay.cameraForward = Vector3::Forward(); overlay.cameraRight = Vector3::Right(); overlay.cameraUp = Vector3::Up(); overlay.verticalFovDegrees = 60.0f; overlay.nearClipPlane = 0.03f; overlay.farClipPlane = 2000.0f; return overlay; } GameObject* CreateMeshObject( Scene& scene, const std::string& name, const Vector3& position, bool enabled = true, bool splitIntoTwoSections = false) { std::unique_ptr mesh = CreateSelectionMesh(++m_nextMeshGuid, splitIntoTwoSections); Mesh* meshPtr = mesh.get(); m_meshes.push_back(std::move(mesh)); GameObject* object = scene.CreateGameObject(name); object->GetTransform()->SetPosition(position); auto* meshFilter = object->AddComponent(); auto* meshRenderer = object->AddComponent(); meshFilter->SetMesh(meshPtr); meshFilter->SetEnabled(enabled); meshRenderer->SetEnabled(enabled); return object; } private: uint64_t m_nextMeshGuid = 1; std::vector> m_meshes; }; TEST_F(SceneViewportSelectionUtilsTest, BuildSceneViewportCameraDataMatchesViewportMathConvention) { const SceneViewportOverlayData overlay = CreateOverlay(); const auto cameraData = BuildSceneViewportCameraData(overlay, 1280, 720); EXPECT_TRUE(NearlyEqual( cameraData.view.Transpose(), BuildSceneViewportViewMatrix(overlay))); EXPECT_TRUE(NearlyEqual( cameraData.projection.Transpose(), BuildSceneViewportProjectionMatrix(overlay, 1280.0f, 720.0f))); EXPECT_EQ(cameraData.viewportWidth, 1280u); EXPECT_EQ(cameraData.viewportHeight, 720u); EXPECT_EQ(cameraData.worldPosition, overlay.cameraPosition); } TEST_F(SceneViewportSelectionUtilsTest, BuildSceneViewportCameraDataFromCameraMatchesRendererConvention) { Scene scene("SelectionUtilsScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); ASSERT_NE(camera, nullptr); cameraObject->GetTransform()->SetPosition(Vector3(2.0f, 3.0f, -8.0f)); cameraObject->GetTransform()->SetRotation(XCEngine::Math::Quaternion::FromEulerAngles( 12.0f * XCEngine::Math::DEG_TO_RAD, -37.0f * XCEngine::Math::DEG_TO_RAD, 5.0f * XCEngine::Math::DEG_TO_RAD)); camera->SetProjectionType(CameraProjectionType::Perspective); camera->SetFieldOfView(53.0f); camera->SetNearClipPlane(0.03f); camera->SetFarClipPlane(1500.0f); const auto cameraData = BuildSceneViewportCameraData(*camera, 1280, 720); EXPECT_TRUE(NearlyEqual( cameraData.view.Transpose(), cameraObject->GetTransform()->GetWorldToLocalMatrix())); EXPECT_TRUE(NearlyEqual( cameraData.projection.Transpose(), Matrix4x4::Perspective( 53.0f * XCEngine::Math::DEG_TO_RAD, 1280.0f / 720.0f, 0.03f, 1500.0f))); EXPECT_EQ(cameraData.worldPosition, cameraObject->GetTransform()->GetPosition()); } TEST_F(SceneViewportSelectionUtilsTest, CollectSceneViewportSelectionRenderablesFiltersInvalidSelectionTargets) { Scene scene("SelectionUtilsScene"); GameObject* validObject = CreateMeshObject(scene, "Valid", Vector3(0.0f, 0.0f, 4.0f)); GameObject* disabledObject = CreateMeshObject(scene, "Disabled", Vector3(2.0f, 0.0f, 4.0f), false); GameObject* inactiveObject = CreateMeshObject(scene, "Inactive", Vector3(-2.0f, 0.0f, 4.0f)); inactiveObject->SetActive(false); const std::vector selection = { 0u, validObject->GetID(), disabledObject->GetID(), inactiveObject->GetID(), validObject->GetID(), 987654321u }; const auto renderables = CollectSceneViewportSelectionRenderables( scene, selection, CreateOverlay().cameraPosition); ASSERT_EQ(renderables.size(), 1u); EXPECT_EQ(renderables[0].gameObject, validObject); EXPECT_EQ(renderables[0].meshRenderer, validObject->GetComponent()); EXPECT_TRUE(renderables[0].hasSection); } TEST_F(SceneViewportSelectionUtilsTest, CollectSceneViewportSelectionRenderablesExpandsMeshSections) { Scene scene("SelectionUtilsScene"); GameObject* object = CreateMeshObject( scene, "Sectioned", Vector3(1.0f, 0.0f, 6.0f), true, true); const auto renderables = CollectSceneViewportSelectionRenderables( scene, { object->GetID() }, CreateOverlay().cameraPosition); ASSERT_EQ(renderables.size(), 2u); EXPECT_EQ(renderables[0].gameObject, object); EXPECT_EQ(renderables[0].sectionIndex, 0u); EXPECT_EQ(renderables[0].materialIndex, 0u); EXPECT_EQ(renderables[1].sectionIndex, 1u); EXPECT_EQ(renderables[1].materialIndex, 1u); } } // namespace