FIX: keep gizmo screen size constant under a perspective camera - #11960
Open
vvzvlad wants to merge 1 commit into
Open
FIX: keep gizmo screen size constant under a perspective camera#11960vvzvlad wants to merge 1 commit into
vvzvlad wants to merge 1 commit into
Conversation
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.
Haidiye00
reviewed
Aug 20, 2026
Haidiye00
left a comment
Contributor
There was a problem hiding this comment.
Hi, please record the GIFs before and after the changes, and then submit them. Thanks.
guanyun-gudujian
requested changes
Aug 28, 2026
guanyun-gudujian
left a comment
Contributor
There was a problem hiding this comment.
Currently, we already have a constant gizmo screen size. Although there are some artifacts, it is good enough for now.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
With default settings the gizmos (move / rotate / scale) do not keep a constant size on screen. Their size depends on where the selected object is relative to the point the camera orbits around. In a project with many plates, the gizmo of an object on a far plate becomes tiny, and it becomes huge when the object is closer to the camera than the orbit point. It also changes while panning the view, without touching the zoom.
Default settings here means
gizmo_keep_screen_size = trueand the perspective camera, both are defaults (AppConfig.cppsetsgizmo_keep_screen_sizeto true,Camera.hpphasEType m_type{ EType::Perspective }).Reports that look like this problem: #5845, #3753, #6806, #5035, #11785.
Where it comes from
Gizmos compute a world size and cancel the zoom with
INV_ZOOM = 1 / camera.get_zoom():This cancels the projection only when the projection is orthographic. In
Camera::apply_projection()the perspective branch does:If you expand the resulting frustum matrix,
near_zcancels out and the projected size of a world space object becomeszoomis cancelled byINV_ZOOM, butm_distance / depthis not cancelled by anything.m_distanceis the distance from the camera to its orbit target,depthis the eye space depth of the object. They are equal only for an object sitting exactly on the orbit plane, and the camera orbits the center of the plate, not the selection. So the further the selected object is from that plane, the more wrong the size is.Two more things make it move on its own:
camera.set_target(...)inGLCanvas3D.cpp), and withzoom_to_mousethe wheel callscamera.translate(displacement), sodepthchanges on almost every navigation action;Camera::calc_tight_frustrum_zs_around()callsset_distance(m_distance + delta)when the near plane hitsFrustrumMinNearZ, som_distanceitself changes depending on what is inside the frustum.Numbers
This does not need a build. The script below builds the same
glOrtho/glFrustummatrices from the values used inapply_projection(), and projects the gizmo radius produced bymodify_radius():proof.py
Output:
The orthographic column is exactly
0.2 * min(viewport)at any depth, which is what the code intends. The perspective column is off by exactlyorbit_distance / depth, from 4x too big to 4x too small in one scene. The second block changes the zoom by 8x and the size does not move at all, which showsINV_ZOOMcompensates the zoom and only the zoom.Steps to reproduce in the app
Step 5 is the check that this is the projection and not something else, because the sizing code is the same in both cases.
The fix
Cancel the missing term: multiply the screen space sizes by
depth / orbit_distance, wheredepthis the eye space depth of the selection center.Camera::get_distance()andCamera::get_target()are not const, so they cannot be called from the const render path. But the perspective branch already storesm_gui_scale = near_z / m_distance, so the orbit distance isget_near_z() / get_gui_scale(), and both getters are const.The correction only applies when it is needed, and stays
1.0fotherwise:gizmo_keep_screen_size == false: that path is left bit identical, see the known limits below;The factor is clamped to
[0.1, 8.0]. The lower end keeps the grabber model matrix from becoming degenerate (normal_matrixtakes an inverse of it), the upper end keeps the gizmo AABB from inflating too much, because it is merged into_max_bounding_box()and ends up inapply_projection()again.Selection::get_screen_scalling_matrix()gets the same factor. It sizes the sidebar position and rotation hints under the same flag, from the same anchor, and they are drawn one line before the gizmo in the same frame, so leaving it out would make the hint arrows and the gizmo arrows differ in length by the same factor.Because of that the update moved from
_render_current_gizmo()up intorender(), right afterapply_projection()fills innear_zandgui_scale. Both consumers now read the value of the current frame._render_current_gizmo()is the only call site of the old assignment, so nothing else loses the refresh.Existing preferences keep their meaning,
grabber_size_factorstill scales the same way.Known limits of this patch
gizmo_keep_screen_sizeoff nothing changes at all. That path is also foreshortened, but there the grabber size and the arrow offset both scale as1/zoomand stay consistent with each other, so correcting only one of them would make it worse.on_render(), which runs after_picking_pass(), so hit boxes lag one frame as before. Both passes use the same world sizes, so this patch does not change that, but it may be part of why grabbers are hard to hit while the camera moves (Visual and "object" issue #6806).