|
@@ -0,0 +1,1921 @@
|
|
|
+/*************************************************************************/
|
|
|
+/* room_manager.cpp */
|
|
|
+/*************************************************************************/
|
|
|
+/* This file is part of: */
|
|
|
+/* GODOT ENGINE */
|
|
|
+/* https://godotengine.org */
|
|
|
+/*************************************************************************/
|
|
|
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
|
|
|
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
|
|
|
+/* */
|
|
|
+/* 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 "room_manager.h"
|
|
|
+
|
|
|
+#include "core/bitfield_dynamic.h"
|
|
|
+#include "core/engine.h"
|
|
|
+#include "core/math/quick_hull.h"
|
|
|
+#include "core/os/os.h"
|
|
|
+#include "editor/editor_node.h"
|
|
|
+#include "mesh_instance.h"
|
|
|
+#include "modules/csg/csg_shape.h"
|
|
|
+#include "portal.h"
|
|
|
+#include "room_group.h"
|
|
|
+#include "scene/3d/camera.h"
|
|
|
+#include "scene/3d/light.h"
|
|
|
+#include "visibility_notifier.h"
|
|
|
+
|
|
|
+// #define GODOT_PORTALS_USE_BULLET_CONVEX_HULL
|
|
|
+
|
|
|
+#ifdef GODOT_PORTALS_USE_BULLET_CONVEX_HULL
|
|
|
+#include "core/math/convex_hull.h"
|
|
|
+#endif
|
|
|
+
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+RoomManager *RoomManager::active_room_manager = nullptr;
|
|
|
+#endif
|
|
|
+
|
|
|
+RoomManager::RoomManager() {
|
|
|
+ // some high value, we want room manager to be processed after other
|
|
|
+ // nodes because the camera should be moved first
|
|
|
+ set_process_priority(10000);
|
|
|
+
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+ // note this mechanism may fail to work correctly if the user creates two room managers,
|
|
|
+ // but should not create major problems as it is just used to auto update when portals etc
|
|
|
+ // are changed in the editor, and there is a check for nullptr.
|
|
|
+ active_room_manager = this;
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+RoomManager::~RoomManager() {
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+ active_room_manager = nullptr;
|
|
|
+#endif
|
|
|
+}
|
|
|
+void RoomManager::_preview_camera_update() {
|
|
|
+ Ref<World> world = get_world();
|
|
|
+ RID scenario = world->get_scenario();
|
|
|
+
|
|
|
+ if (_godot_preview_camera_ID != (ObjectID)-1) {
|
|
|
+ Camera *cam = Object::cast_to<Camera>(ObjectDB::get_instance(_godot_preview_camera_ID));
|
|
|
+ if (!cam) {
|
|
|
+ _godot_preview_camera_ID = (ObjectID)-1;
|
|
|
+ } else {
|
|
|
+ // get camera position and direction
|
|
|
+ Vector3 camera_pos = cam->get_global_transform().origin;
|
|
|
+ Vector<Plane> planes = cam->get_frustum();
|
|
|
+
|
|
|
+ // only update the visual server when there is a change.. as it will request a screen redraw
|
|
|
+ // this is kinda silly, but the other way would be keeping track of the override camera in visual server
|
|
|
+ // and tracking the camera deletes, which might be more error prone for a debug feature...
|
|
|
+ bool changed = false;
|
|
|
+ if (camera_pos != _godot_camera_pos) {
|
|
|
+ changed = true;
|
|
|
+
|
|
|
+ // update gameplay monitor
|
|
|
+ Vector<Vector3> camera_positions;
|
|
|
+ camera_positions.push_back(camera_pos);
|
|
|
+ VisualServer::get_singleton()->rooms_update_gameplay_monitor(scenario, camera_positions);
|
|
|
+ }
|
|
|
+ // check planes
|
|
|
+ if (!changed) {
|
|
|
+ if (planes.size() != _godot_camera_planes.size()) {
|
|
|
+ changed = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!changed) {
|
|
|
+ // num of planes must be identical
|
|
|
+ for (int n = 0; n < planes.size(); n++) {
|
|
|
+ if (planes[n] != _godot_camera_planes[n]) {
|
|
|
+ changed = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (changed) {
|
|
|
+ _godot_camera_pos = camera_pos;
|
|
|
+ _godot_camera_planes = planes;
|
|
|
+ VisualServer::get_singleton()->rooms_override_camera(scenario, true, camera_pos, &planes);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_notification(int p_what) {
|
|
|
+ switch (p_what) {
|
|
|
+ case NOTIFICATION_ENTER_TREE: {
|
|
|
+ if (Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ set_process_internal(_godot_preview_camera_ID != (ObjectID)-1);
|
|
|
+ } else {
|
|
|
+ if (_settings_gameplay_monitor_enabled) {
|
|
|
+ set_process_internal(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } break;
|
|
|
+ case NOTIFICATION_INTERNAL_PROCESS: {
|
|
|
+ // can't call visual server if not inside world
|
|
|
+ if (!is_inside_world()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ _preview_camera_update();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_settings_gameplay_monitor_enabled) {
|
|
|
+ Ref<World> world = get_world();
|
|
|
+ RID scenario = world->get_scenario();
|
|
|
+
|
|
|
+ List<Camera *> cameras;
|
|
|
+ world->get_camera_list(&cameras);
|
|
|
+
|
|
|
+ Vector<Vector3> positions;
|
|
|
+
|
|
|
+ for (int n = 0; n < cameras.size(); n++) {
|
|
|
+ positions.push_back(cameras[n]->get_global_transform().origin);
|
|
|
+ }
|
|
|
+
|
|
|
+ VisualServer::get_singleton()->rooms_update_gameplay_monitor(scenario, positions);
|
|
|
+ }
|
|
|
+
|
|
|
+ } break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_bind_methods() {
|
|
|
+ BIND_ENUM_CONSTANT(RoomManager::PVS_MODE_DISABLED);
|
|
|
+ BIND_ENUM_CONSTANT(RoomManager::PVS_MODE_PARTIAL);
|
|
|
+ BIND_ENUM_CONSTANT(RoomManager::PVS_MODE_FULL);
|
|
|
+
|
|
|
+ // main functions
|
|
|
+ ClassDB::bind_method(D_METHOD("rooms_convert"), &RoomManager::rooms_convert);
|
|
|
+ ClassDB::bind_method(D_METHOD("rooms_clear"), &RoomManager::rooms_clear);
|
|
|
+
|
|
|
+ ClassDB::bind_method(D_METHOD("set_pvs_mode", "pvs_mode"), &RoomManager::set_pvs_mode);
|
|
|
+ ClassDB::bind_method(D_METHOD("get_pvs_mode"), &RoomManager::get_pvs_mode);
|
|
|
+
|
|
|
+ // These are commented out for now, but available in case we want to cache PVS to disk, the functionality exists
|
|
|
+ // ClassDB::bind_method(D_METHOD("set_pvs_filename", "pvs_filename"), &RoomManager::set_pvs_filename);
|
|
|
+ // ClassDB::bind_method(D_METHOD("get_pvs_filename"), &RoomManager::get_pvs_filename);
|
|
|
+
|
|
|
+ // just some macros to make setting inspector values easier
|
|
|
+#define LPORTAL_STRINGIFY(x) #x
|
|
|
+#define LPORTAL_TOSTRING(x) LPORTAL_STRINGIFY(x)
|
|
|
+
|
|
|
+#define LIMPL_PROPERTY(P_TYPE, P_NAME, P_SET, P_GET) \
|
|
|
+ ClassDB::bind_method(D_METHOD(LPORTAL_TOSTRING(P_SET), LPORTAL_TOSTRING(P_NAME)), &RoomManager::P_SET); \
|
|
|
+ ClassDB::bind_method(D_METHOD(LPORTAL_TOSTRING(P_GET)), &RoomManager::P_GET); \
|
|
|
+ ADD_PROPERTY(PropertyInfo(P_TYPE, LPORTAL_TOSTRING(P_NAME)), LPORTAL_TOSTRING(P_SET), LPORTAL_TOSTRING(P_GET));
|
|
|
+
|
|
|
+#define LIMPL_PROPERTY_RANGE(P_TYPE, P_NAME, P_SET, P_GET, P_RANGE_STRING) \
|
|
|
+ ClassDB::bind_method(D_METHOD(LPORTAL_TOSTRING(P_SET), LPORTAL_TOSTRING(P_NAME)), &RoomManager::P_SET); \
|
|
|
+ ClassDB::bind_method(D_METHOD(LPORTAL_TOSTRING(P_GET)), &RoomManager::P_GET); \
|
|
|
+ ADD_PROPERTY(PropertyInfo(P_TYPE, LPORTAL_TOSTRING(P_NAME), PROPERTY_HINT_RANGE, P_RANGE_STRING), LPORTAL_TOSTRING(P_SET), LPORTAL_TOSTRING(P_GET));
|
|
|
+
|
|
|
+ ADD_GROUP("Main", "");
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, active, rooms_set_active, rooms_get_active);
|
|
|
+ LIMPL_PROPERTY(Variant::NODE_PATH, roomlist, set_roomlist_path, get_roomlist_path);
|
|
|
+
|
|
|
+ ADD_GROUP("PVS", "");
|
|
|
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "pvs_mode", PROPERTY_HINT_ENUM, "Disabled,Partial,Full"), "set_pvs_mode", "get_pvs_mode");
|
|
|
+ // ADD_PROPERTY(PropertyInfo(Variant::STRING, "pvs_filename", PROPERTY_HINT_FILE, "*.pvs"), "set_pvs_filename", "get_pvs_filename");
|
|
|
+
|
|
|
+ ADD_GROUP("Gameplay", "");
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, gameplay_monitor, set_gameplay_monitor_enabled, get_gameplay_monitor_enabled);
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, use_secondary_pvs, set_use_secondary_pvs, get_use_secondary_pvs);
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, use_signals, set_use_signals, get_use_signals);
|
|
|
+
|
|
|
+ ADD_GROUP("Optimize", "");
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, merge_meshes, set_merge_meshes, get_merge_meshes);
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, remove_danglers, set_remove_danglers, get_remove_danglers);
|
|
|
+
|
|
|
+ ADD_GROUP("Debug", "");
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, show_debug, set_show_debug, get_show_debug);
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, show_margins, set_show_margins, get_show_margins);
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, debug_sprawl, set_debug_sprawl, get_debug_sprawl);
|
|
|
+ LIMPL_PROPERTY_RANGE(Variant::INT, overlap_warning_threshold, set_overlap_warning_threshold, get_overlap_warning_threshold, "1,1000,1");
|
|
|
+ LIMPL_PROPERTY(Variant::NODE_PATH, preview_camera, set_preview_camera_path, get_preview_camera_path);
|
|
|
+
|
|
|
+ ADD_GROUP("Advanced", "");
|
|
|
+ LIMPL_PROPERTY(Variant::BOOL, flip_portal_meshes, set_flip_portal_meshes, get_flip_portal_meshes);
|
|
|
+ LIMPL_PROPERTY_RANGE(Variant::INT, portal_depth_limit, set_portal_depth_limit, get_portal_depth_limit, "0,255,1");
|
|
|
+ LIMPL_PROPERTY_RANGE(Variant::REAL, room_simplify, set_room_simplify, get_room_simplify, "0.0,1.0,0.005");
|
|
|
+ LIMPL_PROPERTY_RANGE(Variant::REAL, default_portal_margin, set_default_portal_margin, get_default_portal_margin, "0.0, 10.0, 0.01");
|
|
|
+
|
|
|
+#undef LIMPL_PROPERTY
|
|
|
+#undef LIMPL_PROPERTY_RANGE
|
|
|
+#undef LPORTAL_STRINGIFY
|
|
|
+#undef LPORTAL_TOSTRING
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_preview_camera_path(const NodePath &p_path) {
|
|
|
+ _settings_path_preview_camera = p_path;
|
|
|
+
|
|
|
+ resolve_preview_camera_path();
|
|
|
+
|
|
|
+ bool camera_on = _godot_preview_camera_ID != (ObjectID)-1;
|
|
|
+
|
|
|
+ // make sure the cached camera planes are invalid, this will
|
|
|
+ // force an update to the visual server on the next internal_process
|
|
|
+ _godot_camera_planes.clear();
|
|
|
+
|
|
|
+ // if in the editor, turn processing on or off
|
|
|
+ // according to whether the camera is overridden
|
|
|
+ if (Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ if (is_inside_tree()) {
|
|
|
+ set_process_internal(camera_on);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // if we are turning camera override off, must inform visual server
|
|
|
+ if (!camera_on && is_inside_world() && get_world().is_valid() && get_world()->get_scenario().is_valid()) {
|
|
|
+ VisualServer::get_singleton()->rooms_override_camera(get_world()->get_scenario(), false, Vector3(), nullptr);
|
|
|
+ }
|
|
|
+
|
|
|
+ // we couldn't resolve the path, let's set it to null
|
|
|
+ if (!camera_on) {
|
|
|
+ _settings_path_preview_camera = NodePath();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_room_simplify(real_t p_value) {
|
|
|
+ _room_simplify_info.set_simplify(p_value);
|
|
|
+}
|
|
|
+
|
|
|
+real_t RoomManager::get_room_simplify() const {
|
|
|
+ return _room_simplify_info._plane_simplify;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_flip_portal_meshes(bool p_flip) {
|
|
|
+ Portal::_portal_plane_convention = p_flip;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::get_flip_portal_meshes() const {
|
|
|
+ return Portal::_portal_plane_convention;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_portal_depth_limit(int p_limit) {
|
|
|
+ _portal_depth_limit = p_limit;
|
|
|
+
|
|
|
+ if (is_inside_world() && get_world().is_valid()) {
|
|
|
+ VisualServer::get_singleton()->rooms_set_params(get_world()->get_scenario(), p_limit);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_default_portal_margin(real_t p_dist) {
|
|
|
+ _default_portal_margin = p_dist;
|
|
|
+
|
|
|
+ // send to portals
|
|
|
+ Spatial *roomlist = _resolve_path<Spatial>(_settings_path_roomlist);
|
|
|
+ if (!roomlist) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _update_portal_margins(roomlist, _default_portal_margin);
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_update_portal_margins(Spatial *p_node, real_t p_margin) {
|
|
|
+ Portal *portal = Object::cast_to<Portal>(p_node);
|
|
|
+
|
|
|
+ if (portal) {
|
|
|
+ portal->_default_margin = p_margin;
|
|
|
+ portal->update_gizmo();
|
|
|
+ }
|
|
|
+
|
|
|
+ // recurse
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_node->get_child(n));
|
|
|
+
|
|
|
+ if (child) {
|
|
|
+ _update_portal_margins(child, p_margin);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+real_t RoomManager::get_default_portal_margin() const {
|
|
|
+ return _default_portal_margin;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_show_margins(bool p_show) {
|
|
|
+ Portal::_settings_gizmo_show_margins = p_show;
|
|
|
+
|
|
|
+ Spatial *roomlist = _resolve_path<Spatial>(_settings_path_roomlist);
|
|
|
+ if (!roomlist) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _update_gizmos_recursive(roomlist);
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::get_show_margins() const {
|
|
|
+ return Portal::_settings_gizmo_show_margins;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_show_debug(bool p_show) {
|
|
|
+ // force not to show when not in editor
|
|
|
+ if (!Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ p_show = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ _show_debug = p_show;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::get_show_debug() const {
|
|
|
+ return _show_debug;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_debug_sprawl(bool p_enable) {
|
|
|
+ if (is_inside_world() && get_world().is_valid()) {
|
|
|
+ VisualServer::get_singleton()->rooms_set_debug_feature(get_world()->get_scenario(), VisualServer::ROOMS_DEBUG_SPRAWL, p_enable);
|
|
|
+ _debug_sprawl = p_enable;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::get_debug_sprawl() const {
|
|
|
+ return _debug_sprawl;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_merge_meshes(bool p_enable) {
|
|
|
+ _settings_merge_meshes = p_enable;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::get_merge_meshes() const {
|
|
|
+ return _settings_merge_meshes;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_remove_danglers(bool p_enable) {
|
|
|
+ _settings_remove_danglers = p_enable;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::get_remove_danglers() const {
|
|
|
+ return _settings_remove_danglers;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::show_warning(const String &p_string, const String &p_extra_string, bool p_alert) {
|
|
|
+ if (p_extra_string != "") {
|
|
|
+ WARN_PRINT(p_string + " " + p_extra_string);
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+ if (p_alert && Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ EditorNode::get_singleton()->show_warning(TTR(p_string + "\n" + p_extra_string));
|
|
|
+ }
|
|
|
+#endif
|
|
|
+ } else {
|
|
|
+ WARN_PRINT(p_string);
|
|
|
+ // OS::get_singleton()->alert(p_string, p_title);
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+ if (p_alert && Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ EditorNode::get_singleton()->show_warning(TTR(p_string));
|
|
|
+ }
|
|
|
+#endif
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::debug_print_line(String p_string, int p_priority) {
|
|
|
+ if (_show_debug) {
|
|
|
+ if (!p_priority) {
|
|
|
+ print_verbose(p_string);
|
|
|
+ } else {
|
|
|
+ print_line(p_string);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::rooms_set_active(bool p_active) {
|
|
|
+ if (is_inside_world() && get_world().is_valid()) {
|
|
|
+ VisualServer::get_singleton()->rooms_set_active(get_world()->get_scenario(), p_active);
|
|
|
+ _active = p_active;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::rooms_get_active() const {
|
|
|
+ return _active;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_pvs_mode(PVSMode p_mode) {
|
|
|
+ _pvs_mode = p_mode;
|
|
|
+}
|
|
|
+
|
|
|
+RoomManager::PVSMode RoomManager::get_pvs_mode() const {
|
|
|
+ return _pvs_mode;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::set_pvs_filename(String p_filename) {
|
|
|
+ _pvs_filename = p_filename;
|
|
|
+}
|
|
|
+
|
|
|
+String RoomManager::get_pvs_filename() const {
|
|
|
+ return _pvs_filename;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_rooms_changed() {
|
|
|
+ _rooms.clear();
|
|
|
+ if (is_inside_world() && get_world().is_valid()) {
|
|
|
+ VisualServer::get_singleton()->rooms_unload(get_world()->get_scenario());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::rooms_clear() {
|
|
|
+ _rooms.clear();
|
|
|
+ if (is_inside_world() && get_world().is_valid()) {
|
|
|
+ VisualServer::get_singleton()->rooms_and_portals_clear(get_world()->get_scenario());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::rooms_flip_portals() {
|
|
|
+ // this is a helper emergency function to deal with situations where the user has ended up with Portal nodes
|
|
|
+ // pointing in the wrong direction (by doing initial conversion with flip_portal_meshes set incorrectly).
|
|
|
+ _roomlist = _resolve_path<Spatial>(_settings_path_roomlist);
|
|
|
+ if (!_roomlist) {
|
|
|
+ WARN_PRINT("Cannot resolve nodepath");
|
|
|
+ show_warning("RoomList path is invalid.", "Please check the RoomList branch has been assigned in the RoomManager");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _flip_portals_recursive(_roomlist);
|
|
|
+ _rooms_changed();
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::rooms_convert() {
|
|
|
+ // set all error conditions to false
|
|
|
+ _warning_misnamed_nodes_detected = false;
|
|
|
+ _warning_portal_link_room_not_found = false;
|
|
|
+ _warning_portal_autolink_failed = false;
|
|
|
+ _warning_room_overlap_detected = false;
|
|
|
+
|
|
|
+ _roomlist = _resolve_path<Spatial>(_settings_path_roomlist);
|
|
|
+ if (!_roomlist) {
|
|
|
+ WARN_PRINT("Cannot resolve nodepath");
|
|
|
+ show_warning("RoomList path is invalid.", "Please check the RoomList branch has been assigned in the RoomManager");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ERR_FAIL_COND(!is_inside_world() || !get_world().is_valid());
|
|
|
+
|
|
|
+ // every time we run convert we increment this,
|
|
|
+ // to prevent individual rooms / portals being converted
|
|
|
+ // more than once in one run
|
|
|
+ _conversion_tick++;
|
|
|
+
|
|
|
+ rooms_clear();
|
|
|
+
|
|
|
+ // first check that the roomlist is valid, and the user hasn't made
|
|
|
+ // a silly scene tree
|
|
|
+ Node *invalid_node = _check_roomlist_validity_recursive(_roomlist);
|
|
|
+ if (invalid_node) {
|
|
|
+ show_warning("RoomList contains invalid node", "RoomList should only contain Rooms, RoomGroups and Spatials.\nInvalid node : " + invalid_node->get_name());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalVector<Portal *> portals;
|
|
|
+ LocalVector<RoomGroup *> roomgroups;
|
|
|
+
|
|
|
+ // find the rooms and portals
|
|
|
+ _convert_rooms_recursive(_roomlist, portals, roomgroups);
|
|
|
+
|
|
|
+ if (!_rooms.size()) {
|
|
|
+ rooms_clear();
|
|
|
+ show_warning("RoomList contains no Rooms, aborting");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // add portal links
|
|
|
+ _second_pass_portals(_roomlist, portals);
|
|
|
+
|
|
|
+ // create the statics
|
|
|
+ _second_pass_rooms(roomgroups, portals);
|
|
|
+
|
|
|
+ // third pass
|
|
|
+ // autolink portals that are not already manually linked
|
|
|
+ // and finalize the portals
|
|
|
+ _third_pass_portals(_roomlist, portals);
|
|
|
+
|
|
|
+ // finalize the room hulls
|
|
|
+ _third_pass_rooms(portals);
|
|
|
+
|
|
|
+ bool generate_pvs = false;
|
|
|
+ bool pvs_cull = false;
|
|
|
+ switch (_pvs_mode) {
|
|
|
+ default: {
|
|
|
+ } break;
|
|
|
+ case PVS_MODE_PARTIAL: {
|
|
|
+ generate_pvs = true;
|
|
|
+ } break;
|
|
|
+ case PVS_MODE_FULL: {
|
|
|
+ generate_pvs = true;
|
|
|
+ pvs_cull = true;
|
|
|
+ } break;
|
|
|
+ }
|
|
|
+
|
|
|
+ VisualServer::get_singleton()->rooms_finalize(get_world()->get_scenario(), generate_pvs, pvs_cull, _settings_use_secondary_pvs, _settings_use_signals, _pvs_filename);
|
|
|
+
|
|
|
+ // refresh whether to show portals etc
|
|
|
+ set_show_debug(_show_debug);
|
|
|
+
|
|
|
+ // refresh portal depth limit
|
|
|
+ set_portal_depth_limit(get_portal_depth_limit());
|
|
|
+
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+ _generate_room_overlap_zones();
|
|
|
+#endif
|
|
|
+
|
|
|
+ // just delete any intermediate data
|
|
|
+ _cleanup_after_conversion();
|
|
|
+
|
|
|
+ // display error dialogs
|
|
|
+ if (_warning_misnamed_nodes_detected) {
|
|
|
+ show_warning("Misnamed nodes detected, check output log for details. Aborting.");
|
|
|
+ rooms_clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_warning_portal_link_room_not_found) {
|
|
|
+ show_warning("Portal link room not found, check output log for details.");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_warning_portal_autolink_failed) {
|
|
|
+ show_warning("Portal autolink failed, check output log for details.\nCheck the portal is facing outwards from the source room.");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_warning_room_overlap_detected) {
|
|
|
+ show_warning("Room overlap detected, cameras may work incorrectly in overlapping area.\nCheck output log for details..");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_second_pass_room(Room *p_room, const LocalVector<RoomGroup *> &p_roomgroups, const LocalVector<Portal *> &p_portals) {
|
|
|
+ if (_settings_merge_meshes) {
|
|
|
+ _merge_meshes_in_room(p_room);
|
|
|
+ }
|
|
|
+
|
|
|
+ // find statics and manual bound
|
|
|
+ bool manual_bound_found = false;
|
|
|
+
|
|
|
+ // points making up the room geometry, in world space, to create the convex hull
|
|
|
+ Vector<Vector3> room_pts;
|
|
|
+
|
|
|
+ for (int n = 0; n < p_room->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_room->get_child(n));
|
|
|
+
|
|
|
+ if (child) {
|
|
|
+ if (_name_starts_with(child, "GPortal", true) || _node_is_type<Portal>(child)) {
|
|
|
+ // the adding of portal points is done after this stage, because
|
|
|
+ // we need to take into account incoming as well as outgoing portals
|
|
|
+ } else if (_name_starts_with(child, "Bound", true)) {
|
|
|
+ manual_bound_found = _convert_manual_bound(p_room, child, p_portals);
|
|
|
+ } else {
|
|
|
+ _find_statics_recursive(p_room, child, room_pts);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Has the bound been specified using points in the room?
|
|
|
+ // in that case, overwrite the room_pts
|
|
|
+ if (p_room->_bound_pts.size() && p_room->is_inside_tree()) {
|
|
|
+ Transform tr = p_room->get_global_transform();
|
|
|
+
|
|
|
+ room_pts.clear();
|
|
|
+ room_pts.resize(p_room->_bound_pts.size());
|
|
|
+ for (int n = 0; n < room_pts.size(); n++) {
|
|
|
+ room_pts.set(n, tr.xform(p_room->_bound_pts[n]));
|
|
|
+ }
|
|
|
+
|
|
|
+ // we override and manual bound with the room points
|
|
|
+ manual_bound_found = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!manual_bound_found) {
|
|
|
+ // rough aabb for checking portals for warning conditions
|
|
|
+ AABB aabb;
|
|
|
+ aabb.create_from_points(room_pts);
|
|
|
+
|
|
|
+ for (int n = 0; n < p_room->_portals.size(); n++) {
|
|
|
+ int portal_id = p_room->_portals[n];
|
|
|
+ Portal *portal = p_portals[portal_id];
|
|
|
+
|
|
|
+ // don't add portals to the world bound that are internal to this room!
|
|
|
+ if (portal->is_portal_internal(p_room->_room_ID)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // check portal for suspect conditions, like a long way from the room AABB,
|
|
|
+ // or possibly flipped the wrong way
|
|
|
+ _check_portal_for_warnings(portal, aabb);
|
|
|
+ }
|
|
|
+
|
|
|
+ // create convex hull
|
|
|
+ _convert_room_hull_preliminary(p_room, room_pts, p_portals);
|
|
|
+ }
|
|
|
+
|
|
|
+ // add the room to roomgroups
|
|
|
+ for (int n = 0; n < p_room->_roomgroups.size(); n++) {
|
|
|
+ int roomgroup_id = p_room->_roomgroups[n];
|
|
|
+ p_roomgroups[roomgroup_id]->add_room(p_room);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_second_pass_rooms(const LocalVector<RoomGroup *> &p_roomgroups, const LocalVector<Portal *> &p_portals) {
|
|
|
+ for (int n = 0; n < _rooms.size(); n++) {
|
|
|
+ _second_pass_room(_rooms[n], p_roomgroups, p_portals);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+void RoomManager::_generate_room_overlap_zones() {
|
|
|
+ for (int n = 0; n < _rooms.size(); n++) {
|
|
|
+ Room *room = _rooms[n];
|
|
|
+
|
|
|
+ // no planes .. no overlap
|
|
|
+ if (!room->_planes.size()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int c = n + 1; c < _rooms.size(); c++) {
|
|
|
+ if (c == n) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Room *other = _rooms[c];
|
|
|
+
|
|
|
+ // do a quick reject AABB
|
|
|
+ if (!room->_aabb.intersects(other->_aabb) || (!other->_planes.size())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // if the room priorities are different (i.e. an internal room), they are allowed to overlap
|
|
|
+ if (room->_room_priority != other->_room_priority) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // get all the planes of both rooms in a contiguous list
|
|
|
+ LocalVector<Plane, int32_t> planes;
|
|
|
+ planes.resize(room->_planes.size() + other->_planes.size());
|
|
|
+ Plane *dest = planes.ptr();
|
|
|
+ memcpy(dest, &room->_planes[0], room->_planes.size() * sizeof(Plane));
|
|
|
+ dest += room->_planes.size();
|
|
|
+
|
|
|
+ memcpy(dest, &other->_planes[0], other->_planes.size() * sizeof(Plane));
|
|
|
+
|
|
|
+ Vector<Vector3> overlap_pts = Geometry::compute_convex_mesh_points(planes.ptr(), planes.size());
|
|
|
+
|
|
|
+ if (overlap_pts.size() < 4) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // there is an overlap, create a mesh from the points
|
|
|
+ Geometry::MeshData md;
|
|
|
+ Error err = _build_convex_hull(overlap_pts, md);
|
|
|
+
|
|
|
+ if (err != OK) {
|
|
|
+ WARN_PRINT("QuickHull failed building room overlap hull");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // only if the volume is more than some threshold
|
|
|
+ real_t volume = Geometry::calculate_convex_hull_volume(md);
|
|
|
+ if (volume > _overlap_warning_threshold) {
|
|
|
+ WARN_PRINT("Room overlap of " + String(Variant(volume)) + " detected between " + room->get_name() + " and " + other->get_name());
|
|
|
+ room->_gizmo_overlap_zones.push_back(md);
|
|
|
+ _warning_room_overlap_detected = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+#endif
|
|
|
+
|
|
|
+void RoomManager::_third_pass_rooms(const LocalVector<Portal *> &p_portals) {
|
|
|
+ bool found_errors = false;
|
|
|
+
|
|
|
+ for (int n = 0; n < _rooms.size(); n++) {
|
|
|
+ Room *room = _rooms[n];
|
|
|
+ if (!_convert_room_hull_final(room, p_portals)) {
|
|
|
+ found_errors = true;
|
|
|
+ }
|
|
|
+ room->update_gizmo();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (found_errors) {
|
|
|
+ show_warning("ERROR calculating room bounds.", "Ensure all rooms contain geometry or manual bounds.");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_second_pass_portals(Spatial *p_roomlist, LocalVector<Portal *> &r_portals) {
|
|
|
+ convert_log("_second_pass_portals");
|
|
|
+
|
|
|
+ for (unsigned int n = 0; n < r_portals.size(); n++) {
|
|
|
+ Portal *portal = r_portals[n];
|
|
|
+ String string_link_room_shortname = _find_name_after(portal, "Portal");
|
|
|
+ String string_link_room = "Room" + GODOT_PORTAL_DELINEATOR + string_link_room_shortname;
|
|
|
+
|
|
|
+ if (string_link_room_shortname != "") {
|
|
|
+ Room *linked_room = Object::cast_to<Room>(p_roomlist->find_node(string_link_room, true, false));
|
|
|
+ if (linked_room) {
|
|
|
+ NodePath path = portal->get_path_to(linked_room);
|
|
|
+ portal->set_linked_room_internal(path);
|
|
|
+ } else {
|
|
|
+ WARN_PRINT("Portal link room : " + string_link_room + " not found.");
|
|
|
+ _warning_portal_link_room_not_found = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // get the room we are linking from
|
|
|
+ int room_from_id = portal->_linkedroom_ID[0];
|
|
|
+ if (room_from_id != -1) {
|
|
|
+ Room *room_from = _rooms[room_from_id];
|
|
|
+ portal->resolve_links(room_from->_room_rid);
|
|
|
+
|
|
|
+ // add the portal id to the room from and the room to.
|
|
|
+ // These are used so we can later add the portal geometry to the room bounds.
|
|
|
+ room_from->_portals.push_back(n);
|
|
|
+
|
|
|
+ int room_to_id = portal->_linkedroom_ID[1];
|
|
|
+ if (room_to_id != -1) {
|
|
|
+ Room *room_to = _rooms[room_to_id];
|
|
|
+ room_to->_portals.push_back(n);
|
|
|
+
|
|
|
+ // make the portal internal if necessary
|
|
|
+ portal->_internal = room_from->_room_priority > room_to->_room_priority;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_third_pass_portals(Spatial *p_roomlist, LocalVector<Portal *> &r_portals) {
|
|
|
+ convert_log("_third_pass_portals");
|
|
|
+
|
|
|
+ for (unsigned int n = 0; n < r_portals.size(); n++) {
|
|
|
+ Portal *portal = r_portals[n];
|
|
|
+
|
|
|
+ // all portals should have a source room
|
|
|
+ DEV_ASSERT(portal->_linkedroom_ID[0] != -1);
|
|
|
+ const Room *source_room = _rooms[portal->_linkedroom_ID[0]];
|
|
|
+
|
|
|
+ if (portal->_linkedroom_ID[1] != -1) {
|
|
|
+ // already manually linked
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool autolink_found = false;
|
|
|
+
|
|
|
+ // try to autolink
|
|
|
+ // try points iteratively out from the portal center and find the first that is in a room that isn't the source room
|
|
|
+ for (int attempt = 0; attempt < 4; attempt++) {
|
|
|
+ // found
|
|
|
+ if (portal->_linkedroom_ID[1] != -1) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // these numbers are arbitrary .. we could alternatively reuse the portal margins for this?
|
|
|
+ real_t dist = 0.01;
|
|
|
+ switch (attempt) {
|
|
|
+ default: {
|
|
|
+ dist = 0.01;
|
|
|
+ } break;
|
|
|
+ case 1: {
|
|
|
+ dist = 0.1;
|
|
|
+ } break;
|
|
|
+ case 2: {
|
|
|
+ dist = 1.0;
|
|
|
+ } break;
|
|
|
+ case 3: {
|
|
|
+ dist = 2.0;
|
|
|
+ } break;
|
|
|
+ }
|
|
|
+
|
|
|
+ Vector3 test_pos = portal->_pt_center_world + (dist * portal->_plane.normal);
|
|
|
+
|
|
|
+ for (int r = 0; r < _rooms.size(); r++) {
|
|
|
+ Room *room = _rooms[r];
|
|
|
+ if (room->_room_ID == portal->_linkedroom_ID[0]) {
|
|
|
+ // can't link back to the source room
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // first do a rough aabb check
|
|
|
+ if (!room->_aabb.has_point(test_pos)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool outside = false;
|
|
|
+ for (int p = 0; p < room->_preliminary_planes.size(); p++) {
|
|
|
+ const Plane &plane = room->_preliminary_planes[p];
|
|
|
+ if (plane.distance_to(test_pos) > 0.0) {
|
|
|
+ outside = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } // for through planes
|
|
|
+
|
|
|
+ if (!outside) {
|
|
|
+ // great, we found a linked room!
|
|
|
+ convert_log("\tauto linked portal from room " + source_room->get_name() + " to room " + room->get_name(), 1);
|
|
|
+ portal->_linkedroom_ID[1] = r;
|
|
|
+
|
|
|
+ // add the portal to the portals list for the receiving room
|
|
|
+ room->_portals.push_back(n);
|
|
|
+
|
|
|
+ // send complete link to visual server so the portal will be active in the visual server room system
|
|
|
+ VisualServer::get_singleton()->portal_link(portal->_portal_rid, source_room->_room_rid, room->_room_rid, portal->_settings_two_way);
|
|
|
+
|
|
|
+ autolink_found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ } // for through rooms
|
|
|
+
|
|
|
+ } // for attempt
|
|
|
+
|
|
|
+ // error condition
|
|
|
+ if (!autolink_found) {
|
|
|
+ WARN_PRINT("Portal autolink failed for Portal from " + source_room->get_name());
|
|
|
+ _warning_portal_autolink_failed = true;
|
|
|
+ }
|
|
|
+ } // for portal
|
|
|
+}
|
|
|
+
|
|
|
+// to prevent users creating mistakes for themselves, we limit what can be put into the room list branch.
|
|
|
+// returns invalid node, or NULL
|
|
|
+Node *RoomManager::_check_roomlist_validity_recursive(Node *p_node) {
|
|
|
+ bool ok = false;
|
|
|
+
|
|
|
+ // is this a room?
|
|
|
+ if (_name_starts_with(p_node, "Room") || _node_is_type<Room>(p_node)) {
|
|
|
+ // end the recursion here
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // is this a roomgroup?
|
|
|
+ if (_name_starts_with(p_node, "RoomGroup") || _node_is_type<RoomGroup>(p_node)) {
|
|
|
+ // end the recursion here
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // now we are getting dodgy.
|
|
|
+ // is it a Spatial? (and not a derived)
|
|
|
+ if (p_node->get_class_name() == "Spatial") {
|
|
|
+ ok = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ok) {
|
|
|
+ // return the invalid node
|
|
|
+ return p_node;
|
|
|
+ }
|
|
|
+
|
|
|
+ // recurse
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Node *child = p_node->get_child(n);
|
|
|
+ if (child) {
|
|
|
+ Node *invalid_node = _check_roomlist_validity_recursive(child);
|
|
|
+ if (invalid_node) {
|
|
|
+ return invalid_node;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nullptr;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_convert_rooms_recursive(Spatial *p_node, LocalVector<Portal *> &r_portals, LocalVector<RoomGroup *> &r_roomgroups, int p_roomgroup) {
|
|
|
+ // is this a room?
|
|
|
+ if (_name_starts_with(p_node, "Room") || _node_is_type<Room>(p_node)) {
|
|
|
+ _convert_room(p_node, r_portals, r_roomgroups, p_roomgroup);
|
|
|
+ }
|
|
|
+
|
|
|
+ // is this a roomgroup?
|
|
|
+ if (_name_starts_with(p_node, "RoomGroup") || _node_is_type<RoomGroup>(p_node)) {
|
|
|
+ p_roomgroup = _convert_roomgroup(p_node, r_roomgroups);
|
|
|
+ }
|
|
|
+
|
|
|
+ // recurse through children
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_node->get_child(n));
|
|
|
+
|
|
|
+ if (child) {
|
|
|
+ _convert_rooms_recursive(child, r_portals, r_roomgroups, p_roomgroup);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+int RoomManager::_convert_roomgroup(Spatial *p_node, LocalVector<RoomGroup *> &r_roomgroups) {
|
|
|
+ String string_full_name = p_node->get_name();
|
|
|
+
|
|
|
+ // is it already a roomgroup?
|
|
|
+ RoomGroup *roomgroup = Object::cast_to<RoomGroup>(p_node);
|
|
|
+
|
|
|
+ // if not already a RoomGroup, convert the node and move all children
|
|
|
+ if (!roomgroup) {
|
|
|
+ // create a RoomGroup
|
|
|
+ roomgroup = _change_node_type<RoomGroup>(p_node, "G");
|
|
|
+ } else {
|
|
|
+ // already hit this tick?
|
|
|
+ if (roomgroup->_conversion_tick == _conversion_tick) {
|
|
|
+ return roomgroup->_roomgroup_ID;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ convert_log("convert_roomgroup : " + string_full_name, 1);
|
|
|
+
|
|
|
+ // make sure the roomgroup is blank, especially if already created
|
|
|
+ roomgroup->clear();
|
|
|
+
|
|
|
+ // make sure the object ID is sent to the visual server
|
|
|
+ VisualServer::get_singleton()->roomgroup_prepare(roomgroup->_room_group_rid, roomgroup->get_instance_id());
|
|
|
+
|
|
|
+ // mark so as only to convert once
|
|
|
+ roomgroup->_conversion_tick = _conversion_tick;
|
|
|
+
|
|
|
+ roomgroup->_roomgroup_ID = r_roomgroups.size();
|
|
|
+
|
|
|
+ r_roomgroups.push_back(roomgroup);
|
|
|
+
|
|
|
+ return r_roomgroups.size() - 1;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_convert_room(Spatial *p_node, LocalVector<Portal *> &r_portals, const LocalVector<RoomGroup *> &p_roomgroups, int p_roomgroup) {
|
|
|
+ String string_full_name = p_node->get_name();
|
|
|
+
|
|
|
+ // is it already an lroom?
|
|
|
+ Room *room = Object::cast_to<Room>(p_node);
|
|
|
+
|
|
|
+ // if not already a Room, convert the node and move all children
|
|
|
+ if (!room) {
|
|
|
+ // create a Room
|
|
|
+ room = _change_node_type<Room>(p_node, "G");
|
|
|
+ } else {
|
|
|
+ // already hit this tick?
|
|
|
+ if (room->_conversion_tick == _conversion_tick) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ convert_log("convert_room : " + string_full_name, 1);
|
|
|
+
|
|
|
+ // make sure the room is blank, especially if already created
|
|
|
+ room->clear();
|
|
|
+
|
|
|
+ // mark so as only to convert once
|
|
|
+ room->_conversion_tick = _conversion_tick;
|
|
|
+
|
|
|
+ // set roomgroup
|
|
|
+ if (p_roomgroup != -1) {
|
|
|
+ room->_roomgroups.push_back(p_roomgroup);
|
|
|
+ room->_room_priority = p_roomgroups[p_roomgroup]->_settings_priority;
|
|
|
+
|
|
|
+ VisualServer::get_singleton()->room_prepare(room->_room_rid, room->_room_priority);
|
|
|
+ }
|
|
|
+
|
|
|
+ // add to the list of rooms
|
|
|
+ room->_room_ID = _rooms.size();
|
|
|
+ _rooms.push_back(room);
|
|
|
+
|
|
|
+ _find_portals_recursive(room, room, r_portals);
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_find_portals_recursive(Spatial *p_node, Room *p_room, LocalVector<Portal *> &r_portals) {
|
|
|
+ MeshInstance *mi = Object::cast_to<MeshInstance>(p_node);
|
|
|
+ if ((mi && _name_starts_with(mi, "Portal", true)) || _node_is_type<Portal>(p_node)) {
|
|
|
+ _convert_portal(p_room, p_node, r_portals);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_node->get_child(n));
|
|
|
+
|
|
|
+ if (child) {
|
|
|
+ _find_portals_recursive(child, p_room, r_portals);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_check_portal_for_warnings(Portal *p_portal, const AABB &p_room_aabb_without_portals) {
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+ AABB bb = p_room_aabb_without_portals;
|
|
|
+ bb = bb.grow(bb.get_longest_axis_size() * 0.5);
|
|
|
+
|
|
|
+ bool changed = false;
|
|
|
+
|
|
|
+ // far outside the room?
|
|
|
+ const Vector3 &pos = p_portal->get_global_transform().origin;
|
|
|
+ if (p_portal->_warning_outside_room_aabb != (!bb.has_point(pos))) {
|
|
|
+ p_portal->_warning_outside_room_aabb = !p_portal->_warning_outside_room_aabb;
|
|
|
+ changed = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (p_portal->_warning_outside_room_aabb) {
|
|
|
+ WARN_PRINT(String(p_portal->get_name()) + " possibly in the wrong room.");
|
|
|
+ }
|
|
|
+
|
|
|
+ // facing wrong way?
|
|
|
+ Vector3 offset = pos - bb.get_center();
|
|
|
+ real_t dot = offset.dot(p_portal->_plane.normal);
|
|
|
+ if (p_portal->_warning_facing_wrong_way != (dot < 0.0)) {
|
|
|
+ p_portal->_warning_facing_wrong_way = !p_portal->_warning_facing_wrong_way;
|
|
|
+ changed = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (p_portal->_warning_facing_wrong_way) {
|
|
|
+ WARN_PRINT(String(p_portal->get_name()) + " possibly facing the wrong way.");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (changed) {
|
|
|
+ p_portal->update_gizmo();
|
|
|
+ }
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_find_statics_recursive(Room *p_room, Spatial *p_node, Vector<Vector3> &r_room_pts) {
|
|
|
+ bool ignore = false;
|
|
|
+ VisualInstance *vi = Object::cast_to<VisualInstance>(p_node);
|
|
|
+
|
|
|
+ // we are only interested in VIs with static or dynamic mode
|
|
|
+ if (vi) {
|
|
|
+ switch (vi->get_portal_mode()) {
|
|
|
+ default: {
|
|
|
+ ignore = true;
|
|
|
+ } break;
|
|
|
+ case CullInstance::PORTAL_MODE_DYNAMIC:
|
|
|
+ case CullInstance::PORTAL_MODE_STATIC:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ignore) {
|
|
|
+ // lights
|
|
|
+ Light *light = Object::cast_to<Light>(p_node);
|
|
|
+ if (light) {
|
|
|
+ convert_log("\tfound Light " + light->get_name());
|
|
|
+
|
|
|
+ Vector<Vector3> dummy_pts;
|
|
|
+ VisualServer::get_singleton()->room_add_instance(p_room->_room_rid, light->get_instance(), light->get_transformed_aabb(), dummy_pts);
|
|
|
+ }
|
|
|
+
|
|
|
+ GeometryInstance *gi = Object::cast_to<GeometryInstance>(p_node);
|
|
|
+
|
|
|
+ if (gi) {
|
|
|
+ MeshInstance *mi = Object::cast_to<MeshInstance>(p_node);
|
|
|
+ if (mi) {
|
|
|
+ convert_log("\tfound MeshInst " + mi->get_name());
|
|
|
+
|
|
|
+ Vector<Vector3> object_pts;
|
|
|
+ AABB aabb;
|
|
|
+ // get the object points and don't immediately add to the room
|
|
|
+ // points, as we want to use these points for sprawling algorithm in
|
|
|
+ // the visual server.
|
|
|
+ if (_bound_findpoints_mesh_instance(mi, object_pts, aabb)) {
|
|
|
+ // need to keep track of room bound
|
|
|
+ // NOTE the is_visible check MAY cause problems if conversion run on nodes that
|
|
|
+ // aren't properly in the tree. It can optionally be removed. Certainly calling is_visible_in_tree
|
|
|
+ // DID cause problems.
|
|
|
+ if (mi->get_include_in_bound() && mi->is_visible()) {
|
|
|
+ r_room_pts.append_array(object_pts);
|
|
|
+ }
|
|
|
+
|
|
|
+ VisualServer::get_singleton()->room_add_instance(p_room->_room_rid, mi->get_instance(), mi->get_transformed_aabb(), object_pts);
|
|
|
+ } // if bound found points
|
|
|
+ } else {
|
|
|
+ // geometry instance but not a mesh instance .. just use AABB
|
|
|
+ convert_log("\tfound GeomInst " + gi->get_name());
|
|
|
+ // Vector<Vector3> dummy_pts;
|
|
|
+ // VisualServer::get_singleton()->room_add_instance(p_room->_room_rid, gi->get_instance(), gi->get_transformed_aabb(), dummy_pts);
|
|
|
+ Vector<Vector3> object_pts;
|
|
|
+ AABB aabb;
|
|
|
+ if (_bound_findpoints_geom_instance(gi, object_pts, aabb)) {
|
|
|
+ // need to keep track of room bound
|
|
|
+ // NOTE the is_visible check MAY cause problems if conversion run on nodes that
|
|
|
+ // aren't properly in the tree. It can optionally be removed. Certainly calling is_visible_in_tree
|
|
|
+ // DID cause problems.
|
|
|
+ if (gi->get_include_in_bound() && gi->is_visible()) {
|
|
|
+ r_room_pts.append_array(object_pts);
|
|
|
+ }
|
|
|
+
|
|
|
+ VisualServer::get_singleton()->room_add_instance(p_room->_room_rid, gi->get_instance(), gi->get_transformed_aabb(), object_pts);
|
|
|
+ } // if bound found points
|
|
|
+ }
|
|
|
+ } // if gi
|
|
|
+
|
|
|
+ VisibilityNotifier *vn = Object::cast_to<VisibilityNotifier>(p_node);
|
|
|
+ if (vn && ((vn->get_portal_mode() == CullInstance::PORTAL_MODE_DYNAMIC) || (vn->get_portal_mode() == CullInstance::PORTAL_MODE_STATIC))) {
|
|
|
+ convert_log("\tfound VisibilityNotifier " + vn->get_name());
|
|
|
+
|
|
|
+ AABB world_aabb = vn->get_global_transform().xform(vn->get_aabb());
|
|
|
+ VisualServer::get_singleton()->room_add_ghost(p_room->_room_rid, vn->get_instance_id(), world_aabb);
|
|
|
+ }
|
|
|
+
|
|
|
+ } // if not ignore
|
|
|
+
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_node->get_child(n));
|
|
|
+
|
|
|
+ if (child) {
|
|
|
+ _find_statics_recursive(p_room, child, r_room_pts);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_convert_manual_bound(Room *p_room, Spatial *p_node, const LocalVector<Portal *> &p_portals) {
|
|
|
+ MeshInstance *mi = Object::cast_to<MeshInstance>(p_node);
|
|
|
+ if (!mi) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Vector<Vector3> points;
|
|
|
+ AABB aabb;
|
|
|
+ if (!_bound_findpoints_mesh_instance(mi, points, aabb)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ mi->set_portal_mode(CullInstance::PORTAL_MODE_IGNORE);
|
|
|
+
|
|
|
+ // hide bounds after conversion
|
|
|
+ // set to portal mode ignore?
|
|
|
+ mi->hide();
|
|
|
+
|
|
|
+ return _convert_room_hull_preliminary(p_room, points, p_portals);
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_convert_room_hull_preliminary(Room *p_room, const Vector<Vector3> &p_room_pts, const LocalVector<Portal *> &p_portals) {
|
|
|
+ if (p_room_pts.size() <= 3) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Geometry::MeshData md;
|
|
|
+
|
|
|
+ Error err = OK;
|
|
|
+
|
|
|
+ // if there are too many room points, quickhull will fail or freeze etc, so we will revert
|
|
|
+ // to a bounding rect and send an error message
|
|
|
+ if (p_room_pts.size() > 100000) {
|
|
|
+ WARN_PRINT(String(p_room->get_name()) + " contains too many vertices to find convex hull, use a manual bound instead.");
|
|
|
+
|
|
|
+ AABB aabb;
|
|
|
+ aabb.create_from_points(p_room_pts);
|
|
|
+
|
|
|
+ LocalVector<Vector3> pts;
|
|
|
+ Vector3 mins = aabb.position;
|
|
|
+ Vector3 maxs = mins + aabb.size;
|
|
|
+
|
|
|
+ pts.push_back(Vector3(mins.x, mins.y, mins.z));
|
|
|
+ pts.push_back(Vector3(mins.x, maxs.y, mins.z));
|
|
|
+ pts.push_back(Vector3(maxs.x, maxs.y, mins.z));
|
|
|
+ pts.push_back(Vector3(maxs.x, mins.y, mins.z));
|
|
|
+ pts.push_back(Vector3(mins.x, mins.y, maxs.z));
|
|
|
+ pts.push_back(Vector3(mins.x, maxs.y, maxs.z));
|
|
|
+ pts.push_back(Vector3(maxs.x, maxs.y, maxs.z));
|
|
|
+ pts.push_back(Vector3(maxs.x, mins.y, maxs.z));
|
|
|
+
|
|
|
+ err = _build_convex_hull(pts, md);
|
|
|
+ } else {
|
|
|
+ err = _build_room_convex_hull(p_room, p_room_pts, md);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (err != OK) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // add any existing portals planes first, as these will trump any other existing planes further out
|
|
|
+ for (int n = 0; n < p_room->_portals.size(); n++) {
|
|
|
+ int portal_id = p_room->_portals[n];
|
|
|
+ Portal *portal = p_portals[portal_id];
|
|
|
+
|
|
|
+ // don't add portals to the hull that are internal to this room!
|
|
|
+ if (portal->is_portal_internal(p_room->_room_ID)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Plane plane = portal->_plane;
|
|
|
+
|
|
|
+ // does it need to be reversed? (i.e. is the portal incoming rather than outgoing)
|
|
|
+ if (portal->_linkedroom_ID[1] == p_room->_room_ID) {
|
|
|
+ plane = -plane;
|
|
|
+ }
|
|
|
+
|
|
|
+ _add_plane_if_unique(p_room, p_room->_preliminary_planes, plane);
|
|
|
+ }
|
|
|
+
|
|
|
+ // add the planes from the geometry or manual bound
|
|
|
+ for (int n = 0; n < md.faces.size(); n++) {
|
|
|
+ const Plane &p = md.faces[n].plane;
|
|
|
+ _add_plane_if_unique(p_room, p_room->_preliminary_planes, p);
|
|
|
+ }
|
|
|
+
|
|
|
+ // temporary copy of mesh data for the boundary points
|
|
|
+ // to form a new hull in _convert_room_hull_final
|
|
|
+ p_room->_bound_mesh_data = md;
|
|
|
+
|
|
|
+ // aabb (should later include portals too, these are added in _convert_room_hull_final)
|
|
|
+ p_room->_aabb.create_from_points(md.vertices);
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_convert_room_hull_final(Room *p_room, const LocalVector<Portal *> &p_portals) {
|
|
|
+ Vector<Vector3> vertices_including_portals = p_room->_bound_mesh_data.vertices;
|
|
|
+
|
|
|
+ // add the portals planes first, as these will trump any other existing planes further out
|
|
|
+ int num_portals_added = 0;
|
|
|
+
|
|
|
+ for (int n = 0; n < p_room->_portals.size(); n++) {
|
|
|
+ int portal_id = p_room->_portals[n];
|
|
|
+ Portal *portal = p_portals[portal_id];
|
|
|
+
|
|
|
+ // don't add portals to the world bound that are internal to this room!
|
|
|
+ if (portal->is_portal_internal(p_room->_room_ID)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Plane plane = portal->_plane;
|
|
|
+
|
|
|
+ // does it need to be reversed? (i.e. is the portal incoming rather than outgoing)
|
|
|
+ if (portal->_linkedroom_ID[1] == p_room->_room_ID) {
|
|
|
+ plane = -plane;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_add_plane_if_unique(p_room, p_room->_planes, plane)) {
|
|
|
+ num_portals_added++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // add any new portals to the aabb of the room
|
|
|
+ for (int p = 0; p < portal->_pts_world.size(); p++) {
|
|
|
+ const Vector3 &pt = portal->_pts_world[p];
|
|
|
+ vertices_including_portals.push_back(pt);
|
|
|
+ p_room->_aabb.expand_to(pt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // create new convex hull
|
|
|
+ Geometry::MeshData md;
|
|
|
+ Error err = _build_room_convex_hull(p_room, vertices_including_portals, md);
|
|
|
+
|
|
|
+ if (err != OK) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // add the planes from the new hull
|
|
|
+ for (int n = 0; n < md.faces.size(); n++) {
|
|
|
+ const Plane &p = md.faces[n].plane;
|
|
|
+ _add_plane_if_unique(p_room, p_room->_planes, p);
|
|
|
+ }
|
|
|
+
|
|
|
+ // recreate the points within the new simplified bound, and then recreate the convex hull
|
|
|
+ // by running quickhull a second time... (this enables the gizmo to accurately show the simplified hull)
|
|
|
+ int num_planes_before_simplification = p_room->_planes.size();
|
|
|
+ Geometry::MeshData md_simplified;
|
|
|
+ _build_simplified_bound(p_room, md_simplified, p_room->_planes, num_portals_added);
|
|
|
+
|
|
|
+ convert_log("\t\t\tcontained " + itos(num_planes_before_simplification) + " planes before simplification, " + itos(p_room->_planes.size()) + " planes after.");
|
|
|
+
|
|
|
+ // make a copy of the mesh data for debugging
|
|
|
+ // note this could be avoided in release builds? NYI
|
|
|
+ p_room->_bound_mesh_data = md_simplified;
|
|
|
+
|
|
|
+ // send bound to visual server
|
|
|
+ VisualServer::get_singleton()->room_set_bound(p_room->_room_rid, p_room->get_instance_id(), p_room->_planes, p_room->_aabb, md_simplified.vertices);
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+#ifdef TOOLS_ENABLED
|
|
|
+bool RoomManager::_room_regenerate_bound(Room *p_room) {
|
|
|
+ // for a preview, we allow the editor to change the bound
|
|
|
+ ERR_FAIL_COND_V(!p_room, false);
|
|
|
+
|
|
|
+ if (!p_room->_bound_pts.size()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // can't do yet if not in the tree
|
|
|
+ if (!p_room->is_inside_tree()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Transform tr = p_room->get_global_transform();
|
|
|
+
|
|
|
+ Vector<Vector3> pts;
|
|
|
+ pts.resize(p_room->_bound_pts.size());
|
|
|
+ for (int n = 0; n < pts.size(); n++) {
|
|
|
+ pts.set(n, tr.xform(p_room->_bound_pts[n]));
|
|
|
+ }
|
|
|
+
|
|
|
+ Geometry::MeshData md;
|
|
|
+ Error err = _build_room_convex_hull(p_room, pts, md);
|
|
|
+
|
|
|
+ if (err != OK) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ p_room->_bound_mesh_data = md;
|
|
|
+ p_room->update_gizmo();
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+#endif
|
|
|
+
|
|
|
+void RoomManager::_build_simplified_bound(const Room *p_room, Geometry::MeshData &r_md, LocalVector<Plane, int32_t> &r_planes, int p_num_portal_planes) {
|
|
|
+ if (!r_planes.size()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Vector<Vector3> pts = Geometry::compute_convex_mesh_points(&r_planes[0], r_planes.size(), 0.001);
|
|
|
+ Error err = _build_room_convex_hull(p_room, pts, r_md);
|
|
|
+
|
|
|
+ if (err != OK) {
|
|
|
+ WARN_PRINT("QuickHull failed building simplified bound");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // if the number of faces is less than the number of planes, we can use this simplified version to reduce the number of planes
|
|
|
+ if (r_md.faces.size() < r_planes.size()) {
|
|
|
+ // always include the portal planes
|
|
|
+ r_planes.resize(p_num_portal_planes);
|
|
|
+
|
|
|
+ for (int n = 0; n < r_md.faces.size(); n++) {
|
|
|
+ _add_plane_if_unique(p_room, r_planes, r_md.faces[n].plane);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+Error RoomManager::_build_room_convex_hull(const Room *p_room, const Vector<Vector3> &p_points, Geometry::MeshData &r_mesh) {
|
|
|
+ // calculate an epsilon based on the simplify value, and use this to build the hull
|
|
|
+ real_t s = 0.0;
|
|
|
+
|
|
|
+ DEV_ASSERT(p_room);
|
|
|
+ if (p_room->_use_default_simplify) {
|
|
|
+ s = _room_simplify_info._plane_simplify;
|
|
|
+ } else {
|
|
|
+ s = p_room->_simplify_info._plane_simplify;
|
|
|
+ }
|
|
|
+
|
|
|
+ // value between 0.3 (accurate) and 10.0 (very rough)
|
|
|
+ // * UNIT_EPSILON
|
|
|
+ s *= s;
|
|
|
+ s *= 40.0;
|
|
|
+ s += 0.3; // minimum
|
|
|
+ s *= UNIT_EPSILON;
|
|
|
+ return _build_convex_hull(p_points, r_mesh, s);
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_add_plane_if_unique(const Room *p_room, LocalVector<Plane, int32_t> &r_planes, const Plane &p) {
|
|
|
+ DEV_ASSERT(p_room);
|
|
|
+ if (p_room->_use_default_simplify) {
|
|
|
+ return _room_simplify_info.add_plane_if_unique(r_planes, p);
|
|
|
+ }
|
|
|
+
|
|
|
+ return p_room->_simplify_info.add_plane_if_unique(r_planes, p);
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_convert_portal(Room *p_room, Spatial *p_node, LocalVector<Portal *> &portals) {
|
|
|
+ String string_full_name = p_node->get_name();
|
|
|
+ convert_log("convert_portal : " + string_full_name);
|
|
|
+
|
|
|
+ Portal *portal = Object::cast_to<Portal>(p_node);
|
|
|
+
|
|
|
+ // if not a gportal already, convert the node type
|
|
|
+ if (!portal) {
|
|
|
+ portal = _change_node_type<Portal>(p_node, "G", false);
|
|
|
+ portal->create_from_mesh_instance(Object::cast_to<MeshInstance>(p_node));
|
|
|
+
|
|
|
+ p_node->queue_delete();
|
|
|
+ } else {
|
|
|
+ // only allow converting once
|
|
|
+ if (portal->_conversion_tick == _conversion_tick) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // mark so as only to convert once
|
|
|
+ portal->_conversion_tick = _conversion_tick;
|
|
|
+
|
|
|
+ // link rooms
|
|
|
+ portal->portal_update();
|
|
|
+
|
|
|
+ // keep a list of portals for second pass
|
|
|
+ portals.push_back(portal);
|
|
|
+
|
|
|
+ // the portal is linking from this first room it is added to
|
|
|
+ portal->_linkedroom_ID[0] = p_room->_room_ID;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_bound_findpoints_geom_instance(GeometryInstance *p_gi, Vector<Vector3> &r_room_pts, AABB &r_aabb) {
|
|
|
+ // max opposite extents .. note AABB storing size is rubbish in this aspect
|
|
|
+ // it can fail once mesh min is larger than FLT_MAX / 2.
|
|
|
+ r_aabb.position = Vector3(FLT_MAX / 2, FLT_MAX / 2, FLT_MAX / 2);
|
|
|
+ r_aabb.size = Vector3(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
|
|
+
|
|
|
+ CSGShape *shape = Object::cast_to<CSGShape>(p_gi);
|
|
|
+ if (shape) {
|
|
|
+ Array arr = shape->get_meshes();
|
|
|
+ if (!arr.size()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Ref<ArrayMesh> arr_mesh = arr[1];
|
|
|
+ if (!arr_mesh.is_valid()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (arr_mesh->get_surface_count() == 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // for converting meshes to world space
|
|
|
+ Transform trans = p_gi->get_global_transform();
|
|
|
+
|
|
|
+ for (int surf = 0; surf < arr_mesh->get_surface_count(); surf++) {
|
|
|
+ Array arrays = arr_mesh->surface_get_arrays(surf);
|
|
|
+
|
|
|
+ if (!arrays.size()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ PoolVector<Vector3> vertices = arrays[VS::ARRAY_VERTEX];
|
|
|
+
|
|
|
+ // convert to world space
|
|
|
+ for (int n = 0; n < vertices.size(); n++) {
|
|
|
+ Vector3 ptWorld = trans.xform(vertices[n]);
|
|
|
+ r_room_pts.push_back(ptWorld);
|
|
|
+
|
|
|
+ // keep the bound up to date
|
|
|
+ r_aabb.expand_to(ptWorld);
|
|
|
+ }
|
|
|
+
|
|
|
+ } // for through the surfaces
|
|
|
+
|
|
|
+ } // if csg shape
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_bound_findpoints_mesh_instance(MeshInstance *p_mi, Vector<Vector3> &r_room_pts, AABB &r_aabb) {
|
|
|
+ // max opposite extents .. note AABB storing size is rubbish in this aspect
|
|
|
+ // it can fail once mesh min is larger than FLT_MAX / 2.
|
|
|
+ r_aabb.position = Vector3(FLT_MAX / 2, FLT_MAX / 2, FLT_MAX / 2);
|
|
|
+ r_aabb.size = Vector3(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
|
|
+
|
|
|
+ // some godot jiggery pokery to get the mesh verts in local space
|
|
|
+ Ref<Mesh> rmesh = p_mi->get_mesh();
|
|
|
+
|
|
|
+ ERR_FAIL_COND_V(!rmesh.is_valid(), false);
|
|
|
+
|
|
|
+ if (rmesh->get_surface_count() == 0) {
|
|
|
+ String string;
|
|
|
+ string = "MeshInstance '" + p_mi->get_name() + "' has no surfaces, ignoring";
|
|
|
+ WARN_PRINT(string);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool success = false;
|
|
|
+
|
|
|
+ // for converting meshes to world space
|
|
|
+ Transform trans = p_mi->get_global_transform();
|
|
|
+
|
|
|
+ for (int surf = 0; surf < rmesh->get_surface_count(); surf++) {
|
|
|
+ Array arrays = rmesh->surface_get_arrays(surf);
|
|
|
+
|
|
|
+ // possible to have a meshinstance with no geometry .. don't want to crash
|
|
|
+ if (!arrays.size()) {
|
|
|
+ WARN_PRINT_ONCE("PConverter::bound_findpoints MeshInstance surface with no mesh, ignoring");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ success = true;
|
|
|
+
|
|
|
+ PoolVector<Vector3> vertices = arrays[VS::ARRAY_VERTEX];
|
|
|
+
|
|
|
+ // convert to world space
|
|
|
+ for (int n = 0; n < vertices.size(); n++) {
|
|
|
+ Vector3 ptWorld = trans.xform(vertices[n]);
|
|
|
+ r_room_pts.push_back(ptWorld);
|
|
|
+
|
|
|
+ // keep the bound up to date
|
|
|
+ r_aabb.expand_to(ptWorld);
|
|
|
+ }
|
|
|
+
|
|
|
+ } // for through the surfaces
|
|
|
+
|
|
|
+ return success;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_cleanup_after_conversion() {
|
|
|
+ for (int n = 0; n < _rooms.size(); n++) {
|
|
|
+ Room *room = _rooms[n];
|
|
|
+ room->_portals.reset();
|
|
|
+ room->_preliminary_planes.reset();
|
|
|
+
|
|
|
+ // outside the editor, there's no need to keep the data for the convex hull
|
|
|
+ // drawing, as it is only used for gizmos.
|
|
|
+ if (!Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ room->_bound_mesh_data = Geometry::MeshData();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::resolve_preview_camera_path() {
|
|
|
+ Camera *camera = _resolve_path<Camera>(_settings_path_preview_camera);
|
|
|
+
|
|
|
+ if (camera) {
|
|
|
+ _godot_preview_camera_ID = camera->get_instance_id();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ _godot_preview_camera_ID = -1;
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+template <class NODE_TYPE>
|
|
|
+NODE_TYPE *RoomManager::_resolve_path(NodePath p_path) const {
|
|
|
+ if (has_node(p_path)) {
|
|
|
+ NODE_TYPE *node = Object::cast_to<NODE_TYPE>(get_node(p_path));
|
|
|
+ if (node) {
|
|
|
+ return node;
|
|
|
+ } else {
|
|
|
+ WARN_PRINT("node is incorrect type");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nullptr;
|
|
|
+}
|
|
|
+
|
|
|
+template <class NODE_TYPE>
|
|
|
+bool RoomManager::_node_is_type(Node *p_node) const {
|
|
|
+ NODE_TYPE *node = Object::cast_to<NODE_TYPE>(p_node);
|
|
|
+ return node != nullptr;
|
|
|
+}
|
|
|
+
|
|
|
+template <class T>
|
|
|
+T *RoomManager::_change_node_type(Spatial *p_node, String p_prefix, bool p_delete) {
|
|
|
+ String string_full_name = p_node->get_name();
|
|
|
+
|
|
|
+ Node *parent = p_node->get_parent();
|
|
|
+ if (!parent) {
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // owner should normally be root
|
|
|
+ Node *owner = p_node->get_owner();
|
|
|
+
|
|
|
+ // change the name of the node to be deleted
|
|
|
+ p_node->set_name(p_prefix + string_full_name);
|
|
|
+
|
|
|
+ // create the new class T object
|
|
|
+ T *pNew = memnew(T);
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // new lroom should have same transform
|
|
|
+ pNew->set_transform(p_node->get_transform());
|
|
|
+
|
|
|
+ // move each child
|
|
|
+ while (p_node->get_child_count()) {
|
|
|
+ Node *child = p_node->get_child(0);
|
|
|
+ p_node->remove_child(child);
|
|
|
+
|
|
|
+ // needs to set owner to appear in IDE
|
|
|
+ pNew->add_child(child);
|
|
|
+ }
|
|
|
+
|
|
|
+ // needs to set owner to appear in IDE
|
|
|
+ _set_owner_recursive(pNew, owner);
|
|
|
+
|
|
|
+ // delete old node
|
|
|
+ if (p_delete) {
|
|
|
+ p_node->queue_delete();
|
|
|
+ }
|
|
|
+
|
|
|
+ return pNew;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_update_gizmos_recursive(Node *p_node) {
|
|
|
+ Portal *portal = Object::cast_to<Portal>(p_node);
|
|
|
+
|
|
|
+ if (portal) {
|
|
|
+ portal->update_gizmo();
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ _update_gizmos_recursive(p_node->get_child(n));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+Error RoomManager::_build_convex_hull(const Vector<Vector3> &p_points, Geometry::MeshData &r_mesh, real_t p_epsilon) {
|
|
|
+#ifdef GODOT_PORTALS_USE_BULLET_CONVEX_HULL
|
|
|
+ return ConvexHullComputer::convex_hull(p_points, r_mesh);
|
|
|
+#if 0
|
|
|
+ // test comparison of methods
|
|
|
+ QuickHull::build(p_points, r_mesh, p_epsilon);
|
|
|
+
|
|
|
+ int qh_faces = r_mesh.faces.size();
|
|
|
+ int qh_verts = r_mesh.vertices.size();
|
|
|
+
|
|
|
+ r_mesh.vertices.clear();
|
|
|
+ r_mesh.faces.clear();
|
|
|
+ r_mesh.edges.clear();
|
|
|
+ Error err = ConvexHullComputer::convex_hull(p_points, r_mesh);
|
|
|
+
|
|
|
+ int bh_faces = r_mesh.faces.size();
|
|
|
+ int bh_verts = r_mesh.vertices.size();
|
|
|
+
|
|
|
+ if (qh_faces != bh_faces) {
|
|
|
+ print_line("qh_faces : " + itos(qh_faces) + ", bh_faces : " + itos(bh_faces));
|
|
|
+ }
|
|
|
+ if (qh_verts != bh_verts) {
|
|
|
+ print_line("qh_verts : " + itos(qh_verts) + ", bh_verts : " + itos(bh_verts));
|
|
|
+ }
|
|
|
+
|
|
|
+ return err;
|
|
|
+#endif
|
|
|
+
|
|
|
+#else
|
|
|
+ QuickHull::_flag_warnings = false;
|
|
|
+ Error err = QuickHull::build(p_points, r_mesh, p_epsilon);
|
|
|
+ QuickHull::_flag_warnings = true;
|
|
|
+ return err;
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_flip_portals_recursive(Spatial *p_node) {
|
|
|
+ Portal *portal = Object::cast_to<Portal>(p_node);
|
|
|
+
|
|
|
+ if (portal) {
|
|
|
+ portal->flip();
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_node->get_child(n));
|
|
|
+ if (child) {
|
|
|
+ _flip_portals_recursive(child);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_set_owner_recursive(Node *p_node, Node *p_owner) {
|
|
|
+ if (p_node != p_owner) {
|
|
|
+ p_node->set_owner(p_owner);
|
|
|
+ }
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ _set_owner_recursive(p_node->get_child(n), p_owner);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_check_for_misnamed_node(const Node *p_node, String p_start_string) {
|
|
|
+ // don't check the roomlist name, as it often has a conflict with Room
|
|
|
+ if (p_node == _roomlist) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String name = p_node->get_name();
|
|
|
+
|
|
|
+ int ss_length = p_start_string.length();
|
|
|
+
|
|
|
+ if (name.substr(0, ss_length).to_lower() == p_start_string.to_lower()) {
|
|
|
+ if (p_start_string == "Room") {
|
|
|
+ // do allow RoomGroup and RoomManager
|
|
|
+ if (name.substr(0, 9) == "RoomGroup") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (name.substr(0, 11) == "RoomManager") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (p_start_string == "RoomGroup") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ WARN_PRINT("Possible misnamed node : " + name);
|
|
|
+ _warning_misnamed_nodes_detected = true;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_name_starts_with(const Node *p_node, String p_search_string, bool p_allow_no_delineator) {
|
|
|
+ String name = p_node->get_name();
|
|
|
+
|
|
|
+ if (p_allow_no_delineator && (name == p_search_string)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ String search_string = p_search_string + GODOT_PORTAL_DELINEATOR;
|
|
|
+ int sl = search_string.length();
|
|
|
+
|
|
|
+ if (name.substr(0, sl) == search_string) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ _check_for_misnamed_node(p_node, p_search_string);
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+String RoomManager::_find_name_after(Node *p_node, String p_string_start) {
|
|
|
+ p_string_start += GODOT_PORTAL_DELINEATOR;
|
|
|
+
|
|
|
+ String string_result;
|
|
|
+ String name = p_node->get_name();
|
|
|
+ string_result = name.substr(p_string_start.length());
|
|
|
+
|
|
|
+ // because godot doesn't support multiple nodes with the same name, we will strip e.g. a number
|
|
|
+ // after an @ on the end of the name...
|
|
|
+ // e.g. portal_kitchen@2
|
|
|
+ for (int c = 0; c < string_result.length(); c++) {
|
|
|
+ if (string_result[c] == '*') {
|
|
|
+ // remove everything after and including this character
|
|
|
+ string_result = string_result.substr(0, c);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return string_result;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_merge_meshes_in_room(Room *p_room) {
|
|
|
+ // only do in running game so as not to lose data
|
|
|
+ if (Engine::get_singleton()->is_editor_hint()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _merge_log("merging room " + p_room->get_name());
|
|
|
+
|
|
|
+ // list of meshes suitable
|
|
|
+ LocalVector<MeshInstance *, int32_t> source_meshes;
|
|
|
+ _list_mergeable_mesh_instances(p_room, source_meshes);
|
|
|
+
|
|
|
+ // none suitable
|
|
|
+ if (!source_meshes.size()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _merge_log("\t" + itos(source_meshes.size()) + " source meshes");
|
|
|
+
|
|
|
+ BitFieldDynamic bf;
|
|
|
+ bf.create(source_meshes.size(), true);
|
|
|
+
|
|
|
+ for (int n = 0; n < source_meshes.size(); n++) {
|
|
|
+ LocalVector<MeshInstance *, int32_t> merge_list;
|
|
|
+
|
|
|
+ // find similar meshes
|
|
|
+ MeshInstance *a = source_meshes[n];
|
|
|
+ merge_list.push_back(a);
|
|
|
+
|
|
|
+ // may not be necessary
|
|
|
+ bf.set_bit(n, true);
|
|
|
+
|
|
|
+ for (int c = n + 1; c < source_meshes.size(); c++) {
|
|
|
+ // if not merged already
|
|
|
+ if (!bf.get_bit(c)) {
|
|
|
+ MeshInstance *b = source_meshes[c];
|
|
|
+
|
|
|
+ // if (_are_meshes_mergeable(a, b)) {
|
|
|
+ if (a->is_mergeable_with(*b)) {
|
|
|
+ merge_list.push_back(b);
|
|
|
+ bf.set_bit(c, true);
|
|
|
+ }
|
|
|
+ } // if not merged already
|
|
|
+ } // for c through secondary mesh
|
|
|
+
|
|
|
+ // only merge if more than 1
|
|
|
+ if (merge_list.size() > 1) {
|
|
|
+ // we can merge!
|
|
|
+ // create a new holder mesh
|
|
|
+
|
|
|
+ MeshInstance *merged = memnew(MeshInstance);
|
|
|
+ merged->set_name("MergedMesh");
|
|
|
+
|
|
|
+ _merge_log("\t\t" + merged->get_name());
|
|
|
+
|
|
|
+ if (merged->create_by_merging(merge_list)) {
|
|
|
+ // 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ // and set the new merged mesh to static
|
|
|
+ merged->set_portal_mode(CullInstance::PORTAL_MODE_STATIC);
|
|
|
+
|
|
|
+ // attach to scene tree
|
|
|
+ p_room->add_child(merged);
|
|
|
+ merged->set_owner(p_room->get_owner());
|
|
|
+
|
|
|
+ // compensate for room transform, as the verts are now in world space
|
|
|
+ Transform tr = p_room->get_global_transform();
|
|
|
+ tr.affine_invert();
|
|
|
+ merged->set_transform(tr);
|
|
|
+
|
|
|
+ // delete originals?
|
|
|
+ // note this isn't perfect, it may still end up with dangling spatials, but they can be
|
|
|
+ // deleted later.
|
|
|
+ for (int i = 0; i < merge_list.size(); i++) {
|
|
|
+ MeshInstance *mi = merge_list[i];
|
|
|
+ if (!mi->get_child_count()) {
|
|
|
+ mi->queue_delete();
|
|
|
+ } else {
|
|
|
+ Node *parent = mi->get_parent();
|
|
|
+ if (parent) {
|
|
|
+ // if there are children, we don't want to delete it, but we do want to
|
|
|
+ // remove the mesh drawing, e.g. by replacing it with a spatial
|
|
|
+ String name = mi->get_name();
|
|
|
+ mi->set_name("DeleteMe"); // can be anything, just to avoid name conflict with replacement node
|
|
|
+ Spatial *replacement = memnew(Spatial);
|
|
|
+ replacement->set_name(name);
|
|
|
+
|
|
|
+ parent->add_child(replacement);
|
|
|
+
|
|
|
+ // make the transform and owner match
|
|
|
+ replacement->set_owner(mi->get_owner());
|
|
|
+ replacement->set_transform(mi->get_transform());
|
|
|
+
|
|
|
+ // move all children from the mesh instance to the replacement
|
|
|
+ while (mi->get_child_count()) {
|
|
|
+ Node *child = mi->get_child(0);
|
|
|
+ mi->remove_child(child);
|
|
|
+ replacement->add_child(child);
|
|
|
+ }
|
|
|
+
|
|
|
+ } // if the mesh instance has a parent (should hopefully be always the case?)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // no success
|
|
|
+ memdelete(merged);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } // for n through primary mesh
|
|
|
+
|
|
|
+ if (_settings_remove_danglers) {
|
|
|
+ _remove_redundant_dangling_nodes(p_room);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool RoomManager::_remove_redundant_dangling_nodes(Spatial *p_node) {
|
|
|
+ int non_queue_delete_children = 0;
|
|
|
+
|
|
|
+ // do the children first
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Node *node_child = p_node->get_child(n);
|
|
|
+
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(node_child);
|
|
|
+ if (child) {
|
|
|
+ _remove_redundant_dangling_nodes(child);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node_child && !node_child->is_queued_for_deletion()) {
|
|
|
+ non_queue_delete_children++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!non_queue_delete_children) {
|
|
|
+ // only remove true spatials, not derived classes
|
|
|
+ if (p_node->get_class_name() == "Spatial") {
|
|
|
+ p_node->queue_delete();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+void RoomManager::_list_mergeable_mesh_instances(Spatial *p_node, LocalVector<MeshInstance *, int32_t> &r_list) {
|
|
|
+ MeshInstance *mi = Object::cast_to<MeshInstance>(p_node);
|
|
|
+
|
|
|
+ if (mi) {
|
|
|
+ // only interested in static portal mode meshes
|
|
|
+ VisualInstance *vi = Object::cast_to<VisualInstance>(mi);
|
|
|
+
|
|
|
+ // we are only interested in VIs with static or dynamic mode
|
|
|
+ if (vi && vi->get_portal_mode() == CullInstance::PORTAL_MODE_STATIC) {
|
|
|
+ // disallow for portals or bounds
|
|
|
+ // mesh instance portals should be queued for deletion by this point, we don't want to merge portals!
|
|
|
+ if (!_node_is_type<Portal>(mi) && !_name_starts_with(mi, "Bound", true) && !mi->is_queued_for_deletion()) {
|
|
|
+ // only merge if visible
|
|
|
+ if (mi->is_inside_tree() && mi->is_visible()) {
|
|
|
+ r_list.push_back(mi);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int n = 0; n < p_node->get_child_count(); n++) {
|
|
|
+ Spatial *child = Object::cast_to<Spatial>(p_node->get_child(n));
|
|
|
+ if (child) {
|
|
|
+ _list_mergeable_mesh_instances(child, r_list);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|