From c61b2326897ec6fd66d26849ed4a8228a5c5625d Mon Sep 17 00:00:00 2001 From: Vasco Rodrigues Date: Thu, 20 Jun 2024 17:41:17 +0100 Subject: [PATCH] Implement a zoom and pan gizmo for the 3d editor This implementation features the addition of 2 new gizmos on the 3D editor view on the top right. These 2 gizmos that can be dragged on either with a mouse or touch to either zoom in or out (for the first gizmo) or pan the camera vertically or horizontally (for the second gizmo) depending on the direction of the drag. These can be enabled and disabled in the Editor Settings in the tab Editors/3D/Navigation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Guilherme Luis Rogão Viana --- .../icons/ViewportPOVTranslationControl.svg | 1 + editor/plugins/node_3d_editor_plugin.cpp | 373 +++++++++++++++++- editor/plugins/node_3d_editor_plugin.h | 60 +++ 3 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 editor/icons/ViewportPOVTranslationControl.svg diff --git a/editor/icons/ViewportPOVTranslationControl.svg b/editor/icons/ViewportPOVTranslationControl.svg new file mode 100644 index 000000000000..88065c66bf8f --- /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 468d7fb0516e..7f62ce19e828 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 7ddbb74006dc..b071dd31481b 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