From 6455810db88a70e02f6633f3b46101b548d96b82 Mon Sep 17 00:00:00 2001 From: smix8 <52464204+smix8@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:38:12 +0200 Subject: [PATCH] Add CSG options to bake to static mesh and collision shape Adds API to bake a CSG root node operation to either a static ArrayMesh or a ConcavePolygonShape3D physics collision shape. Adds menu options to the editor plugin when selecting a CSG root node to add baked sibling nodes. --- modules/csg/csg_shape.cpp | 61 ++++++++--- modules/csg/csg_shape.h | 5 + modules/csg/doc_classes/CSGShape3D.xml | 19 +++- modules/csg/editor/csg_gizmos.cpp | 146 +++++++++++++++++++++++++ modules/csg/editor/csg_gizmos.h | 36 ++++++ 5 files changed, 249 insertions(+), 18 deletions(-) diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index 296cda627aa..765c42ec1d1 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -460,27 +460,31 @@ void CSGShape3D::_update_shape() { _update_collision_faces(); } -void CSGShape3D::_update_collision_faces() { - if (use_collision && is_root_shape() && root_collision_shape.is_valid()) { - CSGBrush *n = _get_brush(); - ERR_FAIL_NULL_MSG(n, "Cannot get CSGBrush."); - Vector physics_faces; - physics_faces.resize(n->faces.size() * 3); - Vector3 *physicsw = physics_faces.ptrw(); +Vector CSGShape3D::_get_brush_collision_faces() { + Vector collision_faces; + CSGBrush *n = _get_brush(); + ERR_FAIL_NULL_V_MSG(n, collision_faces, "Cannot get CSGBrush."); + collision_faces.resize(n->faces.size() * 3); + Vector3 *collision_faces_ptrw = collision_faces.ptrw(); - for (int i = 0; i < n->faces.size(); i++) { - int order[3] = { 0, 1, 2 }; + for (int i = 0; i < n->faces.size(); i++) { + int order[3] = { 0, 1, 2 }; - if (n->faces[i].invert) { - SWAP(order[1], order[2]); - } - - physicsw[i * 3 + 0] = n->faces[i].vertices[order[0]]; - physicsw[i * 3 + 1] = n->faces[i].vertices[order[1]]; - physicsw[i * 3 + 2] = n->faces[i].vertices[order[2]]; + if (n->faces[i].invert) { + SWAP(order[1], order[2]); } - root_collision_shape->set_faces(physics_faces); + collision_faces_ptrw[i * 3 + 0] = n->faces[i].vertices[order[0]]; + collision_faces_ptrw[i * 3 + 1] = n->faces[i].vertices[order[1]]; + collision_faces_ptrw[i * 3 + 2] = n->faces[i].vertices[order[2]]; + } + + return collision_faces; +} + +void CSGShape3D::_update_collision_faces() { + if (use_collision && is_root_shape() && root_collision_shape.is_valid()) { + root_collision_shape->set_faces(_get_brush_collision_faces()); if (_is_debug_collision_shape_visible()) { _update_debug_collision_shape(); @@ -488,6 +492,26 @@ void CSGShape3D::_update_collision_faces() { } } +Ref CSGShape3D::bake_static_mesh() { + Ref baked_mesh; + if (is_root_shape() && root_mesh.is_valid()) { + baked_mesh = root_mesh; + } + return baked_mesh; +} + +Ref CSGShape3D::bake_collision_shape() { + Ref baked_collision_shape; + if (is_root_shape() && root_collision_shape.is_valid()) { + baked_collision_shape.instantiate(); + baked_collision_shape->set_faces(root_collision_shape->get_faces()); + } else if (is_root_shape()) { + baked_collision_shape.instantiate(); + baked_collision_shape->set_faces(_get_brush_collision_faces()); + } + return baked_collision_shape; +} + bool CSGShape3D::_is_debug_collision_shape_visible() { return is_inside_tree() && (get_tree()->is_debugging_collisions_hint() || Engine::get_singleton()->is_editor_hint()); } @@ -704,6 +728,9 @@ void CSGShape3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_meshes"), &CSGShape3D::get_meshes); + ClassDB::bind_method(D_METHOD("bake_static_mesh"), &CSGShape3D::bake_static_mesh); + ClassDB::bind_method(D_METHOD("bake_collision_shape"), &CSGShape3D::bake_collision_shape); + ADD_PROPERTY(PropertyInfo(Variant::INT, "operation", PROPERTY_HINT_ENUM, "Union,Intersection,Subtraction"), "set_operation", "get_operation"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "snap", PROPERTY_HINT_RANGE, "0.000001,1,0.000001,suffix:m"), "set_snap", "get_snap"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "calculate_tangents"), "set_calculate_tangents", "is_calculating_tangents"); diff --git a/modules/csg/csg_shape.h b/modules/csg/csg_shape.h index bb7c8be4315..8f23ae2f9e2 100644 --- a/modules/csg/csg_shape.h +++ b/modules/csg/csg_shape.h @@ -113,6 +113,7 @@ private: void _update_debug_collision_shape(); void _clear_debug_collision_shape(); void _on_transform_changed(); + Vector _get_brush_collision_faces(); protected: void _notification(int p_what); @@ -161,6 +162,10 @@ public: bool is_calculating_tangents() const; bool is_root_shape() const; + + Ref bake_static_mesh(); + Ref bake_collision_shape(); + CSGShape3D(); ~CSGShape3D(); }; diff --git a/modules/csg/doc_classes/CSGShape3D.xml b/modules/csg/doc_classes/CSGShape3D.xml index f9017e47c74..ac62d8dd83d 100644 --- a/modules/csg/doc_classes/CSGShape3D.xml +++ b/modules/csg/doc_classes/CSGShape3D.xml @@ -5,12 +5,29 @@ This is the CSG base class that provides CSG operation support to the various CSG nodes in Godot. - [b]Note:[/b] CSG nodes are intended to be used for level prototyping. Creating CSG nodes has a significant CPU cost compared to creating a [MeshInstance3D] with a [PrimitiveMesh]. Moving a CSG node within another CSG node also has a significant CPU cost, so it should be avoided during gameplay. + [b]Performance:[/b] CSG nodes are only intended for prototyping as they have a significant CPU performance cost. + Consider baking final CSG operation results into static geometry that replaces the CSG nodes. + Individual CSG root node results can be baked to nodes with static resources with the editor menu that appears when a CSG root node is selected. + Individual CSG root nodes can also be baked to static resources with scripts by calling [method bake_static_mesh] for the visual mesh or [method bake_collision_shape] for the physics collision. + Entire scenes of CSG nodes can be baked to static geometry and exported with the editor gltf scene exporter. $DOCS_URL/tutorials/3d/csg_tools.html + + + + Returns a baked physics [ConcavePolygonShape3D] of this node's CSG operation result. Returns an empty shape if the node is not a CSG root node or has no valid geometry. + [b]Performance:[/b] If the CSG operation results in a very detailed geometry with many faces physics performance will be very slow. Concave shapes should in general only be used for static level geometry and not with dynamic objects that are moving. + + + + + + Returns a baked static [ArrayMesh] of this node's CSG operation result. Materials from involved CSG nodes are added as extra mesh surfaces. Returns an empty mesh if the node is not a CSG root node or has no valid geometry. + + diff --git a/modules/csg/editor/csg_gizmos.cpp b/modules/csg/editor/csg_gizmos.cpp index ea7b6d225e8..72676f4a40c 100644 --- a/modules/csg/editor/csg_gizmos.cpp +++ b/modules/csg/editor/csg_gizmos.cpp @@ -38,6 +38,135 @@ #include "editor/plugins/gizmos/gizmo_3d_helper.h" #include "editor/plugins/node_3d_editor_plugin.h" #include "scene/3d/camera_3d.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/physics/collision_shape_3d.h" +#include "scene/3d/physics/static_body_3d.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/menu_button.h" + +void CSGShapeEditor::_node_removed(Node *p_node) { + if (p_node == node) { + node = nullptr; + options->hide(); + } +} + +void CSGShapeEditor::edit(CSGShape3D *p_csg_shape) { + node = p_csg_shape; + if (node) { + options->show(); + } else { + options->hide(); + } +} + +void CSGShapeEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + options->set_icon(get_editor_theme_icon(SNAME("CSGCombiner3D"))); + } break; + } +} + +void CSGShapeEditor::_menu_option(int p_option) { + Array meshes = node->get_meshes(); + if (meshes.is_empty()) { + err_dialog->set_text(TTR("CSG operation returned an empty array.")); + err_dialog->popup_centered(); + return; + } + + switch (p_option) { + case MENU_OPTION_BAKE_MESH_INSTANCE: { + _create_baked_mesh_instance(); + } break; + case MENU_OPTION_BAKE_COLLISION_SHAPE: { + _create_baked_collision_shape(); + } break; + } +} + +void CSGShapeEditor::_create_baked_mesh_instance() { + if (node == get_tree()->get_edited_scene_root()) { + err_dialog->set_text(TTR("Can not add a baked mesh as sibling for the scene root.\nMove the CSG root node below a parent node.")); + err_dialog->popup_centered(); + return; + } + + Ref mesh = node->bake_static_mesh(); + if (mesh.is_null()) { + err_dialog->set_text(TTR("CSG operation returned an empty mesh.")); + err_dialog->popup_centered(); + return; + } + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Create baked CSGShape3D Mesh Instance")); + + Node *owner = get_tree()->get_edited_scene_root(); + + MeshInstance3D *mi = memnew(MeshInstance3D); + mi->set_mesh(mesh); + mi->set_name("CSGBakedMeshInstance3D"); + mi->set_transform(node->get_transform()); + ur->add_do_method(node, "add_sibling", mi, true); + ur->add_do_method(mi, "set_owner", owner); + ur->add_do_method(Node3DEditor::get_singleton(), SceneStringName(_request_gizmo), mi); + + ur->add_do_reference(mi); + ur->add_undo_method(node->get_parent(), "remove_child", mi); + + ur->commit_action(); +} + +void CSGShapeEditor::_create_baked_collision_shape() { + if (node == get_tree()->get_edited_scene_root()) { + err_dialog->set_text(TTR("Can not add a baked collision shape as sibling for the scene root.\nMove the CSG root node below a parent node.")); + err_dialog->popup_centered(); + return; + } + + Ref shape = node->bake_collision_shape(); + if (shape.is_null()) { + err_dialog->set_text(TTR("CSG operation returned an empty shape.")); + err_dialog->popup_centered(); + return; + } + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Create baked CSGShape3D Collision Shape")); + + Node *owner = get_tree()->get_edited_scene_root(); + + CollisionShape3D *cshape = memnew(CollisionShape3D); + cshape->set_shape(shape); + cshape->set_name("CSGBakedCollisionShape3D"); + cshape->set_transform(node->get_transform()); + ur->add_do_method(node, "add_sibling", cshape, true); + ur->add_do_method(cshape, "set_owner", owner); + ur->add_do_method(Node3DEditor::get_singleton(), SceneStringName(_request_gizmo), cshape); + + ur->add_do_reference(cshape); + ur->add_undo_method(node->get_parent(), "remove_child", cshape); + + ur->commit_action(); +} + +CSGShapeEditor::CSGShapeEditor() { + options = memnew(MenuButton); + options->hide(); + options->set_text(TTR("CSG")); + options->set_switch_on_hover(true); + Node3DEditor::get_singleton()->add_control_to_menu_panel(options); + + options->get_popup()->add_item(TTR("Bake Mesh Instance"), MENU_OPTION_BAKE_MESH_INSTANCE); + options->get_popup()->add_item(TTR("Bake Collision Shape"), MENU_OPTION_BAKE_COLLISION_SHAPE); + + options->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &CSGShapeEditor::_menu_option)); + + err_dialog = memnew(AcceptDialog); + add_child(err_dialog); +} /////////// @@ -393,9 +522,26 @@ void CSGShape3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { } } +void EditorPluginCSG::edit(Object *p_object) { + CSGShape3D *csg_shape = Object::cast_to(p_object); + if (csg_shape && csg_shape->is_root_shape()) { + csg_shape_editor->edit(csg_shape); + } else { + csg_shape_editor->edit(nullptr); + } +} + +bool EditorPluginCSG::handles(Object *p_object) const { + CSGShape3D *csg_shape = Object::cast_to(p_object); + return csg_shape && csg_shape->is_root_shape(); +} + EditorPluginCSG::EditorPluginCSG() { Ref gizmo_plugin = Ref(memnew(CSGShape3DGizmoPlugin)); Node3DEditor::get_singleton()->add_gizmo_plugin(gizmo_plugin); + + csg_shape_editor = memnew(CSGShapeEditor); + EditorNode::get_singleton()->get_main_screen_control()->add_child(csg_shape_editor); } #endif // TOOLS_ENABLED diff --git a/modules/csg/editor/csg_gizmos.h b/modules/csg/editor/csg_gizmos.h index de19b33e7da..c562fe9fe78 100644 --- a/modules/csg/editor/csg_gizmos.h +++ b/modules/csg/editor/csg_gizmos.h @@ -37,8 +37,11 @@ #include "editor/plugins/editor_plugin.h" #include "editor/plugins/node_3d_editor_gizmos.h" +#include "scene/gui/control.h" +class AcceptDialog; class Gizmo3DHelper; +class MenuButton; class CSGShape3DGizmoPlugin : public EditorNode3DGizmoPlugin { GDCLASS(CSGShape3DGizmoPlugin, EditorNode3DGizmoPlugin); @@ -62,10 +65,43 @@ public: ~CSGShape3DGizmoPlugin(); }; +class CSGShapeEditor : public Control { + GDCLASS(CSGShapeEditor, Control); + + enum Menu { + MENU_OPTION_BAKE_MESH_INSTANCE, + MENU_OPTION_BAKE_COLLISION_SHAPE, + }; + + CSGShape3D *node = nullptr; + MenuButton *options = nullptr; + AcceptDialog *err_dialog = nullptr; + + void _menu_option(int p_option); + + void _create_baked_mesh_instance(); + void _create_baked_collision_shape(); + +protected: + void _node_removed(Node *p_node); + + void _notification(int p_what); + +public: + void edit(CSGShape3D *p_csg_shape); + CSGShapeEditor(); +}; + class EditorPluginCSG : public EditorPlugin { GDCLASS(EditorPluginCSG, EditorPlugin); + CSGShapeEditor *csg_shape_editor = nullptr; + public: + virtual String get_name() const override { return "CSGShape3D"; } + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + EditorPluginCSG(); };