diff --git a/doc/classes/CullInstance.xml b/doc/classes/CullInstance.xml index ade317262..c3b6bdfd9 100644 --- a/doc/classes/CullInstance.xml +++ b/doc/classes/CullInstance.xml @@ -17,7 +17,8 @@ This allows fine control over the mesh merging feature in the [RoomManager]. - Setting this option to [code]false[/code] can be used to prevent an instance being merged. + Setting this option to [code]false[/code] can be used to prevent an instance being merged. When set to [code]true[/code] (the default), merging will be determined by [member Spatial.merging_mode]. + [i]Deprecated.[/i] This property has been deprecated and is only included for backward compatibility. Please use [member Spatial.merging_mode] instead. When set to [code]0[/code], [CullInstance]s will be autoplaced in the [Room] with the highest priority. diff --git a/doc/classes/MergeGroup.xml b/doc/classes/MergeGroup.xml new file mode 100644 index 000000000..731332ef8 --- /dev/null +++ b/doc/classes/MergeGroup.xml @@ -0,0 +1,114 @@ + + + + MergeGroups allow merging of suitable meshes, which can enhance performance. + + + [MergeGroup] is a way of grouping nodes into logical blocks that contain meshes that are suitable for joining together, in order to increase rendering efficiency and reduce the number of nodes to simplify the scene. + Only children and descendants will be considered for merging. [MergeGroup] has no effect on parents or siblings. + Meshes must be static (non-moving) in relation to one another to be joined. For instance, a level background is often intended to be static. However, logical blocks that [b]move[/b] together, such as a ship, or car, are also good candidates for merging. + Within these blocks you will often want to prevent certain nodes or branches from being merged, because they [b]are[/b] intended to move, or change visibility, in relation to the main block. An example might be a steering wheel on a ship. You can finely control this with [member Spatial.merging_mode]. Be aware that [member Spatial.merging_mode] will be inherited from parents and ancestors of the [MergeGroup]. + There are two ways of performing merging: + - At runtime, using [method merge_meshes] or [member auto_merge]. + - Baking at design time to a separate scene, using the [code]bake[/code] button in the Editor inspector. + Merging at runtime is usually best, because it is non-destructive, and will minimize the binary size of the [code]pck[/code] file. It can however take a small amount of time to merge the meshes (usually during level load), but this will usually be well under a second. + Baking ahead of time allows fastest possible load times, but it is by nature a [i]destructive[/i] operation - you should keep a copy of the source scene for later editing, because you cannot reconstruct an unmerged scene from a baked scene. It can also bloat the size of the [code]pck[/code] file considerably, as for example storing 10 merged trees will have 10x the geometry of the scene before merging. + On the other hand, baking ahead of time is very useful for previewing what will happen after merging, and diagnosing problems. It is also convenient for some workflows such as constructing a scene out of merged modular units. + + + + + + + + + Returns the value of the specified [enum MergeGroup.Param] parameter. + + + + + + + Gets the value of the specified [enum MergeGroup.ParamEnabled] parameter. + + + + + + You can choose to either automatically merge when the [MergeGroup] enters the scene (usually during loading) using [member auto_merge], or you can manually trigger merging by calling this function. + Manually activating merging is especially useful when you are [i]procedurally generating[/i] your level, and when you want to set advanced parameters prior to merging at runtime. + + + + + + + + Sets the value of the specified [enum MergeGroup.Param] parameter. + + + + + + + + Sets the value of the specified [enum MergeGroup.ParamEnabled] parameter. + + + + + + Activates merging automatically when the [MergeGroup] enters the scene (usually during loading). + Alternatively you can switch this off and use [method merge_meshes] to manually activate merging. + + + If [code]true[/code], a [b]shadow proxy[/b] will be generated. This is a merged mesh that is a duplicate of the existing opaque geometry, set to cast shadows only. The source meshes will have shadow casting switched off. + This can be more efficient for rendering shadows, because the requirements for merging a [b]shadow mesh[/b] are far lower than for regular merging. Providing materials are opaque, meshes with different materials can often be merged together for the purposes of shadow casting. This can reduce drawcalls. + [b]Tip:[/b] Try running with and without a [b]shadow proxy[/b] and measure performance, sometimes it will be faster, sometimes not. + + + + + Activates merging automatically when the [MergeGroup] enters the scene (usually during loading). + Alternatively you can switch this off and use [method merge_meshes] to manually activate merging. + + + If [code]true[/code], a [b]shadow proxy[/b] will be generated. This is a merged mesh that is a duplicate of the existing opaque geometry, set to cast shadows only. The source meshes will have shadow casting switched off. + This can be more efficient for rendering shadows, because the requirements for merging a [b]shadow mesh[/b] are far lower than for regular merging. Providing materials are opaque, meshes with different materials can often be merged together for the purposes of shadow casting. This can reduce drawcalls. + [b]Tip:[/b] Try running with and without a [b]shadow proxy[/b] and measure performance, sometimes it will be faster, sometimes not. + + + If [code]true[/code], [code]CSG[/code] nodes will be converted to [MeshInstance]s. These [MeshInstance]s can then be merged if suitable matches are found. + + + If [code]true[/code], [GridMap]s will be converted to [MeshInstance]s. These [MeshInstance]s can then be merged if suitable matches are found. + [b]Note:[/b] [GridMap]s are usually rendered as [MultiMesh]es very efficiently, so converting these will often be counterproductive. Exceptions include when using the [code]GLES2[/code] backend, which can be inefficient at rendering [MultiMesh]. + + + If [code]true[/code], as a final step, matching [MeshInstance]s can be joined by combining their surfaces to form an [i]"uber mesh instance"[/i]. + While this is convenient, it does have the downside that all the constituent meshes will be culled as one unit, which can make culling less efficient in some situations. + + + Cleans and removes degenerate triangles from meshes, which can make them more suitable for later processing, such as generating secondary UVs for lightmapping. + [b]Note:[/b] This step can be slow and should typically only be used when [i]baking[/i] the [MergeGroup]. + + + When set to [code]0[/code], all matching meshes will be merged within the [MergeGroup]. + If set to [code]1[/code] or above, only groups of a maximum of [code]group_size[/code] meshes will be merged together. These groups will be chosen by locality. This enables getting some of the benefits of merging, while still allowing some culling to take place. + [b]Tip:[/b] Use [i]baking[/i] to preview what the scene will look like after merging. + + + When set to a value above [code]1[/code], mesh geometry will be [i]split by locality[/i] into a grid of [MeshInstance]s. + For instance a value of [code]2[/code] will split meshes into a grid of 2x2 (on the [code]x[/code] and [code]z[/code] axes), for greater culling efficiency. + [b]Note:[/b] Greater culling efficiency must be balanced against a greater number of drawcalls. + + + This setting acts exactly as [constant PARAM_SPLITS_HORIZONTAL], except it determines the grid split on the vertical axis. + A grid with [constant PARAM_SPLITS_HORIZONTAL] [code]3[/code], and [constant PARAM_SPLITS_VERTICAL] [code]2[/code] will produce a grid of 3x2x3 (on the [code]x[/code] and [code]y[/code] and [code]z[/code] axes respectively). + + + When using [i]split by locality[/i] using [constant PARAM_SPLITS_HORIZONTAL] and / or [constant PARAM_SPLITS_VERTICAL], you can specify that the split will only occur for meshes above this specified poly count. + There is often little to gain by splitting meshes with low poly count. + + + diff --git a/doc/classes/MeshInstance.xml b/doc/classes/MeshInstance.xml index 0db39bd75..197f5ed24 100644 --- a/doc/classes/MeshInstance.xml +++ b/doc/classes/MeshInstance.xml @@ -65,6 +65,7 @@ + Returns [code]true[/code] if this [MeshInstance] can be merged with the specified [code]other_mesh_instance[/code], using the [method MeshInstance.merge_meshes] function. In order to be mergeable, properties of the [MeshInstance] must match, and each surface must match, in terms of material, attributes and vertex format. @@ -72,9 +73,10 @@ - + + This function can merge together the data from several source [MeshInstance]s into a single destination [MeshInstance] (the MeshInstance the function is called from). This is primarily useful for improving performance by reducing the number of drawcalls and [Node]s. Merging should only be attempted for simple meshes that do not contain animation. diff --git a/doc/classes/Spatial.xml b/doc/classes/Spatial.xml index a7ce80ba5..7243f7460 100644 --- a/doc/classes/Spatial.xml +++ b/doc/classes/Spatial.xml @@ -294,6 +294,11 @@ Global position of this node. This is equivalent to [code]global_transform.origin[/code]. + + The merging mode determines whether merging features of the engine ([MergeGroup] and [RoomManager]) will attempt to operate on branches of the scene tree. + The default mode inherited from the scene tree root is [constant MERGING_MODE_ON]. + [b]Note:[/b] Merging mode determines whether the merging is [b]allowed[/b] to be performed. It does not guarantee that merging will occur, which depends on whether there are suitable matching objects. + Rotation part of the local transformation in radians, specified in terms of YXZ-Euler angles in the format (X angle, Y angle, Z angle). [b]Note:[/b] In the mathematical sense, rotation is a matrix and not a vector. The three Euler angles, which are the three independent parameters of the Euler-angle parametrization of the rotation matrix, are stored in a [Vector3] data structure not because the rotation is a vector, but only because [Vector3] exists as a convenient data-structure to store 3 floating-point numbers. Therefore, applying affine operations on the rotation "vector" is not meaningful. @@ -353,5 +358,14 @@ Spatial nodes receives this notification if the portal system gameplay monitor detects they have exited the gameplay area. + + Inherits merging mode from the node's parent. For the root node, it is equivalent to [constant MERGING_MODE_ON]. Default. + + + Turn off merging in this node and children set to [constant MERGING_MODE_INHERIT]. + + + Turn on merging in this node and children set to [constant MERGING_MODE_INHERIT]. + diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index a52fd2ace..2eddb5c72 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -124,6 +124,7 @@ #include "editor/plugins/light_occluder_2d_editor_plugin.h" #include "editor/plugins/line_2d_editor_plugin.h" #include "editor/plugins/material_editor_plugin.h" +#include "editor/plugins/merge_group_editor_plugin.h" #include "editor/plugins/mesh_editor_plugin.h" #include "editor/plugins/mesh_instance_editor_plugin.h" #include "editor/plugins/multimesh_editor_plugin.h" @@ -7138,6 +7139,7 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(NavigationObstacleEditorPlugin(this))); add_editor_plugin(memnew(PortalEditorPlugin(this))); add_editor_plugin(memnew(PackedSceneEditorPlugin(this))); + add_editor_plugin(memnew(MergeGroupEditorPlugin(this))); add_editor_plugin(memnew(Path2DEditorPlugin(this))); add_editor_plugin(memnew(PathEditorPlugin(this))); add_editor_plugin(memnew(Line2DEditorPlugin(this))); diff --git a/editor/editor_property_name_processor.cpp b/editor/editor_property_name_processor.cpp index da2486dc8..4e875b27b 100644 --- a/editor/editor_property_name_processor.cpp +++ b/editor/editor_property_name_processor.cpp @@ -79,6 +79,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() { capitalize_string_remaps["api"] = "API"; capitalize_string_remaps["Bvh"] = "BVH"; capitalize_string_remaps["Csg"] = "CSG"; + capitalize_string_remaps["Csgs"] = "CSGs"; capitalize_string_remaps["Cpu"] = "CPU"; capitalize_string_remaps["Db"] = "dB"; capitalize_string_remaps["Dof"] = "DoF"; diff --git a/editor/icons/icon_merge_group.svg b/editor/icons/icon_merge_group.svg new file mode 100644 index 000000000..2f1cc6d3a --- /dev/null +++ b/editor/icons/icon_merge_group.svg @@ -0,0 +1 @@ + diff --git a/editor/plugins/merge_group_editor_plugin.cpp b/editor/plugins/merge_group_editor_plugin.cpp new file mode 100644 index 000000000..22e34f348 --- /dev/null +++ b/editor/plugins/merge_group_editor_plugin.cpp @@ -0,0 +1,590 @@ +/*************************************************************************/ +/* merge_group_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* PANDEMONIUM ENGINE */ +/* https://github.com/Relintai/pandemonium_engine */ +/*************************************************************************/ +/* Copyright (c) 2022-present Péter Magyar. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "merge_group_editor_plugin.h" + +#include "core/io/resource_saver.h" +#include "editor/editor_file_dialog.h" +#include "editor/spatial_editor_gizmos.h" +#include "scene/3d/mesh_instance.h" +#include "scene/gui/check_box.h" +#include "scene/gui/spin_box.h" +#include "scene/resources/mesh/merging_tool.h" +#include "scene/resources/packed_scene.h" + +EditorProgress *MergeGroupEditorPlugin::tmp_progress = nullptr; +EditorProgress *MergeGroupEditorPlugin::tmp_subprogress = nullptr; + +void MergeGroupEditorBakeDialog::_bake_confirm() { + _owner_plugin->dialog_pressed_bake(_single_scene->is_pressed(), (int)_subscene_polycount_threshold->get_value()); +} + +void MergeGroupEditorBakeDialog::_add_bake_checkbox(Node *p_parent, CheckBox **pp_checkbox, const String &p_text, const String &p_tooltip, bool p_default) { + *pp_checkbox = memnew(CheckBox); + (*pp_checkbox)->set_text(TTR(p_text)); + (*pp_checkbox)->set_tooltip(TTR(p_tooltip)); + (*pp_checkbox)->set_pressed(p_default); + p_parent->add_child(*pp_checkbox); +} + +void MergeGroupEditorBakeDialog::_add_bake_spinbox(VBoxContainer *p_parent, SpinBox **pp_spinbox, const String &p_text, const String &p_tooltip, int32_t p_min, int32_t p_max, int32_t p_step, int32_t p_default) { + *pp_spinbox = memnew(SpinBox); + (*pp_spinbox)->set_min(p_min); + (*pp_spinbox)->set_max(p_max); + (*pp_spinbox)->set_step(p_step); + (*pp_spinbox)->set_value(p_default); + (*pp_spinbox)->set_tooltip(p_tooltip); + p_parent->add_margin_child(TTR(p_text), *pp_spinbox); +} + +void MergeGroupEditorBakeDialog::fill_merge_group_params(MergeGroup &r_merge_group) { + r_merge_group.set_param_enabled(MergeGroup::PARAM_ENABLED_SHADOW_PROXY, _shadow_proxy->is_pressed()); + r_merge_group.set_param_enabled(MergeGroup::PARAM_ENABLED_CONVERT_CSGS, _convert_csgs->is_pressed()); + r_merge_group.set_param_enabled(MergeGroup::PARAM_ENABLED_CONVERT_GRIDMAPS, _convert_gridmaps->is_pressed()); + r_merge_group.set_param_enabled(MergeGroup::PARAM_ENABLED_COMBINE_SURFACES, _combine_surfaces->is_pressed()); + r_merge_group.set_param_enabled(MergeGroup::PARAM_ENABLED_CLEAN_MESHES, _clean_meshes->is_pressed()); + + r_merge_group.set_param(MergeGroup::PARAM_GROUP_SIZE, _group_size->get_value()); + r_merge_group.set_param(MergeGroup::PARAM_SPLITS_HORIZONTAL, _splits_horz->get_value()); + r_merge_group.set_param(MergeGroup::PARAM_SPLITS_VERTICAL, _splits_vert->get_value()); + r_merge_group.set_param(MergeGroup::PARAM_MIN_SPLIT_POLY_COUNT, _min_split_poly_count->get_value()); +} + +MergeGroupEditorBakeDialog::MergeGroupEditorBakeDialog(MergeGroupEditorPlugin *p_owner) { + _owner_plugin = p_owner; + + set_title("Bake MergeGroup"); + + get_ok()->connect("pressed", this, "_bake_confirm"); + + VBoxContainer *vbc = memnew(VBoxContainer); + add_child(vbc); + + _add_bake_checkbox(vbc, &_single_scene, "Single Scene", "Save meshes as a single scene or separate scenes.", true); + + _add_bake_spinbox(vbc, &_subscene_polycount_threshold, "Subscene Polycount Threshold:", "Threshold polycount to split scenes into separate subscenes.", 0, 1024 * 128, 64, 1024); + + _add_bake_checkbox(vbc, &_shadow_proxy, "Shadow Proxy", "Separate meshes for shadow rendering.", true); + _add_bake_checkbox(vbc, &_convert_csgs, "Convert CSGs", "Convert CSGs to meshes so they can be merged.", true); + _add_bake_checkbox(vbc, &_convert_gridmaps, "Convert GridMaps", "Convert GridMaps to meshes so they can be merged."); + _add_bake_checkbox(vbc, &_combine_surfaces, "Combine Surfaces", "Combine merged surfaces to form an \"ubermesh\".", true); + _add_bake_checkbox(vbc, &_clean_meshes, "Clean Meshes", "Clean geometry data, remove degenerate triangles."); + + _add_bake_spinbox(vbc, &_group_size, "Group Size", "When non-zero, only local groups of the corresponding number of meshes will be merged.", 0, 128, 1, 0); + _add_bake_spinbox(vbc, &_splits_horz, "Splits Horizontal", "When above 1, the final meshes will be split into a grid on the horizontal axis.", 0, 16, 1, 1); + _add_bake_spinbox(vbc, &_splits_vert, "Splits Vertical", "When above 1, the final meshes will be split into a grid on the vertical axis.", 0, 16, 1, 1); + _add_bake_spinbox(vbc, &_min_split_poly_count, "Min Split Polycount", "When splitting by grid, only meshes above this minimum polycount will be split.", 0, 1024 * 128, 256, 1024); +} + +void MergeGroupEditorBakeDialog::_bind_methods() { + ClassDB::bind_method("_bake_confirm", &MergeGroupEditorBakeDialog::_bake_confirm); +} + +////////////////////////////////////////////////////////// + +bool MergeGroupEditorPlugin::bake_func_step(float p_progress, const String &p_description, void *, bool p_force_refresh) { + if (!tmp_progress) { + tmp_progress = memnew(EditorProgress("bake_merge_group", TTR("Bake MergeGroup"), 1000, true)); + ERR_FAIL_NULL_V(tmp_progress, false); + } + return tmp_progress->step(p_description, p_progress * 1000, p_force_refresh); +} + +bool MergeGroupEditorPlugin::bake_func_substep(float p_progress, const String &p_description, void *, bool p_force_refresh) { + if (!tmp_subprogress) { + tmp_subprogress = memnew(EditorProgress("bake_merge_group_substep", "", 1000, true)); + ERR_FAIL_NULL_V(tmp_subprogress, false); + } + return tmp_subprogress->step(p_description, p_progress * 1000, p_force_refresh); +} + +void MergeGroupEditorPlugin::bake_func_end(uint32_t p_time_started) { + if (tmp_progress != nullptr) { + memdelete(tmp_progress); + tmp_progress = nullptr; + } + + if (tmp_subprogress != nullptr) { + memdelete(tmp_subprogress); + tmp_subprogress = nullptr; + } + + const int time_taken = (OS::get_singleton()->get_ticks_msec() - p_time_started) * 0.001; + if (time_taken >= 1) { + // Only print a message and request attention if baking took at least 1 second. + print_line(vformat("Done baking MergeGroup in %02d:%02d:%02d.", time_taken / 3600, (time_taken % 3600) / 60, time_taken % 60)); + + // Request attention in case the user was doing something else. + OS::get_singleton()->request_attention(); + } +} + +void MergeGroupEditorPlugin::dialog_pressed_bake(bool p_single_scene, int p_subscene_polycount_threshold) { + if (!_merge_group) { + return; + } + + _params.single_scene = p_single_scene; + _params.subscene_polycount_threshold = p_subscene_polycount_threshold; + + Node *root = _merge_group->get_tree()->get_edited_scene_root(); + + if (root == _merge_group) { + EditorNode::get_singleton()->show_warning(TTR("Cannot bake MergeGroup when it is scene root.")); + return; + } + + String scene_path = _merge_group->get_filename(); + if (scene_path == String()) { + scene_path = _merge_group->get_owner()->get_filename(); + } + if (scene_path == String()) { + EditorNode::get_singleton()->show_warning(TTR("Can't determine a save path for merge group.\nSave your scene and try again.")); + return; + } + scene_path = scene_path.get_basename() + ".tscn"; + + file_dialog->set_current_path(scene_path); + file_dialog->popup_centered_ratio(); +} + +bool MergeGroupEditorPlugin::_find_visual_instances_recursive(Node *p_node) { + if (Object::cast_to(p_node)) { + return true; + } + + for (int n = 0; n < p_node->get_child_count(); n++) { + if (_find_visual_instances_recursive(p_node->get_child(n))) { + return true; + } + } + + return false; +} + +void MergeGroupEditorPlugin::_bake() { + ERR_FAIL_NULL(_merge_group); + + // If the merge group does not contain any VisualInstance children, flag an error. + if (!_find_visual_instances_recursive(_merge_group)) { + EditorNode::get_singleton()->show_warning(TTR("MergeGroup does not contain any VisualInstances.\nCannot Bake.")); + return; + } + + bake_dialog->show(); +} + +Spatial *MergeGroupEditorPlugin::_convert_merge_group_to_spatial(MergeGroup *p_merge_group) { + ERR_FAIL_NULL_V(p_merge_group, nullptr); + Node *parent = p_merge_group->get_parent(); + ERR_FAIL_NULL_V(parent, nullptr); + + Spatial *spatial = memnew(Spatial); + parent->add_child(spatial); + + // They can't have the same name at the same time. + String name = p_merge_group->get_name(); + p_merge_group->set_name(name + "_temp"); + spatial->set_name(name); + + // Identical transforms. + spatial->set_transform(p_merge_group->get_transform()); + + // Move the children. + // GODOT is abysmally bad at moving children in order unfortunately. + // So reverse order for now. + for (int n = p_merge_group->get_child_count() - 1; n >= 0; n--) { + Node *child = p_merge_group->get_child(n); + p_merge_group->remove_child(child); + spatial->add_child(child); + } + + // Change owners. + MergingTool::_invalidate_owner_recursive(spatial, nullptr, p_merge_group->get_owner()); + + // Delete AND detach the merge group from the tree. + p_merge_group->_delete_node(p_merge_group); + + return spatial; +} + +void MergeGroupEditorPlugin::_bake_select_file(const String &p_file) { + if (!_merge_group) { + return; + } + + // Special treatment for scene root. + Node *root = _merge_group->get_tree()->get_edited_scene_root(); + + // Cannot bake scene root. + if (root == _merge_group) { + EditorNode::get_singleton()->show_warning(TTR("Cannot bake scene root.\nPlease move to a branch before baking.")); + ERR_FAIL_COND(root == _merge_group); + } + + Node *parent = _merge_group->get_parent(); + ERR_FAIL_NULL(parent); + + // Disallow saving to the same scene as the root scene + // (this is usually user error), and prevents losing work. + if (root->get_filename() == p_file) { + EditorNode::get_singleton()->show_warning(TTR("Cannot save to the currently edited scene.\nPlease save to a different scene.")); + ERR_FAIL_COND(root->get_filename() == p_file); + } + + // Ensure to reset this when exiting this routine! + // Spatial gizmos, especially for meshes are very expensive + // in terms of RAM and performance, and are totally + // unnecessary for temporary objects + SpatialEditor::_prevent_gizmo_generation = true; + +#ifdef GODOT_MERGING_VERBOSE + MergingTool::debug_branch(_merge_group, "START_SCENE"); +#endif + + Spatial *hanger = memnew(Spatial); + hanger->set_name("hanger"); + parent->add_child(hanger); + hanger->set_owner(_merge_group->get_owner()); + + uint32_t time_start = OS::get_singleton()->get_ticks_msec(); + bake_func_step(0.0, "Duplicating Branch", nullptr, true); + + _duplicate_branch(_merge_group, hanger); + + // Temporarily hide source branch, to speed things up in the editor. + bool was_visible = _merge_group->is_visible_in_tree(); + _merge_group->hide(); + + MergeGroup *merge_group_copy = Object::cast_to(hanger->get_child(0)); + + // Set the parameters in the copy mergegroup to those set up in the bake dialog. + bake_dialog->fill_merge_group_params(*merge_group_copy); + + if (merge_group_copy->merge_meshes_in_editor()) { + if (!bake_func_step(1.0, "Saving Scene", nullptr, true)) { + // Convert the merge node to a spatial.. + // Once baked we don't want baked scenes to be merged AGAIN + // when incorporated into scenes. + Spatial *final_branch = _convert_merge_group_to_spatial(merge_group_copy); + + // Only save if not cancelled by user. + _save_scene(final_branch, p_file); + } + } + +#ifdef GODOT_MERGING_VERBOSE + MergingTool::debug_branch(hanger, "END_SCENE"); +#endif + + // Finished. + hanger->queue_delete(); + _merge_group->set_visible(was_visible); + + SpatialEditor::_prevent_gizmo_generation = false; + + bake_func_end(time_start); +} + +void MergeGroupEditorPlugin::_remove_queue_deleted_nodes_recursive(Node *p_node) { + if (p_node->is_queued_for_deletion()) { + p_node->get_parent()->remove_child(p_node); + return; + } + + for (int n = p_node->get_child_count() - 1; n >= 0; n--) { + _remove_queue_deleted_nodes_recursive(p_node->get_child(n)); + } +} + +uint32_t MergeGroupEditorPlugin::_get_mesh_poly_count(const MeshInstance &p_mi) const { + Ref rmesh = p_mi.get_mesh(); + if (rmesh.is_valid()) { + return rmesh->get_face_count(); + } + + return 0; +} + +bool MergeGroupEditorPlugin::_replace_with_branch_scene(const String &p_file, Node *p_base) { + Node *old_owner = p_base->get_owner(); + + Ref sdata = ResourceLoader::load(p_file); + if (!sdata.is_valid()) { + ERR_PRINT("Error loading scene from \"" + p_file + "\"."); + return false; + } + + Node *instanced_scene = sdata->instance(PackedScene::GEN_EDIT_STATE_INSTANCE); + if (!instanced_scene) { + ERR_PRINT("Error instancing scene from \"" + p_file + "\"."); + return false; + } + + Node *parent = p_base->get_parent(); + int pos = p_base->get_index(); + + parent->remove_child(p_base); + parent->add_child(instanced_scene); + parent->move_child(instanced_scene, pos); + + List owned; + p_base->get_owned_by(p_base->get_owner(), &owned); + Array owners; + for (List::Element *F = owned.front(); F; F = F->next()) { + owners.push_back(F->get()); + } + + instanced_scene->set_owner(old_owner); + + p_base->queue_delete(); + + return true; +} + +bool MergeGroupEditorPlugin::_save_subscene(Node *p_root, Node *p_branch, String p_base_filename, int &r_subscene_count) { + bake_func_substep(0.0, p_branch->get_name(), nullptr, false); + + Node *scene_root = p_root; + + RBMap reown; + reown[scene_root] = p_branch; + + Node *copy = p_branch->duplicate_and_reown(reown); + + bake_func_substep(0.2, p_branch->get_name(), nullptr, false); + + if (copy) { + Ref sdata = memnew(PackedScene); + Error err = sdata->pack(copy); + memdelete(copy); + + bake_func_substep(0.4, p_branch->get_name(), nullptr, false); + + if (err != OK) { + WARN_PRINT("Couldn't save subscene \"" + p_branch->get_name() + "\" . Likely dependencies (instances) couldn't be satisfied. Saving as part of main scene instead."); + return false; + } + + String filename = p_base_filename + "_" + itos(r_subscene_count++) + ".scn"; + +#ifdef DEV_ENABLED + print_verbose("Save subscene: " + filename); +#endif + + err = ResourceSaver::save(filename, sdata, ResourceSaver::FLAG_COMPRESS); + + bake_func_substep(0.6, p_branch->get_name(), nullptr, false); + + if (err != OK) { + WARN_PRINT("Error saving subscene \"" + p_branch->get_name() + "\" , saving as part of main scene instead."); + return false; + } + _replace_with_branch_scene(filename, p_branch); + } else { + WARN_PRINT("Error duplicating subscene \"" + p_branch->get_name() + "\" , saving as part of main scene instead."); + return false; + } + + return true; +} + +void MergeGroupEditorPlugin::_save_mesh_subscenes_recursive(Node *p_root, Node *p_node, String p_base_filename, int &r_subscene_count) { + if (p_node->is_queued_for_deletion()) { + return; + } + // Is a subscene already? + if (p_node->get_filename().length() && (p_node != p_root)) { + return; + } + + // Is it a mesh instance? + MeshInstance *mi = Object::cast_to(p_node); + + // Don't save subscenes for trivially sized meshes. + if (mi && (!_params.subscene_polycount_threshold || ((int)_get_mesh_poly_count(*mi) > _params.subscene_polycount_threshold))) { + // Save as subscene. + if (_save_subscene(p_root, p_node, p_base_filename, r_subscene_count)) { + return; + } + } + + // Replaced subscenes will be added to the last child, so going in reverse order is necessary. + for (int n = p_node->get_child_count() - 1; n >= 0; n--) { + _save_mesh_subscenes_recursive(p_root, p_node->get_child(n), p_base_filename, r_subscene_count); + } +} + +void MergeGroupEditorPlugin::_push_mesh_data_to_gpu_recursive(Node *p_node) { + // Is it a mesh instance? + MeshInstance *mi = Object::cast_to(p_node); + + if (mi) { + Ref rmesh = mi->get_mesh(); + if (rmesh.is_valid()) { + rmesh->set_storage_mode(Mesh::STORAGE_MODE_GPU); + } + } + + for (int n = 0; n < p_node->get_child_count(); n++) { + _push_mesh_data_to_gpu_recursive(p_node->get_child(n)); + } +} + +bool MergeGroupEditorPlugin::_save_scene(Node *p_branch, String p_filename) { + // For some reason the saving machinery doesn't deal well with nodes queued for deletion, + // so we will remove them from the scene tree (as risk of leaks, but the queue delete machinery + // should still work when detached). + _remove_queue_deleted_nodes_recursive(p_branch); + + // All mesh data must be on the GPU for the Mesh saving routines to work. + _push_mesh_data_to_gpu_recursive(p_branch); + + Node *scene_root = p_branch->get_tree()->get_edited_scene_root(); + + RBMap reown; + reown[scene_root] = p_branch; + + Node *copy = p_branch->duplicate_and_reown(reown); + +#ifdef GODOT_MERGING_VERBOSE + MergingTool::debug_branch(copy, "SAVE SCENE:"); +#endif + + bake_func_substep(0.0, p_filename, nullptr, false); + + if (copy) { + // Save any large meshes as compressed resources. + if (!_params.single_scene) { + int subscene_count = 0; + _save_mesh_subscenes_recursive(copy, copy, p_filename.get_basename(), subscene_count); + } + + bake_func_substep(0.4, p_filename, nullptr, false); + + Ref sdata = memnew(PackedScene); + Error err = sdata->pack(copy); + memdelete(copy); + + bake_func_substep(0.8, p_filename, nullptr, false); + + if (err != OK) { + EditorNode::get_singleton()->show_warning(TTR("Couldn't save merged branch.\nLikely dependencies (instances) couldn't be satisfied.")); + return false; + } + + err = ResourceSaver::save(p_filename, sdata, ResourceSaver::FLAG_COMPRESS); + if (err != OK) { + EditorNode::get_singleton()->show_warning(TTR("Error saving scene.")); + return false; + } + } else { + EditorNode::get_singleton()->show_warning(TTR("Error duplicating scene to save it.")); + return false; + } + + return true; +} + +void MergeGroupEditorPlugin::_duplicate_branch(Node *p_branch, Node *p_new_parent) { + Node *dup = p_branch->duplicate(); + + ERR_FAIL_NULL(dup); + + p_new_parent->add_child(dup); + + Node *new_owner = p_new_parent->get_owner(); + dup->set_owner(new_owner); + + MergingTool::_invalidate_owner_recursive(dup, nullptr, new_owner); +} + +void MergeGroupEditorPlugin::edit(Object *p_object) { + MergeGroup *mg = Object::cast_to(p_object); + if (!mg) { + return; + } + + _merge_group = mg; +} + +bool MergeGroupEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("MergeGroup"); +} + +void MergeGroupEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + button_bake->show(); + } else { + button_bake->hide(); + bake_dialog->hide(); + } +} + +void MergeGroupEditorPlugin::_bind_methods() { + ClassDB::bind_method("_bake", &MergeGroupEditorPlugin::_bake); + ClassDB::bind_method("_bake_select_file", &MergeGroupEditorPlugin::_bake_select_file); +} + +MergeGroupEditorPlugin::MergeGroupEditorPlugin(EditorNode *p_node) { + editor = p_node; + + button_bake = memnew(ToolButton); + button_bake->set_icon(editor->get_gui_base()->get_theme_icon("Bake", "EditorIcons")); + button_bake->set_text(TTR("Bake")); + button_bake->set_tooltip(TTR("Bake MergeGroup to Scene.")); + button_bake->hide(); + button_bake->connect("pressed", this, "_bake"); + + file_dialog = memnew(EditorFileDialog); + file_dialog->set_mode(EditorFileDialog::MODE_SAVE_FILE); + file_dialog->add_filter("*.tscn ; " + TTR("Scene")); + file_dialog->add_filter("*.scn ; " + TTR("Binary Scene")); + file_dialog->set_title(TTR("Save Merged Scene As...")); + file_dialog->connect("file_selected", this, "_bake_select_file"); + button_bake->add_child(file_dialog); + + bake_dialog = memnew(MergeGroupEditorBakeDialog(this)); + bake_dialog->set_anchors_and_margins_preset(Control::PRESET_CENTER); + bake_dialog->hide(); + button_bake->add_child(bake_dialog); + + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, button_bake); + + _merge_group = nullptr; + + MergeGroup::bake_step_function = bake_func_step; + MergeGroup::bake_substep_function = bake_func_substep; + MergeGroup::bake_end_function = bake_func_end; +} + +MergeGroupEditorPlugin::~MergeGroupEditorPlugin() { +} diff --git a/editor/plugins/merge_group_editor_plugin.h b/editor/plugins/merge_group_editor_plugin.h new file mode 100644 index 000000000..d81f7f347 --- /dev/null +++ b/editor/plugins/merge_group_editor_plugin.h @@ -0,0 +1,126 @@ + +#ifndef MERGE_GROUP_EDITOR_PLUGIN_H +#define MERGE_GROUP_EDITOR_PLUGIN_H + +/*************************************************************************/ +/* merge_group_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* PANDEMONIUM ENGINE */ +/* https://github.com/Relintai/pandemonium_engine */ +/*************************************************************************/ +/* Copyright (c) 2022-present Péter Magyar. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/editor_node.h" +#include "editor/editor_plugin.h" +#include "scene/3d/merge_group.h" +#include "scene/gui/dialogs.h" +#include "scene/resources/material/material.h" + +class CheckBox; +class SpinBox; +class MergeGroupEditorPlugin; + +class MergeGroupEditorBakeDialog : public ConfirmationDialog { + GDCLASS(MergeGroupEditorBakeDialog, ConfirmationDialog); + + MergeGroupEditorPlugin *_owner_plugin = nullptr; + CheckBox *_single_scene = nullptr; + SpinBox *_subscene_polycount_threshold = nullptr; + + CheckBox *_shadow_proxy = nullptr; + CheckBox *_convert_csgs = nullptr; + CheckBox *_convert_gridmaps = nullptr; + CheckBox *_combine_surfaces = nullptr; + CheckBox *_clean_meshes = nullptr; + SpinBox *_group_size = nullptr; + SpinBox *_splits_horz = nullptr; + SpinBox *_splits_vert = nullptr; + SpinBox *_min_split_poly_count = nullptr; + + void _bake_confirm(); + void _add_bake_checkbox(Node *p_parent, CheckBox **pp_checkbox, const String &p_text, const String &p_tooltip, bool p_default = false); + void _add_bake_spinbox(VBoxContainer *p_parent, SpinBox **pp_spinbox, const String &p_text, const String &p_tooltip, int32_t p_min, int32_t p_max, int32_t p_step, int32_t p_default); + +protected: + static void _bind_methods(); + +public: + void fill_merge_group_params(MergeGroup &r_merge_group); + MergeGroupEditorBakeDialog(MergeGroupEditorPlugin *p_owner); +}; + +class MergeGroupEditorPlugin : public EditorPlugin { + GDCLASS(MergeGroupEditorPlugin, EditorPlugin); + + MergeGroup *_merge_group; + ToolButton *button_bake; + EditorFileDialog *file_dialog; + EditorNode *editor; + MergeGroupEditorBakeDialog *bake_dialog; + + struct Params { + bool single_scene = false; + int subscene_polycount_threshold = 1024; + } _params; + + static EditorProgress *tmp_progress; + static EditorProgress *tmp_subprogress; + + static bool bake_func_step(float p_progress, const String &p_description, void *, bool p_force_refresh); + static bool bake_func_substep(float p_progress, const String &p_description, void *, bool p_force_refresh); + static void bake_func_end(uint32_t p_time_started); + + void _bake(); + void _bake_select_file(const String &p_file); + void _duplicate_branch(Node *p_branch, Node *p_new_parent); + bool _save_scene(Node *p_branch, String p_filename); + void _remove_queue_deleted_nodes_recursive(Node *p_node); + void _push_mesh_data_to_gpu_recursive(Node *p_node); + Spatial *_convert_merge_group_to_spatial(MergeGroup *p_merge_group); + bool _find_visual_instances_recursive(Node *p_node); + + void _save_mesh_subscenes_recursive(Node *p_root, Node *p_node, String p_base_filename, int &r_subscene_count); + bool _save_subscene(Node *p_root, Node *p_branch, String p_base_filename, int &r_subscene_count); + bool _replace_with_branch_scene(const String &p_file, Node *p_base); + uint32_t _get_mesh_poly_count(const MeshInstance &p_mi) const; + +protected: + static void _bind_methods(); + +public: + virtual String get_name() const { return "MergeGroup"; } + bool has_main_screen() const { return false; } + virtual void edit(Object *p_object); + virtual bool handles(Object *p_object) const; + virtual void make_visible(bool p_visible); + + void dialog_pressed_bake(bool p_single_scene, int p_subscene_polycount_threshold); + + MergeGroupEditorPlugin(EditorNode *p_node); + ~MergeGroupEditorPlugin(); +}; + +#endif // MERGE_GROUP_EDITOR_PLUGIN_H diff --git a/editor/plugins/spatial_editor_plugin.cpp b/editor/plugins/spatial_editor_plugin.cpp index 0036728f9..4c0df8b62 100644 --- a/editor/plugins/spatial_editor_plugin.cpp +++ b/editor/plugins/spatial_editor_plugin.cpp @@ -128,6 +128,8 @@ #define MIN_FOV 0.01 #define MAX_FOV 179 +bool SpatialEditor::_prevent_gizmo_generation = false; + void ViewportRotationControl::_notification(int p_what) { if (p_what == NOTIFICATION_ENTER_TREE) { axis_menu_options.clear(); @@ -6894,6 +6896,10 @@ void SpatialEditor::move_control_to_right_panel(Control *p_control) { } void SpatialEditor::_request_gizmo(Object *p_obj) { + if (_prevent_gizmo_generation) { + return; + } + Spatial *sp = Object::cast_to(p_obj); if (!sp) { return; diff --git a/editor/plugins/spatial_editor_plugin.h b/editor/plugins/spatial_editor_plugin.h index f984de311..9cbef9b76 100644 --- a/editor/plugins/spatial_editor_plugin.h +++ b/editor/plugins/spatial_editor_plugin.h @@ -822,6 +822,10 @@ public: return current_hover_gizmo_handle; } + // Simple way to turn off (expensive) gizmo generation + // especially for temporary objects in the editor. + static bool _prevent_gizmo_generation; + void set_can_preview(Camera *p_preview); void set_message(String p_message, float p_time = 5); diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index f5a7da680..51263c44c 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -31,6 +31,8 @@ #include "csg_shape.h" +#include "scene/3d/mesh_instance.h" +#include "scene/resources/mesh/merging_tool.h" #include "scene/resources/mesh/mesh.h" #include "scene/resources/world_3d.h" #include "servers/physics_server.h" @@ -497,6 +499,50 @@ PoolVector CSGShape::get_brush_faces() { return faces; } +bool CSGShape::split_by_surface(Vector p_destination_mesh_instances) { + ERR_FAIL_COND_V_MSG(!is_inside_tree(), false, "Source CSGShape must be inside the SceneTree."); + + // For simplicity we are requiring that the destination MeshInstances have the same parent + // as the source. This means we can use identical transforms. + Node *parent = get_parent(); + ERR_FAIL_NULL_V_MSG(parent, false, "Source CSGShape must have a parent node."); + + // Bound function only support variants, so we need to convert to a list of MeshInstances. + Vector mis; + + for (int n = 0; n < p_destination_mesh_instances.size(); n++) { + MeshInstance *mi = Object::cast_to(p_destination_mesh_instances[n]); + if (mi) { + ERR_FAIL_COND_V_MSG(mi->get_parent() != parent, false, "Destination MeshInstances must be siblings of the source CSGShape."); + mis.push_back(mi); + } else { + ERR_FAIL_V_MSG(false, "Only MeshInstances can be split."); + } + } + + force_update_shape(); + + Array arr = get_meshes(); + ERR_FAIL_COND_V(arr.empty(), false); + + Ref arr_mesh = arr[1]; + ERR_FAIL_COND_V(!arr_mesh.is_valid(), false); + + int num_surfaces = arr_mesh->get_surface_count(); + ERR_FAIL_COND_V(num_surfaces == 0, false); + + ERR_FAIL_COND_V_MSG(mis.size() != num_surfaces, false, "Number of surfaces and number of destination MeshInstances must match."); + + CSGBrush *brush = _get_brush(); + ERR_FAIL_NULL_V_MSG(brush, false, "Cannot get CSGBrush."); + + for (int s = 0; s < num_surfaces; s++) { + MergingTool::split_csg_surface_to_mesh_instance(*this, *mis[s], arr_mesh, brush, s); + } + + return true; +} + PoolVector CSGShape::get_faces(uint32_t p_usage_flags) const { return PoolVector(); } diff --git a/modules/csg/csg_shape.h b/modules/csg/csg_shape.h index ec1fb8b13..488b38ab8 100644 --- a/modules/csg/csg_shape.h +++ b/modules/csg/csg_shape.h @@ -40,6 +40,8 @@ #include "scene/resources/shapes/concave_polygon_shape.h" #include "thirdparty/misc/mikktspace.h" +class MeshInstance; + class CSGShape : public GeometryInstance { GDCLASS(CSGShape, GeometryInstance); @@ -133,6 +135,8 @@ public: virtual AABB get_aabb() const; virtual PoolVector get_faces(uint32_t p_usage_flags) const; + bool split_by_surface(Vector p_destination_mesh_instances); + void set_use_collision(bool p_enable); bool is_using_collision() const; diff --git a/scene/3d/cull_instance.h b/scene/3d/cull_instance.h index d9e1cc7ac..7a26ab2d5 100644 --- a/scene/3d/cull_instance.h +++ b/scene/3d/cull_instance.h @@ -38,7 +38,7 @@ class CullInstance : public Spatial { GDCLASS(CullInstance, Spatial); public: - enum PortalMode { + enum PortalMode : unsigned int { PORTAL_MODE_STATIC, // not moving within a room PORTAL_MODE_DYNAMIC, // moving within room PORTAL_MODE_ROAMING, // moving between rooms diff --git a/scene/3d/merge_group.cpp b/scene/3d/merge_group.cpp new file mode 100644 index 000000000..c9c71df9f --- /dev/null +++ b/scene/3d/merge_group.cpp @@ -0,0 +1,1195 @@ +/*************************************************************************/ +/* merge_group.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* PANDEMONIUM ENGINE */ +/* https://github.com/Relintai/pandemonium_engine */ +/*************************************************************************/ +/* Copyright (c) 2022-present Péter Magyar. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "merge_group.h" + +#include "core/config/engine.h" +#include "core/containers/bitfield_dynamic.h" +#include "core/os/os.h" +#include "scene/3d/mesh_instance.h" +#include "scene/3d/physics_body.h" +#include "scene/resources/mesh/merging_tool.h" +#include "scene/resources/world_3d.h" + +#include "modules/modules_enabled.gen.h" // For csg. +#ifdef MODULE_CSG_ENABLED +#include "modules/csg/csg_shape.h" +#endif + +#ifdef MODULE_GRIDMAP_ENABLED +#include "modules/gridmap/grid_map.h" +#endif + +int MergeGroup::MeshAABB::_sort_axis = 0; + +MergeGroup::BakeStepFunc MergeGroup::bake_step_function = nullptr; +MergeGroup::BakeStepFunc MergeGroup::bake_substep_function = nullptr; +MergeGroup::BakeEndFunc MergeGroup::bake_end_function = nullptr; + +void MergeGroup::_bind_methods() { + ClassDB::bind_method(D_METHOD("merge_meshes"), &MergeGroup::merge_meshes); + + ClassDB::bind_method(D_METHOD("set_param", "param", "value"), &MergeGroup::set_param); + ClassDB::bind_method(D_METHOD("get_param", "param"), &MergeGroup::get_param); + + ClassDB::bind_method(D_METHOD("set_param_enabled", "param", "value"), &MergeGroup::set_param_enabled); + ClassDB::bind_method(D_METHOD("get_param_enabled", "param"), &MergeGroup::get_param_enabled); + + ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "auto_merge"), "set_param_enabled", "get_param_enabled", PARAM_ENABLED_AUTO_MERGE); + ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "shadow_proxy"), "set_param_enabled", "get_param_enabled", PARAM_ENABLED_SHADOW_PROXY); + + BIND_ENUM_CONSTANT(PARAM_ENABLED_AUTO_MERGE); + BIND_ENUM_CONSTANT(PARAM_ENABLED_SHADOW_PROXY); + BIND_ENUM_CONSTANT(PARAM_ENABLED_CONVERT_CSGS); + BIND_ENUM_CONSTANT(PARAM_ENABLED_CONVERT_GRIDMAPS); + BIND_ENUM_CONSTANT(PARAM_ENABLED_COMBINE_SURFACES); + BIND_ENUM_CONSTANT(PARAM_ENABLED_CLEAN_MESHES); + + BIND_ENUM_CONSTANT(PARAM_GROUP_SIZE); + BIND_ENUM_CONSTANT(PARAM_SPLITS_HORIZONTAL); + BIND_ENUM_CONSTANT(PARAM_SPLITS_VERTICAL); + BIND_ENUM_CONSTANT(PARAM_MIN_SPLIT_POLY_COUNT); +} + +void MergeGroup::merge_meshes() { + if (!Engine::get_singleton()->is_editor_hint()) { + _merge_meshes(); + } +} + +bool MergeGroup::merge_meshes_in_editor() { + return _merge_meshes(); +} + +void MergeGroup::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POST_ENTER_TREE: { + ERR_FAIL_COND(get_world_3d().is_null()); + + if (data.params_enabled[PARAM_ENABLED_AUTO_MERGE] && !Engine::get_singleton()->is_editor_hint()) { + _merge_meshes(); + } + } break; + } +} + +// If this CSGShape is successfully split, all children are added to the first sibling (as the transform +// relationship should be preserved) and the source CSGShape is queue deleted. +// The children of a CSGCombiner are also treated specially, as they may not need to be preserved +// after the baking operation. +void MergeGroup::_split_csg_by_surface(CSGShape *p_shape) { +#ifdef MODULE_CSG_ENABLED + ERR_FAIL_NULL(p_shape); + + // Probably a child of a CSG combiner. + if (p_shape->is_queued_for_deletion()) { + return; + } + + // Shapes will not be up to date on the first frame due to a quirk + // of CSG - it defers updates to the next frame. So we need to explicitly + // force an update to make sure the CSG is correct on level load. + p_shape->force_update_shape(); + + Array arr = p_shape->get_meshes(); + if (!arr.size()) { + return; + } + + Ref arr_mesh = arr[1]; + if (!arr_mesh.is_valid()) { + return; + } + + int num_surfaces = arr_mesh->get_surface_count(); + if (num_surfaces == 0) { + return; + } + + // First create siblings. + Node *parent = p_shape->get_parent(); + ERR_FAIL_NULL(parent); + + // The first new sibling will be the first new child. + int first_sibling_id = parent->get_child_count(); + + Vector siblings; + for (int n = 0; n < num_surfaces; n++) { + MeshInstance *sib = memnew(MeshInstance); + MergingTool::_reparent(sib, parent, data.scene_root); + + String new_name = String(p_shape->get_name()) + "_surf_" + itos(n); + sib->set_name(new_name); + + siblings.push_back(sib); + } + + _node_changed(parent); + + if (!p_shape->split_by_surface(siblings)) { + return; + } + + // Failed to split. + if (parent->get_child_count() <= first_sibling_id) { + return; + } + + Node *first_sibling = parent->get_child(first_sibling_id); + ERR_FAIL_NULL(first_sibling); + + // Special case, do not move CSG children of a CSG combiner. + if (Object::cast_to(p_shape)) { + for (int n = 0; n < p_shape->get_child_count(); n++) { + CSGShape *child = Object::cast_to(p_shape->get_child(n)); + if (child) { + if (!child->get_child_count()) { + child->queue_delete(); +#ifdef TOOLS_ENABLED + _log("CSGShape \"" + child->get_name() + "\" child of CSGCombiner detected, deleting."); +#endif + } else { + // Panic stations .. convert to spatial to preserve the children. + _convert_source_to_spatial(child); +#ifdef TOOLS_ENABLED + _log("CSGShape \"" + child->get_name() + "\" child of CSGCombiner detected, converting to Spatial."); +#endif + } + } + } + } + + _move_children(p_shape, first_sibling); + + // Remove source. + _delete_node(p_shape); +#endif +} + +void MergeGroup::_logt(int p_tabs, String p_string) { +#ifdef TOOLS_ENABLED + if (p_tabs) { + String str; + for (int n = 0; n < p_tabs; n++) { + str += "\t"; + } + str += p_string; + _log(str); + } else { + _log(p_string); + } +#endif +} + +void MergeGroup::_log(String p_string) { +#ifdef TOOLS_ENABLED +#ifdef DEV_ENABLED + print_line(p_string); +#else + print_verbose(p_string); +#endif +#endif +} + +bool MergeGroup::_split_by_locality() { + LocalVector mis; + _find_mesh_instances_recursive(0, this, mis, false); + + if (!mis.size()) { + return true; + } + + // Find the overall AABB. + AABB aabb = mis[0]->get_transformed_aabb(); + + for (unsigned int n = 1; n < mis.size(); n++) { + aabb.merge_with(mis[n]->get_transformed_aabb()); + } + + for (unsigned int n = 0; n < mis.size(); n++) { + MeshInstance *mi = mis[n]; + + Node *parent = mi->get_parent(); + if (!parent) { + continue; + } + +#ifdef TOOLS_ENABLED + if (bake_substep_function) { + if (bake_substep_function((float)n / mis.size(), mi->get_name(), nullptr, false)) { + return false; + } + } +#endif + + // The first new sibling will be the first new child. + int first_sibling_id = parent->get_child_count(); + + MergingTool::split_mesh_instance_by_locality(*mi, aabb, data.params[PARAM_SPLITS_HORIZONTAL], data.params[PARAM_SPLITS_VERTICAL], data.params[PARAM_MIN_SPLIT_POLY_COUNT]); + + // Failed to split. + if (parent->get_child_count() <= first_sibling_id) { + continue; + } + + Node *first_sibling = parent->get_child(first_sibling_id); + + // This really should not happen. + ERR_FAIL_NULL_V(first_sibling, true); + + _move_children(mi, first_sibling); + + // Delete source. + _delete_node(mi); + } + + return true; +} + +// If this MeshInstance is successfully split, all children are added to the first sibling (as the transform +// relationship should be preserved) and the source MeshInstance is queue deleted. +void MergeGroup::_split_mesh_by_surface(MeshInstance *p_mi, int p_num_surfaces) { + ERR_FAIL_COND(p_num_surfaces <= 1); + + // First create siblings. + Node *parent = p_mi->get_parent(); + ERR_FAIL_NULL(parent); + + Vector siblings; + + // The first new sibling will be the first new child. + int first_sibling_id = parent->get_child_count(); + + for (int n = 0; n < p_num_surfaces; n++) { + MeshInstance *sib = memnew(MeshInstance); + MergingTool::_reparent(sib, parent, data.scene_root); + + String new_name = String(p_mi->get_name()) + "_surf_" + itos(n); + sib->set_name(new_name); + +#ifdef TOOLS_ENABLED + _log("split by surface to " + new_name + "."); +#endif + + siblings.push_back(sib); + } + + _node_changed(parent); + + p_mi->split_by_surface(siblings); + + // Failed to split. + if (parent->get_child_count() <= first_sibling_id) { + return; + } + + Node *first_sibling = parent->get_child(first_sibling_id); + ERR_FAIL_NULL(first_sibling); + +#ifdef TOOLS_ENABLED + if (bake_substep_function) { + if (bake_substep_function(1.0, p_mi->get_name(), nullptr, false)) { + return; + } + } +#endif + + _move_children(p_mi, first_sibling); + + // Remove source. + _delete_node(p_mi); +} + +void MergeGroup::_node_changed_internal(Node *p_node) { + // Wipe filenames. + if (p_node->get_filename().size()) { + // Don't wipe the filename of the tree root (well actually the child, as the root is a Viewport). + if (p_node->get_parent() != (Node *)p_node->get_tree()->get_root()) { +#ifdef TOOLS_ENABLED +#if 0 + _log("\tchanging filename of \"" + p_node->get_name() + "\" from " + p_node->get_filename() + " to NULL"); +#endif +#endif + p_node->set_filename(""); + + // Invalidate any child / grandchild nodes that were owned by this scene, + // make them owned by the scene root. + MergingTool::_invalidate_owner_recursive(p_node, p_node, data.scene_root); + } + } + + // Terminate if we reach the owning MergeGroup, we don't want to "corrupt" scene trees + // when baking in the editor. + // Note, this may cause problems if people attempt to merge at runtime then save at runtime + // above the MergeGroup, because subscenes won't have been invalidated. + if (p_node == this) { + return; + } + + // Set owner to the merge group to clear any subscenes from this point upward. + MergingTool::_set_owner_logged(p_node, data.scene_root); + + Node *parent = p_node->get_parent(); + if (parent) { + _node_changed_internal(parent); + } +} + +void MergeGroup::_node_changed(Node *p_node) { + //MergingTool::_invalidate_owner_recursive(p_node, p_node, data._scene_root); + _node_changed_internal(p_node); +} + +void MergeGroup::_move_children(Node *p_from, Node *p_to, bool p_recalculate_transforms) { + ERR_FAIL_NULL(p_from); + ERR_FAIL_NULL(p_to); + + // Invalidate any child nodes owned by this. + MergingTool::_invalidate_owner_recursive(p_from, p_from, data.scene_root); + + int num_children = p_from->get_child_count(); + + // Note these will be readded in reverse order. + // This is more efficient but users should not rely on this order. + for (int n = num_children - 1; n >= 0; n--) { + Node *child = p_from->get_child(n); + + if (p_recalculate_transforms) { + Spatial *child_spatial = Object::cast_to(child); + Transform old_global_xform = child_spatial->get_global_transform(); + MergingTool::_reparent(child, p_to, data.scene_root); + + // only set the new transform if it is out, to prevent float error when not needed + if (!child_spatial->get_global_transform().is_equal_approx(old_global_xform)) { + child_spatial->set_global_transform(old_global_xform); + } + + } else { + MergingTool::_reparent(child, p_to, data.scene_root); + } + +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + MergingTool::append_editor_description(child, "moved from parent", p_from); + } +#endif + } + + // Subscenes are also invalidated. + _node_changed(p_to); +} + +bool MergeGroup::_merge_meshes() { +#ifdef TOOLS_ENABLED + uint32_t before = OS::get_singleton()->get_ticks_msec(); +#endif + data.iteration = 0; + + data.scene_root = get_owner(); + ERR_FAIL_NULL_V(data.scene_root, false); + + if (data.params_enabled[PARAM_ENABLED_CONVERT_CSGS]) { + if (bake_step_function) { + if (bake_step_function(0.0, "Converting CSGs", nullptr, true)) { + return false; + } + } + // First find csgs and convert to meshes. + LocalVector csgs; + _find_csg_recursive(0, this, csgs); + + if (csgs.size()) { + _log("converting " + itos(csgs.size()) + " CSGShapes to MeshInstances."); + } + + for (unsigned int n = 0; n < csgs.size(); n++) { +#ifdef TOOLS_ENABLED + if (bake_substep_function) { + if (bake_substep_function((float)n / csgs.size(), itos(n), nullptr, false)) { + return false; + } + } +#endif + + CSGShape *csg = csgs[n]; + _split_csg_by_surface(csg); + } + } + + if (data.params_enabled[PARAM_ENABLED_CONVERT_GRIDMAPS]) { + if (bake_step_function) { + if (bake_step_function(1.0 / 8, "Converting GridMaps", nullptr, true)) { + return false; + } + } + // First find gridmaps and convert to meshes. + LocalVector gridmaps; + _find_gridmap_recursive(0, this, gridmaps); + + if (gridmaps.size()) { + _log("converting " + itos(gridmaps.size()) + " GridMaps to MeshInstances."); + } + + for (unsigned int n = 0; n < gridmaps.size(); n++) { +#ifdef TOOLS_ENABLED + if (bake_substep_function) { + if (bake_substep_function((float)n / gridmaps.size(), "Gridmap " + itos(n), nullptr, false)) { + return false; + } + } +#endif + GridMap *gridmap = gridmaps[n]; + _bake_gridmap(gridmap); + } + } + + // Split by surface? + if (data.split_by_surface) { + if (bake_step_function) { + if (bake_step_function(2.0 / 8, "Split by Surface", nullptr, true)) { + return false; + } + } + _log("split by surface"); + + LocalVector split_instances; + _find_mesh_instances_recursive(0, this, split_instances, false); + + for (unsigned int n = 0; n < split_instances.size(); n++) { + MeshInstance *mi = split_instances[n]; + +#ifdef TOOLS_ENABLED + if (bake_substep_function) { + if (bake_substep_function((float)n / split_instances.size(), mi->get_name(), nullptr, false)) { + return false; + } + } +#endif + + // Should be checked in the find recursive routine. + DEV_ASSERT(mi->get_mesh().is_valid()); + int num_surfs = mi->get_mesh()->get_surface_count(); + if (num_surfs > 1) { + _split_mesh_by_surface(mi, num_surfs); + } + } // for n + } + + // First create a list of mesh instances. + uint32_t try_to_merge_count = 0; + { + _log("merging meshes"); +#ifdef TOOLS_ENABLED + if (bake_step_function) { + if (bake_step_function(3.0 / 8, "Merging Meshes", nullptr, true)) { + return false; + } + } +#endif + LocalVector mesh_instances; + _find_mesh_instances_recursive(0, this, mesh_instances, false, true); + try_to_merge_count = mesh_instances.size(); + + while (_merge_similar(mesh_instances, false)) { + if (bake_substep_function && mesh_instances.size()) { + if (bake_substep_function((float)(try_to_merge_count - mesh_instances.size()) / try_to_merge_count, mesh_instances[0]->get_name(), nullptr, false)) { + return false; + } + } + + data.iteration++; + } + } + + if (data.params_enabled[PARAM_ENABLED_CLEAN_MESHES]) { +#ifdef TOOLS_ENABLED + if (bake_step_function) { + if (bake_step_function(4.0 / 8, "Cleaning Meshes", nullptr, true)) { + return false; + } + } +#endif + _log("cleaning meshes"); + LocalVector mesh_instances_clean; + _find_mesh_instances_recursive(0, this, mesh_instances_clean, false); + for (uint32_t n = 0; n < mesh_instances_clean.size(); n++) { + if (bake_substep_function) { + if (bake_substep_function((float)n / mesh_instances_clean.size(), mesh_instances_clean[n]->get_name(), nullptr, false)) { + return false; + } + } + + if (MergingTool::clean_mesh_instance(*mesh_instances_clean[n])) { + _node_changed(mesh_instances_clean[n]); + } + } + } + + if (data.params_enabled[PARAM_ENABLED_SHADOW_PROXY]) { +#ifdef TOOLS_ENABLED + if (bake_step_function) { + if (bake_step_function(5.0 / 8, "Creating Shadow Proxy", nullptr, true)) { + return false; + } + } +#endif + _log("creating shadow proxy"); + LocalVector mesh_instances_shadow; + _find_mesh_instances_recursive(0, this, mesh_instances_shadow, true); + unsigned int orig_num_found = mesh_instances_shadow.size(); + + while (_merge_similar(mesh_instances_shadow, true)) { + if (bake_substep_function && mesh_instances_shadow.size()) { + if (bake_substep_function((float)(orig_num_found - mesh_instances_shadow.size()) / orig_num_found, mesh_instances_shadow[0]->get_name(), nullptr, false)) { + return false; + } + } + } + } + + bool split_by_locality = (data.params[PARAM_SPLITS_HORIZONTAL] > 1) || (data.params[PARAM_SPLITS_VERTICAL] > 1); + if (split_by_locality) { + _log("split by locality"); + if (bake_step_function) { + if (bake_step_function(6.0 / 8, "Split by Locality", nullptr, true)) { + return false; + } + } + if (!_split_by_locality()) { + return false; + } + } + + if (data.params_enabled[PARAM_ENABLED_COMBINE_SURFACES]) { + if ((data.params[PARAM_GROUP_SIZE] > 1) || (split_by_locality)) { + WARN_PRINT("Mesh Joining is disabled for MergeGroups with split by locality or max merges."); + } else { +#ifdef GODOT_MERGING_VERBOSE + MergingTool::debug_branch(this, "BEFORE JOINING"); +#endif +#ifdef TOOLS_ENABLED + if (bake_step_function) { + if (bake_step_function(7.0 / 8, "Joining Meshes", nullptr, true)) { + return false; + } + } +#endif + _log("join meshes"); + LocalVector mesh_instances_join; + _find_mesh_instances_recursive(0, this, mesh_instances_join, false); + unsigned int orig_num_found = mesh_instances_join.size(); + + while (_join_similar(mesh_instances_join)) { + if (bake_substep_function && mesh_instances_join.size()) { + if (bake_substep_function((float)(orig_num_found - mesh_instances_join.size()) / orig_num_found, mesh_instances_join[0]->get_name(), nullptr, false)) { + return false; + } + } + } +#ifdef GODOT_MERGING_VERBOSE + MergingTool::debug_branch(this, "AFTER JOINING"); +#endif + } + } + +#ifdef TOOLS_ENABLED + uint32_t after = OS::get_singleton()->get_ticks_msec(); + _log("Merging for \"" + get_name() + "\" took " + itos(after - before) + " ms. Attempted to merge " + itos(try_to_merge_count) + " meshes."); +#endif + + return true; +} + +bool MergeGroup::_join_similar(LocalVector &r_mis) { + if (!r_mis.size()) { + return false; + } + + LocalVector list; + + MeshInstance *first = nullptr; + + for (int n = 0; n < (int)r_mis.size(); n++) { + MeshInstance *mi = r_mis[n]; + + // Is this mesh suitable? + Ref rmesh = mi->get_mesh(); + if (!rmesh.is_valid()) { + r_mis.remove_unordered(n); + n--; + continue; + } + + // Either the first member of the list, or mergeable with the existing list. + if (!first || MergingTool::is_mergeable_with(*first, *mi, false)) { + first = mi; + list.push_back(mi); + r_mis.remove_unordered(n); + n--; + } + } + + // No joins possible for this mi. + if (list.size() <= 1) { + return true; + } + + MeshInstance *joined = memnew(MeshInstance); + MergingTool::_reparent(joined, this, data.scene_root); + _node_changed(this); + + // Rename. + joined->set_name("Joined [" + first->get_name() + "]"); + + // Either all of them join, or none. + if (!MergingTool::join_meshes(*joined, list)) { + // Failed to join. + _delete_node(joined); + return false; + } + + _cleanup_source_meshes(list); + // MergingTool::debug_mesh_instance(*joined); + + return true; +} + +bool MergeGroup::_merge_similar(LocalVector &r_mis, bool p_shadows) { + if (!r_mis.size()) { + return false; + } + + MeshInstance *first = r_mis[0]; + + LocalVector list; + list.push_back(first); + + r_mis.remove_unordered(0); + + for (int n = 0; n < (int)r_mis.size(); n++) { + MeshInstance *mi = r_mis[n]; + + if (first->is_mergeable_with(mi, p_shadows)) { + list.push_back(mi); + r_mis.remove_unordered(n); + n--; + } + } + + // Don't whittle for shadows for now. + if (p_shadows) { + _merge_list(list, p_shadows); + return true; + } + + // If set to zero, we merge everything we can.. + // If set to 1, we merge nothing. + if (data.params[PARAM_GROUP_SIZE] <= 1) { + if (!data.params[PARAM_GROUP_SIZE] && (list.size() > 1)) { + _merge_list(list, p_shadows); + } + return true; + } + + int whittle_group = 0; + + LocalVector mesh_aabbs; + mesh_aabbs.resize(list.size()); + for (uint32_t n = 0; n < list.size(); n++) { + mesh_aabbs[n].mi = list[n]; + mesh_aabbs[n].aabb = list[n]->get_transformed_aabb(); + } + + _recursive_tree_merge(whittle_group, mesh_aabbs); + + return true; +} + +void MergeGroup::_recursive_tree_merge(int &r_whittle_group, LocalVector p_list) { + DEV_ASSERT(data.params[PARAM_GROUP_SIZE] > 1); + + // If less than the leaf size, merge + if (p_list.size() <= data.params[PARAM_GROUP_SIZE]) { + if (p_list.size() > 1) { + _merge_list_ex(p_list, false, r_whittle_group++); + } + return; + } + + // Attempt to split. + // Calculate AABB. + AABB aabb = p_list[0].aabb; + for (uint32_t n = 1; n < p_list.size(); n++) { + aabb.merge_with(p_list[n].aabb); + } + + int order[3]; + + order[0] = aabb.get_longest_axis_index(); + order[2] = aabb.get_shortest_axis_index(); + order[1] = 3 - (order[0] + order[2]); + + bool sort_ok = false; + + // Try sorting on each axis in order of longest first. + for (int n = 0; n < 3; n++) { + int axis = order[n]; + MeshAABB::_sort_axis = axis; + p_list.sort(); + + // Is this sorting ok? + // some epsilon? NYI + if (p_list[0].aabb.position.coord[axis] != p_list[p_list.size() - 1].aabb.position.coord[axis]) { + sort_ok = true; + break; + } + } + + // If the sort failed, they are all in kind of the same place, we will just merge them all + // and abandon the whittling... + if (!sort_ok) { + _merge_list_ex(p_list, false, r_whittle_group++); + return; + } + + // Sort was ok, lets split into 2 lists and call recursive. + LocalVector list_b; + int last = p_list.size() / 2; + for (int n = (int)p_list.size() - 1; n >= last; n--) { + list_b.push_back(p_list[n]); + p_list.remove_unordered(n); + } + + _recursive_tree_merge(r_whittle_group, p_list); + _recursive_tree_merge(r_whittle_group, list_b); +} + +void MergeGroup::_merge_list_ex(const LocalVector &p_mesh_aabbs, bool p_shadows, int p_whittle_group) { + LocalVector mis; + mis.resize(p_mesh_aabbs.size()); + + for (uint32_t n = 0; n < mis.size(); n++) { + mis[n] = p_mesh_aabbs[n].mi; + } + + _merge_list(mis, p_shadows, p_whittle_group); +} + +void MergeGroup::_merge_list(const LocalVector &p_mis, bool p_shadows, int p_whittle_group) { + MeshInstance *merged = memnew(MeshInstance); + MergingTool::_reparent(merged, this, data.scene_root); + _node_changed(this); + + String new_name; + if (p_shadows) { + new_name = "Shadow"; + } else { + new_name = "Merged"; + } + new_name += " [" + get_name() + "]"; + if (!p_shadows) { + new_name += " " + itos(data.iteration); + } + if (p_whittle_group != -1) { + new_name += " [wg " + itos(p_whittle_group) + "]"; + } + + merged->set_name(new_name); + + Vector varlist; + for (unsigned int n = 0; n < p_mis.size(); n++) { + varlist.push_back(Variant(p_mis[n])); + } + + if (!merged->merge_meshes(varlist, false, false, p_shadows)) { + _log("MERGE_MESHES failed."); + _delete_node(merged); + return; + } + + if (!p_shadows) { + // For deleting the old MeshInstances, we should not delete + // nodes that have children (e.g. Static physics). However MeshInstances + // that are deleted, can then free up parent MeshInstances for deletion, + // so we should call this in a recursive fashion. + LocalVector del_list = p_mis; + + if (data.delete_sources) { + _cleanup_source_meshes(del_list); + } + + if (data.convert_sources) { + // Any that have not been deleted can now be converted to spatials. + for (unsigned int n = 0; n < del_list.size(); n++) { + _convert_source_to_spatial(del_list[n]); + } + } else { + // or have their mesh set to NULL. + for (unsigned int n = 0; n < del_list.size(); n++) { + _reset_mesh_instance(del_list[n]); + } + } + + } else { + // Shadows .. turn shadow casting off for all these meshes. + for (unsigned int n = 0; n < p_mis.size(); n++) { + p_mis[n]->set_cast_shadows_setting(GeometryInstance::ShadowCastingSetting::SHADOW_CASTING_SETTING_OFF); + _node_changed(p_mis[n]); + } + } +} + +void MergeGroup::_cleanup_source_meshes(LocalVector &r_cleanup_list) { + for (unsigned int n = 0; n < r_cleanup_list.size(); n++) { + MeshInstance *mi = r_cleanup_list[n]; + Node *parent = mi->get_parent(); + _move_children(mi, parent, true); + _delete_node(mi); + _delete_dangling_spatials(parent); + } + + // All are deleted. + r_cleanup_list.clear(); +} + +void MergeGroup::_delete_node(Node *p_node) { + p_node->queue_delete(); + // This is only a problem in the editor, Godot saving code cannot currently deal with + // nodes queued for deletion, so they must be detached. + // This makes the whole process much slower after detaching, because of the logarithmic + // calling of notifications (Godot doesn't deal well with large numbers of nodes). +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint() && p_node->get_parent()) { + p_node->get_parent()->remove_child(p_node); + } +#endif +} + +bool MergeGroup::_node_ok_to_delete(Node *p_node) { + return (!MergingTool::_node_has_valid_children(p_node)) && (!p_node->get_script_instance()); +} + +void MergeGroup::_delete_dangling_spatials(Node *p_node) { + while (p_node) { + if (_node_ok_to_delete(p_node) && (get_class() == "Spatial")) { + Node *parent = p_node->get_parent(); + _delete_node(p_node); + p_node = parent; + } else { + return; + } + } +} + +void MergeGroup::_find_gridmap_recursive(int p_depth, Node *p_node, LocalVector &r_gridmaps) { +#ifdef MODULE_GRIDMAP_ENABLED + if (_terminate_search(p_node)) { + return; + } + + GridMap *gridmap = Object::cast_to(p_node); + if (gridmap && !gridmap->is_queued_for_deletion() && !gridmap->get_script_instance()) { + _logt(p_depth, "found GridMap : \"" + gridmap->get_name() + "\""); + r_gridmaps.push_back(gridmap); + } + + for (int c = p_node->get_child_count() - 1; c >= 0; c--) { + _find_gridmap_recursive(p_depth + 1, p_node->get_child(c), r_gridmaps); + } +#endif +} + +void MergeGroup::_bake_gridmap(GridMap *p_gridmap) { +#ifdef MODULE_GRIDMAP_ENABLED + Node *parent = p_gridmap->get_parent(); + ERR_FAIL_NULL(parent); + + Array meshes = p_gridmap->get_meshes(); + + Transform gridmap_xform = p_gridmap->get_transform(); + + for (int n = 0; n < meshes.size(); n++) { + Transform tr = meshes[n]; + n++; + Ref rmesh = meshes[n]; + + MeshInstance *mi = memnew(MeshInstance); + String new_name = String(p_gridmap->get_name()) + " [cell " + itos(n / 2) + "]"; + mi->set_name(new_name); + + MergingTool::_reparent(mi, parent, data.scene_root); + + mi->set_mesh(rmesh); + mi->set_transform(gridmap_xform * tr); + +#ifdef TOOLS_ENABLED + if (bake_substep_function) { + _log("baking gridmap, creating mesh instance \"" + new_name + "\""); + if (bake_substep_function((float)n / meshes.size(), new_name, nullptr, false)) { + break; + } + } +#endif + } + + _node_changed(p_gridmap); + + // ALTERNATIVE IMPLEMENTATION - may be better if we decide to bake physics reps + // Array cells = p_gridmap->get_used_cells(); + // Ref rmeshlib = p_gridmap->get_mesh_library(); + // Transform gridmap_xform = p_gridmap->get_transform(); + // real_t cell_scale = p_gridmap->get_cell_scale(); + + // for (int32_t k = 0; k < cells.size(); k++) { + // Vector3 cell_location = cells[k]; + // int x = Math::round(cell_location.x); + // int y = Math::round(cell_location.y); + // int z = Math::round(cell_location.z); + + // int32_t cell_item = p_gridmap->get_cell_item(x, y, z); + // if (cell_item == GridMap::INVALID_CELL_ITEM) { + // continue; + // } + // Transform cell_xform; + // int orientation = p_gridmap->get_cell_item_orientation(x, y, z); + // DEV_ASSERT(orientation != -1); + // cell_xform.basis.set_orthogonal_index(orientation); + // cell_xform.basis.scale(Vector3(cell_scale, cell_scale, cell_scale)); + // cell_xform.set_origin(p_gridmap->map_to_world(x, y, z)); + + // // may not be required, fire didn't have this + // //const Transform &item_xform = rmeshlib->get_item_mesh_transform(cell_item); + // //cell_xform *= item_xform; + + // MeshInstance *mi = memnew(MeshInstance); + // parent->add_child(mi); + // _set_reasonable_owner(mi); + + // String new_name = "Gridmap [" + p_gridmap->get_name() + "] " + itos(x) + "," + itos(y) + "," + itos(z); + // _log("creating " + new_name); + // mi->set_name(new_name); + + // mi->set_mesh(rmeshlib->get_item_mesh(cell_item)); + // mi->set_transform(gridmap_xform * cell_xform); + // } + + // Move children of source gridmap, so hiding doesn't affect them. + _move_children(p_gridmap, p_gridmap->get_parent(), true); + + // Set to invisible rather than delete to allow physics to work. + p_gridmap->set_visible(false); +#endif +} + +// Certain node types will terminate finding mesh instances etc, for convenience, +// as they will always be part of a different moving "block" and not suitable +// for static merging. +bool MergeGroup::_terminate_search(Node *p_node) { + if (Object::cast_to(p_node)) { + return true; + } + if (Object::cast_to(p_node)) { + return true; + } + if (Object::cast_to(p_node) && (p_node != this)) { + return true; + } + + return false; +} + +void MergeGroup::_find_csg_recursive(int p_depth, Node *p_node, LocalVector &r_csgs) { +#ifdef MODULE_CSG_ENABLED + if (_terminate_search(p_node)) { + return; + } + + CSGShape *shape = Object::cast_to(p_node); + if (shape && shape->is_merging_allowed() && !shape->is_queued_for_deletion() && !shape->get_script_instance()) { + // Is this the child of a CSG combiner? + CSGCombiner *parent = Object::cast_to(shape->get_parent()); + if (parent && parent->is_merging_allowed()) { + // Do not add children of combiners, as the combiner will use the children to generate + // the mesh. + // Possible problem: + // CSGShape children of CSGCombiners that themselves have children (e.g. static bodies?) + // What should we do with these? + _logt(p_depth, "found CSGShape with CSGCombiner parent : \"" + shape->get_name() + "\""); + } else { + r_csgs.push_back(shape); + _logt(p_depth, "found CSGShape : \"" + shape->get_name() + "\""); + } + } + + for (int c = p_node->get_child_count() - 1; c >= 0; c--) { + _find_csg_recursive(p_depth + 1, p_node->get_child(c), r_csgs); + } +#endif +} + +void MergeGroup::_find_mesh_instances_recursive(int p_depth, Node *p_node, LocalVector &r_mis, bool p_shadows, bool p_flag_invalid_meshes) { + if (_terminate_search(p_node)) { + return; + } + + MeshInstance *mi = Object::cast_to(p_node); + + if (mi && mi->is_merging_allowed() && !mi->is_queued_for_deletion() && !mi->get_script_instance()) { + Ref rmesh = mi->get_mesh(); + if (rmesh.is_valid()) { + if (rmesh->get_surface_count()) { + if (!p_shadows || mi->is_mergeable_with(mi, true)) { + r_mis.push_back(mi); + // _logt(p_depth, "found MeshInstance : \"" + mi->get_name() + "\""); + } + } // Contains surfaces. + else if (p_flag_invalid_meshes) { +#ifdef TOOLS_ENABLED + WARN_PRINT("MeshInstance \"" + mi->get_name() + "\" contains no surfaces."); +#endif + } + } // Mesh valid. + } + + // Important: + // Ensure meshes are added in reverse order. + // This is important for performance because + // it turns out queue_delete is very inefficient + // deleteing multiple child nodes from the front of the list + // due to ordered_remove() etc. + for (int c = p_node->get_child_count() - 1; c >= 0; c--) { + _find_mesh_instances_recursive(p_depth + 1, p_node->get_child(c), r_mis, p_shadows, p_flag_invalid_meshes); + } +} + +void MergeGroup::_reset_mesh_instance(MeshInstance *p_mi) { + p_mi->set_mesh(Ref()); + _node_changed(p_mi); +} + +// Convert any dangling MeshInstances to Spatials so they will be cheaper +// in the VIsualServer. They are only required now for relative positioning +// of children. +// Note that this will go horribly wrong if the user code keeps a +// reference / pointer to the source node before this stage, +// hence why this step is optional. +void MergeGroup::_convert_source_to_spatial(Spatial *p_node) { + ERR_FAIL_NULL(p_node); + if (p_node->get_script_instance()) { + return; + } + +#ifdef TOOLS_ENABLED + _log("converting source to Spatial \"" + p_node->get_name() + "\""); +#endif + + Node *parent = p_node->get_parent(); + + // This should not happen, as sources should always be under a merge group. + ERR_FAIL_NULL(parent); + + // Change the name of the node to be deleted. + String string_full_name = p_node->get_name(); + p_node->set_name("_merge_source_ " + string_full_name); + + // Create the new class T object. + Spatial *pNew = memnew(Spatial); + pNew->set_name(string_full_name); + + // Add the child at the same position as the old node + // (this is more convenient for users) + //parent->add_child_below_node(p_node, pNew); + MergingTool::_reparent(pNew, parent, data.scene_root); + + // New node should have same transform. + pNew->set_transform(p_node->get_transform()); + + // Move each child. + _move_children(p_node, pNew); + + // Delete old node. + _delete_node(p_node); +} + +void MergeGroup::set_param_enabled(ParamEnabled p_param, bool p_enabled) { + data.params_enabled[p_param] = p_enabled; +} + +bool MergeGroup::get_param_enabled(ParamEnabled p_param) { + return data.params_enabled[p_param]; +} + +void MergeGroup::set_param(Param p_param, int p_value) { + // Check specific param ranges. + switch (p_param) { + case PARAM_SPLITS_HORIZONTAL: + case PARAM_SPLITS_VERTICAL: { + p_value = CLAMP(p_value, 1, 16); + } break; + case PARAM_GROUP_SIZE: { + p_value = CLAMP(p_value, 0, 128); + } break; + default: + break; + } + + data.params[p_param] = (uint32_t)CLAMP(p_value, 0, INT32_MAX); +} + +int MergeGroup::get_param(Param p_param) { + return data.params[p_param]; +} + +MergeGroup::Data::Data() { + for (int n = 0; n < PARAM_ENABLED_MAX; n++) { + params_enabled[n] = false; + } + for (int n = 0; n < PARAM_MAX; n++) { + params[n] = 0; + } + + params_enabled[PARAM_ENABLED_AUTO_MERGE] = true; + params_enabled[PARAM_ENABLED_SHADOW_PROXY] = true; + params_enabled[PARAM_ENABLED_CONVERT_CSGS] = true; + params_enabled[PARAM_ENABLED_CONVERT_GRIDMAPS] = false; + params_enabled[PARAM_ENABLED_COMBINE_SURFACES] = true; + params_enabled[PARAM_ENABLED_CLEAN_MESHES] = false; + + params[PARAM_GROUP_SIZE] = 0; + params[PARAM_SPLITS_HORIZONTAL] = 1; + params[PARAM_SPLITS_VERTICAL] = 1; + params[PARAM_MIN_SPLIT_POLY_COUNT] = 1024; + + delete_sources = true; + convert_sources = true; + split_by_surface = true; + iteration = 0; +} diff --git a/scene/3d/merge_group.h b/scene/3d/merge_group.h new file mode 100644 index 000000000..397e2437b --- /dev/null +++ b/scene/3d/merge_group.h @@ -0,0 +1,155 @@ +#ifndef MERGE_GROUP_H +#define MERGE_GROUP_H + +/*************************************************************************/ +/* merge_group.h */ +/*************************************************************************/ +/* This file is part of: */ +/* PANDEMONIUM ENGINE */ +/* https://github.com/Relintai/pandemonium_engine */ +/*************************************************************************/ +/* Copyright (c) 2022-present Péter Magyar. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "scene/main/spatial.h" + +class MeshInstance; +class CSGShape; +class GridMap; + +class MergeGroup : public Spatial { + GDCLASS(MergeGroup, Spatial); + friend class MergeGroupEditorPlugin; + +public: + enum ParamEnabled { + PARAM_ENABLED_AUTO_MERGE, + PARAM_ENABLED_SHADOW_PROXY, + PARAM_ENABLED_CONVERT_CSGS, + PARAM_ENABLED_CONVERT_GRIDMAPS, + PARAM_ENABLED_COMBINE_SURFACES, + PARAM_ENABLED_CLEAN_MESHES, + PARAM_ENABLED_MAX, + }; + + enum Param { + PARAM_GROUP_SIZE, + PARAM_SPLITS_HORIZONTAL, + PARAM_SPLITS_VERTICAL, + PARAM_MIN_SPLIT_POLY_COUNT, + PARAM_MAX, + }; + + void merge_meshes(); + bool merge_meshes_in_editor(); + + void set_param_enabled(ParamEnabled p_param, bool p_enabled); + bool get_param_enabled(ParamEnabled p_param); + + void set_param(Param p_param, int p_value); + int get_param(Param p_param); + + // These enable feedback in the Godot UI as we bake. + typedef bool (*BakeStepFunc)(float, const String &, void *, bool); // Progress, step description, userdata, force refresh. + typedef void (*BakeEndFunc)(uint32_t); // time_started + + static BakeStepFunc bake_step_function; + static BakeStepFunc bake_substep_function; + static BakeEndFunc bake_end_function; + +protected: + static void _bind_methods(); + void _notification(int p_what); + +private: + struct MeshAABB { + MeshInstance *mi = nullptr; + AABB aabb; + static int _sort_axis; + bool operator<(const MeshAABB &p_b) const { + real_t a_min = aabb.position.coord[_sort_axis]; + real_t b_min = p_b.aabb.position.coord[_sort_axis]; + return a_min < b_min; + } + }; + + // Main function. + bool _merge_meshes(); + + // Merging. + void _find_mesh_instances_recursive(int p_depth, Node *p_node, LocalVector &r_mis, bool p_shadows, bool p_flag_invalid_meshes = false); + bool _merge_similar(LocalVector &r_mis, bool p_shadows); + void _merge_list(const LocalVector &p_mis, bool p_shadows, int p_whittle_group = -1); + void _merge_list_ex(const LocalVector &p_mesh_aabbs, bool p_shadows, int p_whittle_group = -1); + bool _join_similar(LocalVector &r_mis); + void _split_mesh_by_surface(MeshInstance *p_mi, int p_num_surfaces); + bool _split_by_locality(); + + // Helper funcs. + void _convert_source_to_spatial(Spatial *p_node); + void _reset_mesh_instance(MeshInstance *p_mi); + void _move_children(Node *p_from, Node *p_to, bool p_recalculate_transforms = false); + void _recursive_tree_merge(int &r_whittle_group, LocalVector p_list); + void _delete_node(Node *p_node); + bool _node_ok_to_delete(Node *p_node); + void _cleanup_source_meshes(LocalVector &r_cleanup_list); + void _delete_dangling_spatials(Node *p_node); + + void _node_changed(Node *p_node); + void _node_changed_internal(Node *p_node); + + // CSG + void _find_csg_recursive(int p_depth, Node *p_node, LocalVector &r_csgs); + void _split_csg_by_surface(CSGShape *p_shape); + + bool _terminate_search(Node *p_node); + + // Gridmap + void _find_gridmap_recursive(int p_depth, Node *p_node, LocalVector &r_gridmaps); + void _bake_gridmap(GridMap *p_gridmap); + + void _log(String p_string); + void _logt(int p_tabs, String p_string); + + struct Data { + bool params_enabled[PARAM_ENABLED_MAX]; + uint32_t params[PARAM_MAX]; + + // Hidden Params. + bool delete_sources = false; + bool convert_sources = false; + bool split_by_surface = false; + + // Each merge is an iteration. + uint32_t iteration = 0; + Node *scene_root = nullptr; + + Data(); + } data; +}; + +VARIANT_ENUM_CAST(MergeGroup::Param); +VARIANT_ENUM_CAST(MergeGroup::ParamEnabled); + +#endif // MERGE_GROUP_H diff --git a/scene/3d/mesh_instance.cpp b/scene/3d/mesh_instance.cpp index c26a79cc7..389f4883c 100644 --- a/scene/3d/mesh_instance.cpp +++ b/scene/3d/mesh_instance.cpp @@ -38,6 +38,7 @@ #include "scene/main/scene_string_names.h" #include "scene/resources/material/material.h" #include "scene/resources/material/spatial_material.h" +#include "scene/resources/mesh/merging_tool.h" #include "scene/resources/mesh/mesh.h" #include "servers/rendering/rendering_server_globals.h" @@ -884,8 +885,50 @@ void MeshInstance::create_debug_tangents() { } } -bool MeshInstance::merge_meshes(Vector p_list, bool p_use_global_space, bool p_check_compatibility) { - // bound function only support variants, so we need to convert to a list of MeshInstances +bool MeshInstance::split_by_surface(Vector p_destination_mesh_instances) { + ERR_FAIL_COND_V_MSG(!is_inside_tree(), false, "Source MeshInstance must be inside the SceneTree."); + + // For simplicity we are requiring that the destination MeshInstances have the same parent + // as the source. This means we can use identical transforms. + Node *parent = get_parent(); + ERR_FAIL_NULL_V_MSG(parent, false, "Source MeshInstance must have a parent node."); + + // Bound function only support variants, so we need to convert to a list of MeshInstances. + Vector mis; + + for (int n = 0; n < p_destination_mesh_instances.size(); n++) { + MeshInstance *mi = Object::cast_to(p_destination_mesh_instances[n]); + if (mi) { + if (mi != this) { + ERR_FAIL_COND_V_MSG(mi->get_parent() != parent, false, "Destination MeshInstances must be siblings of the source MeshInstance."); + + mis.push_back(mi); + } else { + ERR_FAIL_V_MSG(false, "Source MeshInstance cannot be a destination."); + } + } else { + ERR_FAIL_V_MSG(false, "Only MeshInstances can be split."); + } + } + + ERR_FAIL_COND_V_MSG(!get_mesh().is_valid(), false, "Mesh is invalid."); + ERR_FAIL_COND_V_MSG(mis.size() != get_mesh()->get_surface_count(), false, "Number of surfaces and number of destination MeshInstances must match."); + + // Go through each surface, and fill the relevant mesh instance. + const Mesh *source_mesh = get_mesh().ptr(); + DEV_ASSERT(source_mesh); + + ERR_FAIL_COND_V_MSG(source_mesh->get_surface_count() <= 1, false, "Source MeshInstance must contain multiple surfaces."); + + for (int s = 0; s < source_mesh->get_surface_count(); s++) { + MergingTool::split_surface_to_mesh_instance(*this, s, *mis[s]); + } + + return true; +} + +bool MeshInstance::merge_meshes(Vector p_list, bool p_use_global_space, bool p_check_compatibility, bool p_shadows_only) { + // Bound function only support variants, so we need to convert to a list of MeshInstances. Vector mis; for (int n = 0; n < p_list.size(); n++) { @@ -902,476 +945,28 @@ bool MeshInstance::merge_meshes(Vector p_list, bool p_use_global_space, } ERR_FAIL_COND_V(!mis.size(), "Array contains no MeshInstances"); - return _merge_meshes(mis, p_use_global_space, p_check_compatibility); + + if (p_shadows_only) { + return MergingTool::merge_shadow_meshes(*this, mis, p_use_global_space, p_check_compatibility); + } + + return MergingTool::merge_meshes(*this, mis, p_use_global_space, p_check_compatibility); } -bool MeshInstance::is_mergeable_with(Node *p_other) const { +bool MeshInstance::is_mergeable_with(Node *p_other, bool p_shadows_only) const { const MeshInstance *mi = Object::cast_to(p_other); if (mi) { - return _is_mergeable_with(*mi); + if (p_shadows_only) { + return MergingTool::is_shadow_mergeable_with(*this, *mi); + } else { + return MergingTool::is_mergeable_with(*this, *mi, true); + } } return false; } -bool MeshInstance::_is_mergeable_with(const MeshInstance &p_other) const { - if (!get_mesh().is_valid() || !p_other.get_mesh().is_valid()) { - return false; - } - if (!get_allow_merging() || !p_other.get_allow_merging()) { - return false; - } - - // various settings that must match - if (get_material_overlay() != p_other.get_material_overlay()) { - return false; - } - if (get_material_override() != p_other.get_material_override()) { - return false; - } - if (get_cast_shadows_setting() != p_other.get_cast_shadows_setting()) { - return false; - } - if (is_visible() != p_other.is_visible()) { - return false; - } - - Ref rmesh_a = get_mesh(); - Ref rmesh_b = p_other.get_mesh(); - - int num_surfaces = rmesh_a->get_surface_count(); - if (num_surfaces != rmesh_b->get_surface_count()) { - return false; - } - - for (int n = 0; n < num_surfaces; n++) { - // materials must match - if (get_active_material(n) != p_other.get_active_material(n)) { - return false; - } - - // formats must match - uint32_t format_a = rmesh_a->surface_get_format(n); - uint32_t format_b = rmesh_b->surface_get_format(n); - - if (format_a != format_b) { - return false; - } - } - - // NOTE : These three commented out sections below are more conservative - // checks for whether to allow mesh merging. I am not absolutely sure a priori - // how conservative we need to be, so we can further enable this if testing - // shows they are required. - - // if (get_surface_material_count() != p_other.get_surface_material_count()) { - // return false; - // } - - // for (int n = 0; n < get_surface_material_count(); n++) { - // if (get_surface_material(n) != p_other.get_surface_material(n)) { - // return false; - // } - // } - - // test only allow identical meshes - // if (get_mesh() != p_other.get_mesh()) { - // return false; - // } - - return true; -} - -void MeshInstance::_merge_into_mesh_data(const MeshInstance &p_mi, const Transform &p_dest_tr_inv, int p_surface_id, LocalVector &r_verts, LocalVector &r_norms, LocalVector &r_tangents, LocalVector &r_colors, LocalVector &r_uvs, LocalVector &r_uv2s, LocalVector &r_inds) { - _merge_log("\t\t\tmesh data from " + p_mi.get_name()); - - // get the mesh verts in local space - Ref rmesh = p_mi.get_mesh(); - - if (rmesh->get_surface_count() <= p_surface_id) { - return; - } - - Array arrays = rmesh->surface_get_arrays(p_surface_id); - - LocalVector verts = PoolVector(arrays[RS::ARRAY_VERTEX]); - if (!verts.size()) { - // early out if there are no vertices, no point in doing anything else - return; - } - - LocalVector normals = PoolVector(arrays[RS::ARRAY_NORMAL]); - LocalVector tangents = PoolVector(arrays[RS::ARRAY_TANGENT]); - LocalVector colors = PoolVector(arrays[RS::ARRAY_COLOR]); - LocalVector uvs = PoolVector(arrays[RS::ARRAY_TEX_UV]); - LocalVector uv2s = PoolVector(arrays[RS::ARRAY_TEX_UV2]); - LocalVector indices = PoolVector(arrays[RS::ARRAY_INDEX]); - - // The attributes present must match the first mesh for the attributes - // to remain in sync. Here we reject meshes with different attributes. - // We could alternatively invent missing attributes. - // This should hopefully be already caught by the mesh_format, but is included just in case here. - - // Don't perform these checks on the first Mesh, the first Mesh is a master - // and determines the attributes we want to be present. - if (r_verts.size() != 0) { - if ((bool)r_norms.size() != (bool)normals.size()) { - ERR_FAIL_MSG("Attribute mismatch with first Mesh (Normals), ignoring surface."); - } - if ((bool)r_tangents.size() != (bool)tangents.size()) { - ERR_FAIL_MSG("Attribute mismatch with first Mesh (Tangents), ignoring surface."); - } - if ((bool)r_colors.size() != (bool)colors.size()) { - ERR_FAIL_MSG("Attribute mismatch with first Mesh (Colors), ignoring surface."); - } - if ((bool)r_uvs.size() != (bool)uvs.size()) { - ERR_FAIL_MSG("Attribute mismatch with first Mesh (UVs), ignoring surface."); - } - if ((bool)r_uv2s.size() != (bool)uv2s.size()) { - ERR_FAIL_MSG("Attribute mismatch with first Mesh (UV2s), ignoring surface."); - } - } - - // The checking for valid triangles should be on WORLD SPACE vertices, - // NOT model space - - // special case, if no indices, create some - int num_indices_before = indices.size(); - if (!_ensure_indices_valid(indices, verts)) { - _merge_log("\tignoring INVALID TRIANGLES (duplicate indices or zero area triangle) detected in " + p_mi.get_name() + ", num inds before / after " + itos(num_indices_before) + " / " + itos(indices.size())); - } - - // the first index of this mesh is offset from the verts we already have stored in the merged mesh - int starting_index = r_verts.size(); - - // transform verts to world space - Transform tr = p_mi.get_global_transform(); - - // But relative to the destination transform. - // This can either be identity (when the destination is global space), - // or the global transform of the owner MeshInstance (if using local space is selected). - tr = p_dest_tr_inv * tr; - - // to transform normals - Basis normal_basis = tr.basis.inverse(); - normal_basis.transpose(); - - int num_verts = verts.size(); - - // verts - DEV_ASSERT(num_verts > 0); - int first_vert = r_verts.size(); - r_verts.resize(first_vert + num_verts); - Vector3 *dest_verts = &r_verts[first_vert]; - - for (int n = 0; n < num_verts; n++) { - Vector3 pt_world = tr.xform(verts[n]); - *dest_verts++ = pt_world; - } - - // normals - if (normals.size()) { - int first_norm = r_norms.size(); - r_norms.resize(first_norm + num_verts); - Vector3 *dest_norms = &r_norms[first_norm]; - for (int n = 0; n < num_verts; n++) { - Vector3 pt_norm = normal_basis.xform(normals[n]); - pt_norm.normalize(); - *dest_norms++ = pt_norm; - } - } - - // tangents - if (tangents.size()) { - int first_tang = r_tangents.size(); - r_tangents.resize(first_tang + (num_verts * 4)); - real_t *dest_tangents = &r_tangents[first_tang]; - - for (int n = 0; n < num_verts; n++) { - int tstart = n * 4; - Vector3 pt_tangent = Vector3(tangents[tstart], tangents[tstart + 1], tangents[tstart + 2]); - real_t fourth = tangents[tstart + 3]; - - pt_tangent = normal_basis.xform(pt_tangent); - pt_tangent.normalize(); - *dest_tangents++ = pt_tangent.x; - *dest_tangents++ = pt_tangent.y; - *dest_tangents++ = pt_tangent.z; - *dest_tangents++ = fourth; - } - } - - // colors - if (colors.size()) { - int first_col = r_colors.size(); - r_colors.resize(first_col + num_verts); - Color *dest_colors = &r_colors[first_col]; - - for (int n = 0; n < num_verts; n++) { - *dest_colors++ = colors[n]; - } - } - - // uvs - if (uvs.size()) { - int first_uv = r_uvs.size(); - r_uvs.resize(first_uv + num_verts); - Vector2 *dest_uvs = &r_uvs[first_uv]; - - for (int n = 0; n < num_verts; n++) { - *dest_uvs++ = uvs[n]; - } - } - - // uv2s - if (uv2s.size()) { - int first_uv2 = r_uv2s.size(); - r_uv2s.resize(first_uv2 + num_verts); - Vector2 *dest_uv2s = &r_uv2s[first_uv2]; - - for (int n = 0; n < num_verts; n++) { - *dest_uv2s++ = uv2s[n]; - } - } - - // indices - if (indices.size()) { - int first_ind = r_inds.size(); - r_inds.resize(first_ind + indices.size()); - int *dest_inds = &r_inds[first_ind]; - - for (unsigned int n = 0; n < indices.size(); n++) { - int ind = indices[n] + starting_index; - *dest_inds++ = ind; - } - } -} - -bool MeshInstance::_ensure_indices_valid(LocalVector &r_indices, const PoolVector &p_verts) const { - // no indices? create some - if (!r_indices.size()) { - _merge_log("\t\t\t\tindices are blank, creating..."); - - // indices are blank!! let's create some, assuming the mesh is using triangles - r_indices.resize(p_verts.size()); - - // this is assuming each triangle vertex is unique - for (unsigned int n = 0; n < r_indices.size(); n++) { - r_indices[n] = n; - } - } - - if (!_check_for_valid_indices(r_indices, p_verts, nullptr)) { - LocalVector new_inds; - _check_for_valid_indices(r_indices, p_verts, &new_inds); - - // copy the new indices - r_indices = new_inds; - - return false; - } - - return true; -} - -// check for invalid tris, or make a list of the valid triangles, depending on whether r_inds is set -bool MeshInstance::_check_for_valid_indices(const LocalVector &p_inds, const PoolVector &p_verts, LocalVector *r_inds) const { - int nTris = p_inds.size(); - nTris /= 3; - int indCount = 0; - - for (int t = 0; t < nTris; t++) { - int i0 = p_inds[indCount++]; - int i1 = p_inds[indCount++]; - int i2 = p_inds[indCount++]; - - bool ok = true; - - // if the indices are the same, the triangle is invalid - if (i0 == i1) { - ok = false; - } - if (i1 == i2) { - ok = false; - } - if (i0 == i2) { - ok = false; - } - - // check positions - if (ok) { - // vertex positions - const Vector3 &p0 = p_verts[i0]; - const Vector3 &p1 = p_verts[i1]; - const Vector3 &p2 = p_verts[i2]; - - // if the area is zero, the triangle is invalid (and will crash xatlas if we use it) - if (_triangle_is_degenerate(p0, p1, p2, 0.00001)) { - _merge_log("\t\tdetected zero area triangle, ignoring"); - ok = false; - } - } - - if (ok) { - // if the triangle is ok, we will output it if we are outputting - if (r_inds) { - r_inds->push_back(i0); - r_inds->push_back(i1); - r_inds->push_back(i2); - } - } else { - // if triangle not ok, return failed check if we are not outputting - if (!r_inds) { - return false; - } - } - } - - return true; -} - -bool MeshInstance::_triangle_is_degenerate(const Vector3 &p_a, const Vector3 &p_b, const Vector3 &p_c, real_t p_epsilon) const { - // not interested in the actual area, but numerical stability - Vector3 edge1 = p_b - p_a; - Vector3 edge2 = p_c - p_a; - - // for numerical stability keep these values reasonably high - edge1 *= 1024.0; - edge2 *= 1024.0; - - Vector3 vec = edge1.cross(edge2); - real_t sl = vec.length_squared(); - - if (sl <= p_epsilon) { - return true; - } - - return false; -} - -// If p_check_compatibility is set to false you MUST have performed a prior check using -// is_mergeable_with, otherwise you could get mismatching surface formats leading to graphical errors etc. -bool MeshInstance::_merge_meshes(Vector p_list, bool p_use_global_space, bool p_check_compatibility) { - if (p_list.size() < 1) { - // should not happen but just in case - return false; - } - - // use the first mesh instance to get common data like number of surfaces - const MeshInstance *first = p_list[0]; - - // Mesh compatibility checking. This is relatively expensive, so if done already (e.g. in Room system) - // this step can be avoided. - LocalVector compat_list; - if (p_check_compatibility) { - compat_list.resize(p_list.size()); - - for (int n = 0; n < p_list.size(); n++) { - compat_list[n] = false; - } - - compat_list[0] = true; - - for (uint32_t n = 1; n < compat_list.size(); n++) { - compat_list[n] = first->_is_mergeable_with(*p_list[n]); - - if (compat_list[n] == false) { - WARN_PRINT("MeshInstance " + p_list[n]->get_name() + " is incompatible for merging with " + first->get_name() + ", ignoring."); - } - } - } - - Ref am; - am.instance(); - - // If we want a local space result, we need the world space transform of this MeshInstance - // available to back transform verts from world space. - Transform dest_tr_inv; - if (!p_use_global_space) { - if (is_inside_tree()) { - dest_tr_inv = get_global_transform(); - dest_tr_inv.affine_invert(); - } else { - WARN_PRINT("MeshInstance must be inside tree to merge using local space, falling back to global space."); - } - } - - for (int s = 0; s < first->get_mesh()->get_surface_count(); s++) { - LocalVector verts; - LocalVector normals; - LocalVector tangents; - LocalVector colors; - LocalVector uvs; - LocalVector uv2s; - LocalVector inds; - - for (int n = 0; n < p_list.size(); n++) { - // Ignore if the mesh is incompatible - if (p_check_compatibility && (!compat_list[n])) { - continue; - } - - _merge_into_mesh_data(*p_list[n], dest_tr_inv, s, verts, normals, tangents, colors, uvs, uv2s, inds); - } // for n through source meshes - - if (!verts.size()) { - WARN_PRINT_ONCE("No vertices for surface"); - } - - // sanity check on the indices - for (unsigned int n = 0; n < inds.size(); n++) { - int i = inds[n]; - if ((unsigned int)i >= verts.size()) { - WARN_PRINT_ONCE("Mesh index out of range, invalid mesh, aborting"); - return false; - } - } - - Array arr; - arr.resize(Mesh::ARRAY_MAX); - arr[Mesh::ARRAY_VERTEX] = PoolVector(verts); - if (normals.size()) { - arr[Mesh::ARRAY_NORMAL] = PoolVector(normals); - } - if (tangents.size()) { - arr[Mesh::ARRAY_TANGENT] = PoolVector(tangents); - } - if (colors.size()) { - arr[Mesh::ARRAY_COLOR] = PoolVector(colors); - } - if (uvs.size()) { - arr[Mesh::ARRAY_TEX_UV] = PoolVector(uvs); - } - if (uv2s.size()) { - arr[Mesh::ARRAY_TEX_UV2] = PoolVector(uv2s); - } - arr[Mesh::ARRAY_INDEX] = PoolVector(inds); - - am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr, Array(), Mesh::ARRAY_COMPRESS_DEFAULT); - } // for s through surfaces - - // set all the surfaces on the mesh - set_mesh(am); - - // set merged materials - int num_surfaces = first->get_mesh()->get_surface_count(); - for (int n = 0; n < num_surfaces; n++) { - set_surface_material(n, first->get_active_material(n)); - } - - // set some properties to match the merged meshes - set_material_overlay(first->get_material_overlay()); - set_material_override(first->get_material_override()); - set_cast_shadows_setting(first->get_cast_shadows_setting()); - - return true; -} - -void MeshInstance::_merge_log(String p_string) const { - print_verbose(p_string); -} - void MeshInstance::_bind_methods() { ClassDB::bind_method(D_METHOD("set_mesh", "mesh"), &MeshInstance::set_mesh); ClassDB::bind_method(D_METHOD("get_mesh"), &MeshInstance::get_mesh); @@ -1414,8 +1009,8 @@ void MeshInstance::_bind_methods() { ClassDB::bind_method(D_METHOD("create_debug_tangents"), &MeshInstance::create_debug_tangents); ClassDB::set_method_flags("MeshInstance", "create_debug_tangents", METHOD_FLAGS_DEFAULT | METHOD_FLAG_EDITOR); - ClassDB::bind_method(D_METHOD("is_mergeable_with", "other_mesh_instance"), &MeshInstance::is_mergeable_with); - ClassDB::bind_method(D_METHOD("merge_meshes", "mesh_instances", "use_global_space", "check_compatibility"), &MeshInstance::merge_meshes, DEFVAL(Vector()), DEFVAL(false), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("is_mergeable_with", "other_mesh_instance", "shadows_only"), &MeshInstance::is_mergeable_with, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("merge_meshes", "mesh_instances", "use_global_space", "check_compatibility", "shadows_only"), &MeshInstance::merge_meshes, DEFVAL(false), DEFVAL(true), DEFVAL(false)); ClassDB::set_method_flags("MeshInstance", "merge_meshes", METHOD_FLAGS_DEFAULT); } diff --git a/scene/3d/mesh_instance.h b/scene/3d/mesh_instance.h index 95e407aca..96ae66f00 100644 --- a/scene/3d/mesh_instance.h +++ b/scene/3d/mesh_instance.h @@ -50,6 +50,8 @@ class NodePath; class MeshInstance : public GeometryInstance { GDCLASS(MeshInstance, GeometryInstance); + friend class CSGShape; + protected: Ref mesh; @@ -109,16 +111,6 @@ protected: void _update_skinning(); #endif -private: - // merging - bool _merge_meshes(Vector p_list, bool p_use_global_space, bool p_check_compatibility); - bool _is_mergeable_with(const MeshInstance &p_other) const; - void _merge_into_mesh_data(const MeshInstance &p_mi, const Transform &p_dest_tr_inv, int p_surface_id, LocalVector &r_verts, LocalVector &r_norms, LocalVector &r_tangents, LocalVector &r_colors, LocalVector &r_uvs, LocalVector &r_uv2s, LocalVector &r_inds); - bool _ensure_indices_valid(LocalVector &r_indices, const PoolVector &p_verts) const; - bool _check_for_valid_indices(const LocalVector &p_inds, const PoolVector &p_verts, LocalVector *r_inds) const; - bool _triangle_is_degenerate(const Vector3 &p_a, const Vector3 &p_b, const Vector3 &p_c, real_t p_epsilon) const; - void _merge_log(String p_string) const; - protected: bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; @@ -162,9 +154,10 @@ public: void create_debug_tangents(); - // merging - bool is_mergeable_with(Node *p_other) const; - bool merge_meshes(Vector p_list, bool p_use_global_space, bool p_check_compatibility); + // Merging. + bool is_mergeable_with(Node *p_other, bool p_shadows_only) const; + bool merge_meshes(Vector p_list, bool p_use_global_space, bool p_check_compatibility, bool p_shadows_only); + bool split_by_surface(Vector p_destination_mesh_instances); virtual AABB get_aabb() const; virtual PoolVector get_faces(uint32_t p_usage_flags) const; diff --git a/scene/3d/room_manager.cpp b/scene/3d/room_manager.cpp index c127b82af..d1629bf62 100644 --- a/scene/3d/room_manager.cpp +++ b/scene/3d/room_manager.cpp @@ -2188,7 +2188,7 @@ void RoomManager::_merge_meshes_in_room(Room *p_room) { if (!bf.get_bit(c)) { MeshInstance *b = source_meshes[c]; - if (a->is_mergeable_with(b)) { + if (a->is_mergeable_with(b, false)) { merge_list.push_back(b); bf.set_bit(c, true); } @@ -2212,7 +2212,7 @@ void RoomManager::_merge_meshes_in_room(Room *p_room) { variant_merge_list.set(i, merge_list[i]); } - if (merged->merge_meshes(variant_merge_list, true, false)) { + if (merged->merge_meshes(variant_merge_list, true, false, false)) { // set all the source meshes to portal mode ignore so not shown for (int i = 0; i < merge_list.size(); i++) { merge_list[i]->set_portal_mode(CullInstance::PORTAL_MODE_IGNORE); @@ -2317,8 +2317,10 @@ void RoomManager::_list_mergeable_mesh_instances(Spatial *p_node, LocalVector(mi) && !_name_ends_with(mi, "-bound") && !mi->is_queued_for_deletion()) { - // only merge if visible - if (mi->is_inside_tree() && mi->is_visible()) { + // Only merge if visible. + // N.B. get_allow_merging() is the old flag on CullInstance, and is maintained for backward compatibility only. + // It is overruled by the Spatial "merging_mode" if this is set. + if (mi->is_inside_tree() && mi->is_visible() && mi->get_allow_merging()) { r_list.push_back(mi); } } diff --git a/scene/3d/visual_instance.cpp b/scene/3d/visual_instance.cpp index de705ca42..f13bc14ca 100644 --- a/scene/3d/visual_instance.cpp +++ b/scene/3d/visual_instance.cpp @@ -307,9 +307,10 @@ bool GeometryInstance::get_flag(Flags p_flag) const { } void GeometryInstance::set_cast_shadows_setting(ShadowCastingSetting p_shadow_casting_setting) { - shadow_casting_setting = p_shadow_casting_setting; - - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(get_instance(), (RS::ShadowCastingSetting)p_shadow_casting_setting); + if (p_shadow_casting_setting != shadow_casting_setting) { + shadow_casting_setting = p_shadow_casting_setting; + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(get_instance(), (RS::ShadowCastingSetting)p_shadow_casting_setting); + } } GeometryInstance::ShadowCastingSetting GeometryInstance::get_cast_shadows_setting() const { @@ -318,8 +319,11 @@ GeometryInstance::ShadowCastingSetting GeometryInstance::get_cast_shadows_settin void GeometryInstance::set_extra_cull_margin(float p_margin) { ERR_FAIL_COND(p_margin < 0); - extra_cull_margin = p_margin; - RS::get_singleton()->instance_set_extra_visibility_margin(get_instance(), extra_cull_margin); + + if (p_margin != extra_cull_margin) { + extra_cull_margin = p_margin; + RS::get_singleton()->instance_set_extra_visibility_margin(get_instance(), extra_cull_margin); + } } float GeometryInstance::get_extra_cull_margin() const { diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 9641bcb70..dd9455333 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -2657,6 +2657,14 @@ Node *Node::_duplicate(int p_flags, RBMap *r_duplimap) con bool instanced = false; + // No need to load a packed scene more than once if features + // several times in the branch being duplicated. + struct LoadedPackedScene { + Ref scene; + String filename; + }; + LocalVector loaded_scenes; + if (Object::cast_to(this)) { const InstancePlaceholder *ip = Object::cast_to(this); InstancePlaceholder *nip = memnew(InstancePlaceholder); @@ -2664,7 +2672,25 @@ Node *Node::_duplicate(int p_flags, RBMap *r_duplimap) con node = nip; } else if ((p_flags & DUPLICATE_USE_INSTANCING) && get_filename() != String()) { - Ref res = ResourceLoader::load(get_filename()); + // already loaded? + int found = -1; + for (unsigned int n = 0; n < loaded_scenes.size(); n++) { + if (loaded_scenes[n].filename == get_filename()) { + found = n; + break; + } + } + Ref res = Ref(); + if (found != -1) { + res = loaded_scenes[found].scene; + } else { + LoadedPackedScene ps; + ps.filename = get_filename(); + ps.scene = ResourceLoader::load(get_filename()); + res = ps.scene; + loaded_scenes.push_back(ps); + } + ERR_FAIL_COND_V(res.is_null(), nullptr); PackedScene::GenEditState ges = PackedScene::GEN_EDIT_STATE_DISABLED; #ifdef TOOLS_ENABLED diff --git a/scene/main/spatial.cpp b/scene/main/spatial.cpp index dfa8b4ab6..1549882a7 100644 --- a/scene/main/spatial.cpp +++ b/scene/main/spatial.cpp @@ -166,6 +166,14 @@ void Spatial::_notification(int p_what) { data.toplevel_active = true; } + if (data.merging_mode == MERGING_MODE_INHERIT) { + bool merging_allowed = true; // Root node default is for merging to be on + if (data.parent) { + merging_allowed = data.parent->is_merging_allowed(); + } + _propagate_merging_allowed(merging_allowed); + } + data.dirty |= DIRTY_GLOBAL; //global is always dirty upon entering a scene _notify_dirty(); @@ -720,6 +728,35 @@ void Spatial::_propagate_visibility_changed() { } } +void Spatial::_propagate_merging_allowed(bool p_merging_allowed) { + switch (data.merging_mode) { + case MERGING_MODE_INHERIT: + // Keep the parent p_allow_merging. + break; + case MERGING_MODE_OFF: { + p_merging_allowed = false; + } break; + case MERGING_MODE_ON: { + p_merging_allowed = true; + } break; + } + + // No change? No need to propagate further. + if (data.merging_allowed == p_merging_allowed) { + return; + } + + data.merging_allowed = p_merging_allowed; + + for (List::Element *E = data.children.front(); E; E = E->next()) { + Spatial *c = E->get(); + if (!c) { + continue; + } + c->_propagate_merging_allowed(p_merging_allowed); + } +} + void Spatial::show() { if (data.visible) { return; @@ -899,6 +936,32 @@ bool Spatial::is_local_transform_notification_enabled() const { return data.notify_local_transform; } +void Spatial::set_merging_mode(MergingMode p_mode) { + if (data.merging_mode == p_mode) { + return; + } + + data.merging_mode = p_mode; + + bool merging_allowed = true; // Default for root node. + + switch (p_mode) { + case MERGING_MODE_INHERIT: { + if (get_parent_spatial()) { + merging_allowed = get_parent_spatial()->is_merging_allowed(); + } + } break; + case MERGING_MODE_OFF: { + merging_allowed = false; + } break; + case MERGING_MODE_ON: { + merging_allowed = true; + } break; + } + + _propagate_merging_allowed(merging_allowed); +} + void Spatial::force_update_transform() { ERR_FAIL_COND(!is_inside_tree()); if (!xform_change.in_list()) { @@ -977,6 +1040,9 @@ void Spatial::_bind_methods() { ClassDB::bind_method(D_METHOD("look_at", "target", "up"), &Spatial::look_at); ClassDB::bind_method(D_METHOD("look_at_from_position", "position", "target", "up"), &Spatial::look_at_from_position); + ClassDB::bind_method(D_METHOD("set_merging_mode", "mode"), &Spatial::set_merging_mode); + ClassDB::bind_method(D_METHOD("get_merging_mode"), &Spatial::get_merging_mode); + ClassDB::bind_method(D_METHOD("to_local", "global_point"), &Spatial::to_local); ClassDB::bind_method(D_METHOD("to_global", "local_point"), &Spatial::to_global); @@ -987,6 +1053,10 @@ void Spatial::_bind_methods() { BIND_CONSTANT(NOTIFICATION_ENTER_GAMEPLAY); BIND_CONSTANT(NOTIFICATION_EXIT_GAMEPLAY); + BIND_ENUM_CONSTANT(MERGING_MODE_INHERIT); + BIND_ENUM_CONSTANT(MERGING_MODE_OFF); + BIND_ENUM_CONSTANT(MERGING_MODE_ON); + ADD_GROUP("Transform", ""); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "translation", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_translation", "get_translation"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "rotation_degrees", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_rotation_degrees", "get_rotation_degrees"); @@ -1001,6 +1071,9 @@ void Spatial::_bind_methods() { ADD_GROUP("Visibility", ""); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible"); + ADD_GROUP("Misc", ""); + ADD_PROPERTY(PropertyInfo(Variant::INT, "merging_mode", PROPERTY_HINT_ENUM, "Inherit,Off,On"), "set_merging_mode", "get_merging_mode"); + ADD_SIGNAL(MethodInfo("visibility_changed")); ADD_SIGNAL(MethodInfo("gameplay_entered")); ADD_SIGNAL(MethodInfo("gameplay_exited")); @@ -1019,6 +1092,8 @@ Spatial::Spatial() : data.visible = true; data.disable_scale = false; data.vi_visible = true; + data.merging_allowed = true; + data.merging_mode = MERGING_MODE_INHERIT; data.client_physics_interpolation_data = nullptr; diff --git a/scene/main/spatial.h b/scene/main/spatial.h index 9b852dd9a..9ac0d5bcd 100644 --- a/scene/main/spatial.h +++ b/scene/main/spatial.h @@ -55,6 +55,14 @@ class Spatial : public Node { GDCLASS(Spatial, Node); OBJ_CATEGORY("3D"); +public: + enum MergingMode : unsigned int { + MERGING_MODE_INHERIT, + MERGING_MODE_OFF, + MERGING_MODE_ON + }; + +private: // optionally stored if we need to do interpolation // client side (i.e. not in RenderingServer) so interpolated transforms // can be read back with get_global_transform_interpolated() @@ -83,6 +91,8 @@ class Spatial : public Node { mutable int dirty; + MergingMode merging_mode : 2; + bool toplevel_active : 1; bool toplevel : 1; bool inside_world : 1; @@ -99,6 +109,8 @@ class Spatial : public Node { bool visible : 1; bool disable_scale : 1; + bool merging_allowed : 1; + int children_lock; Spatial *parent; List children; @@ -120,6 +132,7 @@ class Spatial : public Node { void _propagate_transform_changed(Spatial *p_origin); void _propagate_visibility_changed(); + void _propagate_merging_allowed(bool p_merging_allowed); protected: _FORCE_INLINE_ void set_ignore_transform_notification(bool p_ignore) { @@ -235,6 +248,10 @@ public: void set_notify_local_transform(bool p_enable); bool is_local_transform_notification_enabled() const; + void set_merging_mode(MergingMode p_mode); + MergingMode get_merging_mode() const { return data.merging_mode; } + _FORCE_INLINE_ bool is_merging_allowed() const { return data.merging_allowed; } + void orthonormalize(); void set_identity(); @@ -250,4 +267,6 @@ public: ~Spatial(); }; +VARIANT_ENUM_CAST(Spatial::MergingMode); + #endif diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index bffb7c108..f4abcdede 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -199,6 +199,7 @@ #include "scene/3d/interpolated_camera.h" #include "scene/3d/light.h" #include "scene/3d/listener.h" +#include "scene/3d/merge_group.h" #include "scene/3d/mesh_instance.h" #include "scene/3d/multimesh_instance.h" #include "scene/3d/navigation.h" @@ -463,6 +464,7 @@ void register_scene_types() { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::set_class_enabled("RootMotionView", false); //disabled by default, enabled by editor diff --git a/scene/resources/mesh/merging_tool.cpp b/scene/resources/mesh/merging_tool.cpp new file mode 100644 index 000000000..1e8800a4f --- /dev/null +++ b/scene/resources/mesh/merging_tool.cpp @@ -0,0 +1,1118 @@ +/*************************************************************************/ +/* merging_tool.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* PANDEMONIUM ENGINE */ +/* https://github.com/Relintai/pandemonium_engine */ +/*************************************************************************/ +/* Copyright (c) 2022-present Péter Magyar. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "merging_tool.h" + +#include "core/config/engine.h" +#include "core/os/os.h" +#include "scene/3d/mesh_instance.h" +#include "scene/resources/material/spatial_material.h" +#include "scene/resources/mesh/surface_tool.h" + +#include "modules/modules_enabled.gen.h" // For csg. +#ifdef MODULE_CSG_ENABLED +#include "modules/csg/csg_shape.h" +#endif + +bool MergingTool::_is_material_opaque(const Ref &p_mat) { + if (p_mat.is_null()) { + return true; + } + + Ref material = p_mat; + if (material.is_null()) { + // Shaders not yet supported. + return false; + } + + if (material->get_feature(SpatialMaterial::FEATURE_TRANSPARENT)) { + return false; + } + + // Not sure if this can only occur with FEATURE_TRANSPARENT? + if (material->get_flag(SpatialMaterial::FLAG_USE_ALPHA_SCISSOR)) { + return false; + } + + // Only supporting default cull mode for now. + if (material->get_cull_mode() != SpatialMaterial::CULL_BACK) { + return false; + } + + return true; +} + +bool MergingTool::_is_shadow_mergeable(const MeshInstance &p_mi) { + if (p_mi.get_cast_shadows_setting() == GeometryInstance::ShadowCastingSetting::SHADOW_CASTING_SETTING_OFF) { + return false; + } + + if (!_is_material_opaque(p_mi.get_material_overlay())) { + return false; + } + + if (!_is_material_opaque(p_mi.get_material_override())) { + return false; + } + + int num_surfaces = p_mi.get_mesh()->get_surface_count(); + for (int n = 0; n < num_surfaces; n++) { + if (!_is_material_opaque(p_mi.get_active_material(n))) { + return false; + } + } + + return true; +} + +bool MergingTool::is_shadow_mergeable_with(const MeshInstance &p_mi, const MeshInstance &p_other) { + // Various settings that must match. + if (!_is_mergeable_with_common(p_mi, p_other)) { + return false; + } + + if (!_is_shadow_mergeable(p_mi) || !_is_shadow_mergeable(p_other)) { + return false; + } + + return true; +} + +bool MergingTool::_is_mergeable_with_common(const MeshInstance &p_mi, const MeshInstance &p_other) { + if (!p_mi.get_mesh().is_valid() || !p_other.get_mesh().is_valid()) { + return false; + } + if (!p_mi.is_merging_allowed() || !p_other.is_merging_allowed()) { + return false; + } + + if (p_mi.get_cast_shadows_setting() != p_other.get_cast_shadows_setting()) { + return false; + } + if (p_mi.is_visible() != p_other.is_visible()) { + return false; + } + if (p_mi.is_visible_in_tree() != p_other.is_visible_in_tree()) { + return false; + } + if (p_mi.get_layer_mask() != p_other.get_layer_mask()) { + return false; + } + + if (p_mi.get_portal_mode() != p_other.get_portal_mode()) { + return false; + } + if (p_mi.get_include_in_bound() != p_other.get_include_in_bound()) { + return false; + } + if (p_mi.get_portal_autoplace_priority() != p_other.get_portal_autoplace_priority()) { + return false; + } + if (p_mi.get_extra_cull_margin() != p_other.get_extra_cull_margin()) { + return false; + } + + return true; +} + +bool MergingTool::is_mergeable_with(const MeshInstance &p_mi, const MeshInstance &p_other, bool p_check_surface_material_match) { + if (!_is_mergeable_with_common(p_mi, p_other)) { + return false; + } + + // Various settings that must match. + if (p_mi.is_visible() != p_other.is_visible()) { + return false; + } + if (p_mi.get_material_overlay() != p_other.get_material_overlay()) { + return false; + } + if (p_mi.get_material_override() != p_other.get_material_override()) { + return false; + } + /* + if (p_mi.get_flag(GeometryInstance::FLAG_USE_BAKED_LIGHT) != p_other.get_flag(GeometryInstance::FLAG_USE_BAKED_LIGHT)) { + return false; + } + if (p_mi.get_generate_lightmap() != p_other.get_generate_lightmap()) { + return false; + } + if (p_mi.get_lightmap_scale() != p_other.get_lightmap_scale()) { + return false; + } + */ + + if (p_check_surface_material_match) { + Ref rmesh_a = p_mi.get_mesh(); + Ref rmesh_b = p_other.get_mesh(); + + int num_surfaces = rmesh_a->get_surface_count(); + if (num_surfaces != rmesh_b->get_surface_count()) { + return false; + } + + for (int n = 0; n < num_surfaces; n++) { + // Materials must match. + if (p_mi.get_active_material(n) != p_other.get_active_material(n)) { + return false; + } + + // Formats must match. + uint32_t format_a = rmesh_a->surface_get_format(n); + uint32_t format_b = rmesh_b->surface_get_format(n); + + if (format_a != format_b) { + return false; + } + } + } + + // NOTE : These three commented out sections below are more conservative + // checks for whether to allow mesh merging. I am not absolutely sure a priori + // how conservative we need to be, so we can further enable this if testing + // shows they are required. + + // if (get_surface_material_count() != p_other.get_surface_material_count()) { + // return false; + // } + + // for (int n = 0; n < get_surface_material_count(); n++) { + // if (get_surface_material(n) != p_other.get_surface_material(n)) { + // return false; + // } + // } + + // test only allow identical meshes + // if (get_mesh() != p_other.get_mesh()) { + // return false; + // } + + return true; +} + +void MergingTool::split_mesh_instance_by_locality(MeshInstance &r_mi, const AABB &p_bound, uint32_t p_splits_horz, uint32_t p_splits_vert, uint32_t p_min_split_poly_count) { + Ref rmesh = r_mi.get_mesh(); + if (!rmesh.is_valid()) { + return; + } + + // Need a parent to attach results to. + if (!r_mi.get_parent()) { + return; + } + + Vector3 cell_size = p_bound.size; + cell_size.x /= p_splits_horz; + cell_size.y /= p_splits_vert; + cell_size.z /= p_splits_horz; + + DEV_ASSERT(p_splits_horz); + DEV_ASSERT(p_splits_vert); + int splits_horz_minus_one = p_splits_horz - 1; + int splits_vert_minus_one = p_splits_vert - 1; + + // This is to prevent a warning as error in release builds, as this is only used + // for DEV_ASSERT +#ifdef DEV_ENABLED + uint32_t total_zones = p_splits_horz * p_splits_horz * p_splits_vert; +#endif + + AABB aabb; + aabb.size = cell_size; + + ERR_FAIL_COND(!r_mi.is_inside_tree()); + Transform xform = r_mi.get_global_transform(); + + SurfaceTool st_main; + for (int s = 0; s < rmesh->get_surface_count(); s++) { + st_main.create_from(rmesh, s); + + uint32_t tri_count = st_main.get_num_draw_vertices() / 3; + + // Bug .. we want to keep this surface in this case! and not delete the whole mesh instance? + // at the moment this ASSUMES there is only one surface. + if (tri_count < p_min_split_poly_count) { + continue; + } + + // Input for bounds routine should be deindexed. + st_main.deindex(); + + // Assign each triangle to a split zone. + uint32_t num_tris = st_main.vertex_array.size() / 3; + + Vector3 v[3]; + const SurfaceTool::Vertex *input = st_main.vertex_array.ptr(); + + LocalVector tri_ids; + tri_ids.resize(num_tris); + + for (uint32_t t = 0; t < num_tris; t++) { + // Split in world space. + Vector3 center; + + for (int c = 0; c < 3; c++) { + v[c] = input->vertex; + input++; + v[c] = xform.xform(v[c]); + center += v[c]; + } + center /= 3; + + // Get relative to bound. + center -= p_bound.position; + + // Find the x y z . + center /= cell_size; + int x = center.x; + int y = center.y; + int z = center.z; + x = CLAMP(x, 0, splits_horz_minus_one); + y = CLAMP(y, 0, splits_vert_minus_one); + z = CLAMP(z, 0, splits_horz_minus_one); + + uint32_t id = (x + (z * p_splits_horz) + (y * p_splits_horz * p_splits_vert)); + tri_ids[t] = id; + DEV_ASSERT(id < total_zones); + } + + for (uint32_t x = 0; x < p_splits_horz; x++) { + for (uint32_t y = 0; y < p_splits_vert; y++) { + for (uint32_t z = 0; z < p_splits_horz; z++) { + uint32_t id = (x + (z * p_splits_horz) + (y * p_splits_horz * p_splits_vert)); + _split_mesh_instance_by_locality(st_main, r_mi, tri_ids, id, s, x, y, z); + } + } + } + + } // for s +} + +void MergingTool::_split_mesh_instance_by_locality(const SurfaceTool &p_st_main, const MeshInstance &p_source_mi, const LocalVector &p_tri_ids, uint32_t p_local_id, uint32_t p_surface_id, uint32_t p_x, uint32_t p_y, uint32_t p_z) { + SurfaceTool st; + int num_inds = st.create_from_subset(p_st_main, p_tri_ids, p_local_id); + + // This could be quite common, bounds with no triangles within. + if (!num_inds) { + return; + } + + Node *parent = p_source_mi.get_parent(); + DEV_ASSERT(parent); + + // Create a mesh instance to hold this "zone". + MeshInstance *sib = memnew(MeshInstance); + parent->add_child(sib); + sib->set_owner(p_source_mi.get_owner()); + + String new_name = String(p_source_mi.get_name()); + if (p_surface_id) { + new_name += " _surf_" + itos(p_surface_id); + } + new_name += " split (" + itos(p_x) + "," + itos(p_y) + "," + itos(p_z) + ")"; + + sib->set_name(new_name); + +#ifdef TOOLS_ENABLED +#if 0 + _merge_log("_split_mesh_instance_by_locality " + itos(num_inds) + " inds : " + new_name); +#endif +#endif + + Ref am; + am.instance(); + + Array arr = st.commit_to_arrays(); + am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr, Array(), Mesh::ARRAY_COMPRESS_DEFAULT); + + // Set all the surfaces on the mesh. + sib->set_mesh(am); + + _copy_mesh_instance_settings(p_source_mi, *sib, true, true); +} + +bool MergingTool::clean_mesh_instance(MeshInstance &p_mi) { + Ref rmesh = p_mi.get_mesh(); + ERR_FAIL_COND_V(!rmesh.is_valid(), false); + ERR_FAIL_COND_V(!p_mi.is_inside_tree(), false); + + Transform tr = p_mi.get_global_transform(); + String name = p_mi.get_name(); + + bool data_changed = false; + + Ref am; + am.instance(); + + int inds_removed = 0; + + for (int s = 0; s < rmesh->get_surface_count(); s++) { + inds_removed += _clean_mesh_surface(name, tr, rmesh, s, am); + } + + if (inds_removed) { + _merge_log("cleaning MeshInstance \"" + p_mi.get_name() + "\" removed " + itos(inds_removed) + " indices.", 2); + p_mi.set_mesh(am); + } + + return data_changed; +} + +int MergingTool::_clean_mesh_surface(const String &p_source_name, const Transform &p_xform, Ref &p_rmesh, int p_surface_id, Ref r_dest_mesh) { + Array arrays = p_rmesh->surface_get_arrays(p_surface_id); + LocalVector verts = PoolVector(arrays[RS::ARRAY_VERTEX]); + if (!verts.size()) { + // Early out if there are no vertices, no point in doing anything else. + return 0; + } + LocalVector indices = PoolVector(arrays[RS::ARRAY_INDEX]); + + // Transform verts to world space. + for (uint32_t n = 0; n < verts.size(); n++) { + verts[n] = p_xform.xform(verts[n]); + } + + // Special case, if no indices, create some. + unsigned int num_indices_before = indices.size(); + if (!_ensure_indices_valid(indices, verts)) { +#ifdef TOOLS_ENABLED + _merge_log("\tignoring INVALID TRIANGLES (duplicate indices or zero area triangle) detected in " + p_source_name + ", num inds before / after " + itos(num_indices_before) + " / " + itos(indices.size())); +#endif + + // Save the modified index array. + arrays[RS::ARRAY_INDEX] = PoolVector(indices); + + // Note we aren't removing the unused verts here, to save hassle, but hopefully there won't be too many. + r_dest_mesh->add_surface_from_arrays(p_rmesh->surface_get_primitive_type(p_surface_id), arrays); + r_dest_mesh->surface_set_material(p_surface_id, p_rmesh->surface_get_material(p_surface_id)); + + // Returns true if data changed. + if (indices.size() >= num_indices_before) { + ERR_PRINT_ONCE("Indices after cleaning is higher than before."); + return 1; + } + return num_indices_before - indices.size(); + } + // Still add the surface, as a later one may be modified. + r_dest_mesh->add_surface_from_arrays(p_rmesh->surface_get_primitive_type(p_surface_id), arrays); + r_dest_mesh->surface_set_material(p_surface_id, p_rmesh->surface_get_material(p_surface_id)); + + return 0; +} + +bool MergingTool::_ensure_indices_valid(LocalVector &r_indices, const PoolVector &p_verts) { + // No indices? create some. + if (!r_indices.size()) { +#ifdef TOOLS_ENABLED + _merge_log("\t\t\t\tindices are blank, creating..."); +#endif + + // Indices are blank!! Let's create some, assuming the mesh is using triangles. + r_indices.resize(p_verts.size()); + + // This is assuming each triangle vertex is unique. + for (unsigned int n = 0; n < r_indices.size(); n++) { + r_indices[n] = n; + } + } + + if (!_check_for_valid_indices(r_indices, p_verts, nullptr)) { + LocalVector new_inds; + _check_for_valid_indices(r_indices, p_verts, &new_inds); + + // Copy the new indices. + r_indices = new_inds; + + return false; + } + + return true; +} + +// Check for invalid tris, or make a list of the valid triangles, depending on whether r_inds is set. +bool MergingTool::_check_for_valid_indices(const LocalVector &p_inds, const PoolVector &p_verts, LocalVector *r_inds) { + int nTris = p_inds.size(); + nTris /= 3; + int indCount = 0; + + for (int t = 0; t < nTris; t++) { + int i0 = p_inds[indCount++]; + int i1 = p_inds[indCount++]; + int i2 = p_inds[indCount++]; + + bool ok = true; + + // If the indices are the same, the triangle is invalid. + if (i0 == i1) { + ok = false; + } + if (i1 == i2) { + ok = false; + } + if (i0 == i2) { + ok = false; + } + + // Check positions. + if (ok) { + // Vertex positions. + const Vector3 &p0 = p_verts[i0]; + const Vector3 &p1 = p_verts[i1]; + const Vector3 &p2 = p_verts[i2]; + + // If the area is zero, the triangle is invalid (and will crash xatlas if we use it). + if (_triangle_is_degenerate(p0, p1, p2, 0.00001)) { +#ifdef TOOLS_ENABLED + _merge_log("\t\tdetected zero area triangle, ignoring"); +#endif + ok = false; + } + } + + if (ok) { + // If the triangle is ok, we will output it if we are outputting. + if (r_inds) { + r_inds->push_back(i0); + r_inds->push_back(i1); + r_inds->push_back(i2); + } + } else { + // If triangle not ok, return failed check if we are not outputting. + if (!r_inds) { + return false; + } + } + } + + return true; +} + +bool MergingTool::_triangle_is_degenerate(const Vector3 &p_a, const Vector3 &p_b, const Vector3 &p_c, real_t p_epsilon) { + // Not interested in the actual area, but numerical stability. + Vector3 edge1 = p_b - p_a; + Vector3 edge2 = p_c - p_a; + + // For numerical stability keep these values reasonably high. + edge1 *= 1024.0; + edge2 *= 1024.0; + + Vector3 vec = edge1.cross(edge2); + real_t sl = vec.length_squared(); + + if (sl <= p_epsilon) { + return true; + } + + return false; +} + +// If p_check_compatibility is set to false you MUST have performed a prior check using +// is_shadow_mergeable_with, otherwise you could get mismatching surface formats leading to graphical errors etc. +bool MergingTool::merge_shadow_meshes(MeshInstance &r_dest_mi, Vector p_list, bool p_use_global_space, bool p_check_compatibility) { + ERR_FAIL_COND_V(p_list.size() < 1, false); + + // Use the first mesh instance to get common data like number of surfaces. + const MeshInstance *first = p_list[0]; + + // Mesh compatibility checking. This is relatively expensive, so if done already (e.g. in Room system) + // this step can be avoided. + LocalVector compat_list; + if (p_check_compatibility) { + compat_list.resize(p_list.size()); + + for (int n = 0; n < p_list.size(); n++) { + compat_list[n] = false; + } + + compat_list[0] = true; + + for (uint32_t n = 1; n < compat_list.size(); n++) { + compat_list[n] = is_shadow_mergeable_with(*first, *p_list[n]); + + if (compat_list[n] == false) { + WARN_PRINT("MeshInstance " + p_list[n]->get_name() + " is incompatible for shadow merging with " + first->get_name() + ", ignoring."); + } + } + } + + Ref am; + am.instance(); + + // If we want a local space result, we need the world space transform of this MeshInstance + // available to back transform verts from world space. + Transform dest_tr_inv; + if (!p_use_global_space) { + if (r_dest_mi.is_inside_tree()) { + dest_tr_inv = r_dest_mi.get_global_transform(); + dest_tr_inv.affine_invert(); + } else { + WARN_PRINT("MeshInstance must be inside tree to merge using local space, falling back to global space."); + } + } + + SurfaceTool surface_tool; + for (int n = 0; n < p_list.size(); n++) { + // Ignore if the mesh is incompatible. + if (p_check_compatibility && (!compat_list[n])) { + continue; + } + MeshInstance *source_mi = p_list[n]; + Ref rmesh = source_mi->get_mesh(); + + Transform adjustment_xform = dest_tr_inv * source_mi->get_global_transform(); + for (int s = 0; s < rmesh->get_surface_count(); s++) { + surface_tool.append_from(rmesh, s, adjustment_xform); + +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + _merge_log("merging from \"" + source_mi->get_name() + "\" surf " + itos(s) + " to \"" + r_dest_mi.get_name() + "\""); + MergingTool::append_editor_description(&r_dest_mi, "merging from", source_mi); + } +#endif + } + } // for n through source meshes + + // We are only interested in position data for shadow proxy meshes, and indices if present. + surface_tool._mask_format_flags(Mesh::ARRAY_FORMAT_VERTEX | Mesh::ARRAY_FORMAT_INDEX); + + Array arr = surface_tool.commit_to_arrays(); + am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr, Array(), Mesh::ARRAY_COMPRESS_DEFAULT); + + // Set all the surfaces on the mesh. + r_dest_mi.set_mesh(am); + + _copy_geometry_instance_settings(*first, r_dest_mi, false); + r_dest_mi.set_cast_shadows_setting(GeometryInstance::ShadowCastingSetting::SHADOW_CASTING_SETTING_SHADOWS_ONLY); + + // Don't want these set, they get set by the _copy_geometry_instance_settings call. + r_dest_mi.set_material_overlay(Ref()); + r_dest_mi.set_material_override(Ref()); + + return true; +} + +void MergingTool::_mesh_set_storage_mode(Mesh *p_mesh, Mesh::StorageMode p_mode) { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + ERR_FAIL_NULL(p_mesh); + p_mesh->set_storage_mode(p_mode); + } +#endif +} + +bool MergingTool::split_surface_to_mesh_instance(const MeshInstance &p_source_mi, int p_surface_id, MeshInstance &r_mi) { + SurfaceTool surface_tool; + Ref rmesh = p_source_mi.get_mesh(); + if (!rmesh.is_valid()) { + return false; + } + + // Hard coded to local space for now. + surface_tool.append_from(rmesh, p_surface_id, Transform()); + + Ref am; + am.instance(); + _mesh_set_storage_mode(am.ptr(), Mesh::STORAGE_MODE_CPU); + + Array arr = surface_tool.commit_to_arrays(); + am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr, Array(), Mesh::ARRAY_COMPRESS_DEFAULT); + + r_mi.set_mesh(am); + + // Set the material on the new mesh instance. + _set_rmesh_material(r_mi, r_mi.get_mesh(), 0, p_source_mi.get_active_material(p_surface_id)); + + // Set some properties to match the source mesh. + // As they are guaranteed siblings, the transform can be identical. + _copy_geometry_instance_settings(p_source_mi, r_mi, true); + + return true; +} + +#ifdef TOOLS_ENABLED +void MergingTool::append_editor_description(Node *p_node, String p_string, Node *p_node_named) { + ERR_FAIL_NULL(p_node); + String existing = p_node->get_editor_description(); + if (existing.size() > 512) { + // limit the max length of the description to prevent things getting ridiculous + return; + } + String add; + if (existing.size()) { + add += "\n"; + } + add += p_string; + if (p_node_named) { + add += " \"" + p_node_named->get_name() + "\""; + } + p_node->set_editor_description(existing + add); +} +#endif + +#ifdef DEV_ENABLED +void MergingTool::debug_branch(Node *p_node, const char *p_title, int p_depth) { + if (OS::get_singleton()->is_stdout_verbose()) { + if (p_title) { + _merge_log(p_title); + } + + if (p_node->is_queued_for_deletion()) { + return; + } + + String s; + for (int n = 0; n < p_depth; n++) { + s += "\t"; + } + s += "\"" + p_node->get_name() + "\"\t"; + + String filename = p_node->get_filename(); + if (filename.size()) { + s += "[filename " + p_node->get_filename() + "] "; + } + + s += "owner ("; + if (p_node->get_owner()) { + s += p_node->get_owner()->get_name(); + } else { + s += "NULL"; + } + s += ")"; + _merge_log(s); + + for (int n = 0; n < p_node->get_child_count(); n++) { + debug_branch(p_node->get_child(n), nullptr, p_depth + 1); + } + } // if verbose output +} +#endif + +void MergingTool::debug_mesh_instance(const MeshInstance &p_mi) { +#ifdef DEV_ENABLED + _merge_log("debug " + p_mi.get_name()); + Ref rmesh = p_mi.get_mesh(); + if (!rmesh.is_valid()) { + _merge_log("\tinvalid mesh"); + return; + } + for (int s = 0; s < rmesh->get_surface_count(); s++) { + _merge_log("\tsurf " + itos(s) + " inds " + itos(rmesh->surface_get_array_index_len(s)) + " verts " + itos(rmesh->surface_get_array_len(s))); + } +#endif +} + +bool MergingTool::join_mesh_surface(const MeshInstance &p_source_mi, uint32_t p_source_surface_id, MeshInstance &r_dest_mi) { + Ref r_sourcemesh = p_source_mi.get_mesh(); + ERR_FAIL_COND_V(!r_sourcemesh.is_valid(), false); + ERR_FAIL_COND_V((int)p_source_surface_id >= r_sourcemesh->get_surface_count(), false); + + // Note this can be NULL if the destination mesh instance contains no meshes yet. + // We should deal with this case. + Ref ra_destmesh = r_dest_mi.get_mesh(); + if (!ra_destmesh.is_valid()) { + ra_destmesh.instance(); + _mesh_set_storage_mode(ra_destmesh.ptr(), Mesh::STORAGE_MODE_CPU); + } + + // Relative xform .. + Transform relative_xform = r_dest_mi.get_global_transform().inverse() * p_source_mi.get_global_transform(); + + SurfaceTool surface_tool; + surface_tool.append_from(r_sourcemesh, p_source_surface_id, relative_xform); + + int new_surface_id = 0; + if (ra_destmesh.is_valid()) { + new_surface_id = ra_destmesh->get_surface_count(); + r_dest_mi.set_mesh(surface_tool.commit(ra_destmesh)); + } else { + r_dest_mi.set_mesh(surface_tool.commit()); + } + + Ref new_rmesh = r_dest_mi.get_mesh(); + + // If no surface has been added. + ERR_FAIL_COND_V(new_rmesh->get_surface_count() <= new_surface_id, false); + + // Deal with materials. + _set_rmesh_material(r_dest_mi, new_rmesh, new_surface_id, p_source_mi.get_active_material(p_source_surface_id)); + + return true; +} + +// No compat checking, no renaming. +bool MergingTool::join_meshes(MeshInstance &r_dest_mi, Vector p_list) { + if (p_list.size() < 1) { + // Should not happen but just in case... + return false; + } + + // For future use of compatibility check. + LocalVector list = p_list; + + MeshInstance *first = list[0]; + + // First copy the properties of the first meshinstance. + _copy_mesh_instance_settings(*first, r_dest_mi, false, false); + + for (unsigned int n = 0; n < list.size(); n++) { + MeshInstance *mi = list[n]; + Ref rmesh = mi->get_mesh(); + + for (int s = 0; s < rmesh->get_surface_count(); s++) { + if (MergingTool::join_mesh_surface(*mi, s, r_dest_mi)) { +#ifdef DEV_ENABLED + _merge_log("joining \"" + mi->get_name() + "\" to \"" + r_dest_mi.get_name() + "\""); +#endif +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + MergingTool::append_editor_description(&r_dest_mi, "joined to", mi); + } +#endif + } else { +#ifdef DEV_ENABLED + _merge_log("failed to join \"" + mi->get_name() + "\" to \"" + r_dest_mi.get_name() + "\""); +#endif + } + } + } + + return true; +} + +// If p_check_compatibility is set to false you MUST have performed a prior check using +// is_mergeable_with, otherwise you could get mismatching surface formats leading to graphical errors etc. +bool MergingTool::merge_meshes(MeshInstance &r_dest_mi, Vector p_list, bool p_use_global_space, bool p_check_compatibility) { + ERR_FAIL_COND_V(p_list.size() < 1, false); + + // Use the first mesh instance to get common data like number of surfaces. + const MeshInstance *first = p_list[0]; + Ref rmesh_first = first->get_mesh(); + if (!rmesh_first.is_valid()) { + return false; + } + int surface_count = rmesh_first->get_surface_count(); + if (surface_count <= 0) { +#ifdef TOOLS_ENABLED + _merge_log("merge_meshes : " + first->get_name() + " contains no surfaces, ignoring."); +#endif + return false; + } + + // Mesh compatibility checking. This is relatively expensive, so if done already (e.g. in Room system) + // this step can be avoided. + LocalVector compat_list; + if (p_check_compatibility) { + compat_list.resize(p_list.size()); + + for (int n = 0; n < p_list.size(); n++) { + compat_list[n] = false; + } + + compat_list[0] = true; + + for (uint32_t n = 1; n < compat_list.size(); n++) { + compat_list[n] = is_mergeable_with(*first, *p_list[n], true); + + if (compat_list[n] == false) { + WARN_PRINT("MeshInstance " + p_list[n]->get_name() + " is incompatible for merging with " + first->get_name() + ", ignoring."); + } + } + } + + Ref am; + am.instance(); + _mesh_set_storage_mode(am.ptr(), Mesh::STORAGE_MODE_CPU); + + // If we want a local space result, we need the world space transform of this MeshInstance + // available to back transform verts from world space. + Transform dest_tr_inv; + if (!p_use_global_space) { + if (r_dest_mi.is_inside_tree()) { + dest_tr_inv = r_dest_mi.get_global_transform(); + dest_tr_inv.affine_invert(); + } else { + WARN_PRINT("MeshInstance must be inside tree to merge using local space, falling back to global space."); + } + } + + for (int s = 0; s < surface_count; s++) { + SurfaceTool surface_tool; + + for (int n = 0; n < p_list.size(); n++) { + // Ignore if the mesh is incompatible. + if (p_check_compatibility && (!compat_list[n])) { + continue; + } + + Ref rmesh = p_list[n]->get_mesh(); + + Transform adjustment_xform = dest_tr_inv * p_list[n]->get_global_transform(); + surface_tool.append_from(rmesh, s, adjustment_xform); + +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + _merge_log("merging from \"" + p_list[n]->get_name() + "\" surf " + itos(s) + " to \"" + r_dest_mi.get_name() + "\""); + MergingTool::append_editor_description(&r_dest_mi, "merging from", p_list[n]); + } +#endif + } + + Array arr = surface_tool.commit_to_arrays(); + am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr, Array(), Mesh::ARRAY_COMPRESS_DEFAULT); + } // for s + + // Set all the surfaces on the mesh. + r_dest_mi.set_mesh(am); + + // Set some properties to match the merged meshes. + _copy_mesh_instance_settings(*first, r_dest_mi, false, true); + + return true; +} + +bool MergingTool::split_csg_surface_to_mesh_instance(const CSGShape &p_shape, MeshInstance &r_mi, const Ref &p_array_mesh, CSGBrush *p_brush, int p_surface) { +#ifdef MODULE_CSG_ENABLED + + SurfaceTool surface_tool; + Ref am; + am.instance(); + + Ref rmesh = p_array_mesh; + + // We are matching the local transforms of the source and destination, as they are always + // siblings for now. + surface_tool.append_from(rmesh, p_surface, Transform()); + + Array arr = surface_tool.commit_to_arrays(); + + am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr, Array(), Mesh::ARRAY_COMPRESS_DEFAULT); + r_mi.set_mesh(am); + + // Set the material on the new mesh instance. + if (p_surface < p_brush->materials.size()) { + _set_rmesh_material(r_mi, r_mi.get_mesh(), 0, p_brush->materials[p_surface]); + } + + // As they are guaranteed siblings, the transform can be identical. + _copy_geometry_instance_settings(p_shape, r_mi, true); + + return true; +#else + return false; +#endif +} + +void MergingTool::_copy_mesh_instance_settings(const MeshInstance &p_source, MeshInstance &r_dest, bool p_copy_transform, bool p_copy_materials) { + _copy_geometry_instance_settings(p_source, r_dest, p_copy_transform); + + if (p_copy_materials) { + // Set merged materials. + Ref rmesh = p_source.get_mesh(); + if (rmesh.is_valid()) { + for (int n = 0; n < rmesh->get_surface_count(); n++) { + _set_rmesh_material(r_dest, r_dest.get_mesh(), n, p_source.get_active_material(n)); + } + } + } +} + +void MergingTool::_set_rmesh_material(MeshInstance &r_mi, Ref r_rmesh, int p_surface_id, Ref p_material) { + // Here we can either set the material on the rmesh, or on the mesh instance. + // Setting it directly in the rmesh seems more desired by users, but perhaps this could be + // switchable? + r_rmesh->surface_set_material(p_surface_id, p_material); + // r_mi.set_surface_material(p_surface_id, p_material); +} + +void MergingTool::_copy_geometry_instance_settings(const GeometryInstance &p_source, MeshInstance &r_dest, bool p_copy_transform) { + // Set some properties to match the source mesh. + r_dest.set_material_overlay(p_source.get_material_overlay()); + r_dest.set_material_override(p_source.get_material_override()); + r_dest.set_cast_shadows_setting(p_source.get_cast_shadows_setting()); + //r_dest.set_flag(GeometryInstance::FLAG_USE_BAKED_LIGHT, p_source.get_flag(GeometryInstance::FLAG_USE_BAKED_LIGHT)); + + r_dest.set_portal_mode(p_source.get_portal_mode()); + r_dest.set_include_in_bound(p_source.get_include_in_bound()); + r_dest.set_portal_autoplace_priority(p_source.get_portal_autoplace_priority()); + r_dest.set_extra_cull_margin(p_source.get_extra_cull_margin()); + + // As they are guaranteed siblings, the transform can be identical. + if (p_copy_transform) { + r_dest.set_transform(p_source.get_transform()); + } + + // Preserve visibility. + // If they are siblings, they can share the visible flag, if not, we need to take into account visibility in tree. + if (p_source.get_parent() == r_dest.get_parent()) { + r_dest.set_visible(p_source.is_visible()); + } else { + r_dest.set_visible(p_source.is_visible_in_tree()); + } +} + +void MergingTool::_merge_log(String p_string, int p_priority) { +#ifdef TOOLS_ENABLED + switch (p_priority) { + case 0: { + print_verbose(p_string); + } break; + case 2: { + print_line(p_string); + } break; + default: { +#ifdef DEV_ENABLED + print_line(p_string); +#else + print_verbose(p_string); +#endif + } break; + } +#endif +} + +void MergingTool::_set_owner_logged(Node *p_node, Node *p_owner) { + DEV_ASSERT(p_node != p_owner); + +#ifdef DEV_ENABLED +#if 0 + // Check whether the Node::set_owner() routine will allow this .. the owner must be in the tree above + // for the call to work. + bool valid = false; + Node *probe = p_node->get_parent(); + while (probe) { + if (probe == p_owner) { + valid = true; + break; + } + probe = probe->get_parent(); + } + + DEV_ASSERT(valid); +#endif +#endif + + if (p_node->get_owner() == p_owner) { + return; + } + +#ifdef DEV_ENABLED +#if 0 + String string = "\tchanging owner of \"" + p_node->get_name() + "\" from "; + if (p_node->get_owner()) { + string += p_node->get_owner()->get_name(); + } else { + string += "NULL"; + } + string += " to "; + if (p_owner) { + string += p_owner->get_name(); + } else { + string += "NULL"; + } + + _merge_log(string); +#endif +#endif + + p_node->set_owner(p_owner); + DEV_ASSERT(p_node->get_owner()); +} + +bool MergingTool::_node_has_valid_children(Node *p_node) { + for (int n = 0; n < p_node->get_child_count(); n++) { + if (!p_node->get_child(n)->is_queued_for_deletion()) { + return true; + } + } + return false; +} + +void MergingTool::_invalidate_owner_recursive(Node *p_node, Node *p_old_owner, Node *p_new_owner) { + if (p_node->get_owner() == p_old_owner) { + _set_owner_logged(p_node, p_new_owner); + } + + for (int n = 0; n < p_node->get_child_count(); n++) { + _invalidate_owner_recursive(p_node->get_child(n), p_old_owner, p_new_owner); + } +} + +void MergingTool::_reparent(Node *p_branch, Node *p_new_parent, Node *p_new_owner) { +#ifdef PANDEMONIUM_MERGING_VERBOSE + if (p_branch->get_parent()) { + _merge_log("reparenting child " + p_branch->get_name() + " from parent " + p_branch->get_parent()->get_name() + " to parent " + p_new_parent->get_name()); + } else { + _merge_log("reparenting child " + p_branch->get_name() + " from parent NULL to parent " + p_new_parent->get_name()); + } +#endif + + // noop + if (p_branch->get_parent() == p_new_parent) { + return; + } + + // Detach (if attached). + if (p_branch->get_parent()) { + p_branch->get_parent()->remove_child(p_branch); + } + + // Must be added to the scene BEFORE setting the new owner + // otherwise the set_owner() calls will fail to find the new owner. + p_new_parent->add_child(p_branch); + + _reparent_subscene_send_new_owner(p_branch, p_new_owner); +} + +void MergingTool::_reparent_subscene_send_new_owner(Node *p_node, Node *p_new_owner) { + bool owner_found = false; + + // Is the current owner in the subscene? If so keep it, else change. + Node *current_owner = p_node->get_owner(); + if (current_owner) { + Node *probe = p_node; + while (probe) { + if (probe == current_owner) { + // Owner already exists in the subscene, no need to change. + owner_found = true; + break; + } + probe = probe->get_parent(); + } + } // if there was a current owner + + if (!owner_found) { + _set_owner_logged(p_node, p_new_owner); + } + + for (int n = 0; n < p_node->get_child_count(); n++) { + _reparent_subscene_send_new_owner(p_node->get_child(n), p_new_owner); + } +} diff --git a/scene/resources/mesh/merging_tool.h b/scene/resources/mesh/merging_tool.h new file mode 100644 index 000000000..57e8ef7ff --- /dev/null +++ b/scene/resources/mesh/merging_tool.h @@ -0,0 +1,115 @@ +#ifndef MERGING_TOOL_H +#define MERGING_TOOL_H + +/*************************************************************************/ +/* merging_tool.h */ +/*************************************************************************/ +/* This file is part of: */ +/* PANDEMONIUM ENGINE */ +/* https://github.com/Relintai/pandemonium_engine */ +/*************************************************************************/ +/* Copyright (c) 2022-present Péter Magyar. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "core/containers/local_vector.h" +#include "core/containers/vector.h" +#include "scene/resources/mesh/mesh.h" + +class MeshInstance; +class GeometryInstance; +class CSGShape; +class SurfaceTool; +struct CSGBrush; + +#ifdef DEV_ENABLED +// Only enable this for development testing. +// #define PANDEMONIUM_MERGING_VERBOSE +#endif + +// NOTE : These merging and joining functions DO NOT move children, or delete source nodes. That is the responsibility of the caller. +class MergingTool { +public: + // Are two mesh instances mergeable with each other? + static bool is_mergeable_with(const MeshInstance &p_mi, const MeshInstance &p_other, bool p_check_surface_material_match); + static bool is_shadow_mergeable_with(const MeshInstance &p_mi, const MeshInstance &p_other); + + // Merges all mesh details. + static bool merge_meshes(MeshInstance &r_dest_mi, Vector p_list, bool p_use_global_space, bool p_check_compatibility); + + // Join all surfaces into one ubermesh. + static bool join_meshes(MeshInstance &r_dest_mi, Vector p_list); + + // Adds a surface from one mesh to another. + static bool join_mesh_surface(const MeshInstance &p_source_mi, uint32_t p_source_surface_id, MeshInstance &r_dest_mi); + + // Only concerned with data necessary for shadow proxy - opaque tris, no normals / tangents / uvs etc. + static bool merge_shadow_meshes(MeshInstance &r_dest_mi, Vector p_list, bool p_use_global_space, bool p_check_compatibility); + + // For splitting a MeshInstance with multiple surfaces to a MeshInstance per surface. + static bool split_surface_to_mesh_instance(const MeshInstance &p_source_mi, int p_surface_id, MeshInstance &r_mi); + + // Convert a CSG surface to MeshInstance. + static bool split_csg_surface_to_mesh_instance(const CSGShape &p_shape, MeshInstance &r_mi, const Ref &p_array_mesh, CSGBrush *p_brush, int p_surface); + + // Remove degenerate triangles. + static bool clean_mesh_instance(MeshInstance &p_mi); + + static void split_mesh_instance_by_locality(MeshInstance &r_mi, const AABB &p_bound, uint32_t p_splits_horz, uint32_t p_splits_vert, uint32_t p_min_split_poly_count); + + // For debugging purposes. + static void debug_mesh_instance(const MeshInstance &p_mi); +#ifdef DEV_ENABLED + static void debug_branch(Node *p_node, const char *p_title = nullptr, int p_depth = 0); +#endif +#ifdef TOOLS_ENABLED + static void append_editor_description(Node *p_node, String p_string, Node *p_node_named = nullptr); +#endif + + // Helper functions (used from MergeGroup). + static void _set_owner_logged(Node *p_node, Node *p_owner); + static void _reparent(Node *p_branch, Node *p_new_parent, Node *p_new_owner); + static void _invalidate_owner_recursive(Node *p_node, Node *p_old_owner, Node *p_new_owner); + static bool _node_has_valid_children(Node *p_node); + static void _mesh_set_storage_mode(Mesh *p_mesh, Mesh::StorageMode p_mode); + +private: + static void _reparent_subscene_send_new_owner(Node *p_node, Node *p_new_owner); + + static void _copy_mesh_instance_settings(const MeshInstance &p_source, MeshInstance &r_dest, bool p_copy_transform, bool p_copy_materials); + static bool _is_mergeable_with_common(const MeshInstance &p_mi, const MeshInstance &p_other); + static bool _is_shadow_mergeable(const MeshInstance &p_mi); + static bool _is_material_opaque(const Ref &p_mat); + static bool _ensure_indices_valid(LocalVector &r_indices, const PoolVector &p_verts); + static bool _check_for_valid_indices(const LocalVector &p_inds, const PoolVector &p_verts, LocalVector *r_inds); + static bool _triangle_is_degenerate(const Vector3 &p_a, const Vector3 &p_b, const Vector3 &p_c, real_t p_epsilon); + static int _clean_mesh_surface(const String &p_source_name, const Transform &p_xform, Ref &p_rmesh, int p_surface_id, Ref r_dest_mesh); + static void _copy_geometry_instance_settings(const GeometryInstance &p_source, MeshInstance &r_dest, bool p_copy_transform); + static void _set_rmesh_material(MeshInstance &r_mi, Ref r_rmesh, int p_surface_id, Ref p_material); + + static void _split_mesh_instance_by_locality(const SurfaceTool &p_st_main, const MeshInstance &p_source_mi, const LocalVector &p_tri_ids, uint32_t p_local_id, uint32_t p_surface_id, uint32_t p_x, uint32_t p_y, uint32_t p_z); + + static void _merge_log(String p_string, int p_priority = 1); +}; + +#endif // MERGING_TOOL_H diff --git a/scene/resources/mesh/mesh.cpp b/scene/resources/mesh/mesh.cpp index 1b8c0924d..0f21a5320 100644 --- a/scene/resources/mesh/mesh.cpp +++ b/scene/resources/mesh/mesh.cpp @@ -43,32 +43,116 @@ Mesh::ConvexDecompositionFunc Mesh::convex_decomposition_function = nullptr; +int Mesh::surface_get_face_count(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, get_surface_count(), 0); + + switch (surface_get_primitive_type(p_idx)) { + case PRIMITIVE_TRIANGLES: { + int len = (surface_get_format(p_idx) & ARRAY_FORMAT_INDEX) ? surface_get_array_index_len(p_idx) : surface_get_array_len(p_idx); + // Don't error if zero, it's valid (we'll just skip it later). + ERR_FAIL_COND_V_MSG((len % 3) != 0, 0, vformat("Ignoring surface %d, incorrect %s count: %d (for PRIMITIVE_TRIANGLES).", p_idx, (surface_get_format(p_idx) & ARRAY_FORMAT_INDEX) ? "index" : "vertex", len)); + return len; + } break; + case PRIMITIVE_TRIANGLE_FAN: + case PRIMITIVE_TRIANGLE_STRIP: { + int len = (surface_get_format(p_idx) & ARRAY_FORMAT_INDEX) ? surface_get_array_index_len(p_idx) : surface_get_array_len(p_idx); + // Don't error if zero, it's valid (we'll just skip it later). + ERR_FAIL_COND_V_MSG(len != 0 && len < 3, 0, vformat("Ignoring surface %d, incorrect %s count: %d (for %s).", p_idx, (surface_get_format(p_idx) & ARRAY_FORMAT_INDEX) ? "index" : "vertex", len, (surface_get_primitive_type(p_idx) == PRIMITIVE_TRIANGLE_FAN) ? "PRIMITIVE_TRIANGLE_FAN" : "PRIMITIVE_TRIANGLE_STRIP")); + return (len == 0) ? 0 : (len - 2) * 3; + } break; + default: { + } break; + } + + return 0; +} + +int Mesh::get_face_count() const { + int faces_size = 0; + + for (int i = 0; i < get_surface_count(); i++) { + faces_size += surface_get_face_count(i); + } + + return faces_size; +} + +Ref Mesh::generate_triangle_mesh_from_aabb() const { + AABB aabb = get_aabb(); + + Vector3 pts[8]; + Vector3 s = aabb.position; + Vector3 l = s + aabb.size; + + pts[0] = Vector3(s.x, s.y, s.z); + pts[1] = Vector3(l.x, s.y, s.z); + pts[2] = Vector3(l.x, l.y, s.z); + pts[3] = Vector3(s.x, l.y, s.z); + pts[4] = Vector3(l.x, l.y, l.z); + pts[5] = Vector3(s.x, l.y, l.z); + pts[6] = Vector3(s.x, s.y, l.z); + pts[7] = Vector3(l.x, s.y, l.z); + + PoolVector face_pts; + face_pts.resize(6 * 2 * 3); + PoolVector::Write w = face_pts.write(); + int wc = 0; + w[wc++] = pts[0]; + w[wc++] = pts[1]; + w[wc++] = pts[2]; + w[wc++] = pts[0]; + w[wc++] = pts[2]; + w[wc++] = pts[3]; + + w[wc++] = pts[6]; + w[wc++] = pts[5]; + w[wc++] = pts[4]; + w[wc++] = pts[6]; + w[wc++] = pts[4]; + w[wc++] = pts[7]; + + w[wc++] = pts[1]; + w[wc++] = pts[7]; + w[wc++] = pts[4]; + w[wc++] = pts[1]; + w[wc++] = pts[4]; + w[wc++] = pts[2]; + + w[wc++] = pts[0]; + w[wc++] = pts[3]; + w[wc++] = pts[5]; + w[wc++] = pts[0]; + w[wc++] = pts[5]; + w[wc++] = pts[6]; + + w[wc++] = pts[0]; + w[wc++] = pts[6]; + w[wc++] = pts[7]; + w[wc++] = pts[0]; + w[wc++] = pts[7]; + w[wc++] = pts[1]; + + w[wc++] = pts[2]; + w[wc++] = pts[4]; + w[wc++] = pts[5]; + w[wc++] = pts[2]; + w[wc++] = pts[5]; + w[wc++] = pts[3]; + + w.release(); + + Ref tmesh = Ref(memnew(TriangleMesh)); + tmesh->create(face_pts); + + return tmesh; +} + Ref Mesh::generate_triangle_mesh() const { if (triangle_mesh.is_valid()) { return triangle_mesh; } - int faces_size = 0; - - for (int i = 0; i < get_surface_count(); i++) { - switch (surface_get_primitive_type(i)) { - case PRIMITIVE_TRIANGLES: { - int len = (surface_get_format(i) & ARRAY_FORMAT_INDEX) ? surface_get_array_index_len(i) : surface_get_array_len(i); - // Don't error if zero, it's valid (we'll just skip it later). - ERR_CONTINUE_MSG((len % 3) != 0, vformat("Ignoring surface %d, incorrect %s count: %d (for PRIMITIVE_TRIANGLES).", i, (surface_get_format(i) & ARRAY_FORMAT_INDEX) ? "index" : "vertex", len)); - faces_size += len; - } break; - case PRIMITIVE_TRIANGLE_FAN: - case PRIMITIVE_TRIANGLE_STRIP: { - int len = (surface_get_format(i) & ARRAY_FORMAT_INDEX) ? surface_get_array_index_len(i) : surface_get_array_len(i); - // Don't error if zero, it's valid (we'll just skip it later). - ERR_CONTINUE_MSG(len != 0 && len < 3, vformat("Ignoring surface %d, incorrect %s count: %d (for %s).", i, (surface_get_format(i) & ARRAY_FORMAT_INDEX) ? "index" : "vertex", len, (surface_get_primitive_type(i) == PRIMITIVE_TRIANGLE_FAN) ? "PRIMITIVE_TRIANGLE_FAN" : "PRIMITIVE_TRIANGLE_STRIP")); - faces_size += (len == 0) ? 0 : (len - 2) * 3; - } break; - default: { - } break; - } - } + int faces_size = get_face_count(); if (faces_size == 0) { return triangle_mesh; @@ -523,6 +607,9 @@ void Mesh::_bind_methods() { BIND_ENUM_CONSTANT(ARRAY_MAX); } +void Mesh::set_storage_mode(StorageMode p_storage_mode) { +} + void Mesh::clear_cache() const { triangle_mesh.unref(); debug_lines.clear(); @@ -685,6 +772,9 @@ bool ArrayMesh::_get(const StringName &p_name, Variant &r_ret) const { return false; } + // Data must be in GPU for this routine to work. + ERR_FAIL_COND_V(!_on_gpu, false); + String sname = p_name; if (p_name == "blend_shape/names") { @@ -796,18 +886,67 @@ void ArrayMesh::add_surface(uint32_t p_format, PrimitiveType p_primitive, const Surface s; s.aabb = p_aabb; s.is_2d = p_format & ARRAY_FLAG_USE_2D_VERTICES; + s.creation_format = p_format; surfaces.push_back(s); _recompute_aabb(); RenderingServer::get_singleton()->mesh_add_surface(mesh, p_format, (RS::PrimitiveType)p_primitive, p_array, p_vertex_count, p_index_array, p_index_count, p_aabb, p_blend_shapes, p_bone_aabbs); } +void ArrayMesh::clear_cpu_surfaces() { + for (unsigned int n = 0; n < _cpu_surfaces.size(); n++) { + CPUSurface *s = _cpu_surfaces[n]; + DEV_ASSERT(s); + memdelete(s); + } + + _cpu_surfaces.clear(); +} + +void ArrayMesh::add_surface_from_arrays_cpu_with_probe(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_flags, int p_surface_id) { + uint32_t creation_format = 0; + + if (_on_gpu) { + // query the last created surface format + creation_format = RenderingServer::get_singleton()->mesh_surface_get_format(mesh, surfaces.size()); + } else { + creation_format = RenderingServer::get_singleton()->mesh_find_format_from_arrays((RS::PrimitiveType)p_primitive, p_arrays, p_blend_shapes, p_flags); + } + + Surface s = surfaces[p_surface_id]; + s.creation_flags = p_flags; + s.creation_format = creation_format; + surfaces.set(p_surface_id, s); + + add_surface_from_arrays_cpu(p_primitive, p_arrays, p_blend_shapes); +} + +void ArrayMesh::add_surface_from_arrays_cpu(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes) { + CPUSurface *s = memnew(CPUSurface); + _cpu_surfaces.push_back(s); + + s->primitive_type = p_primitive; + s->arrays = p_arrays; + s->blend_shapes = p_blend_shapes; + + if (p_arrays.size() > RS::ARRAY_VERTEX) { + // This is horrible but RenderingServer uses this .. it may do a conversion to PoolVector3Array? + // Maybe this rarely happens. + s->num_verts = PoolVector3Array(p_arrays[RS::ARRAY_VERTEX]).size(); + } + if (p_arrays.size() > RS::ARRAY_INDEX) { + s->num_inds = PoolIntArray(p_arrays[RS::ARRAY_INDEX]).size(); + } +} + void ArrayMesh::add_surface_from_arrays(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_flags) { ERR_FAIL_COND(p_arrays.size() != ARRAY_MAX); Surface s; - RenderingServer::get_singleton()->mesh_add_surface_from_arrays(mesh, (RenderingServer::PrimitiveType)p_primitive, p_arrays, p_blend_shapes, p_flags); + if (_on_gpu) { + RenderingServer::get_singleton()->mesh_add_surface_from_arrays(mesh, (RenderingServer::PrimitiveType)p_primitive, p_arrays, p_blend_shapes, p_flags); + } /* make aABB? */ { Variant arr = p_arrays[ARRAY_VERTEX]; @@ -829,11 +968,16 @@ void ArrayMesh::add_surface_from_arrays(PrimitiveType p_primitive, const Array & s.aabb = aabb; s.is_2d = arr.get_type() == Variant::POOL_VECTOR2_ARRAY; + s.creation_flags = p_flags; surfaces.push_back(s); _recompute_aabb(); } + if (_on_cpu) { + add_surface_from_arrays_cpu_with_probe(p_primitive, p_arrays, p_blend_shapes, p_flags, surfaces.size() - 1); + } + clear_cache(); _change_notify(); emit_changed(); @@ -841,10 +985,22 @@ void ArrayMesh::add_surface_from_arrays(PrimitiveType p_primitive, const Array & Array ArrayMesh::surface_get_arrays(int p_surface) const { ERR_FAIL_INDEX_V(p_surface, surfaces.size(), Array()); + + // preferentially read from CPU as quicker + if (on_cpu()) { + return _cpu_surfaces[p_surface]->arrays; + } + return RenderingServer::get_singleton()->mesh_surface_get_arrays(mesh, p_surface); } Array ArrayMesh::surface_get_blend_shape_arrays(int p_surface) const { ERR_FAIL_INDEX_V(p_surface, surfaces.size(), Array()); + + // preferentially read from CPU as quicker + if (on_cpu()) { + return _cpu_surfaces[p_surface]->blend_shapes; + } + return RenderingServer::get_singleton()->mesh_surface_get_blend_shape_arrays(mesh, p_surface); } @@ -913,6 +1069,13 @@ void ArrayMesh::surface_remove(int p_idx) { RenderingServer::get_singleton()->mesh_remove_surface(mesh, p_idx); surfaces.remove(p_idx); + if (on_cpu()) { + CPUSurface *s = _cpu_surfaces[p_idx]; + DEV_ASSERT(s); + memdelete(s); + _cpu_surfaces.remove(p_idx); + } + clear_cache(); _recompute_aabb(); _change_notify(); @@ -921,21 +1084,48 @@ void ArrayMesh::surface_remove(int p_idx) { int ArrayMesh::surface_get_array_len(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, surfaces.size(), -1); + + if (on_cpu()) { + CPUSurface *s = _cpu_surfaces[p_idx]; + DEV_ASSERT(s); + return s->num_verts; + } + return RenderingServer::get_singleton()->mesh_surface_get_array_len(mesh, p_idx); } int ArrayMesh::surface_get_array_index_len(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, surfaces.size(), -1); + + if (on_cpu()) { + CPUSurface *s = _cpu_surfaces[p_idx]; + DEV_ASSERT(s); + return s->num_inds; + } + return RenderingServer::get_singleton()->mesh_surface_get_array_index_len(mesh, p_idx); } uint32_t ArrayMesh::surface_get_format(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, surfaces.size(), 0); + + // not sure whether we need to support this yet? + if (!_on_gpu) { + return surfaces[p_idx].creation_format; + } + return RenderingServer::get_singleton()->mesh_surface_get_format(mesh, p_idx); } ArrayMesh::PrimitiveType ArrayMesh::surface_get_primitive_type(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, surfaces.size(), PRIMITIVE_LINES); + + if (on_cpu()) { + CPUSurface *s = _cpu_surfaces[p_idx]; + DEV_ASSERT(s); + return s->primitive_type; + } + return (PrimitiveType)RenderingServer::get_singleton()->mesh_surface_get_primitive_type(mesh, p_idx); } @@ -945,7 +1135,10 @@ void ArrayMesh::surface_set_material(int p_idx, const Ref &p_material) return; } surfaces.write[p_idx].material = p_material; - RenderingServer::get_singleton()->mesh_surface_set_material(mesh, p_idx, p_material.is_null() ? RID() : p_material->get_rid()); + + if (_on_gpu) { + RenderingServer::get_singleton()->mesh_surface_set_material(mesh, p_idx, p_material.is_null() ? RID() : p_material->get_rid()); + } _change_notify("material"); emit_changed(); @@ -1029,6 +1222,11 @@ void ArrayMesh::clear_surfaces() { if (!mesh.is_valid()) { return; } + + if (_on_cpu) { + clear_cpu_surfaces(); + } + RS::get_singleton()->mesh_clear(mesh); surfaces.clear(); aabb = AABB(); @@ -1132,6 +1330,80 @@ void ArrayMesh::reload_from_file() { _change_notify(); } +void ArrayMesh::set_storage_mode(StorageMode p_storage_mode) { + if (_storage_mode == p_storage_mode) { + return; + } + + bool new_on_cpu = false; + bool new_on_gpu = false; + + switch (p_storage_mode) { + default: { + new_on_cpu = false; + new_on_gpu = true; + } break; + case STORAGE_MODE_CPU: { + new_on_cpu = true; + new_on_gpu = false; + } break; + case STORAGE_MODE_CPU_AND_GPU: { + new_on_cpu = true; + new_on_gpu = true; + } break; + } + + // cpu to gpu? + if (new_on_gpu && !_on_gpu) { + // must be on cpu to go to gpu + DEV_CHECK(_on_cpu); + if (mesh.is_valid()) { + // make sure mesh is clear (may not be necessary) + RS::get_singleton()->mesh_clear(mesh); + + for (unsigned int n = 0; n < _cpu_surfaces.size(); n++) { + CPUSurface *s = _cpu_surfaces[n]; + DEV_ASSERT(s); + RenderingServer::get_singleton()->mesh_add_surface_from_arrays(mesh, (RenderingServer::PrimitiveType)s->primitive_type, s->arrays, s->blend_shapes, surfaces[n].creation_flags); + + ERR_CONTINUE((int)n >= surfaces.size()); + const Ref &mat = surfaces[n].material; + RenderingServer::get_singleton()->mesh_surface_set_material(mesh, n, mat.is_null() ? RID() : mat->get_rid()); + } + } + } + + // gpu to cpu? + if (new_on_cpu && !_on_cpu) { + // must be on gpu to go to cpu + DEV_CHECK(_on_gpu); + clear_cpu_surfaces(); + + if (mesh.is_valid()) { + for (int n = 0; n < surfaces.size(); n++) { + Array arrays = RenderingServer::get_singleton()->mesh_surface_get_arrays(mesh, n); + Array blend_shapes = RenderingServer::get_singleton()->mesh_surface_get_blend_shape_arrays(mesh, n); + PrimitiveType primitive = (PrimitiveType)RenderingServer::get_singleton()->mesh_surface_get_primitive_type(mesh, n); + add_surface_from_arrays_cpu(primitive, arrays, blend_shapes); + } + } // mesh valid + } + + // clear anything not used + if (!new_on_cpu) { + clear_cpu_surfaces(); + } + if (!new_on_gpu && _on_gpu) { + if (mesh.is_valid()) { + RS::get_singleton()->mesh_clear(mesh); + } + } + + _on_cpu = new_on_cpu; + _on_gpu = new_on_gpu; + _storage_mode = p_storage_mode; +} + ArrayMesh::ArrayMesh() { mesh = RID_PRIME(RenderingServer::get_singleton()->mesh_create()); blend_shape_mode = BLEND_SHAPE_MODE_RELATIVE; @@ -1139,4 +1411,5 @@ ArrayMesh::ArrayMesh() { ArrayMesh::~ArrayMesh() { RenderingServer::get_singleton()->free(mesh); + clear_cpu_surfaces(); } diff --git a/scene/resources/mesh/mesh.h b/scene/resources/mesh/mesh.h index cda029003..57dc0b96e 100644 --- a/scene/resources/mesh/mesh.h +++ b/scene/resources/mesh/mesh.h @@ -32,6 +32,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#include "core/containers/local_vector.h" #include "core/math/face3.h" #include "core/math/triangle_mesh.h" #include "core/object/resource.h" @@ -119,6 +120,12 @@ public: BLEND_SHAPE_MODE_RELATIVE = RS::BLEND_SHAPE_MODE_RELATIVE, }; + enum StorageMode { + STORAGE_MODE_GPU, + STORAGE_MODE_CPU, + STORAGE_MODE_CPU_AND_GPU, + }; + virtual int get_surface_count() const = 0; virtual int surface_get_array_len(int p_idx) const = 0; virtual int surface_get_array_index_len(int p_idx) const = 0; @@ -130,11 +137,14 @@ public: virtual void surface_set_material(int p_idx, const Ref &p_material) = 0; virtual Ref surface_get_material(int p_idx) const = 0; virtual int get_blend_shape_count() const = 0; + int surface_get_face_count(int p_idx) const; virtual StringName get_blend_shape_name(int p_index) const = 0; virtual void set_blend_shape_name(int p_index, const StringName &p_name) = 0; + int get_face_count() const; PoolVector get_faces() const; Ref generate_triangle_mesh() const; + Ref generate_triangle_mesh_from_aabb() const; void generate_debug_mesh_lines(Vector &r_lines); void generate_debug_mesh_indices(Vector &r_points); @@ -144,6 +154,7 @@ public: Ref create_outline(float p_margin) const; virtual AABB get_aabb() const = 0; + virtual void set_storage_mode(StorageMode p_storage_mode); void clear_cache() const; @@ -161,13 +172,32 @@ class ArrayMesh : public Mesh { RES_BASE_EXTENSION("mesh"); private: + // Storing the mesh data on CPU + struct CPUSurface { + Array arrays; + Array blend_shapes; + PrimitiveType primitive_type; + int num_verts = 0; + int num_inds = 0; + }; + struct Surface { String name; AABB aabb; Ref material; bool is_2d; + + // Watch for bugs here. + // When calling add_surface() rather than add_surface_from_arrays(), + // the creation flags will be unset, and left at default. + // Conversion from CPU to GPU memory assumes that creation_flags are + // correct, so is only TRULY safe when used with add_surface_from_arrays(). + uint32_t creation_flags = ARRAY_COMPRESS_DEFAULT; + uint32_t creation_format = 0; }; + Vector surfaces; + LocalVector _cpu_surfaces; RID mesh; AABB aabb; BlendShapeMode blend_shape_mode; @@ -176,6 +206,18 @@ private: void _recompute_aabb(); + // Data can be held on GPU, CPU or both. + // CPU is quicker for modifications, but can't be + // used for rendering. + bool _on_cpu = false; + bool _on_gpu = true; + StorageMode _storage_mode = STORAGE_MODE_GPU; + + void add_surface_from_arrays_cpu(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes); + void add_surface_from_arrays_cpu_with_probe(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_flags, int p_surface_id); + void clear_cpu_surfaces(); + bool on_cpu() const { return _on_cpu && ((int)_cpu_surfaces.size() == surfaces.size()); } + protected: virtual bool _is_generated() const { return false; } @@ -233,6 +275,8 @@ public: virtual void reload_from_file(); + virtual void set_storage_mode(StorageMode p_storage_mode); + ArrayMesh(); ~ArrayMesh(); diff --git a/scene/resources/mesh/surface_tool.cpp b/scene/resources/mesh/surface_tool.cpp index a3ebdc46f..c7e547790 100644 --- a/scene/resources/mesh/surface_tool.cpp +++ b/scene/resources/mesh/surface_tool.cpp @@ -793,6 +793,62 @@ void SurfaceTool::create_from_blend_shape(const Ref &p_existing, int p_sur _create_list_from_arrays(arr[shape_idx], &vertex_array, &index_array, format); } +// returns number of indices found within the subset +int SurfaceTool::create_from_subset(const SurfaceTool &p_source, const LocalVector &p_ids, uint32_t p_subset_id) { + clear(); + + bool was_indexed = p_source.index_array.size() != 0; + + // expecting deindexed input for now as easier to deal with + ERR_FAIL_COND_V(was_indexed, 0); + + // only deals with triangles + ERR_FAIL_COND_V(p_source.primitive != Mesh::PRIMITIVE_TRIANGLES, 0); + primitive = p_source.primitive; + + uint32_t num_source_tris = p_source.vertex_array.size() / 3; + DEV_ASSERT((p_source.vertex_array.size() % 3) == 0); + + ERR_FAIL_COND_V(num_source_tris != p_ids.size(), 0); + + const Vertex *v[3]; + const Vertex *input = p_source.vertex_array.ptr(); + + HashMap indices; + + for (uint32_t t = 0; t < num_source_tris; t++) { + v[0] = input++; + v[1] = input++; + v[2] = input++; + + if (p_ids[t] == p_subset_id) { + // we can use this triangle + for (int i = 0; i < 3; i++) { + const Vertex &vert = *v[i]; + + int *idxptr = indices.getptr(vert); + + int idx; + if (!idxptr) { + idx = indices.size(); + vertex_array.push_back(vert); + indices[vert] = idx; + } else { + idx = *idxptr; + } + + index_array.push_back(idx); + } // for i + } // bound intersects + } + + // steal the format from the source surface tool + format = p_source.format; + format |= Mesh::ARRAY_FORMAT_INDEX; + + return get_num_draw_vertices(); +} + void SurfaceTool::append_from(const Ref &p_existing, int p_surface, const Transform &p_xform) { ERR_FAIL_COND_MSG(p_existing.is_null(), "First argument in SurfaceTool::append_from() must be a valid object of type Mesh"); if (vertex_array.size() == 0) { diff --git a/scene/resources/mesh/surface_tool.h b/scene/resources/mesh/surface_tool.h index ce466c4c9..4ea88717d 100644 --- a/scene/resources/mesh/surface_tool.h +++ b/scene/resources/mesh/surface_tool.h @@ -39,6 +39,8 @@ class SurfaceTool : public Reference { GDCLASS(SurfaceTool, Reference); + friend class MergingTool; + public: struct Vertex { enum { MAX_BONES = 4 }; @@ -149,6 +151,7 @@ public: void create_from_blend_shape(const Ref &p_existing, int p_surface, const String &p_blend_shape_name); void append_from(const Ref &p_existing, int p_surface, const Transform &p_xform); Ref commit(const Ref &p_existing = Ref(), uint32_t p_flags = Mesh::ARRAY_COMPRESS_DEFAULT); + int create_from_subset(const SurfaceTool &p_source, const LocalVector &p_ids, uint32_t p_subset_id); SurfaceTool(); }; diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index a59dabbae..aa237d50f 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -1027,47 +1027,28 @@ void RenderingServer::mesh_surface_make_offsets_from_format(uint32_t p_format, i } } -void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_compress_format) { - ERR_FAIL_INDEX(p_primitive, RS::PRIMITIVE_MAX); - ERR_FAIL_COND(p_arrays.size() != RS::ARRAY_MAX); - - bool use_split_stream = GLOBAL_GET("rendering/misc/mesh_storage/split_stream") && !(p_compress_format & RS::ARRAY_FLAG_USE_DYNAMIC_UPDATE); - - uint32_t format = 0; - - // validation - int index_array_len = 0; - int array_len = 0; +// This function is separated from the main mesh_add_surface_from_arrays() to allow finding the format WITHOUT creating data. +// This is necessary for CPU meshes, where we may want to know the final format without creating final data. +bool RenderingServer::_mesh_find_format(RS::PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_compress_format, bool p_use_split_stream, uint32_t r_offsets[], int &r_attributes_base_offset, int &r_attributes_stride, int &r_positions_stride, uint32_t &r_format, int &r_index_array_len, int &r_array_len) { + ERR_FAIL_INDEX_V(p_primitive, RS::PRIMITIVE_MAX, false); + ERR_FAIL_COND_V(p_arrays.size() != RS::ARRAY_MAX, false); for (int i = 0; i < p_arrays.size(); i++) { if (p_arrays[i].get_type() == Variant::NIL) { continue; } - format |= (1 << i); + r_format |= (1 << i); if (i == RS::ARRAY_VERTEX) { - Variant var = p_arrays[i]; - switch (var.get_type()) { - case Variant::POOL_VECTOR2_ARRAY: { - PoolVector v2 = var; - } break; - case Variant::POOL_VECTOR3_ARRAY: { - PoolVector v3 = var; - } break; - default: { - Array v = var; - } break; - } - - array_len = PoolVector3Array(p_arrays[i]).size(); - ERR_FAIL_COND(array_len == 0); + r_array_len = PoolVector3Array(p_arrays[i]).size(); + ERR_FAIL_COND_V(r_array_len == 0, false); } else if (i == RS::ARRAY_INDEX) { - index_array_len = PoolIntArray(p_arrays[i]).size(); + r_index_array_len = PoolIntArray(p_arrays[i]).size(); } } - ERR_FAIL_COND((format & RS::ARRAY_FORMAT_VERTEX) == 0); // mandatory + ERR_FAIL_COND_V((r_format & RS::ARRAY_FORMAT_VERTEX) == 0, false); // mandatory if (p_blend_shapes.size()) { //validate format for morphs @@ -1080,21 +1061,14 @@ void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_p } } - ERR_FAIL_COND((bsformat) != (format & (RS::ARRAY_FORMAT_INDEX - 1))); + ERR_FAIL_COND_V((bsformat) != (r_format & (RS::ARRAY_FORMAT_INDEX - 1)), false); } } - uint32_t offsets[RS::ARRAY_MAX]; - uint32_t strides[RS::ARRAY_MAX]; - - int attributes_base_offset = 0; - int attributes_stride = 0; - int positions_stride = 0; - for (int i = 0; i < RS::ARRAY_MAX; i++) { - offsets[i] = 0; //reset + r_offsets[i] = 0; //reset - if (!(format & (1 << i))) { // no array + if (!(r_format & (1 << i))) { // no array continue; } @@ -1105,15 +1079,15 @@ void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_p Variant arr = p_arrays[0]; if (arr.get_type() == Variant::POOL_VECTOR2_ARRAY) { elem_size = 2; - p_compress_format |= ARRAY_FLAG_USE_2D_VERTICES; + p_compress_format |= RS::ARRAY_FLAG_USE_2D_VERTICES; } else if (arr.get_type() == Variant::POOL_VECTOR3_ARRAY) { - p_compress_format &= ~ARRAY_FLAG_USE_2D_VERTICES; + p_compress_format &= ~RS::ARRAY_FLAG_USE_2D_VERTICES; elem_size = 3; } else { - elem_size = (p_compress_format & ARRAY_FLAG_USE_2D_VERTICES) ? 2 : 3; + elem_size = (p_compress_format & RS::ARRAY_FLAG_USE_2D_VERTICES) ? 2 : 3; } - if (p_compress_format & ARRAY_COMPRESS_VERTEX) { + if (p_compress_format & RS::ARRAY_COMPRESS_VERTEX) { elem_size *= sizeof(int16_t); } else { elem_size *= sizeof(float); @@ -1124,94 +1098,94 @@ void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_p elem_size = 8; } - offsets[i] = 0; - positions_stride = elem_size; - if (use_split_stream) { - attributes_base_offset = elem_size * array_len; + r_offsets[i] = 0; + r_positions_stride = elem_size; + if (p_use_split_stream) { + r_attributes_base_offset = elem_size * r_array_len; } else { - attributes_base_offset = elem_size; + r_attributes_base_offset = elem_size; } } break; case RS::ARRAY_NORMAL: { - if (p_compress_format & ARRAY_FLAG_USE_OCTAHEDRAL_COMPRESSION) { + if (p_compress_format & RS::ARRAY_FLAG_USE_OCTAHEDRAL_COMPRESSION) { // normal will always be oct32 (4 byte) encoded // UNLESS tangent exists and is also compressed // then it will be oct16 encoded along with tangent - if ((p_compress_format & ARRAY_COMPRESS_NORMAL) && (format & ARRAY_FORMAT_TANGENT) && (p_compress_format & ARRAY_COMPRESS_TANGENT)) { + if ((p_compress_format & RS::ARRAY_COMPRESS_NORMAL) && (r_format & ARRAY_FORMAT_TANGENT) && (p_compress_format & RS::ARRAY_COMPRESS_TANGENT)) { elem_size = sizeof(uint8_t) * 2; } else { elem_size = sizeof(uint16_t) * 2; } } else { - if (p_compress_format & ARRAY_COMPRESS_NORMAL) { + if (p_compress_format & RS::ARRAY_COMPRESS_NORMAL) { elem_size = sizeof(uint32_t); } else { elem_size = sizeof(float) * 3; } } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_TANGENT: { - if (p_compress_format & ARRAY_FLAG_USE_OCTAHEDRAL_COMPRESSION) { - if (p_compress_format & ARRAY_COMPRESS_TANGENT && (format & ARRAY_FORMAT_NORMAL) && (p_compress_format & ARRAY_COMPRESS_NORMAL)) { + if (p_compress_format & RS::ARRAY_FLAG_USE_OCTAHEDRAL_COMPRESSION) { + if (p_compress_format & RS::ARRAY_COMPRESS_TANGENT && (r_format & RS::ARRAY_FORMAT_NORMAL) && (p_compress_format & RS::ARRAY_COMPRESS_NORMAL)) { elem_size = sizeof(uint8_t) * 2; } else { elem_size = sizeof(uint16_t) * 2; } } else { - if (p_compress_format & ARRAY_COMPRESS_TANGENT) { + if (p_compress_format & RS::ARRAY_COMPRESS_TANGENT) { elem_size = sizeof(uint32_t); } else { elem_size = sizeof(float) * 4; } } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_COLOR: { - if (p_compress_format & ARRAY_COMPRESS_COLOR) { + if (p_compress_format & RS::ARRAY_COMPRESS_COLOR) { elem_size = sizeof(uint32_t); } else { elem_size = sizeof(float) * 4; } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_TEX_UV: { - if (p_compress_format & ARRAY_COMPRESS_TEX_UV) { + if (p_compress_format & RS::ARRAY_COMPRESS_TEX_UV) { elem_size = sizeof(uint32_t); } else { elem_size = sizeof(float) * 2; } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_TEX_UV2: { - if (p_compress_format & ARRAY_COMPRESS_TEX_UV2) { + if (p_compress_format & RS::ARRAY_COMPRESS_TEX_UV2) { elem_size = sizeof(uint32_t); } else { elem_size = sizeof(float) * 2; } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_WEIGHTS: { - if (p_compress_format & ARRAY_COMPRESS_WEIGHTS) { + if (p_compress_format & RS::ARRAY_COMPRESS_WEIGHTS) { elem_size = sizeof(uint16_t) * 4; } else { elem_size = sizeof(float) * 4; } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_BONES: { @@ -1227,37 +1201,83 @@ void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_p } if (max_bone > 255) { - p_compress_format |= ARRAY_FLAG_USE_16_BIT_BONES; + p_compress_format |= RS::ARRAY_FLAG_USE_16_BIT_BONES; elem_size = sizeof(uint16_t) * 4; } else { - p_compress_format &= ~ARRAY_FLAG_USE_16_BIT_BONES; + p_compress_format &= ~RS::ARRAY_FLAG_USE_16_BIT_BONES; elem_size = sizeof(uint32_t); } - offsets[i] = attributes_base_offset + attributes_stride; - attributes_stride += elem_size; + r_offsets[i] = r_attributes_base_offset + r_attributes_stride; + r_attributes_stride += elem_size; } break; case RS::ARRAY_INDEX: { - if (index_array_len <= 0) { + if (r_index_array_len <= 0) { ERR_PRINT("index_array_len==NO_INDEX_ARRAY"); break; } /* determine whether using 16 or 32 bits indices */ - if (array_len >= (1 << 16)) { + if (r_array_len >= (1 << 16)) { elem_size = 4; } else { elem_size = 2; } - offsets[i] = elem_size; + r_offsets[i] = elem_size; continue; } default: { - ERR_FAIL(); + ERR_FAIL_V(false); } } } + uint32_t mask = (1 << RS::ARRAY_MAX) - 1; + r_format |= (~mask) & p_compress_format; //make the full format + + return true; +} + +uint32_t RenderingServer::mesh_find_format_from_arrays(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_compress_format) { + bool use_split_stream = GLOBAL_GET("rendering/misc/mesh_storage/split_stream") && !(p_compress_format & RS::ARRAY_FLAG_USE_DYNAMIC_UPDATE); + + uint32_t offsets[RS::ARRAY_MAX]; + + int attributes_base_offset = 0; + int attributes_stride = 0; + int positions_stride = 0; + + uint32_t format = 0; + + // validation + int index_array_len = 0; + int array_len = 0; + + bool res = _mesh_find_format(p_primitive, p_arrays, p_blend_shapes, p_compress_format, use_split_stream, offsets, attributes_base_offset, attributes_stride, positions_stride, format, index_array_len, array_len); + ERR_FAIL_COND_V(!res, 0); + return format; +} + +void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_compress_format) { + bool use_split_stream = GLOBAL_GET("rendering/misc/mesh_storage/split_stream") && !(p_compress_format & RS::ARRAY_FLAG_USE_DYNAMIC_UPDATE); + + uint32_t offsets[RS::ARRAY_MAX]; + + int attributes_base_offset = 0; + int attributes_stride = 0; + int positions_stride = 0; + + uint32_t format = 0; + + // validation + int index_array_len = 0; + int array_len = 0; + + bool res = _mesh_find_format(p_primitive, p_arrays, p_blend_shapes, p_compress_format, use_split_stream, offsets, attributes_base_offset, attributes_stride, positions_stride, format, index_array_len, array_len); + ERR_FAIL_COND(!res); + + uint32_t strides[RS::ARRAY_MAX]; + if (use_split_stream) { strides[RS::ARRAY_VERTEX] = positions_stride; for (int i = 1; i < RS::ARRAY_MAX - 1; i++) { @@ -1269,9 +1289,6 @@ void RenderingServer::mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_p } } - uint32_t mask = (1 << ARRAY_MAX) - 1; - format |= (~mask) & p_compress_format; //make the full format - int array_size = (positions_stride + attributes_stride) * array_len; PoolVector vertex_array; diff --git a/servers/rendering_server.h b/servers/rendering_server.h index cd6c842d8..19c0e577a 100644 --- a/servers/rendering_server.h +++ b/servers/rendering_server.h @@ -297,6 +297,8 @@ public: /// Returns stride virtual void mesh_surface_make_offsets_from_format(uint32_t p_format, int p_vertex_len, int p_index_len, uint32_t *r_offsets, uint32_t *r_strides) const; virtual void mesh_add_surface_from_arrays(RID p_mesh, PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes = Array(), uint32_t p_compress_format = ARRAY_COMPRESS_DEFAULT); + virtual uint32_t mesh_find_format_from_arrays(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes = Array(), uint32_t p_compress_format = ARRAY_COMPRESS_DEFAULT); + bool _mesh_find_format(PrimitiveType p_primitive, const Array &p_arrays, const Array &p_blend_shapes, uint32_t p_compress_format, bool p_use_split_stream, uint32_t r_offsets[], int &r_attributes_base_offset, int &r_attributes_stride, int &r_positions_stride, uint32_t &r_format, int &r_index_array_len, int &r_array_len); virtual void mesh_add_surface(RID p_mesh, uint32_t p_format, PrimitiveType p_primitive, const PoolVector &p_array, int p_vertex_count, const PoolVector &p_index_array, int p_index_count, const AABB &p_aabb, const Vector> &p_blend_shapes = Vector>(), const Vector &p_bone_aabbs = Vector()) = 0; virtual void mesh_set_blend_shape_count(RID p_mesh, int p_amount) = 0;