#include #include "Viewport/SceneViewportPicker.h" #include #include #include #include #include #include #include #include namespace { using XCEngine::Components::GameObject; using XCEngine::Components::MeshFilterComponent; using XCEngine::Components::MeshRendererComponent; using XCEngine::Components::Scene; using XCEngine::Editor::BuildSceneViewportRay; using XCEngine::Editor::PickSceneViewportEntity; using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Editor::SceneViewportPickRequest; using XCEngine::Math::Quaternion; using XCEngine::Math::Ray; using XCEngine::Math::Vector2; using XCEngine::Math::Vector3; using XCEngine::Resources::Mesh; using XCEngine::Resources::MeshSection; using XCEngine::Resources::StaticMeshVertex; std::unique_ptr CreateTriangleMesh(uint64_t guid) { auto mesh = std::make_unique(); XCEngine::Resources::IResource::ConstructParams params = {}; params.name = "TestTriangle"; params.path = "memory://test_triangle"; 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) } }; const uint16_t indices[] = { 0, 1, 2 }; 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.0f, 0.0f, 0.0f), Vector3(2.0f, 2.0f, 0.0f))); MeshSection section = {}; section.baseVertex = 0; section.vertexCount = static_cast(std::size(vertices)); section.startIndex = 0; section.indexCount = static_cast(std::size(indices)); section.materialID = 0; section.bounds = mesh->GetBounds(); mesh->AddSection(section); return mesh; } class SceneViewportPickerTest : public ::testing::Test { protected: SceneViewportOverlayData CreateDefaultOverlay() const { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3::Zero(); 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* CreateTriangleObject( Scene& scene, const std::string& name, const Vector3& position, const Quaternion& rotation = Quaternion::Identity(), const Vector3& scale = Vector3::One()) { std::unique_ptr mesh = CreateTriangleMesh(++m_nextMeshGuid); Mesh* meshPtr = mesh.get(); m_meshes.push_back(std::move(mesh)); GameObject* object = scene.CreateGameObject(name); object->GetTransform()->SetPosition(position); object->GetTransform()->SetRotation(rotation); object->GetTransform()->SetScale(scale); auto* meshFilter = object->AddComponent(); auto* meshRenderer = object->AddComponent(); meshFilter->SetMesh(meshPtr); meshRenderer->SetEnabled(true); return object; } private: uint64_t m_nextMeshGuid = 1; std::vector> m_meshes; }; TEST_F(SceneViewportPickerTest, BuildSceneViewportRayPointsThroughViewportCenter) { Ray ray; ASSERT_TRUE(BuildSceneViewportRay( CreateDefaultOverlay(), Vector2(1280.0f, 720.0f), Vector2(640.0f, 360.0f), ray)); EXPECT_NEAR(ray.origin.x, 0.0f, 1e-4f); EXPECT_NEAR(ray.origin.y, 0.0f, 1e-4f); EXPECT_NEAR(ray.origin.z, 0.0f, 1e-4f); EXPECT_NEAR(ray.direction.x, 0.0f, 1e-4f); EXPECT_NEAR(ray.direction.y, 0.0f, 1e-4f); EXPECT_NEAR(ray.direction.z, 1.0f, 1e-4f); } TEST_F(SceneViewportPickerTest, PickSceneViewportEntityReturnsNearestHit) { Scene scene("PickerScene"); GameObject* farObject = CreateTriangleObject(scene, "Far", Vector3(0.0f, 0.0f, 6.0f)); GameObject* nearObject = CreateTriangleObject(scene, "Near", Vector3(0.0f, 0.0f, 3.0f)); SceneViewportPickRequest request = {}; request.scene = &scene; request.overlay = CreateDefaultOverlay(); request.viewportSize = Vector2(1280.0f, 720.0f); request.viewportPosition = Vector2(640.0f, 360.0f); const auto result = PickSceneViewportEntity(request); EXPECT_TRUE(result.hit); EXPECT_EQ(result.entityId, nearObject->GetID()); EXPECT_LT(result.distanceSq, (farObject->GetTransform()->GetPosition() - request.overlay.cameraPosition).SqrMagnitude()); } TEST_F(SceneViewportPickerTest, PickSceneViewportEntityHandlesRotatedAndScaledObjects) { Scene scene("PickerScene"); GameObject* object = CreateTriangleObject( scene, "Rotated", Vector3(0.0f, 0.0f, 4.0f), Quaternion::FromEulerAngles(0.0f, 45.0f * XCEngine::Math::DEG_TO_RAD, 0.0f), Vector3(2.0f, 2.0f, 2.0f)); SceneViewportPickRequest request = {}; request.scene = &scene; request.overlay = CreateDefaultOverlay(); request.viewportSize = Vector2(1280.0f, 720.0f); request.viewportPosition = Vector2(640.0f, 360.0f); const auto result = PickSceneViewportEntity(request); EXPECT_TRUE(result.hit); EXPECT_EQ(result.entityId, object->GetID()); } TEST_F(SceneViewportPickerTest, PickSceneViewportEntityReturnsNoHitForEmptySpace) { Scene scene("PickerScene"); CreateTriangleObject(scene, "Offset", Vector3(3.0f, 0.0f, 4.0f)); SceneViewportPickRequest request = {}; request.scene = &scene; request.overlay = CreateDefaultOverlay(); request.viewportSize = Vector2(1280.0f, 720.0f); request.viewportPosition = Vector2(640.0f, 360.0f); const auto result = PickSceneViewportEntity(request); EXPECT_FALSE(result.hit); EXPECT_EQ(result.entityId, 0u); } } // namespace