diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index f11bd0f0202..0feea6289ef 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -407,6 +407,9 @@
The mouse cursor movement direction to use when zooming by moving the mouse. This does not affect zooming with the mouse wheel.
+
+ The angle threshold for snapping camera rotation to 45-degree angles while orbiting with [kbd]Alt[/kbd] held.
+
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.
diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp
index 959add1db26..23a326ed6b6 100644
--- a/editor/scene/3d/node_3d_editor_plugin.cpp
+++ b/editor/scene/3d/node_3d_editor_plugin.cpp
@@ -1213,7 +1213,7 @@ void Node3DEditorViewport::_update_name() {
} break;
}
- if (auto_orthogonal) {
+ if (orthogonal && auto_orthogonal) {
// TRANSLATORS: This will be appended to the view name when Auto Orthogonal is enabled.
name += " " + TTR("[auto]");
}
@@ -2510,27 +2510,32 @@ void Node3DEditorViewport::_sinput(const Ref &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.
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;
_update_name();
}
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.
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;
_update_name();
}
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", p_event)) {
cursor.y_rot -= Math::PI / 12.0;
+ cursor.unsnapped_y_rot = cursor.y_rot;
view_type = VIEW_TYPE_USER;
_update_name();
}
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", p_event)) {
cursor.y_rot += Math::PI / 12.0;
+ cursor.unsnapped_y_rot = cursor.y_rot;
view_type = VIEW_TYPE_USER;
_update_name();
}
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", p_event)) {
cursor.y_rot += Math::PI;
+ cursor.unsnapped_y_rot = cursor.y_rot;
view_type = VIEW_TYPE_USER;
_update_name();
}
@@ -2726,30 +2731,69 @@ void Node3DEditorViewport::_nav_orbit(Ref p_event, cons
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 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_x_axis = EDITOR_GET("editors/3d/navigation/invert_x_axis");
- if (invert_y_axis) {
- cursor.x_rot -= p_relative.y * radians_per_pixel;
- } else {
- 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);
+ cursor.unsnapped_x_rot += p_relative.y * radians_per_pixel * (invert_y_axis ? -1 : 1);
+ cursor.unsnapped_x_rot = CLAMP(cursor.unsnapped_x_rot, -1.57, 1.57);
+ cursor.unsnapped_y_rot += p_relative.x * radians_per_pixel * (invert_x_axis ? -1 : 1);
- if (invert_x_axis) {
- cursor.y_rot -= p_relative.x * radians_per_pixel;
- } else {
- cursor.y_rot += p_relative.x * radians_per_pixel;
+ cursor.x_rot = cursor.unsnapped_x_rot;
+ 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 {
+ // Only switch to ortho for 90-degree views.
+ return;
+ }
+ _set_auto_orthogonal();
+ _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;
- _update_name();
+ if (orthogonal && auto_orthogonal) {
+ _menu_option(VIEW_PERSPECTIVE);
+ }
}
void Node3DEditorViewport::_nav_look(Ref p_event, const Vector2 &p_relative) {
@@ -2777,8 +2821,10 @@ void Node3DEditorViewport::_nav_look(Ref p_event, const
}
// 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.unsnapped_x_rot = cursor.x_rot;
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
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.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) {
@@ -3771,6 +3819,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_TOP: {
cursor.y_rot = 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);
view_type = VIEW_TYPE_TOP;
_set_auto_orthogonal();
@@ -3780,6 +3830,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_BOTTOM: {
cursor.y_rot = 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);
view_type = VIEW_TYPE_BOTTOM;
_set_auto_orthogonal();
@@ -3789,6 +3841,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_LEFT: {
cursor.x_rot = 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);
view_type = VIEW_TYPE_LEFT;
_set_auto_orthogonal();
@@ -3798,6 +3852,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_RIGHT: {
cursor.x_rot = 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);
view_type = VIEW_TYPE_RIGHT;
_set_auto_orthogonal();
@@ -3807,6 +3863,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_FRONT: {
cursor.x_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);
view_type = VIEW_TYPE_FRONT;
_set_auto_orthogonal();
@@ -3816,6 +3874,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
case VIEW_REAR: {
cursor.x_rot = 0;
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);
view_type = VIEW_TYPE_REAR;
_set_auto_orthogonal();
@@ -4445,9 +4505,11 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) {
}
if (p_state.has("x_rotation")) {
cursor.x_rot = p_state["x_rotation"];
+ cursor.unsnapped_x_rot = cursor.x_rot;
}
if (p_state.has("y_rotation")) {
cursor.y_rot = p_state["y_rotation"];
+ cursor.unsnapped_y_rot = cursor.y_rot;
}
if (p_state.has("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.
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_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_2", TTRC("Viewport Pan Modifier 2"), Key::NONE);
register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTRC("Viewport Zoom Modifier 1"), Key::SHIFT);
diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h
index 22322a12bff..624790c92b6 100644
--- a/editor/scene/3d/node_3d_editor_plugin.h
+++ b/editor/scene/3d/node_3d_editor_plugin.h
@@ -403,6 +403,7 @@ private:
struct Cursor {
Vector3 pos;
real_t x_rot, y_rot, distance, fov_scale;
+ real_t unsnapped_x_rot, unsnapped_y_rot;
Vector3 eye_pos; // Used in freelook mode
bool region_select;
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.
x_rot = 0.5;
y_rot = -0.5;
+ unsnapped_x_rot = x_rot;
+ unsnapped_y_rot = y_rot;
distance = 4;
fov_scale = 1.0;
region_select = false;
diff --git a/editor/settings/editor_settings.cpp b/editor/settings/editor_settings.cpp
index 22db86c60eb..d3428a77590 100644
--- a/editor/settings/editor_settings.cpp
+++ b/editor/settings/editor_settings.cpp
@@ -936,6 +936,7 @@ void EditorSettings::_load_defaults(Ref 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/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/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_navigation_gizmo", DisplayServer::get_singleton()->is_touchscreen_available());