Fix scene move gizmo axis projection
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
#include "SceneViewportPicker.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -31,17 +34,21 @@ Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
|
||||
Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportGizmoAxis::X:
|
||||
return Math::Color(0.937f, 0.325f, 0.314f, 1.0f);
|
||||
return Math::Color(0.91f, 0.09f, 0.05f, 1.0f);
|
||||
case SceneViewportGizmoAxis::Y:
|
||||
return Math::Color(0.400f, 0.733f, 0.416f, 1.0f);
|
||||
return Math::Color(0.45f, 1.0f, 0.12f, 1.0f);
|
||||
case SceneViewportGizmoAxis::Z:
|
||||
return Math::Color(0.259f, 0.647f, 0.961f, 1.0f);
|
||||
return Math::Color(0.11f, 0.29f, 1.0f, 1.0f);
|
||||
case SceneViewportGizmoAxis::None:
|
||||
default:
|
||||
return Math::Color::White();
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
|
||||
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
|
||||
}
|
||||
|
||||
bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) {
|
||||
return context.mousePosition.x >= 0.0f &&
|
||||
context.mousePosition.y >= 0.0f &&
|
||||
@@ -49,6 +56,87 @@ bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) {
|
||||
context.mousePosition.y <= context.viewportSize.y;
|
||||
}
|
||||
|
||||
void EncapsulateTransformedBounds(
|
||||
const Math::Bounds& localBounds,
|
||||
const Components::TransformComponent& transform,
|
||||
Math::Bounds& inOutBounds,
|
||||
bool& inOutHasBounds) {
|
||||
const Math::Vector3 localMin = localBounds.GetMin();
|
||||
const Math::Vector3 localMax = localBounds.GetMax();
|
||||
const Math::Vector3 corners[] = {
|
||||
Math::Vector3(localMin.x, localMin.y, localMin.z),
|
||||
Math::Vector3(localMax.x, localMin.y, localMin.z),
|
||||
Math::Vector3(localMin.x, localMax.y, localMin.z),
|
||||
Math::Vector3(localMin.x, localMin.y, localMax.z),
|
||||
Math::Vector3(localMax.x, localMax.y, localMin.z),
|
||||
Math::Vector3(localMin.x, localMax.y, localMax.z),
|
||||
Math::Vector3(localMax.x, localMin.y, localMax.z),
|
||||
Math::Vector3(localMax.x, localMax.y, localMax.z)
|
||||
};
|
||||
|
||||
for (const Math::Vector3& localCorner : corners) {
|
||||
const Math::Vector3 worldCorner = transform.TransformPoint(localCorner);
|
||||
if (!inOutHasBounds) {
|
||||
inOutBounds = Math::Bounds(worldCorner, Math::Vector3::Zero());
|
||||
inOutHasBounds = true;
|
||||
} else {
|
||||
inOutBounds.Encapsulate(worldCorner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CollectRenderableWorldBoundsRecursive(
|
||||
const Components::GameObject& gameObject,
|
||||
Math::Bounds& inOutBounds,
|
||||
bool& inOutHasBounds) {
|
||||
if (!gameObject.IsActiveInHierarchy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* meshFilter = gameObject.GetComponent<Components::MeshFilterComponent>();
|
||||
const auto* meshRenderer = gameObject.GetComponent<Components::MeshRendererComponent>();
|
||||
if (meshFilter != nullptr &&
|
||||
meshRenderer != nullptr &&
|
||||
meshFilter->IsEnabled() &&
|
||||
meshRenderer->IsEnabled()) {
|
||||
const auto* mesh = meshFilter->GetMesh();
|
||||
if (mesh != nullptr) {
|
||||
EncapsulateTransformedBounds(mesh->GetBounds(), *gameObject.GetTransform(), inOutBounds, inOutHasBounds);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t childIndex = 0; childIndex < gameObject.GetChildCount(); ++childIndex) {
|
||||
const Components::GameObject* child = gameObject.GetChild(childIndex);
|
||||
if (child != nullptr) {
|
||||
CollectRenderableWorldBoundsRecursive(*child, inOutBounds, inOutHasBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float ComputeWorldUnitsPerPixel(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector3& worldPoint,
|
||||
float viewportHeight) {
|
||||
if (!overlay.valid || viewportHeight <= 1.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const Math::Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
|
||||
const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward);
|
||||
if (depth <= Math::EPSILON) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight;
|
||||
}
|
||||
|
||||
Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) {
|
||||
Math::Bounds worldBounds = {};
|
||||
bool hasBounds = false;
|
||||
CollectRenderableWorldBoundsRecursive(gameObject, worldBounds, hasBounds);
|
||||
return hasBounds ? worldBounds.center : gameObject.GetTransform()->GetPosition();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
|
||||
@@ -88,8 +176,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 worldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(worldPosition, dragPlaneNormal);
|
||||
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject);
|
||||
const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, dragPlaneNormal);
|
||||
float hitDistance = 0.0f;
|
||||
if (!worldRay.Intersects(dragPlane, hitDistance)) {
|
||||
return false;
|
||||
@@ -105,8 +194,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
m_activeEntityId = context.selectedObject->GetID();
|
||||
m_activeAxisDirection = worldAxis;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartWorldPosition = worldPosition;
|
||||
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - worldPosition, worldAxis);
|
||||
m_dragStartObjectWorldPosition = objectWorldPosition;
|
||||
m_dragStartPivotWorldPosition = pivotWorldPosition;
|
||||
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis);
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
@@ -133,10 +223,10 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con
|
||||
}
|
||||
|
||||
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
|
||||
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartWorldPosition, m_activeAxisDirection);
|
||||
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
|
||||
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
|
||||
context.selectedObject->GetTransform()->SetPosition(
|
||||
m_dragStartWorldPosition + m_activeAxisDirection * deltaScalar);
|
||||
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
|
||||
}
|
||||
|
||||
void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
@@ -151,7 +241,8 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
m_activeAxis = SceneViewportGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_activeAxisDirection = Math::Vector3::Zero();
|
||||
m_dragStartWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartAxisScalar = 0.0f;
|
||||
RefreshHandleState();
|
||||
}
|
||||
@@ -164,7 +255,8 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
m_activeAxis = SceneViewportGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_activeAxisDirection = Math::Vector3::Zero();
|
||||
m_dragStartWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartAxisScalar = 0.0f;
|
||||
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
RefreshHandleState();
|
||||
@@ -198,17 +290,28 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 gizmoWorldOrigin = GetGizmoWorldOrigin(*selectedObject);
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
context.viewportSize.y,
|
||||
selectedObject->GetTransform()->GetPosition());
|
||||
gizmoWorldOrigin);
|
||||
if (!projectedPivot.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_drawData.visible = true;
|
||||
m_drawData.pivot = projectedPivot.screenPosition;
|
||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(
|
||||
context.overlay,
|
||||
gizmoWorldOrigin,
|
||||
context.viewportSize.y);
|
||||
if (worldUnitsPerPixel <= Math::EPSILON) {
|
||||
m_drawData = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const float axisLengthWorld = worldUnitsPerPixel * kMoveGizmoHandleLengthPixels;
|
||||
|
||||
const SceneViewportGizmoAxis axes[] = {
|
||||
SceneViewportGizmoAxis::X,
|
||||
@@ -220,12 +323,22 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
handle.axis = axes[index];
|
||||
handle.start = projectedPivot.screenPosition;
|
||||
|
||||
Math::Vector2 screenDirection = Math::Vector2::Zero();
|
||||
if (!ProjectSceneViewportAxisDirection(context.overlay, GetAxisVector(handle.axis), screenDirection)) {
|
||||
const Math::Vector3 axisEndWorld =
|
||||
gizmoWorldOrigin + GetAxisVector(handle.axis) * axisLengthWorld;
|
||||
const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
context.viewportSize.y,
|
||||
axisEndWorld);
|
||||
if (projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
handle.end = handle.start + screenDirection * kMoveGizmoHandleLengthPixels;
|
||||
if ((projectedEnd.screenPosition - projectedPivot.screenPosition).SqrMagnitude() <= Math::EPSILON) {
|
||||
continue;
|
||||
}
|
||||
|
||||
handle.end = projectedEnd.screenPosition;
|
||||
handle.visible = true;
|
||||
handle.color = GetAxisBaseColor(handle.axis);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
float HandleLength(const SceneViewportMoveGizmoHandleDrawData& handle) {
|
||||
return (handle.end - handle.start).Magnitude();
|
||||
}
|
||||
|
||||
class SceneViewportMoveGizmoTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
@@ -26,6 +30,19 @@ protected:
|
||||
return overlay;
|
||||
}
|
||||
|
||||
static SceneViewportOverlayData MakeDownwardTiltedOverlay() {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
overlay.valid = true;
|
||||
overlay.cameraPosition = Math::Vector3(0.0f, 10.0f, -1.0f);
|
||||
overlay.cameraForward = (Math::Vector3::Zero() - overlay.cameraPosition).Normalized();
|
||||
overlay.cameraRight = Math::Vector3::Cross(Math::Vector3::Up(), overlay.cameraForward).Normalized();
|
||||
overlay.cameraUp = Math::Vector3::Cross(overlay.cameraForward, overlay.cameraRight).Normalized();
|
||||
overlay.verticalFovDegrees = 60.0f;
|
||||
overlay.nearClipPlane = 0.03f;
|
||||
overlay.farClipPlane = 2000.0f;
|
||||
return overlay;
|
||||
}
|
||||
|
||||
static SceneViewportMoveGizmoContext MakeContext(
|
||||
Components::GameObject* selectedObject,
|
||||
const Math::Vector2& mousePosition) {
|
||||
@@ -37,6 +54,18 @@ protected:
|
||||
return context;
|
||||
}
|
||||
|
||||
static SceneViewportMoveGizmoContext MakeContext(
|
||||
Components::GameObject* selectedObject,
|
||||
const Math::Vector2& mousePosition,
|
||||
const SceneViewportOverlayData& overlay) {
|
||||
SceneViewportMoveGizmoContext context = {};
|
||||
context.overlay = overlay;
|
||||
context.viewportSize = Math::Vector2(800.0f, 600.0f);
|
||||
context.mousePosition = mousePosition;
|
||||
context.selectedObject = selectedObject;
|
||||
return context;
|
||||
}
|
||||
|
||||
SceneManager& GetSceneManager() {
|
||||
return dynamic_cast<SceneManager&>(m_context.GetSceneManager());
|
||||
}
|
||||
@@ -92,5 +121,23 @@ TEST_F(SceneViewportMoveGizmoTest, DraggingXAxisOnlyChangesWorldXAndCreatesUndoS
|
||||
EXPECT_NEAR(restoredPosition.z, 0.0f, 1e-4f);
|
||||
}
|
||||
|
||||
TEST_F(SceneViewportMoveGizmoTest, AxisNearlyFacingCameraShrinksInsteadOfKeepingFullLength) {
|
||||
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
|
||||
SceneViewportMoveGizmo gizmo;
|
||||
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), MakeDownwardTiltedOverlay()));
|
||||
|
||||
ASSERT_TRUE(gizmo.GetDrawData().visible);
|
||||
const float xLength = HandleLength(gizmo.GetDrawData().handles[0]);
|
||||
const float yLength = HandleLength(gizmo.GetDrawData().handles[1]);
|
||||
const float zLength = HandleLength(gizmo.GetDrawData().handles[2]);
|
||||
|
||||
EXPECT_GT(xLength, 60.0f);
|
||||
EXPECT_GT(zLength, 60.0f);
|
||||
EXPECT_LT(yLength, xLength * 0.5f);
|
||||
EXPECT_LT(yLength, zLength * 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
|
||||
Reference in New Issue
Block a user