From ed2d96dee602caa4a8c43eed35afb16a2bb8d16c Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Thu, 20 Aug 2026 03:22:53 +0300 Subject: [PATCH] FIX: keep gizmo screen size constant under a perspective camera Gizmo sizes in the screen size mode are computed in world units and compensated with INV_ZOOM = 1/zoom, which cancels the projection only when the projection is orthographic. In Camera::apply_projection the perspective branch scales the frustum by near_z / m_distance, so the projected size of a world space object is world_size * zoom * (m_distance / depth). The m_distance / depth term is left uncompensated, and m_distance is the distance to the camera orbit target, not to the selection, so the gizmo of an object on a far plate is drawn several times too small and grows when the object is closer than the orbit point. Cancel that term with a DEPTH_CORRECTION factor built from the eye space depth of the selection center. m_gui_scale already holds near_z / m_distance under perspective, so the orbit distance is available from const getters. The factor stays 1.0 for an orthographic camera, when gizmo_keep_screen_size is off, and when there is no selection, so those paths keep the current behaviour. It is clamped so that a degenerate grabber matrix or an inflated gizmo AABB cannot feed back into apply_projection. --- src/slic3r/GUI/GLCanvas3D.cpp | 5 +-- src/slic3r/GUI/Gizmos/GLGizmoBase.cpp | 46 +++++++++++++++++++++++++-- src/slic3r/GUI/Gizmos/GLGizmoBase.hpp | 11 +++++++ src/slic3r/GUI/Selection.cpp | 8 +++-- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index ea897dd4af..d0c9d90230 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2807,6 +2807,9 @@ void GLCanvas3D::render(bool only_init) camera.apply_projection(_max_bounding_box(true, true, true,is_volumes_limit_to_expand_plate())); camera.update_frustum(); + //BBS update gizmo camera scaling (inv_zoom + perspective depth correction) + GLGizmoBase::update_camera_scaling(camera, m_selection); + m_frame_callback_list.clear(); const std::array& viewport = camera.get_viewport(); @@ -9428,8 +9431,6 @@ void GLCanvas3D::_render_volumes_for_picking() const void GLCanvas3D::_render_current_gizmo() const { - //BBS update inv_zoom - GLGizmoBase::INV_ZOOM = (float)get_active_camera().get_inv_zoom(); m_gizmos.render_current_gizmo(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index 4514699330..8bec6508ce 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -5,6 +5,7 @@ #include #include +#include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_Colors.hpp" #include "slic3r/GUI/OpenGLManager.hpp" @@ -15,6 +16,7 @@ namespace Slic3r { namespace GUI { float GLGizmoBase::INV_ZOOM = 1.0f; +float GLGizmoBase::DEPTH_CORRECTION = 1.0f; const float GLGizmoBase::Grabber::SizeFactor = 0.05f; @@ -324,11 +326,51 @@ void GLGizmoBase::render_lines(const std::vector &points) wxGetApp().unbind_shader(); } +void GLGizmoBase::update_camera_scaling(const Camera& camera, const Selection& selection) +{ + INV_ZOOM = (float)camera.get_inv_zoom(); + DEPTH_CORRECTION = 1.0f; + + if (camera.get_type() != Camera::EType::Perspective) + return; + + // Left untouched on purpose: with the flag off the gizmo geometry is anchored to the object + // bounding box in world space and only the grabber cubes follow 1/zoom, so that path was never + // screen size stable to begin with. Reworking it is out of scope here. + const auto& ogl_manager = wxGetApp().get_opengl_manager(); + if (!ogl_manager || !ogl_manager->is_gizmo_keep_screen_size_enabled()) + return; + + if (selection.is_empty()) + return; + + const BoundingBoxf3& box = selection.get_bounding_box(); + if (!box.defined) + return; + + const double near_z = camera.get_near_z(); + const double gui_scale = camera.get_gui_scale(); // == near_z / orbit_distance under perspective + if (near_z <= EPSILON || gui_scale <= EPSILON) + return; + + // Scaling a world space length by 1/zoom keeps it constant on screen only under an orthographic + // projection. Under perspective the projected size also scales with orbit_distance / depth, so a + // gizmo anchored away from the orbit plane grows or shrinks on its own. Cancel that term here. + const double depth = -(camera.get_view_matrix() * box.center()).z(); // eye space depth, positive in front of the camera + if (depth <= near_z) // anchor is in front of the near plane, nothing is drawn there anyway + return; + + // Clamped on both ends: a large factor inflates the gizmo AABB which feeds back into + // Camera::apply_projection, a tiny one makes the grabber model matrix degenerate. + const double orbit_distance = near_z / gui_scale; + DEPTH_CORRECTION = (float)std::clamp(depth / orbit_distance, 0.1, 8.0); +} + float GLGizmoBase::get_grabber_size() { float grabber_size = 8.0f; if (GLGizmoBase::INV_ZOOM > 0) { - grabber_size = GLGizmoBase::Grabber::FixedGrabberSize * GLGizmoBase::Grabber::GrabberSizeFactor * GLGizmoBase::INV_ZOOM; + grabber_size = GLGizmoBase::Grabber::FixedGrabberSize * GLGizmoBase::Grabber::GrabberSizeFactor * GLGizmoBase::INV_ZOOM * GLGizmoBase::DEPTH_CORRECTION; } return grabber_size; } @@ -688,7 +730,7 @@ void GLGizmoBase::modify_radius(float& radius) const uint32_t t_height = 0; ogl_manager->get_viewport_size(t_width, t_height); radius = 0.2f * std::min(t_width, t_height); - radius *= GLGizmoBase::INV_ZOOM; + radius *= GLGizmoBase::INV_ZOOM * GLGizmoBase::DEPTH_CORRECTION; } } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index f5acee79fb..e9c75a0ba6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -32,6 +32,7 @@ namespace GUI { class ImGuiWrapper; class GLCanvas3D; +struct Camera; enum class CommonGizmosDataID; class CommonGizmosDataPool; class Selection; @@ -44,6 +45,16 @@ class GLGizmoBase static const unsigned int BASE_ID = 255 * 255 * 254; static float INV_ZOOM; + // Cancels the perspective foreshortening for screen-space-sized gizmos, stays 1.0 under an orthographic camera. + static float DEPTH_CORRECTION; + + // Refreshes INV_ZOOM and DEPTH_CORRECTION for the given camera. The selection provides the anchor the + // gizmo is drawn around; DEPTH_CORRECTION falls back to 1.0 with an orthographic camera, with nothing + // selected, or when the screen size mode is off. + // Call once per frame, after Camera::apply_projection(), which is what fills in near_z and gui_scale. + // Single anchor per frame: grabbers offset from the selection center (Scale corners, the Cut plane, + // the Text/SVG cube) keep a residual error proportional to their own depth offset. + static void update_camera_scaling(const Camera& camera, const Selection& selection); //BBS colors static std::array DEFAULT_BASE_COLOR; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index bdbfb315d4..9663dc6551 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -2872,9 +2872,11 @@ Transform3d get_screen_scalling_matrix() if (p_ogl_manager) { if (p_ogl_manager->is_gizmo_keep_screen_size_enabled()) { const auto& t_zoom = camera.get_zoom(); - screen_scalling_matrix.data()[0 * 4 + 0] = 5.0f / t_zoom; - screen_scalling_matrix.data()[1 * 4 + 1] = 5.0f / t_zoom; - screen_scalling_matrix.data()[2 * 4 + 2] = 5.0f / t_zoom; + // Same perspective correction the gizmos get, so the hints stay the same length as the gizmo arrows. + const double t_scale = 5.0 * (double)GLGizmoBase::DEPTH_CORRECTION / t_zoom; + screen_scalling_matrix.data()[0 * 4 + 0] = t_scale; + screen_scalling_matrix.data()[1 * 4 + 1] = t_scale; + screen_scalling_matrix.data()[2 * 4 + 2] = t_scale; } } return screen_scalling_matrix;