diff --git a/editor/scene/3d/node_3d_editor_gizmos.cpp b/editor/scene/3d/node_3d_editor_gizmos.cpp index 1dc497091d..7c90353bcb 100644 --- a/editor/scene/3d/node_3d_editor_gizmos.cpp +++ b/editor/scene/3d/node_3d_editor_gizmos.cpp @@ -71,6 +71,7 @@ void EditorNode3DGizmo::clear() { billboard_handle = false; collision_segments.clear(); collision_meshes.clear(); + collision_meshes_are_snap_source = false; instances.clear(); handles.clear(); handle_ids.clear(); diff --git a/editor/scene/3d/node_3d_editor_gizmos.h b/editor/scene/3d/node_3d_editor_gizmos.h index b36c086444..33160558d3 100644 --- a/editor/scene/3d/node_3d_editor_gizmos.h +++ b/editor/scene/3d/node_3d_editor_gizmos.h @@ -58,6 +58,7 @@ class EditorNode3DGizmo : public Node3DGizmo { Vector collision_segments; LocalVector> collision_meshes; + bool collision_meshes_are_snap_source = false; Vector handles; Vector handle_ids; @@ -102,7 +103,10 @@ public: void add_mesh(const Ref &p_mesh, const Ref &p_material = Ref(), const Transform3D &p_xform = Transform3D(), const Ref &p_skin_reference = Ref()); void add_collision_segments(const Vector &p_lines); void add_collision_triangles(const Ref &p_tmesh); + void set_collision_meshes_are_snap_source(bool p_enable) { collision_meshes_are_snap_source = p_enable; } + bool get_collision_meshes_are_snap_source() const { return collision_meshes_are_snap_source; } const LocalVector> &get_collision_meshes() const { return collision_meshes; } + const Vector &get_collision_segments() const { return collision_segments; } void add_unscaled_billboard(const Ref &p_material, real_t p_scale = 1, const Color &p_modulate = Color(1, 1, 1)); void add_handles(const Vector &p_handles, const Ref &p_material, const Vector &p_ids = Vector(), bool p_billboard = false, bool p_secondary = false); void add_solid_box(const Ref &p_material, Vector3 p_size, Vector3 p_position = Vector3(), const Transform3D &p_xform = Transform3D()); diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index cdc0a20606..b81bd4d4bb 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -857,8 +857,42 @@ float Node3DEditorViewport::_min_screen_dist_to_aabb(const AABB &p_aabb, const T return Math::sqrt(dx * dx + dy * dy); } +static bool _node_is_snap_source(Node *p_node, bool p_use_collision) { + if (!p_use_collision) { + return Object::cast_to(p_node); + } + if (Object::cast_to(p_node)) { + return true; + } + Node3D *n3d = Object::cast_to(p_node); + if (!n3d) { + return false; + } + for (const Ref &g : n3d->get_gizmos()) { + Ref seg = g; + if (seg.is_valid() && seg->get_collision_meshes_are_snap_source()) { + return true; + } + } + return false; +} + +static bool _node_has_snap_target(Node *p_node, bool p_use_collision) { + if (_node_is_snap_source(p_node, p_use_collision)) { + return true; + } + for (int i = 0; i < p_node->get_child_count(); i++) { + if (_node_has_snap_target(p_node->get_child(i), p_use_collision)) { + return true; + } + } + return false; +} + bool Node3DEditorViewport::_find_closest_vertex_on_node(const Point2 &p_screen_pos, Node3D *p_node, float &r_closest_screen_dist, Vector3 &r_vertex_world) const { bool found = false; + bool use_collision = Node3DEditor::get_singleton()->is_vertex_snap_use_collision(); + bool walk_collision_segments = use_collision && Object::cast_to(p_node); Transform3D gt = p_node->get_global_transform(); Vector> gizmos = p_node->get_gizmos(); @@ -869,53 +903,72 @@ bool Node3DEditorViewport::_find_closest_vertex_on_node(const Point2 &p_screen_p continue; } - const LocalVector> &meshes = seg->get_collision_meshes(); - for (const Ref &tm : meshes) { - if (tm.is_null() || !tm->is_valid()) { - continue; + if (walk_collision_segments) { + const Vector &segments = seg->get_collision_segments(); + for (int si = 0; si < segments.size(); si++) { + Vector3 world_v = gt.xform(segments[si]); + if (camera->is_position_behind(world_v)) { + continue; + } + Vector2 screen_v = camera->unproject_position(world_v); + float dist = screen_v.distance_to(p_screen_pos); + if (dist < r_closest_screen_dist) { + r_closest_screen_dist = dist; + r_vertex_world = world_v; + found = true; + } } + } - const Vector &bvh = tm->get_bvh(); - const Vector &triangles = tm->get_triangles(); - const Vector &vertices = tm->get_vertices(); - - if (bvh.is_empty()) { - continue; - } - - // Traverse the TriangleMesh BVH, pruning branches whose screen-space - // AABB is farther than the current best. - LocalVector stack; - stack.push_back(bvh.size() - 1); - - while (!stack.is_empty()) { - int node_idx = stack[stack.size() - 1]; - stack.resize(stack.size() - 1); - - const TriangleMesh::BVH &b = bvh[node_idx]; - - if (_min_screen_dist_to_aabb(b.aabb, gt, p_screen_pos) >= r_closest_screen_dist) { + if (!use_collision || seg->get_collision_meshes_are_snap_source()) { + const LocalVector> &meshes = seg->get_collision_meshes(); + for (const Ref &tm : meshes) { + if (tm.is_null() || !tm->is_valid()) { continue; } - if (b.face_index >= 0) { - const TriangleMesh::Triangle &tri = triangles[b.face_index]; - for (int vi = 0; vi < 3; vi++) { - Vector3 world_v = gt.xform(vertices[tri.indices[vi]]); - if (camera->is_position_behind(world_v)) { - continue; - } - Vector2 screen_v = camera->unproject_position(world_v); - float dist = screen_v.distance_to(p_screen_pos); - if (dist < r_closest_screen_dist) { - r_closest_screen_dist = dist; - r_vertex_world = world_v; - found = true; - } + const Vector &bvh = tm->get_bvh(); + const Vector &triangles = tm->get_triangles(); + const Vector &vertices = tm->get_vertices(); + + if (bvh.is_empty()) { + continue; + } + + // Traverse the TriangleMesh BVH, pruning branches whose screen-space + // AABB is farther than the current best. + LocalVector stack; + stack.push_back(bvh.size() - 1); + + while (!stack.is_empty()) { + int node_idx = stack[stack.size() - 1]; + stack.resize(stack.size() - 1); + + const TriangleMesh::BVH &b = bvh[node_idx]; + + if (_min_screen_dist_to_aabb(b.aabb, gt, p_screen_pos) >= r_closest_screen_dist) { + continue; + } + + if (b.face_index >= 0) { + const TriangleMesh::Triangle &tri = triangles[b.face_index]; + for (int vi = 0; vi < 3; vi++) { + Vector3 world_v = gt.xform(vertices[tri.indices[vi]]); + if (camera->is_position_behind(world_v)) { + continue; + } + Vector2 screen_v = camera->unproject_position(world_v); + float dist = screen_v.distance_to(p_screen_pos); + if (dist < r_closest_screen_dist) { + r_closest_screen_dist = dist; + r_vertex_world = world_v; + found = true; + } + } + } else { + stack.push_back(b.left); + stack.push_back(b.right); } - } else { - stack.push_back(b.left); - stack.push_back(b.right); } } } @@ -932,12 +985,14 @@ bool Node3DEditorViewport::_find_closest_vertex_in_scene(const Point2 &p_screen_ Point2 max_pos(p_screen_pos.x + p_threshold, p_screen_pos.y + p_threshold); Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_frustum_query(_build_screen_frustum(min_pos, max_pos)); + bool use_collision = Node3DEditor::get_singleton()->is_vertex_snap_use_collision(); + for (Node3D *spat : nodes_with_gizmos) { if (!spat) { continue; } - if (!Object::cast_to(spat)) { + if (!_node_is_snap_source(spat, use_collision)) { continue; } @@ -1042,18 +1097,6 @@ bool Node3DEditorViewport::_is_vertex_occluded(const Vector3 &p_world_pos, const return false; } -static bool _node_has_geometry(Node *p_node) { - if (Object::cast_to(p_node)) { - return true; - } - for (int i = 0; i < p_node->get_child_count(); i++) { - if (_node_has_geometry(p_node->get_child(i))) { - return true; - } - } - return false; -} - void Node3DEditorViewport::_vertex_snap_update_source(const Point2 &p_screen_pos) { const List &selection = editor_selection->get_top_selected_node_list(); if (selection.is_empty()) { @@ -1068,7 +1111,8 @@ void Node3DEditorViewport::_vertex_snap_update_source(const Point2 &p_screen_pos if (spatial_editor->is_vertex_snap_origin_mode()) { found = _find_closest_vertex_in_scene(p_screen_pos, threshold, vw); } else { - bool selection_has_geometry = false; + bool use_collision = spatial_editor->is_vertex_snap_use_collision(); + bool selection_has_snap_target = false; for (Node *E : selection) { Node3D *sp = Object::cast_to(E); if (!sp) { @@ -1080,11 +1124,11 @@ void Node3DEditorViewport::_vertex_snap_update_source(const Point2 &p_screen_pos while (!descendants.is_empty()) { Node3D *node = descendants[descendants.size() - 1]; descendants.resize(descendants.size() - 1); - if (Object::cast_to(node)) { - selection_has_geometry = true; - } - if (_find_closest_vertex_on_node(p_screen_pos, node, threshold, vw)) { - found = true; + if (_node_is_snap_source(node, use_collision)) { + selection_has_snap_target = true; + if (_find_closest_vertex_on_node(p_screen_pos, node, threshold, vw)) { + found = true; + } } for (int i = 0; i < node->get_child_count(); i++) { Node3D *child = Object::cast_to(node->get_child(i)); @@ -1095,7 +1139,7 @@ void Node3DEditorViewport::_vertex_snap_update_source(const Point2 &p_screen_pos } } - if (!found && !selection_has_geometry) { + if (!found && !selection_has_snap_target) { found = _find_closest_vertex_in_scene(p_screen_pos, threshold, vw); } } @@ -2023,7 +2067,9 @@ void Node3DEditorViewport::_sinput(const Ref &p_event) { } if (k.is_valid()) { - if (!vertex_snap_mode && !vertex_snap_dragging && k->is_pressed() && _edit.mode == TRANSFORM_NONE && ED_IS_SHORTCUT("spatial_editor/vertex_snap", p_event)) { + Ref k_no_shift = k->duplicate(); + k_no_shift->set_shift_pressed(false); + if (!vertex_snap_mode && !vertex_snap_dragging && k->is_pressed() && _edit.mode == TRANSFORM_NONE && ED_IS_SHORTCUT("spatial_editor/vertex_snap", k_no_shift)) { vertex_snap_mode = true; vertex_snap_keycode = k->get_physical_keycode() != Key::NONE ? k->get_physical_keycode() : k->get_keycode(); _disable_follow_mode(); @@ -2074,19 +2120,15 @@ void Node3DEditorViewport::_sinput(const Ref &p_event) { bool has_subgizmos = se && se->gizmo.is_valid() && !se->subgizmos.is_empty(); if (!use_origin_snap) { - if (has_subgizmos) { + bool has_snap_target = false; + for (Node *E : selection) { + if (_node_has_snap_target(E, spatial_editor->is_vertex_snap_use_collision())) { + has_snap_target = true; + break; + } + } + if (!has_snap_target) { use_origin_snap = true; - } else { - bool has_geometry = false; - for (Node *E : selection) { - if (_node_has_geometry(E)) { - has_geometry = true; - break; - } - } - if (!has_geometry) { - use_origin_snap = true; - } } } @@ -7529,6 +7571,7 @@ Dictionary Node3DEditor::get_state() const { d["viewports"] = vpdata; d["vertex_snap_origin_mode"] = vertex_snap_origin_mode; + d["vertex_snap_use_collision"] = vertex_snap_use_collision; d["show_grid"] = view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_GRID)); d["show_origin"] = view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_ORIGIN)); d["fov"] = get_fov(); @@ -7611,6 +7654,14 @@ void Node3DEditor::set_state(const Dictionary &p_state) { transform_menu->get_popup()->set_item_checked(idx_origin, vertex_snap_origin_mode); } + if (d.has("vertex_snap_use_collision")) { + vertex_snap_use_collision = d["vertex_snap_use_collision"]; + int idx_mesh = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH); + int idx_collision = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION); + transform_menu->get_popup()->set_item_checked(idx_mesh, !vertex_snap_use_collision); + transform_menu->get_popup()->set_item_checked(idx_collision, vertex_snap_use_collision); + } + if (d.has("local_coords")) { tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_pressed(d["local_coords"]); update_transform_gizmo(); @@ -7793,6 +7844,10 @@ void Node3DEditor::_update_vertex_snap_tooltips() { vformat(TTR("Hold %s to highlight a vertex on the currently selected node,\nthen drag to move the node and snap it to vertices on neighboring nodes.\n\nFor nodes without a vertex-based representation,\nSnap Origin to Vertex is always used instead."), snap_key)); p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_BASE_ORIGIN), vformat(TTR("Hold %s to highlight another node's vertex,\nthen click to teleport the selected node to the highlighted vertex."), snap_key)); + p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH), + TTR("Snap to vertices of visual meshes.\nHold Shift while vertex snapping to temporarily snap to collision shapes instead.")); + p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION), + TTR("Snap to vertices of collision shapes.\nHold Shift while vertex snapping to temporarily snap to mesh vertices instead.")); } void Node3DEditor::_xform_dialog_action() { @@ -7974,6 +8029,20 @@ void Node3DEditor::_menu_item_pressed(int p_option) { transform_menu->get_popup()->set_item_checked(idx_vertex, false); transform_menu->get_popup()->set_item_checked(idx_origin, true); } break; + case MENU_VERTEX_SNAP_SOURCE_MESH: { + vertex_snap_use_collision = false; + int idx_mesh = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH); + int idx_collision = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION); + transform_menu->get_popup()->set_item_checked(idx_mesh, true); + transform_menu->get_popup()->set_item_checked(idx_collision, false); + } break; + case MENU_VERTEX_SNAP_SOURCE_COLLISION: { + vertex_snap_use_collision = true; + int idx_mesh = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH); + int idx_collision = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION); + transform_menu->get_popup()->set_item_checked(idx_mesh, false); + transform_menu->get_popup()->set_item_checked(idx_collision, true); + } break; case MENU_TRANSFORM_DIALOG: { for (int i = 0; i < 3; i++) { xform_translate[i]->set_text("0"); @@ -10588,6 +10657,11 @@ Node3DEditor::Node3DEditor() { p->add_radio_check_item(TTRC("Snap Vertex to Vertex"), MENU_VERTEX_SNAP_BASE_VERTEX); p->set_item_checked(p->get_item_index(MENU_VERTEX_SNAP_BASE_VERTEX), true); p->add_radio_check_item(TTRC("Snap Origin to Vertex"), MENU_VERTEX_SNAP_BASE_ORIGIN); + + p->add_separator(); + p->add_radio_check_item(TTRC("Snap to Mesh Vertices"), MENU_VERTEX_SNAP_SOURCE_MESH); + p->set_item_checked(p->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH), true); + p->add_radio_check_item(TTRC("Snap to Collision Vertices"), MENU_VERTEX_SNAP_SOURCE_COLLISION); _update_vertex_snap_tooltips(); p->add_separator(); @@ -11090,6 +11164,10 @@ bool Node3DEditor::is_gizmo_visible() const { return gizmo.visible; } +bool Node3DEditor::is_vertex_snap_use_collision() const { + return vertex_snap_use_collision != Input::get_singleton()->is_key_pressed(Key::SHIFT); +} + real_t Node3DEditor::get_translate_snap() const { real_t snap_value = snap_translate_value; if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h index cf0dc3120d..a94238c6f4 100644 --- a/editor/scene/3d/node_3d_editor_plugin.h +++ b/editor/scene/3d/node_3d_editor_plugin.h @@ -740,6 +740,8 @@ private: MENU_RULER, MENU_VERTEX_SNAP_BASE_VERTEX, MENU_VERTEX_SNAP_BASE_ORIGIN, + MENU_VERTEX_SNAP_SOURCE_MESH, + MENU_VERTEX_SNAP_SOURCE_COLLISION, }; Button *tool_button[TOOL_MAX]; @@ -758,6 +760,7 @@ private: bool snap_enabled = false; bool snap_key_enabled = false; bool vertex_snap_origin_mode = false; + bool vertex_snap_use_collision = false; EditorSpinSlider *snap_translate = nullptr; EditorSpinSlider *snap_rotate = nullptr; EditorSpinSlider *snap_scale = nullptr; @@ -944,6 +947,7 @@ public: bool is_preserve_children_transform_enabled() const { return tool_option_button[Node3DEditor::TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed(); } bool is_snap_enabled() const { return snap_enabled ^ snap_key_enabled; } bool is_vertex_snap_origin_mode() const { return vertex_snap_origin_mode; } + bool is_vertex_snap_use_collision() const; real_t get_translate_snap() const; real_t get_rotate_snap() const; real_t get_scale_snap() const; diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index 914937ab37..17ac21b876 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -120,6 +120,7 @@ void CSGShape3D::set_use_collision(bool p_enable) { root_collision_shape.unref(); } notify_property_list_changed(); + update_gizmos(); } bool CSGShape3D::is_using_collision() const { diff --git a/modules/csg/editor/csg_gizmos.cpp b/modules/csg/editor/csg_gizmos.cpp index 945e451345..db3f8b0f3b 100644 --- a/modules/csg/editor/csg_gizmos.cpp +++ b/modules/csg/editor/csg_gizmos.cpp @@ -428,6 +428,10 @@ void CSGShape3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { collision_mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, collision_array); p_gizmo->add_collision_triangles(collision_mesh->generate_triangle_mesh()); + if (cs->is_using_collision() && cs->is_root_shape()) { + p_gizmo->set_collision_meshes_are_snap_source(true); + } + if (p_gizmo->is_selected()) { // Draw a translucent representation of the CSG node Ref mesh = memnew(ArrayMesh);