diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index 2e8ae1a286d..f79b5027cb2 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -886,13 +886,15 @@ void Node3DEditorViewport::_update_name() { view_menu->reset_size(); } -void Node3DEditorViewport::_compute_edit(const Point2 &p_point) { +void Node3DEditorViewport::_compute_edit(const Point2 &p_point, const bool p_auto_center) { _edit.original_local = spatial_editor->are_local_coords_enabled(); _edit.click_ray = _get_ray(p_point); _edit.click_ray_pos = _get_ray_pos(p_point); _edit.plane = TRANSFORM_VIEW; + if (p_auto_center) { + _edit.center = spatial_editor->get_gizmo_transform().origin; + } spatial_editor->update_transform_gizmo(); - _edit.center = spatial_editor->get_gizmo_transform().origin; Node3D *selected = spatial_editor->get_single_selected_node(); Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; @@ -1364,6 +1366,7 @@ void Node3DEditorViewport::_sinput(const Ref &p_event) { } } + _edit.center = spatial_editor->get_gizmo_target_center(); Ref b = p_event; if (b.is_valid()) { @@ -3364,7 +3367,7 @@ void Node3DEditorViewport::update_transform_gizmo_view() { Transform3D xform = spatial_editor->get_gizmo_transform(); - Transform3D camera_xform = camera->get_transform(); + const Transform3D camera_xform = camera->get_transform(); if (xform.origin.is_equal_approx(camera_xform.origin)) { for (int i = 0; i < 3; i++) { @@ -3383,11 +3386,63 @@ void Node3DEditorViewport::update_transform_gizmo_view() { const Vector3 camz = -camera_xform.get_basis().get_axis(2).normalized(); const Vector3 camy = -camera_xform.get_basis().get_axis(1).normalized(); const Plane p = Plane(camz, camera_xform.origin); - const real_t gizmo_d = MAX(Math::abs(p.distance_to(xform.origin)), CMP_EPSILON); + const real_t gizmo_d = CLAMP(Math::abs(p.distance_to(xform.origin)), camera->get_near() * 2, camera->get_far() / 2); const real_t d0 = camera->unproject_position(camera_xform.origin + camz * gizmo_d).y; const real_t d1 = camera->unproject_position(camera_xform.origin + camz * gizmo_d + camy).y; const real_t dd = MAX(Math::abs(d0 - d1), CMP_EPSILON); + // This code ensures the gizmo stays on the screen. This includes if + // the gizmo would otherwise be behind the camera, to the sides of + // the camera, too close to the edge of the screen, too close to + // the camera, or too far away from the camera. First we calculate + // where the gizmo would go on screen, then we put it there. + const Vector3 object_position = spatial_editor->get_gizmo_target_center(); + Vector2 gizmo_screen_position = camera->unproject_position(object_position); + const Vector2 viewport_size = viewport->get_size(); + // We would use "camera.is_position_behind(parent_translation)" instead of dot, + // except that it also accounts for the near clip plane, which we don't want. + const bool is_in_front = camera_xform.basis.get_column(2).dot(object_position - camera_xform.origin) < 0; + const bool is_in_viewport = is_in_front && Rect2(Vector2(0, 0), viewport_size).has_point(gizmo_screen_position); + if (spatial_editor->is_keep_gizmo_onscreen_enabled()) { + if (!spatial_editor->is_gizmo_visible() || is_in_viewport) { + // In this case, the gizmo is either not visible, or in the viewport + // already, so we should hide the offscreen line. + gizmo_offscreen_line->hide(); + } else { + // In this case, the point is not "normally" on screen, and + // it should be placed in the center. + const Vector2 half_viewport_size = viewport_size / 2; + gizmo_screen_position = half_viewport_size; + // The rest of this is for drawing the offscreen line. + // One point goes in the center of the viewport. + // Calculate where to put the other point of the line. + Vector2 unprojected_position = camera->unproject_position(object_position); + if (!is_in_front) { + // When the object is behind, we need to flip and grow the line. + unprojected_position -= half_viewport_size; + unprojected_position = unprojected_position.normalized() * -half_viewport_size.length_squared(); + unprojected_position += half_viewport_size; + } + gizmo_offscreen_line->point1 = half_viewport_size; + gizmo_offscreen_line->point2 = unprojected_position; + gizmo_offscreen_line->update(); + gizmo_offscreen_line->show(); + } + // Update the gizmo's position using what we calculated. + xform.origin = camera->project_position(gizmo_screen_position, gizmo_d); + } else { + // In this case, the user does not want the gizmo to be + // kept on screen, so we should hide the offscreen line, + // and just use the gizmo's unmodified position. + gizmo_offscreen_line->hide(); + if (is_in_viewport) { + xform.origin = camera->project_position(gizmo_screen_position, gizmo_d); + } else { + xform.origin = object_position; + } + } + spatial_editor->set_gizmo_transform(xform); + const real_t gizmo_size = EditorSettings::get_singleton()->get("editors/3d/manipulator_gizmo_size"); // At low viewport heights, multiply the gizmo scale based on the viewport height. // This prevents the gizmo from growing very large and going outside the viewport. @@ -3396,7 +3451,6 @@ void Node3DEditorViewport::update_transform_gizmo_view() { (gizmo_size / Math::abs(dd)) * MAX(1, EDSCALE) * MIN(viewport_base_height, subviewport_container->get_size().height) / viewport_base_height / subviewport_container->get_stretch_shrink(); - Vector3 scale = Vector3(1, 1, 1) * gizmo_scale; // if the determinant is zero, we should disable the gizmo from being rendered // this prevents supplying bad values to the renderer and then having to filter it out again @@ -3418,7 +3472,7 @@ void Node3DEditorViewport::update_transform_gizmo_view() { if (xform.basis.get_axis(i).normalized().dot(xform.basis.get_axis((i + 1) % 3).normalized()) < 1.0) { axis_angle = axis_angle.looking_at(xform.basis.get_axis(i).normalized(), xform.basis.get_axis((i + 1) % 3).normalized()); } - axis_angle.basis.scale(scale); + axis_angle.basis *= gizmo_scale; axis_angle.origin = xform.origin; RenderingServer::get_singleton()->instance_set_transform(move_gizmo_instance[i], axis_angle); RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], spatial_editor->is_gizmo_visible() && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE)); @@ -3441,7 +3495,7 @@ void Node3DEditorViewport::update_transform_gizmo_view() { // Rotation white outline xform.orthonormalize(); - xform.basis.scale(scale); + xform.basis *= gizmo_scale; RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[3], xform); RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], spatial_editor->is_gizmo_visible() && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)); } @@ -4023,7 +4077,7 @@ void Node3DEditorViewport::drop_data_fw(const Point2 &p_point, const Variant &p_ void Node3DEditorViewport::begin_transform(TransformMode p_mode, bool instant) { if (get_selected_count() > 0) { _edit.mode = p_mode; - _compute_edit(_edit.mouse_pos); + _compute_edit(_edit.mouse_pos, false); _edit.instant = instant; _edit.snap = spatial_editor->is_snap_enabled(); } @@ -4426,15 +4480,14 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, Edito zoom_indicator_delay = 0.0; spatial_editor = p_spatial_editor; - SubViewportContainer *c = memnew(SubViewportContainer); - subviewport_container = c; - c->set_stretch(true); - add_child(c); - c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + subviewport_container = memnew(SubViewportContainer); + subviewport_container->set_stretch(true); + add_child(subviewport_container); + subviewport_container->set_anchors_and_offsets_preset(Control::PRESET_WIDE); viewport = memnew(SubViewport); viewport->set_disable_input(true); - c->add_child(viewport); + subviewport_container->add_child(viewport); surface = memnew(Control); surface->set_drag_forwarding(this); add_child(surface); @@ -4447,6 +4500,9 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, Edito camera->make_current(); surface->set_focus_mode(FOCUS_ALL); + gizmo_offscreen_line = memnew(GizmoOffScreenLine); + subviewport_container->add_child(gizmo_offscreen_line); + VBoxContainer *vbox = memnew(VBoxContainer); surface->add_child(vbox); vbox->set_offset(SIDE_LEFT, 10 * EDSCALE); @@ -5071,6 +5127,7 @@ void Node3DEditor::update_transform_gizmo() { gizmo.visible = count > 0; gizmo.transform.origin = (count > 0) ? gizmo_center / count : Vector3(); gizmo.transform.basis = (count == 1) ? gizmo_basis : Basis(); + gizmo.target_center = gizmo_center / count; for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) { viewports[i]->update_transform_gizmo_view(); @@ -5223,6 +5280,7 @@ Dictionary Node3DEditor::get_state() const { d["show_grid"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_GRID)); d["show_origin"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_ORIGIN)); + d["keep_gizmo_onscreen"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(MENU_KEEP_GIZMO_ONSCREEN)); d["fov"] = get_fov(); d["znear"] = get_znear(); d["zfar"] = get_zfar(); @@ -5347,6 +5405,13 @@ void Node3DEditor::set_state(const Dictionary &p_state) { RenderingServer::get_singleton()->instance_set_visible(origin_instance, use); } } + if (d.has("keep_gizmo_onscreen")) { + bool use = d["keep_gizmo_onscreen"]; + + if (use != view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(MENU_KEEP_GIZMO_ONSCREEN))) { + _menu_item_pressed(MENU_KEEP_GIZMO_ONSCREEN); + } + } if (d.has("gizmos_status")) { Dictionary gizmos_status = d["gizmos_status"]; @@ -5700,7 +5765,13 @@ void Node3DEditor::_menu_item_pressed(int p_option) { _init_grid(); view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(p_option), grid_enabled); - + } break; + case MENU_KEEP_GIZMO_ONSCREEN: { + keep_gizmo_onscreen = !view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(p_option)); + for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) { + get_editor_viewport(i)->update_transform_gizmo_view(); + } + view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(p_option), keep_gizmo_onscreen); } break; case MENU_VIEW_CAMERA_SETTINGS: { settings_dialog->popup_centered(settings_vbc->get_combined_minimum_size() + Size2(50, 50)); @@ -7685,12 +7756,14 @@ Node3DEditor::Node3DEditor(EditorNode *p_editor) { p->add_separator(); p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_origin", TTR("View Origin")), MENU_VIEW_ORIGIN); p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid", TTR("View Grid"), Key::NUMBERSIGN), MENU_VIEW_GRID); + p->add_check_shortcut(ED_SHORTCUT("spatial_editor/keep_gizmo_onscreen", TTR("Keep Gizmo On Screen"), KeyModifierMask::CMD + KeyModifierMask::ALT + Key::G), MENU_KEEP_GIZMO_ONSCREEN); p->add_separator(); p->add_shortcut(ED_SHORTCUT("spatial_editor/settings", TTR("Settings...")), MENU_VIEW_CAMERA_SETTINGS); p->set_item_checked(p->get_item_index(MENU_VIEW_ORIGIN), true); p->set_item_checked(p->get_item_index(MENU_VIEW_GRID), true); + p->set_item_checked(p->get_item_index(MENU_KEEP_GIZMO_ONSCREEN), true); p->connect("id_pressed", callable_mp(this, &Node3DEditor::_menu_item_pressed)); diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index bbe56155708..f14f8b90b91 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -87,6 +87,19 @@ public: void set_viewport(Node3DEditorViewport *p_viewport); }; +class GizmoOffScreenLine : public Control { + GDCLASS(GizmoOffScreenLine, Control); + +public: + Vector2 point1; + Vector2 point2; + void _notification(int p_what) { + if (p_what == NOTIFICATION_DRAW && is_visible()) { + draw_line(point1, point2, Color(0.0f, 0.75f, 0.75f), 2); + } + } +}; + class Node3DEditorViewport : public Control { GDCLASS(Node3DEditorViewport, Control); friend class Node3DEditor; @@ -201,6 +214,7 @@ private: CheckBox *preview_camera; SubViewportContainer *subviewport_container; + GizmoOffScreenLine *gizmo_offscreen_line; MenuButton *view_menu; PopupMenu *display_submenu; @@ -237,7 +251,7 @@ private: }; void _update_name(); - void _compute_edit(const Point2 &p_point); + void _compute_edit(const Point2 &p_point, const bool p_auto_center = true); void _clear_selected(); void _select_clicked(bool p_allow_locked); ObjectID _select_ray(const Point2 &p_pos); @@ -507,6 +521,35 @@ class Node3DEditor : public VBoxContainer { public: static const unsigned int VIEWPORTS_COUNT = 4; + enum MenuOption { + MENU_GROUP_SELECTED, + MENU_KEEP_GIZMO_ONSCREEN, + MENU_LOCK_SELECTED, + MENU_SNAP_TO_FLOOR, + MENU_TOOL_LIST_SELECT, + MENU_TOOL_LOCAL_COORDS, + MENU_TOOL_MOVE, + MENU_TOOL_OVERRIDE_CAMERA, + MENU_TOOL_ROTATE, + MENU_TOOL_SCALE, + MENU_TOOL_SELECT, + MENU_TOOL_USE_SNAP, + MENU_TRANSFORM_CONFIGURE_SNAP, + MENU_TRANSFORM_DIALOG, + MENU_UNGROUP_SELECTED, + MENU_UNLOCK_SELECTED, + MENU_VIEW_CAMERA_SETTINGS, + MENU_VIEW_GIZMOS_3D_ICONS, + MENU_VIEW_GRID, + MENU_VIEW_ORIGIN, + MENU_VIEW_USE_1_VIEWPORT, + MENU_VIEW_USE_2_VIEWPORTS, + MENU_VIEW_USE_2_VIEWPORTS_ALT, + MENU_VIEW_USE_3_VIEWPORTS, + MENU_VIEW_USE_3_VIEWPORTS_ALT, + MENU_VIEW_USE_4_VIEWPORTS, + }; + enum ToolMode { TOOL_MODE_SELECT, TOOL_MODE_MOVE, @@ -525,7 +568,6 @@ public: TOOL_OPT_USE_SNAP, TOOL_OPT_OVERRIDE_CAMERA, TOOL_OPT_MAX - }; private: @@ -554,6 +596,7 @@ private: Camera3D::Projection grid_camera_last_update_perspective = Camera3D::PROJECTION_PERSPECTIVE; Vector3 grid_camera_last_update_position = Vector3(); + bool keep_gizmo_onscreen = true; Ref move_gizmo[3], move_plane_gizmo[3], rotate_gizmo[4], scale_gizmo[3], scale_plane_gizmo[3], axis_gizmo[3]; Ref gizmo_color[3]; Ref plane_gizmo_color[3]; @@ -587,37 +630,10 @@ private: struct Gizmo { bool visible = false; real_t scale = 0; + Vector3 target_center; Transform3D transform; } gizmo; - enum MenuOption { - MENU_TOOL_SELECT, - MENU_TOOL_MOVE, - MENU_TOOL_ROTATE, - MENU_TOOL_SCALE, - MENU_TOOL_LIST_SELECT, - MENU_TOOL_LOCAL_COORDS, - MENU_TOOL_USE_SNAP, - MENU_TOOL_OVERRIDE_CAMERA, - MENU_TRANSFORM_CONFIGURE_SNAP, - MENU_TRANSFORM_DIALOG, - MENU_VIEW_USE_1_VIEWPORT, - MENU_VIEW_USE_2_VIEWPORTS, - MENU_VIEW_USE_2_VIEWPORTS_ALT, - MENU_VIEW_USE_3_VIEWPORTS, - MENU_VIEW_USE_3_VIEWPORTS_ALT, - MENU_VIEW_USE_4_VIEWPORTS, - MENU_VIEW_ORIGIN, - MENU_VIEW_GRID, - MENU_VIEW_GIZMOS_3D_ICONS, - MENU_VIEW_CAMERA_SETTINGS, - MENU_LOCK_SELECTED, - MENU_UNLOCK_SELECTED, - MENU_GROUP_SELECTED, - MENU_UNGROUP_SELECTED, - MENU_SNAP_TO_FLOOR - }; - Button *tool_button[TOOL_MAX]; Button *tool_option_button[TOOL_OPT_MAX]; @@ -776,7 +792,10 @@ public: float get_zfar() const { return settings_zfar->get_value(); } float get_fov() const { return settings_fov->get_value(); } + Vector3 get_gizmo_target_center() const { return gizmo.target_center; } Transform3D get_gizmo_transform() const { return gizmo.transform; } + void set_gizmo_transform(const Transform3D &p_transform) { gizmo.transform = p_transform; } + bool is_keep_gizmo_onscreen_enabled() const { return keep_gizmo_onscreen; } bool is_gizmo_visible() const; ToolMode get_tool_mode() const { return tool_mode; }