├── .gitignore ├── .gitattributes ├── screenshots ├── icon.png ├── scene_to_mesh_menu.png ├── scene_to_mesh_test_mesh.png └── scene_to_mesh_test_scene.png ├── addons └── scenetomeshconverter │ ├── plugin.cfg │ ├── plugin_button.tscn │ ├── plugin.gd │ └── plugin_button.gd ├── project.godot ├── README.md ├── icon.svg ├── LICENSE.md └── tests ├── test_scales.tscn └── test.tscn /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | *.import 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /screenshots/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Magodra/SceneToMeshConverter/HEAD/screenshots/icon.png -------------------------------------------------------------------------------- /screenshots/scene_to_mesh_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Magodra/SceneToMeshConverter/HEAD/screenshots/scene_to_mesh_menu.png -------------------------------------------------------------------------------- /screenshots/scene_to_mesh_test_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Magodra/SceneToMeshConverter/HEAD/screenshots/scene_to_mesh_test_mesh.png -------------------------------------------------------------------------------- /screenshots/scene_to_mesh_test_scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Magodra/SceneToMeshConverter/HEAD/screenshots/scene_to_mesh_test_scene.png -------------------------------------------------------------------------------- /addons/scenetomeshconverter/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Scene to Mesh Converter" 4 | description="Plugin to convert Scenes to MeshInstance" 5 | author="Anders Reggestad" 6 | version="1.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/scenetomeshconverter/plugin_button.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cnsthwdybcdw7"] 2 | 3 | [ext_resource type="Script" path="res://addons/scenetomeshconverter/plugin_button.gd" id="1_bbpyn"] 4 | 5 | [node name="ConvertSceneToMeshButton" type="Button"] 6 | offset_right = 185.0 7 | offset_bottom = 31.0 8 | size_flags_horizontal = 0 9 | size_flags_vertical = 0 10 | text = "Convert scene to mesh" 11 | flat = true 12 | script = ExtResource("1_bbpyn") 13 | 14 | [connection signal="pressed" from="." to="." method="_on_convert_scene_to_mesh_button_pressed"] 15 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Scene to mesh converter" 14 | config/features=PackedStringArray("4.3", "Forward Plus") 15 | config/icon="res://icon.svg" 16 | 17 | [editor_plugins] 18 | 19 | enabled=PackedStringArray("res://addons/scenetomeshconverter/plugin.cfg") 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scene to mesh converter 2 | 3 | ![Scene to mesh converter](screenshots/icon.png) 4 | 5 | The **Scene to mesh converter** is a tool that convert a hierarchy of Node3D nodes into a MeshInstance3D. 6 | 7 | # How does it work? 8 | 1. Select a node to convert. 9 | 2. Push the **Convert scene to mesh** button on the Spatial editor menu. 10 | 3. The scene is replaced with a MeshInstace3D. 11 | 12 | > [!NOTE] 13 | > If you would like to keep the scene tree, please make a copy before converting. Undo/Redo might still have issues. 14 | 15 | ## Screenshots 16 | 17 | ![Convert scene to mesh, scene selected](screenshots/scene_to_mesh_test_scene.png) 18 | ![Convert scene to mesh, scene converted](screenshots/scene_to_mesh_test_mesh.png) 19 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023-2024 Anders Reggestad, Timotej Ponek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/test_scales.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://cdly2fsek0jqk"] 2 | 3 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_j0f5s"] 4 | albedo_color = Color(0.129412, 1, 0.137255, 1) 5 | 6 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ui7nd"] 7 | albedo_color = Color(1, 0.117647, 0.313726, 1) 8 | 9 | [node name="Test" type="Node3D"] 10 | 11 | [node name="Scene" type="Node3D" parent="."] 12 | 13 | [node name="CSGCombiner3D" type="CSGCombiner3D" parent="Scene"] 14 | transform = Transform3D(0.765, 0, 0, 0, 0.558747, -0.522519, 0, 0.522519, 0.558747, -0.993009, 0, 0) 15 | 16 | [node name="CSGBox3D" type="CSGBox3D" parent="Scene/CSGCombiner3D"] 17 | material = SubResource("StandardMaterial3D_j0f5s") 18 | 19 | [node name="CSGCylinder3D" type="CSGCylinder3D" parent="Scene/CSGCombiner3D"] 20 | operation = 2 21 | radius = 0.259 22 | height = 1.0 23 | sides = 13 24 | 25 | [node name="Node3D" type="Node3D" parent="Scene"] 26 | transform = Transform3D(1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, 1.32, -0.195, 0) 27 | 28 | [node name="CSGBox3D" type="CSGBox3D" parent="Scene/Node3D"] 29 | transform = Transform3D(-1.22268, 0.662572, -0.0978825, -0.430672, -1.69041, 0.545189, -0.0978826, -0.838752, -1.17609, 0, 0, 0) 30 | material = SubResource("StandardMaterial3D_ui7nd") 31 | -------------------------------------------------------------------------------- /tests/test.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://e68yulkqigum"] 2 | 3 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ui7nd"] 4 | albedo_color = Color(1, 0.117647, 0.313726, 1) 5 | 6 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_j0f5s"] 7 | albedo_color = Color(0.129412, 1, 0.137255, 1) 8 | 9 | [node name="Test" type="Node3D"] 10 | 11 | [node name="Scene" type="Node3D" parent="."] 12 | 13 | [node name="Node3D" type="Node3D" parent="Scene"] 14 | transform = Transform3D(0.784701, 0, 0.619875, 0, 1, 0, -0.619875, 0, 0.784701, 1.18951, 0, 0) 15 | 16 | [node name="CSGBox3D" type="CSGBox3D" parent="Scene/Node3D"] 17 | transform = Transform3D(0.940521, -0.331286, 0.0752943, 0.331286, 0.845206, -0.419376, 0.0752943, 0.419376, 0.904685, 0, 0, 0) 18 | material = SubResource("StandardMaterial3D_ui7nd") 19 | 20 | [node name="CSGCombiner3D" type="CSGCombiner3D" parent="Scene"] 21 | transform = Transform3D(1, 0, 0, 0, 0.730389, -0.683032, 0, 0.683032, 0.730389, -0.993009, 0, 0) 22 | 23 | [node name="CSGBox3D" type="CSGBox3D" parent="Scene/CSGCombiner3D"] 24 | material = SubResource("StandardMaterial3D_j0f5s") 25 | 26 | [node name="CSGCylinder3D" type="CSGCylinder3D" parent="Scene/CSGCombiner3D"] 27 | operation = 2 28 | radius = 0.259 29 | height = 1.0 30 | sides = 13 31 | -------------------------------------------------------------------------------- /addons/scenetomeshconverter/plugin.gd: -------------------------------------------------------------------------------- 1 | ## Copyright (c) 2023 Anders Reggestad 2 | ## 3 | ## Licensed under the MIT license, see LICENSE.txt 4 | ## in the project root folder for more information. 5 | @tool 6 | extends EditorPlugin 7 | ## 8 | ## A plugin that convert scenes to a mesh instance. Since there are multiple 9 | ## ways meshes are repesented, this plugin use the GLTF from scene to access 10 | ## all meshes in one uniform way. 11 | ## 12 | 13 | ## Referense to the button in the Spatial editor menu 14 | var plugin_button : SceneToMeshConverterButton 15 | 16 | 17 | ## Connect the plugin to the editor when the plugin entering the tree. 18 | func _enter_tree(): 19 | plugin_button = preload("res://addons/scenetomeshconverter/plugin_button.tscn").instantiate() 20 | plugin_button.undo_redo = get_undo_redo() 21 | add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, plugin_button) 22 | 23 | # By default hide the button 24 | plugin_button.hide() 25 | 26 | add_tool_menu_item("Convert scene to mesh", plugin_button.convert_node_to_meshinstance ) 27 | 28 | # Monitor when objects selected in tree changes 29 | get_editor_interface().get_selection().selection_changed.connect(self.selection_changed) 30 | 31 | 32 | ## Disconnect the plugin elements when the plugin exit the tree. 33 | func _exit_tree(): 34 | 35 | remove_tool_menu_item("Convert scene to mesh") 36 | 37 | remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, plugin_button) 38 | 39 | if plugin_button: 40 | plugin_button.free() 41 | 42 | 43 | ## Montor the selection changed, and display the button if the type is a node 44 | ## that can be converted to a mesh. 45 | func selection_changed() -> void: 46 | var selection = get_editor_interface().get_selection().get_selected_nodes() 47 | 48 | var can_convert = selection.size() == 1 and selection[0] is Node3D 49 | 50 | # If selected object in tree is csg 51 | if can_convert: 52 | var root : Node = get_tree().get_edited_scene_root() 53 | var node : Node3D = selection[0] 54 | plugin_button.show_button(root, node) 55 | else: 56 | plugin_button.hide_button() 57 | -------------------------------------------------------------------------------- /addons/scenetomeshconverter/plugin_button.gd: -------------------------------------------------------------------------------- 1 | ## Copyright (c) 2023-2024 Anders Reggestad 2 | ## 3 | ## Licensed under the MIT license, see LICENSE.txt 4 | ## in the project root folder for more information. 5 | @tool 6 | class_name SceneToMeshConverterButton 7 | extends Button 8 | ## 9 | ## Class used by the Scene to Mesh Converter plugin to convert scenes to meshes. 10 | ## 11 | 12 | var root :Node 13 | var node :Node3D 14 | var undo_redo : EditorUndoRedoManager 15 | 16 | 17 | ## Show button in UI, untoggled 18 | func show_button(root: Node, node :Node3D): 19 | self.root = root 20 | self.node = node 21 | show() 22 | 23 | 24 | ## Hide button in UI, untoggled 25 | func hide_button(): 26 | hide() 27 | 28 | 29 | ## Callback from the button in the CONTAINER_SPATIAL_EDITOR_MENU 30 | func _on_convert_scene_to_mesh_button_pressed(): 31 | convert_node_to_meshinstance() 32 | 33 | ## Perform the conversion of the scene to a mesh instance. 34 | func convert_node_to_meshinstance(): 35 | var mesh_instance = MeshInstance3D.new() 36 | var mesh = ArrayMesh.new() 37 | 38 | mesh_instance.mesh = mesh 39 | 40 | # Since there are so many different ways meshes can be stored ArrayMesh, CSGShape3D, ... 41 | # we first export to GLTF and then extracte the surfaces from that 42 | var gltf_document := GLTFDocument.new() 43 | var gltf_state := GLTFState.new() 44 | 45 | # Store the node transform, cleare the node transform before generating the gltf 46 | # Restoring the transform to maintain data for undo operations. 47 | var node_transform = node.global_transform 48 | node.transform = Transform3D() 49 | gltf_document.append_from_scene(node, gltf_state) 50 | node.global_transform = node_transform 51 | 52 | var xform : Transform3D = Transform3D() 53 | extract_meshes(mesh, gltf_state, gltf_state.root_nodes, xform) 54 | 55 | # Replace the scene with the mesh instance node. 56 | var node_name = node.name 57 | var parent = node.get_parent() 58 | var idx = node.get_index() 59 | 60 | undo_redo.create_action("Convert scene to mesh") 61 | undo_redo.add_do_method(parent, "add_child", mesh_instance) 62 | undo_redo.add_do_method(self, "set_undo_owner", mesh_instance, root) 63 | undo_redo.add_undo_method(parent, "remove_child", mesh_instance) 64 | 65 | undo_redo.add_do_method(parent, "remove_child", node) 66 | undo_redo.add_do_method(mesh_instance, "set_name", node_name) 67 | undo_redo.add_undo_method(parent, "add_child", node) 68 | undo_redo.add_undo_method(self, "set_undo_owner", node, root) 69 | undo_redo.commit_action() 70 | 71 | # Debug in running scene 72 | # node.get_parent().add_child(mesh_instance) 73 | # node.get_parent().remove_child(node) 74 | 75 | mesh_instance.owner = root 76 | mesh_instance.global_transform = node_transform 77 | mesh_instance.name = node_name 78 | 79 | 80 | ## Function used to set owner of a tree in undo actions, to include into scene again 81 | func set_undo_owner(node, root): 82 | node.set_owner(root) 83 | for child in node.get_children(): 84 | set_undo_owner(child, root) 85 | 86 | 87 | ## Locate and extract meshes recursive 88 | func extract_meshes(mesh : ArrayMesh, gltf_state : GLTFState, node_idxes : PackedInt32Array, parent_xform : Transform3D): 89 | for idx in range (0, node_idxes.size()): 90 | var node_idx = node_idxes[idx] 91 | var gltf_node := gltf_state.get_nodes()[node_idx] 92 | 93 | # GLTF specivication state T * R * S for transformation 94 | var node_basis = Basis(gltf_node.rotation)*Basis.from_scale(gltf_node.scale) 95 | var node_xfrom = Transform3D(node_basis, gltf_node.position) 96 | var xform = parent_xform*node_xfrom 97 | 98 | if gltf_node.mesh != -1: 99 | add_mesh(mesh, gltf_state, gltf_node.mesh, xform) 100 | 101 | extract_meshes(mesh, gltf_state, gltf_node.children, xform) 102 | 103 | 104 | ## Add meshes from the GLTF to the mesh 105 | func add_mesh(mesh : ArrayMesh, gltf_state : GLTFState, mesh_idx : int, xform : Transform3D): 106 | var gltf_mesh = gltf_state.get_meshes()[mesh_idx] 107 | 108 | for idx in range (0,gltf_mesh.mesh.get_surface_count()): 109 | var add_idx = mesh.get_surface_count() 110 | var arrays = gltf_mesh.mesh.get_surface_arrays(idx) 111 | 112 | # Transform vertexes 113 | for vertex_idx in range (0, arrays[Mesh.ARRAY_VERTEX].size()): 114 | var vec = xform*arrays[Mesh.ARRAY_VERTEX][vertex_idx] 115 | arrays[Mesh.ARRAY_VERTEX][vertex_idx] = vec 116 | 117 | # Transform normals using the basis (this fixes weird shading present if mesh/node has not default 0,0,0 rotation) 118 | if arrays[Mesh.ARRAY_NORMAL].size() > 0: 119 | for normal_idx in range(0, arrays[Mesh.ARRAY_NORMAL].size()): 120 | arrays[Mesh.ARRAY_NORMAL][normal_idx] = xform.basis * arrays[Mesh.ARRAY_NORMAL][normal_idx] 121 | arrays[Mesh.ARRAY_NORMAL][normal_idx] = arrays[Mesh.ARRAY_NORMAL][normal_idx].normalized() 122 | 123 | # If mesh/node has negative scale, fix "flipped faces". 124 | if xform.basis.determinant() < 0: 125 | if gltf_mesh.mesh.get_surface_primitive_type(idx) == Mesh.PRIMITIVE_TRIANGLES: 126 | # flip the winding order of triangles 127 | var indices = arrays[Mesh.ARRAY_INDEX] 128 | 129 | # If surface isn't using indices, create them so that we can use them to flipp the faces. 130 | if indices == null or indices.size() == 0: 131 | var vertices = arrays[Mesh.ARRAY_VERTEX] 132 | indices = PackedInt32Array() 133 | arrays[Mesh.ARRAY_INDEX] = indices 134 | for i in range(0, vertices.size()): 135 | indices.append(i) 136 | 137 | # Flipp the faces by rearranging the indices. 138 | if indices and indices.size() > 0: 139 | for i in range(0, indices.size(), 3): 140 | var temp = indices[i + 1] 141 | indices[i + 1] = indices[i + 2] 142 | indices[i + 2] = temp 143 | else: 144 | print("Flipping of primitive type ", gltf_mesh.mesh.get_surface_primitive_type(idx), " not supported!") 145 | 146 | 147 | mesh.add_surface_from_arrays( 148 | gltf_mesh.mesh.get_surface_primitive_type(idx), 149 | arrays, 150 | ) 151 | mesh.surface_set_material(add_idx, gltf_mesh.mesh.get_surface_material(idx)) 152 | mesh.surface_set_name(add_idx, gltf_mesh.mesh.get_surface_name(idx)) 153 | #TODO blend shapes, lods, lightmap_size_hint,++? 154 | --------------------------------------------------------------------------------