From d3b8b4cefdbe6952db69d934e72f2aa783f8b211 Mon Sep 17 00:00:00 2001 From: Robert Yevdokimov <105675984+ryevdokimov@users.noreply.github.com> Date: Tue, 26 Aug 2025 04:29:22 +0400 Subject: [PATCH] Add trackball-style rotation for 3D transform gizmo --- editor/scene/3d/node_3d_editor_plugin.cpp | 143 +++++++++++++++++++++- editor/scene/3d/node_3d_editor_plugin.h | 8 +- 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index 98d6b9f9503e..2592946433d0 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -116,6 +116,13 @@ constexpr real_t GIZMO_VIEW_ROTATION_SIZE = 1.25; constexpr real_t GIZMO_SCALE_OFFSET = GIZMO_CIRCLE_SIZE + 0.3; constexpr real_t GIZMO_ARROW_OFFSET = GIZMO_CIRCLE_SIZE + 0.3; +constexpr real_t TRACKBALL_SENSITIVITY = 0.005; +constexpr int TRACKBALL_SPHERE_RINGS = 16; +constexpr int TRACKBALL_SPHERE_SECTORS = 32; +constexpr real_t TRACKBALL_HIGHLIGHT_ALPHA = 0.01; +constexpr int GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION = 15; +constexpr int GIZMO_HIGHLIGHT_AXIS_TRACKBALL = 16; + constexpr real_t ZOOM_FREELOOK_MIN = 0.01; constexpr real_t ZOOM_FREELOOK_MULTIPLIER = 1.08; constexpr real_t ZOOM_FREELOOK_INDICATOR_DELAY_S = 1.5; @@ -1383,6 +1390,7 @@ bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, b if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) { int col_axis = -1; bool view_rotation_selected = false; + bool trackball_selected = false; Vector3 hit_position; Vector3 hit_normal; @@ -1434,12 +1442,14 @@ bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, b if (Math::abs(distance_ray_to_center - view_rotation_radius) < circumference_tolerance && ray_length_to_center > 0) { view_rotation_selected = true; + } else if (distance_ray_to_center < gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && ray_length_to_center > 0) { + trackball_selected = true; } } if (view_rotation_selected) { if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(15); + spatial_editor->select_gizmo_highlight_axis(GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION); } else { _edit.mode = TRANSFORM_ROTATE; _compute_edit(p_screenpos); @@ -1450,6 +1460,21 @@ bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, b _edit.gizmo_initiated = true; } return true; + } else if (trackball_selected) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(GIZMO_HIGHLIGHT_AXIS_TRACKBALL); + } else { + _edit.mode = TRANSFORM_ROTATE; + _compute_edit(p_screenpos); + _edit.plane = TRANSFORM_VIEW; + _edit.is_trackball = true; + _edit.show_rotation_line = false; + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_axis = _get_camera_normal(); + _edit.gizmo_initiated = true; + spatial_editor->select_gizmo_highlight_axis(-1); + } + return true; } else if (col_axis != -1) { if (p_highlight_only) { spatial_editor->select_gizmo_highlight_axis(col_axis + 3); @@ -3725,7 +3750,7 @@ void Node3DEditorViewport::_draw() { break; } - if (_is_rotation_arc_visible() && !_edit.initial_click_vector.is_zero_approx()) { + if (!_edit.is_trackball && _is_rotation_arc_visible() && !_edit.initial_click_vector.is_zero_approx()) { Vector3 up = _edit.rotation_axis; Vector3 right = _edit.initial_click_vector; @@ -4432,6 +4457,16 @@ void Node3DEditorViewport::_init_gizmo_instance(int p_idx) { RS::get_singleton()->instance_geometry_set_flag(rotate_gizmo_instance[i], RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); RS::get_singleton()->instance_geometry_set_flag(rotate_gizmo_instance[i], RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false); } + + // Create trackball sphere instance + trackball_sphere_instance = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(trackball_sphere_instance, spatial_editor->get_trackball_sphere_gizmo()->get_rid()); + RS::get_singleton()->instance_set_scenario(trackball_sphere_instance, get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(trackball_sphere_instance, false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(trackball_sphere_instance, RS::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(trackball_sphere_instance, layer); + RS::get_singleton()->instance_geometry_set_flag(trackball_sphere_instance, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(trackball_sphere_instance, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false); } void Node3DEditorViewport::_finish_gizmo_instances() { @@ -4446,6 +4481,8 @@ void Node3DEditorViewport::_finish_gizmo_instances() { } // Rotation white outline RS::get_singleton()->free_rid(rotate_gizmo_instance[3]); + + RS::get_singleton()->free_rid(trackball_sphere_instance); } void Node3DEditorViewport::_toggle_camera_preview(bool p_activate) { @@ -4580,8 +4617,9 @@ void Node3DEditorViewport::update_transform_gizmo_view() { } bool hide_during_rotation = _is_rotation_arc_visible(); + bool hide_during_trackball = (_edit.mode == TRANSFORM_ROTATE && _edit.is_trackball); - bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_during_rotation; + bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_during_rotation && !hide_during_trackball; bool show_rotate_gizmo = show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE); for (int i = 0; i < 3; i++) { @@ -4610,7 +4648,12 @@ void Node3DEditorViewport::update_transform_gizmo_view() { RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[3], view_rotation_xform); RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], show_rotate_gizmo); - bool show_axes = spatial_editor->is_gizmo_visible() && _edit.mode != TRANSFORM_NONE; + bool can_show_trackball = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_during_rotation; + bool show_trackball_sphere = can_show_trackball && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) && !hide_during_trackball; + RenderingServer::get_singleton()->instance_set_transform(trackball_sphere_instance, view_rotation_xform); + RenderingServer::get_singleton()->instance_set_visible(trackball_sphere_instance, show_trackball_sphere); + + bool show_axes = spatial_editor->is_gizmo_visible() && _edit.mode != TRANSFORM_NONE && !hide_during_trackball; RenderingServer *rs = RenderingServer::get_singleton(); rs->instance_set_visible(axis_gizmo_instance[0], show_axes && (_edit.plane == TRANSFORM_X_AXIS || _edit.plane == TRANSFORM_XY || _edit.plane == TRANSFORM_XZ)); rs->instance_set_visible(axis_gizmo_instance[1], show_axes && (_edit.plane == TRANSFORM_Y_AXIS || _edit.plane == TRANSFORM_XY || _edit.plane == TRANSFORM_YZ)); @@ -5817,6 +5860,36 @@ void Node3DEditorViewport::update_transform(bool p_shift) { plane = Plane(_get_camera_normal(), _edit.center); } + if (_edit.is_trackball) { + Vector2 motion_delta = _edit.mouse_pos - _edit.original_mouse_pos; + real_t sensitivity = TRACKBALL_SENSITIVITY * EDSCALE; + Vector2 rotation_input = motion_delta * sensitivity; + + Transform3D cam_transform = to_camera_transform(cursor); + Vector3 cam_right = cam_transform.basis.get_column(0).normalized(); + Vector3 cam_up = cam_transform.basis.get_column(1).normalized(); + Vector3 rotation_axis = cam_up * rotation_input.x + cam_right * rotation_input.y; + + real_t rotation_angle = rotation_axis.length(); + if (rotation_angle > 0.0f) { + rotation_axis /= rotation_angle; + + bool snapping = _edit.snap || spatial_editor->is_snap_enabled(); + if (snapping) { + double snap_step = spatial_editor->get_rotate_snap(); + double angle_deg = Math::rad_to_deg(rotation_angle); + angle_deg = Math::snapped(angle_deg, snap_step); + rotation_angle = Math::deg_to_rad(angle_deg); + } + + double angle_deg = Math::rad_to_deg(rotation_angle); + set_message(vformat(TTR("Rotating %s degrees."), String::num(angle_deg, 2))); + + apply_transform(rotation_axis, rotation_angle); + } + break; + } + Vector3 local_axis; Vector3 global_axis; switch (_edit.plane) { @@ -5973,6 +6046,7 @@ void Node3DEditorViewport::finish_transform() { _edit.numeric_input = 0; _edit.numeric_next_decimal = 0; _edit.numeric_negate = false; + _edit.is_trackball = false; _edit.initial_click_vector = Vector3(); _edit.previous_rotation_vector = Vector3(); _edit.accumulated_rotation_angle = 0.0; @@ -6784,12 +6858,15 @@ void Node3DEditor::select_gizmo_highlight_axis(int p_axis) { for (int i = 0; i < 4; i++) { bool highlight; if (i == 3) { - highlight = (p_axis == 15); + highlight = (p_axis == GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION); } else { highlight = (i + 3) == p_axis; } rotate_gizmo[i]->surface_set_material(0, highlight ? rotate_gizmo_color_hl[i] : rotate_gizmo_color[i]); } + + bool highlight_trackball = (p_axis == GIZMO_HIGHLIGHT_AXIS_TRACKBALL); + trackball_sphere_gizmo->surface_set_material(0, highlight_trackball ? trackball_sphere_material_hl : trackball_sphere_material); } void Node3DEditor::update_transform_gizmo() { @@ -8166,6 +8243,62 @@ void fragment() { } } + // Create trackball sphere + { + trackball_sphere_gizmo.instantiate(); + Ref surftool; + surftool.instantiate(); + surftool->begin(Mesh::PRIMITIVE_TRIANGLES); + + const int sphere_rings = TRACKBALL_SPHERE_RINGS; + const int sphere_sectors = TRACKBALL_SPHERE_SECTORS; + const real_t sphere_radius = GIZMO_CIRCLE_SIZE; + + for (int r = 0; r <= sphere_rings; ++r) { + for (int s = 0; s <= sphere_sectors; ++s) { + real_t ring_angle = Math::PI * r / sphere_rings; + real_t sector_angle = 2.0 * Math::PI * s / sphere_sectors; + + Vector3 vertex( + sphere_radius * Math::sin(ring_angle) * Math::cos(sector_angle), + sphere_radius * Math::cos(ring_angle), + sphere_radius * Math::sin(ring_angle) * Math::sin(sector_angle)); + + surftool->set_normal(vertex.normalized()); + surftool->add_vertex(vertex); + } + } + + for (int r = 0; r < sphere_rings; ++r) { + for (int s = 0; s < sphere_sectors; ++s) { + int current = r * (sphere_sectors + 1) + s; + int next = current + sphere_sectors + 1; + + surftool->add_index(current); + surftool->add_index(next); + surftool->add_index(current + 1); + + surftool->add_index(current + 1); + surftool->add_index(next); + surftool->add_index(next + 1); + } + } + + trackball_sphere_material.instantiate(); + trackball_sphere_material->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); + trackball_sphere_material->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); + trackball_sphere_material->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA); + trackball_sphere_material->set_cull_mode(StandardMaterial3D::CULL_DISABLED); + trackball_sphere_material->set_albedo(Color(1.0, 1.0, 1.0, 0.0)); + trackball_sphere_material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); + + trackball_sphere_material_hl = trackball_sphere_material->duplicate(); + trackball_sphere_material_hl->set_albedo(Color(1.0, 1.0, 1.0, TRACKBALL_HIGHLIGHT_ALPHA)); + + surftool->set_material(trackball_sphere_material); + surftool->commit(trackball_sphere_gizmo); + } + _generate_selection_boxes(); } diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h index 957d7e20d074..e6a70e170ad2 100644 --- a/editor/scene/3d/node_3d_editor_plugin.h +++ b/editor/scene/3d/node_3d_editor_plugin.h @@ -383,6 +383,7 @@ class Node3DEditorViewport : public Control { Point2 original_mouse_pos; bool snap = false; bool show_rotation_line = false; + bool is_trackball = false; Ref gizmo; int gizmo_handle = 0; bool gizmo_handle_secondary = false; @@ -466,6 +467,7 @@ class Node3DEditorViewport : public Control { int zoom_failed_attempts_count = 0; RID move_gizmo_instance[3], move_plane_gizmo_instance[3], rotate_gizmo_instance[4], scale_gizmo_instance[3], scale_plane_gizmo_instance[3], axis_gizmo_instance[3]; + RID trackball_sphere_instance; String last_message; String message; @@ -532,7 +534,7 @@ class Node3DEditorViewport : public Control { void _project_settings_changed(); - Transform3D _compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis = false); + Transform3D _compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis); void _reset_transform(TransformType p_type); @@ -709,12 +711,15 @@ class Node3DEditor : public VBoxContainer { Vector3 grid_camera_last_update_position; Ref move_gizmo[3], move_plane_gizmo[3], rotate_gizmo[4], scale_gizmo[3], scale_plane_gizmo[3], axis_gizmo[3]; + Ref trackball_sphere_gizmo; Ref gizmo_color[3]; Ref plane_gizmo_color[3]; Ref rotate_gizmo_color[4]; Ref gizmo_color_hl[3]; Ref plane_gizmo_color_hl[3]; Ref rotate_gizmo_color_hl[4]; + Ref trackball_sphere_material; + Ref trackball_sphere_material_hl; Ref current_hover_gizmo; int current_hover_gizmo_handle; @@ -992,6 +997,7 @@ class Node3DEditor : public VBoxContainer { Ref get_rotate_gizmo(int idx) const { return rotate_gizmo[idx]; } Ref get_scale_gizmo(int idx) const { return scale_gizmo[idx]; } Ref get_scale_plane_gizmo(int idx) const { return scale_plane_gizmo[idx]; } + Ref get_trackball_sphere_gizmo() const { return trackball_sphere_gizmo; } void update_grid(); void update_transform_gizmo();