#include #include "Viewport/SceneViewportCameraController.h" #include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportMath.h" #include #include #include #include namespace { bool NearlyEqual(float lhs, float rhs, float epsilon = 1e-4f) { return std::abs(lhs - rhs) <= epsilon; } bool NearlyEqual(const XCEngine::Math::Vector3& lhs, const XCEngine::Math::Vector3& rhs, float epsilon = 1e-4f) { return NearlyEqual(lhs.x, rhs.x, epsilon) && NearlyEqual(lhs.y, rhs.y, epsilon) && NearlyEqual(lhs.z, rhs.z, epsilon); } bool NearlyEqual( const XCEngine::Math::Matrix4x4& lhs, const XCEngine::Math::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; } bool IsPowerOfTenSpacing(float value) { if (value <= 0.0f) { return false; } const float exponent = std::floor(std::log10(value)); const float base = std::pow(10.0f, exponent); const float normalized = value / base; return NearlyEqual(normalized, 1.0f, 1e-4f); } } // namespace XCEngine::Rendering::Passes::InfiniteGridPassData ToInfiniteGridPassData( const XCEngine::Editor::SceneViewportOverlayData& overlay) { XCEngine::Rendering::Passes::InfiniteGridPassData data = {}; data.valid = overlay.valid; data.cameraPosition = overlay.cameraPosition; data.cameraForward = overlay.cameraForward; data.cameraRight = overlay.cameraRight; data.cameraUp = overlay.cameraUp; data.verticalFovDegrees = overlay.verticalFovDegrees; data.nearClipPlane = overlay.nearClipPlane; data.farClipPlane = overlay.farClipPlane; data.orbitDistance = overlay.orbitDistance; return data; } using XCEngine::Editor::BuildSceneViewportAxisDragPlaneNormal; using XCEngine::Editor::SceneViewportCameraController; using XCEngine::Editor::SceneViewportHudOverlayHitKind; using XCEngine::Editor::SceneViewportHudOverlayHitResult; using XCEngine::Editor::BuildSceneViewportProjectionMatrix; using XCEngine::Editor::BuildSceneViewportHudOverlayData; using XCEngine::Editor::BuildSceneViewportViewMatrix; using XCEngine::Editor::HitTestSceneViewportHudOverlay; using XCEngine::Editor::ProjectSceneViewportWorldPoint; using XCEngine::Editor::SceneViewportOverlayFrameData; using XCEngine::Editor::SceneViewportOverlaySpritePrimitive; using XCEngine::Editor::SceneViewportOverlaySpriteTextureKind; using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Components::GameObject; using XCEngine::Math::Vector3; using XCEngine::Math::Vector4; using XCEngine::Rendering::Passes::BuildInfiniteGridParameters; using XCEngine::Rendering::Passes::InfiniteGridParameters; TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersUsesPowerOfTenSpacingSeries) { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3(0.0f, 6.0f, -6.0f); overlay.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); const InfiniteGridParameters parameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(overlay)); EXPECT_TRUE(parameters.valid); EXPECT_TRUE(IsPowerOfTenSpacing(parameters.baseScale)); EXPECT_GE(parameters.transitionBlend, 0.0f); EXPECT_LE(parameters.transitionBlend, 1.0f); EXPECT_GT(parameters.fadeDistance, parameters.baseScale); } TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersIsStableAcrossHorizontalCameraMovement) { SceneViewportOverlayData left = {}; left.valid = true; left.cameraPosition = Vector3(-120.0f, 12.0f, 40.0f); left.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); SceneViewportOverlayData right = left; right.cameraPosition = Vector3(380.0f, 12.0f, -260.0f); const InfiniteGridParameters leftParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(left)); const InfiniteGridParameters rightParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(right)); EXPECT_TRUE(leftParameters.valid); EXPECT_TRUE(rightParameters.valid); EXPECT_FLOAT_EQ(leftParameters.baseScale, rightParameters.baseScale); EXPECT_FLOAT_EQ(leftParameters.transitionBlend, rightParameters.transitionBlend); EXPECT_FLOAT_EQ(leftParameters.fadeDistance, rightParameters.fadeDistance); } TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersExpandsScaleAsCameraHeightGrows) { SceneViewportOverlayData nearOverlay = {}; nearOverlay.valid = true; nearOverlay.cameraPosition = Vector3(0.0f, 3.0f, -4.0f); nearOverlay.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); SceneViewportOverlayData farOverlay = nearOverlay; farOverlay.cameraPosition = Vector3(0.0f, 120.0f, -150.0f); const InfiniteGridParameters nearParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(nearOverlay)); const InfiniteGridParameters farParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(farOverlay)); EXPECT_TRUE(nearParameters.valid); EXPECT_TRUE(farParameters.valid); EXPECT_GE(farParameters.baseScale, nearParameters.baseScale); EXPECT_GE(farParameters.fadeDistance, nearParameters.fadeDistance); } TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersPromotesMajorLinesByDecimalGrouping) { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3(0.0f, 14.0f, -18.0f); overlay.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); const InfiniteGridParameters parameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(overlay)); EXPECT_TRUE(parameters.valid); EXPECT_TRUE(IsPowerOfTenSpacing(parameters.baseScale)); EXPECT_TRUE(IsPowerOfTenSpacing(parameters.baseScale * 10.0f)); } TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersFadesTowardNextScaleBeforeThreshold) { SceneViewportOverlayData earlyOverlay = {}; earlyOverlay.valid = true; earlyOverlay.cameraPosition = Vector3(0.0f, 4.0f, -6.0f); SceneViewportOverlayData lateOverlay = earlyOverlay; lateOverlay.cameraPosition = Vector3(0.0f, 18.0f, -6.0f); const InfiniteGridParameters earlyParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(earlyOverlay)); const InfiniteGridParameters lateParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(lateOverlay)); EXPECT_FLOAT_EQ(earlyParameters.baseScale, 1.0f); EXPECT_FLOAT_EQ(lateParameters.baseScale, 1.0f); EXPECT_LT(earlyParameters.transitionBlend, 0.05f); EXPECT_GT(lateParameters.transitionBlend, 0.90f); } TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersSwitchesScaleAtHalfThePreviousDistanceThreshold) { SceneViewportOverlayData nearOverlay = {}; nearOverlay.valid = true; nearOverlay.cameraPosition = Vector3(0.0f, 19.9f, -6.0f); SceneViewportOverlayData farOverlay = nearOverlay; farOverlay.cameraPosition = Vector3(0.0f, 20.0f, -6.0f); const InfiniteGridParameters nearParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(nearOverlay)); const InfiniteGridParameters farParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(farOverlay)); EXPECT_FLOAT_EQ(nearParameters.baseScale, 1.0f); EXPECT_FLOAT_EQ(farParameters.baseScale, 10.0f); EXPECT_GT(nearParameters.transitionBlend, 0.95f); EXPECT_LT(farParameters.transitionBlend, 0.05f); } TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersIgnoresStaleOrbitDistanceForSameView) { SceneViewportOverlayData nearOrbit = {}; nearOrbit.valid = true; nearOrbit.cameraPosition = Vector3(0.0f, 12.0f, -24.0f); nearOrbit.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); nearOrbit.orbitDistance = 2.0f; SceneViewportOverlayData farOrbit = nearOrbit; farOrbit.orbitDistance = 200.0f; const InfiniteGridParameters nearOrbitParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(nearOrbit)); const InfiniteGridParameters farOrbitParameters = BuildInfiniteGridParameters(ToInfiniteGridPassData(farOrbit)); EXPECT_TRUE(nearOrbitParameters.valid); EXPECT_TRUE(farOrbitParameters.valid); EXPECT_FLOAT_EQ(nearOrbitParameters.baseScale, farOrbitParameters.baseScale); EXPECT_FLOAT_EQ(nearOrbitParameters.transitionBlend, farOrbitParameters.transitionBlend); EXPECT_FLOAT_EQ(nearOrbitParameters.fadeDistance, farOrbitParameters.fadeDistance); } TEST(SceneViewportOverlayRenderer_Test, ViewMatrixKeepsForwardWorldPointsInFrontOfCamera) { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3(0.0f, 0.0f, 0.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; const auto view = BuildSceneViewportViewMatrix(overlay); const Vector3 pointInView = view.MultiplyPoint(Vector3(0.0f, 0.0f, 5.0f)); EXPECT_TRUE(NearlyEqual(pointInView, Vector3(0.0f, 0.0f, 5.0f), 1e-4f)); const auto projection = BuildSceneViewportProjectionMatrix(overlay, 1280.0f, 720.0f); const Vector4 clipPoint = projection * Vector4(Vector3(0.0f, 0.0f, 5.0f), 1.0f); EXPECT_GT(clipPoint.w, 0.0f); } TEST(SceneViewportOverlayRenderer_Test, ViewMatrixMatchesSceneCameraTransformConvention) { SceneViewportCameraController controller; controller.Reset(); controller.Focus(Vector3(2.0f, 1.5f, -3.0f)); GameObject cameraObject("EditorCamera"); controller.ApplyTo(*cameraObject.GetTransform()); SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = cameraObject.GetTransform()->GetPosition(); overlay.cameraForward = cameraObject.GetTransform()->GetForward(); overlay.cameraRight = cameraObject.GetTransform()->GetRight(); overlay.cameraUp = cameraObject.GetTransform()->GetUp(); overlay.verticalFovDegrees = 60.0f; overlay.nearClipPlane = 0.03f; overlay.farClipPlane = 2000.0f; EXPECT_TRUE(NearlyEqual( BuildSceneViewportViewMatrix(overlay), cameraObject.GetTransform()->GetWorldToLocalMatrix(), 1e-3f)); } TEST(SceneViewportOverlayRenderer_Test, ProjectSceneViewportWorldPointMapsOriginToViewportCenter) { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3(0.0f, 0.0f, -5.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; const auto projected = ProjectSceneViewportWorldPoint(overlay, 800.0f, 600.0f, Vector3::Zero()); EXPECT_TRUE(projected.visible); EXPECT_NEAR(projected.screenPosition.x, 400.0f, 1e-3f); EXPECT_NEAR(projected.screenPosition.y, 300.0f, 1e-3f); } TEST(SceneViewportOverlayRenderer_Test, BuildSceneViewportAxisDragPlaneNormalFallsBackWhenForwardAlignsWithAxis) { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraForward = Vector3::Right(); overlay.cameraRight = Vector3::Back(); overlay.cameraUp = Vector3::Up(); Vector3 planeNormal = Vector3::Zero(); ASSERT_TRUE(BuildSceneViewportAxisDragPlaneNormal(overlay, Vector3::Right(), planeNormal)); EXPECT_NEAR(Vector3::Dot(planeNormal, Vector3::Right()), 0.0f, 1e-4f); EXPECT_GT(planeNormal.SqrMagnitude(), 0.5f); } TEST(SceneViewportOverlayRenderer_Test, BuildSceneViewportHudOverlayDataTracksVisibilityIntent) { SceneViewportOverlayData overlay = {}; overlay.valid = true; const auto visibleHud = BuildSceneViewportHudOverlayData(overlay); const auto hiddenHud = BuildSceneViewportHudOverlayData(overlay, false); EXPECT_TRUE(visibleHud.HasVisibleElements()); EXPECT_FALSE(hiddenHud.HasVisibleElements()); } TEST(SceneViewportOverlayRenderer_Test, BuildSceneViewportHudOverlayDataCanExposeSceneIconsWithoutOrientationGizmo) { SceneViewportOverlayData overlay = {}; overlay.valid = true; SceneViewportOverlayFrameData frameData = {}; frameData.overlay = overlay; SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back(); sprite.worldPosition = Vector3::Zero(); sprite.sizePixels = XCEngine::Math::Vector2(32.0f, 32.0f); sprite.textureKind = SceneViewportOverlaySpriteTextureKind::Camera; const auto iconsOnlyHud = BuildSceneViewportHudOverlayData( overlay, false, &frameData, true); EXPECT_TRUE(iconsOnlyHud.HasVisibleElements()); } TEST(SceneViewportOverlayRenderer_Test, HitTestSceneViewportHudOverlaySkipsInvalidOrHiddenOverlay) { const SceneViewportHudOverlayHitResult invalidHit = HitTestSceneViewportHudOverlay({}, ImVec2(0.0f, 0.0f), ImVec2(200.0f, 200.0f), ImVec2(100.0f, 100.0f)); EXPECT_EQ(invalidHit.kind, SceneViewportHudOverlayHitKind::None); SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = Vector3(0.0f, 0.0f, -5.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; const SceneViewportHudOverlayHitResult hiddenHit = HitTestSceneViewportHudOverlay( BuildSceneViewportHudOverlayData(overlay, false), ImVec2(0.0f, 0.0f), ImVec2(200.0f, 200.0f), ImVec2(100.0f, 100.0f)); EXPECT_EQ(hiddenHit.kind, SceneViewportHudOverlayHitKind::None); }