Add vertex snap to collision shape vertices in 3D editor

This commit is contained in:
ryevdokimov
2026-03-26 20:26:16 -04:00
parent 1aabcb9e9b
commit c375f0b6b3
6 changed files with 166 additions and 74 deletions
@@ -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();
+4
View File
@@ -58,6 +58,7 @@ class EditorNode3DGizmo : public Node3DGizmo {
Vector<Vector3> collision_segments;
LocalVector<Ref<TriangleMesh>> collision_meshes;
bool collision_meshes_are_snap_source = false;
Vector<Vector3> handles;
Vector<int> handle_ids;
@@ -102,7 +103,10 @@ public:
void add_mesh(const Ref<Mesh> &p_mesh, const Ref<Material> &p_material = Ref<Material>(), const Transform3D &p_xform = Transform3D(), const Ref<SkinReference> &p_skin_reference = Ref<SkinReference>());
void add_collision_segments(const Vector<Vector3> &p_lines);
void add_collision_triangles(const Ref<TriangleMesh> &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<Ref<TriangleMesh>> &get_collision_meshes() const { return collision_meshes; }
const Vector<Vector3> &get_collision_segments() const { return collision_segments; }
void add_unscaled_billboard(const Ref<Material> &p_material, real_t p_scale = 1, const Color &p_modulate = Color(1, 1, 1));
void add_handles(const Vector<Vector3> &p_handles, const Ref<Material> &p_material, const Vector<int> &p_ids = Vector<int>(), bool p_billboard = false, bool p_secondary = false);
void add_solid_box(const Ref<Material> &p_material, Vector3 p_size, Vector3 p_position = Vector3(), const Transform3D &p_xform = Transform3D());
+152 -74
View File
@@ -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<GeometryInstance3D>(p_node);
}
if (Object::cast_to<CollisionShape3D>(p_node)) {
return true;
}
Node3D *n3d = Object::cast_to<Node3D>(p_node);
if (!n3d) {
return false;
}
for (const Ref<Node3DGizmo> &g : n3d->get_gizmos()) {
Ref<EditorNode3DGizmo> 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<CollisionShape3D>(p_node);
Transform3D gt = p_node->get_global_transform();
Vector<Ref<Node3DGizmo>> gizmos = p_node->get_gizmos();
@@ -869,53 +903,72 @@ bool Node3DEditorViewport::_find_closest_vertex_on_node(const Point2 &p_screen_p
continue;
}
const LocalVector<Ref<TriangleMesh>> &meshes = seg->get_collision_meshes();
for (const Ref<TriangleMesh> &tm : meshes) {
if (tm.is_null() || !tm->is_valid()) {
continue;
if (walk_collision_segments) {
const Vector<Vector3> &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<TriangleMesh::BVH> &bvh = tm->get_bvh();
const Vector<TriangleMesh::Triangle> &triangles = tm->get_triangles();
const Vector<Vector3> &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<int> 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<Ref<TriangleMesh>> &meshes = seg->get_collision_meshes();
for (const Ref<TriangleMesh> &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<TriangleMesh::BVH> &bvh = tm->get_bvh();
const Vector<TriangleMesh::Triangle> &triangles = tm->get_triangles();
const Vector<Vector3> &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<int> 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<Node3D *> 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<GeometryInstance3D>(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<GeometryInstance3D>(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<Node *> &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<Node3D>(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<GeometryInstance3D>(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<Node3D>(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<InputEvent> &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<InputEventKey> 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<InputEvent> &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)) {
+4
View File
@@ -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;
+1
View File
@@ -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 {
+4
View File
@@ -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<ArrayMesh> mesh = memnew(ArrayMesh);