refactor: align scene view camera controls with unity
This commit is contained in:
@@ -35,6 +35,7 @@ struct SceneViewportInput {
|
||||
float mouseWheel = 0.0f;
|
||||
bool hovered = false;
|
||||
bool focused = false;
|
||||
bool looking = false;
|
||||
bool orbiting = false;
|
||||
bool panning = false;
|
||||
bool focusSelectionRequested = false;
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
struct SceneViewportCameraInputState {
|
||||
float lookDeltaX = 0.0f;
|
||||
float lookDeltaY = 0.0f;
|
||||
float orbitDeltaX = 0.0f;
|
||||
float orbitDeltaY = 0.0f;
|
||||
float panDeltaX = 0.0f;
|
||||
@@ -27,6 +29,7 @@ public:
|
||||
m_distance = 6.0f;
|
||||
m_yawDegrees = -35.0f;
|
||||
m_pitchDegrees = -20.0f;
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
const Math::Vector3& GetFocalPoint() const {
|
||||
@@ -56,11 +59,12 @@ public:
|
||||
}
|
||||
|
||||
Math::Vector3 GetPosition() const {
|
||||
return m_focalPoint - GetForward() * m_distance;
|
||||
return m_position;
|
||||
}
|
||||
|
||||
void Focus(const Math::Vector3& point) {
|
||||
m_focalPoint = point;
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
void ApplyInput(const SceneViewportCameraInputState& input) {
|
||||
@@ -68,28 +72,31 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::abs(input.zoomDelta) > Math::EPSILON) {
|
||||
const float zoomFactor = std::pow(0.85f, input.zoomDelta);
|
||||
m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f);
|
||||
if (std::abs(input.lookDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.lookDeltaY) > Math::EPSILON) {
|
||||
ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY);
|
||||
UpdateFocalPointFromPosition();
|
||||
}
|
||||
|
||||
if (std::abs(input.orbitDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.orbitDeltaY) > Math::EPSILON) {
|
||||
m_yawDegrees += input.orbitDeltaX * 0.30f;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - input.orbitDeltaY * 0.20f, -89.0f, 89.0f);
|
||||
ApplyRotationDelta(input.orbitDeltaX, input.orbitDeltaY);
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
if (std::abs(input.panDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.panDeltaY) > Math::EPSILON) {
|
||||
const Math::Vector3 forward = GetForward();
|
||||
const Math::Vector3 right = Math::Vector3::Normalize(
|
||||
Math::Vector3::Cross(Math::Vector3::Up(), forward));
|
||||
const Math::Vector3 up = Math::Vector3::Normalize(
|
||||
Math::Vector3::Cross(forward, right));
|
||||
|
||||
const float worldUnitsPerPixel =
|
||||
2.0f * m_distance * std::tan(60.0f * Math::DEG_TO_RAD * 0.5f) / input.viewportHeight;
|
||||
const Math::Vector3 right = GetRight();
|
||||
const Math::Vector3 up = GetUp();
|
||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight);
|
||||
m_focalPoint += ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel;
|
||||
m_position += ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel;
|
||||
}
|
||||
|
||||
if (std::abs(input.zoomDelta) > Math::EPSILON) {
|
||||
const float zoomFactor = std::pow(0.85f, input.zoomDelta);
|
||||
m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f);
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +109,37 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void ApplyRotationDelta(float deltaX, float deltaY) {
|
||||
m_yawDegrees += deltaX * 0.30f;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f);
|
||||
}
|
||||
|
||||
Math::Vector3 GetRight() const {
|
||||
const Math::Vector3 right = Math::Vector3::Cross(Math::Vector3::Up(), GetForward());
|
||||
if (right.SqrMagnitude() <= Math::EPSILON) {
|
||||
return Math::Vector3::Right();
|
||||
}
|
||||
return Math::Vector3::Normalize(right);
|
||||
}
|
||||
|
||||
Math::Vector3 GetUp() const {
|
||||
return Math::Vector3::Normalize(Math::Vector3::Cross(GetForward(), GetRight()));
|
||||
}
|
||||
|
||||
float ComputeWorldUnitsPerPixel(float viewportHeight) const {
|
||||
return 2.0f * m_distance * std::tan(60.0f * Math::DEG_TO_RAD * 0.5f) / viewportHeight;
|
||||
}
|
||||
|
||||
void UpdatePositionFromFocalPoint() {
|
||||
m_position = m_focalPoint - GetForward() * m_distance;
|
||||
}
|
||||
|
||||
void UpdateFocalPointFromPosition() {
|
||||
m_focalPoint = m_position + GetForward() * m_distance;
|
||||
}
|
||||
|
||||
private:
|
||||
Math::Vector3 m_position = Math::Vector3::Zero();
|
||||
Math::Vector3 m_focalPoint = Math::Vector3::Zero();
|
||||
float m_distance = 6.0f;
|
||||
float m_yawDegrees = -35.0f;
|
||||
|
||||
@@ -92,6 +92,11 @@ public:
|
||||
controllerInput.viewportHeight = input.viewportSize.y;
|
||||
controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f;
|
||||
|
||||
if (input.looking) {
|
||||
controllerInput.lookDeltaX = input.mouseDelta.x;
|
||||
controllerInput.lookDeltaY = input.mouseDelta.y;
|
||||
}
|
||||
|
||||
if (input.orbiting) {
|
||||
controllerInput.orbitDeltaX = input.mouseDelta.x;
|
||||
controllerInput.orbitDeltaY = input.mouseDelta.y;
|
||||
|
||||
@@ -239,14 +239,22 @@ void SceneViewPanel::Render() {
|
||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
|
||||
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
if (!m_orbitDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
const bool altDown = io.KeyAlt;
|
||||
|
||||
if (!m_lookDragging && content.hovered && !altDown && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
m_lookDragging = true;
|
||||
}
|
||||
if (!m_orbitDragging && content.hovered && altDown && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
m_orbitDragging = true;
|
||||
}
|
||||
if (!m_panDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
|
||||
m_panDragging = true;
|
||||
}
|
||||
|
||||
if (m_orbitDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||
if (m_lookDragging && (!ImGui::IsMouseDown(ImGuiMouseButton_Right) || altDown)) {
|
||||
m_lookDragging = false;
|
||||
}
|
||||
if (m_orbitDragging && (!ImGui::IsMouseDown(ImGuiMouseButton_Left) || !altDown)) {
|
||||
m_orbitDragging = false;
|
||||
}
|
||||
if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
|
||||
@@ -258,12 +266,13 @@ void SceneViewPanel::Render() {
|
||||
input.hovered = content.hovered;
|
||||
input.focused = content.focused;
|
||||
input.mouseWheel = content.hovered ? io.MouseWheel : 0.0f;
|
||||
input.looking = m_lookDragging;
|
||||
input.orbiting = m_orbitDragging;
|
||||
input.panning = m_panDragging;
|
||||
input.focusSelectionRequested =
|
||||
content.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false);
|
||||
|
||||
if (m_orbitDragging || m_panDragging) {
|
||||
if (m_lookDragging || m_orbitDragging || m_panDragging) {
|
||||
input.mouseDelta = io.MouseDelta;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public:
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
bool m_lookDragging = false;
|
||||
bool m_orbitDragging = false;
|
||||
bool m_panDragging = false;
|
||||
};
|
||||
|
||||
@@ -41,28 +41,68 @@ TEST(SceneViewportCameraController_Test, ApplyToMatchesComputedPositionAndForwar
|
||||
EXPECT_GT(Vector3::Dot(cameraObject.GetTransform()->GetUp().Normalized(), Vector3::Up()), 0.0f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, ApplyInputUpdatesOrbitPanAndZoomState) {
|
||||
TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDistance) {
|
||||
SceneViewportCameraController controller;
|
||||
controller.Reset();
|
||||
|
||||
const float initialDistance = controller.GetDistance();
|
||||
const float initialYaw = controller.GetYawDegrees();
|
||||
const float initialPitch = controller.GetPitchDegrees();
|
||||
const Vector3 initialPosition = controller.GetPosition();
|
||||
const Vector3 initialFocus = controller.GetFocalPoint();
|
||||
|
||||
SceneViewportCameraInputState input = {};
|
||||
input.viewportHeight = 720.0f;
|
||||
input.zoomDelta = 1.0f;
|
||||
input.orbitDeltaX = 20.0f;
|
||||
input.orbitDeltaY = -15.0f;
|
||||
input.panDeltaX = 16.0f;
|
||||
input.panDeltaY = -10.0f;
|
||||
input.lookDeltaX = 20.0f;
|
||||
input.lookDeltaY = -15.0f;
|
||||
controller.ApplyInput(input);
|
||||
|
||||
EXPECT_LT(controller.GetDistance(), initialDistance);
|
||||
EXPECT_NE(controller.GetYawDegrees(), initialYaw);
|
||||
EXPECT_NE(controller.GetPitchDegrees(), initialPitch);
|
||||
EXPECT_TRUE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||
EXPECT_TRUE(NearlyEqual(
|
||||
controller.GetFocalPoint(),
|
||||
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
||||
1e-3f));
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, OrbitInputRotatesAroundFocalPointAndKeepsDistance) {
|
||||
SceneViewportCameraController controller;
|
||||
controller.Reset();
|
||||
|
||||
const Vector3 initialPosition = controller.GetPosition();
|
||||
const Vector3 initialFocus = controller.GetFocalPoint();
|
||||
const float initialDistance = controller.GetDistance();
|
||||
|
||||
SceneViewportCameraInputState input = {};
|
||||
input.viewportHeight = 720.0f;
|
||||
input.orbitDeltaX = 20.0f;
|
||||
input.orbitDeltaY = -15.0f;
|
||||
controller.ApplyInput(input);
|
||||
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||
EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||
EXPECT_NEAR((controller.GetFocalPoint() - controller.GetPosition()).Magnitude(), initialDistance, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, PanAndZoomUpdateCameraStateConsistently) {
|
||||
SceneViewportCameraController controller;
|
||||
controller.Reset();
|
||||
|
||||
const Vector3 initialPosition = controller.GetPosition();
|
||||
const Vector3 initialFocus = controller.GetFocalPoint();
|
||||
const float initialDistance = controller.GetDistance();
|
||||
|
||||
SceneViewportCameraInputState input = {};
|
||||
input.viewportHeight = 720.0f;
|
||||
input.panDeltaX = 16.0f;
|
||||
input.panDeltaY = -10.0f;
|
||||
input.zoomDelta = 1.0f;
|
||||
controller.ApplyInput(input);
|
||||
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||
EXPECT_LT(controller.GetDistance(), initialDistance);
|
||||
EXPECT_TRUE(NearlyEqual(
|
||||
controller.GetFocalPoint(),
|
||||
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
||||
1e-3f));
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, FocusMovesPivotWithoutChangingDistance) {
|
||||
|
||||
Reference in New Issue
Block a user