refactor: align scene view camera controls with unity

This commit is contained in:
2026-03-28 18:21:18 +08:00
parent da486075e1
commit af2f30dad6
6 changed files with 123 additions and 29 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -11,6 +11,7 @@ public:
void Render() override;
private:
bool m_lookDragging = false;
bool m_orbitDragging = false;
bool m_panDragging = false;
};