Implement orbit snapping in 3D viewport

This commit is contained in:
passivestar 2025-10-11 16:26:07 +04:00
parent cb7cd815ee
commit d739700178
4 changed files with 88 additions and 17 deletions

View file

@ -407,6 +407,9 @@
<member name="editors/3d/navigation/zoom_style" type="int" setter="" getter=""> <member name="editors/3d/navigation/zoom_style" type="int" setter="" getter="">
The mouse cursor movement direction to use when zooming by moving the mouse. This does not affect zooming with the mouse wheel. The mouse cursor movement direction to use when zooming by moving the mouse. This does not affect zooming with the mouse wheel.
</member> </member>
<member name="editors/3d/navigation_feel/angle_snap_threshold" type="float" setter="" getter="">
The angle threshold for snapping camera rotation to 45-degree angles while orbiting with [kbd]Alt[/kbd] held.
</member>
<member name="editors/3d/navigation_feel/orbit_inertia" type="float" setter="" getter=""> <member name="editors/3d/navigation_feel/orbit_inertia" type="float" setter="" getter="">
The inertia to use when orbiting in the 3D editor. Higher values make the camera start and stop slower, which looks smoother but adds latency. The inertia to use when orbiting in the 3D editor. Higher values make the camera start and stop slower, which looks smoother but adds latency.
</member> </member>

View file

@ -1213,7 +1213,7 @@ void Node3DEditorViewport::_update_name() {
} break; } break;
} }
if (auto_orthogonal) { if (orthogonal && auto_orthogonal) {
// TRANSLATORS: This will be appended to the view name when Auto Orthogonal is enabled. // TRANSLATORS: This will be appended to the view name when Auto Orthogonal is enabled.
name += " " + TTR("[auto]"); name += " " + TTR("[auto]");
} }
@ -2510,27 +2510,32 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_down", p_event)) { if (ED_IS_SHORTCUT("spatial_editor/orbit_view_down", p_event)) {
// Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. // Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
cursor.x_rot = CLAMP(cursor.x_rot - Math::PI / 12.0, -1.57, 1.57); cursor.x_rot = CLAMP(cursor.x_rot - Math::PI / 12.0, -1.57, 1.57);
cursor.unsnapped_x_rot = cursor.x_rot;
view_type = VIEW_TYPE_USER; view_type = VIEW_TYPE_USER;
_update_name(); _update_name();
} }
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_up", p_event)) { if (ED_IS_SHORTCUT("spatial_editor/orbit_view_up", p_event)) {
// Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. // Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
cursor.x_rot = CLAMP(cursor.x_rot + Math::PI / 12.0, -1.57, 1.57); cursor.x_rot = CLAMP(cursor.x_rot + Math::PI / 12.0, -1.57, 1.57);
cursor.unsnapped_x_rot = cursor.x_rot;
view_type = VIEW_TYPE_USER; view_type = VIEW_TYPE_USER;
_update_name(); _update_name();
} }
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", p_event)) { if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", p_event)) {
cursor.y_rot -= Math::PI / 12.0; cursor.y_rot -= Math::PI / 12.0;
cursor.unsnapped_y_rot = cursor.y_rot;
view_type = VIEW_TYPE_USER; view_type = VIEW_TYPE_USER;
_update_name(); _update_name();
} }
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", p_event)) { if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", p_event)) {
cursor.y_rot += Math::PI / 12.0; cursor.y_rot += Math::PI / 12.0;
cursor.unsnapped_y_rot = cursor.y_rot;
view_type = VIEW_TYPE_USER; view_type = VIEW_TYPE_USER;
_update_name(); _update_name();
} }
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", p_event)) { if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", p_event)) {
cursor.y_rot += Math::PI; cursor.y_rot += Math::PI;
cursor.unsnapped_y_rot = cursor.y_rot;
view_type = VIEW_TYPE_USER; view_type = VIEW_TYPE_USER;
_update_name(); _update_name();
} }
@ -2726,30 +2731,69 @@ void Node3DEditorViewport::_nav_orbit(Ref<InputEventWithModifiers> p_event, cons
return; return;
} }
if (orthogonal && auto_orthogonal) {
_menu_option(VIEW_PERSPECTIVE);
}
const real_t degrees_per_pixel = EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity"); const real_t degrees_per_pixel = EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity");
const real_t radians_per_pixel = Math::deg_to_rad(degrees_per_pixel); const real_t radians_per_pixel = Math::deg_to_rad(degrees_per_pixel);
const bool invert_y_axis = EDITOR_GET("editors/3d/navigation/invert_y_axis"); const bool invert_y_axis = EDITOR_GET("editors/3d/navigation/invert_y_axis");
const bool invert_x_axis = EDITOR_GET("editors/3d/navigation/invert_x_axis"); const bool invert_x_axis = EDITOR_GET("editors/3d/navigation/invert_x_axis");
if (invert_y_axis) { cursor.unsnapped_x_rot += p_relative.y * radians_per_pixel * (invert_y_axis ? -1 : 1);
cursor.x_rot -= p_relative.y * radians_per_pixel; cursor.unsnapped_x_rot = CLAMP(cursor.unsnapped_x_rot, -1.57, 1.57);
} else { cursor.unsnapped_y_rot += p_relative.x * radians_per_pixel * (invert_x_axis ? -1 : 1);
cursor.x_rot += p_relative.y * radians_per_pixel;
}
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
if (invert_x_axis) { cursor.x_rot = cursor.unsnapped_x_rot;
cursor.y_rot -= p_relative.x * radians_per_pixel; cursor.y_rot = cursor.unsnapped_y_rot;
if (_is_nav_modifier_pressed("spatial_editor/viewport_orbit_snap_modifier_1") &&
_is_nav_modifier_pressed("spatial_editor/viewport_orbit_snap_modifier_2")) {
const real_t snap_angle = Math::deg_to_rad(45.0);
const real_t snap_threshold = Math::deg_to_rad((real_t)EDITOR_GET("editors/3d/navigation_feel/angle_snap_threshold"));
real_t x_rot_snapped = Math::snapped(cursor.unsnapped_x_rot, snap_angle);
real_t y_rot_snapped = Math::snapped(cursor.unsnapped_y_rot, snap_angle);
real_t x_dist = Math::abs(cursor.unsnapped_x_rot - x_rot_snapped);
real_t y_dist = Math::abs(cursor.unsnapped_y_rot - y_rot_snapped);
if (x_dist < snap_threshold && y_dist < snap_threshold) {
cursor.x_rot = x_rot_snapped;
cursor.y_rot = y_rot_snapped;
real_t y_rot_wrapped = Math::wrapf(y_rot_snapped, (real_t)-Math::PI, (real_t)Math::PI);
if (Math::abs(x_rot_snapped) < snap_threshold) {
if (Math::abs(y_rot_wrapped) < snap_threshold) {
view_type = VIEW_TYPE_FRONT;
} else if (Math::abs(Math::abs(y_rot_wrapped) - Math::PI) < snap_threshold) {
view_type = VIEW_TYPE_REAR;
} else if (Math::abs(y_rot_wrapped - Math::PI / 2.0) < snap_threshold) {
view_type = VIEW_TYPE_LEFT;
} else if (Math::abs(y_rot_wrapped + Math::PI / 2.0) < snap_threshold) {
view_type = VIEW_TYPE_RIGHT;
} else { } else {
cursor.y_rot += p_relative.x * radians_per_pixel; // Only switch to ortho for 90-degree views.
return;
} }
view_type = VIEW_TYPE_USER; _set_auto_orthogonal();
_update_name(); _update_name();
} else if (Math::abs(Math::abs(x_rot_snapped) - Math::PI / 2.0) < snap_threshold) {
if (Math::abs(y_rot_wrapped) < snap_threshold ||
Math::abs(Math::abs(y_rot_wrapped) - Math::PI) < snap_threshold ||
Math::abs(y_rot_wrapped - Math::PI / 2.0) < snap_threshold ||
Math::abs(y_rot_wrapped + Math::PI / 2.0) < snap_threshold) {
view_type = x_rot_snapped > 0 ? VIEW_TYPE_TOP : VIEW_TYPE_BOTTOM;
_set_auto_orthogonal();
_update_name();
}
}
return;
}
}
view_type = VIEW_TYPE_USER;
if (orthogonal && auto_orthogonal) {
_menu_option(VIEW_PERSPECTIVE);
}
} }
void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative) { void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative) {
@ -2777,8 +2821,10 @@ void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const
} }
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. // Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57); cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.y_rot += p_relative.x * radians_per_pixel; cursor.y_rot += p_relative.x * radians_per_pixel;
cursor.unsnapped_y_rot = cursor.y_rot;
// Look is like the opposite of Orbit: the focus point rotates around the camera // Look is like the opposite of Orbit: the focus point rotates around the camera
Transform3D camera_transform = to_camera_transform(cursor); Transform3D camera_transform = to_camera_transform(cursor);
@ -3763,6 +3809,8 @@ void Node3DEditorViewport::_apply_camera_transform_to_cursor() {
cursor.x_rot = -camera_transform.basis.get_euler().x; cursor.x_rot = -camera_transform.basis.get_euler().x;
cursor.y_rot = -camera_transform.basis.get_euler().y; cursor.y_rot = -camera_transform.basis.get_euler().y;
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.unsnapped_y_rot = cursor.y_rot;
} }
void Node3DEditorViewport::_menu_option(int p_option) { void Node3DEditorViewport::_menu_option(int p_option) {
@ -3771,6 +3819,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_TOP: { case VIEW_TOP: {
cursor.y_rot = 0; cursor.y_rot = 0;
cursor.x_rot = Math::PI / 2.0; cursor.x_rot = Math::PI / 2.0;
cursor.unsnapped_y_rot = cursor.y_rot;
cursor.unsnapped_x_rot = cursor.x_rot;
set_message(TTR("Top View."), 2); set_message(TTR("Top View."), 2);
view_type = VIEW_TYPE_TOP; view_type = VIEW_TYPE_TOP;
_set_auto_orthogonal(); _set_auto_orthogonal();
@ -3780,6 +3830,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_BOTTOM: { case VIEW_BOTTOM: {
cursor.y_rot = 0; cursor.y_rot = 0;
cursor.x_rot = -Math::PI / 2.0; cursor.x_rot = -Math::PI / 2.0;
cursor.unsnapped_y_rot = cursor.y_rot;
cursor.unsnapped_x_rot = cursor.x_rot;
set_message(TTR("Bottom View."), 2); set_message(TTR("Bottom View."), 2);
view_type = VIEW_TYPE_BOTTOM; view_type = VIEW_TYPE_BOTTOM;
_set_auto_orthogonal(); _set_auto_orthogonal();
@ -3789,6 +3841,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_LEFT: { case VIEW_LEFT: {
cursor.x_rot = 0; cursor.x_rot = 0;
cursor.y_rot = Math::PI / 2.0; cursor.y_rot = Math::PI / 2.0;
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.unsnapped_y_rot = cursor.y_rot;
set_message(TTR("Left View."), 2); set_message(TTR("Left View."), 2);
view_type = VIEW_TYPE_LEFT; view_type = VIEW_TYPE_LEFT;
_set_auto_orthogonal(); _set_auto_orthogonal();
@ -3798,6 +3852,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_RIGHT: { case VIEW_RIGHT: {
cursor.x_rot = 0; cursor.x_rot = 0;
cursor.y_rot = -Math::PI / 2.0; cursor.y_rot = -Math::PI / 2.0;
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.unsnapped_y_rot = cursor.y_rot;
set_message(TTR("Right View."), 2); set_message(TTR("Right View."), 2);
view_type = VIEW_TYPE_RIGHT; view_type = VIEW_TYPE_RIGHT;
_set_auto_orthogonal(); _set_auto_orthogonal();
@ -3807,6 +3863,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_FRONT: { case VIEW_FRONT: {
cursor.x_rot = 0; cursor.x_rot = 0;
cursor.y_rot = 0; cursor.y_rot = 0;
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.unsnapped_y_rot = cursor.y_rot;
set_message(TTR("Front View."), 2); set_message(TTR("Front View."), 2);
view_type = VIEW_TYPE_FRONT; view_type = VIEW_TYPE_FRONT;
_set_auto_orthogonal(); _set_auto_orthogonal();
@ -3816,6 +3874,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_REAR: { case VIEW_REAR: {
cursor.x_rot = 0; cursor.x_rot = 0;
cursor.y_rot = Math::PI; cursor.y_rot = Math::PI;
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.unsnapped_y_rot = cursor.y_rot;
set_message(TTR("Rear View."), 2); set_message(TTR("Rear View."), 2);
view_type = VIEW_TYPE_REAR; view_type = VIEW_TYPE_REAR;
_set_auto_orthogonal(); _set_auto_orthogonal();
@ -4445,9 +4505,11 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) {
} }
if (p_state.has("x_rotation")) { if (p_state.has("x_rotation")) {
cursor.x_rot = p_state["x_rotation"]; cursor.x_rot = p_state["x_rotation"];
cursor.unsnapped_x_rot = cursor.x_rot;
} }
if (p_state.has("y_rotation")) { if (p_state.has("y_rotation")) {
cursor.y_rot = p_state["y_rotation"]; cursor.y_rot = p_state["y_rotation"];
cursor.unsnapped_y_rot = cursor.y_rot;
} }
if (p_state.has("distance")) { if (p_state.has("distance")) {
cursor.distance = p_state["distance"]; cursor.distance = p_state["distance"];
@ -6016,6 +6078,8 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
// Registering with Key::NONE intentionally creates an empty Array. // Registering with Key::NONE intentionally creates an empty Array.
register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTRC("Viewport Orbit Modifier 1"), Key::NONE); register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTRC("Viewport Orbit Modifier 1"), Key::NONE);
register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTRC("Viewport Orbit Modifier 2"), Key::NONE); register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTRC("Viewport Orbit Modifier 2"), Key::NONE);
register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_1", TTRC("Viewport Orbit Snap Modifier 1"), Key::ALT);
register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_2", TTRC("Viewport Orbit Snap Modifier 2"), Key::NONE);
register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTRC("Viewport Pan Modifier 1"), Key::SHIFT); register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTRC("Viewport Pan Modifier 1"), Key::SHIFT);
register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTRC("Viewport Pan Modifier 2"), Key::NONE); register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTRC("Viewport Pan Modifier 2"), Key::NONE);
register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTRC("Viewport Zoom Modifier 1"), Key::SHIFT); register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTRC("Viewport Zoom Modifier 1"), Key::SHIFT);

View file

@ -403,6 +403,7 @@ private:
struct Cursor { struct Cursor {
Vector3 pos; Vector3 pos;
real_t x_rot, y_rot, distance, fov_scale; real_t x_rot, y_rot, distance, fov_scale;
real_t unsnapped_x_rot, unsnapped_y_rot;
Vector3 eye_pos; // Used in freelook mode Vector3 eye_pos; // Used in freelook mode
bool region_select; bool region_select;
Point2 region_begin, region_end; Point2 region_begin, region_end;
@ -411,6 +412,8 @@ private:
// These rotations place the camera in +X +Y +Z, aka south east, facing north west. // These rotations place the camera in +X +Y +Z, aka south east, facing north west.
x_rot = 0.5; x_rot = 0.5;
y_rot = -0.5; y_rot = -0.5;
unsnapped_x_rot = x_rot;
unsnapped_y_rot = y_rot;
distance = 4; distance = 4;
fov_scale = 1.0; fov_scale = 1.0;
region_select = false; region_select = false;

View file

@ -936,6 +936,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/orbit_inertia", 0.0, "0,1,0.001") EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/orbit_inertia", 0.0, "0,1,0.001")
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/translation_inertia", 0.05, "0,1,0.001") EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/translation_inertia", 0.05, "0,1,0.001")
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/zoom_inertia", 0.05, "0,1,0.001") EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/zoom_inertia", 0.05, "0,1,0.001")
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/angle_snap_threshold", 10.0, "1,20,0.1,degrees")
_initial_set("editors/3d/navigation/show_viewport_rotation_gizmo", true); _initial_set("editors/3d/navigation/show_viewport_rotation_gizmo", true);
_initial_set("editors/3d/navigation/show_viewport_navigation_gizmo", DisplayServer::get_singleton()->is_touchscreen_available()); _initial_set("editors/3d/navigation/show_viewport_navigation_gizmo", DisplayServer::get_singleton()->is_touchscreen_available());