diff --git a/editor/icons/ViewportPOVTranslationControl.svg b/editor/icons/ViewportPOVTranslationControl.svg
new file mode 100644
index 0000000000000..88065c66bf8f6
--- /dev/null
+++ b/editor/icons/ViewportPOVTranslationControl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index 468d7fb0516ef..7f62ce19e8283 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -489,6 +489,352 @@ void ViewportRotationControl::set_viewport(Node3DEditorViewport *p_viewport) {
viewport = p_viewport;
}
+void ViewportZoomGizmoControl::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ if (!is_connected("mouse_exited", callable_mp(this, &ViewportZoomGizmoControl::_on_mouse_exited))) {
+ connect("mouse_exited", callable_mp(this, &ViewportZoomGizmoControl::_on_mouse_exited));
+ }
+ if (!is_connected("mouse_entered", callable_mp(this, &ViewportZoomGizmoControl::_on_mouse_entered))) {
+ connect("mouse_entered", callable_mp(this, &ViewportZoomGizmoControl::_on_mouse_entered));
+ }
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ if (viewport != nullptr) {
+ _draw();
+ _update_navigation();
+ }
+ } break;
+ }
+}
+
+void ViewportZoomGizmoControl::_draw() {
+ if (nav_mode == Node3DEditorViewport::NAVIGATION_NONE) {
+ return;
+ }
+
+ Vector2 center = get_size() / 2.0;
+ float radius = get_size().x / 5.0;
+
+ const bool focused = focused_index != -1;
+ draw_circle(center, radius, Color(0, 0, 0, focused || hovered ? 0.8 : 0.5));
+
+ const Color c = focused || hovered ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.5);
+
+ Vector2 circle_pos = focused ? center.move_toward(focused_pos, radius) : center;
+
+ Ref magGlass = EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Zoom"), EditorStringName(EditorIcons));
+ draw_texture(magGlass, circle_pos - Vector2(magGlass->get_width() / 2, magGlass->get_height() / 2), c);
+}
+
+void ViewportZoomGizmoControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) {
+ hovered = false;
+ queue_redraw();
+
+ if (focused_index != -1 && focused_index != p_index) {
+ return;
+ }
+ if (p_pressed) {
+ if (p_position.distance_to(get_size() / 2.0) < get_size().x / 2.0) {
+ focused_pos = p_position;
+ focused_index = p_index;
+ queue_redraw();
+ }
+ } else {
+ focused_index = -1;
+ if (Input::get_singleton()->get_mouse_mode() == Input::MOUSE_MODE_CAPTURED) {
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE);
+ Input::get_singleton()->warp_mouse(focused_mouse_start);
+ }
+ }
+}
+
+void ViewportZoomGizmoControl::_process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position) {
+ if (focused_index == p_index) {
+ if (Input::get_singleton()->get_mouse_mode() == Input::MOUSE_MODE_VISIBLE) {
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_CAPTURED);
+ focused_mouse_start = p_position;
+ }
+ focused_pos += p_relative_position;
+ queue_redraw();
+ }
+}
+
+void ViewportZoomGizmoControl::gui_input(const Ref &p_event) {
+ ERR_FAIL_COND(p_event.is_null());
+
+ // Mouse events
+ const Ref mouse_button = p_event;
+ if (mouse_button.is_valid() && mouse_button->get_button_index() == MouseButton::LEFT) {
+ _process_click(100, mouse_button->get_position(), mouse_button->is_pressed());
+ }
+
+ const Ref mouse_motion = p_event;
+ if (mouse_motion.is_valid()) {
+ _process_drag(100, mouse_motion->get_global_position(), viewport->_get_warped_mouse_motion(mouse_motion));
+ }
+
+ // Touch events
+ const Ref screen_touch = p_event;
+ if (screen_touch.is_valid()) {
+ _process_click(screen_touch->get_index(), screen_touch->get_position(), screen_touch->is_pressed());
+ }
+
+ const Ref screen_drag = p_event;
+ if (screen_drag.is_valid()) {
+ _process_drag(screen_drag->get_index(), screen_drag->get_position(), screen_drag->get_relative());
+ }
+}
+
+void ViewportZoomGizmoControl::_update_navigation() {
+ if (focused_index == -1) {
+ return;
+ }
+
+ Vector2 delta = focused_pos - (get_size() / 2.0);
+ Vector2 delta_normalized = delta.normalized();
+ switch (nav_mode) {
+ case Node3DEditorViewport::NavigationMode::NAVIGATION_MOVE: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x * 100.0), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+
+ const Node3DEditorViewport::FreelookNavigationScheme navigation_scheme = (Node3DEditorViewport::FreelookNavigationScheme)EditorSettings::get_singleton()->get("editors/3d/freelook/freelook_navigation_scheme").operator int();
+
+ Vector3 forward;
+ if (navigation_scheme == Node3DEditorViewport::FreelookNavigationScheme::FREELOOK_FULLY_AXIS_LOCKED) {
+ // Forward/backward keys will always go straight forward/backward, never moving on the Y axis.
+ forward = Vector3(0, 0, delta_normalized.y).rotated(Vector3(0, 1, 0), viewport->camera->get_rotation().y);
+ } else {
+ // Forward/backward keys will be relative to the camera pitch.
+ forward = viewport->camera->get_transform().basis.xform(Vector3(0, 0, delta_normalized.y));
+ }
+
+ const Vector3 right = viewport->camera->get_transform().basis.xform(Vector3(delta_normalized.x, 0, 0));
+
+ const Vector3 direction = forward + right;
+ const Vector3 motion = direction * speed;
+ viewport->cursor.pos += motion;
+ viewport->cursor.eye_pos += motion;
+ } break;
+
+ case Node3DEditorViewport::NavigationMode::NAVIGATION_LOOK: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x * 2.5), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_look(nullptr, delta_normalized * speed);
+ } break;
+
+ case Node3DEditorViewport::NAVIGATION_PAN: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_pan(nullptr, -delta_normalized * speed);
+ } break;
+ case Node3DEditorViewport::NAVIGATION_ZOOM: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_zoom(nullptr, delta_normalized * speed);
+ } break;
+ case Node3DEditorViewport::NAVIGATION_ORBIT: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_orbit(nullptr, delta_normalized * speed);
+ } break;
+ case Node3DEditorViewport::NAVIGATION_NONE: {
+ } break;
+ }
+}
+
+void ViewportZoomGizmoControl::_on_mouse_entered() {
+ hovered = true;
+ queue_redraw();
+}
+
+void ViewportZoomGizmoControl::_on_mouse_exited() {
+ hovered = false;
+ queue_redraw();
+}
+
+void ViewportZoomGizmoControl::set_navigation_mode(Node3DEditorViewport::NavigationMode p_nav_mode) {
+ nav_mode = p_nav_mode;
+}
+
+void ViewportZoomGizmoControl::set_viewport(Node3DEditorViewport *p_viewport) {
+ viewport = p_viewport;
+}
+
+void ViewportPOVTranslationControl::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ if (!is_connected("mouse_exited", callable_mp(this, &ViewportPOVTranslationControl::_on_mouse_exited))) {
+ connect("mouse_exited", callable_mp(this, &ViewportPOVTranslationControl::_on_mouse_exited));
+ }
+ if (!is_connected("mouse_entered", callable_mp(this, &ViewportPOVTranslationControl::_on_mouse_entered))) {
+ connect("mouse_entered", callable_mp(this, &ViewportPOVTranslationControl::_on_mouse_entered));
+ }
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ if (viewport != nullptr) {
+ _draw();
+ _update_navigation();
+ }
+ } break;
+ }
+}
+
+void ViewportPOVTranslationControl::_draw() {
+ if (nav_mode == Node3DEditorViewport::NAVIGATION_NONE) {
+ return;
+ }
+
+ Vector2 center = get_size() / 2.0;
+ float radius = get_size().x / 5.0;
+
+ const bool focused = focused_index != -1;
+ draw_circle(center, radius, Color(0, 0, 0, focused || hovered ? 0.8 : 0.5));
+
+ const Color c = focused || hovered ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.5);
+
+ Vector2 circle_pos = focused ? center.move_toward(focused_pos, radius) : center;
+
+ Ref hand = EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("ViewportPOVTranslationControl"), EditorStringName(EditorIcons));
+ draw_texture(hand, circle_pos - Vector2(hand->get_width() / 2, hand->get_height() / 2), c);
+}
+
+void ViewportPOVTranslationControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) {
+ hovered = false;
+ queue_redraw();
+
+ if (focused_index != -1 && focused_index != p_index) {
+ return;
+ }
+ if (p_pressed) {
+ if (p_position.distance_to(get_size() / 2.0) < get_size().x / 2.0) {
+ focused_pos = p_position;
+ focused_index = p_index;
+ queue_redraw();
+ }
+ } else {
+ focused_index = -1;
+ if (Input::get_singleton()->get_mouse_mode() == Input::MOUSE_MODE_CAPTURED) {
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE);
+ Input::get_singleton()->warp_mouse(focused_mouse_start);
+ }
+ }
+}
+
+void ViewportPOVTranslationControl::_process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position) {
+ if (focused_index == p_index) {
+ if (Input::get_singleton()->get_mouse_mode() == Input::MOUSE_MODE_VISIBLE) {
+ Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_CAPTURED);
+ focused_mouse_start = p_position;
+ }
+ focused_pos += p_relative_position;
+ queue_redraw();
+ }
+}
+
+void ViewportPOVTranslationControl::gui_input(const Ref &p_event) {
+ ERR_FAIL_COND(p_event.is_null());
+
+ // Mouse events
+ const Ref mouse_button = p_event;
+ if (mouse_button.is_valid() && mouse_button->get_button_index() == MouseButton::LEFT) {
+ _process_click(100, mouse_button->get_position(), mouse_button->is_pressed());
+ }
+
+ const Ref mouse_motion = p_event;
+ if (mouse_motion.is_valid()) {
+ _process_drag(100, mouse_motion->get_global_position(), viewport->_get_warped_mouse_motion(mouse_motion));
+ }
+
+ // Touch events
+ const Ref screen_touch = p_event;
+ if (screen_touch.is_valid()) {
+ _process_click(screen_touch->get_index(), screen_touch->get_position(), screen_touch->is_pressed());
+ }
+
+ const Ref screen_drag = p_event;
+ if (screen_drag.is_valid()) {
+ _process_drag(screen_drag->get_index(), screen_drag->get_position(), screen_drag->get_relative());
+ }
+}
+
+void ViewportPOVTranslationControl::_update_navigation() {
+ if (focused_index == -1) {
+ return;
+ }
+
+ Vector2 delta = focused_pos - (get_size() / 2.0);
+ Vector2 delta_normalized = delta.normalized();
+ switch (nav_mode) {
+ case Node3DEditorViewport::NavigationMode::NAVIGATION_MOVE: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x * 100.0), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+
+ const Node3DEditorViewport::FreelookNavigationScheme navigation_scheme = (Node3DEditorViewport::FreelookNavigationScheme)EditorSettings::get_singleton()->get("editors/3d/freelook/freelook_navigation_scheme").operator int();
+
+ Vector3 forward;
+ if (navigation_scheme == Node3DEditorViewport::FreelookNavigationScheme::FREELOOK_FULLY_AXIS_LOCKED) {
+ // Forward/backward keys will always go straight forward/backward, never moving on the Y axis.
+ forward = Vector3(0, 0, delta_normalized.y).rotated(Vector3(0, 1, 0), viewport->camera->get_rotation().y);
+ } else {
+ // Forward/backward keys will be relative to the camera pitch.
+ forward = viewport->camera->get_transform().basis.xform(Vector3(0, 0, delta_normalized.y));
+ }
+
+ const Vector3 right = viewport->camera->get_transform().basis.xform(Vector3(delta_normalized.x, 0, 0));
+
+ const Vector3 direction = forward + right;
+ const Vector3 motion = direction * speed;
+ viewport->cursor.pos += motion;
+ viewport->cursor.eye_pos += motion;
+ } break;
+
+ case Node3DEditorViewport::NavigationMode::NAVIGATION_LOOK: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x * 2.5), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_look(nullptr, delta_normalized * speed);
+ } break;
+
+ case Node3DEditorViewport::NAVIGATION_PAN: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_pan(nullptr, -delta_normalized * speed);
+ } break;
+ case Node3DEditorViewport::NAVIGATION_ZOOM: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_zoom(nullptr, delta_normalized * speed);
+ } break;
+ case Node3DEditorViewport::NAVIGATION_ORBIT: {
+ real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0);
+ real_t speed = viewport->freelook_speed * speed_multiplier;
+ viewport->_nav_orbit(nullptr, delta_normalized * speed);
+ } break;
+ case Node3DEditorViewport::NAVIGATION_NONE: {
+ } break;
+ }
+}
+
+void ViewportPOVTranslationControl::_on_mouse_entered() {
+ hovered = true;
+ queue_redraw();
+}
+
+void ViewportPOVTranslationControl::_on_mouse_exited() {
+ hovered = false;
+ queue_redraw();
+}
+
+void ViewportPOVTranslationControl::set_navigation_mode(Node3DEditorViewport::NavigationMode p_nav_mode) {
+ nav_mode = p_nav_mode;
+}
+
+void ViewportPOVTranslationControl::set_viewport(Node3DEditorViewport *p_viewport) {
+ viewport = p_viewport;
+}
+
void Node3DEditorViewport::_view_settings_confirmed(real_t p_interp_delta) {
// Set FOV override multiplier back to the default, so that the FOV
// setting specified in the View menu is correctly applied.
@@ -504,6 +850,12 @@ void Node3DEditorViewport::_update_navigation_controls_visibility() {
bool show_viewport_navigation_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_navigation_gizmo") && (!previewing_cinema && !previewing_camera);
position_control->set_visible(show_viewport_navigation_gizmo);
look_control->set_visible(show_viewport_navigation_gizmo);
+
+ bool show_viewport_zoom_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_zoom_gizmo") && (!previewing_cinema && !previewing_camera);
+ zoom_control->set_visible(show_viewport_zoom_gizmo);
+
+ bool show_viewport_pov_translation_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_pov_translation_gizmo") && (!previewing_cinema && !previewing_camera);
+ pov_translation_control->set_visible(show_viewport_pov_translation_gizmo);
}
void Node3DEditorViewport::_update_camera(real_t p_interp_delta) {
@@ -591,6 +943,8 @@ void Node3DEditorViewport::_update_camera(real_t p_interp_delta) {
rotation_control->queue_redraw();
position_control->queue_redraw();
look_control->queue_redraw();
+ zoom_control->queue_redraw();
+ pov_translation_control->queue_redraw();
spatial_editor->update_grid();
}
}
@@ -5314,9 +5668,10 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
// Make sure frame time labels don't touch the viewport's edge.
top_right_vbox->set_custom_minimum_size(Size2(100, 0) * EDSCALE);
// Prevent visible spacing between frame time labels.
- top_right_vbox->add_theme_constant_override("separation", 0);
+ top_right_vbox->add_theme_constant_override("separation", -22);
const int navigation_control_size = 150;
+ const int zoom_and_pov_control_size = 80;
position_control = memnew(ViewportNavigationControl);
position_control->set_navigation_mode(Node3DEditorViewport::NAVIGATION_MOVE);
@@ -5345,6 +5700,20 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
rotation_control->set_h_size_flags(SIZE_SHRINK_END);
rotation_control->set_viewport(this);
top_right_vbox->add_child(rotation_control);
+
+ zoom_control = memnew(ViewportZoomGizmoControl);
+ zoom_control->set_navigation_mode(Node3DEditorViewport::NAVIGATION_ZOOM);
+ zoom_control->set_custom_minimum_size(Size2(zoom_and_pov_control_size,zoom_and_pov_control_size) * EDSCALE);
+ zoom_control->set_h_size_flags(SIZE_SHRINK_END);
+ zoom_control->set_viewport(this);
+ top_right_vbox->add_child(zoom_control);
+
+ pov_translation_control = memnew(ViewportPOVTranslationControl);
+ pov_translation_control->set_navigation_mode(Node3DEditorViewport::NAVIGATION_PAN);
+ pov_translation_control->set_custom_minimum_size(Size2(zoom_and_pov_control_size, zoom_and_pov_control_size) * EDSCALE);
+ pov_translation_control->set_h_size_flags(SIZE_SHRINK_END);
+ pov_translation_control->set_viewport(this);
+ top_right_vbox->add_child(pov_translation_control);
// Individual Labels are used to allow coloring each label with its own color.
cpu_time_label = memnew(Label);
@@ -8734,6 +9103,8 @@ Node3DEditor::Node3DEditor() {
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::FLOAT, "editors/3d/manipulator_gizmo_opacity", PROPERTY_HINT_RANGE, "0,1,0.01"));
EDITOR_DEF("editors/3d/navigation/show_viewport_rotation_gizmo", true);
EDITOR_DEF("editors/3d/navigation/show_viewport_navigation_gizmo", DisplayServer::get_singleton()->is_touchscreen_available());
+ EDITOR_DEF("editors/3d/navigation/show_viewport_zoom_gizmo", true);
+ EDITOR_DEF("editors/3d/navigation/show_viewport_pov_translation_gizmo", true);
current_hover_gizmo_handle = -1;
current_hover_gizmo_handle_secondary = false;
diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h
index 7ddbb74006dcc..b071dd31481b9 100644
--- a/editor/plugins/node_3d_editor_plugin.h
+++ b/editor/plugins/node_3d_editor_plugin.h
@@ -59,6 +59,10 @@ class SubViewportContainer;
class VSeparator;
class VSplitContainer;
class ViewportNavigationControl;
+class ViewportZoomGizmoControl;
+class ViewportPOVTranslationControl;
+class ViewportZoomGizmoControl;
+class ViewportPOVTranslationControl;
class WorldEnvironment;
class ViewportRotationControl : public Control {
@@ -105,6 +109,10 @@ class Node3DEditorViewport : public Control {
friend class Node3DEditor;
friend class ViewportNavigationControl;
friend class ViewportRotationControl;
+ friend class ViewportZoomGizmoControl;
+ friend class ViewportPOVTranslationControl;
+ friend class ViewportZoomGizmoControl;
+ friend class ViewportPOVTranslationControl;
enum {
VIEW_TOP,
VIEW_BOTTOM,
@@ -254,6 +262,8 @@ class Node3DEditorViewport : public Control {
ViewportNavigationControl *position_control = nullptr;
ViewportNavigationControl *look_control = nullptr;
ViewportRotationControl *rotation_control = nullptr;
+ ViewportZoomGizmoControl *zoom_control = nullptr;
+ ViewportPOVTranslationControl *pov_translation_control = nullptr;
Gradient *frame_time_gradient = nullptr;
Label *cpu_time_label = nullptr;
Label *gpu_time_label = nullptr;
@@ -985,4 +995,54 @@ class ViewportNavigationControl : public Control {
void set_viewport(Node3DEditorViewport *p_viewport);
};
+class ViewportPOVTranslationControl : public Control {
+ GDCLASS(ViewportPOVTranslationControl, Control);
+
+ Node3DEditorViewport *viewport = nullptr;
+ bool hovered = false;
+ Vector2i focused_mouse_start;
+ Vector2 focused_pos;
+ int focused_index = -1;
+ Node3DEditorViewport::NavigationMode nav_mode = Node3DEditorViewport::NavigationMode::NAVIGATION_NONE;
+
+protected:
+ void _notification(int p_what);
+ virtual void gui_input(const Ref &p_event) override;
+ void _draw();
+ void _on_mouse_entered();
+ void _on_mouse_exited();
+ void _process_click(int p_index, Vector2 p_position, bool p_pressed);
+ void _process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position);
+ void _update_navigation();
+
+public:
+ void set_navigation_mode(Node3DEditorViewport::NavigationMode p_nav_mode);
+ void set_viewport(Node3DEditorViewport *p_viewport);
+};
+
+class ViewportZoomGizmoControl : public Control {
+ GDCLASS(ViewportZoomGizmoControl, Control);
+
+ Node3DEditorViewport *viewport = nullptr;
+ bool hovered = false;
+ Vector2i focused_mouse_start;
+ Vector2 focused_pos;
+ int focused_index = -1;
+ Node3DEditorViewport::NavigationMode nav_mode = Node3DEditorViewport::NavigationMode::NAVIGATION_NONE;
+
+protected:
+ void _notification(int p_what);
+ virtual void gui_input(const Ref &p_event) override;
+ void _draw();
+ void _on_mouse_entered();
+ void _on_mouse_exited();
+ void _process_click(int p_index, Vector2 p_position, bool p_pressed);
+ void _process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position);
+ void _update_navigation();
+
+ public:
+ void set_navigation_mode(Node3DEditorViewport::NavigationMode p_nav_mode);
+ void set_viewport(Node3DEditorViewport *p_viewport);
+};
+
#endif // NODE_3D_EDITOR_PLUGIN_H