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