#pragma once #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { struct SceneViewportCameraInputState { float lookDeltaX = 0.0f; float lookDeltaY = 0.0f; float orbitDeltaX = 0.0f; float orbitDeltaY = 0.0f; float panDeltaX = 0.0f; float panDeltaY = 0.0f; float zoomDelta = 0.0f; float flySpeedDelta = 0.0f; float deltaTime = 0.0f; float moveForward = 0.0f; float moveRight = 0.0f; float moveUp = 0.0f; float viewportHeight = 0.0f; bool fastMove = false; }; class SceneViewportCameraController { public: void Reset() { m_focalPoint = Math::Vector3::Zero(); m_distance = 6.0f; m_flySpeed = 5.0f; m_yawDegrees = -35.0f; m_pitchDegrees = -20.0f; m_snapAnimating = false; UpdatePositionFromFocalPoint(); } const Math::Vector3& GetFocalPoint() const { return m_focalPoint; } float GetDistance() const { return m_distance; } float GetFlySpeed() const { return m_flySpeed; } float GetYawDegrees() const { return m_yawDegrees; } float GetPitchDegrees() const { return m_pitchDegrees; } Math::Vector3 GetForward() const { const float yawRadians = m_yawDegrees * Math::DEG_TO_RAD; const float pitchRadians = m_pitchDegrees * Math::DEG_TO_RAD; return Math::Vector3::Normalize(Math::Vector3( std::cos(pitchRadians) * std::sin(yawRadians), std::sin(pitchRadians), std::cos(pitchRadians) * std::cos(yawRadians))); } Math::Vector3 GetPosition() const { return m_position; } void Focus(const Math::Vector3& point) { m_focalPoint = point; UpdatePositionFromFocalPoint(); } void SnapToForward(const Math::Vector3& forward) { if (forward.SqrMagnitude() <= Math::EPSILON) { return; } const OrientationAngles target = ComputeOrientationAngles(forward); m_yawDegrees = target.yawDegrees; m_pitchDegrees = target.pitchDegrees; m_snapAnimating = false; UpdatePositionFromFocalPoint(); } void AnimateToForward(const Math::Vector3& forward) { if (forward.SqrMagnitude() <= Math::EPSILON) { return; } const OrientationAngles target = ComputeOrientationAngles(forward); m_snapStartYawDegrees = m_yawDegrees; m_snapStartPitchDegrees = m_pitchDegrees; m_snapTargetYawDegrees = target.yawDegrees; m_snapTargetPitchDegrees = target.pitchDegrees; m_snapElapsed = 0.0f; m_snapAnimating = true; } void ApplyInput(const SceneViewportCameraInputState& input) { if (input.viewportHeight <= 0.0f) { return; } const bool hasManualInput = std::abs(input.lookDeltaX) > Math::EPSILON || std::abs(input.lookDeltaY) > Math::EPSILON || std::abs(input.orbitDeltaX) > Math::EPSILON || std::abs(input.orbitDeltaY) > Math::EPSILON || std::abs(input.panDeltaX) > Math::EPSILON || std::abs(input.panDeltaY) > Math::EPSILON || std::abs(input.zoomDelta) > Math::EPSILON || std::abs(input.flySpeedDelta) > Math::EPSILON || std::abs(input.moveForward) > Math::EPSILON || std::abs(input.moveRight) > Math::EPSILON || std::abs(input.moveUp) > Math::EPSILON; if (hasManualInput) { m_snapAnimating = false; } if (std::abs(input.lookDeltaX) > Math::EPSILON || std::abs(input.lookDeltaY) > Math::EPSILON) { ApplyRotationDelta( input.lookDeltaX, input.lookDeltaY, kLookYawSensitivity, kLookPitchSensitivity); UpdateFocalPointFromPosition(); } if (std::abs(input.orbitDeltaX) > Math::EPSILON || std::abs(input.orbitDeltaY) > Math::EPSILON) { ApplyRotationDelta( input.orbitDeltaX, input.orbitDeltaY, kOrbitYawSensitivity, kOrbitPitchSensitivity); UpdatePositionFromFocalPoint(); } if (std::abs(input.panDeltaX) > Math::EPSILON || std::abs(input.panDeltaY) > Math::EPSILON) { const Math::Vector3 right = GetRight(); const Math::Vector3 up = GetUp(); const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight); const Math::Vector3 delta = ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel; m_focalPoint += delta; m_position += delta; } if (std::abs(input.flySpeedDelta) > Math::EPSILON) { const float speedFactor = std::pow(1.20f, input.flySpeedDelta); m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.1f, 500.0f); } if (input.deltaTime > 0.0f && (std::abs(input.moveForward) > Math::EPSILON || std::abs(input.moveRight) > Math::EPSILON || std::abs(input.moveUp) > Math::EPSILON)) { const Math::Vector3 movement = GetForward() * input.moveForward + GetRight() * input.moveRight + Math::Vector3::Up() * input.moveUp; if (movement.SqrMagnitude() > Math::EPSILON) { const float speedMultiplier = input.fastMove ? 4.0f : 1.0f; const float flySpeed = m_flySpeed * speedMultiplier; const Math::Vector3 delta = Math::Vector3::Normalize(movement) * flySpeed * input.deltaTime; m_position += delta; m_focalPoint += delta; } } 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(); } if (m_snapAnimating && input.deltaTime > 0.0f) { m_snapElapsed += input.deltaTime; const float t = std::clamp(m_snapElapsed / kSnapDurationSeconds, 0.0f, 1.0f); const float easedT = 1.0f - std::pow(1.0f - t, 3.0f); m_yawDegrees = LerpAngleDegrees(m_snapStartYawDegrees, m_snapTargetYawDegrees, easedT); m_pitchDegrees = m_snapStartPitchDegrees + (m_snapTargetPitchDegrees - m_snapStartPitchDegrees) * easedT; UpdatePositionFromFocalPoint(); if (t >= 1.0f) { m_snapAnimating = false; } } } void ApplyTo(Components::TransformComponent& transform) const { transform.SetPosition(GetPosition()); transform.SetRotation(Math::Quaternion::FromEulerAngles( -m_pitchDegrees * Math::DEG_TO_RAD, m_yawDegrees * Math::DEG_TO_RAD, 0.0f)); } private: static constexpr float kSnapDurationSeconds = 0.22f; static constexpr float kLookYawSensitivity = 0.18f; static constexpr float kLookPitchSensitivity = 0.12f; static constexpr float kOrbitYawSensitivity = 0.30f; static constexpr float kOrbitPitchSensitivity = 0.20f; struct OrientationAngles { float yawDegrees = 0.0f; float pitchDegrees = 0.0f; }; static float NormalizeDegrees(float value) { while (value > 180.0f) { value -= 360.0f; } while (value < -180.0f) { value += 360.0f; } return value; } static float LerpAngleDegrees(float fromDegrees, float toDegrees, float t) { const float delta = NormalizeDegrees(toDegrees - fromDegrees); return NormalizeDegrees(fromDegrees + delta * t); } static OrientationAngles ComputeOrientationAngles(const Math::Vector3& forward) { OrientationAngles result = {}; const Math::Vector3 normalizedForward = forward.Normalized(); const float horizontalLengthSq = normalizedForward.x * normalizedForward.x + normalizedForward.z * normalizedForward.z; if (horizontalLengthSq <= Math::EPSILON) { result.yawDegrees = 0.0f; } else { result.yawDegrees = std::atan2(normalizedForward.x, normalizedForward.z) * Math::RAD_TO_DEG; } result.pitchDegrees = std::clamp( std::asin(std::clamp(normalizedForward.y, -1.0f, 1.0f)) * Math::RAD_TO_DEG, -89.0f, 89.0f); return result; } void ApplyRotationDelta( float deltaX, float deltaY, float yawSensitivity, float pitchSensitivity) { m_yawDegrees += deltaX * yawSensitivity; m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * pitchSensitivity, -89.0f, 89.0f); } Math::Vector3 GetRight() const { return Math::Vector3::Cross(Math::Vector3::Up(), GetForward()).Normalized(); } Math::Vector3 GetUp() const { return Math::Vector3::Cross(GetForward(), GetRight()).Normalized(); } float ComputeWorldUnitsPerPixel(float viewportHeight) const { if (viewportHeight <= Math::EPSILON) { return 0.0f; } const float verticalFovRadians = 60.0f * Math::DEG_TO_RAD; const float viewportWorldHeight = 2.0f * std::tan(verticalFovRadians * 0.5f) * m_distance; return viewportWorldHeight / viewportHeight; } void UpdatePositionFromFocalPoint() { m_position = m_focalPoint - GetForward() * m_distance; } void UpdateFocalPointFromPosition() { m_focalPoint = m_position + GetForward() * m_distance; } Math::Vector3 m_focalPoint = Math::Vector3::Zero(); Math::Vector3 m_position = Math::Vector3(0.0f, 0.0f, -6.0f); float m_distance = 6.0f; float m_flySpeed = 5.0f; float m_yawDegrees = -35.0f; float m_pitchDegrees = -20.0f; float m_snapStartYawDegrees = 0.0f; float m_snapStartPitchDegrees = 0.0f; float m_snapTargetYawDegrees = 0.0f; float m_snapTargetPitchDegrees = 0.0f; float m_snapElapsed = 0.0f; bool m_snapAnimating = false; }; } // namespace XCEngine::UI::Editor::App