Add vertex snap to collision shape vertices in 3D editor
This commit is contained in:
1 parent
1aabcb9e9b
commit
c375f0b6b3
6 files changed
+165
-73
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user