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.
This commit is contained in:
smix8 2024-06-16 21:38:12 +02:00
parent eb20a68b32
commit 6455810db8
5 changed files with 249 additions and 18 deletions

View File

@ -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<Vector3> physics_faces;
physics_faces.resize(n->faces.size() * 3);
Vector3 *physicsw = physics_faces.ptrw();
Vector<Vector3> CSGShape3D::_get_brush_collision_faces() {
Vector<Vector3> 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<ArrayMesh> CSGShape3D::bake_static_mesh() {
Ref<ArrayMesh> baked_mesh;
if (is_root_shape() && root_mesh.is_valid()) {
baked_mesh = root_mesh;
}
return baked_mesh;
}
Ref<ConcavePolygonShape3D> CSGShape3D::bake_collision_shape() {
Ref<ConcavePolygonShape3D> 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");

View File

@ -113,6 +113,7 @@ private:
void _update_debug_collision_shape();
void _clear_debug_collision_shape();
void _on_transform_changed();
Vector<Vector3> _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<ArrayMesh> bake_static_mesh();
Ref<ConcavePolygonShape3D> bake_collision_shape();
CSGShape3D();
~CSGShape3D();
};

View File

@ -5,12 +5,29 @@
</brief_description>
<description>
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.
</description>
<tutorials>
<link title="Prototyping levels with CSG">$DOCS_URL/tutorials/3d/csg_tools.html</link>
</tutorials>
<methods>
<method name="bake_collision_shape">
<return type="ConcavePolygonShape3D" />
<description>
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.
</description>
</method>
<method name="bake_static_mesh">
<return type="ArrayMesh" />
<description>
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.
</description>
</method>
<method name="get_collision_layer_value" qualifiers="const">
<return type="bool" />
<param index="0" name="layer_number" type="int" />

View File

@ -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<ArrayMesh> 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<Shape3D> 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<CSGShape3D>(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<CSGShape3D>(p_object);
return csg_shape && csg_shape->is_root_shape();
}
EditorPluginCSG::EditorPluginCSG() {
Ref<CSGShape3DGizmoPlugin> gizmo_plugin = Ref<CSGShape3DGizmoPlugin>(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

View File

@ -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();
};