From eb6f98ec559b3df1d1346d88ae4f94323db2d383 Mon Sep 17 00:00:00 2001 From: lawnjelly Date: Thu, 4 Feb 2021 10:43:08 +0000 Subject: [PATCH] Portal occlusion culling Adds support for occlusion culling via rooms and portals. --- core/bitfield_dynamic.cpp | 79 + core/bitfield_dynamic.h | 111 + core/engine.cpp | 5 + core/engine.h | 3 + core/error_macros.h | 18 + core/math/aabb.cpp | 38 + core/math/aabb.h | 2 + core/math/face3.h | 7 + core/math/geometry.cpp | 188 +- core/math/geometry.h | 5 +- core/math/quick_hull.cpp | 68 +- core/math/quick_hull.h | 13 +- core/math/vector3.h | 5 + core/object.h | 1 + core/pooled_list.h | 71 + doc/classes/CullInstance.xml | 45 + doc/classes/Portal.xml | 41 + doc/classes/Room.xml | 31 + doc/classes/RoomGroup.xml | 24 + doc/classes/RoomManager.xml | 123 ++ doc/classes/Spatial.xml | 16 + doc/classes/VisibilityNotifier.xml | 2 +- doc/classes/VisualInstance.xml | 2 +- editor/editor_node.cpp | 3 + editor/icons/icon_gizmo_portal.svg | 1 + editor/icons/icon_room_group.svg | 1 + editor/icons/icon_room_manager.svg | 1 + editor/plugins/room_manager_editor_plugin.cpp | 172 ++ editor/plugins/room_manager_editor_plugin.h | 92 + editor/plugins/spatial_editor_plugin.cpp | 14 + editor/plugins/spatial_editor_plugin.h | 1 + editor/spatial_editor_gizmos.cpp | 277 +++ editor/spatial_editor_gizmos.h | 26 + main/main.cpp | 12 + scene/3d/cull_instance.cpp | 66 + scene/3d/cull_instance.h | 66 + scene/3d/mesh_instance.cpp | 322 +++ scene/3d/mesh_instance.h | 12 + scene/3d/portal.cpp | 639 ++++++ scene/3d/portal.h | 174 ++ scene/3d/room.cpp | 226 ++ scene/3d/room.h | 131 ++ scene/3d/room_group.cpp | 84 + scene/3d/room_group.h | 79 + scene/3d/room_manager.cpp | 1921 +++++++++++++++++ scene/3d/room_manager.h | 273 +++ scene/3d/spatial.cpp | 24 + scene/3d/spatial.h | 3 + scene/3d/visibility_notifier.cpp | 102 +- scene/3d/visibility_notifier.h | 12 +- scene/3d/visual_instance.cpp | 5 +- scene/3d/visual_instance.h | 8 +- scene/register_scene_types.cpp | 9 + servers/visual/SCsub | 2 + servers/visual/portals/SCsub | 5 + .../portals/portal_gameplay_monitor.cpp | 381 ++++ .../visual/portals/portal_gameplay_monitor.h | 87 + servers/visual/portals/portal_pvs.cpp | 37 + servers/visual/portals/portal_pvs.h | 59 + servers/visual/portals/portal_pvs_builder.cpp | 453 ++++ servers/visual/portals/portal_pvs_builder.h | 71 + servers/visual/portals/portal_renderer.cpp | 989 +++++++++ servers/visual/portals/portal_renderer.h | 292 +++ servers/visual/portals/portal_rooms_bsp.cpp | 641 ++++++ servers/visual/portals/portal_rooms_bsp.h | 106 + servers/visual/portals/portal_tracer.cpp | 475 ++++ servers/visual/portals/portal_tracer.h | 165 ++ servers/visual/portals/portal_types.cpp | 234 ++ servers/visual/portals/portal_types.h | 354 +++ servers/visual/visual_server_raster.h | 38 + servers/visual/visual_server_scene.cpp | 486 ++++- servers/visual/visual_server_scene.h | 149 +- servers/visual/visual_server_wrap_mt.cpp | 4 + servers/visual/visual_server_wrap_mt.h | 38 + servers/visual_server.cpp | 5 + servers/visual_server.h | 54 + servers/visual_server_callbacks.cpp | 65 + servers/visual_server_callbacks.h | 66 + 78 files changed, 10865 insertions(+), 45 deletions(-) create mode 100644 core/bitfield_dynamic.cpp create mode 100644 core/bitfield_dynamic.h create mode 100644 doc/classes/CullInstance.xml create mode 100644 doc/classes/Portal.xml create mode 100644 doc/classes/Room.xml create mode 100644 doc/classes/RoomGroup.xml create mode 100644 doc/classes/RoomManager.xml create mode 100644 editor/icons/icon_gizmo_portal.svg create mode 100644 editor/icons/icon_room_group.svg create mode 100644 editor/icons/icon_room_manager.svg create mode 100644 editor/plugins/room_manager_editor_plugin.cpp create mode 100644 editor/plugins/room_manager_editor_plugin.h create mode 100644 scene/3d/cull_instance.cpp create mode 100644 scene/3d/cull_instance.h create mode 100644 scene/3d/portal.cpp create mode 100644 scene/3d/portal.h create mode 100644 scene/3d/room.cpp create mode 100644 scene/3d/room.h create mode 100644 scene/3d/room_group.cpp create mode 100644 scene/3d/room_group.h create mode 100644 scene/3d/room_manager.cpp create mode 100644 scene/3d/room_manager.h create mode 100644 servers/visual/portals/SCsub create mode 100644 servers/visual/portals/portal_gameplay_monitor.cpp create mode 100644 servers/visual/portals/portal_gameplay_monitor.h create mode 100644 servers/visual/portals/portal_pvs.cpp create mode 100644 servers/visual/portals/portal_pvs.h create mode 100644 servers/visual/portals/portal_pvs_builder.cpp create mode 100644 servers/visual/portals/portal_pvs_builder.h create mode 100644 servers/visual/portals/portal_renderer.cpp create mode 100644 servers/visual/portals/portal_renderer.h create mode 100644 servers/visual/portals/portal_rooms_bsp.cpp create mode 100644 servers/visual/portals/portal_rooms_bsp.h create mode 100644 servers/visual/portals/portal_tracer.cpp create mode 100644 servers/visual/portals/portal_tracer.h create mode 100644 servers/visual/portals/portal_types.cpp create mode 100644 servers/visual/portals/portal_types.h create mode 100644 servers/visual_server_callbacks.cpp create mode 100644 servers/visual_server_callbacks.h diff --git a/core/bitfield_dynamic.cpp b/core/bitfield_dynamic.cpp new file mode 100644 index 00000000000..1177c066d39 --- /dev/null +++ b/core/bitfield_dynamic.cpp @@ -0,0 +1,79 @@ +/*************************************************************************/ +/* bitfield_dynamic.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 "bitfield_dynamic.h" + +#include "core/os/memory.h" + +#include + +void BitFieldDynamic::copy_from(const BitFieldDynamic &p_source) { + create(p_source.get_num_bits(), false); + memcpy(_data, p_source.get_data(), p_source.get_num_bytes()); +} + +void BitFieldDynamic::create(uint32_t p_num_bits, bool p_blank) { + // first delete any initial + destroy(); + + _num_bits = p_num_bits; + if (p_num_bits) { + _num_bytes = (p_num_bits / 8) + 1; + _data = (uint8_t *)memalloc(_num_bytes); + + if (p_blank) { + blank(false); + } + } +} + +void BitFieldDynamic::destroy() { + if (_data) { + memfree(_data); + _data = nullptr; + } + + _num_bytes = 0; + _num_bits = 0; +} + +void BitFieldDynamic::blank(bool p_set_or_zero) { + if (p_set_or_zero) { + memset(_data, 255, _num_bytes); + } else { + memset(_data, 0, _num_bytes); + } +} + +void BitFieldDynamic::invert() { + for (uint32_t n = 0; n < _num_bytes; n++) { + _data[n] = ~_data[n]; + } +} diff --git a/core/bitfield_dynamic.h b/core/bitfield_dynamic.h new file mode 100644 index 00000000000..ffb8a609bcd --- /dev/null +++ b/core/bitfield_dynamic.h @@ -0,0 +1,111 @@ +/*************************************************************************/ +/* bitfield_dynamic.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef BITFIELD_DYNAMIC_H +#define BITFIELD_DYNAMIC_H + +#include "core/error_macros.h" + +class BitFieldDynamic { +public: + ~BitFieldDynamic() { destroy(); } + +private: + // prevent copying (see effective C++ scott meyers) + // there is no implementation for copy constructor, hence compiler will complain if you try to copy + // feel free to add one if needed... + BitFieldDynamic &operator=(const BitFieldDynamic &); + +public: + // create automatically blanks + void create(uint32_t p_num_bits, bool p_blank = true); + void destroy(); + + // public funcs + uint32_t get_num_bits() const { return _num_bits; } + uint32_t get_bit(uint32_t p_bit) const; + void set_bit(uint32_t p_bit, uint32_t p_set); + bool check_and_set(uint32_t p_bit); + void blank(bool p_set_or_zero = false); + void invert(); + void copy_from(const BitFieldDynamic &p_source); + + // loading / saving + uint8_t *get_data() { return _data; } + const uint8_t *get_data() const { return _data; } + uint32_t get_num_bytes() const { return _num_bytes; } + +protected: + // member vars + uint8_t *_data = nullptr; + uint32_t _num_bytes = 0; + uint32_t _num_bits = 0; +}; + +inline uint32_t BitFieldDynamic::get_bit(uint32_t p_bit) const { + DEV_ASSERT(_data); + uint32_t byte_number = p_bit >> 3; // divide by 8 + DEV_ASSERT(byte_number < _num_bytes); + uint8_t uc = _data[byte_number]; + uint32_t bit_set = uc & (1 << (p_bit & 7)); + return bit_set; +} + +inline bool BitFieldDynamic::check_and_set(uint32_t p_bit) { + DEV_ASSERT(_data); + uint32_t byte_number = p_bit >> 3; // divide by 8 + DEV_ASSERT(byte_number < _num_bytes); + uint8_t &uc = _data[byte_number]; + uint32_t mask = (1 << (p_bit & 7)); + uint32_t bit_set = uc & mask; + if (bit_set) { + return false; + } + + // set + uc = uc | mask; + return true; +} + +inline void BitFieldDynamic::set_bit(uint32_t p_bit, uint32_t p_set) { + DEV_ASSERT(_data); + uint32_t byte_number = p_bit >> 3; // divide by 8 + DEV_ASSERT(byte_number < _num_bytes); + uint8_t uc = _data[byte_number]; + uint32_t mask = 1 << (p_bit & 7); + if (p_set) { + uc = uc | mask; + } else { + uc &= ~mask; + } + _data[byte_number] = uc; +} + +#endif diff --git a/core/engine.cpp b/core/engine.cpp index 56755d31979..e7c28a4c7bf 100644 --- a/core/engine.cpp +++ b/core/engine.cpp @@ -83,6 +83,10 @@ float Engine::get_time_scale() const { return _time_scale; } +void Engine::set_portals_active(bool p_active) { + _portals_active = p_active; +} + Dictionary Engine::get_version_info() const { Dictionary dict; dict["major"] = VERSION_MAJOR; @@ -224,6 +228,7 @@ Engine::Engine() { _frame_ticks = 0; _frame_step = 0; editor_hint = false; + _portals_active = false; } Engine::Singleton::Singleton(const StringName &p_name, Object *p_ptr) : diff --git a/core/engine.h b/core/engine.h index ef6a86e85de..e8454405421 100644 --- a/core/engine.h +++ b/core/engine.h @@ -60,6 +60,7 @@ private: bool _gpu_pixel_snap; uint64_t _physics_frames; float _physics_interpolation_fraction; + bool _portals_active; uint64_t _idle_frames; bool _in_physics; @@ -106,6 +107,8 @@ public: Object *get_singleton_object(const String &p_name) const; _FORCE_INLINE_ bool get_use_gpu_pixel_snap() const { return _gpu_pixel_snap; } + bool are_portals_active() const { return _portals_active; } + void set_portals_active(bool p_active); #ifdef TOOLS_ENABLED _FORCE_INLINE_ void set_editor_hint(bool p_enabled) { editor_hint = p_enabled; } diff --git a/core/error_macros.h b/core/error_macros.h index d9710c07594..afd8feb0819 100644 --- a/core/error_macros.h +++ b/core/error_macros.h @@ -320,6 +320,24 @@ void _err_print_index_error(const char *p_function, const char *p_file, int p_li } \ } +/** + * Should assert only if making a build with dev asserts. + * This should be a 'free' check for program flow and should not be needed in any releases, + * only used in dev builds. + */ +// #define DEV_ASSERTS_ENABLED +#ifdef DEV_ASSERTS_ENABLED +#define DEV_ASSERT(m_cond) \ + { \ + if (unlikely(!(m_cond))) { \ + _err_print_error(FUNCTION_STR, __FILE__, __LINE__, "FATAL: DEV_ASSERT failed \"" _STR(m_cond) "\" is false."); \ + GENERATE_TRAP \ + } \ + } +#else +#define DEV_ASSERT(m_cond) +#endif + /** * If `m_cond` evaluates to `true`, crashes the engine immediately with a generic error message. * Only use this if there's no sensible fallback (i.e. the error is unrecoverable). diff --git a/core/math/aabb.cpp b/core/math/aabb.cpp index fcb14e62d00..9b00a910dc9 100644 --- a/core/math/aabb.cpp +++ b/core/math/aabb.cpp @@ -43,6 +43,44 @@ bool AABB::operator!=(const AABB &p_rval) const { return ((position != p_rval.position) || (size != p_rval.size)); } +bool AABB::create_from_points(const Vector &p_points) { + if (!p_points.size()) { + return false; + } + + Vector3 begin = p_points[0]; + Vector3 end = begin; + + for (int n = 1; n < p_points.size(); n++) { + const Vector3 &pt = p_points[n]; + + if (pt.x < begin.x) { + begin.x = pt.x; + } + if (pt.y < begin.y) { + begin.y = pt.y; + } + if (pt.z < begin.z) { + begin.z = pt.z; + } + + if (pt.x > end.x) { + end.x = pt.x; + } + if (pt.y > end.y) { + end.y = pt.y; + } + if (pt.z > end.z) { + end.z = pt.z; + } + } + + position = begin; + size = end - begin; + + return true; +} + void AABB::merge_with(const AABB &p_aabb) { Vector3 beg_1, beg_2; Vector3 end_1, end_2; diff --git a/core/math/aabb.h b/core/math/aabb.h index c896732c19a..1e7933bef2f 100644 --- a/core/math/aabb.h +++ b/core/math/aabb.h @@ -58,6 +58,7 @@ public: void set_position(const Vector3 &p_pos) { position = p_pos; } const Vector3 &get_size() const { return size; } void set_size(const Vector3 &p_size) { size = p_size; } + Vector3 get_center() const { return position + (size * 0.5); } bool operator==(const AABB &p_rval) const; bool operator!=(const AABB &p_rval) const; @@ -98,6 +99,7 @@ public: AABB expand(const Vector3 &p_vector) const; _FORCE_INLINE_ void project_range_in_plane(const Plane &p_plane, real_t &r_min, real_t &r_max) const; _FORCE_INLINE_ void expand_to(const Vector3 &p_vector); /** expand to contain a point if necessary */ + bool create_from_points(const Vector &p_points); _FORCE_INLINE_ AABB abs() const { return AABB(Vector3(position.x + MIN(size.x, 0), position.y + MIN(size.y, 0), position.z + MIN(size.z, 0)), size.abs()); diff --git a/core/math/face3.h b/core/math/face3.h index 2e86b0a9043..20c7980aa87 100644 --- a/core/math/face3.h +++ b/core/math/face3.h @@ -65,6 +65,7 @@ public: bool is_degenerate() const; real_t get_area() const; + real_t get_twice_area_squared() const; Vector3 get_median_point() const; Vector3 get_closest_point_to(const Vector3 &p_point) const; @@ -96,6 +97,12 @@ public: } }; +inline real_t Face3::get_twice_area_squared() const { + Vector3 edge1 = vertex[1] - vertex[0]; + Vector3 edge2 = vertex[2] - vertex[0]; + return edge1.cross(edge2).length_squared(); +} + bool Face3::intersects_aabb2(const AABB &p_aabb) const { Vector3 perp = (vertex[0] - vertex[2]).cross(vertex[0] - vertex[1]); diff --git a/core/math/geometry.cpp b/core/math/geometry.cpp index f7c7dc8b219..6d5de7775c8 100644 --- a/core/math/geometry.cpp +++ b/core/math/geometry.cpp @@ -1175,7 +1175,127 @@ Vector> Geometry::_polypath_offset(const Vector &p_polypa return polypaths; } -Vector Geometry::compute_convex_mesh_points(const Plane *p_planes, int p_plane_count) { +real_t Geometry::calculate_convex_hull_volume(const Geometry::MeshData &p_md) { + if (!p_md.vertices.size()) { + return 0.0; + } + + // find center + Vector3 center; + for (int n = 0; n < p_md.vertices.size(); n++) { + center += p_md.vertices[n]; + } + center /= p_md.vertices.size(); + + Face3 fa; + + real_t volume = 0.0; + + // volume of each cone is 1/3 * height * area of face + for (int f = 0; f < p_md.faces.size(); f++) { + const Geometry::MeshData::Face &face = p_md.faces[f]; + + real_t height = 0.0; + real_t face_area = 0.0; + + for (int c = 0; c < face.indices.size() - 2; c++) { + fa.vertex[0] = p_md.vertices[face.indices[0]]; + fa.vertex[1] = p_md.vertices[face.indices[c + 1]]; + fa.vertex[2] = p_md.vertices[face.indices[c + 2]]; + + if (!c) { + // calculate height + Plane plane(fa.vertex[0], fa.vertex[1], fa.vertex[2]); + height = -plane.distance_to(center); + } + + face_area += Math::sqrt(fa.get_twice_area_squared()); + } + volume += face_area * height; + } + + volume *= (1.0 / 3.0) * 0.5; + return volume; +} + +// note this function is slow, because it builds meshes etc. Not ideal to use in realtime. +// Planes must face OUTWARD from the center of the convex hull, by convention. +bool Geometry::convex_hull_intersects_convex_hull(const Plane *p_planes_a, int p_plane_count_a, const Plane *p_planes_b, int p_plane_count_b) { + if (!p_plane_count_a || !p_plane_count_b) { + return false; + } + + // OR alternative approach, we can call compute_convex_mesh_points() + // with both sets of planes, to get an intersection. Not sure which method is + // faster... this may be faster with more complex hulls. + + // the usual silliness to get from one vector format to another... + PoolVector planes_a; + PoolVector planes_b; + + { + planes_a.resize(p_plane_count_a); + PoolVector::Write w = planes_a.write(); + memcpy(w.ptr(), p_planes_a, p_plane_count_a * sizeof(Plane)); + } + { + planes_b.resize(p_plane_count_b); + PoolVector::Write w = planes_b.write(); + memcpy(w.ptr(), p_planes_b, p_plane_count_b * sizeof(Plane)); + } + + Geometry::MeshData md_A = build_convex_mesh(planes_a); + Geometry::MeshData md_B = build_convex_mesh(planes_b); + + // hull can't be built + if (!md_A.vertices.size() || !md_B.vertices.size()) { + return false; + } + + // first check the points against the planes + for (int p = 0; p < p_plane_count_a; p++) { + const Plane &plane = p_planes_a[p]; + + for (int n = 0; n < md_B.vertices.size(); n++) { + if (!plane.is_point_over(md_B.vertices[n])) { + return true; + } + } + } + + for (int p = 0; p < p_plane_count_b; p++) { + const Plane &plane = p_planes_b[p]; + + for (int n = 0; n < md_A.vertices.size(); n++) { + if (!plane.is_point_over(md_A.vertices[n])) { + return true; + } + } + } + + // now check edges + for (int n = 0; n < md_A.edges.size(); n++) { + const Vector3 &pt_a = md_A.vertices[md_A.edges[n].a]; + const Vector3 &pt_b = md_A.vertices[md_A.edges[n].b]; + + if (segment_intersects_convex(pt_a, pt_b, p_planes_b, p_plane_count_b, nullptr, nullptr)) { + return true; + } + } + + for (int n = 0; n < md_B.edges.size(); n++) { + const Vector3 &pt_a = md_B.vertices[md_B.edges[n].a]; + const Vector3 &pt_b = md_B.vertices[md_B.edges[n].b]; + + if (segment_intersects_convex(pt_a, pt_b, p_planes_a, p_plane_count_a, nullptr, nullptr)) { + return true; + } + } + + return false; +} + +Vector Geometry::compute_convex_mesh_points(const Plane *p_planes, int p_plane_count, real_t p_epsilon) { Vector points; // Iterate through every unique combination of any three planes. @@ -1191,8 +1311,8 @@ Vector Geometry::compute_convex_mesh_points(const Plane *p_planes, int bool excluded = false; for (int n = 0; n < p_plane_count; n++) { if (n != i && n != j && n != k) { - real_t dp = p_planes[n].normal.dot(convex_shape_point); - if (dp - p_planes[n].d > CMP_EPSILON) { + real_t dist = p_planes[n].distance_to(convex_shape_point); + if (dist > p_epsilon) { excluded = true; break; } @@ -1242,3 +1362,65 @@ Vector Geometry::partial_pack_rects(const Vector &r_verts, bool p_clockwise) { + // sort winding order of a (primarily convex) polygon. + // It can handle some concave polygons, but not + // where a vertex 'goes back on' a previous vertex .. + // i.e. it will change the shape in some concave cases. + struct ElementComparator { + Vector2 center; + bool operator()(const Vector2 &a, const Vector2 &b) const { + if (a.x - center.x >= 0 && b.x - center.x < 0) { + return true; + } + if (a.x - center.x < 0 && b.x - center.x >= 0) { + return false; + } + if (a.x - center.x == 0 && b.x - center.x == 0) { + if (a.y - center.y >= 0 || b.y - center.y >= 0) { + return a.y > b.y; + } + return b.y > a.y; + } + + // compute the cross product of vectors (center -> a) x (center -> b) + real_t det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y); + if (det < 0.0) { + return true; + } + if (det > 0.0) { + return false; + } + + // points a and b are on the same line from the center + // check which point is closer to the center + real_t d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y); + real_t d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y); + return d1 > d2; + } + }; + + int npoints = r_verts.size(); + if (!npoints) { + return; + } + + // first calculate center + Vector2 center; + for (int n = 0; n < npoints; n++) { + center += r_verts[n]; + } + center /= npoints; + + SortArray sorter; + sorter.compare.center = center; + sorter.sort(r_verts.ptrw(), r_verts.size()); + + // if not clockwise, reverse order + if (!p_clockwise) { + r_verts.invert(); + } +} diff --git a/core/math/geometry.h b/core/math/geometry.h index 74c4f970663..511a0db1e8a 100644 --- a/core/math/geometry.h +++ b/core/math/geometry.h @@ -1071,6 +1071,7 @@ public: static PoolVector build_box_planes(const Vector3 &p_extents); static PoolVector build_cylinder_planes(real_t p_radius, real_t p_height, int p_sides, Vector3::Axis p_axis = Vector3::AXIS_Z); static PoolVector build_capsule_planes(real_t p_radius, real_t p_height, int p_sides, int p_lats, Vector3::Axis p_axis = Vector3::AXIS_Z); + static void sort_polygon_winding(Vector &r_verts, bool p_clockwise = true); static void make_atlas(const Vector &p_rects, Vector &r_result, Size2i &r_size); @@ -1081,7 +1082,9 @@ public: }; static Vector partial_pack_rects(const Vector &p_sizes, const Size2i &p_atlas_size); - static Vector compute_convex_mesh_points(const Plane *p_planes, int p_plane_count); + static Vector compute_convex_mesh_points(const Plane *p_planes, int p_plane_count, real_t p_epsilon = CMP_EPSILON); + static bool convex_hull_intersects_convex_hull(const Plane *p_planes_a, int p_plane_count_a, const Plane *p_planes_b, int p_plane_count_b); + static real_t calculate_convex_hull_volume(const Geometry::MeshData &p_md); private: static Vector> _polypaths_do_operation(PolyBooleanOperation p_op, const Vector &p_polypath_a, const Vector &p_polypath_b, bool is_a_open = false); diff --git a/core/math/quick_hull.cpp b/core/math/quick_hull.cpp index 879e4d36c5f..42a5ffbc410 100644 --- a/core/math/quick_hull.cpp +++ b/core/math/quick_hull.cpp @@ -33,18 +33,13 @@ #include "core/map.h" uint32_t QuickHull::debug_stop_after = 0xFFFFFFFF; +bool QuickHull::_flag_warnings = true; -Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_mesh) { +Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_mesh, real_t p_over_tolerance_epsilon) { /* CREATE AABB VOLUME */ AABB aabb; - for (int i = 0; i < p_points.size(); i++) { - if (i == 0) { - aabb.position = p_points[i]; - } else { - aabb.expand_to(p_points[i]); - } - } + aabb.create_from_points(p_points); if (aabb.size == Vector3()) { return ERR_CANT_CREATE; @@ -171,7 +166,7 @@ Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_me faces.push_back(f); } - real_t over_tolerance = 3 * UNIT_EPSILON * (aabb.size.x + aabb.size.y + aabb.size.z); + real_t over_tolerance = p_over_tolerance_epsilon * (aabb.size.x + aabb.size.y + aabb.size.z); /* COMPUTE AVAILABLE VERTICES */ @@ -366,6 +361,10 @@ Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_me //fill faces + bool warning_f = false; + bool warning_o_equal_e = false; + bool warning_o = false; + for (List::Element *E = ret_faces.front(); E; E = E->next()) { Geometry::MeshData::Face &f = E->get(); @@ -376,10 +375,20 @@ Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_me Map::Element *F = ret_edges.find(e); - ERR_CONTINUE(!F); + if (unlikely(!F)) { + warning_f = true; + continue; + } + List::Element *O = F->get().left == E ? F->get().right : F->get().left; - ERR_CONTINUE(O == E); - ERR_CONTINUE(O == nullptr); + if (unlikely(O == E)) { + warning_o_equal_e = true; + continue; + } + if (unlikely(!O)) { + warning_o = true; + continue; + } if (O->get().plane.is_equal_approx(f.plane)) { //merge and delete edge and contiguous face, while repointing edges (uuugh!) @@ -434,6 +443,18 @@ Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_me } } + if (_flag_warnings) { + if (warning_f) { + WARN_PRINT("QuickHull : !F"); + } + if (warning_o_equal_e) { + WARN_PRINT("QuickHull : O == E"); + } + if (warning_o) { + WARN_PRINT("QuickHull : O == nullptr"); + } + } + //fill mesh r_mesh.faces.clear(); r_mesh.faces.resize(ret_faces.size()); @@ -451,7 +472,28 @@ Error QuickHull::build(const Vector &p_points, Geometry::MeshData &r_me r_mesh.edges.write[idx++] = e; } - r_mesh.vertices = p_points; + // we are only interested in outputting the points that are used + Vector out_indices; + + for (int n = 0; n < r_mesh.faces.size(); n++) { + Geometry::MeshData::Face face = r_mesh.faces[n]; + for (int i = 0; i < face.indices.size(); i++) { + face.indices.set(i, find_or_create_output_index(face.indices[i], out_indices)); + } + r_mesh.faces.set(n, face); + } + for (int n = 0; n < r_mesh.edges.size(); n++) { + Geometry::MeshData::Edge e = r_mesh.edges[n]; + e.a = find_or_create_output_index(e.a, out_indices); + e.b = find_or_create_output_index(e.b, out_indices); + r_mesh.edges.set(n, e); + } + + // rejig the final vertices + r_mesh.vertices.resize(out_indices.size()); + for (int n = 0; n < out_indices.size(); n++) { + r_mesh.vertices.set(n, p_points[out_indices[n]]); + } return OK; } diff --git a/core/math/quick_hull.h b/core/math/quick_hull.h index ed63b53ace9..72503b33e8e 100644 --- a/core/math/quick_hull.h +++ b/core/math/quick_hull.h @@ -84,9 +84,20 @@ private: } }; + static int find_or_create_output_index(int p_old_index, Vector &r_out_indices) { + for (int n = 0; n < r_out_indices.size(); n++) { + if (r_out_indices[n] == p_old_index) { + return n; + } + } + r_out_indices.push_back(p_old_index); + return r_out_indices.size() - 1; + } + public: static uint32_t debug_stop_after; - static Error build(const Vector &p_points, Geometry::MeshData &r_mesh); + static bool _flag_warnings; + static Error build(const Vector &p_points, Geometry::MeshData &r_mesh, real_t p_over_tolerance_epsilon = 3.0 * UNIT_EPSILON); }; #endif // QUICK_HULL_H diff --git a/core/math/vector3.h b/core/math/vector3.h index 1157addcf8c..43e87062b66 100644 --- a/core/math/vector3.h +++ b/core/math/vector3.h @@ -129,6 +129,7 @@ struct Vector3 { _FORCE_INLINE_ Vector3 reflect(const Vector3 &p_normal) const; bool is_equal_approx(const Vector3 &p_v) const; + inline bool is_equal_approx(const Vector3 &p_v, real_t p_tolerance) const; /* Operators */ @@ -451,4 +452,8 @@ Vector3 Vector3::reflect(const Vector3 &p_normal) const { return 2.0 * p_normal * this->dot(p_normal) - *this; } +bool Vector3::is_equal_approx(const Vector3 &p_v, real_t p_tolerance) const { + return Math::is_equal_approx(x, p_v.x, p_tolerance) && Math::is_equal_approx(y, p_v.y, p_tolerance) && Math::is_equal_approx(z, p_v.z, p_tolerance); +} + #endif // VECTOR3_H diff --git a/core/object.h b/core/object.h index 3ece1f35d6a..a795ee83ef4 100644 --- a/core/object.h +++ b/core/object.h @@ -678,6 +678,7 @@ public: void call_multilevel(const StringName &p_name, VARIANT_ARG_LIST); // C++ helper void notification(int p_notification, bool p_reversed = false); + virtual void notification_callback(int p_message_type) {} virtual String to_string(); //used mainly by script, get and set all INCLUDING string diff --git a/core/pooled_list.h b/core/pooled_list.h index 51ca5423a18..24dadeb7656 100644 --- a/core/pooled_list.h +++ b/core/pooled_list.h @@ -93,3 +93,74 @@ public: _used_size--; } }; + +// a pooled list which automatically keeps a list of the active members +template +class TrackedPooledList { +public: + int pool_size() const { return _pool.size(); } + int active_size() const { return _active_list.size(); } + + uint32_t get_active_id(uint32_t p_index) const { + return _active_list[p_index]; + } + + const T &get_active(uint32_t p_index) const { + return _pool[get_active_id(p_index)]; + } + + T &get_active(uint32_t p_index) { + return _pool[get_active_id(p_index)]; + } + + const T &operator[](uint32_t p_index) const { + return _pool[p_index]; + } + T &operator[](uint32_t p_index) { + return _pool[p_index]; + } + + T *request(uint32_t &r_id) { + T *item = _pool.request(r_id); + + // add to the active list + uint32_t active_list_id = _active_list.size(); + _active_list.push_back(r_id); + + // expand the active map (this should be in sync with the pool list + if (_pool.size() > (int)_active_map.size()) { + _active_map.resize(_pool.size()); + } + + // store in the active map + _active_map[r_id] = active_list_id; + + return item; + } + + void free(const uint32_t &p_id) { + _pool.free(p_id); + + // remove from the active list. + uint32_t list_id = _active_map[p_id]; + + // zero the _active map to detect bugs (only in debug?) + _active_map[p_id] = -1; + + _active_list.remove_unordered(list_id); + + // keep the replacement in sync with the correct list Id + if (list_id < (uint32_t)_active_list.size()) { + // which pool id has been replaced in the active list + uint32_t replacement_id = _active_list[list_id]; + + // keep that replacements map up to date with the new position + _active_map[replacement_id] = list_id; + } + } + +private: + PooledList _pool; + LocalVector _active_map; + LocalVector _active_list; +}; diff --git a/doc/classes/CullInstance.xml b/doc/classes/CullInstance.xml new file mode 100644 index 00000000000..36d7fc69e72 --- /dev/null +++ b/doc/classes/CullInstance.xml @@ -0,0 +1,45 @@ + + + + Parent of all nodes that can be culled by the Portal system. + + + Provides common functionality to nodes that can be culled by the [Portal] system. + [code]Static[/code] and [code]Dynamic[/code] objects are the most efficiently managed objects in the system, but there are some caveats. They are expected to be present initially when [Room]s are converted using the [RoomManager] [code]rooms_convert[/code] function, and their lifetime should be the same as the game level (i.e. present until you call [code]rooms_clear[/code] on the [RoomManager]. Although you shouldn't create / delete these objects during gameplay, you can manage their visibility with the standard [code]hide[/code] and [code]show[/code] commands. + [code]Roaming[/code] objects on the other hand, require extra processing to keep track of which [Room] they are within. This enables them to be culled effectively, wherever they are. + [code]Global[/code] objects are not culled by the portal system, and use view frustum culling only. + Objects that are not [code]Static[/code] or [code]Dynamic[/code] can be freely created and deleted during the lifetime of the game level. + + + + + + + + When a manual bound has not been explicitly specified for a [Room], the convex hull bound will be estimated from the geometry of the objects within the room. This setting determines whether the geometry of an object is included in this estimate of the room bound. + [b]Note:[/b] This setting is only relevant when the object is set to [code]PORTAL_MODE_STATIC[/code] or [code]PORTAL_MODE_DYNAMIC[/code], and for [Portal]s. + + + When using [Room]s and [Portal]s, this specifies how the [CullInstance] is processed in the system. + + + + + Use for instances within [Room]s that will [b]not move[/b] - e.g. walls, floors. + [b]Note:[/b] If you attempt to delete a [code]PORTAL_MODE_STATIC[/code] instance while the room graph is loaded (converted), it will unload the room graph and deactivate portal culling. This is because the [b]room graph[/b] data has been invalidated. You will need to reconvert the rooms using the [RoomManager] to activate the system again. + + + Use for instances within rooms that will move but [b]not change room[/b] - e.g. moving platforms. + [b]Note:[/b] If you attempt to delete a [code]PORTAL_MODE_DYNAMIC[/code] instance while the room graph is loaded (converted), it will unload the room graph and deactivate portal culling. This is because the [b]room graph[/b] data has been invalidated. You will need to reconvert the rooms using the [RoomManager] to activate the system again. + + + Use for instances that will move [b]between[/b] [Room]s - e.g. players. + + + Use for instances that will be frustum culled only - e.g. first person weapon, debug. + + + Use for instances that will not be shown at all - e.g. [b]manual room bounds[/b] (specified by prefix [i]'Bound_'[/i]). + + + diff --git a/doc/classes/Portal.xml b/doc/classes/Portal.xml new file mode 100644 index 00000000000..da41b6dc0b4 --- /dev/null +++ b/doc/classes/Portal.xml @@ -0,0 +1,41 @@ + + + + Portal nodes are used to enable visibility between [Room]s. + + + [Portal]s are a special type of [MeshInstance] that allow the portal culling system to 'see' from one room to the next. They often correspond to doors and windows in level geometry. By only allowing [Camera]s to see through portals, this allows the system to cull out all the objects in rooms that cannot be seen through portals. This is a form of [b]occlusion culling[/b], and can greatly increase performance. + There are some limitations to the form of portals: + They must be single sided convex polygons, and usually you would orientate their front faces [b]outward[/b] from the [Room] they are placed in. The vertices should be positioned on a single plane (although their positioning does not have to be perfect). + There is no need to place an opposite portal in an adjacent room, links are made two-way automatically. + + + + + + + + This is a shortcut for setting the linked [Room] in the name of the [Portal] (the name is used during conversion). + + + The points defining the shape of the [Portal] polygon (which should be convex). + These are defined in 2D, with [code]0,0[/code] being the origin of the [Portal] node's [code]global_transform[/code]. + [code]Note:[/code] These raw points are sanitized for winding order internally. + + + Visibility through [Portal]s can be turned on and off at runtime - this is useful for having closable doors. + + + Some objects are so big that they may be present in more than one [Room] ('sprawling'). As we often don't want objects that *just* breach the edges to be assigned to neighbouring rooms, you can assign an extra margin through the [Portal] to allow objects to breach without sprawling. + + + Portals default to being two way - see through in both directions, however you can make them one way, visible from the source room only. + + + In most cases you will want to use the default [Portal] margin in your portals (this is set in the [RoomManager]). + If you want to override this default, set this value to [code]false[/code], and the local [code]portal_margin[/code] will take effect. + + + + + diff --git a/doc/classes/Room.xml b/doc/classes/Room.xml new file mode 100644 index 00000000000..92a4958ae10 --- /dev/null +++ b/doc/classes/Room.xml @@ -0,0 +1,31 @@ + + + + Room node, used to group objects together locally for [Portal] culling. + + + The [Portal] culling system requires levels to be built using objects grouped together by location in areas called [Room]s. In many cases these will correspond to actual rooms in buildings, but not necessarily (a canyon area may be treated as a room). + Any [VisualInstance] that is a child or grandchild of a [Room] will be assigned to that room, if the [code]portal_mode[/code] of that [VisualInstance] is set to [code]STATIC[/code] (does not move) or [code]DYNAMIC[/code] (moves only within the room). + Internally the room boundary must form a [b]convex hull[/b], and by default this is determined automatically by the geometry of the objects you place within the room. + You can alternatively precisely specify a [b]manual bound[/b]. If you place a [MeshInstance] with a name prefixed by [code]Bound_[/code], it will turn off the bound generation from geometry, and instead use the vertices of this MeshInstance to directly calculate a convex hull during the conversion stage (see [RoomManager]). + In order to see from one room into an adjacent room, [Portal]s must be placed over non-occluded openings between rooms. These will often be placed over doors and windows. + + + + + + + + If [code]points[/code] are set, the [Room] bounding convex hull will be built from these points. If no points are set, the room bound will either be derived from a manual bound ([MeshInstance] with name prefix [code]Bound_[/code]), or from the geometry within the room. + Note that you can use the [code]Generate Points[/code] editor button to get started. This will use either the geometry or manual bound to generate the room hull, and save the resulting points, allowing you to edit them to further refine the bound. + + + The [code]simplify[/code] value determines to what degree room hulls (bounds) are simplified, by removing similar planes. A value of 0 gives no simplification, 1 gives maximum simplification. + + + The room hull simplification can either use the default value set in the [RoomManager], or override this and use the per room setting. + + + + + diff --git a/doc/classes/RoomGroup.xml b/doc/classes/RoomGroup.xml new file mode 100644 index 00000000000..c1b45ff0f26 --- /dev/null +++ b/doc/classes/RoomGroup.xml @@ -0,0 +1,24 @@ + + + + Groups [Room]s together to allow common functionality. + + + Although [Room] behaviour can be specified individually, sometimes it is faster and more convenient to write functionality for a group of rooms. + [RoomGroup]s should be placed as children of the [b]room list[/b] (the parent [Node] of your [Room]s), and [Room]s should be placed in turn as children of a [RoomGroup] in order to assign them to the RoomGroup. + A [RoomGroup] can for example be used to specify [Room]s that are [b]outside[/b], and switch on or off a directional light, sky, or rain effect as the player enters / exits the area. + [RoomGroup]s receive [b]gameplay callbacks[/b] when the [code]gameplay_monitor[/code] is switched on, as [code]signal[/code]s or [code]notification[/code]s as they enter and exit the [b]gameplay area[/b] (see [RoomManager] for details). + + + + + + + + This priority will be applied to [Room]s within the group. The [Room] priority allows the use of [b]internal rooms[/b], rooms [i]within[/i] another room or rooms. + When the [Camera] is within more than one room (regular and internal), the higher priority room will take precedence. So with for example, a house inside a terrain 'room', you would make the house higher priority, so that when the camera is within the house, the house is used as the source room, but outside the house, the terrain room would be used instead. + + + + + diff --git a/doc/classes/RoomManager.xml b/doc/classes/RoomManager.xml new file mode 100644 index 00000000000..8fc8a9df647 --- /dev/null +++ b/doc/classes/RoomManager.xml @@ -0,0 +1,123 @@ + + + + The RoomManager node is used to control the portal culling system. + + + In order to utilize the portal occlusion culling system, you must build your level using [Room]s and [Portal]s. Before these can be used at runtime, they must undergo a short conversion process to build the [code]room graph[/code], runtime data needed for portal culling. The [code]room graph[/code] is controlled by the [RoomManager] node, and the [RoomManager] also contains settings that are common throughout the portal system. + + + + + + + + + This function clears all converted data from the [b]room graph[/b]. Use this before unloading a level, when transitioning from level to level, or returning to a main menu. + + + + + + + This is the most important function in the whole portal culling system. Without it, the system cannot function. + First it goes through every [Room] that is a child of the [code]room list[/code] node (and [RoomGroup]s within) and converts and adds it to the [code]room graph[/code]. + This works for both [Room] nodes, and [Spatial] nodes that follow a special naming convention. They should begin with the prefix [i]'Room_'[/i], followed by the name you wish to give the room, e.g. [i]'Room_lounge'[/i]. This will automatically convert such [Spatial]s to [Room] nodes for you. This is useful if you want to build you entire room system in e.g. Blender, and reimport multiple times as you work on the level. + The conversion will try to assign [VisualInstance]s that are children and grandchildren of the [Room] to the room. These should be given a suitable [code]portal mode[/code] (see the [CullInstance] documentation). The default [code]portal mode[/code] is [code]STATIC[/code] - objects which are not expected to move while the level is played, which will typically be most objects. + The conversion will usually use the geometry of these [VisualInstance]s (and the [Portal]s) to calculate a convex hull bound for the room. These bounds will be shown in the editor with a wireframe. Alternatively you can specify a manual custom bound for any room, see the [Room] documentation. + By definition, [Camera]s within a room can see everything else within the room (that is one advantage to using convex hulls). However, in order to see from one room into adjacent rooms, you must place [Portal]s, which represent openings that the camera can see through, like windows and doors. + [Portal]s are really just specialized [MeshInstance]s. In fact you will usually first create a portal by creating a [MeshInstance], especially a [code]plane[/code] mesh instance. You would move the plane in the editor to cover a window or doorway, with the front face pointing outward from the room. To let the conversion process know you want this mesh to be a portal, again we use a special naming convention. [MeshInstance]s to be converted to a [Portal] should start with the prefix [i]'Portal_'[/i]. + You now have a choice - you can leave the name as [i]'Portal_'[/i] and allow the system to automatically detect the nearest [Room] to link. In most cases this will work fine. + An alternative method is to specify the [Room] to link to manually, appending a suffix to the portal name, which should be the name of the room you intend to link to. For example [i]'Portal_lounge'[/i] will attempt to link to the room named [i]'Room_lounge'[/i]. + There is a special case here - Godot does not allow two nodes to share the same name. What if you want to manually have more than one portal leading into the same room? Surely they will need to both be called, e.g. [i]'Portal_lounge'[/i]? + The solution is a wildcard character. After the room name, if you use the character [i]'*'[/i], this character and anything following it will be ignored. So you can use for example [i]'Portal_lounge*0'[/i], [i]'Portal_lounge*1'[/i] etc. + Note that [Portal]s that have already been converted to [Portal] nodes (rather than [MeshInstance]s) still need to follow the same naming convention, as they will be relinked each time during conversion. + It is recommended that you only place objects in rooms that are desired to stay within those rooms - i.e. [code]portal mode[/code]s [code]STATIC[/code] or [code]DYNAMIC[/code] (not crossing portals). [code]GLOBAL[/code] and [code]ROAMING[/code] objects are best placed in another part of the scene tree, to avoid confusion. See [CullInstance] for a full description of portal modes. + + + + + + Switches the portal culling system on and off. + It is important to note that when portal culling is active, it is responsible for [b]all[/b] the 3d culling. Some editor functionality may be more difficult to use, so switching the active flag is intended to be used to make sure your [Room] / [Portal] layout works within the editor. + Switching to [code]active[/code] will have no effect when the [code]room graph[/code] is unloaded (the rooms have not yet been converted). + + + Large objects can 'sprawl' over (be present in) more than one room. It can be useful to visualize which objects are sprawling outside the current room. + Toggling this setting turns this debug view on and off. + + + Usually we don't want objects that only [b]just[/b] cross a boundary into an adjacent [Room] to sprawl into that room. To prevent this, each [Portal] has an extra margin, or tolerance zone where objects can enter without sprawling to a neighbouring room. + In most cases you can set this here for all portals. It is possible to override the margin for each portal. + + + The default convention is for portal normals to point outward (face outward) from the source room. If you accidentally build your level with portals facing the wrong way, this setting can fix the problem. It will flip named portal meshes (i.e. [code]Portal_[/code]) on the initial convertion to [Portal] nodes. + + + When using a partial or full PVS, the gameplay monitor allows you to receive callbacks when roaming objects or rooms enter or exit the [b]gameplay area[/b]. The gameplay area is defined as either the primary, or secondary PVS. + These callbacks allow you to, for example, reduce processing for objects that are far from the player, or turn on and off AI. + You can either choose to receive callbacks as notifications through the [code]_notification[/code] function, or as signals. + [code]NOTIFICATION_ENTER_GAMEPLAY[/code] + [code]NOTIFICATION_EXIT_GAMEPLAY[/code] + Signals: [code]"gameplay_entered"[/code], [code]"gameplay_exited"[/code] + + + If enabled, the system will attempt to merge similar meshes (particularly in terms of materials) within [Room]s during conversion. This can significantly reduce the number of drawcalls and state changes required during rendering, albeit at a cost of reduced culling granularity. + [b]Note:[/b] This operates at runtime during the conversion process, and will only operate on exported or running projects, in order to prevent accidental alteration to the scene and loss of data. + + + When converting rooms, the editor will warn you if overlap is detected between rooms. Overlap can interfere with determining the room that cameras and objects are within. A small amount can be acceptable, depending on your level. Here you can alter the threshold at which the editor warning appears. There are no other side effects. + + + Portal rendering is recursive - each time a portal is seen through an earlier portal there is some cost. For this reason, and to prevent the possibility of infinite loops, this setting provides a hard limit on the recursion depth. + [b]Note:[/b] This value is unused when using [code]Full[/code] PVS mode. + + + Portal culling normally operates using the current [Camera] / [Camera]s, however for debugging purposes within the editor, you can use this setting to override this behaviour and force it to use a particular camera to get a better idea of what the occlusion culling is doing. + + + + Optionally during conversion the potentially visible set (PVS) of rooms that are potentially visible from each room can be calculated. This can be used either to aid in dynamic portal culling, or to totally replace portal culling. + In [code]Full[/code] PVS Mode, all objects within the potentially visible rooms will be frustum culled, and rendered if they are within the view frustum. + + + If enabled, while merging meshes, the system will also attempt to remove [Spatial] nodes that no longer have any children. + Reducing the number of [Node]s in the scene tree can make traversal more efficient, but can be switched off in case you wish to use empty [Spatial]s for markers or some other purpose. + + + During the conversion process, the geometry of objects within [Room]s, or a custom specified manual bound, are used to generate a [b]convex hull bound[/b]. + This convex hull is [b]required[/b] in the visibility system, and is used for many purposes. Most importantly, it is used to decide whether the [Camera] (or an object) is within a [Room]. The convex hull generating algorithm is good, but occasionally it can create too many (or too few) planes to give a good representation of the room volume. + The [code]room_simplify[/code] value can be used to gain fine control over this process. It determines how similar planes can be for them to be considered the same (and duplicates removed). The value can be set between 0 (no simplification) and 1 (maximum simplification). + The value set here is the default for all rooms, but individual rooms can override this value if desired. + The room convex hulls are shown as a wireframe in the editor. + + + For the [Room] conversion process to succeed, you must point the [RoomManager] to the parent [Node] of your [Room]s and [RoomGroup]s, which we refer to as the [code]roomlist[/code] (the roomlist is not a special node type, it is normally just a [Spatial]). + + + Show debugging information - including [Portal]s, and conversion logs. + [b]Note:[/b] This will automatically be disabled in exports. + + + Shows the [Portal] margins when the portal gizmo is used in the editor. + + + When receiving gameplay callbacks when objects enter and exit gameplay, the [b]gameplay area[/b] can be defined by either the primary PVS (potentially visible set) of [Room]s, or the secondary PVS (the primary PVS and their neighbouring [Room]s). + Sometimes using the larger gameplay area of the secondary PVS may be preferable. + + + Gameplay callbacks can either be sent as [code]signals[/code] or [code]notifications[/code]. + + + + + Use only [Portal]s at runtime to determine visibility. PVS will not be generated at [Room]s conversion, and gameplay notifications cannot be used. + + + Use a combination of PVS and [Portal]s to determine visibility (this is usually fastest and most accurate). + + + Use only the PVS (potentially visible set) of [Room]s to determine visibility. + + + diff --git a/doc/classes/Spatial.xml b/doc/classes/Spatial.xml index 1e8307e6eae..17e4c5b5436 100644 --- a/doc/classes/Spatial.xml +++ b/doc/classes/Spatial.xml @@ -328,6 +328,16 @@ + + + Emitted by portal system gameplay monitor when a node enters the gameplay area. + + + + + Emitted by portal system gameplay monitor when a node exits the gameplay area. + + Emitted when node visibility changes. @@ -348,5 +358,11 @@ Spatial nodes receives this notification when their visibility changes. + + Spatial nodes receives this notification if the portal system gameplay monitor detects they have entered the gameplay area. + + + Spatial nodes receives this notification if the portal system gameplay monitor detects they have exited the gameplay area. + diff --git a/doc/classes/VisibilityNotifier.xml b/doc/classes/VisibilityNotifier.xml index fad942d7504..cca90e16e3a 100644 --- a/doc/classes/VisibilityNotifier.xml +++ b/doc/classes/VisibilityNotifier.xml @@ -1,5 +1,5 @@ - + Detects approximately when the node is visible on screen. diff --git a/doc/classes/VisualInstance.xml b/doc/classes/VisualInstance.xml index 2a1530a31bd..aa7a539a8ed 100644 --- a/doc/classes/VisualInstance.xml +++ b/doc/classes/VisualInstance.xml @@ -1,5 +1,5 @@ - + Parent of all visual 3D nodes. diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index cd07b276a05..ea8de97de78 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -142,6 +142,7 @@ #include "editor/plugins/physical_bone_plugin.h" #include "editor/plugins/polygon_2d_editor_plugin.h" #include "editor/plugins/resource_preloader_editor_plugin.h" +#include "editor/plugins/room_manager_editor_plugin.h" #include "editor/plugins/root_motion_editor_plugin.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/plugins/script_text_editor.h" @@ -6798,6 +6799,8 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(Particles2DEditorPlugin(this))); add_editor_plugin(memnew(GIProbeEditorPlugin(this))); add_editor_plugin(memnew(BakedLightmapEditorPlugin(this))); + add_editor_plugin(memnew(RoomManagerEditorPlugin(this))); + add_editor_plugin(memnew(RoomEditorPlugin(this))); add_editor_plugin(memnew(Path2DEditorPlugin(this))); add_editor_plugin(memnew(PathEditorPlugin(this))); add_editor_plugin(memnew(Line2DEditorPlugin(this))); diff --git a/editor/icons/icon_gizmo_portal.svg b/editor/icons/icon_gizmo_portal.svg new file mode 100644 index 00000000000..019d53d8e56 --- /dev/null +++ b/editor/icons/icon_gizmo_portal.svg @@ -0,0 +1 @@ + diff --git a/editor/icons/icon_room_group.svg b/editor/icons/icon_room_group.svg new file mode 100644 index 00000000000..2aadf19dd13 --- /dev/null +++ b/editor/icons/icon_room_group.svg @@ -0,0 +1 @@ + diff --git a/editor/icons/icon_room_manager.svg b/editor/icons/icon_room_manager.svg new file mode 100644 index 00000000000..f23213320a9 --- /dev/null +++ b/editor/icons/icon_room_manager.svg @@ -0,0 +1 @@ + diff --git a/editor/plugins/room_manager_editor_plugin.cpp b/editor/plugins/room_manager_editor_plugin.cpp new file mode 100644 index 00000000000..ac23b7be791 --- /dev/null +++ b/editor/plugins/room_manager_editor_plugin.cpp @@ -0,0 +1,172 @@ +/*************************************************************************/ +/* room_manager_editor_plugin.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_editor_plugin.h" + +#include "editor/spatial_editor_gizmos.h" + +void RoomManagerEditorPlugin::_rooms_convert() { + if (_room_manager) { + _room_manager->rooms_convert(); + } +} + +void RoomManagerEditorPlugin::_flip_portals() { + if (_room_manager) { + _room_manager->rooms_flip_portals(); + } +} + +void RoomManagerEditorPlugin::edit(Object *p_object) { + RoomManager *s = Object::cast_to(p_object); + if (!s) { + return; + } + + _room_manager = s; +} + +bool RoomManagerEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("RoomManager"); +} + +void RoomManagerEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + button_rooms_convert->show(); + button_flip_portals->show(); + } else { + button_rooms_convert->hide(); + button_flip_portals->hide(); + } +} + +void RoomManagerEditorPlugin::_bind_methods() { + ClassDB::bind_method("_rooms_convert", &RoomManagerEditorPlugin::_rooms_convert); + ClassDB::bind_method("_flip_portals", &RoomManagerEditorPlugin::_flip_portals); +} + +RoomManagerEditorPlugin::RoomManagerEditorPlugin(EditorNode *p_node) { + editor = p_node; + + button_flip_portals = memnew(ToolButton); + button_flip_portals->set_icon(editor->get_gui_base()->get_icon("Portal", "EditorIcons")); + button_flip_portals->set_text(TTR("Flip Portals")); + button_flip_portals->hide(); + button_flip_portals->connect("pressed", this, "_flip_portals"); + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, button_flip_portals); + + button_rooms_convert = memnew(ToolButton); + button_rooms_convert->set_icon(editor->get_gui_base()->get_icon("RoomGroup", "EditorIcons")); + button_rooms_convert->set_text(TTR("Convert Rooms")); + button_rooms_convert->hide(); + button_rooms_convert->connect("pressed", this, "_rooms_convert"); + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, button_rooms_convert); + + _room_manager = nullptr; + + Ref room_gizmo_plugin = Ref(memnew(RoomGizmoPlugin)); + SpatialEditor::get_singleton()->add_gizmo_plugin(room_gizmo_plugin); + + Ref portal_gizmo_plugin = Ref(memnew(PortalGizmoPlugin)); + SpatialEditor::get_singleton()->add_gizmo_plugin(portal_gizmo_plugin); +} + +RoomManagerEditorPlugin::~RoomManagerEditorPlugin() { +} + +/////////////////////// + +void RoomEditorPlugin::_generate_points() { + if (_room) { + PoolVector old_pts = _room->get_points(); + + // only generate points if none already exist + if (_room->_bound_pts.size()) { + _room->set_points(PoolVector()); + } + + PoolVector pts = _room->generate_points(); + + // allow the user to undo generating points, because it is + // frustrating to lose old data + undo_redo->create_action("generate_points"); + undo_redo->add_do_property(_room, "points", pts); + undo_redo->add_undo_property(_room, "points", old_pts); + undo_redo->commit_action(); + } +} + +void RoomEditorPlugin::edit(Object *p_object) { + Room *s = Object::cast_to(p_object); + if (!s) { + return; + } + + _room = s; + + if (SpatialEditor::get_singleton()->is_visible() && s->_planes.size()) { + String string = String(s->get_name()) + " [" + itos(s->_planes.size()) + " planes]"; + SpatialEditor::get_singleton()->set_message(string); + } +} + +bool RoomEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("Room"); +} + +void RoomEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + button_generate->show(); + } else { + button_generate->hide(); + } +} + +void RoomEditorPlugin::_bind_methods() { + ClassDB::bind_method("_generate_points", &RoomEditorPlugin::_generate_points); +} + +RoomEditorPlugin::RoomEditorPlugin(EditorNode *p_node) { + editor = p_node; + + button_generate = memnew(ToolButton); + button_generate->set_icon(editor->get_gui_base()->get_icon("Room", "EditorIcons")); + button_generate->set_text(TTR("Generate Points")); + button_generate->hide(); + button_generate->connect("pressed", this, "_generate_points"); + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, button_generate); + + _room = nullptr; + + undo_redo = EditorNode::get_undo_redo(); +} + +RoomEditorPlugin::~RoomEditorPlugin() { +} diff --git a/editor/plugins/room_manager_editor_plugin.h b/editor/plugins/room_manager_editor_plugin.h new file mode 100644 index 00000000000..435e4e58189 --- /dev/null +++ b/editor/plugins/room_manager_editor_plugin.h @@ -0,0 +1,92 @@ +/*************************************************************************/ +/* room_manager_editor_plugin.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef ROOM_MANAGER_EDITOR_PLUGIN_H +#define ROOM_MANAGER_EDITOR_PLUGIN_H + +#include "editor/editor_node.h" +#include "editor/editor_plugin.h" +#include "scene/3d/room.h" +#include "scene/3d/room_manager.h" +#include "scene/resources/material.h" + +class RoomManagerEditorPlugin : public EditorPlugin { + GDCLASS(RoomManagerEditorPlugin, EditorPlugin); + + RoomManager *_room_manager; + + ToolButton *button_rooms_convert; + ToolButton *button_flip_portals; + EditorNode *editor; + + void _rooms_convert(); + void _flip_portals(); + +protected: + static void _bind_methods(); + +public: + virtual String get_name() const { return "RoomManager"; } + 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); + + RoomManagerEditorPlugin(EditorNode *p_node); + ~RoomManagerEditorPlugin(); +}; + +/////////////////////// + +class RoomEditorPlugin : public EditorPlugin { + GDCLASS(RoomEditorPlugin, EditorPlugin); + + Room *_room; + ToolButton *button_generate; + EditorNode *editor; + UndoRedo *undo_redo; + + void _generate_points(); + +protected: + static void _bind_methods(); + +public: + virtual String get_name() const { return "Room"; } + 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); + + RoomEditorPlugin(EditorNode *p_node); + ~RoomEditorPlugin(); +}; + +#endif diff --git a/editor/plugins/spatial_editor_plugin.cpp b/editor/plugins/spatial_editor_plugin.cpp index aa763b0e3cc..aec3310758c 100644 --- a/editor/plugins/spatial_editor_plugin.cpp +++ b/editor/plugins/spatial_editor_plugin.cpp @@ -3019,6 +3019,7 @@ void SpatialEditorViewport::_init_gizmo_instance(int p_idx) { VS::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); VS::get_singleton()->instance_geometry_set_cast_shadows_setting(move_gizmo_instance[i], VS::SHADOW_CASTING_SETTING_OFF); VS::get_singleton()->instance_set_layer_mask(move_gizmo_instance[i], layer); + VS::get_singleton()->instance_set_portal_mode(move_gizmo_instance[i], VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); move_plane_gizmo_instance[i] = VS::get_singleton()->instance_create(); VS::get_singleton()->instance_set_base(move_plane_gizmo_instance[i], spatial_editor->get_move_plane_gizmo(i)->get_rid()); @@ -3026,6 +3027,7 @@ void SpatialEditorViewport::_init_gizmo_instance(int p_idx) { VS::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); VS::get_singleton()->instance_geometry_set_cast_shadows_setting(move_plane_gizmo_instance[i], VS::SHADOW_CASTING_SETTING_OFF); VS::get_singleton()->instance_set_layer_mask(move_plane_gizmo_instance[i], layer); + VS::get_singleton()->instance_set_portal_mode(move_plane_gizmo_instance[i], VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); rotate_gizmo_instance[i] = VS::get_singleton()->instance_create(); VS::get_singleton()->instance_set_base(rotate_gizmo_instance[i], spatial_editor->get_rotate_gizmo(i)->get_rid()); @@ -3033,6 +3035,7 @@ void SpatialEditorViewport::_init_gizmo_instance(int p_idx) { VS::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); VS::get_singleton()->instance_geometry_set_cast_shadows_setting(rotate_gizmo_instance[i], VS::SHADOW_CASTING_SETTING_OFF); VS::get_singleton()->instance_set_layer_mask(rotate_gizmo_instance[i], layer); + VS::get_singleton()->instance_set_portal_mode(rotate_gizmo_instance[i], VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); scale_gizmo_instance[i] = VS::get_singleton()->instance_create(); VS::get_singleton()->instance_set_base(scale_gizmo_instance[i], spatial_editor->get_scale_gizmo(i)->get_rid()); @@ -3040,6 +3043,7 @@ void SpatialEditorViewport::_init_gizmo_instance(int p_idx) { VS::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); VS::get_singleton()->instance_geometry_set_cast_shadows_setting(scale_gizmo_instance[i], VS::SHADOW_CASTING_SETTING_OFF); VS::get_singleton()->instance_set_layer_mask(scale_gizmo_instance[i], layer); + VS::get_singleton()->instance_set_portal_mode(scale_gizmo_instance[i], VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); scale_plane_gizmo_instance[i] = VS::get_singleton()->instance_create(); VS::get_singleton()->instance_set_base(scale_plane_gizmo_instance[i], spatial_editor->get_scale_plane_gizmo(i)->get_rid()); @@ -3047,6 +3051,7 @@ void SpatialEditorViewport::_init_gizmo_instance(int p_idx) { VS::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); VS::get_singleton()->instance_geometry_set_cast_shadows_setting(scale_plane_gizmo_instance[i], VS::SHADOW_CASTING_SETTING_OFF); VS::get_singleton()->instance_set_layer_mask(scale_plane_gizmo_instance[i], layer); + VS::get_singleton()->instance_set_portal_mode(scale_plane_gizmo_instance[i], VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); } // Rotation white outline @@ -6001,6 +6006,15 @@ void SpatialEditor::set_can_preview(Camera *p_preview) { } } +void SpatialEditor::set_message(String p_message, float p_time) { + for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) { + SpatialEditorViewport *viewport = get_editor_viewport(i); + if (viewport->is_visible()) { + viewport->set_message(p_message, p_time); + } + } +} + VSplitContainer *SpatialEditor::get_shader_split() { return shader_split; } diff --git a/editor/plugins/spatial_editor_plugin.h b/editor/plugins/spatial_editor_plugin.h index ab5137b311a..8d680cb0105 100644 --- a/editor/plugins/spatial_editor_plugin.h +++ b/editor/plugins/spatial_editor_plugin.h @@ -781,6 +781,7 @@ public: void set_over_gizmo_handle(int idx) { over_gizmo_handle = idx; } void set_can_preview(Camera *p_preview); + void set_message(String p_message, float p_time = 5); SpatialEditorViewport *get_editor_viewport(int p_idx) { ERR_FAIL_INDEX_V(p_idx, static_cast(VIEWPORTS_COUNT), nullptr); diff --git a/editor/spatial_editor_gizmos.cpp b/editor/spatial_editor_gizmos.cpp index 8cdfc00eb45..c0386809626 100644 --- a/editor/spatial_editor_gizmos.cpp +++ b/editor/spatial_editor_gizmos.cpp @@ -44,9 +44,11 @@ #include "scene/3d/navigation_mesh.h" #include "scene/3d/particles.h" #include "scene/3d/physics_joint.h" +#include "scene/3d/portal.h" #include "scene/3d/position_3d.h" #include "scene/3d/ray_cast.h" #include "scene/3d/reflection_probe.h" +#include "scene/3d/room.h" #include "scene/3d/soft_body.h" #include "scene/3d/spring_arm.h" #include "scene/3d/sprite_3d.h" @@ -162,6 +164,7 @@ void EditorSpatialGizmo::set_spatial_node(Spatial *p_node) { void EditorSpatialGizmo::Instance::create_instance(Spatial *p_base, bool p_hidden) { instance = VS::get_singleton()->instance_create2(mesh->get_rid(), p_base->get_world()->get_scenario()); + VS::get_singleton()->instance_set_portal_mode(instance, VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); VS::get_singleton()->instance_attach_object_instance_id(instance, p_base->get_instance_id()); if (skin_reference.is_valid()) { VS::get_singleton()->instance_attach_skeleton(instance, skin_reference->get_skeleton()); @@ -4429,3 +4432,277 @@ void JointSpatialGizmoPlugin::CreateGeneric6DOFJointGizmo( #undef ADD_VTX } + +//// + +RoomGizmoPlugin::RoomGizmoPlugin() { + create_material("room", Color(0.5, 1.0, 0.0), false, true, false); + create_material("room_overlap", Color(1.0, 0.0, 0.0), false, false, false); +} + +bool RoomGizmoPlugin::has_gizmo(Spatial *p_spatial) { + if (Object::cast_to(p_spatial)) { + return true; + } + + return false; +} + +String RoomGizmoPlugin::get_name() const { + return "Room"; +} + +int RoomGizmoPlugin::get_priority() const { + return -1; +} + +void RoomGizmoPlugin::redraw(EditorSpatialGizmo *p_gizmo) { + p_gizmo->clear(); + + Room *room = Object::cast_to(p_gizmo->get_spatial_node()); + + if (room) { + const Geometry::MeshData &md = room->_bound_mesh_data; + if (!md.edges.size()) + return; + + Vector lines; + Transform tr = room->get_global_transform(); + tr.affine_invert(); + + Ref material = get_material("room", p_gizmo); + Ref material_overlap = get_material("room_overlap", p_gizmo); + Color color(1, 1, 1, 1); + + for (int n = 0; n < md.edges.size(); n++) { + Vector3 a = md.vertices[md.edges[n].a]; + Vector3 b = md.vertices[md.edges[n].b]; + + // xform + a = tr.xform(a); + b = tr.xform(b); + + lines.push_back(a); + lines.push_back(b); + } + + p_gizmo->add_lines(lines, material, false, color); + + // overlap zones + for (int z = 0; z < room->_gizmo_overlap_zones.size(); z++) { + const Geometry::MeshData &md_overlap = room->_gizmo_overlap_zones[z]; + Vector pts; + + for (int f = 0; f < md_overlap.faces.size(); f++) { + const Geometry::MeshData::Face &face = md_overlap.faces[f]; + + for (int c = 0; c < face.indices.size() - 2; c++) { + pts.push_back(tr.xform(md_overlap.vertices[face.indices[0]])); + pts.push_back(tr.xform(md_overlap.vertices[face.indices[c + 1]])); + pts.push_back(tr.xform(md_overlap.vertices[face.indices[c + 2]])); + } + } + + Ref mesh = memnew(ArrayMesh); + Array array; + array.resize(Mesh::ARRAY_MAX); + array[Mesh::ARRAY_VERTEX] = pts; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array); + p_gizmo->add_mesh(mesh, false, Ref(), material_overlap); + } + } +} + +//// + +PortalGizmoPlugin::PortalGizmoPlugin() { + create_icon_material("portal_icon", SpatialEditor::get_singleton()->get_icon("GizmoPortal", "EditorIcons"), true); + create_material("portal", Color(1.0, 1.0, 1.0, 1.0), false, false, true); + create_material("portal_margin", Color(1.0, 0.1, 0.1, 0.3), false, false, false); + create_material("portal_edge", Color(0.0, 0.0, 0.0, 0.3), false, false, false); + create_material("portal_arrow", Color(1.0, 1.0, 1.0, 1.0), false, false, false); +} + +bool PortalGizmoPlugin::has_gizmo(Spatial *p_spatial) { + if (Object::cast_to(p_spatial)) { + return true; + } + + return false; +} + +String PortalGizmoPlugin::get_name() const { + return "Portal"; +} + +int PortalGizmoPlugin::get_priority() const { + return -1; +} + +void PortalGizmoPlugin::redraw(EditorSpatialGizmo *p_gizmo) { + p_gizmo->clear(); + + Portal *portal = Object::cast_to(p_gizmo->get_spatial_node()); + + if (portal) { + // warnings + if (portal->_warning_outside_room_aabb || portal->_warning_facing_wrong_way) { + Ref icon = get_material("portal_icon", p_gizmo); + p_gizmo->add_unscaled_billboard(icon, 0.05); + } + + Transform tr = portal->get_global_transform(); + tr.affine_invert(); + + Ref material_portal = get_material("portal", p_gizmo); + Ref material_margin = get_material("portal_margin", p_gizmo); + Ref material_edge = get_material("portal_edge", p_gizmo); + Ref material_arrow = get_material("portal_arrow", p_gizmo); + Color color(1, 1, 1, 1); + Color color_portal_front(0.05, 0.05, 1.0, 0.3); + Color color_portal_back(1.0, 1.0, 0.0, 0.15); + + // make sure world points are up to date + portal->portal_update(); + + int num_points = portal->_pts_world.size(); + + // prevent compiler warnings later on + if (num_points < 3) { + return; + } + + // margins + real_t margin = portal->get_active_portal_margin(); + bool show_margins = Portal::_settings_gizmo_show_margins; + + if (margin < 0.05f) { + show_margins = false; + } + + PoolVector pts_portal; + PoolVector cols_portal; + PoolVector pts_margin; + Vector edge_pts; + + Vector3 portal_normal_world_space = portal->_plane.normal; + portal_normal_world_space *= margin; + + // this may not be necessary, dealing with non uniform scales, + // possible the affine_invert dealt with this earlier .. but it's just for + // the editor so not performance critical + Basis normal_basis = tr.basis; + + Vector3 portal_normal = normal_basis.xform(portal_normal_world_space); + Vector3 pt_portal_first = tr.xform(portal->_pts_world[0]); + + for (int n = 0; n < num_points; n++) { + Vector3 pt = portal->_pts_world[n]; + pt = tr.xform(pt); + + // CI for visual studio can't seem to get around the possibility + // that this could cause a divide by zero, so using a local to preclude the + // possibility of aliasing from another thread + int m = (n + 1) % num_points; + Vector3 pt_next = portal->_pts_world[m]; + pt_next = tr.xform(pt_next); + + // don't need the first and last triangles + if ((n != 0) && (n != (num_points - 1))) { + pts_portal.push_back(pt_portal_first); + pts_portal.push_back(pt); + pts_portal.push_back(pt_next); + cols_portal.push_back(color_portal_front); + cols_portal.push_back(color_portal_front); + cols_portal.push_back(color_portal_front); + + pts_portal.push_back(pt_next); + pts_portal.push_back(pt); + pts_portal.push_back(pt_portal_first); + cols_portal.push_back(color_portal_back); + cols_portal.push_back(color_portal_back); + cols_portal.push_back(color_portal_back); + } + + if (show_margins) { + Vector3 pt0 = pt - portal_normal; + Vector3 pt1 = pt + portal_normal; + Vector3 pt2 = pt_next - portal_normal; + Vector3 pt3 = pt_next + portal_normal; + + pts_margin.push_back(pt0); + pts_margin.push_back(pt2); + pts_margin.push_back(pt1); + + pts_margin.push_back(pt2); + pts_margin.push_back(pt3); + pts_margin.push_back(pt1); + + edge_pts.push_back(pt0); + edge_pts.push_back(pt2); + edge_pts.push_back(pt1); + edge_pts.push_back(pt3); + } + } + + // portal itself + { + Ref mesh = memnew(ArrayMesh); + Array array; + array.resize(Mesh::ARRAY_MAX); + array[Mesh::ARRAY_VERTEX] = pts_portal; + array[Mesh::ARRAY_COLOR] = cols_portal; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array); + p_gizmo->add_mesh(mesh, false, Ref(), material_portal); + } + + if (show_margins) { + Ref mesh = memnew(ArrayMesh); + Array array; + array.resize(Mesh::ARRAY_MAX); + array[Mesh::ARRAY_VERTEX] = pts_margin; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array); + p_gizmo->add_mesh(mesh, false, Ref(), material_margin); + + // lines around the outside of mesh + p_gizmo->add_lines(edge_pts, material_edge, false, color); + } // only if the margin is sufficient to be worth drawing + + // arrow + if (show_margins) { + const int arrow_points = 7; + const float arrow_length = 0.5; // 1.5 + const float arrow_width = 0.1; // 0.3 + const float arrow_barb = 0.27; // 0.8 + + Vector3 arrow[arrow_points] = { + Vector3(0, 0, -1), + Vector3(0, arrow_barb, 0), + Vector3(0, arrow_width, 0), + Vector3(0, arrow_width, arrow_length), + Vector3(0, -arrow_width, arrow_length), + Vector3(0, -arrow_width, 0), + Vector3(0, -arrow_barb, 0) + }; + + int arrow_sides = 2; + + Vector lines; + + for (int i = 0; i < arrow_sides; i++) { + for (int j = 0; j < arrow_points; j++) { + Basis ma(Vector3(0, 0, 1), Math_PI * i / arrow_sides); + + Vector3 v1 = arrow[j] - Vector3(0, 0, arrow_length); + Vector3 v2 = arrow[(j + 1) % arrow_points] - Vector3(0, 0, arrow_length); + + lines.push_back(ma.xform(v1)); + lines.push_back(ma.xform(v2)); + } + } + + p_gizmo->add_lines(lines, material_arrow, false, color); + } + + } // was portal +} diff --git a/editor/spatial_editor_gizmos.h b/editor/spatial_editor_gizmos.h index 348bd82a61d..7a5c9fd5a8a 100644 --- a/editor/spatial_editor_gizmos.h +++ b/editor/spatial_editor_gizmos.h @@ -429,4 +429,30 @@ public: JointSpatialGizmoPlugin(); }; +class RoomGizmoPlugin : public EditorSpatialGizmoPlugin { + GDCLASS(RoomGizmoPlugin, EditorSpatialGizmoPlugin); + +protected: + virtual bool has_gizmo(Spatial *p_spatial); + String get_name() const; + int get_priority() const; + void redraw(EditorSpatialGizmo *p_gizmo); + +public: + RoomGizmoPlugin(); +}; + +class PortalGizmoPlugin : public EditorSpatialGizmoPlugin { + GDCLASS(PortalGizmoPlugin, EditorSpatialGizmoPlugin); + +protected: + virtual bool has_gizmo(Spatial *p_spatial); + String get_name() const; + int get_priority() const; + void redraw(EditorSpatialGizmo *p_gizmo); + +public: + PortalGizmoPlugin(); +}; + #endif // SPATIAL_EDITOR_GIZMOS_H diff --git a/main/main.cpp b/main/main.cpp index 74e6ae85a8d..49be27bb165 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -69,6 +69,7 @@ #include "servers/physics_2d_server.h" #include "servers/physics_server.h" #include "servers/register_server_types.h" +#include "servers/visual_server_callbacks.h" #ifdef TOOLS_ENABLED #include "editor/doc/doc_data.h" @@ -103,6 +104,8 @@ static CameraServer *camera_server = nullptr; static ARVRServer *arvr_server = nullptr; static PhysicsServer *physics_server = nullptr; static Physics2DServer *physics_2d_server = nullptr; +static VisualServerCallbacks *visual_server_callbacks = nullptr; + // We error out if setup2() doesn't turn this true static bool _start_success = false; @@ -1452,6 +1455,10 @@ Error Main::setup2(Thread::ID p_main_tid_override) { if (use_debug_profiler && script_debugger) { script_debugger->profiling_start(); } + + visual_server_callbacks = memnew(VisualServerCallbacks); + VisualServer::get_singleton()->callbacks_register(visual_server_callbacks); + _start_success = true; locale = String(); @@ -2098,6 +2105,7 @@ bool Main::iteration() { if (OS::get_singleton()->get_main_loop()->idle(step * time_scale)) { exit = true; } + visual_server_callbacks->flush(); message_queue->flush(); VisualServer::get_singleton()->sync(); //sync if still drawing from previous frames. @@ -2290,6 +2298,10 @@ void Main::cleanup(bool p_force) { message_queue->flush(); memdelete(message_queue); + if (visual_server_callbacks) { + memdelete(visual_server_callbacks); + } + unregister_core_driver_types(); unregister_core_types(); diff --git a/scene/3d/cull_instance.cpp b/scene/3d/cull_instance.cpp new file mode 100644 index 00000000000..8cc30eb1158 --- /dev/null +++ b/scene/3d/cull_instance.cpp @@ -0,0 +1,66 @@ +/*************************************************************************/ +/* cull_instance.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 "cull_instance.h" + +VARIANT_ENUM_CAST(CullInstance::PortalMode); + +void CullInstance::set_portal_mode(CullInstance::PortalMode p_mode) { + _portal_mode = p_mode; + _refresh_portal_mode(); +} + +CullInstance::PortalMode CullInstance::get_portal_mode() const { + return _portal_mode; +} + +void CullInstance::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_portal_mode", "mode"), &CullInstance::set_portal_mode); + ClassDB::bind_method(D_METHOD("get_portal_mode"), &CullInstance::get_portal_mode); + + ClassDB::bind_method(D_METHOD("set_include_in_bound"), &CullInstance::set_include_in_bound); + ClassDB::bind_method(D_METHOD("get_include_in_bound"), &CullInstance::get_include_in_bound); + + ADD_GROUP("Portals", ""); + + BIND_ENUM_CONSTANT(PORTAL_MODE_STATIC); + BIND_ENUM_CONSTANT(PORTAL_MODE_DYNAMIC); + BIND_ENUM_CONSTANT(PORTAL_MODE_ROAMING); + BIND_ENUM_CONSTANT(PORTAL_MODE_GLOBAL); + BIND_ENUM_CONSTANT(PORTAL_MODE_IGNORE); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "portal_mode", PROPERTY_HINT_ENUM, "Static,Dynamic,Roaming,Global,Ignore"), "set_portal_mode", "get_portal_mode"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "include_in_bound"), "set_include_in_bound", "get_include_in_bound"); +} + +CullInstance::CullInstance() { + _portal_mode = PORTAL_MODE_STATIC; + _include_in_bound = true; +} diff --git a/scene/3d/cull_instance.h b/scene/3d/cull_instance.h new file mode 100644 index 00000000000..87d47cf7637 --- /dev/null +++ b/scene/3d/cull_instance.h @@ -0,0 +1,66 @@ +/*************************************************************************/ +/* cull_instance.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef CULL_INSTANCE_H +#define CULL_INSTANCE_H + +#include "scene/3d/spatial.h" + +class CullInstance : public Spatial { + GDCLASS(CullInstance, Spatial); + +public: + enum PortalMode { + PORTAL_MODE_STATIC, // not moving within a room + PORTAL_MODE_DYNAMIC, // moving within room + PORTAL_MODE_ROAMING, // moving between rooms + PORTAL_MODE_GLOBAL, // frustum culled only + PORTAL_MODE_IGNORE, // don't show at all - e.g. manual bounds, hidden portals + }; + + void set_portal_mode(CullInstance::PortalMode p_mode); + CullInstance::PortalMode get_portal_mode() const; + + void set_include_in_bound(bool p_enable) { _include_in_bound = p_enable; } + bool get_include_in_bound() const { return _include_in_bound; } + + CullInstance(); + +protected: + virtual void _refresh_portal_mode() = 0; + + static void _bind_methods(); + +private: + PortalMode _portal_mode; + bool _include_in_bound; +}; + +#endif diff --git a/scene/3d/mesh_instance.cpp b/scene/3d/mesh_instance.cpp index 68fb4bd548c..38fb11d2409 100644 --- a/scene/3d/mesh_instance.cpp +++ b/scene/3d/mesh_instance.cpp @@ -821,6 +821,328 @@ void MeshInstance::create_debug_tangents() { } } +bool MeshInstance::is_mergeable_with(const MeshInstance &p_other) { + if (!get_mesh().is_valid() || !p_other.get_mesh().is_valid()) { + 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, int p_surface_id, PoolVector &r_verts, PoolVector &r_norms, PoolVector &r_tangents, PoolVector &r_colors, PoolVector &r_uvs, PoolVector &r_uv2s, PoolVector &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); + + PoolVector verts = arrays[VS::ARRAY_VERTEX]; + PoolVector normals = arrays[VS::ARRAY_NORMAL]; + PoolVector tangents = arrays[VS::ARRAY_TANGENT]; + PoolVector colors = arrays[VS::ARRAY_COLOR]; + PoolVector uvs = arrays[VS::ARRAY_TEX_UV]; + PoolVector uv2s = arrays[VS::ARRAY_TEX_UV2]; + PoolVector indices = arrays[VS::ARRAY_INDEX]; + + // NEW .. 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 first_index = r_verts.size(); + + // transform verts to world space + Transform tr = p_mi.get_global_transform(); + + // to transform normals + Basis normal_basis = tr.basis.inverse(); + normal_basis.transpose(); + + for (int n = 0; n < verts.size(); n++) { + Vector3 pt_world = tr.xform(verts[n]); + r_verts.push_back(pt_world); + + if (normals.size()) { + Vector3 pt_norm = normal_basis.xform(normals[n]); + pt_norm.normalize(); + r_norms.push_back(pt_norm); + } + + if (tangents.size()) { + 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(); + r_tangents.push_back(pt_tangent.x); + r_tangents.push_back(pt_tangent.y); + r_tangents.push_back(pt_tangent.z); + r_tangents.push_back(fourth); + } + + if (colors.size()) { + r_colors.push_back(colors[n]); + } + + if (uvs.size()) { + r_uvs.push_back(uvs[n]); + } + + if (uv2s.size()) { + r_uv2s.push_back(uv2s[n]); + } + } + + // indices + for (int n = 0; n < indices.size(); n++) { + int ind = indices[n] + first_index; + r_inds.push_back(ind); + } +} + +bool MeshInstance::_ensure_indices_valid(PoolVector &r_indices, const PoolVector &p_verts) { + // 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()); + PoolVector::Write write = r_indices.write(); + int *pi = write.ptr(); + + // this is assuming each triangle vertex is unique + for (int n = 0; n < p_verts.size(); n++) { + *pi = n; + pi++; + } + } + + 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.resize(new_inds.size()); + PoolVector::Write write = r_indices.write(); + int *pi = write.ptr(); + + for (int n = 0; n < new_inds.size(); n++) { + pi[n] = new_inds[n]; + } + + 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 PoolVector &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)) { + _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) { + // 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; +} + +bool MeshInstance::create_by_merging(Vector p_list) { + // must be at least 2 meshes to merge + if (p_list.size() < 2) { + // 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]; + + Ref am; + am.instance(); + + for (int s = 0; s < first->get_mesh()->get_surface_count(); s++) { + PoolVector verts; + PoolVector normals; + PoolVector tangents; + PoolVector colors; + PoolVector uvs; + PoolVector uv2s; + PoolVector inds; + + for (int n = 0; n < p_list.size(); n++) { + _merge_into_mesh_data(*p_list[n], 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 (int n = 0; n < inds.size(); n++) { + int i = inds[n]; + if (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] = verts; + if (normals.size()) { + arr[Mesh::ARRAY_NORMAL] = normals; + } + if (tangents.size()) { + arr[Mesh::ARRAY_TANGENT] = tangents; + } + if (colors.size()) { + arr[Mesh::ARRAY_COLOR] = colors; + } + if (uvs.size()) { + arr[Mesh::ARRAY_TEX_UV] = uvs; + } + if (uv2s.size()) { + arr[Mesh::ARRAY_TEX_UV2] = uv2s; + } + arr[Mesh::ARRAY_INDEX] = 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)); + } + + return true; +} + +void MeshInstance::_merge_log(String p_string) { + 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); diff --git a/scene/3d/mesh_instance.h b/scene/3d/mesh_instance.h index 8b84b4545fa..52ab39601ce 100644 --- a/scene/3d/mesh_instance.h +++ b/scene/3d/mesh_instance.h @@ -94,6 +94,14 @@ protected: void _initialize_skinning(bool p_force_reset = false, bool p_call_attach_skeleton = true); void _update_skinning(); +private: + // merging + void _merge_into_mesh_data(const MeshInstance &p_mi, int p_surface_id, PoolVector &r_verts, PoolVector &r_norms, PoolVector &r_tangents, PoolVector &r_colors, PoolVector &r_uvs, PoolVector &r_uv2s, PoolVector &r_inds); + bool _ensure_indices_valid(PoolVector &r_indices, const PoolVector &p_verts); + bool _check_for_valid_indices(const PoolVector &p_inds, const PoolVector &p_verts, LocalVector *r_inds); + bool _triangle_is_degenerate(const Vector3 &p_a, const Vector3 &p_b, const Vector3 &p_c, real_t p_epsilon); + void _merge_log(String p_string); + protected: bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; @@ -133,6 +141,10 @@ public: void create_debug_tangents(); + // merging + bool is_mergeable_with(const MeshInstance &p_other); + bool create_by_merging(Vector p_list); + virtual AABB get_aabb() const; virtual PoolVector get_faces(uint32_t p_usage_flags) const; diff --git a/scene/3d/portal.cpp b/scene/3d/portal.cpp new file mode 100644 index 00000000000..06643a7a3b7 --- /dev/null +++ b/scene/3d/portal.cpp @@ -0,0 +1,639 @@ +/*************************************************************************/ +/* portal.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 "portal.h" + +#include "core/engine.h" +#include "mesh_instance.h" +#include "room.h" +#include "room_manager.h" +#include "scene/main/viewport.h" +#include "servers/visual_server.h" + +bool Portal::_portal_plane_convention = false; +bool Portal::_settings_gizmo_show_margins = true; + +Portal::Portal() { + clear(); + + // the visual server portal lifetime is linked to the lifetime of this object + _portal_rid = VisualServer::get_singleton()->portal_create(); + +#ifdef TOOLS_ENABLED + _room_manager_godot_ID = 0; +#endif + + // portals are defined COUNTER clockwise, + // because they point OUTWARD from the room in the direction + // of the normal + PoolVector points; + points.resize(4); + points.set(0, Vector2(1, -1)); + points.set(1, Vector2(1, 1)); + points.set(2, Vector2(-1, 1)); + points.set(3, Vector2(-1, -1)); + + set_points(points); // default shape +} + +Portal::~Portal() { + if (_portal_rid != RID()) { + VisualServer::get_singleton()->free(_portal_rid); + } +} + +void Portal::set_points(const PoolVector &p_points) { + _pts_local_raw = p_points; + _sanitize_points(); + + if (is_inside_tree()) { + portal_update(); + update_gizmo(); + } +} + +PoolVector Portal::get_points() const { + return _pts_local_raw; +} + +// extra editor links to the room manager to allow unloading +// on change, or re-converting +void Portal::_changed() { +#ifdef TOOLS_ENABLED + RoomManager *rm = RoomManager::active_room_manager; + if (!rm) { + return; + } + + rm->_rooms_changed(); +#endif +} + +void Portal::clear() { + _settings_active = true; + _settings_two_way = true; + _internal = false; + _linkedroom_ID[0] = -1; + _linkedroom_ID[1] = -1; + _pts_world.clear(); + _pts_local.clear(); + _pts_local_raw.resize(0); + _pt_center_world = Vector3(); + _plane = Plane(); + _margin = 1.0f; + _default_margin = 1.0f; + _use_default_margin = true; +} + +void Portal::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_WORLD: { + ERR_FAIL_COND(get_world().is_null()); + + // defer full creation of the visual server portal to when the editor portal is in the scene tree + VisualServer::get_singleton()->portal_set_scenario(_portal_rid, get_world()->get_scenario()); + + // we can't calculate world points until we have entered the tree + portal_update(); + update_gizmo(); + + } break; + case NOTIFICATION_EXIT_WORLD: { + // partially destroy the visual server portal when the editor portal exits the scene tree + VisualServer::get_singleton()->portal_set_scenario(_portal_rid, RID()); + } break; + case NOTIFICATION_TRANSFORM_CHANGED: { + // keep the world points and the visual server up to date + portal_update(); + } break; + } +} + +void Portal::set_portal_active(bool p_active) { + _settings_active = p_active; + VisualServer::get_singleton()->portal_set_active(_portal_rid, p_active); +} + +bool Portal::get_portal_active() const { + return _settings_active; +} + +void Portal::set_use_default_margin(bool p_use) { + _use_default_margin = p_use; + update_gizmo(); +} + +bool Portal::get_use_default_margin() const { + return _use_default_margin; +} + +void Portal::set_portal_margin(real_t p_margin) { + _margin = p_margin; + + if (!_use_default_margin) { + // give visual feedback in the editor for the portal margin zone + update_gizmo(); + } +} + +real_t Portal::get_portal_margin() const { + return _margin; +} + +void Portal::resolve_links(const RID &p_from_room_rid) { + Room *linkedroom = nullptr; + if (has_node(_settings_path_linkedroom)) { + linkedroom = Object::cast_to(get_node(_settings_path_linkedroom)); + } + + if (linkedroom) { + _linkedroom_ID[1] = linkedroom->_room_ID; + + // send to visual server + VisualServer::get_singleton()->portal_link(_portal_rid, p_from_room_rid, linkedroom->_room_rid, _settings_two_way); + } else { + _linkedroom_ID[1] = -1; + } +} + +void Portal::set_linked_room_internal(const NodePath &link_path) { + _settings_path_linkedroom = link_path; +} + +bool Portal::try_set_unique_name(const String &p_name) { + SceneTree *scene_tree = get_tree(); + if (!scene_tree) { + // should not happen in the editor + return false; + } + + Viewport *root = scene_tree->get_root(); + if (!root) { + return false; + } + + Node *found = root->find_node(p_name, true, false); + + // if the name does not already exist in the scene tree, we can use it + if (!found) { + set_name(p_name); + return true; + } + + // we are trying to set the same name this node already has... + if (found == this) { + // noop + return true; + } + + return false; +} + +void Portal::set_linked_room(const NodePath &link_path) { + // change the name of the portal as well, if the link looks legit + Room *linkedroom = nullptr; + if (has_node(link_path)) { + linkedroom = Object::cast_to(get_node(link_path)); + + if (linkedroom) { + if (linkedroom != get_parent()) { + _settings_path_linkedroom = link_path; + + // change the portal name + String string_link_room = RoomManager::_find_name_after(linkedroom, "Room"); + + // we need a unique name for the portal + String string_name_base = "Portal" + GODOT_PORTAL_DELINEATOR + string_link_room; + if (!try_set_unique_name(string_name_base)) { + bool success = false; + for (int n = 0; n < 128; n++) { + String string_name = string_name_base + GODOT_PORTAL_WILDCARD + itos(n); + if (try_set_unique_name(string_name)) { + success = true; + _changed(); + break; + } + } + + if (!success) { + WARN_PRINT("Could not set portal name, set name manually instead."); + } + } else { + _changed(); + } + } else { + WARN_PRINT("Linked room cannot be portal's parent room, ignoring."); + } + } else { + WARN_PRINT("Linked room path is not a room, ignoring."); + } + } else { + WARN_PRINT("Linked room path not found."); + } +} + +NodePath Portal::get_linked_room() const { + return _settings_path_linkedroom; +} + +void Portal::flip() { + // flip portal + Transform tr = get_transform(); + Basis flip_basis = Basis(Vector3(0, Math_PI, 0)); + tr.basis *= flip_basis; + set_transform(tr); + + _pts_local.clear(); + _pts_world.clear(); + + // flip the raw verts + Vector raw; + raw.resize(_pts_local_raw.size()); + for (int n = 0; n < _pts_local_raw.size(); n++) { + const Vector2 &pt = _pts_local_raw[n]; + raw.set(n, Vector2(-pt.x, pt.y)); + } + + // standardize raw verts winding + Geometry::sort_polygon_winding(raw, false); + + for (int n = 0; n < raw.size(); n++) { + _pts_local_raw.set(n, raw[n]); + } + + _sanitize_points(); + portal_update(); + + update_gizmo(); +} + +bool Portal::create_from_mesh_instance(const MeshInstance *p_mi) { + ERR_FAIL_COND_V(!p_mi, false); + + _pts_local.clear(); + _pts_world.clear(); + + Ref rmesh = p_mi->get_mesh(); + ERR_FAIL_COND_V(!rmesh.is_valid(), false); + + if (rmesh->get_surface_count() == 0) { + WARN_PRINT(vformat("Portal '%s' has no surfaces, ignoring", get_name())); + return false; + } + + Array arrays = rmesh->surface_get_arrays(0); + PoolVector vertices = arrays[VS::ARRAY_VERTEX]; + + // get the model space verts and find center + int num_source_points = vertices.size(); + ERR_FAIL_COND_V(num_source_points < 3, false); + + const Transform &tr_source = p_mi->get_global_transform(); + + Vector pts_world; + + for (int n = 0; n < num_source_points; n++) { + Vector3 pt = tr_source.xform(vertices[n]); + + // test for duplicates. + // Some geometry may contain duplicate verts in portals + // which will muck up the winding etc... + bool duplicate = false; + + for (int m = 0; m < pts_world.size(); m++) { + Vector3 diff = pt - pts_world[m]; + // hopefully this epsilon will do in nearly all cases + if (diff.length() < 0.001) { + duplicate = true; + break; + } + } + + if (!duplicate) { + pts_world.push_back(pt); + } + } + + // get the verts sorted with winding, assume that the triangle initial winding + // tells us the normal and hence which way the world space portal should be facing + _sort_verts_clockwise(_portal_plane_convention, pts_world); + + // back calculate the plane from *all* the portal points, this will give us a nice average plane + // (in case of wonky portals where artwork isn't bang on) + _plane = _plane_from_points_newell(pts_world); + + // change the portal transform to match our plane and the center of the portal + Transform tr_global; + tr_global.set_look_at(Vector3(0, 0, 0), _plane.normal, Vector3(0, 1, 0)); + tr_global.origin = _pt_center_world; + + // We can't directly set this global transform on the portal, because the parent node may already + // have a transform applied, so we need to account for this and give a corrected local transform + // for the portal, such that the end result global transform will be correct. + + // find the difference between this new global transform and the transform of the parent + // then use this for the new local transform of the portal + Spatial *parent = Object::cast_to(get_parent()); + ERR_FAIL_COND_V(!parent, false); + + Transform tr_inverse_parent = parent->get_global_transform().affine_inverse(); + Transform new_local_transform = tr_inverse_parent * tr_global; + set_transform(new_local_transform); + + // now back calculate the local space coords of the portal from the world space coords. + // The local space will be used in future for editing and as a 'master' store of the verts. + _pts_local_raw.resize(pts_world.size()); + + // back transform from global space to local space + Transform tr = tr_global.affine_inverse(); + + for (int n = 0; n < pts_world.size(); n++) { + // pt3 is now in local space + Vector3 pt3 = tr.xform(pts_world[n]); + + // only the x and y required + _pts_local_raw.set(n, Vector2(pt3.x, pt3.y)); + + // The z coordinate should be approx zero + // DEV_ASSERT(Math::abs(pt3.z) < 0.1); + } + + _sanitize_points(); + portal_update(); + + return true; +} + +void Portal::_update_aabb() { + _aabb_local = AABB(); + + if (_pts_local.size()) { + Vector3 begin = _vec2to3(_pts_local[0]); + Vector3 end = begin; + + for (int n = 1; n < _pts_local.size(); n++) { + Vector3 pt = _vec2to3(_pts_local[n]); + + if (pt.x < begin.x) { + begin.x = pt.x; + } + if (pt.y < begin.y) { + begin.y = pt.y; + } + if (pt.z < begin.z) { + begin.z = pt.z; + } + + if (pt.x > end.x) { + end.x = pt.x; + } + if (pt.y > end.y) { + end.y = pt.y; + } + if (pt.z > end.z) { + end.z = pt.z; + } + } + + _aabb_local.position = begin; + _aabb_local.size = end - begin; + } +} + +void Portal::portal_update() { + // first calculate the plane from the transform + // (portals are standardized outward from source room once sanitized, + // irrespective of the user portal plane convention) + const Transform &tr = get_global_transform(); + _plane = Plane(0.0, 0.0, -1.0, 0.0); + _plane = tr.xform(_plane); + + // after becoming a portal, the centre world IS the transform origin + _pt_center_world = tr.origin; + + // recalculates world points from the local space + int num_points = _pts_local.size(); + if (_pts_world.size() != num_points) { + _pts_world.resize(num_points); + } + + for (int n = 0; n < num_points; n++) { + _pts_world.set(n, tr.xform(_vec2to3(_pts_local[n]))); + } + + // no need to check winding order, the points are pre-sanitized only when they change + + // extension margin to prevent objects too easily sprawling + real_t margin = get_active_portal_margin(); + VisualServer::get_singleton()->portal_set_geometry(_portal_rid, _pts_world, margin); +} + +real_t Portal::get_active_portal_margin() const { + if (_use_default_margin) { + return _default_margin; + } + return _margin; +} + +void Portal::_sanitize_points() { + // remove duplicates? NYI maybe not necessary + Vector raw; + raw.resize(_pts_local_raw.size()); + for (int n = 0; n < _pts_local_raw.size(); n++) { + raw.set(n, _pts_local_raw[n]); + } + + // this function may get rid of some concave points due to user editing .. + // may not be necessary, no idea how fast it is + _pts_local = Geometry::convex_hull_2d(raw); + + // some pecularity of convex_hull_2d function, it duplicates the last point for some reason + if (_pts_local.size() > 1) { + _pts_local.resize(_pts_local.size() - 1); + } + + // sort winding, the system expects counter clockwise polys + Geometry::sort_polygon_winding(_pts_local, false); + + // a bit of a bodge, but a small epsilon pulling in the portal edges towards the center + // can hide walls in the opposite room that abutt the portal (due to floating point error) + // find 2d center + Vector2 center; + for (int n = 0; n < _pts_local.size(); n++) { + center += _pts_local[n]; + } + center /= _pts_local.size(); + + const real_t pull_in = 0.0001; + + for (int n = 0; n < _pts_local.size(); n++) { + Vector2 offset = _pts_local[n] - center; + real_t l = offset.length(); + + // don't apply the pull in for tiny holes + if (l > (pull_in * 2.0)) { + real_t fract = (l - pull_in) / l; + offset *= fract; + _pts_local.set(n, center + offset); + } + } + + _update_aabb(); +} + +void Portal::_sort_verts_clockwise(bool portal_plane_convention, Vector &r_verts) { + // cannot sort less than 3 verts + if (r_verts.size() < 3) { + return; + } + + // assume first 3 points determine the desired normal, if these first 3 points are garbage, + // the routine will not work. + Plane portal_plane; + if (portal_plane_convention) { + portal_plane = Plane(r_verts[0], r_verts[2], r_verts[1]); + } else { + portal_plane = Plane(r_verts[0], r_verts[1], r_verts[2]); + } + + const Vector3 &portal_normal = portal_plane.normal; + + // find centroid + int num_points = r_verts.size(); + _pt_center_world = Vector3(0, 0, 0); + + for (int n = 0; n < num_points; n++) { + _pt_center_world += r_verts[n]; + } + _pt_center_world /= num_points; + ///////////////////////////////////////// + + // now algorithm + for (int n = 0; n < num_points - 2; n++) { + Vector3 a = r_verts[n] - _pt_center_world; + a.normalize(); + + Plane p = Plane(r_verts[n], _pt_center_world, _pt_center_world + portal_normal); + + double smallest_angle = -1; + int smallest = -1; + + for (int m = n + 1; m < num_points; m++) { + if (p.distance_to(r_verts[m]) > 0.0) { + Vector3 b = r_verts[m] - _pt_center_world; + b.normalize(); + + double angle = a.dot(b); + + if (angle > smallest_angle) { + smallest_angle = angle; + smallest = m; + } + } // which side + + } // for m + + // swap smallest and n+1 vert + if (smallest != -1) { + Vector3 temp = r_verts[smallest]; + r_verts.set(smallest, r_verts[n + 1]); + r_verts.set(n + 1, temp); + } + } // for n + + // the vertices are now sorted, but may be in the opposite order to that wanted. + // we detect this by calculating the normal of the poly, then flipping the order if the normal is pointing + // the wrong way. + Plane plane = Plane(r_verts[0], r_verts[1], r_verts[2]); + + if (portal_normal.dot(plane.normal) < 0.0f) { + // reverse winding order of verts + r_verts.invert(); + } +} + +Plane Portal::_plane_from_points_newell(const Vector &p_pts) { + int num_points = p_pts.size(); + + if (num_points < 3) { + return Plane(); + } + + Vector3 normal; + Vector3 center; + + for (int i = 0; i < num_points; i++) { + int j = (i + 1) % num_points; + + const Vector3 &pi = p_pts[i]; + const Vector3 &pj = p_pts[j]; + + center += pi; + + normal.x += (((pi.z) + (pj.z)) * ((pj.y) - (pi.y))); + normal.y += (((pi.x) + (pj.x)) * ((pj.z) - (pi.z))); + normal.z += (((pi.y) + (pj.y)) * ((pj.x) - (pi.x))); + } + + normal.normalize(); + center /= num_points; + + _pt_center_world = center; + + // point and normal + return Plane(center, normal); +} + +void Portal::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_portal_active", "p_active"), &Portal::set_portal_active); + ClassDB::bind_method(D_METHOD("get_portal_active"), &Portal::get_portal_active); + + ClassDB::bind_method(D_METHOD("set_two_way", "p_two_way"), &Portal::set_two_way); + ClassDB::bind_method(D_METHOD("is_two_way"), &Portal::is_two_way); + + ClassDB::bind_method(D_METHOD("set_use_default_margin", "p_use"), &Portal::set_use_default_margin); + ClassDB::bind_method(D_METHOD("get_use_default_margin"), &Portal::get_use_default_margin); + + ClassDB::bind_method(D_METHOD("set_portal_margin", "p_margin"), &Portal::set_portal_margin); + ClassDB::bind_method(D_METHOD("get_portal_margin"), &Portal::get_portal_margin); + + ClassDB::bind_method(D_METHOD("set_linked_room", "p_room"), &Portal::set_linked_room); + ClassDB::bind_method(D_METHOD("get_linked_room"), &Portal::get_linked_room); + + ClassDB::bind_method(D_METHOD("set_points", "points"), &Portal::set_points); + ClassDB::bind_method(D_METHOD("get_points"), &Portal::get_points); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "portal_active"), "set_portal_active", "get_portal_active"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "two_way"), "set_two_way", "is_two_way"); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "linked_room", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Room"), "set_linked_room", "get_linked_room"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_default_margin"), "set_use_default_margin", "get_use_default_margin"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "portal_margin", PROPERTY_HINT_RANGE, "0.0,10.0,0.01"), "set_portal_margin", "get_portal_margin"); + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR2_ARRAY, "points"), "set_points", "get_points"); +} diff --git a/scene/3d/portal.h b/scene/3d/portal.h new file mode 100644 index 00000000000..5bc9110408b --- /dev/null +++ b/scene/3d/portal.h @@ -0,0 +1,174 @@ +/*************************************************************************/ +/* portal.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_H +#define PORTAL_H + +#include "core/rid.h" +#include "spatial.h" + +class RoomManager; +class MeshInstance; + +class Portal : public Spatial { + GDCLASS(Portal, Spatial); + + RID _portal_rid; + + friend class RoomManager; + friend class PortalGizmoPlugin; + +public: + // ui interface .. will have no effect after room conversion + void set_linked_room(const NodePath &link_path); + NodePath get_linked_room() const; + + // open and close doors + void set_portal_active(bool p_active); + bool get_portal_active() const; + + // whether the portal can be seen through in both directions or not + void set_two_way(bool p_two_way) { + _settings_two_way = p_two_way; + _changed(); + } + bool is_two_way() const { return _settings_two_way; } + + void clear(); + + // whether to use the room manager default + void set_use_default_margin(bool p_use); + bool get_use_default_margin() const; + + // custom portal margin (per portal) .. only valid if use_default_margin is off + void set_portal_margin(real_t p_margin); + real_t get_portal_margin() const; + + // either the default margin or the custom portal margin, depending on the setting + real_t get_active_portal_margin() const; + + // the raw points are used for the IDE Inspector, and also to allow the user + // to edit the geometry of the portal at runtime (they can also just change the portal node transform) + void set_points(const PoolVector &p_points); + PoolVector get_points() const; + + Portal(); + ~Portal(); + + // whether the convention is that the normal of the portal points outward (false) or inward (true) + // normally I'd recommend portal normal faces outward. But you may make a booboo, so this can work + // with either convention. + static bool _portal_plane_convention; + +private: + // updates world coords when the tranform changes, and updates the visual server + void portal_update(); + + void set_linked_room_internal(const NodePath &link_path); + bool try_set_unique_name(const String &p_name); + bool is_portal_internal(int p_room_outer) const { return _internal && (_linkedroom_ID[0] != p_room_outer); } + + bool create_from_mesh_instance(const MeshInstance *p_mi); + void flip(); + void _sanitize_points(); + void _update_aabb(); + Vector3 _vec2to3(const Vector2 &p_pt) const { return Vector3(p_pt.x, p_pt.y, 0.0); } + void _sort_verts_clockwise(bool portal_plane_convention, Vector &r_verts); + Plane _plane_from_points_newell(const Vector &p_pts); + void resolve_links(const RID &p_from_room_rid); + void _changed(); + + // nodepath to the room this outgoing portal leads to + NodePath _settings_path_linkedroom; + + // portal can be turned on and off at runtime, for e.g. + // opening and closing a door + bool _settings_active; + + // user can choose not to include the portal in the convex hull of the room + // during conversion + bool _settings_include_in_bound; + + // portals can be seen through one way or two way + bool _settings_two_way; + + // room from and to, ID in the room manager + int _linkedroom_ID[2]; + + // whether the portal is from a room within a room + bool _internal; + + // normal determined by winding order + Vector _pts_world; + + // points in local space of the plane, + // not necessary in correct winding order + // (as they can be edited by the user) + // Note: these are saved by the IDE + PoolVector _pts_local_raw; + + // sanitized + Vector _pts_local; + AABB _aabb_local; + + // center of the world points + Vector3 _pt_center_world; + + // portal plane in world space, always pointing OUTWARD from the source room + Plane _plane; + + // extension margin + real_t _margin; + real_t _default_margin; + bool _use_default_margin; + + // for editing +#ifdef TOOLS_ENABLED + ObjectID _room_manager_godot_ID; + + // warnings + bool _warning_outside_room_aabb = false; + bool _warning_facing_wrong_way = false; +#endif + + // this is read from the gizmo + static bool _settings_gizmo_show_margins; + +public: + // makes sure portals are not converted more than once per + // call to rooms_convert + int _conversion_tick = -1; + +protected: + static void _bind_methods(); + void _notification(int p_what); +}; + +#endif diff --git a/scene/3d/room.cpp b/scene/3d/room.cpp new file mode 100644 index 00000000000..815aebead35 --- /dev/null +++ b/scene/3d/room.cpp @@ -0,0 +1,226 @@ +/*************************************************************************/ +/* room.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.h" + +#include "portal.h" +#include "room_manager.h" +#include "servers/visual_server.h" + +void Room::SimplifyInfo::set_simplify(real_t p_value, real_t p_room_size) { + _plane_simplify = CLAMP(p_value, 0.0, 1.0); + + // just for reference in case we later want to use degrees... + // _plane_simplify_dot = Math::cos(Math::deg2rad(_plane_simplify_degrees)); + + // _plane_simplify_dot = _plane_simplify; + // _plane_simplify_dot *= _plane_simplify_dot; + // _plane_simplify_dot = 1.0 - _plane_simplify_dot; + + // distance based on size of room + // _plane_simplify_dist = p_room_size * 0.1 * _plane_simplify; + // _plane_simplify_dist = MAX(_plane_simplify_dist, 0.08); + + // test fix + _plane_simplify_dot = 0.99; + _plane_simplify_dist = 0.08; + + // print_verbose("plane simplify dot : " + String(Variant(_plane_simplify_dot))); + // print_verbose("plane simplify dist : " + String(Variant(_plane_simplify_dist))); +} + +bool Room::SimplifyInfo::add_plane_if_unique(LocalVector &r_planes, const Plane &p) const { + for (int n = 0; n < r_planes.size(); n++) { + const Plane &o = r_planes[n]; + + // this is a fudge factor for how close planes can be to be considered the same ... + // to prevent ridiculous amounts of planes + const real_t d = _plane_simplify_dist; // 0.08f + + if (Math::abs(p.d - o.d) > d) { + continue; + } + + real_t dot = p.normal.dot(o.normal); + if (dot < _plane_simplify_dot) // 0.98f + { + continue; + } + + // match! + return false; + } + + r_planes.push_back(p); + return true; +} + +void Room::clear() { + _room_ID = -1; + _planes.clear(); + _preliminary_planes.clear(); + _roomgroups.clear(); + _portals.clear(); + _bound_mesh_data.edges.clear(); + _bound_mesh_data.faces.clear(); + _bound_mesh_data.vertices.clear(); + _aabb = AABB(); +#ifdef TOOLS_ENABLED + _gizmo_overlap_zones.clear(); +#endif +} + +Room::Room() { + _room_rid = VisualServer::get_singleton()->room_create(); +} + +Room::~Room() { + if (_room_rid != RID()) { + VisualServer::get_singleton()->free(_room_rid); + } +} + +void Room::set_room_simplify(real_t p_value) { + _simplify_info.set_simplify(p_value, _aabb.get_longest_axis_size()); +} + +void Room::set_use_default_simplify(bool p_use) { + _use_default_simplify = p_use; +} + +void Room::set_points(const PoolVector &p_points) { + _bound_pts = p_points; + +#ifdef TOOLS_ENABLED + if (p_points.size()) { + _changed(true); + } +#endif +} + +PoolVector Room::get_points() const { + return _bound_pts; +} + +PoolVector Room::generate_points() { + PoolVector pts_returned; +#ifdef TOOLS_ENABLED + // do a rooms convert to make sure the planes are up to date + RoomManager *rm = RoomManager::active_room_manager; + if (rm) { + rm->rooms_convert(); + } + + if (!_planes.size()) { + return pts_returned; + } + + // scale an epsilon using 10.0 for a normal sized room + real_t scaled_epsilon = _aabb.get_longest_axis_size() / 10.0; + scaled_epsilon = MAX(scaled_epsilon * 0.01, 0.001); + + LocalVector pts; + pts = Geometry::compute_convex_mesh_points(&_planes[0], _planes.size(), scaled_epsilon); + + // eliminate duplicates + for (int n = 0; n < pts.size(); n++) { + const Vector3 &a = pts[n]; + + for (int m = n + 1; m < pts.size(); m++) { + const Vector3 &b = pts[m]; + if (a.is_equal_approx(b, scaled_epsilon)) { + // remove b + pts.remove_unordered(m); + m--; // repeat m as the new m is the old last + } + } + } + + // convert vector to poolvector + pts_returned.resize(pts.size()); + + Transform tr = get_global_transform(); + tr.affine_invert(); + + for (int n = 0; n < pts.size(); n++) { + // the points should be saved in LOCAL space, + // so that if we move the room afterwards, the bound points + // will also move in relation to the room. + pts_returned.set(n, tr.xform(pts[n])); + } + +#endif + return pts_returned; +} + +// extra editor links to the room manager to allow unloading +// on change, or re-converting +void Room::_changed(bool p_regenerate_bounds) { +#ifdef TOOLS_ENABLED + RoomManager *rm = RoomManager::active_room_manager; + if (!rm) { + return; + } + + if (p_regenerate_bounds) { + rm->_room_regenerate_bound(this); + } + rm->_rooms_changed(); +#endif +} + +void Room::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_WORLD: { + ERR_FAIL_COND(get_world().is_null()); + VisualServer::get_singleton()->room_set_scenario(_room_rid, get_world()->get_scenario()); + } break; + case NOTIFICATION_EXIT_WORLD: { + VisualServer::get_singleton()->room_set_scenario(_room_rid, RID()); + } break; + } +} + +void Room::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_use_default_simplify", "p_use"), &Room::set_use_default_simplify); + ClassDB::bind_method(D_METHOD("get_use_default_simplify"), &Room::get_use_default_simplify); + + ClassDB::bind_method(D_METHOD("set_room_simplify", "p_value"), &Room::set_room_simplify); + ClassDB::bind_method(D_METHOD("get_room_simplify"), &Room::get_room_simplify); + + ClassDB::bind_method(D_METHOD("set_points", "points"), &Room::set_points); + ClassDB::bind_method(D_METHOD("get_points"), &Room::get_points); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_default_simplify"), "set_use_default_simplify", "get_use_default_simplify"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "room_simplify", PROPERTY_HINT_RANGE, "0.0,1.0,0.005"), "set_room_simplify", "get_room_simplify"); + + ADD_GROUP("Bound", ""); + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR3_ARRAY, "points"), "set_points", "get_points"); +} diff --git a/scene/3d/room.h b/scene/3d/room.h new file mode 100644 index 00000000000..c553668bf8f --- /dev/null +++ b/scene/3d/room.h @@ -0,0 +1,131 @@ +/*************************************************************************/ +/* room.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef ROOM_H +#define ROOM_H + +#include "core/local_vector.h" +#include "core/rid.h" +#include "spatial.h" + +class Portal; + +class Room : public Spatial { + GDCLASS(Room, Spatial); + + friend class RoomManager; + friend class RoomGroup; + friend class Portal; + friend class RoomGizmoPlugin; + friend class RoomEditorPlugin; + + RID _room_rid; + +public: + struct SimplifyInfo { + SimplifyInfo() { set_simplify(0.5); } + void set_simplify(real_t p_value, real_t p_room_size = 0.0); + bool add_plane_if_unique(LocalVector &r_planes, const Plane &p) const; + real_t _plane_simplify = 0.5; + real_t _plane_simplify_dot = 0.98; + real_t _plane_simplify_dist = 0.08; + }; + + Room(); + ~Room(); + + void set_room_simplify(real_t p_value); + real_t get_room_simplify() const { return _simplify_info._plane_simplify; } + + // whether to use the room manager default + void set_use_default_simplify(bool p_use); + bool get_use_default_simplify() const { return _use_default_simplify; } + + void set_points(const PoolVector &p_points); + PoolVector get_points() const; + + // editor only + PoolVector generate_points(); + +private: + void clear(); + void _changed(bool p_regenerate_bounds = false); + + // planes forming convex hull of room + LocalVector _planes; + + // preliminary planes are created during the first conversion pass, + // they do not include the portals, and are used for identifying auto + // linkage of rooms by portals + LocalVector _preliminary_planes; + + Geometry::MeshData _bound_mesh_data; + AABB _aabb; + + // editable points making up the bound + PoolVector _bound_pts; + +#ifdef TOOLS_ENABLED + // to help with editing, when converting, we can generate overlap zones + // that occur between rooms. Ideally these should not occur, as rooms + // should be convex and non-overlapping. But if they do occur, they should + // be minimized. + Vector _gizmo_overlap_zones; +#endif + + // makes sure lrooms are not converted more than once per + // call to rooms_convert + int _conversion_tick = -1; + + // room ID during conversion, used for matching portals links to rooms + int _room_ID; + + // room priority allows rooms to be placed inside other rooms, + // such as a house on a landscape room. + // If the camera is inside more than one room, the higher priority room + // will *win* (e.g. house, rather than landscape) + int _room_priority = 0; + + // a room may be in one or several roomgroups + LocalVector _roomgroups; + + // list of portal ids from or to this room, just used in conversion to determine room bound + LocalVector _portals; + + // each room now stores simplification data + SimplifyInfo _simplify_info; + bool _use_default_simplify = true; + +protected: + static void _bind_methods(); + void _notification(int p_what); +}; + +#endif diff --git a/scene/3d/room_group.cpp b/scene/3d/room_group.cpp new file mode 100644 index 00000000000..e2b2184e1dd --- /dev/null +++ b/scene/3d/room_group.cpp @@ -0,0 +1,84 @@ +/*************************************************************************/ +/* room_group.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_group.h" + +#include "room.h" +#include "room_manager.h" + +void RoomGroup::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_roomgroup_priority", "p_priority"), &RoomGroup::set_roomgroup_priority); + ClassDB::bind_method(D_METHOD("get_roomgroup_priority"), &RoomGroup::get_roomgroup_priority); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "roomgroup_priority", PROPERTY_HINT_RANGE, "-16,16,1", PROPERTY_USAGE_DEFAULT), "set_roomgroup_priority", "get_roomgroup_priority"); +} + +RoomGroup::RoomGroup() { + _room_group_rid = VisualServer::get_singleton()->roomgroup_create(); +} + +RoomGroup::~RoomGroup() { + if (_room_group_rid != RID()) { + VisualServer::get_singleton()->free(_room_group_rid); + } +} + +void RoomGroup::clear() { + _roomgroup_ID = -1; +} + +void RoomGroup::add_room(Room *p_room) { + VisualServer::get_singleton()->roomgroup_add_room(_room_group_rid, p_room->_room_rid); +} + +// extra editor links to the room manager to allow unloading +// on change, or re-converting +void RoomGroup::_changed() { +#ifdef TOOLS_ENABLED + RoomManager *rm = RoomManager::active_room_manager; + if (!rm) { + return; + } + + rm->_rooms_changed(); +#endif +} + +void RoomGroup::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_WORLD: { + ERR_FAIL_COND(get_world().is_null()); + VisualServer::get_singleton()->roomgroup_set_scenario(_room_group_rid, get_world()->get_scenario()); + } break; + case NOTIFICATION_EXIT_WORLD: { + VisualServer::get_singleton()->roomgroup_set_scenario(_room_group_rid, RID()); + } break; + } +} diff --git a/scene/3d/room_group.h b/scene/3d/room_group.h new file mode 100644 index 00000000000..6bcc51a7a0d --- /dev/null +++ b/scene/3d/room_group.h @@ -0,0 +1,79 @@ +/*************************************************************************/ +/* room_group.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef ROOM_GROUP_H +#define ROOM_GROUP_H + +#include "core/rid.h" +#include "spatial.h" + +class Room; + +class RoomGroup : public Spatial { + GDCLASS(RoomGroup, Spatial); + + friend class RoomManager; + + RID _room_group_rid; + +public: + RoomGroup(); + ~RoomGroup(); + + void add_room(Room *p_room); + + void set_roomgroup_priority(int p_priority) { + _settings_priority = p_priority; + _changed(); + } + int get_roomgroup_priority() const { return _settings_priority; } + +private: + void clear(); + void _changed(); + + // roomgroup ID during conversion + int _roomgroup_ID; + + // the roomgroup can be used to set a number of rooms to a different priority + // to allow a group of rooms WITHIN another room / rooms. + // This is for e.g. buildings on landscape. + int _settings_priority = 0; + + // makes sure lrooms are not converted more than once per + // call to rooms_convert + int _conversion_tick = -1; + +protected: + static void _bind_methods(); + void _notification(int p_what); +}; + +#endif diff --git a/scene/3d/room_manager.cpp b/scene/3d/room_manager.cpp new file mode 100644 index 00000000000..3f95d32c454 --- /dev/null +++ b/scene/3d/room_manager.cpp @@ -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 = get_world(); + RID scenario = world->get_scenario(); + + if (_godot_preview_camera_ID != (ObjectID)-1) { + Camera *cam = Object::cast_to(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 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 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 = get_world(); + RID scenario = world->get_scenario(); + + List cameras; + world->get_camera_list(&cameras); + + Vector 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(_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(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(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(_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(_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(_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 portals; + LocalVector 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 &p_roomgroups, const LocalVector &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 room_pts; + + for (int n = 0; n < p_room->get_child_count(); n++) { + Spatial *child = Object::cast_to(p_room->get_child(n)); + + if (child) { + if (_name_starts_with(child, "GPortal", true) || _node_is_type(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 &p_roomgroups, const LocalVector &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 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 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 &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 &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(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 &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(p_node)) { + // end the recursion here + return nullptr; + } + + // is this a roomgroup? + if (_name_starts_with(p_node, "RoomGroup") || _node_is_type(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 &r_portals, LocalVector &r_roomgroups, int p_roomgroup) { + // is this a room? + if (_name_starts_with(p_node, "Room") || _node_is_type(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(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(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 &r_roomgroups) { + String string_full_name = p_node->get_name(); + + // is it already a roomgroup? + RoomGroup *roomgroup = Object::cast_to(p_node); + + // if not already a RoomGroup, convert the node and move all children + if (!roomgroup) { + // create a RoomGroup + roomgroup = _change_node_type(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 &r_portals, const LocalVector &p_roomgroups, int p_roomgroup) { + String string_full_name = p_node->get_name(); + + // is it already an lroom? + Room *room = Object::cast_to(p_node); + + // if not already a Room, convert the node and move all children + if (!room) { + // create a Room + room = _change_node_type(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 &r_portals) { + MeshInstance *mi = Object::cast_to(p_node); + if ((mi && _name_starts_with(mi, "Portal", true)) || _node_is_type(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(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 &r_room_pts) { + bool ignore = false; + VisualInstance *vi = Object::cast_to(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(p_node); + if (light) { + convert_log("\tfound Light " + light->get_name()); + + Vector 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(p_node); + + if (gi) { + MeshInstance *mi = Object::cast_to(p_node); + if (mi) { + convert_log("\tfound MeshInst " + mi->get_name()); + + Vector 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 dummy_pts; + // VisualServer::get_singleton()->room_add_instance(p_room->_room_rid, gi->get_instance(), gi->get_transformed_aabb(), dummy_pts); + Vector 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(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(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 &p_portals) { + MeshInstance *mi = Object::cast_to(p_node); + if (!mi) { + return false; + } + + Vector 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 &p_room_pts, const LocalVector &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 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 &p_portals) { + Vector 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 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 &r_planes, int p_num_portal_planes) { + if (!r_planes.size()) { + return; + } + + Vector 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 &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 &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 &portals) { + String string_full_name = p_node->get_name(); + convert_log("convert_portal : " + string_full_name); + + Portal *portal = Object::cast_to(p_node); + + // if not a gportal already, convert the node type + if (!portal) { + portal = _change_node_type(p_node, "G", false); + portal->create_from_mesh_instance(Object::cast_to(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 &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(p_gi); + if (shape) { + Array arr = shape->get_meshes(); + if (!arr.size()) { + return false; + } + + Ref 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 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 &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 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 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(_settings_path_preview_camera); + + if (camera) { + _godot_preview_camera_ID = camera->get_instance_id(); + return true; + } + _godot_preview_camera_ID = -1; + return false; +} + +template +NODE_TYPE *RoomManager::_resolve_path(NodePath p_path) const { + if (has_node(p_path)) { + NODE_TYPE *node = Object::cast_to(get_node(p_path)); + if (node) { + return node; + } else { + WARN_PRINT("node is incorrect type"); + } + } + + return nullptr; +} + +template +bool RoomManager::_node_is_type(Node *p_node) const { + NODE_TYPE *node = Object::cast_to(p_node); + return node != nullptr; +} + +template +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(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 &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(p_node); + + if (portal) { + portal->flip(); + } + + for (int n = 0; n < p_node->get_child_count(); n++) { + Spatial *child = Object::cast_to(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 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 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(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 &r_list) { + MeshInstance *mi = Object::cast_to(p_node); + + if (mi) { + // only interested in static portal mode meshes + VisualInstance *vi = Object::cast_to(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(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(p_node->get_child(n)); + if (child) { + _list_mergeable_mesh_instances(child, r_list); + } + } +} diff --git a/scene/3d/room_manager.h b/scene/3d/room_manager.h new file mode 100644 index 00000000000..850d7889c58 --- /dev/null +++ b/scene/3d/room_manager.h @@ -0,0 +1,273 @@ +/*************************************************************************/ +/* room_manager.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef ROOM_MANAGER_H +#define ROOM_MANAGER_H + +#include "core/local_vector.h" +#include "room.h" +#include "spatial.h" + +class Portal; +class RoomGroup; +class MeshInstance; +class GeometryInstance; + +#define GODOT_PORTAL_DELINEATOR String("_") +#define GODOT_PORTAL_WILDCARD String("*") + +class RoomManager : public Spatial { + GDCLASS(RoomManager, Spatial); + +public: + enum PVSMode { + PVS_MODE_DISABLED, + PVS_MODE_PARTIAL, + PVS_MODE_FULL, + }; + + void set_roomlist_path(const NodePath &p_path) { + _settings_path_roomlist = p_path; + } + + NodePath get_roomlist_path() const { + return _settings_path_roomlist; + } + + void set_preview_camera_path(const NodePath &p_path); + + NodePath get_preview_camera_path() const { + return _settings_path_preview_camera; + } + + void rooms_set_active(bool p_active); + bool rooms_get_active() const; + + void set_show_debug(bool p_show); + bool get_show_debug() const; + + void set_show_margins(bool p_show); + bool get_show_margins() const; + + void set_debug_sprawl(bool p_enable); + bool get_debug_sprawl() const; + + void set_merge_meshes(bool p_enable); + bool get_merge_meshes() const; + + void set_remove_danglers(bool p_enable); + bool get_remove_danglers() const; + + void set_room_simplify(real_t p_value); + real_t get_room_simplify() const; + + void set_default_portal_margin(real_t p_dist); + real_t get_default_portal_margin() const; + + void set_overlap_warning_threshold(int p_value) { _overlap_warning_threshold = p_value; } + int get_overlap_warning_threshold() const { return (int)_overlap_warning_threshold; } + + void set_portal_depth_limit(int p_limit); + int get_portal_depth_limit() const { return _portal_depth_limit; } + + void set_flip_portal_meshes(bool p_flip); + bool get_flip_portal_meshes() const; + + void set_pvs_mode(PVSMode p_mode); + PVSMode get_pvs_mode() const; + + void set_pvs_filename(String p_filename); + String get_pvs_filename() const; + + void set_use_secondary_pvs(bool p_enable) { _settings_use_secondary_pvs = p_enable; } + bool get_use_secondary_pvs() const { return _settings_use_secondary_pvs; } + + void set_use_signals(bool p_enable) { _settings_use_signals = p_enable; } + bool get_use_signals() const { return _settings_use_signals; } + + void set_gameplay_monitor_enabled(bool p_enable) { _settings_gameplay_monitor_enabled = p_enable; } + bool get_gameplay_monitor_enabled() const { return _settings_gameplay_monitor_enabled; } + + void rooms_convert(); + void rooms_clear(); + void rooms_flip_portals(); + + // for internal use in the editor.. + // either we can clear the rooms and unload, + // or reconvert. + void _rooms_changed(); + +#ifdef TOOLS_ENABLED + // for a preview, we allow the editor to change the bound + bool _room_regenerate_bound(Room *p_room); +#endif + + RoomManager(); + ~RoomManager(); + + // an easy way of grabbing the active room manager for tools purposes +#ifdef TOOLS_ENABLED + static RoomManager *active_room_manager; +#endif + +private: + // funcs + bool resolve_preview_camera_path(); + void _preview_camera_update(); + + // conversion + // FIRST PASS + void _convert_rooms_recursive(Spatial *p_node, LocalVector &r_portals, LocalVector &r_roomgroups, int p_roomgroup = -1); + void _convert_room(Spatial *p_node, LocalVector &r_portals, const LocalVector &p_roomgroups, int p_roomgroup); + int _convert_roomgroup(Spatial *p_node, LocalVector &r_roomgroups); + + void _find_portals_recursive(Spatial *p_node, Room *p_room, LocalVector &r_portals); + void _convert_portal(Room *p_room, Spatial *p_node, LocalVector &portals); + + // SECOND PASS + void _second_pass_portals(Spatial *p_roomlist, LocalVector &r_portals); + void _second_pass_rooms(const LocalVector &p_roomgroups, const LocalVector &p_portals); + void _second_pass_room(Room *p_room, const LocalVector &p_roomgroups, const LocalVector &p_portals); + + bool _convert_manual_bound(Room *p_room, Spatial *p_node, const LocalVector &p_portals); + void _check_portal_for_warnings(Portal *p_portal, const AABB &p_room_aabb_without_portals); + void _find_statics_recursive(Room *p_room, Spatial *p_node, Vector &r_room_pts); + bool _convert_room_hull_preliminary(Room *p_room, const Vector &p_room_pts, const LocalVector &p_portals); + + bool _bound_findpoints_mesh_instance(MeshInstance *p_mi, Vector &r_room_pts, AABB &r_aabb); + bool _bound_findpoints_geom_instance(GeometryInstance *p_gi, Vector &r_room_pts, AABB &r_aabb); + + // THIRD PASS + void _third_pass_portals(Spatial *p_roomlist, LocalVector &r_portals); + void _third_pass_rooms(const LocalVector &p_portals); + + bool _convert_room_hull_final(Room *p_room, const LocalVector &p_portals); + void _build_simplified_bound(const Room *p_room, Geometry::MeshData &r_md, LocalVector &r_planes, int p_num_portal_planes); + + // misc + bool _add_plane_if_unique(const Room *p_room, LocalVector &r_planes, const Plane &p); + void _update_portal_margins(Spatial *p_node, real_t p_margin); + Node *_check_roomlist_validity_recursive(Node *p_node); + void _cleanup_after_conversion(); + Error _build_room_convex_hull(const Room *p_room, const Vector &p_points, Geometry::MeshData &r_mesh); +#ifdef TOOLS_ENABLED + void _generate_room_overlap_zones(); +#endif + + // merging + void _merge_meshes_in_room(Room *p_room); + void _list_mergeable_mesh_instances(Spatial *p_node, LocalVector &r_list); + void _merge_log(String p_string) { debug_print_line(p_string); } + bool _remove_redundant_dangling_nodes(Spatial *p_node); + + // helper funcs + bool _name_starts_with(const Node *p_node, String p_search_string, bool p_allow_no_delineator = false); + void _check_for_misnamed_node(const Node *p_node, String p_start_string); + template + NODE_TYPE *_resolve_path(NodePath p_path) const; + template + bool _node_is_type(Node *p_node) const; + template + T *_change_node_type(Spatial *p_node, String p_prefix, bool p_delete = true); + void _update_gizmos_recursive(Node *p_node); + void _set_owner_recursive(Node *p_node, Node *p_owner); + void _flip_portals_recursive(Spatial *p_node); + Error _build_convex_hull(const Vector &p_points, Geometry::MeshData &r_mesh, real_t p_epsilon = 3.0 * UNIT_EPSILON); + + // output strings during conversion process + void convert_log(String p_string, int p_priority = 0) { debug_print_line(p_string, p_priority); } + + // only prints when user has set 'debug' in the room manager inspector + // also does not show in non editor builds + void debug_print_line(String p_string, int p_priority = 0); + +public: + static String _find_name_after(Node *p_node, String p_string_start); + static void show_warning(const String &p_string, const String &p_extra_string = "", bool p_alert = true); + +private: + // accessible from UI + NodePath _settings_path_roomlist; + NodePath _settings_path_preview_camera; + + // resolved node + Spatial *_roomlist = nullptr; + bool _warning_misnamed_nodes_detected = false; + bool _warning_portal_link_room_not_found = false; + bool _warning_portal_autolink_failed = false; + bool _warning_room_overlap_detected = false; + + // merge suitable meshes in rooms? + bool _settings_merge_meshes = false; + + // remove redundant childless spatials after merging + bool _settings_remove_danglers = true; + + bool _active = true; + + // portals, room hulls etc + bool _show_debug = true; + bool _debug_sprawl = false; + + // pvs + PVSMode _pvs_mode = PVS_MODE_PARTIAL; + String _pvs_filename; + bool _settings_use_secondary_pvs = false; + bool _settings_use_signals = true; + bool _settings_gameplay_monitor_enabled = false; + + int _conversion_tick = 0; + + // just used during conversion, could be invalidated + // later by user deleting rooms etc. + LocalVector _rooms; + + // advanced params + real_t _default_portal_margin = 1.0; + real_t _overlap_warning_threshold = 1.0; + Room::SimplifyInfo _room_simplify_info; + int _portal_depth_limit = 16; + + // debug override camera + ObjectID _godot_preview_camera_ID = -1; + // local version of the godot camera frustum, + // to prevent updating the visual server (and causing + // a screen refresh) where not necessary. + Vector3 _godot_camera_pos; + Vector _godot_camera_planes; + +protected: + static void _bind_methods(); + void _notification(int p_what); +}; + +VARIANT_ENUM_CAST(RoomManager::PVSMode); + +#endif diff --git a/scene/3d/spatial.cpp b/scene/3d/spatial.cpp index 3c1c36d58be..0ed14a83661 100644 --- a/scene/3d/spatial.cpp +++ b/scene/3d/spatial.cpp @@ -35,6 +35,7 @@ #include "scene/main/scene_tree.h" #include "scene/main/viewport.h" #include "scene/scene_string_names.h" +#include "servers/visual_server_callbacks.h" /* @@ -119,6 +120,25 @@ void Spatial::_propagate_transform_changed(Spatial *p_origin) { data.children_lock--; } +void Spatial::notification_callback(int p_message_type) { + switch (p_message_type) { + default: + break; + case VisualServerCallbacks::CALLBACK_NOTIFICATION_ENTER_GAMEPLAY: { + notification(NOTIFICATION_ENTER_GAMEPLAY); + } break; + case VisualServerCallbacks::CALLBACK_NOTIFICATION_EXIT_GAMEPLAY: { + notification(NOTIFICATION_EXIT_GAMEPLAY); + } break; + case VisualServerCallbacks::CALLBACK_SIGNAL_ENTER_GAMEPLAY: { + emit_signal("gameplay_entered"); + } break; + case VisualServerCallbacks::CALLBACK_SIGNAL_EXIT_GAMEPLAY: { + emit_signal("gameplay_exited"); + } break; + } +} + void Spatial::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -759,6 +779,8 @@ void Spatial::_bind_methods() { BIND_CONSTANT(NOTIFICATION_ENTER_WORLD); BIND_CONSTANT(NOTIFICATION_EXIT_WORLD); BIND_CONSTANT(NOTIFICATION_VISIBILITY_CHANGED); + BIND_CONSTANT(NOTIFICATION_ENTER_GAMEPLAY); + BIND_CONSTANT(NOTIFICATION_EXIT_GAMEPLAY); //ADD_PROPERTY( PropertyInfo(Variant::TRANSFORM,"transform/global",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR ), "set_global_transform", "get_global_transform") ; ADD_GROUP("Transform", ""); @@ -774,6 +796,8 @@ void Spatial::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "gizmo", PROPERTY_HINT_RESOURCE_TYPE, "SpatialGizmo", 0), "set_gizmo", "get_gizmo"); ADD_SIGNAL(MethodInfo("visibility_changed")); + ADD_SIGNAL(MethodInfo("gameplay_entered")); + ADD_SIGNAL(MethodInfo("gameplay_exited")); } Spatial::Spatial() : diff --git a/scene/3d/spatial.h b/scene/3d/spatial.h index 7b18b69ef81..8562ce658da 100644 --- a/scene/3d/spatial.h +++ b/scene/3d/spatial.h @@ -139,8 +139,11 @@ public: NOTIFICATION_EXIT_WORLD = 42, NOTIFICATION_VISIBILITY_CHANGED = 43, NOTIFICATION_LOCAL_TRANSFORM_CHANGED = 44, + NOTIFICATION_ENTER_GAMEPLAY = 45, + NOTIFICATION_EXIT_GAMEPLAY = 46, }; + virtual void notification_callback(int p_message_type); Spatial *get_parent_spatial() const; Ref get_world() const; diff --git a/scene/3d/visibility_notifier.cpp b/scene/3d/visibility_notifier.cpp index 6317e655f66..5b0448f55dd 100644 --- a/scene/3d/visibility_notifier.cpp +++ b/scene/3d/visibility_notifier.cpp @@ -41,7 +41,13 @@ void VisibilityNotifier::_enter_camera(Camera *p_camera) { ERR_FAIL_COND(cameras.has(p_camera)); cameras.insert(p_camera); - if (cameras.size() == 1) { + + bool in_gameplay = _in_gameplay; + if (!Engine::get_singleton()->are_portals_active()) { + in_gameplay = true; + } + + if ((cameras.size() == 1) && in_gameplay) { emit_signal(SceneStringNames::get_singleton()->screen_entered); _screen_enter(); } @@ -53,8 +59,13 @@ void VisibilityNotifier::_exit_camera(Camera *p_camera) { ERR_FAIL_COND(!cameras.has(p_camera)); cameras.erase(p_camera); + bool in_gameplay = _in_gameplay; + if (!Engine::get_singleton()->are_portals_active()) { + in_gameplay = true; + } + emit_signal(SceneStringNames::get_singleton()->camera_exited, p_camera); - if (cameras.size() == 0) { + if ((cameras.size() == 0) && (in_gameplay)) { emit_signal(SceneStringNames::get_singleton()->screen_exited); _screen_exit(); @@ -79,25 +90,85 @@ AABB VisibilityNotifier::get_aabb() const { return aabb; } +void VisibilityNotifier::_refresh_portal_mode() { + // only create in the visual server if we are roaming. + // All other cases don't require a visual server rep. + // Global and ignore are the same (existing client side functionality only). + // Static and dynamic require only a one off creation at conversion. + if (get_portal_mode() == PORTAL_MODE_ROAMING) { + if (is_inside_world()) { + if (_cull_instance_rid == RID()) { + _cull_instance_rid = VisualServer::get_singleton()->ghost_create(); + } + + if (is_inside_world() && get_world().is_valid() && get_world()->get_scenario().is_valid() && is_inside_tree()) { + AABB world_aabb = get_global_transform().xform(aabb); + VisualServer::get_singleton()->ghost_set_scenario(_cull_instance_rid, get_world()->get_scenario(), get_instance_id(), world_aabb); + } + } else { + if (_cull_instance_rid != RID()) { + VisualServer::get_singleton()->free(_cull_instance_rid); + _cull_instance_rid = RID(); + } + } + + } else { + if (_cull_instance_rid != RID()) { + VisualServer::get_singleton()->free(_cull_instance_rid); + _cull_instance_rid = RID(); + } + } +} + void VisibilityNotifier::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_WORLD: { world = get_world(); ERR_FAIL_COND(!world.is_valid()); - world->_register_notifier(this, get_global_transform().xform(aabb)); + + AABB world_aabb = get_global_transform().xform(aabb); + world->_register_notifier(this, world_aabb); + _refresh_portal_mode(); } break; case NOTIFICATION_TRANSFORM_CHANGED: { - world->_update_notifier(this, get_global_transform().xform(aabb)); + AABB world_aabb = get_global_transform().xform(aabb); + world->_update_notifier(this, world_aabb); + + if (_cull_instance_rid != RID()) { + VisualServer::get_singleton()->ghost_update(_cull_instance_rid, world_aabb); + } } break; case NOTIFICATION_EXIT_WORLD: { ERR_FAIL_COND(!world.is_valid()); world->_remove_notifier(this); + + if (_cull_instance_rid != RID()) { + VisualServer::get_singleton()->ghost_set_scenario(_cull_instance_rid, RID(), get_instance_id(), AABB()); + } + } break; + case NOTIFICATION_ENTER_GAMEPLAY: { + _in_gameplay = true; + if (cameras.size() && Engine::get_singleton()->are_portals_active()) { + emit_signal(SceneStringNames::get_singleton()->screen_entered); + _screen_enter(); + } + } break; + case NOTIFICATION_EXIT_GAMEPLAY: { + _in_gameplay = false; + if (cameras.size() && Engine::get_singleton()->are_portals_active()) { + emit_signal(SceneStringNames::get_singleton()->screen_exited); + _screen_exit(); + } } break; } } bool VisibilityNotifier::is_on_screen() const { - return cameras.size() != 0; + if (!Engine::get_singleton()->are_portals_active()) { + return cameras.size() != 0; + } + + return (cameras.size() != 0) && _in_gameplay; } void VisibilityNotifier::_bind_methods() { @@ -116,6 +187,13 @@ void VisibilityNotifier::_bind_methods() { VisibilityNotifier::VisibilityNotifier() { aabb = AABB(Vector3(-1, -1, -1), Vector3(2, 2, 2)); set_notify_transform(true); + _in_gameplay = false; +} + +VisibilityNotifier::~VisibilityNotifier() { + if (_cull_instance_rid != RID()) { + VisualServer::get_singleton()->free(_cull_instance_rid); + } } ////////////////////////////////////// @@ -152,6 +230,20 @@ void VisibilityEnabler::_find_nodes(Node *p_node) { add = true; } + { + AnimationTree *at = Object::cast_to(p_node); + if (at) { + add = true; + } + } + + { + AnimationTreePlayer *atp = Object::cast_to(p_node); + if (atp) { + add = true; + } + } + if (add) { p_node->connect(SceneStringNames::get_singleton()->tree_exiting, this, "_node_removed", varray(p_node), CONNECT_ONESHOT); nodes[p_node] = meta; diff --git a/scene/3d/visibility_notifier.h b/scene/3d/visibility_notifier.h index 8dc9a19b9e4..fee1efc6869 100644 --- a/scene/3d/visibility_notifier.h +++ b/scene/3d/visibility_notifier.h @@ -31,21 +31,26 @@ #ifndef VISIBILITY_NOTIFIER_H #define VISIBILITY_NOTIFIER_H -#include "scene/3d/spatial.h" +#include "scene/3d/cull_instance.h" class World; class Camera; -class VisibilityNotifier : public Spatial { - GDCLASS(VisibilityNotifier, Spatial); +class VisibilityNotifier : public CullInstance { + GDCLASS(VisibilityNotifier, CullInstance); Ref world; Set cameras; AABB aabb; + // if using rooms and portals + RID _cull_instance_rid; + bool _in_gameplay; + protected: virtual void _screen_enter() {} virtual void _screen_exit() {} + virtual void _refresh_portal_mode(); void _notification(int p_what); static void _bind_methods(); @@ -60,6 +65,7 @@ public: bool is_on_screen() const; VisibilityNotifier(); + ~VisibilityNotifier(); }; class VisibilityEnabler : public VisibilityNotifier { diff --git a/scene/3d/visual_instance.cpp b/scene/3d/visual_instance.cpp index edb0b68d7b0..7e51fe70751 100644 --- a/scene/3d/visual_instance.cpp +++ b/scene/3d/visual_instance.cpp @@ -38,6 +38,10 @@ AABB VisualInstance::get_transformed_aabb() const { return get_global_transform().xform(get_aabb()); } +void VisualInstance::_refresh_portal_mode() { + VisualServer::get_singleton()->instance_set_portal_mode(instance, (VisualServer::InstancePortalMode)get_portal_mode()); +} + void VisualInstance::_update_visibility() { if (!is_inside_tree()) { return; @@ -137,7 +141,6 @@ void VisualInstance::_bind_methods() { ClassDB::bind_method(D_METHOD("get_layer_mask"), &VisualInstance::get_layer_mask); ClassDB::bind_method(D_METHOD("set_layer_mask_bit", "layer", "enabled"), &VisualInstance::set_layer_mask_bit); ClassDB::bind_method(D_METHOD("get_layer_mask_bit", "layer"), &VisualInstance::get_layer_mask_bit); - ClassDB::bind_method(D_METHOD("get_transformed_aabb"), &VisualInstance::get_transformed_aabb); ADD_PROPERTY(PropertyInfo(Variant::INT, "layers", PROPERTY_HINT_LAYERS_3D_RENDER), "set_layer_mask", "get_layer_mask"); diff --git a/scene/3d/visual_instance.h b/scene/3d/visual_instance.h index a8a0853736f..6487fc199e0 100644 --- a/scene/3d/visual_instance.h +++ b/scene/3d/visual_instance.h @@ -33,11 +33,11 @@ #include "core/math/face3.h" #include "core/rid.h" -#include "scene/3d/spatial.h" +#include "scene/3d/cull_instance.h" #include "scene/resources/material.h" -class VisualInstance : public Spatial { - GDCLASS(VisualInstance, Spatial); +class VisualInstance : public CullInstance { + GDCLASS(VisualInstance, CullInstance); OBJ_CATEGORY("3D Visual Nodes"); RID base; @@ -48,6 +48,7 @@ class VisualInstance : public Spatial { protected: void _update_visibility(); + virtual void _refresh_portal_mode(); void _notification(int p_what); static void _bind_methods(); @@ -57,7 +58,6 @@ public: FACES_SOLID = 1, // solid geometry FACES_ENCLOSING = 2, FACES_DYNAMIC = 4 // dynamic object geometry - }; RID get_instance() const; diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 44a6a8adcca..19b84a27d63 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -195,11 +195,15 @@ #include "scene/3d/path.h" #include "scene/3d/physics_body.h" #include "scene/3d/physics_joint.h" +#include "scene/3d/portal.h" #include "scene/3d/position_3d.h" #include "scene/3d/proximity_group.h" #include "scene/3d/ray_cast.h" #include "scene/3d/reflection_probe.h" #include "scene/3d/remote_transform.h" +#include "scene/3d/room.h" +#include "scene/3d/room_group.h" +#include "scene/3d/room_manager.h" #include "scene/3d/skeleton.h" #include "scene/3d/soft_body.h" #include "scene/3d/spring_arm.h" @@ -397,6 +401,7 @@ void register_scene_types() { #ifndef _3D_DISABLED ClassDB::register_virtual_class(); + ClassDB::register_virtual_class(); ClassDB::register_virtual_class(); ClassDB::register_class(); ClassDB::register_class(); @@ -426,6 +431,10 @@ void register_scene_types() { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + 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/servers/visual/SCsub b/servers/visual/SCsub index 86681f9c74d..3e6180b3447 100644 --- a/servers/visual/SCsub +++ b/servers/visual/SCsub @@ -3,3 +3,5 @@ Import("env") env.add_source_files(env.servers_sources, "*.cpp") + +SConscript("portals/SCsub") diff --git a/servers/visual/portals/SCsub b/servers/visual/portals/SCsub new file mode 100644 index 00000000000..86681f9c74d --- /dev/null +++ b/servers/visual/portals/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.servers_sources, "*.cpp") diff --git a/servers/visual/portals/portal_gameplay_monitor.cpp b/servers/visual/portals/portal_gameplay_monitor.cpp new file mode 100644 index 00000000000..de515a4eabd --- /dev/null +++ b/servers/visual/portals/portal_gameplay_monitor.cpp @@ -0,0 +1,381 @@ +/*************************************************************************/ +/* portal_gameplay_monitor.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 "portal_gameplay_monitor.h" + +#include "portal_renderer.h" +#include "portal_types.h" +#include "servers/visual/visual_server_globals.h" +#include "servers/visual/visual_server_scene.h" + +PortalGameplayMonitor::PortalGameplayMonitor() { + _active_moving_pool_ids_prev = &_active_moving_pool_ids[0]; + _active_moving_pool_ids_curr = &_active_moving_pool_ids[1]; + + _active_rghost_pool_ids_curr = &_active_rghost_pool_ids[0]; + _active_rghost_pool_ids_prev = &_active_rghost_pool_ids[1]; + + _active_room_ids_prev = &_active_room_ids[0]; + _active_room_ids_curr = &_active_room_ids[1]; + + _active_roomgroup_ids_prev = &_active_roomgroup_ids[0]; + _active_roomgroup_ids_curr = &_active_roomgroup_ids[1]; + + _active_sghost_ids_prev = &_active_sghost_ids[0]; + _active_sghost_ids_curr = &_active_sghost_ids[1]; +} + +bool PortalGameplayMonitor::_source_rooms_changed(const int *p_source_room_ids, int p_num_source_rooms) { + bool source_rooms_changed = false; + if (p_num_source_rooms == _source_rooms_prev.size()) { + for (int n = 0; n < p_num_source_rooms; n++) { + if (p_source_room_ids[n] != (int)_source_rooms_prev[n]) { + source_rooms_changed = true; + break; + } + } + } else { + source_rooms_changed = true; + } + if (source_rooms_changed) { + _source_rooms_prev.clear(); + for (int n = 0; n < p_num_source_rooms; n++) { + _source_rooms_prev.push_back(p_source_room_ids[n]); + } + } + + return source_rooms_changed; +} + +void PortalGameplayMonitor::set_params(bool p_use_secondary_pvs, bool p_use_signals) { + _use_secondary_pvs = p_use_secondary_pvs; + _use_signals = p_use_signals; + + if (_use_signals) { + _enter_callback_type = VisualServerCallbacks::CALLBACK_SIGNAL_ENTER_GAMEPLAY; + _exit_callback_type = VisualServerCallbacks::CALLBACK_SIGNAL_EXIT_GAMEPLAY; + } else { + _enter_callback_type = VisualServerCallbacks::CALLBACK_NOTIFICATION_ENTER_GAMEPLAY; + _exit_callback_type = VisualServerCallbacks::CALLBACK_NOTIFICATION_EXIT_GAMEPLAY; + } +} + +// can work with 1 or multiple cameras +void PortalGameplayMonitor::update_gameplay(PortalRenderer &p_portal_renderer, const int *p_source_room_ids, int p_num_source_rooms) { + const PVS &pvs = p_portal_renderer.get_pvs(); + + _gameplay_tick++; + + // if there is no change in the source room IDs, then we can optimize out a lot of the checks + // (anything not to do with roamers) + bool source_rooms_changed = _source_rooms_changed(p_source_room_ids, p_num_source_rooms); + + // lock output + VisualServerCallbacks *callbacks = VSG::scene->get_callbacks(); + callbacks->lock(); + + for (int n = 0; n < p_num_source_rooms; n++) { + const VSRoom &source_room = p_portal_renderer.get_room(p_source_room_ids[n]); + + int pvs_size = source_room._pvs_size; + int pvs_first = source_room._pvs_first; + if (_use_secondary_pvs) { + pvs_size = source_room._secondary_pvs_size; + pvs_first = source_room._secondary_pvs_first; + } + + for (int r = 0; r < pvs_size; r++) { + int room_id = pvs.get_pvs_room_id(pvs_first + r); + + _update_gameplay_room(p_portal_renderer, room_id, source_rooms_changed); + + } // for r through the rooms hit in the pvs + + } // for n through source rooms + + // find any moving that were active last tick that are no longer active, and send notifications + for (int n = 0; n < _active_moving_pool_ids_prev->size(); n++) { + int pool_id = (*_active_moving_pool_ids_prev)[n]; + PortalRenderer::Moving &moving = p_portal_renderer.get_pool_moving(pool_id); + + // gone out of view + if (moving.last_gameplay_tick_hit != _gameplay_tick) { + VisualServerCallbacks::Message msg; + msg.object_id = VSG::scene->_instance_get_object_ID(moving.instance); + msg.type = _exit_callback_type; + + callbacks->push_message(msg); + } + } + + // find any roaming ghosts that were active last tick that are no longer active, and send notifications + for (int n = 0; n < _active_rghost_pool_ids_prev->size(); n++) { + int pool_id = (*_active_rghost_pool_ids_prev)[n]; + PortalRenderer::RGhost &moving = p_portal_renderer.get_pool_rghost(pool_id); + + // gone out of view + if (moving.last_gameplay_tick_hit != _gameplay_tick) { + VisualServerCallbacks::Message msg; + msg.object_id = moving.object_id; + msg.type = VisualServerCallbacks::CALLBACK_NOTIFICATION_EXIT_GAMEPLAY; + + callbacks->push_message(msg); + } + } + + if (source_rooms_changed) { + // find any rooms that were active last tick that are no longer active, and send notifications + for (int n = 0; n < _active_room_ids_prev->size(); n++) { + int room_id = (*_active_room_ids_prev)[n]; + const VSRoom &room = p_portal_renderer.get_room(room_id); + + // gone out of view + if (room.last_gameplay_tick_hit != _gameplay_tick) { + VisualServerCallbacks::Message msg; + msg.object_id = room._godot_instance_ID; + msg.type = _exit_callback_type; + + callbacks->push_message(msg); + } + } + + // find any roomgroups that were active last tick that are no longer active, and send notifications + for (int n = 0; n < _active_roomgroup_ids_prev->size(); n++) { + int roomgroup_id = (*_active_roomgroup_ids_prev)[n]; + const VSRoomGroup &roomgroup = p_portal_renderer.get_roomgroup(roomgroup_id); + + // gone out of view + if (roomgroup.last_gameplay_tick_hit != _gameplay_tick) { + VisualServerCallbacks::Message msg; + msg.object_id = roomgroup._godot_instance_ID; + msg.type = _exit_callback_type; + + callbacks->push_message(msg); + } + } + + // find any static ghosts that were active last tick that are no longer active, and send notifications + for (int n = 0; n < _active_sghost_ids_prev->size(); n++) { + int id = (*_active_sghost_ids_prev)[n]; + VSStaticGhost &ghost = p_portal_renderer.get_static_ghost(id); + + // gone out of view + if (ghost.last_gameplay_tick_hit != _gameplay_tick) { + VisualServerCallbacks::Message msg; + msg.object_id = ghost.object_id; + msg.type = VisualServerCallbacks::CALLBACK_NOTIFICATION_EXIT_GAMEPLAY; + + callbacks->push_message(msg); + } + } + } // only need to check these if the source rooms changed + + // unlock + callbacks->unlock(); + + // swap the current and previous lists + _swap(); +} + +void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_renderer, int p_room_id, bool p_source_rooms_changed) { + // get the room + VSRoom &room = p_portal_renderer.get_room(p_room_id); + + int num_roamers = room._roamer_pool_ids.size(); + + VisualServerCallbacks *callbacks = VSG::scene->get_callbacks(); + + for (int n = 0; n < num_roamers; n++) { + uint32_t pool_id = room._roamer_pool_ids[n]; + + PortalRenderer::Moving &moving = p_portal_renderer.get_pool_moving(pool_id); + + // done already? + if (moving.last_gameplay_tick_hit == _gameplay_tick) + continue; + + // add to the active list + _active_moving_pool_ids_curr->push_back(pool_id); + + // if wasn't present in the tick before, add the notification to enter + if (moving.last_gameplay_tick_hit != (_gameplay_tick - 1)) { + VisualServerCallbacks::Message msg; + msg.object_id = VSG::scene->_instance_get_object_ID(moving.instance); + msg.type = _enter_callback_type; + + callbacks->push_message(msg); + } + + // mark as done + moving.last_gameplay_tick_hit = _gameplay_tick; + } + + // roaming ghosts + int num_rghosts = room._rghost_pool_ids.size(); + + for (int n = 0; n < num_rghosts; n++) { + uint32_t pool_id = room._rghost_pool_ids[n]; + + PortalRenderer::RGhost &moving = p_portal_renderer.get_pool_rghost(pool_id); + + // done already? + if (moving.last_gameplay_tick_hit == _gameplay_tick) + continue; + + // add to the active list + _active_rghost_pool_ids_curr->push_back(pool_id); + + // if wasn't present in the tick before, add the notification to enter + if (moving.last_gameplay_tick_hit != (_gameplay_tick - 1)) { + VisualServerCallbacks::Message msg; + msg.object_id = moving.object_id; + msg.type = VisualServerCallbacks::CALLBACK_NOTIFICATION_ENTER_GAMEPLAY; + + callbacks->push_message(msg); + } + + // mark as done + moving.last_gameplay_tick_hit = _gameplay_tick; + } + + // no need to progress from here + if (!p_source_rooms_changed) { + return; + } + + // has the room come into gameplay? + + // later tests only relevant if a room has just come into play + bool room_came_into_play = false; + + if (room.last_gameplay_tick_hit != _gameplay_tick) { + room_came_into_play = true; + + // add the room to the active list + _active_room_ids_curr->push_back(p_room_id); + + // if wasn't present in the tick before, add the notification to enter + if (room.last_gameplay_tick_hit != (_gameplay_tick - 1)) { + VisualServerCallbacks::Message msg; + msg.object_id = room._godot_instance_ID; + msg.type = _enter_callback_type; + + callbacks->push_message(msg); + } + + // mark as done + room.last_gameplay_tick_hit = _gameplay_tick; + } + + // no need to do later tests + if (!room_came_into_play) { + return; + } + /////////////////////////////////////////////////////////////////// + + // has the roomgroup come into gameplay? + for (int n = 0; n < room._roomgroup_ids.size(); n++) { + int roomgroup_id = room._roomgroup_ids[n]; + + VSRoomGroup &roomgroup = p_portal_renderer.get_roomgroup(roomgroup_id); + + if (roomgroup.last_gameplay_tick_hit != _gameplay_tick) { + // add the room to the active list + _active_roomgroup_ids_curr->push_back(roomgroup_id); + + // if wasn't present in the tick before, add the notification to enter + if (roomgroup.last_gameplay_tick_hit != (_gameplay_tick - 1)) { + VisualServerCallbacks::Message msg; + msg.object_id = roomgroup._godot_instance_ID; + msg.type = _enter_callback_type; + + callbacks->push_message(msg); + } + + // mark as done + roomgroup.last_gameplay_tick_hit = _gameplay_tick; + } + } // for through roomgroups + + // static ghosts + int num_sghosts = room._static_ghost_ids.size(); + + for (int n = 0; n < num_sghosts; n++) { + uint32_t id = room._static_ghost_ids[n]; + + VSStaticGhost &ghost = p_portal_renderer.get_static_ghost(id); + + // done already? + if (ghost.last_gameplay_tick_hit == _gameplay_tick) + continue; + + // add to the active list + _active_sghost_ids_curr->push_back(id); + + // if wasn't present in the tick before, add the notification to enter + if (ghost.last_gameplay_tick_hit != (_gameplay_tick - 1)) { + VisualServerCallbacks::Message msg; + msg.object_id = ghost.object_id; + msg.type = VisualServerCallbacks::CALLBACK_NOTIFICATION_ENTER_GAMEPLAY; + + callbacks->push_message(msg); + } + + // mark as done + ghost.last_gameplay_tick_hit = _gameplay_tick; + } +} + +void PortalGameplayMonitor::_swap() { + LocalVector *temp = _active_moving_pool_ids_curr; + _active_moving_pool_ids_curr = _active_moving_pool_ids_prev; + _active_moving_pool_ids_prev = temp; + _active_moving_pool_ids_curr->clear(); + + temp = _active_rghost_pool_ids_curr; + _active_rghost_pool_ids_curr = _active_rghost_pool_ids_prev; + _active_rghost_pool_ids_prev = temp; + _active_rghost_pool_ids_curr->clear(); + + temp = _active_room_ids_curr; + _active_room_ids_curr = _active_room_ids_prev; + _active_room_ids_prev = temp; + _active_room_ids_curr->clear(); + + temp = _active_roomgroup_ids_curr; + _active_roomgroup_ids_curr = _active_roomgroup_ids_prev; + _active_roomgroup_ids_prev = temp; + _active_roomgroup_ids_curr->clear(); + + temp = _active_sghost_ids_curr; + _active_sghost_ids_curr = _active_sghost_ids_prev; + _active_sghost_ids_prev = temp; + _active_sghost_ids_curr->clear(); +} diff --git a/servers/visual/portals/portal_gameplay_monitor.h b/servers/visual/portals/portal_gameplay_monitor.h new file mode 100644 index 00000000000..b915d312b86 --- /dev/null +++ b/servers/visual/portals/portal_gameplay_monitor.h @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* portal_gameplay_monitor.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_GAMEPLAY_MONITOR_H +#define PORTAL_GAMEPLAY_MONITOR_H + +#include "core/local_vector.h" +#include "servers/visual_server_callbacks.h" + +#include + +class PortalRenderer; +struct VSRoom; + +class PortalGameplayMonitor { +public: + PortalGameplayMonitor(); + + // entering and exiting gameplay notifications (requires PVS) + void update_gameplay(PortalRenderer &p_portal_renderer, const int *p_source_room_ids, int p_num_source_rooms); + void set_params(bool p_use_secondary_pvs, bool p_use_signals); + +private: + void _update_gameplay_room(PortalRenderer &p_portal_renderer, int p_room_id, bool p_source_rooms_changed); + bool _source_rooms_changed(const int *p_source_room_ids, int p_num_source_rooms); + void _swap(); + + uint32_t _gameplay_tick = 1; + + // we need two version, current and previous + LocalVector _active_moving_pool_ids[2]; + LocalVector *_active_moving_pool_ids_curr; + LocalVector *_active_moving_pool_ids_prev; + + LocalVector _active_rghost_pool_ids[2]; + LocalVector *_active_rghost_pool_ids_curr; + LocalVector *_active_rghost_pool_ids_prev; + + LocalVector _active_room_ids[2]; + LocalVector *_active_room_ids_curr; + LocalVector *_active_room_ids_prev; + + LocalVector _active_roomgroup_ids[2]; + LocalVector *_active_roomgroup_ids_curr; + LocalVector *_active_roomgroup_ids_prev; + + LocalVector _active_sghost_ids[2]; + LocalVector *_active_sghost_ids_curr; + LocalVector *_active_sghost_ids_prev; + + LocalVector _source_rooms_prev; + + VisualServerCallbacks::CallbackType _enter_callback_type = VisualServerCallbacks::CALLBACK_NOTIFICATION_ENTER_GAMEPLAY; + VisualServerCallbacks::CallbackType _exit_callback_type = VisualServerCallbacks::CALLBACK_NOTIFICATION_EXIT_GAMEPLAY; + + bool _use_secondary_pvs = false; + bool _use_signals = false; +}; + +#endif diff --git a/servers/visual/portals/portal_pvs.cpp b/servers/visual/portals/portal_pvs.cpp new file mode 100644 index 00000000000..6bd7ffbdcd4 --- /dev/null +++ b/servers/visual/portals/portal_pvs.cpp @@ -0,0 +1,37 @@ +/*************************************************************************/ +/* portal_pvs.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 "portal_pvs.h" + +void PVS::clear() { + _room_pvs.clear(); + _room_secondary_pvs.clear(); + _loaded = false; +} diff --git a/servers/visual/portals/portal_pvs.h b/servers/visual/portals/portal_pvs.h new file mode 100644 index 00000000000..525a04700a0 --- /dev/null +++ b/servers/visual/portals/portal_pvs.h @@ -0,0 +1,59 @@ +/*************************************************************************/ +/* portal_pvs.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_PVS_H +#define PORTAL_PVS_H + +#include "core/local_vector.h" + +class PVS { +public: + void clear(); + + void add_to_pvs(int p_room_id) { _room_pvs.push_back(p_room_id); } + int32_t get_pvs_size() const { return _room_pvs.size(); } + int32_t get_pvs_room_id(int32_t p_entry) const { return _room_pvs[p_entry]; } + + void add_to_secondary_pvs(int p_room_id) { _room_secondary_pvs.push_back(p_room_id); } + int32_t get_secondary_pvs_size() const { return _room_secondary_pvs.size(); } + int32_t get_secondary_pvs_room_id(int32_t p_entry) const { return _room_secondary_pvs[p_entry]; } + + void set_loaded(bool p_loaded) { _loaded = p_loaded; } + bool is_loaded() const { return _loaded; } + +private: + // pvs + LocalVector _room_pvs; + // secondary pvs is primary plus the immediate neighbors of the primary pvs + LocalVector _room_secondary_pvs; + bool _loaded = false; +}; + +#endif diff --git a/servers/visual/portals/portal_pvs_builder.cpp b/servers/visual/portals/portal_pvs_builder.cpp new file mode 100644 index 00000000000..a8b89614b1c --- /dev/null +++ b/servers/visual/portals/portal_pvs_builder.cpp @@ -0,0 +1,453 @@ +/*************************************************************************/ +/* portal_pvs_builder.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 "portal_pvs_builder.h" + +#include "core/os/file_access.h" +#include "core/os/os.h" +#include "core/print_string.h" +#include "portal_renderer.h" + +bool PVSBuilder::_log_active = false; + +void PVSBuilder::find_neighbors(LocalVector &r_neighbors) { + // first find the neighbors + int num_rooms = _portal_renderer->get_num_rooms(); + + for (int n = 0; n < num_rooms; n++) { + const VSRoom &room = _portal_renderer->get_room(n); + + // go through each portal + int num_portals = room._portal_ids.size(); + + for (int p = 0; p < num_portals; p++) { + int portal_id = room._portal_ids[p]; + const VSPortal &portal = _portal_renderer->get_portal(portal_id); + + // everything depends on whether the portal is incoming or outgoing. + // if incoming we reverse the logic. + int outgoing = 1; + + int room_a_id = portal._linkedroom_ID[0]; + if (room_a_id != n) { + outgoing = 0; + DEV_ASSERT(portal._linkedroom_ID[1] == n); + } + + // trace through this portal to the next room + int linked_room_id = portal._linkedroom_ID[outgoing]; + + // not relevant, portal doesn't go anywhere + if (linked_room_id == -1) + continue; + + r_neighbors[n].room_ids.push_back(linked_room_id); + } // for p through portals + + } // for n through rooms + + // the secondary PVS is the primary PVS plus the neighbors +} + +void PVSBuilder::create_secondary_pvs(int p_room_id, const LocalVector &p_neighbors, BitFieldDynamic &r_bitfield_rooms) { + VSRoom &room = _portal_renderer->get_room(p_room_id); + room._secondary_pvs_first = _pvs->get_secondary_pvs_size(); + + // go through each primary PVS room, and add the neighbors in the secondary pvs + for (int r = 0; r < room._pvs_size; r++) { + int pvs_entry = room._pvs_first + r; + int pvs_room_id = _pvs->get_pvs_room_id(pvs_entry); + + // add the visible rooms first + _pvs->add_to_secondary_pvs(pvs_room_id); + room._secondary_pvs_size += 1; + + // now any neighbors of this that are not already added + const Neighbours &neigh = p_neighbors[pvs_room_id]; + for (int n = 0; n < neigh.room_ids.size(); n++) { + int neigh_room_id = neigh.room_ids[n]; + + //log("\tconsidering neigh " + itos(neigh_room_id)); + + if (r_bitfield_rooms.check_and_set(neigh_room_id)) { + // add to the secondary pvs for this room + _pvs->add_to_secondary_pvs(neigh_room_id); + room._secondary_pvs_size += 1; + } // neighbor room has not been added yet + } // go through the neighbors + } // go through each room in the primary pvs +} + +#ifdef GODOT_PVS_SUPPORT_SAVE_FILE + +bool PVSBuilder::load_pvs(String p_filename) { + if (p_filename == "") { + return false; + } + + Error err; + FileAccess *file = FileAccess::open(p_filename, FileAccess::READ, &err); + + if (err || !file) { + if (file) { + memdelete(file); + } + return false; + } + + // goto needs vars declaring ahead of time + int32_t num_rooms; + int32_t pvs_size; + + if (!((file->get_8() == 'p') && + (file->get_8() == 'v') && + (file->get_8() == 's') && + (file->get_8() == ' '))) { + goto failed; + } + + num_rooms = file->get_32(); + if (num_rooms != _portal_renderer->get_num_rooms()) { + goto failed; + } + + for (int n = 0; n < num_rooms; n++) { + if (file->eof_reached()) + goto failed; + + VSRoom &room = _portal_renderer->get_room(n); + room._pvs_first = file->get_32(); + room._pvs_size = file->get_32(); + room._secondary_pvs_first = file->get_32(); + room._secondary_pvs_size = file->get_32(); + } + + pvs_size = file->get_32(); + + for (int n = 0; n < pvs_size; n++) { + _pvs->add_to_pvs(file->get_16()); + } + + // secondary pvs + pvs_size = file->get_32(); + + for (int n = 0; n < pvs_size; n++) { + _pvs->add_to_secondary_pvs(file->get_16()); + } + + if (file) { + memdelete(file); + } + + return true; + +failed: + if (file) { + memdelete(file); + } + + return false; +} + +void PVSBuilder::save_pvs(String p_filename) { + if (p_filename == "") { + p_filename = "res://test.pvs"; + } + + Error err; + FileAccess *file = FileAccess::open(p_filename, FileAccess::WRITE, &err); + + if (err || !file) { + if (file) { + memdelete(file); + } + return; + } + + file->store_8('p'); + file->store_8('v'); + file->store_8('s'); + file->store_8(' '); + + // hash? NYI + + // first save the room indices into the pvs + int num_rooms = _portal_renderer->get_num_rooms(); + file->store_32(num_rooms); + + for (int n = 0; n < num_rooms; n++) { + VSRoom &room = _portal_renderer->get_room(n); + file->store_32(room._pvs_first); + file->store_32(room._pvs_size); + file->store_32(room._secondary_pvs_first); + file->store_32(room._secondary_pvs_size); + } + + int32_t pvs_size = _pvs->get_pvs_size(); + file->store_32(pvs_size); + + for (int n = 0; n < pvs_size; n++) { + int16_t room_id = _pvs->get_pvs_room_id(n); + file->store_16(room_id); + } + + pvs_size = _pvs->get_secondary_pvs_size(); + file->store_32(pvs_size); + + for (int n = 0; n < pvs_size; n++) { + int16_t room_id = _pvs->get_secondary_pvs_room_id(n); + file->store_16(room_id); + } + + if (file) { + memdelete(file); + } +} + +#endif + +void PVSBuilder::calculate_pvs(PortalRenderer &p_portal_renderer, String p_filename) { + _portal_renderer = &p_portal_renderer; + _pvs = &p_portal_renderer.get_pvs(); + + // attempt to load from file rather than create each time +#ifdef GODOT_PVS_SUPPORT_SAVE_FILE + if (load_pvs(p_filename)) { + print_line("loaded pvs successfully from file " + p_filename); + _pvs->set_loaded(true); + return; + } +#endif + + uint32_t time_before = OS::get_singleton()->get_ticks_msec(); + + int num_rooms = _portal_renderer->get_num_rooms(); + BitFieldDynamic bf; + bf.create(num_rooms); + + LocalVector neighbors; + neighbors.resize(num_rooms); + + // find the immediate neighbors of each room - + // this is needed to create the secondary pvs + find_neighbors(neighbors); + + for (int n = 0; n < num_rooms; n++) { + bf.blank(); + + //_visible_rooms.clear(); + + LocalVector dummy_planes; + + VSRoom &room = _portal_renderer->get_room(n); + room._pvs_first = _pvs->get_pvs_size(); + + log("pvs from room : " + itos(n)); + + trace_rooms_recursive(0, n, n, -1, false, -1, dummy_planes, bf); + + create_secondary_pvs(n, neighbors, bf); + + if (_log_active) { + String string = ""; + for (int i = 0; i < room._pvs_size; i++) { + int visible_room = _pvs->get_pvs_room_id(room._pvs_first + i); + string += itos(visible_room); + string += ", "; + } + + log("\t" + string); + + string = "secondary : "; + for (int i = 0; i < room._secondary_pvs_size; i++) { + int visible_room = _pvs->get_secondary_pvs_room_id(room._secondary_pvs_first + i); + string += itos(visible_room); + string += ", "; + } + + log("\t" + string); + } + } + + _pvs->set_loaded(true); + + uint32_t time_after = OS::get_singleton()->get_ticks_msec(); + + print_verbose("calculated PVS in " + itos(time_after - time_before) + " ms."); + +#ifdef GODOT_PVS_SUPPORT_SAVE_FILE + save_pvs(p_filename); +#endif +} + +void PVSBuilder::logd(int p_depth, String p_string) { + return; + + String string_long; + for (int n = 0; n < p_depth; n++) { + string_long += "\t"; + } + string_long += p_string; + log(string_long); +} + +void PVSBuilder::log(String p_string) { + if (_log_active) { + print_line(p_string); + } +} + +void PVSBuilder::trace_rooms_recursive(int p_depth, int p_source_room_id, int p_room_id, int p_first_portal_id, bool p_first_portal_outgoing, int p_previous_portal_id, const LocalVector &p_planes, BitFieldDynamic &r_bitfield_rooms) { + // has this room been done already? + if (!r_bitfield_rooms.check_and_set(p_room_id)) { + return; + } + + logd(p_depth, "trace_rooms_recursive room " + itos(p_room_id)); + + // get the room + const VSRoom &room = _portal_renderer->get_room(p_room_id); + + // add to the room PVS of the source room + VSRoom &source_room = _portal_renderer->get_room(p_source_room_id); + _pvs->add_to_pvs(p_room_id); + source_room._pvs_size += 1; + + // go through each portal + int num_portals = room._portal_ids.size(); + + for (int p = 0; p < num_portals; p++) { + int portal_id = room._portal_ids[p]; + const VSPortal &portal = _portal_renderer->get_portal(portal_id); + + // everything depends on whether the portal is incoming or outgoing. + // if incoming we reverse the logic. + int outgoing = 1; + + int room_a_id = portal._linkedroom_ID[0]; + if (room_a_id != p_room_id) { + outgoing = 0; + DEV_ASSERT(portal._linkedroom_ID[1] == p_room_id); + } + + // trace through this portal to the next room + int linked_room_id = portal._linkedroom_ID[outgoing]; + + logd(p_depth + 1, "portal to room " + itos(linked_room_id)); + + // not relevant, portal doesn't go anywhere + if (linked_room_id == -1) + continue; + + // linked room done already? + if (r_bitfield_rooms.get_bit(linked_room_id)) + continue; + + // is it culled by the planes? + VSPortal::ClipResult overall_res = VSPortal::ClipResult::CLIP_INSIDE; + + // while clipping to the planes we maintain a list of partial planes, so we can add them to the + // recursive next iteration of planes to check + static LocalVector partial_planes; + partial_planes.clear(); + + for (int32_t l = 0; l < p_planes.size(); l++) { + VSPortal::ClipResult res = portal.clip_with_plane(p_planes[l]); + + switch (res) { + case VSPortal::ClipResult::CLIP_OUTSIDE: { + overall_res = res; + } break; + case VSPortal::ClipResult::CLIP_PARTIAL: { + // if the portal intersects one of the planes, we should take this plane into account + // in the next call of this recursive trace, because it can be used to cull out more objects + overall_res = res; + partial_planes.push_back(l); + } break; + default: // suppress warning + break; + } + + // if the portal was totally outside the 'frustum' then we can ignore it + if (overall_res == VSPortal::ClipResult::CLIP_OUTSIDE) + break; + } + + // this portal is culled + if (overall_res == VSPortal::ClipResult::CLIP_OUTSIDE) { + logd(p_depth + 2, "portal CLIP_OUTSIDE"); + continue; + } + + // construct new planes + LocalVector planes; + + if (p_first_portal_id != -1) { + // add new planes + const VSPortal &first_portal = _portal_renderer->get_portal(p_first_portal_id); + portal.add_pvs_planes(first_portal, p_first_portal_outgoing, planes, outgoing != 0); + +//#define GODOT_PVS_EXTRA_REJECT_TEST +#ifdef GODOT_PVS_EXTRA_REJECT_TEST + // extra reject test for pvs - was the previous portal points outside the planes formed by the new portal? + // not fully tested and not yet found a situation where needed, but will leave in in case testers find + // such a situation. + if (p_previous_portal_id != -1) { + const VSPortal &prev_portal = _portal_renderer->get_portal(p_previous_portal_id); + if (prev_portal._pvs_is_outside_planes(planes)) { + continue; + } + } +#endif + } + + // if portal is totally inside the planes, don't copy the old planes .. + // i.e. we can now cull using the portal and forget about the rest of the frustum (yay) + if (overall_res != VSPortal::ClipResult::CLIP_INSIDE) { + // if it WASNT totally inside the existing frustum, we also need to add any existing planes + // that cut the portal. + for (uint32_t n = 0; n < partial_planes.size(); n++) + planes.push_back(p_planes[partial_planes[n]]); + } + + // hopefully the portal actually leads somewhere... + if (linked_room_id != -1) { + // we either pass on the first portal id, or we start + // it here, because we are looking through the first portal + int first_portal_id = p_first_portal_id; + if (first_portal_id == -1) { + first_portal_id = portal_id; + p_first_portal_outgoing = outgoing != 0; + } + + trace_rooms_recursive(p_depth + 1, p_source_room_id, linked_room_id, first_portal_id, p_first_portal_outgoing, portal_id, planes, r_bitfield_rooms); + } // linked room is valid + } +} diff --git a/servers/visual/portals/portal_pvs_builder.h b/servers/visual/portals/portal_pvs_builder.h new file mode 100644 index 00000000000..05050cbc28f --- /dev/null +++ b/servers/visual/portals/portal_pvs_builder.h @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* portal_pvs_builder.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_PVS_BUILDER_H +#define PORTAL_PVS_BUILDER_H + +#include "core/bitfield_dynamic.h" +#include "core/local_vector.h" +#include "core/math/plane.h" + +//#define GODOT_PVS_SUPPORT_SAVE_FILE + +class PortalRenderer; +class PVS; + +class PVSBuilder { + struct Neighbours { + LocalVector room_ids; + }; + +public: + void calculate_pvs(PortalRenderer &p_portal_renderer, String p_filename); + +private: +#ifdef GODOT_PVS_SUPPORT_SAVE_FILE + bool load_pvs(String p_filename); + void save_pvs(String p_filename); +#endif + void find_neighbors(LocalVector &r_neighbors); + + void logd(int p_depth, String p_string); + void log(String p_string); + + void trace_rooms_recursive(int p_depth, int p_source_room_id, int p_room_id, int p_first_portal_id, bool p_first_portal_outgoing, int p_previous_portal_id, const LocalVector &p_planes, BitFieldDynamic &r_bitfield_rooms); + + void create_secondary_pvs(int p_room_id, const LocalVector &p_neighbors, BitFieldDynamic &r_bitfield_rooms); + + PortalRenderer *_portal_renderer = nullptr; + PVS *_pvs = nullptr; + + static bool _log_active; +}; + +#endif diff --git a/servers/visual/portals/portal_renderer.cpp b/servers/visual/portals/portal_renderer.cpp new file mode 100644 index 00000000000..5a15901e044 --- /dev/null +++ b/servers/visual/portals/portal_renderer.cpp @@ -0,0 +1,989 @@ +/*************************************************************************/ +/* portal_renderer.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 "portal_renderer.h" + +#include "portal_pvs_builder.h" +#include "servers/visual/visual_server_globals.h" +#include "servers/visual/visual_server_scene.h" + +OcclusionHandle PortalRenderer::instance_moving_create(VSInstance *p_instance, RID p_instance_rid, bool p_global, AABB p_aabb) { + uint32_t pool_id = 0; + Moving *moving = _moving_pool.request(pool_id); + moving->global = p_global; + moving->pool_id = pool_id; + moving->instance = p_instance; + moving->room_id = -1; + +#ifdef PORTAL_RENDERER_STORE_MOVING_RIDS + moving->instance_rid = p_instance_rid; +#endif + + // add to the appropriate list + if (p_global) { + moving->list_id = _moving_list_global.size(); + _moving_list_global.push_back(pool_id); + } else { + // do we need a roaming master list? not sure yet + moving->list_id = _moving_list_roaming.size(); + _moving_list_roaming.push_back(pool_id); + } + + OcclusionHandle handle = pool_id + 1; + instance_moving_update(handle, p_aabb); + return handle; +} + +void PortalRenderer::instance_moving_update(OcclusionHandle p_handle, const AABB &p_aabb, bool p_force_reinsert) { + // we can ignore these, they are statics / dynamics, and don't need updating + // .. these should have been filtered out before calling the visual server... + DEV_ASSERT(!_occlusion_handle_is_in_room(p_handle)); + + p_handle--; + Moving &moving = _moving_pool[p_handle]; + moving.exact_aabb = p_aabb; + + // globals (e.g. interface elements) need their aabb updated irrespective of whether the system is loaded + if (!_loaded || moving.global) { + return; + } + + // quick reject for most roaming cases + if (!p_force_reinsert && moving.expanded_aabb.encloses(p_aabb)) { + return; + } + + // using an expanded aabb allows us to make 'no op' moves + // where the new aabb is within the expanded + moving.expanded_aabb = p_aabb.grow(_roaming_expansion_margin); + + // if we got to here, it is roaming (moving between rooms) + // remove from current rooms + _moving_remove_from_rooms(p_handle); + + // add to new rooms + Vector3 center = p_aabb.position + (p_aabb.size * 0.5); + int new_room = find_room_within(center, moving.room_id); + + moving.room_id = new_room; + if (new_room != -1) { + _bitfield_rooms.blank(); + sprawl_roaming(p_handle, moving, new_room, true); + } +} + +void PortalRenderer::_rghost_remove_from_rooms(uint32_t p_pool_id) { + RGhost &moving = _rghost_pool[p_pool_id]; + + // if we have unloaded the rooms and we try this, it will crash + if (_loaded) { + for (int n = 0; n < moving._rooms.size(); n++) { + VSRoom &room = get_room(moving._rooms[n]); + room.remove_rghost(p_pool_id); + } + } + + // moving is now in no rooms + moving._rooms.clear(); +} + +void PortalRenderer::_moving_remove_from_rooms(uint32_t p_moving_pool_id) { + Moving &moving = _moving_pool[p_moving_pool_id]; + + // if we have unloaded the rooms and we try this, it will crash + if (_loaded) { + for (int n = 0; n < moving._rooms.size(); n++) { + VSRoom &room = get_room(moving._rooms[n]); + room.remove_roamer(p_moving_pool_id); + } + } + + // moving is now in no rooms + moving._rooms.clear(); +} + +void PortalRenderer::_debug_print_global_list() { + _log("globals:"); + for (int n = 0; n < _moving_list_global.size(); n++) { + uint32_t id = _moving_list_global[n]; + const Moving &moving = _moving_pool[id]; + _log("\t" + _addr_to_string(&moving)); + } +} + +void PortalRenderer::_log(String p_string, int p_priority) { + // change this for more debug output .. + // not selectable at runtime yet. + if (p_priority >= 1) { + print_line(p_string); + } else { + print_verbose(p_string); + } +} + +void PortalRenderer::instance_moving_destroy(OcclusionHandle p_handle) { + // deleting an instance that is assigned to a room (STATIC or DYNAMIC) + // is special, it must set the PortalRenderer into unloaded state, because + // there will now be a dangling reference to the instance that was destroyed. + // The alternative is to remove the reference, but this is not currently supported + // (it would mean rejigging rooms etc) + if (_occlusion_handle_is_in_room(p_handle)) { + _ensure_unloaded(); + return; + } + + p_handle--; + + Moving *moving = &_moving_pool[p_handle]; + + // if a roamer, remove from any current rooms + if (!moving->global) { + _moving_remove_from_rooms(p_handle); + } + + // remove from list (and keep in sync) + uint32_t list_id = moving->list_id; + + if (moving->global) { + _moving_list_global.remove_unordered(list_id); + + // keep the replacement moving in sync with the correct list Id + if (list_id < (uint32_t)_moving_list_global.size()) { + uint32_t replacement_id = _moving_list_global[list_id]; + Moving &replacement = _moving_pool[replacement_id]; + replacement.list_id = list_id; + } + } else { + _moving_list_roaming.remove_unordered(list_id); + + // keep the replacement moving in sync with the correct list Id + if (list_id < (uint32_t)_moving_list_roaming.size()) { + uint32_t replacement_id = _moving_list_roaming[list_id]; + Moving &replacement = _moving_pool[replacement_id]; + replacement.list_id = list_id; + } + } + + moving->destroy(); + + // can now free the moving + _moving_pool.free(p_handle); +} + +PortalHandle PortalRenderer::portal_create() { + uint32_t pool_id = 0; + VSPortal *portal = _portal_pool.request(pool_id); + + // explicit constructor + portal->create(); + portal->_portal_id = _portal_pool_ids.size(); + + _portal_pool_ids.push_back(pool_id); + + // plus one based handles, 0 is unset + pool_id++; + return pool_id; +} + +void PortalRenderer::portal_destroy(PortalHandle p_portal) { + ERR_FAIL_COND(!p_portal); + _ensure_unloaded(); + + // plus one based + p_portal--; + + // remove from list of valid portals + VSPortal &portal = _portal_pool[p_portal]; + int portal_id = portal._portal_id; + + // we need to replace the last element in the list + _portal_pool_ids.remove_unordered(portal_id); + + // and reset the id of the portal that was the replacement + if (portal_id < _portal_pool_ids.size()) { + int replacement_pool_id = _portal_pool_ids[portal_id]; + VSPortal &replacement = _portal_pool[replacement_pool_id]; + replacement._portal_id = portal_id; + } + + // explicitly run destructor + _portal_pool[p_portal].destroy(); + + // return to the pool + _portal_pool.free(p_portal); +} + +void PortalRenderer::portal_set_geometry(PortalHandle p_portal, const Vector &p_points) { + ERR_FAIL_COND(!p_portal); + p_portal--; // plus 1 based + VSPortal &portal = _portal_pool[p_portal]; + + portal._pts_world = p_points; + + if (portal._pts_world.size() < 3) { + WARN_PRINT("Portal must have at least 3 vertices"); + return; + } + + // create plane from points + // Allow averaging in case of wonky portals. + + // first calculate average normal + Vector3 average_normal = Vector3(0, 0, 0); + for (int t = 2; t < (int)portal._pts_world.size(); t++) { + Plane p = Plane(portal._pts_world[0], portal._pts_world[t - 1], portal._pts_world[t]); + average_normal += p.normal; + } + // average normal + average_normal /= portal._pts_world.size() - 2; + + // detect user error + ERR_FAIL_COND_MSG(average_normal.length() < 0.1, "Nonsense portal detected, normals should be consistent"); + if (average_normal.length() < 0.7) { + WARN_PRINT("Wonky portal detected, you may see culling errors"); + } + + // calc average point + Vector3 average_pt = Vector3(0, 0, 0); + for (unsigned int n = 0; n < portal._pts_world.size(); n++) { + average_pt += portal._pts_world[n]; + } + average_pt /= portal._pts_world.size(); + + // use the average point and normal to derive the plane + portal._plane = Plane(average_pt, average_normal); + + // aabb + AABB &bb = portal._aabb; + bb.position = p_points[0]; + bb.size = Vector3(0, 0, 0); + + for (int n = 1; n < p_points.size(); n++) { + bb.expand_to(p_points[n]); + } +} + +void PortalRenderer::portal_link(PortalHandle p_portal, RoomHandle p_room_from, RoomHandle p_room_to, bool p_two_way) { + ERR_FAIL_COND(!p_portal); + p_portal--; // plus 1 based + VSPortal &portal = _portal_pool[p_portal]; + + ERR_FAIL_COND(!p_room_from); + p_room_from--; + VSRoom &room_from = _room_pool[p_room_from]; + + ERR_FAIL_COND(!p_room_to); + p_room_to--; + VSRoom &room_to = _room_pool[p_room_to]; + + portal._linkedroom_ID[0] = room_from._room_ID; + portal._linkedroom_ID[1] = room_to._room_ID; + + // is the portal internal? internal portals are treated differently + portal._internal = room_from._priority > room_to._priority; + + // if it is internal, mark the outer room as containing an internal room. + // this is used for rooms lookup. + if (portal._internal) { + room_to._contains_internal_rooms = true; + } + + _log("portal_link from room " + itos(room_from._room_ID) + " to room " + itos(room_to._room_ID)); + + room_from._portal_ids.push_back(portal._portal_id); + + // one way portals simply aren't added to the destination room, so they don't get seen through + if (p_two_way) { + room_to._portal_ids.push_back(portal._portal_id); + } +} + +void PortalRenderer::portal_set_active(PortalHandle p_portal, bool p_active) { + ERR_FAIL_COND(!p_portal); + p_portal--; // plus 1 based + VSPortal &portal = _portal_pool[p_portal]; + + portal._active = p_active; +} + +RoomGroupHandle PortalRenderer::roomgroup_create() { + uint32_t pool_id = 0; + VSRoomGroup *rg = _roomgroup_pool.request(pool_id); + + // explicit constructor + rg->create(); + + // plus one based handles, 0 is unset + pool_id++; + return pool_id; +} + +void PortalRenderer::roomgroup_prepare(RoomGroupHandle p_roomgroup, ObjectID p_roomgroup_object_id) { + // plus one based + p_roomgroup--; + VSRoomGroup &rg = _roomgroup_pool[p_roomgroup]; + rg._godot_instance_ID = p_roomgroup_object_id; +} + +void PortalRenderer::roomgroup_destroy(RoomGroupHandle p_roomgroup) { + ERR_FAIL_COND(!p_roomgroup); + _ensure_unloaded(); + + // plus one based + p_roomgroup--; + + VSRoomGroup &rg = _roomgroup_pool[p_roomgroup]; + + // explicitly run destructor + rg.destroy(); + + // return to the pool + _roomgroup_pool.free(p_roomgroup); +} + +void PortalRenderer::roomgroup_add_room(RoomGroupHandle p_roomgroup, RoomHandle p_room) { + // plus one based + p_roomgroup--; + VSRoomGroup &rg = _roomgroup_pool[p_roomgroup]; + + p_room--; + + // add to room group + rg._room_ids.push_back(p_room); + + // add the room group to the room + VSRoom &room = _room_pool[p_room]; + room._roomgroup_ids.push_back(p_roomgroup); +} + +// Cull Instances +RGhostHandle PortalRenderer::rghost_create(ObjectID p_object_id, const AABB &p_aabb) { + uint32_t pool_id = 0; + RGhost *moving = _rghost_pool.request(pool_id); + moving->pool_id = pool_id; + moving->object_id = p_object_id; + moving->room_id = -1; + + RGhostHandle handle = pool_id + 1; + rghost_update(handle, p_aabb); + return handle; +} + +void PortalRenderer::rghost_update(RGhostHandle p_handle, const AABB &p_aabb, bool p_force_reinsert) { + if (!_loaded) { + return; + } + + p_handle--; + RGhost &moving = _rghost_pool[p_handle]; + moving.exact_aabb = p_aabb; + + // quick reject for most roaming cases + if (!p_force_reinsert && moving.expanded_aabb.encloses(p_aabb)) { + return; + } + + // using an expanded aabb allows us to make 'no op' moves + // where the new aabb is within the expanded + moving.expanded_aabb = p_aabb.grow(_roaming_expansion_margin); + + // if we got to here, it is roaming (moving between rooms) + // remove from current rooms + _rghost_remove_from_rooms(p_handle); + + // add to new rooms + Vector3 center = p_aabb.position + (p_aabb.size * 0.5); + int new_room = find_room_within(center, moving.room_id); + + moving.room_id = new_room; + if (new_room != -1) { + _bitfield_rooms.blank(); + sprawl_roaming(p_handle, moving, new_room, false); + } +} + +void PortalRenderer::rghost_destroy(RGhostHandle p_handle) { + p_handle--; + + RGhost *moving = &_rghost_pool[p_handle]; + + // if a roamer, remove from any current rooms + _rghost_remove_from_rooms(p_handle); + + moving->destroy(); + + // can now free the moving + _rghost_pool.free(p_handle); +} + +// Rooms +RoomHandle PortalRenderer::room_create() { + uint32_t pool_id = 0; + VSRoom *room = _room_pool.request(pool_id); + + // explicit constructor + room->create(); + + // keep our own internal list of rooms + room->_room_ID = _room_pool_ids.size(); + _room_pool_ids.push_back(pool_id); + + // plus one based handles, 0 is unset + pool_id++; + return pool_id; +} + +void PortalRenderer::room_destroy(RoomHandle p_room) { + ERR_FAIL_COND(!p_room); + _ensure_unloaded(); + + // plus one based + p_room--; + + // remove from list of valid rooms + VSRoom &room = _room_pool[p_room]; + int room_id = room._room_ID; + + // we need to replace the last element in the list + _room_pool_ids.remove_unordered(room_id); + + // and reset the id of the portal that was the replacement + if (room_id < _room_pool_ids.size()) { + int replacement_pool_id = _room_pool_ids[room_id]; + VSRoom &replacement = _room_pool[replacement_pool_id]; + replacement._room_ID = room_id; + } + + // explicitly run destructor + _room_pool[p_room].destroy(); + + // return to the pool + _room_pool.free(p_room); +} + +OcclusionHandle PortalRenderer::room_add_ghost(RoomHandle p_room, ObjectID p_object_id, const AABB &p_aabb) { + ERR_FAIL_COND_V(!p_room, 0); + p_room--; // plus one based + + VSStaticGhost ghost; + ghost.object_id = p_object_id; + _static_ghosts.push_back(ghost); + + // sprawl immediately + // precreate a useful bitfield of rooms for use in sprawling + if ((int)_bitfield_rooms.get_num_bits() != get_num_rooms()) { + _bitfield_rooms.create(get_num_rooms()); + } + + // only can do if rooms exist + if (get_num_rooms()) { + // the last one was just added + int ghost_id = _static_ghosts.size() - 1; + + // create a bitfield to indicate which rooms have been + // visited already, to prevent visiting rooms multiple times + _bitfield_rooms.blank(); + sprawl_static_ghost(ghost_id, p_aabb, p_room); + } + + return OCCLUSION_HANDLE_ROOM_BIT; +} + +OcclusionHandle PortalRenderer::room_add_instance(RoomHandle p_room, RID p_instance, const AABB &p_aabb, bool p_dynamic, const Vector &p_object_pts) { + ERR_FAIL_COND_V(!p_room, 0); + p_room--; // plus one based + VSRoom &room = _room_pool[p_room]; + + VSStatic stat; + stat.instance = p_instance; + stat.source_room_id = room._room_ID; + stat.dynamic = p_dynamic; + stat.aabb = p_aabb; + _statics.push_back(stat); + + // sprawl immediately + // precreate a useful bitfield of rooms for use in sprawling + if ((int)_bitfield_rooms.get_num_bits() != get_num_rooms()) { + _bitfield_rooms.create(get_num_rooms()); + } + + // only can do if rooms exist + if (get_num_rooms()) { + // the last one was just added + int static_id = _statics.size() - 1; + + // pop last static + const VSStatic &st = _statics[static_id]; + + // create a bitfield to indicate which rooms have been + // visited already, to prevent visiting rooms multiple times + _bitfield_rooms.blank(); + + if (p_object_pts.size()) { + sprawl_static_geometry(static_id, st, st.source_room_id, p_object_pts); + } else { + sprawl_static(static_id, st, st.source_room_id); + } + } + + return OCCLUSION_HANDLE_ROOM_BIT; +} + +void PortalRenderer::room_prepare(RoomHandle p_room, int32_t p_priority) { + ERR_FAIL_COND(!p_room); + p_room--; // plus one based + VSRoom &room = _room_pool[p_room]; + room._priority = p_priority; +} + +void PortalRenderer::room_set_bound(RoomHandle p_room, ObjectID p_room_object_id, const Vector &p_convex, const AABB &p_aabb, const Vector &p_verts) { + ERR_FAIL_COND(!p_room); + p_room--; // plus one based + VSRoom &room = _room_pool[p_room]; + + room._planes = p_convex; + room._verts = p_verts; + room._aabb = p_aabb; + room._godot_instance_ID = p_room_object_id; +} + +void PortalRenderer::_add_portal_to_convex_hull(LocalVector &p_planes, const Plane &p) { + for (int n = 0; n < p_planes.size(); n++) { + Plane &o = p_planes[n]; + + // this is a fudge factor for how close the portal can be to an existing plane + // to be to be considered the same ... + // to prevent needless extra checks. + + // the epsilons should probably be more exact here than for the convex hull simplification, as it is + // fairly crucial that the portal planes are reasonably accurate for determining the hull. + + // and because the portal plane is more important, we will REPLACE the existing similar plane + // with the portal plane. + const real_t d = 0.03; // 0.08f + + if (Math::abs(p.d - o.d) > d) { + continue; + } + + real_t dot = p.normal.dot(o.normal); + if (dot < 0.99) // 0.98f + { + continue; + } + + // match! + // replace the existing plane + o = p; + return; + } + + // there is no existing plane that is similar, create a new one especially for the portal + p_planes.push_back(p); +} + +void PortalRenderer::_rooms_add_portals_to_convex_hulls() { + for (int n = 0; n < get_num_rooms(); n++) { + VSRoom &room = get_room(n); + + for (int p = 0; p < room._portal_ids.size(); p++) { + const VSPortal &portal = get_portal(room._portal_ids[p]); + + // everything depends on whether the portal is incoming or outgoing. + // if incoming we reverse the logic. + int outgoing = 1; + + int room_a_id = portal._linkedroom_ID[0]; + if (room_a_id != n) { + outgoing = 0; + DEV_ASSERT(portal._linkedroom_ID[1] == n); + } + + // do not add internal portals to the convex hull of outer rooms! + if (!outgoing && portal._internal) { + continue; + } + + // add the portal plane + Plane portal_plane = portal._plane; + if (!outgoing) { + portal_plane = -portal_plane; + } + + // add if sufficiently different from existing convex hull planes + _add_portal_to_convex_hull(room._planes, portal_plane); + } + } +} + +void PortalRenderer::rooms_finalize(bool p_generate_pvs, bool p_cull_using_pvs, bool p_use_secondary_pvs, bool p_use_signals, String p_pvs_filename) { + _gameplay_monitor.set_params(p_use_secondary_pvs, p_use_signals); + + // portals should also bound the rooms, the room geometry may extend past the portal + _rooms_add_portals_to_convex_hulls(); + + // the trace results can never have more hits than the number of static objects + _trace_results.create(_statics.size()); + + // precreate a useful bitfield of rooms for use in sprawling, if not created already + // (may not be necessary but just in case, rooms with no statics etc) + _bitfield_rooms.create(_room_pool_ids.size()); + + // the rooms looksup is a pre-calced grid structure for faster lookup of the nearest room + // from position + _rooms_lookup_bsp.create(*this); + + // calculate the roaming expansion margin based on the average room size + Vector3 total_size = Vector3(0, 0, 0); + for (int n = 0; n < get_num_rooms(); n++) { + total_size += get_room(n)._aabb.size; + } + if (get_num_rooms()) { + total_size /= get_num_rooms(); + AABB temp; + temp.size = total_size; + + // longest axis of average room * fudge factor + _roaming_expansion_margin = temp.get_longest_axis_size() * 0.08; + } + + // calculate PVS + if (p_generate_pvs) { + PVSBuilder pvs; + pvs.calculate_pvs(*this, p_pvs_filename); + _cull_using_pvs = p_cull_using_pvs; // hard code to on for test + } else { + _cull_using_pvs = false; + } + + _loaded = true; + + // all the roaming objects need to be sprawled into the rooms + // (they may have been created before the rooms) + _load_finalize_roaming(); + + // allow deleting any intermediate data + for (int n = 0; n < get_num_rooms(); n++) { + get_room(n).cleanup_after_conversion(); + } + + // this should probably have some thread protection, but I doubt it matters + // as this will worst case give wrong result for a frame + Engine::get_singleton()->set_portals_active(true); + + print_line("Room conversion complete. " + itos(_room_pool_ids.size()) + " rooms, " + itos(_portal_pool_ids.size()) + " portals."); +} + +void PortalRenderer::sprawl_static_geometry(int p_static_id, const VSStatic &p_static, int p_room_id, const Vector &p_object_pts) { + // set, and if room already done, ignore + if (!_bitfield_rooms.check_and_set(p_room_id)) + return; + + VSRoom &room = get_room(p_room_id); + room._static_ids.push_back(p_static_id); + + // go through portals + for (int p = 0; p < room._portal_ids.size(); p++) { + const VSPortal &portal = get_portal(room._portal_ids[p]); + + int room_to_id = portal.geometry_crosses_portal(p_room_id, p_static.aabb, p_object_pts); + + if (room_to_id != -1) { + _log(String(Variant(p_static.aabb)) + " crosses portal"); + + sprawl_static_geometry(p_static_id, p_static, room_to_id, p_object_pts); + } + } +} + +void PortalRenderer::sprawl_static_ghost(int p_ghost_id, const AABB &p_aabb, int p_room_id) { + // set, and if room already done, ignore + if (!_bitfield_rooms.check_and_set(p_room_id)) { + return; + } + + VSRoom &room = get_room(p_room_id); + room._static_ghost_ids.push_back(p_ghost_id); + + // go through portals + for (int p = 0; p < room._portal_ids.size(); p++) { + const VSPortal &portal = get_portal(room._portal_ids[p]); + + int room_to_id = portal.crosses_portal(p_room_id, p_aabb, true); + + if (room_to_id != -1) { + _log(String(Variant(p_aabb)) + " crosses portal"); + + sprawl_static_ghost(p_ghost_id, p_aabb, room_to_id); + } + } +} + +void PortalRenderer::sprawl_static(int p_static_id, const VSStatic &p_static, int p_room_id) { + // set, and if room already done, ignore + if (!_bitfield_rooms.check_and_set(p_room_id)) { + return; + } + + VSRoom &room = get_room(p_room_id); + room._static_ids.push_back(p_static_id); + + // go through portals + for (int p = 0; p < room._portal_ids.size(); p++) { + const VSPortal &portal = get_portal(room._portal_ids[p]); + + int room_to_id = portal.crosses_portal(p_room_id, p_static.aabb, true); + + if (room_to_id != -1) { + _log(String(Variant(p_static.aabb)) + " crosses portal"); + + sprawl_static(p_static_id, p_static, room_to_id); + } + } +} + +void PortalRenderer::_load_finalize_roaming() { + for (int n = 0; n < _moving_list_roaming.size(); n++) { + uint32_t pool_id = _moving_list_roaming[n]; + + Moving &moving = _moving_pool[pool_id]; + const AABB &aabb = moving.exact_aabb; + + OcclusionHandle handle = pool_id + 1; + instance_moving_update(handle, aabb, true); + } + + for (int n = 0; n < _rghost_pool.active_size(); n++) { + RGhost &moving = _rghost_pool.get_active(n); + const AABB &aabb = moving.exact_aabb; + + rghost_update(_rghost_pool.get_active_id(n) + 1, aabb, true); + } +} + +void PortalRenderer::sprawl_roaming(uint32_t p_mover_pool_id, MovingBase &r_moving, int p_room_id, bool p_moving_or_ghost) { + // set, and if room already done, ignore + if (!_bitfield_rooms.check_and_set(p_room_id)) { + return; + } + + // add to the room + VSRoom &room = get_room(p_room_id); + + if (p_moving_or_ghost) { + room.add_roamer(p_mover_pool_id); + } else { + room.add_rghost(p_mover_pool_id); + } + + // add the room to the mover + r_moving._rooms.push_back(p_room_id); + + // go through portals + for (int p = 0; p < room._portal_ids.size(); p++) { + const VSPortal &portal = get_portal(room._portal_ids[p]); + + int room_to_id = portal.crosses_portal(p_room_id, r_moving.expanded_aabb); + + if (room_to_id != -1) { + // _log(String(Variant(p_static.aabb)) + " crosses portal"); + sprawl_roaming(p_mover_pool_id, r_moving, room_to_id, p_moving_or_ghost); + } + } +} + +// This gets called when you delete an instance the the room system depends on +void PortalRenderer::_ensure_unloaded() { + if (_loaded) { + _loaded = false; + _log("Portal system unloaded.", 1); + + // this should probably have some thread protection, but I doubt it matters + // as this will worst case give wrong result for a frame + Engine::get_singleton()->set_portals_active(false); + } +} + +void PortalRenderer::rooms_and_portals_clear() { + _loaded = false; + _statics.clear(); + _static_ghosts.clear(); + + // the rooms and portals should remove their id when they delete themselves + // from the scene tree by calling room_destroy and portal_destroy ... + // therefore there should be no need to clear these here + // _room_pool_ids.clear(); + // _portal_pool_ids.clear(); + + _rooms_lookup_bsp.clear(); + + // clear the portals out of each existing room + for (int n = 0; n < get_num_rooms(); n++) { + VSRoom &room = get_room(n); + room.rooms_and_portals_clear(); + } + + for (int n = 0; n < get_num_portals(); n++) { + VSPortal &portal = get_portal(n); + portal.rooms_and_portals_clear(); + } + + // when the rooms_and_portals_clear message is sent, + // we want to remove all references to old rooms in the moving + // objects, to prevent dangling references. + for (int n = 0; n < get_num_moving_globals(); n++) { + Moving &moving = get_pool_moving(_moving_list_global[n]); + moving.rooms_and_portals_clear(); + } + for (int n = 0; n < _moving_list_roaming.size(); n++) { + Moving &moving = get_pool_moving(_moving_list_roaming[n]); + moving.rooms_and_portals_clear(); + } + + for (int n = 0; n < _rghost_pool.active_size(); n++) { + RGhost &moving = _rghost_pool.get_active(n); + moving.rooms_and_portals_clear(); + } + + _pvs.clear(); +} + +void PortalRenderer::rooms_override_camera(bool p_override, const Vector3 &p_point, const Vector *p_convex) { + _override_camera = p_override; + _override_camera_pos = p_point; + if (p_convex) { + _override_camera_planes = *p_convex; + } +} + +void PortalRenderer::rooms_update_gameplay_monitor(const Vector &p_camera_positions) { + // is the pvs loaded? + if (!_loaded || !_pvs.is_loaded()) { + if (!_pvs.is_loaded()) { + WARN_PRINT_ONCE("RoomManager PVS is required for this functionality"); + } + return; + } + + int *source_rooms = (int *)alloca(sizeof(int) * p_camera_positions.size()); + int num_source_rooms = 0; + + for (int n = 0; n < p_camera_positions.size(); n++) { + int source_room_id = find_room_within(p_camera_positions[n]); + if (source_room_id == -1) { + continue; + } + + source_rooms[num_source_rooms++] = source_room_id; + } + + _gameplay_monitor.update_gameplay(*this, source_rooms, num_source_rooms); +} + +int PortalRenderer::cull_convex_implementation(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint) { + // start room + int start_room_id = find_room_within(p_point, r_previous_room_id_hint); + + // return the previous room hint + r_previous_room_id_hint = start_room_id; + + if (start_room_id == -1) { + return -1; + } + + // planes must be in CameraMatrix order + DEV_ASSERT(p_convex.size() == 6); + + LocalVector planes; + planes = p_convex; + + _trace_results.clear(); + + if (!_debug_sprawl) { + _tracer.trace(*this, p_point, planes, start_room_id, _trace_results); //, near_and_far_planes); + } else { + _tracer.trace_debug_sprawl(*this, p_point, start_room_id, _trace_results); + } + + int num_results = _trace_results.visible_static_ids.size(); + int out_count = 0; + + for (int n = 0; n < num_results; n++) { + uint32_t static_id = _trace_results.visible_static_ids[n]; + RID static_rid = _statics[static_id].instance; + VSInstance *instance = VSG::scene->_instance_get_from_rid(static_rid); + + if (VSG::scene->_instance_cull_check(instance, p_mask)) { + p_result_array[out_count++] = instance; + if (out_count >= p_result_max) { + break; + } + } + } + + // results could be full up already + if (out_count >= p_result_max) { + return out_count; + } + + // add the roaming results + + // cap to the maximum results + int num_roam_hits = _trace_results.visible_roamer_pool_ids.size(); + + // translate + for (int n = 0; n < num_roam_hits; n++) { + const Moving &moving = get_pool_moving(_trace_results.visible_roamer_pool_ids[n]); + + if (VSG::scene->_instance_cull_check(moving.instance, p_mask)) { + p_result_array[out_count++] = moving.instance; + if (out_count >= p_result_max) { + break; + } + } + } + + // results could be full up already + if (out_count >= p_result_max) { + return out_count; + } + + out_count = _tracer.trace_globals(planes, p_result_array, out_count, p_result_max, p_mask); + + return out_count; +} + +String PortalRenderer::_rid_to_string(RID p_rid) { + return _addr_to_string(p_rid.get_data()); +} + +String PortalRenderer::_addr_to_string(const void *p_addr) { + return String::num_uint64((uint64_t)p_addr, 16); +} diff --git a/servers/visual/portals/portal_renderer.h b/servers/visual/portals/portal_renderer.h new file mode 100644 index 00000000000..206e2bb3490 --- /dev/null +++ b/servers/visual/portals/portal_renderer.h @@ -0,0 +1,292 @@ +/*************************************************************************/ +/* portal_renderer.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_RENDERER_H +#define PORTAL_RENDERER_H + +#include "core/pooled_list.h" +#include "core/vector.h" +#include "portal_gameplay_monitor.h" +#include "portal_pvs.h" +#include "portal_rooms_bsp.h" +#include "portal_tracer.h" +#include "portal_types.h" + +struct VSStatic { + // the lifetime of statics is not strictly monitored like moving objects + // therefore we store a RID which could return NULL if the object has been deleted + RID instance; + + AABB aabb; + + // statics are placed in a room, but they can optionally sprawl to other rooms + // if large (like lights) + uint32_t source_room_id; + + // dynamics will request their AABB each frame + // from the visual server in case they have moved. + // But they will NOT update the rooms they are in... + // so this works well for e.g. moving platforms, but not for objects + // that will move between rooms. + uint32_t dynamic; +}; + +// static / dynamic visibility notifiers. +// ghost objects are not culled, but are present in rooms +// and expect to receive gameplay notifications +struct VSStaticGhost { + ObjectID object_id; + + uint32_t last_tick_hit = 0; + uint32_t last_gameplay_tick_hit = 0; +}; + +class PortalRenderer { +public: + // use most significant bit to store whether an instance is being used in the room system + // in which case, deleting such an instance should deactivate the portal system to prevent + // crashes due to dangling references to instances. + static const uint32_t OCCLUSION_HANDLE_ROOM_BIT = 1 << 31; + + struct MovingBase { + // when the rooms_and_portals_clear message is sent, + // we want to remove all references to old rooms in the moving + // objects, to prevent dangling references. + void rooms_and_portals_clear() { destroy(); } + void destroy() { + _rooms.clear(); + room_id = -1; + } + + // the expanded aabb allows objects to move on most frames + // without needing to determine a change of room + AABB expanded_aabb; + + // exact aabb of the object should be used for culling + AABB exact_aabb; + + // which is the primary room this moving object is in + // (it may sprawl into multiple rooms) + int32_t room_id; + + // id in the allocation pool + uint32_t pool_id; + + uint32_t last_tick_hit = 0; + uint32_t last_gameplay_tick_hit = 0; + + // room ids of rooms this moving object is sprawled into + LocalVector _rooms; + }; + + struct Moving : public MovingBase { + // either roaming or global + bool global; + + // in _moving_lists .. not the same as pool ID (handle) + uint32_t list_id; + + // a void pointer, but this is ultimately a pointer to a VisualServerScene::Instance + // (can't have direct pointer because it is a nested class...) + VSInstance *instance; + +#ifdef PORTAL_RENDERER_STORE_MOVING_RIDS + // primarily for testing + RID instance_rid; +#endif + }; + + // So far the only roaming ghosts are VisibilityNotifiers. + // this will always be roaming... statics and dynamics are handled separately, + // and global ghosts do not get created. + struct RGhost : public MovingBase { + ObjectID object_id; + }; + + PortalHandle portal_create(); + void portal_destroy(PortalHandle p_portal); + void portal_set_geometry(PortalHandle p_portal, const Vector &p_points); + void portal_link(PortalHandle p_portal, RoomHandle p_room_from, RoomHandle p_room_to, bool p_two_way); + void portal_set_active(PortalHandle p_portal, bool p_active); + + RoomGroupHandle roomgroup_create(); + void roomgroup_prepare(RoomGroupHandle p_roomgroup, ObjectID p_roomgroup_object_id); + void roomgroup_destroy(RoomGroupHandle p_roomgroup); + void roomgroup_add_room(RoomGroupHandle p_roomgroup, RoomHandle p_room); + + // Rooms + RoomHandle room_create(); + void room_destroy(RoomHandle p_room); + OcclusionHandle room_add_instance(RoomHandle p_room, RID p_instance, const AABB &p_aabb, bool p_dynamic, const Vector &p_object_pts); + OcclusionHandle room_add_ghost(RoomHandle p_room, ObjectID p_object_id, const AABB &p_aabb); + void room_set_bound(RoomHandle p_room, ObjectID p_room_object_id, const Vector &p_convex, const AABB &p_aabb, const Vector &p_verts); + void room_prepare(RoomHandle p_room, int32_t p_priority); + void rooms_and_portals_clear(); + void rooms_finalize(bool p_generate_pvs, bool p_cull_using_pvs, bool p_use_secondary_pvs, bool p_use_signals, String p_pvs_filename); + void rooms_override_camera(bool p_override, const Vector3 &p_point, const Vector *p_convex); + void rooms_set_active(bool p_active) { _active = p_active; } + void rooms_set_params(int p_portal_depth_limit) { _tracer.set_depth_limit(p_portal_depth_limit); } + void rooms_set_cull_using_pvs(bool p_enable) { _cull_using_pvs = p_enable; } + void rooms_update_gameplay_monitor(const Vector &p_camera_positions); + + // for use in the editor only, to allow a cheap way of turning off portals + // if there has been a change, e.g. moving a room etc. + void rooms_unload() { _ensure_unloaded(); } + + // debugging + void set_debug_sprawl(bool p_active) { _debug_sprawl = p_active; } + + // this section handles moving objects - roaming (change rooms) and globals (not in any room) + OcclusionHandle instance_moving_create(VSInstance *p_instance, RID p_instance_rid, bool p_global, AABB p_aabb); + void instance_moving_update(OcclusionHandle p_handle, const AABB &p_aabb, bool p_force_reinsert = false); + void instance_moving_destroy(OcclusionHandle p_handle); + + // spatial derived roamers (non VisualInstances that still need to be portal culled, especially VisibilityNotifiers) + RGhostHandle rghost_create(ObjectID p_object_id, const AABB &p_aabb); + void rghost_update(RGhostHandle p_handle, const AABB &p_aabb, bool p_force_reinsert = false); + void rghost_destroy(RGhostHandle p_handle); + + // note that this relies on a 'frustum' type cull, from a point, and that the planes are specified as in + // CameraMatrix, i.e. + // order PLANE_NEAR,PLANE_FAR,PLANE_LEFT,PLANE_TOP,PLANE_RIGHT,PLANE_BOTTOM + int cull_convex(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint) { + if (!_override_camera) + return cull_convex_implementation(p_point, p_convex, p_result_array, p_result_max, p_mask, r_previous_room_id_hint); + + return cull_convex_implementation(_override_camera_pos, _override_camera_planes, p_result_array, p_result_max, p_mask, r_previous_room_id_hint); + } + + int cull_convex_implementation(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint); + + bool is_active() const { return _active && _loaded; } + + VSStatic &get_static(int p_id) { return _statics[p_id]; } + const VSStatic &get_static(int p_id) const { return _statics[p_id]; } + + int32_t get_num_rooms() const { return _room_pool_ids.size(); } + VSRoom &get_room(int p_id) { return _room_pool[_room_pool_ids[p_id]]; } + const VSRoom &get_room(int p_id) const { return _room_pool[_room_pool_ids[p_id]]; } + + int32_t get_num_portals() const { return _portal_pool_ids.size(); } + VSPortal &get_portal(int p_id) { return _portal_pool[_portal_pool_ids[p_id]]; } + const VSPortal &get_portal(int p_id) const { return _portal_pool[_portal_pool_ids[p_id]]; } + + int32_t get_num_moving_globals() const { return _moving_list_global.size(); } + const Moving &get_moving_global(uint32_t p_id) const { return _moving_pool[_moving_list_global[p_id]]; } + + Moving &get_pool_moving(uint32_t p_pool_id) { return _moving_pool[p_pool_id]; } + const Moving &get_pool_moving(uint32_t p_pool_id) const { return _moving_pool[p_pool_id]; } + + RGhost &get_pool_rghost(uint32_t p_pool_id) { return _rghost_pool[p_pool_id]; } + const RGhost &get_pool_rghost(uint32_t p_pool_id) const { return _rghost_pool[p_pool_id]; } + + VSStaticGhost &get_static_ghost(uint32_t p_id) { return _static_ghosts[p_id]; } + + VSRoomGroup &get_roomgroup(uint32_t p_pool_id) { return _roomgroup_pool[p_pool_id]; } + + PVS &get_pvs() { return _pvs; } + const PVS &get_pvs() const { return _pvs; } + + bool get_cull_using_pvs() const { return _cull_using_pvs; } + +private: + int find_room_within(const Vector3 &p_pos, int p_previous_room_id = -1) { + return _rooms_lookup_bsp.find_room_within(*this, p_pos, p_previous_room_id); + } + + void sprawl_static(int p_static_id, const VSStatic &p_static, int p_room_id); + void sprawl_static_geometry(int p_static_id, const VSStatic &p_static, int p_room_id, const Vector &p_object_pts); + void sprawl_static_ghost(int p_ghost_id, const AABB &p_aabb, int p_room_id); + + void _load_finalize_roaming(); + void sprawl_roaming(uint32_t p_mover_pool_id, MovingBase &r_moving, int p_room_id, bool p_moving_or_ghost); + void _moving_remove_from_rooms(uint32_t p_moving_pool_id); + void _rghost_remove_from_rooms(uint32_t p_pool_id); + void _ensure_unloaded(); + void _rooms_add_portals_to_convex_hulls(); + void _add_portal_to_convex_hull(LocalVector &p_planes, const Plane &p); + + void _debug_print_global_list(); + bool _occlusion_handle_is_in_room(OcclusionHandle p_h) const { + return p_h == OCCLUSION_HANDLE_ROOM_BIT; + } + + void _log(String p_string, int p_priority = 0); + + // note this is vulnerable to crashes, we must monitor for deletion of rooms + LocalVector _room_pool_ids; + LocalVector _portal_pool_ids; + + LocalVector _statics; + LocalVector _static_ghosts; + + // all rooms and portals are allocated from pools. + PooledList _portal_pool; + PooledList _room_pool; + PooledList _roomgroup_pool; + + // moving objects, global and roaming + PooledList _moving_pool; + TrackedPooledList _rghost_pool; + LocalVector _moving_list_global; + LocalVector _moving_list_roaming; + + PVS _pvs; + + bool _active = true; + bool _loaded = false; + bool _debug_sprawl = false; + + // if the pvs is generated, we can either cull using dynamic portals or PVS + bool _cull_using_pvs = false; + + PortalTracer _tracer; + PortalTracer::TraceResult _trace_results; + PortalRoomsBSP _rooms_lookup_bsp; + PortalGameplayMonitor _gameplay_monitor; + + // when moving roaming objects, we expand their bound + // to prevent too many updates. + real_t _roaming_expansion_margin = 1.0; + + // a bitfield to indicate which rooms have been + // visited already in sprawling, to prevent visiting rooms multiple times + BitFieldDynamic _bitfield_rooms; + + bool _override_camera = false; + Vector3 _override_camera_pos; + LocalVector _override_camera_planes; + +public: + static String _rid_to_string(RID p_rid); + static String _addr_to_string(const void *p_addr); +}; + +#endif diff --git a/servers/visual/portals/portal_rooms_bsp.cpp b/servers/visual/portals/portal_rooms_bsp.cpp new file mode 100644 index 00000000000..7ff652f76d9 --- /dev/null +++ b/servers/visual/portals/portal_rooms_bsp.cpp @@ -0,0 +1,641 @@ +/*************************************************************************/ +/* portal_rooms_bsp.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 "portal_rooms_bsp.h" + +#include "core/math/geometry.h" +#include "core/math/plane.h" +#include "core/print_string.h" +#include "core/variant.h" +#include "portal_renderer.h" + +// #define GODOT_VERBOSE_PORTAL_ROOMS_BSP + +void PortalRoomsBSP::_log(String p_string) { +#ifdef GODOT_VERBOSE_PORTAL_ROOMS_BSP + print_line(p_string); +#endif +} + +// rooms which contain internal rooms cannot use the optimization where it terminates the search for +// room within if inside the previous room. We can't use just use the rooms already marked as internal due +// to a portal leading to them, because the internal room network may spread into another room (e.g. terrain) +// which has internal room exit portal. So we need to detect manually all cases of overlap of internal rooms, +// and set the flag. +void PortalRoomsBSP::detect_internal_room_containment(PortalRenderer &r_portal_renderer) { + int num_rooms = r_portal_renderer.get_num_rooms(); + + for (int n = 0; n < num_rooms; n++) { + VSRoom &room = r_portal_renderer.get_room(n); + if (room._contains_internal_rooms) { + // already established it contains internal rooms, no need to test + continue; + } + + // safety + if (!room._planes.size()) { + continue; + } + + for (int i = 0; i < num_rooms; i++) { + // don't test against ourself + if (n == i) { + continue; + } + + // only interested in rooms with a higher priority, these are potential internal rooms + const VSRoom &other = r_portal_renderer.get_room(i); + if (other._priority <= room._priority) { + continue; + } + + // quick aabb check first + if (!room._aabb.intersects(other._aabb)) { + continue; + } + + // safety + if (!other._planes.size()) { + continue; + } + + if (Geometry::convex_hull_intersects_convex_hull(&room._planes[0], room._planes.size(), &other._planes[0], other._planes.size())) { + // it intersects an internal room + room._contains_internal_rooms = true; + break; + } + } + } +} + +int PortalRoomsBSP::find_room_within(const PortalRenderer &p_portal_renderer, const Vector3 &p_pos, int p_previous_room_id) const { + real_t closest = FLT_MAX; + int closest_room_id = -1; + int closest_priority = -10000; + + // first try previous room + if (p_previous_room_id != -1) { + const VSRoom &prev_room = p_portal_renderer.get_room(p_previous_room_id); + + // we can only use this shortcut if the room doesn't include internal rooms. + // otherwise the point may be inside more than one room, and we need to find the room of highest priority. + if (!prev_room._contains_internal_rooms) { + closest = prev_room.is_point_within(p_pos); + closest_room_id = p_previous_room_id; + + if (closest < 0.0) { + return p_previous_room_id; + } + } else { + // don't mark it as checked later, as we haven't done it because it contains internal rooms + p_previous_room_id = -1; + } + } + + int num_bsp_rooms = 0; + const int32_t *bsp_rooms = find_shortlist(p_pos, num_bsp_rooms); + if (!num_bsp_rooms) { + return -1; + } + + // special case, only 1 room in the shortlist, no need to check further + if (num_bsp_rooms == 1) { + return bsp_rooms[0]; + } + + for (int n = 0; n < num_bsp_rooms; n++) { + int room_id = bsp_rooms[n]; + + // the previous room has already been done above, and will be in closest + closest_room_id + if (room_id == p_previous_room_id) { + continue; + } + + const VSRoom &room = p_portal_renderer.get_room(room_id); + real_t dist = room.is_point_within(p_pos); + + // if we are actually inside a room, unless we are dealing with internal rooms, + // we can terminate early, no need to search more + if (dist < 0.0) { + if (!room._contains_internal_rooms) { + // this will happen in most cases + closest = dist; + closest_room_id = room_id; + break; + } else { + // if we are inside, and there are internal rooms involved we need to be a bit careful. + // higher priority always wins (i.e. the internal room) + // but with equal priority we just choose the regular best fit. + if ((room._priority > closest_priority) || ((room._priority == closest_priority) && (dist < closest))) { + closest = dist; + closest_room_id = room_id; + closest_priority = room._priority; + continue; + } + } + } else { + // if we are outside we just pick the closest room, irrespective of priority + if (dist < closest) { + closest = dist; + closest_room_id = room_id; + // do NOT store the priority, we don't want an room that isn't a true hit + // overriding a hit inside the room + } + } + } + + return closest_room_id; +} + +const int32_t *PortalRoomsBSP::find_shortlist(const Vector3 &p_pt, int &r_num_rooms) const { + if (!_nodes.size()) { + r_num_rooms = 0; + return nullptr; + } + + const Node *node = &_nodes[0]; + + while (!node->leaf) { + if (node->plane.is_point_over(p_pt)) { + node = &_nodes[node->child[1]]; + } else { + node = &_nodes[node->child[0]]; + } + } + + r_num_rooms = node->num_ids; + return &_room_ids[node->first_id]; +} + +void PortalRoomsBSP::create(PortalRenderer &r_portal_renderer) { + clear(); + _portal_renderer = &r_portal_renderer; + detect_internal_room_containment(r_portal_renderer); + + // noop + int num_rooms = r_portal_renderer.get_num_rooms(); + + if (!num_rooms) { + return; + } + + LocalVector room_ids; + room_ids.resize(num_rooms); + for (int n = 0; n < num_rooms; n++) { + room_ids[n] = n; + } + + _nodes.push_back(Node()); + _nodes[0].clear(); + + build(0, room_ids); + +#ifdef GODOT_VERBOSE_PORTAL_ROOMS_BSP + debug_print_tree(); +#endif + _log("PortalRoomsBSP " + itos(_nodes.size()) + " nodes."); +} + +void PortalRoomsBSP::build(int p_start_node_id, LocalVector p_orig_room_ids) { + struct Element { + void clear() { room_ids.clear(); } + int node_id; + LocalVector room_ids; + }; + + Element first; + first.node_id = p_start_node_id; + first.room_ids = p_orig_room_ids; + + LocalVector stack; + stack.reserve(1024); + stack.push_back(first); + int stack_size = 1; + + while (stack_size) { + stack_size--; + Element curr = stack[stack_size]; + + Node *node = &_nodes[curr.node_id]; + + int best_fit = 0; + int best_portal_id = -1; + int best_room_a = -1; + int best_room_b = -1; + + // find a splitting plane + for (int n = 0; n < curr.room_ids.size(); n++) { + // go through the portals in this room + int rid = curr.room_ids[n]; + const VSRoom &room = _portal_renderer->get_room(rid); + + for (int p = 0; p < room._portal_ids.size(); p++) { + int pid = room._portal_ids[p]; + // only outward portals + const VSPortal &portal = _portal_renderer->get_portal(pid); + if (portal._linkedroom_ID[1] == rid) { + continue; + } + + int fit = evaluate_portal(pid, curr.room_ids); + if (fit > best_fit) { + best_fit = fit; + best_portal_id = pid; + } + } + } + + bool split_found = false; + Plane split_plane; + + // if a splitting portal was found, we are done + if (best_portal_id != -1) { + _log("found splitting portal : " + itos(best_portal_id)); + + const VSPortal &portal = _portal_renderer->get_portal(best_portal_id); + split_plane = portal._plane; + split_found = true; + } else { + // let's try and find an arbitrary splitting plane + for (int a = 0; a < curr.room_ids.size(); a++) { + for (int b = a + 1; b < curr.room_ids.size(); b++) { + Plane plane; + + // note the actual room ids are not the same as a and b!! + int room_a_id = curr.room_ids[a]; + int room_b_id = curr.room_ids[b]; + + int fit = evaluate_room_split_plane(room_a_id, room_b_id, curr.room_ids, plane); + + if (fit > best_fit) { + best_fit = fit; + + // the room ids, NOT a and b + best_room_a = room_a_id; + best_room_b = room_b_id; + split_plane = plane; + } + } // for b through rooms + } // for a through rooms + + if (best_room_a != -1) { + split_found = true; + // print_line("found splitting plane between rooms : " + itos(best_room_a) + " and " + itos(best_room_b)); + } + } + + // found either a portal plane or arbitrary + if (split_found) { + node->plane = split_plane; + + // add to stack + stack_size += 2; + if (stack_size > stack.size()) { + stack.resize(stack_size); + } + stack[stack_size - 2].clear(); + stack[stack_size - 1].clear(); + + LocalVector &room_ids_back = stack[stack_size - 2].room_ids; + LocalVector &room_ids_front = stack[stack_size - 1].room_ids; + + if (best_portal_id != -1) { + evaluate_portal(best_portal_id, curr.room_ids, &room_ids_back, &room_ids_front); + } else { + DEV_ASSERT(best_room_a != -1); + evaluate_room_split_plane(best_room_a, best_room_b, curr.room_ids, split_plane, &room_ids_back, &room_ids_front); + } + + DEV_ASSERT(room_ids_back.size() <= curr.room_ids.size()); + DEV_ASSERT(room_ids_front.size() <= curr.room_ids.size()); + + _log("\tback contains : " + itos(room_ids_back.size()) + " rooms"); + _log("\tfront contains : " + itos(room_ids_front.size()) + " rooms"); + + // create child nodes + _nodes.push_back(Node()); + _nodes.push_back(Node()); + + // need to reget the node pointer as we may have resized the vector + node = &_nodes[curr.node_id]; + + node->child[0] = _nodes.size() - 2; + node->child[1] = _nodes.size() - 1; + + stack[stack_size - 2].node_id = node->child[0]; + stack[stack_size - 1].node_id = node->child[1]; + + } else { + // couldn't split any further, is leaf + node->leaf = true; + node->first_id = _room_ids.size(); + node->num_ids = curr.room_ids.size(); + + _log("leaf contains : " + itos(curr.room_ids.size()) + " rooms"); + + // add to the main list + int start = _room_ids.size(); + _room_ids.resize(start + curr.room_ids.size()); + for (int n = 0; n < curr.room_ids.size(); n++) { + _room_ids[start + n] = curr.room_ids[n]; + } + } + + } // while stack not empty +} + +void PortalRoomsBSP::debug_print_tree(int p_node_id, int p_depth) { + String string = ""; + for (int n = 0; n < p_depth; n++) { + string += "\t"; + } + + const Node &node = _nodes[p_node_id]; + if (node.leaf) { + string += "L "; + for (int n = 0; n < node.num_ids; n++) { + int room_id = _room_ids[node.first_id + n]; + string += itos(room_id) + ", "; + } + } else { + string += "N "; + } + + print_line(string); + + // children + if (!node.leaf) { + debug_print_tree(node.child[0], p_depth + 1); + debug_print_tree(node.child[1], p_depth + 1); + } +} + +bool PortalRoomsBSP::find_1d_split_point(real_t p_min_a, real_t p_max_a, real_t p_min_b, real_t p_max_b, real_t &r_split_point) const { + if (p_max_a <= p_min_b) { + r_split_point = p_max_a + ((p_min_b - p_max_a) * 0.5); + return true; + } + if (p_max_b <= p_min_a) { + r_split_point = p_max_b + ((p_min_a - p_max_b) * 0.5); + return true; + } + + return false; +} + +bool PortalRoomsBSP::test_freeform_plane(const LocalVector &p_verts_a, const LocalVector &p_verts_b, const Plane &p_plane) const { + // print_line("test_freeform_plane " + String(Variant(p_plane))); + + for (int n = 0; n < p_verts_a.size(); n++) { + real_t dist = p_plane.distance_to(p_verts_a[n]); + // print_line("\tdist_a " + String(Variant(dist))); + if (dist > _plane_epsilon) { + return false; + } + } + + for (int n = 0; n < p_verts_b.size(); n++) { + real_t dist = p_plane.distance_to(p_verts_b[n]); + // print_line("\tdist_b " + String(Variant(dist))); + if (dist < -_plane_epsilon) { + return false; + } + } + + return true; +} + +// even if AABBs fail to have a splitting plane, there still may be another orientation that can split rooms (e.g. diagonal) +bool PortalRoomsBSP::calculate_freeform_splitting_plane(const VSRoom &p_room_a, const VSRoom &p_room_b, Plane &r_plane) const { + const LocalVector &verts_a = p_room_a._verts; + const LocalVector &verts_b = p_room_b._verts; + + // test from room a to room b + for (int i = 0; i < verts_a.size(); i++) { + const Vector3 &pt_a = verts_a[i]; + + for (int j = 0; j < verts_b.size(); j++) { + const Vector3 &pt_b = verts_b[j]; + + for (int k = j + 1; k < verts_b.size(); k++) { + const Vector3 &pt_c = verts_b[k]; + + // make a plane + r_plane = Plane(pt_a, pt_b, pt_c); + + // test the plane + if (test_freeform_plane(verts_a, verts_b, r_plane)) { + return true; + } + } + } + } + + // test from room b to room a + for (int i = 0; i < verts_b.size(); i++) { + const Vector3 &pt_a = verts_b[i]; + + for (int j = 0; j < verts_a.size(); j++) { + const Vector3 &pt_b = verts_a[j]; + + for (int k = j + 1; k < verts_a.size(); k++) { + const Vector3 &pt_c = verts_a[k]; + + // make a plane + r_plane = Plane(pt_a, pt_b, pt_c); + + // test the plane + if (test_freeform_plane(verts_b, verts_a, r_plane)) { + return true; + } + } + } + } + + return false; +} + +bool PortalRoomsBSP::calculate_aabb_splitting_plane(const AABB &p_a, const AABB &p_b, Plane &r_plane) const { + real_t split_point = 0.0; + + const Vector3 &min_a = p_a.position; + const Vector3 &min_b = p_b.position; + Vector3 max_a = min_a + p_a.size; + Vector3 max_b = min_b + p_b.size; + + if (find_1d_split_point(min_a.x, max_a.x, min_b.x, max_b.x, split_point)) { + r_plane = Plane(Vector3(1, 0, 0), split_point); + return true; + } + if (find_1d_split_point(min_a.y, max_a.y, min_b.y, max_b.y, split_point)) { + r_plane = Plane(Vector3(0, 1, 0), split_point); + return true; + } + if (find_1d_split_point(min_a.z, max_a.z, min_b.z, max_b.z, split_point)) { + r_plane = Plane(Vector3(0, 0, 1), split_point); + return true; + } + + return false; +} + +int PortalRoomsBSP::evaluate_room_split_plane(int p_room_a_id, int p_room_b_id, const LocalVector &p_room_ids, Plane &r_plane, LocalVector *r_room_ids_back, LocalVector *r_room_ids_front) { + // try and create a splitting plane between room a and b, then evaluate it. + const VSRoom &room_a = _portal_renderer->get_room(p_room_a_id); + const VSRoom &room_b = _portal_renderer->get_room(p_room_b_id); + + // easiest case, if the rooms don't overlap AABB, we can create an axis aligned plane between them + if (calculate_aabb_splitting_plane(room_a._aabb, room_b._aabb, r_plane)) { + return evaluate_plane(nullptr, r_plane, p_room_ids, r_room_ids_back, r_room_ids_front); + } + + if (calculate_freeform_splitting_plane(room_a, room_b, r_plane)) { + return evaluate_plane(nullptr, r_plane, p_room_ids, r_room_ids_back, r_room_ids_front); + } + + return 0; +} + +int PortalRoomsBSP::evaluate_plane(const VSPortal *p_portal, const Plane &p_plane, const LocalVector &p_room_ids, LocalVector *r_room_ids_back, LocalVector *r_room_ids_front) { + int rooms_front = 0; + int rooms_back = 0; + int rooms_split = 0; + + if (r_room_ids_back) { + DEV_ASSERT(!r_room_ids_back->size()); + } + + if (r_room_ids_front) { + DEV_ASSERT(!r_room_ids_front->size()); + } + +#define GODOT_BSP_PUSH_FRONT \ + rooms_front++; \ + if (r_room_ids_front) { \ + r_room_ids_front->push_back(rid); \ + } + +#define GODOT_BSP_PUSH_BACK \ + rooms_back++; \ + if (r_room_ids_back) { \ + r_room_ids_back->push_back(rid); \ + } + + for (int n = 0; n < p_room_ids.size(); n++) { + int rid = p_room_ids[n]; + const VSRoom &room = _portal_renderer->get_room(rid); + + // easy cases first + real_t r_min, r_max; + room._aabb.project_range_in_plane(p_plane, r_min, r_max); + + if ((r_min <= 0.0) && (r_max <= 0.0)) { + GODOT_BSP_PUSH_BACK + continue; + } + if ((r_min >= 0.0) && (r_max >= 0.0)) { + GODOT_BSP_PUSH_FRONT + continue; + } + + // check if the room uses this portal + // internal portals can link to a room that is both in front and behind, + // so we can only deal with non internal portals here with this cheap test. + if (p_portal && !p_portal->_internal) { + if (p_portal->_linkedroom_ID[0] == rid) { + GODOT_BSP_PUSH_BACK + continue; + } + + if (p_portal->_linkedroom_ID[1] == rid) { + GODOT_BSP_PUSH_FRONT + continue; + } + } + + // most expensive test, test the individual points of the room + // This will catch some off axis rooms that aren't caught by the AABB alone + int points_front = 0; + int points_back = 0; + + for (int p = 0; p < room._verts.size(); p++) { + const Vector3 &pt = room._verts[p]; + real_t dist = p_plane.distance_to(pt); + + // don't take account of points in the epsilon zone, + // these are within the margin of error and could be in front OR behind the plane + if (dist > _plane_epsilon) { + points_front++; + if (points_back) { + break; + } + } else if (dist < -_plane_epsilon) { + points_back++; + if (points_front) { + break; + } + } + } + + // if all points are in front + if (!points_back) { + GODOT_BSP_PUSH_FRONT + continue; + } + // if all points are behind + if (!points_front) { + GODOT_BSP_PUSH_BACK + continue; + } + + // if split, push to both children + if (r_room_ids_front) { + r_room_ids_front->push_back(rid); + } + if (r_room_ids_back) { + r_room_ids_back->push_back(rid); + } + + rooms_split++; + } + +#undef GODOT_BSP_PUSH_BACK +#undef GODOT_BSP_PUSH_FRONT + + // we want the split that splits the most front and back rooms + return rooms_front * rooms_back; +} + +int PortalRoomsBSP::evaluate_portal(int p_portal_id, const LocalVector &p_room_ids, LocalVector *r_room_ids_back, LocalVector *r_room_ids_front) { + const VSPortal &portal = _portal_renderer->get_portal(p_portal_id); + const Plane &plane = portal._plane; + + return evaluate_plane(&portal, plane, p_room_ids, r_room_ids_back, r_room_ids_front); +} diff --git a/servers/visual/portals/portal_rooms_bsp.h b/servers/visual/portals/portal_rooms_bsp.h new file mode 100644 index 00000000000..e0ef1ea53d5 --- /dev/null +++ b/servers/visual/portals/portal_rooms_bsp.h @@ -0,0 +1,106 @@ +/*************************************************************************/ +/* portal_rooms_bsp.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_ROOMS_BSP_H +#define PORTAL_ROOMS_BSP_H + +#include "core/local_vector.h" +#include "core/math/aabb.h" +#include "core/math/plane.h" + +class PortalRenderer; +struct VSPortal; +struct VSRoom; + +class PortalRoomsBSP { + struct Node { + Node() { clear(); } + void clear() { + leaf = false; + child[0] = -1; + child[1] = -1; + } + bool leaf; + union { + int32_t child[2]; + struct { + int32_t first_id; + int32_t num_ids; + }; + }; + Plane plane; + }; + + LocalVector _nodes; + LocalVector _room_ids; + + PortalRenderer *_portal_renderer = nullptr; + + const real_t _plane_epsilon = 0.001; + +public: + // build the BSP on level start + void create(PortalRenderer &r_portal_renderer); + + // clear data, and ready for a new level + void clear() { + _nodes.reset(); + _room_ids.reset(); + } + + // the main function, returns a shortlist of rooms that are possible for a test point + const int32_t *find_shortlist(const Vector3 &p_pt, int &r_num_rooms) const; + + // This is a 'sticky' function, it prefers to stay in the previous room where possible. + // This means there is a hysteresis for room choice that may occur if the user creates + // overlapping rooms... + int find_room_within(const PortalRenderer &p_portal_renderer, const Vector3 &p_pos, int p_previous_room_id) const; + +private: + void build(int p_start_node_id, LocalVector p_orig_room_ids); + void detect_internal_room_containment(PortalRenderer &r_portal_renderer); + + int evaluate_portal(int p_portal_id, const LocalVector &p_room_ids, LocalVector *r_room_ids_back = nullptr, LocalVector *r_room_ids_front = nullptr); + + int evaluate_room_split_plane(int p_room_a_id, int p_room_b_id, const LocalVector &p_room_ids, Plane &r_plane, LocalVector *r_room_ids_back = nullptr, LocalVector *r_room_ids_front = nullptr); + + int evaluate_plane(const VSPortal *p_portal, const Plane &p_plane, const LocalVector &p_room_ids, LocalVector *r_room_ids_back = nullptr, LocalVector *r_room_ids_front = nullptr); + + bool calculate_aabb_splitting_plane(const AABB &p_a, const AABB &p_b, Plane &r_plane) const; + bool calculate_freeform_splitting_plane(const VSRoom &p_room_a, const VSRoom &p_room_b, Plane &r_plane) const; + bool find_1d_split_point(real_t p_min_a, real_t p_max_a, real_t p_min_b, real_t p_max_b, real_t &r_split_point) const; + bool test_freeform_plane(const LocalVector &p_verts_a, const LocalVector &p_verts_b, const Plane &p_plane) const; + + void debug_print_tree(int p_node_id = 0, int p_depth = 0); + + void _log(String p_string); +}; + +#endif diff --git a/servers/visual/portals/portal_tracer.cpp b/servers/visual/portals/portal_tracer.cpp new file mode 100644 index 00000000000..00d002760b8 --- /dev/null +++ b/servers/visual/portals/portal_tracer.cpp @@ -0,0 +1,475 @@ +/*************************************************************************/ +/* portal_tracer.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 "portal_tracer.h" + +#include "portal_renderer.h" +#include "servers/visual/visual_server_globals.h" +#include "servers/visual/visual_server_scene.h" + +PortalTracer::PlanesPool::PlanesPool() { + reset(); + + // preallocate the vectors to a reasonable size + for (int n = 0; n < POOL_MAX; n++) { + _planes[n].resize(32); + } +} + +void PortalTracer::PlanesPool::reset() { + for (int n = 0; n < POOL_MAX; n++) { + _freelist[n] = POOL_MAX - n - 1; + } + + _num_free = POOL_MAX; +} + +unsigned int PortalTracer::PlanesPool::request() { + if (!_num_free) { + return -1; + } + + _num_free--; + return _freelist[_num_free]; +} + +void PortalTracer::PlanesPool::free(unsigned int ui) { + DEV_ASSERT(ui < POOL_MAX); + DEV_ASSERT(_num_free < POOL_MAX); + + _freelist[_num_free] = ui; + _num_free++; +} + +void PortalTracer::trace_debug_sprawl(PortalRenderer &p_portal_renderer, const Vector3 &p_pos, int p_start_room_id, TraceResult &r_result) { + _portal_renderer = &p_portal_renderer; + _trace_start_point = p_pos; + _result = &r_result; + + // all the statics should be not hit to start with + _result->clear(); + + // new test, new tick, to prevent hitting objects more than once + // on a test. + _tick++; + + // if the camera is not in a room do nothing + if (p_start_room_id == -1) { + return; + } + + trace_debug_sprawl_recursive(0, p_start_room_id); +} + +void PortalTracer::trace(PortalRenderer &p_portal_renderer, const Vector3 &p_pos, const LocalVector &p_planes, int p_start_room_id, TraceResult &r_result) { + // store local versions to prevent passing around recursive functions + _portal_renderer = &p_portal_renderer; + _trace_start_point = p_pos; + _result = &r_result; + + // The near and far clipping planes needs special treatment. The problem is, if it is + // say a metre from the camera, it will clip out a portal immediately in front of the camera. + // as a result we want to use the near clipping plane for objects, but construct a fake + // near plane at exactly the position of the camera, to clip out portals that are behind us. + _near_and_far_planes[0] = p_planes[0]; + _near_and_far_planes[1] = p_planes[1]; + + // all the statics should be not hit to start with + _result->clear(); + + // new test, new tick, to prevent hitting objects more than once + // on a test. + _tick++; + + // if the camera is not in a room do nothing + // (this will return no hits, but is unlikely because the find_rooms lookup will return the nearest + // room even if not inside) + if (p_start_room_id == -1) { + return; + } + + // start off the trace with the planes from the camera + LocalVector cam_planes; + cam_planes = p_planes; + + if (p_portal_renderer.get_cull_using_pvs()) { + trace_pvs(p_start_room_id, cam_planes); + } else { + // alternative : instead of copying straight, we create the first (near) clipping + // plane manually, at 0 distance from the camera. This ensures that portals will not be + // missed, while still culling portals and objects behind us. If we use the actual near clipping plane + // then a portal in front of the camera may not be seen through, giving glitches + cam_planes[0] = Plane(p_pos, cam_planes[0].normal); + + TraceParams params; + params.use_pvs = p_portal_renderer.get_pvs().is_loaded(); + + // create bitfield + if (params.use_pvs) { + const PVS &pvs = _portal_renderer->get_pvs(); + if (!pvs.get_pvs_size()) { + params.use_pvs = false; + } else { + // decompress a simple to read roomlist bitfield (could use bits maybe but bytes ok for now) + params.decompressed_room_pvs = nullptr; + params.decompressed_room_pvs = (uint8_t *)alloca(sizeof(uint8_t) * pvs.get_pvs_size()); + memset(params.decompressed_room_pvs, 0, sizeof(uint8_t) * pvs.get_pvs_size()); + const VSRoom &source_room = _portal_renderer->get_room(p_start_room_id); + + for (int n = 0; n < source_room._pvs_size; n++) { + int room_id = pvs.get_pvs_room_id(source_room._pvs_first + n); + params.decompressed_room_pvs[room_id] = 255; + } + } + } + + trace_recursive(params, 0, p_start_room_id, cam_planes); + } +} + +void PortalTracer::cull_roamers(const VSRoom &p_room, const LocalVector &p_planes) { + int num_roamers = p_room._roamer_pool_ids.size(); + + for (int n = 0; n < num_roamers; n++) { + uint32_t pool_id = p_room._roamer_pool_ids[n]; + + PortalRenderer::Moving &moving = _portal_renderer->get_pool_moving(pool_id); + + // done already? + if (moving.last_tick_hit == _tick) { + continue; + } + + // mark as done + moving.last_tick_hit = _tick; + + if (test_cull_inside(moving.exact_aabb, p_planes)) { + _result->visible_roamer_pool_ids.push_back(pool_id); + } + } +} + +void PortalTracer::cull_statics_debug_sprawl(const VSRoom &p_room) { + int num_statics = p_room._static_ids.size(); + + for (int n = 0; n < num_statics; n++) { + uint32_t static_id = p_room._static_ids[n]; + + // VSStatic &stat = _portal_renderer->get_static(static_id); + + // deal with dynamic stats + // if (stat.dynamic) { + // VSG::scene->_instance_get_transformed_aabb(stat.instance, stat.aabb); + // } + + // set the visible bit if not set + if (!_result->bf_visible_statics.check_and_set(static_id)) { + _result->visible_static_ids.push_back(static_id); + } + } +} + +void PortalTracer::cull_statics(const VSRoom &p_room, const LocalVector &p_planes) { + int num_statics = p_room._static_ids.size(); + + for (int n = 0; n < num_statics; n++) { + uint32_t static_id = p_room._static_ids[n]; + + VSStatic &stat = _portal_renderer->get_static(static_id); + + // deal with dynamic stats + if (stat.dynamic) { + VSG::scene->_instance_get_transformed_aabb(stat.instance, stat.aabb); + } + + // estimate the radius .. for now + const AABB &bb = stat.aabb; + + // print("\t\t\tculling object " + pObj->get_name()); + + if (test_cull_inside(bb, p_planes)) { + // bypass the bitfield for now and just show / hide + //stat.show(bShow); + + // set the visible bit if not set + if (_result->bf_visible_statics.check_and_set(static_id)) { + // if wasn't previously set, add to the visible list + _result->visible_static_ids.push_back(static_id); + } + } + + } // for n through statics +} + +int PortalTracer::trace_globals(const LocalVector &p_planes, VSInstance **p_result_array, int first_result, int p_result_max, uint32_t p_mask) { + uint32_t num_globals = _portal_renderer->get_num_moving_globals(); + int current_result = first_result; + + for (uint32_t n = 0; n < num_globals; n++) { + const PortalRenderer::Moving &moving = _portal_renderer->get_moving_global(n); + +#ifdef PORTAL_RENDERER_STORE_MOVING_RIDS + // debug check the instance is valid + void *vss_instance = VSG::scene->_instance_get_from_rid(moving.instance_rid); + + if (vss_instance) { +#endif + if (test_cull_inside(moving.exact_aabb, p_planes, false)) { + if (VSG::scene->_instance_cull_check(moving.instance, p_mask)) { + p_result_array[current_result++] = moving.instance; + + // full up? + if (current_result >= p_result_max) { + return current_result; + } + } + } + +#ifdef PORTAL_RENDERER_STORE_MOVING_RIDS + } else { + WARN_PRINT("vss instance is null " + PortalRenderer::_addr_to_string(moving.instance)); + } +#endif + } + + return current_result; +} + +void PortalTracer::trace_debug_sprawl_recursive(int p_depth, int p_room_id) { + if (p_depth > 1) { + return; + } + + // prevent too much depth + ERR_FAIL_COND_MSG(p_depth > 8, "Portal Depth Limit reached"); + + // get the room + const VSRoom &room = _portal_renderer->get_room(p_room_id); + + int num_portals = room._portal_ids.size(); + + for (int p = 0; p < num_portals; p++) { + const VSPortal &portal = _portal_renderer->get_portal(room._portal_ids[p]); + + if (!portal._active) { + continue; + } + + cull_statics_debug_sprawl(room); + + // everything depends on whether the portal is incoming or outgoing. + int outgoing = 1; + + int room_a_id = portal._linkedroom_ID[0]; + if (room_a_id != p_room_id) { + outgoing = 0; + DEV_ASSERT(portal._linkedroom_ID[1] == p_room_id); + } + + // trace through this portal to the next room + int linked_room_id = portal._linkedroom_ID[outgoing]; + + if (linked_room_id != -1) { + trace_debug_sprawl_recursive(p_depth + 1, linked_room_id); + } // if a linked room exists + + } // for p through portals +} + +void PortalTracer::trace_pvs(int p_source_room_id, const LocalVector &p_planes) { + const PVS &pvs = _portal_renderer->get_pvs(); + const VSRoom &source_room = _portal_renderer->get_room(p_source_room_id); + + for (int r = 0; r < source_room._pvs_size; r++) { + int room_id = pvs.get_pvs_room_id(source_room._pvs_first + r); + + // get the room + const VSRoom &room = _portal_renderer->get_room(room_id); + + cull_statics(room, p_planes); + cull_roamers(room, p_planes); + } +} + +void PortalTracer::trace_recursive(const TraceParams &p_params, int p_depth, int p_room_id, const LocalVector &p_planes) { + // prevent too much depth + if (p_depth > _depth_limit) { + WARN_PRINT_ONCE("Portal Depth Limit reached (seeing through too many portals)"); + return; + } + + // get the room + const VSRoom &room = _portal_renderer->get_room(p_room_id); + + cull_statics(room, p_planes); + cull_roamers(room, p_planes); + + int num_portals = room._portal_ids.size(); + + for (int p = 0; p < num_portals; p++) { + const VSPortal &portal = _portal_renderer->get_portal(room._portal_ids[p]); + + // portals can be switched on and off at runtime, like opening and closing a door + if (!portal._active) { + continue; + } + + // everything depends on whether the portal is incoming or outgoing. + // if incoming we reverse the logic. + int outgoing = 1; + + int room_a_id = portal._linkedroom_ID[0]; + if (room_a_id != p_room_id) { + outgoing = 0; + DEV_ASSERT(portal._linkedroom_ID[1] == p_room_id); + } + + // trace through this portal to the next room + int linked_room_id = portal._linkedroom_ID[outgoing]; + + // cull by PVS + if (p_params.use_pvs && (!p_params.decompressed_room_pvs[linked_room_id])) { + continue; + } + + // cull by portal angle to camera. + + // much better way of culling portals by direction to camera... + // instead of using dot product with a varying view direction, we simply find which side of the portal + // plane the camera is on! If it is behind, the portal can be seen through, if in front, it can't + real_t dist_cam = portal._plane.distance_to(_trace_start_point); + + if (!outgoing) { + dist_cam = -dist_cam; + } + + if (dist_cam >= 0.0) { + continue; + } + + // is it culled by the planes? + VSPortal::ClipResult overall_res = VSPortal::ClipResult::CLIP_INSIDE; + + // while clipping to the planes we maintain a list of partial planes, so we can add them to the + // recursive next iteration of planes to check + static LocalVector partial_planes; + partial_planes.clear(); + + // for portals, we want to ignore the near clipping plane, as we might be right on the edge of a doorway + // and still want to look through the portal. + // so earlier we have set it that the first plane (ASSUMING that plane zero is the near clipping plane) + // starts from the camera position, and NOT the actual near clipping plane. + // if we need quite a distant near plane, we may need a different strategy. + for (uint32_t l = 0; l < p_planes.size(); l++) { + VSPortal::ClipResult res = portal.clip_with_plane(p_planes[l]); + + switch (res) { + case VSPortal::ClipResult::CLIP_OUTSIDE: { + overall_res = res; + } break; + case VSPortal::ClipResult::CLIP_PARTIAL: { + // if the portal intersects one of the planes, we should take this plane into account + // in the next call of this recursive trace, because it can be used to cull out more objects + overall_res = res; + partial_planes.push_back(l); + } break; + default: // suppress warning + break; + } + + // if the portal was totally outside the 'frustum' then we can ignore it + if (overall_res == VSPortal::ClipResult::CLIP_OUTSIDE) + break; + } + + // this portal is culled + if (overall_res == VSPortal::ClipResult::CLIP_OUTSIDE) { + continue; + } + + // hopefully the portal actually leads somewhere... + if (linked_room_id != -1) { + // we need some new planes + unsigned int pool_mem = _planes_pool.request(); + + // if the planes pool is not empty, we got some planes, and can recurse + if (pool_mem != (unsigned int)-1) { + // get a new vector of planes from the pool + LocalVector &new_planes = _planes_pool.get(pool_mem); + + // makes sure there are none left over (as the pool may not clear them) + new_planes.clear(); + + // if portal is totally inside the planes, don't copy the old planes .. + // i.e. we can now cull using the portal and forget about the rest of the frustum (yay) + // note that this loses the far clipping plane .. but that shouldn't be important usually? + // (maybe we might need to account for this in future .. look for issues) + if (overall_res != VSPortal::ClipResult::CLIP_INSIDE) { + // if it WASNT totally inside the existing frustum, we also need to add any existing planes + // that cut the portal. + for (uint32_t n = 0; n < partial_planes.size(); n++) { + new_planes.push_back(p_planes[partial_planes[n]]); + } + } + + // we will always add the portals planes. This could probably be optimized, as some + // portal planes may be culled out by partial planes... NYI + portal.add_planes(_trace_start_point, new_planes, outgoing != 0); + + // always add the far plane. It is likely the portal is inside the far plane, + // but it is still needed in future for culling portals and objects. + // note that there is a small possibility of far plane being added twice here + // in some situations, but I don't think it should be a problem. + // The fake near plane BTW is almost never added (otherwise it would prematurely + // break traversal through the portals), so near clipping must be done + // explicitly on objects. + new_planes.push_back(_near_and_far_planes[1]); + + // go and do the whole lot again in the next room + trace_recursive(p_params, p_depth + 1, linked_room_id, new_planes); + + // no longer need these planes, return them to the pool + _planes_pool.free(pool_mem); + + } // pool mem allocated + else { + // planes pool is empty! + // This will happen if the view goes through shedloads of portals + // The solution is either to increase the plane pool size, or not build levels + // with views through multiple portals. Looking through multiple portals is likely to be + // slow anyway because of the number of planes to test. + WARN_PRINT_ONCE("planes pool is empty"); + // note we also have a depth check at the top of this function. Which will probably get hit + // before the pool gets empty. + } + + } // if a linked room exists + } // for p through portals +} diff --git a/servers/visual/portals/portal_tracer.h b/servers/visual/portals/portal_tracer.h new file mode 100644 index 00000000000..7f0de7856a7 --- /dev/null +++ b/servers/visual/portals/portal_tracer.h @@ -0,0 +1,165 @@ +/*************************************************************************/ +/* portal_tracer.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef PORTAL_TRACER_H +#define PORTAL_TRACER_H + +#include "core/bitfield_dynamic.h" +#include "core/local_vector.h" +#include "portal_types.h" + +#ifdef TOOLS_ENABLED +// use this for checking for instance lifetime errors, disable normally +//#define PORTAL_RENDERER_STORE_MOVING_RIDS +#endif + +class PortalRenderer; +struct VSRoom; + +class PortalTracer { +public: + // a bitfield for which statics have been hit this time, + // and a list of showing statics + class TraceResult { + public: + void create(int p_num_statics) { + bf_visible_statics.create(p_num_statics); + } + void clear() { + bf_visible_statics.blank(); + visible_static_ids.clear(); + visible_roamer_pool_ids.clear(); + } + + BitFieldDynamic bf_visible_statics; + LocalVector visible_static_ids; + LocalVector visible_roamer_pool_ids; + }; + + struct TraceParams { + bool use_pvs; + uint8_t *decompressed_room_pvs; + }; + + // The recursive visibility function needs to allocate lists of planes each time a room is traversed. + // Instead of doing this allocation on the fly we will use a pool which should be much faster and nearer + // constant time. + + // Note this simple pool isn't super optimal but should be fine for now. + class PlanesPool { + public: + // maximum number of vectors in the pool + const static int POOL_MAX = 32; + + void reset(); + + // request a new vector of planes .. returns the pool id, or -1 if pool is empty + unsigned int request(); + + // return pool id to the pool + void free(unsigned int ui); + + LocalVector &get(unsigned int ui) { return _planes[ui]; } + + PlanesPool(); + + private: + LocalVector _planes[POOL_MAX]; + + // list of pool ids that are free and can be allocated + uint8_t _freelist[POOL_MAX]; + uint32_t _num_free; + }; + + // for debugging, instead of doing a normal trace, show the objects that are sprawled from the current room + void trace_debug_sprawl(PortalRenderer &p_portal_renderer, const Vector3 &p_pos, int p_start_room_id, TraceResult &r_result); + + // trace statics, dynamics and roaming + void trace(PortalRenderer &p_portal_renderer, const Vector3 &p_pos, const LocalVector &p_planes, int p_start_room_id, TraceResult &r_result); + + // globals are handled separately as they don't care about the rooms + int trace_globals(const LocalVector &p_planes, VSInstance **p_result_array, int first_result, int p_result_max, uint32_t p_mask); + + void set_depth_limit(int p_limit) { _depth_limit = p_limit; } + +private: + // main tracing function is recursive + void trace_recursive(const TraceParams &p_params, int p_depth, int p_room_id, const LocalVector &p_planes); + + // use pvs to cull instead of dynamically using portals + // this is a faster trace but less accurate. Only possible if PVS has been generated. + void trace_pvs(int p_source_room_id, const LocalVector &p_planes); + + // debug version + void trace_debug_sprawl_recursive(int p_depth, int p_room_id); + + void cull_statics(const VSRoom &p_room, const LocalVector &p_planes); + void cull_statics_debug_sprawl(const VSRoom &p_room); + void cull_roamers(const VSRoom &p_room, const LocalVector &p_planes); + + // if an aabb is in front of any of the culling planes, it can't be seen so returns false + bool test_cull_inside(const AABB &p_aabb, const LocalVector &p_planes, bool p_test_explicit_near_plane = true) const { + for (unsigned int p = 0; p < p_planes.size(); p++) { + real_t r_min, r_max; + p_aabb.project_range_in_plane(p_planes[p], r_min, r_max); + + if (r_min > 0.0) { + return false; + } + } + + if (p_test_explicit_near_plane) { + real_t r_min, r_max; + p_aabb.project_range_in_plane(_near_and_far_planes[0], r_min, r_max); + + if (r_min > 0.0) { + return false; + } + } + + return true; + } + + // local versions to prevent passing around the recursive functions + PortalRenderer *_portal_renderer = nullptr; + Vector3 _trace_start_point; + TraceResult *_result = nullptr; + Plane _near_and_far_planes[2]; + + PlanesPool _planes_pool; + int _depth_limit = 16; + + // keep a tick count for each trace, to avoid adding a visible + // object to the hit list more than once per tick + // (this makes more sense than bitfield for moving objects) + uint32_t _tick = 0; +}; + +#endif diff --git a/servers/visual/portals/portal_types.cpp b/servers/visual/portals/portal_types.cpp new file mode 100644 index 00000000000..240f943c020 --- /dev/null +++ b/servers/visual/portals/portal_types.cpp @@ -0,0 +1,234 @@ +/*************************************************************************/ +/* portal_types.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 "portal_types.h" + +VSPortal::ClipResult VSPortal::clip_with_plane(const Plane &p) const { + int nOutside = 0; + int nPoints = _pts_world.size(); + + for (int n = 0; n < nPoints; n++) { + real_t d = p.distance_to(_pts_world[n]); + + if (d >= 0.0) { + nOutside++; + } + } + + if (nOutside == nPoints) { + return CLIP_OUTSIDE; + } + + if (nOutside == 0) { + return CLIP_INSIDE; + } + + return CLIP_PARTIAL; +} + +void VSPortal::add_pvs_planes(const VSPortal &p_first, bool p_first_outgoing, LocalVector &r_planes, bool p_outgoing) const { + int num_a = p_first._pts_world.size(); + int num_b = _pts_world.size(); + + // get the world points of both in the correct order based on whether outgoing .. note this isn't very efficient... + Vector3 *pts_a = (Vector3 *)alloca(num_a * sizeof(Vector3)); + Vector3 *pts_b = (Vector3 *)alloca(num_b * sizeof(Vector3)); + + if (p_first_outgoing) { + // straight copy + for (int n = 0; n < num_a; n++) { + pts_a[n] = p_first._pts_world[n]; + } + } else { + for (int n = 0; n < num_a; n++) { + pts_a[n] = p_first._pts_world[num_a - 1 - n]; + } + } + + if (p_outgoing) { + // straight copy + for (int n = 0; n < num_b; n++) { + pts_b[n] = _pts_world[n]; + } + } else { + for (int n = 0; n < num_b; n++) { + pts_b[n] = _pts_world[num_b - 1 - n]; + } + } + + // go through and try every combination of points to form a clipping plane + for (int pvA = 0; pvA < num_a; pvA++) { + for (int pvB = 0; pvB < num_b; pvB++) { + int pvC = (pvB + 1) % num_b; + + // three verts + const Vector3 &va = pts_a[pvA]; + const Vector3 &vb = pts_b[pvB]; + const Vector3 &vc = pts_b[pvC]; + + // create plane + Plane plane = Plane(va, vc, vb); + + // already exists similar plane, so ignore + if (_is_plane_duplicate(plane, r_planes)) { + continue; + } + + if (_test_pvs_plane(-plane, pts_a, num_a, pts_b, num_b)) { + // add the plane + r_planes.push_back(plane); + } + + } // for pvB + } // for pvA +} + +// typically we will end up with a bunch of duplicate planes being trying to be added for a portal. +// we can remove any that are too similar +bool VSPortal::_is_plane_duplicate(const Plane &p_plane, const LocalVector &p_planes) const { + const real_t epsilon_d = 0.001; + const real_t epsilon_dot = 0.98; + + for (int n = 0; n < p_planes.size(); n++) { + const Plane &p = p_planes[n]; + if (Math::absf(p_plane.d - p.d) > epsilon_d) { + continue; + } + + real_t dot = p_plane.normal.dot(p.normal); + if (dot < epsilon_dot) { + continue; + } + + // match + return true; + } + + return false; +} + +bool VSPortal::_pvs_is_outside_planes(const LocalVector &p_planes) const { + // short version + const Vector &pts = _pts_world; + int nPoints = pts.size(); + + const real_t epsilon = 0.1; + + for (int p = 0; p < p_planes.size(); p++) { + for (int n = 0; n < nPoints; n++) { + const Vector3 &pt = pts[n]; + real_t dist = p_planes[p].distance_to(pt); + + if (dist < -epsilon) { + return false; + } + } + } + + return true; +} + +bool VSPortal::_test_pvs_plane(const Plane &p_plane, const Vector3 *pts_a, int num_a, const Vector3 *pts_b, int num_b) const { + const real_t epsilon = 0.1; + + for (int n = 0; n < num_a; n++) { + real_t dist = p_plane.distance_to(pts_a[n]); + + if (dist > epsilon) { + return false; + } + } + + for (int n = 0; n < num_b; n++) { + real_t dist = p_plane.distance_to(pts_b[n]); + + if (dist < -epsilon) { + return false; + } + } + + return true; +} + +// add clipping planes to the vector formed by each portal edge and the camera +void VSPortal::add_planes(const Vector3 &p_cam, LocalVector &r_planes, bool p_outgoing) const { + // short version + const Vector &pts = _pts_world; + + int nPoints = pts.size(); + ERR_FAIL_COND(nPoints < 3); + + Plane p; + + int offset_a, offset_b; + if (p_outgoing) { + offset_a = 0; + offset_b = -1; + } else { + offset_a = -1; + offset_b = 0; + } + + for (int n = 1; n < nPoints; n++) { + p = Plane(p_cam, pts[n + offset_a], pts[n + offset_b]); + + // detect null plane + // if (p.normal.length_squared() < 0.1) + // { + // print("NULL plane detected from points : "); + // print(ptCam + pts[n] + pts[n-1]); + // } + r_planes.push_back(p); + debug_check_plane_validity(p); + } + + // first and last + if (p_outgoing) { + p = Plane(p_cam, pts[0], pts[nPoints - 1]); + } else { + p = Plane(p_cam, pts[nPoints - 1], pts[0]); + } + + r_planes.push_back(p); + debug_check_plane_validity(p); + + // debug + // if (!manager.m_bDebugPlanes) + // return; + + // for (int n=0; n &r_planes, bool p_outgoing) const; + void debug_check_plane_validity(const Plane &p) const; + + void add_pvs_planes(const VSPortal &p_first, bool p_first_outgoing, LocalVector &r_planes, bool p_outgoing) const; + bool _pvs_is_outside_planes(const LocalVector &p_planes) const; + +private: + bool _test_pvs_plane(const Plane &p_plane, const Vector3 *pts_a, int num_a, const Vector3 *pts_b, int num_b) const; + bool _is_plane_duplicate(const Plane &p_plane, const LocalVector &p_planes) const; + +public: + // returns the room to if if crosses, or else returns -1 + int geometry_crosses_portal(int p_room_from, const AABB &p_aabb, const Vector &p_pts) const { + // first aabb check + if (!p_aabb.intersects(_aabb)) { + return -1; + } + + // disallow sprawling from outer to inner rooms. + // This is a convenience feature that stops e.g. terrain sprawling into + // a building. If you want geometry to feature in the inner room and the outer, + // simply place it in the inner room. + if (_internal && (_linkedroom_ID[0] != p_room_from)) { + return -1; + } + + // accurate check use portal triangles + // NYI + + const real_t epsilon = _margin; + + if (p_room_from == _linkedroom_ID[0]) { + // outward + // how far do points project over the portal + for (int n = 0; n < p_pts.size(); n++) { + real_t dist = _plane.distance_to(p_pts[n]); + if (dist > epsilon) { + return _linkedroom_ID[1]; + } + } + } else { + // inward + DEV_ASSERT(p_room_from == _linkedroom_ID[1]); + for (int n = 0; n < p_pts.size(); n++) { + real_t dist = _plane.distance_to(p_pts[n]); + if (dist < -epsilon) { + return _linkedroom_ID[0]; + } + } + } + + // no points crossed the portal + return -1; + } + + // returns the room to if if crosses, or else returns -1 + int crosses_portal(int p_room_from, const AABB &p_aabb, bool p_disallow_crossing_internal = false, bool p_accurate_check = false) const { + // first aabb check + if (!p_aabb.intersects(_aabb)) { + return -1; + } + + // disallow sprawling from outer to inner rooms. + // This is a convenience feature that stops e.g. terrain sprawling into + // a building. If you want geometry to feature in the inner room and the outer, + // simply place it in the inner room. + if (p_disallow_crossing_internal && _internal && (_linkedroom_ID[0] != p_room_from)) { + return -1; + } + + // accurate check use portal triangles + // NYI + real_t r_min, r_max; + p_aabb.project_range_in_plane(_plane, r_min, r_max); + + const real_t epsilon = _margin; //10.0; + + if (p_room_from == _linkedroom_ID[0]) { + if (r_max > epsilon) { + return _linkedroom_ID[1]; + } else { + return -1; + } + } + + DEV_ASSERT(p_room_from == _linkedroom_ID[1]); + if (r_min < -epsilon) { + return _linkedroom_ID[0]; + } + + return -1; + } + + // the portal needs a list of unique world points (in order, clockwise?) + LocalVector _pts_world; + + // portal plane + Plane _plane; + + // aabb for quick bounds checks + AABB _aabb; + + uint32_t _portal_id = -1; + + // in order to detect objects crossing portals, + // an extension margin can be used to prevent objects + // that *just* cross the portal extending into the next room + real_t _margin = 1.0; + + // these are room IDs, or -1 if unset + int _linkedroom_ID[2]; + + // can be turned on and off by the user + bool _active = true; + + // internal portals have slightly different behaviour + bool _internal = false; + + VSPortal() { + _linkedroom_ID[0] = -1; + _linkedroom_ID[1] = -1; + } +}; + +struct VSRoomGroup { + void create() { + } + + void destroy() { + _room_ids.reset(); + } + + // used for calculating gameplay notifications + uint32_t last_gameplay_tick_hit = 0; + + ObjectID _godot_instance_ID = 0; + + LocalVector _room_ids; +}; + +struct VSRoom { + // explicit create and destroy rather than constructors / destructors + // because we are using a pool so objects may be reused + void create() { + _room_ID = -1; + _aabb = AABB(); + } + + void destroy() { + _static_ids.reset(); + _static_ghost_ids.reset(); + _planes.reset(); + _verts.reset(); + _portal_ids.reset(); + _roamer_pool_ids.reset(); + _rghost_pool_ids.reset(); + _roomgroup_ids.reset(); + _pvs_first = 0; + _pvs_size = 0; + _secondary_pvs_first = 0; + _secondary_pvs_size = 0; + _priority = 0; + _contains_internal_rooms = false; + } + + void cleanup_after_conversion() { + _verts.reset(); + } + + void rooms_and_portals_clear() { + destroy(); + _aabb = AABB(); + // don't unset the room_ID here, because rooms may be accessed after this is called + } + + // this isn't just useful for checking whether a point is within (i.e. returned value is 0 or less) + // it is useful for finding the CLOSEST room to a point (by plane distance, doesn't take into account corners etc) + real_t is_point_within(const Vector3 &p_pos) const { + // inside by default + real_t closest_dist = -FLT_MAX; + + for (int n = 0; n < _planes.size(); n++) { + real_t dist = _planes[n].distance_to(p_pos); + if (dist > closest_dist) { + closest_dist = dist; + } + } + + return closest_dist; + } + + // not super fast, but there shouldn't be that many roamers per room + bool remove_roamer(uint32_t p_pool_id) { + for (int n = 0; n < _roamer_pool_ids.size(); n++) { + if (_roamer_pool_ids[n] == p_pool_id) { + _roamer_pool_ids.remove_unordered(n); + return true; + } + } + return false; + } + + bool remove_rghost(uint32_t p_pool_id) { + for (int n = 0; n < _rghost_pool_ids.size(); n++) { + if (_rghost_pool_ids[n] == p_pool_id) { + _rghost_pool_ids.remove_unordered(n); + return true; + } + } + return false; + } + + void add_roamer(uint32_t p_pool_id) { + _roamer_pool_ids.push_back(p_pool_id); + } + + void add_rghost(uint32_t p_pool_id) { + _rghost_pool_ids.push_back(p_pool_id); + } + + // keep a list of statics in the room .. statics may appear + // in more than one room due to sprawling! + LocalVector _static_ids; + LocalVector _static_ghost_ids; + + // very rough + AABB _aabb; + + int32_t _room_ID = -1; + ObjectID _godot_instance_ID = 0; + + // rooms with a higher priority are internal rooms .. + // rooms within a room. These will be chosen in preference + // when finding the room within, when within more than one room. + // Example, house in a terrain room. + int32_t _priority = 0; + + bool _contains_internal_rooms = false; + + int32_t _pvs_first = 0; + int32_t _secondary_pvs_first = 0; + uint16_t _pvs_size = 0; + uint16_t _secondary_pvs_size = 0; + + // used for calculating gameplay notifications + uint32_t last_gameplay_tick_hit = 0; + + // convex hull of the room, either determined by geometry or manual bound + LocalVector _planes; + + // vertices of the corners of the hull, passed from the scene tree + // (note these don't take account of any final portal planes adjusted by the portal renderer) + LocalVector _verts; + + // which portals are in the room (ingoing and outgoing) + LocalVector _portal_ids; + + // roaming movers currently in the room + LocalVector _roamer_pool_ids; + LocalVector _rghost_pool_ids; + + // keep track of which roomgroups the room is in, that + // way we can switch on and off roomgroups as they enter / exit view + LocalVector _roomgroup_ids; +}; + +#endif diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h index 6893bf07106..19e3e62fa50 100644 --- a/servers/visual/visual_server_raster.h +++ b/servers/visual/visual_server_raster.h @@ -558,6 +558,44 @@ public: BIND2(instance_set_extra_visibility_margin, RID, real_t) + // Portals + BIND2(instance_set_portal_mode, RID, InstancePortalMode) + + BIND0R(RID, ghost_create) + BIND4(ghost_set_scenario, RID, RID, ObjectID, const AABB &) + BIND2(ghost_update, RID, const AABB &) + + BIND0R(RID, portal_create) + BIND2(portal_set_scenario, RID, RID) + BIND3(portal_set_geometry, RID, const Vector &, float) + BIND4(portal_link, RID, RID, RID, bool) + BIND2(portal_set_active, RID, bool) + + // Roomgroups + BIND0R(RID, roomgroup_create) + BIND2(roomgroup_prepare, RID, ObjectID) + BIND2(roomgroup_set_scenario, RID, RID) + BIND2(roomgroup_add_room, RID, RID) + + // Rooms + BIND0R(RID, room_create) + BIND2(room_set_scenario, RID, RID) + BIND4(room_add_instance, RID, RID, const AABB &, const Vector &) + BIND3(room_add_ghost, RID, ObjectID, const AABB &) + BIND5(room_set_bound, RID, ObjectID, const Vector &, const AABB &, const Vector &) + BIND2(room_prepare, RID, int32_t) + BIND1(rooms_and_portals_clear, RID) + BIND1(rooms_unload, RID) + BIND6(rooms_finalize, RID, bool, bool, bool, bool, String) + BIND4(rooms_override_camera, RID, bool, const Vector3 &, const Vector *) + BIND2(rooms_set_active, RID, bool) + BIND2(rooms_set_params, RID, int) + BIND3(rooms_set_debug_feature, RID, RoomsDebugFeature, bool) + BIND2(rooms_update_gameplay_monitor, RID, const Vector &) + + // Callbacks + BIND1(callbacks_register, VisualServerCallbacks *) + // don't use these in a game! BIND2RC(Vector, instances_cull_aabb, const AABB &, RID) BIND3RC(Vector, instances_cull_ray, const Vector3 &, const Vector3 &, RID) diff --git a/servers/visual/visual_server_scene.cpp b/servers/visual/visual_server_scene.cpp index 8794cf0e3cd..fb1c07faa7f 100644 --- a/servers/visual/visual_server_scene.cpp +++ b/servers/visual/visual_server_scene.cpp @@ -101,7 +101,7 @@ VisualServerScene::SpatialPartitionID VisualServerScene::SpatialPartitioningScen #if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED) // we are relying on this instance to be valid in order to pass // the visible flag to the bvh. - CRASH_COND(!p_userdata); + DEV_ASSERT(p_userdata); #endif return _bvh.create(p_userdata, p_userdata->visible, p_aabb, p_subindex, p_pairable, p_pairable_type, p_pairable_mask) + 1; } @@ -601,6 +601,11 @@ void VisualServerScene::instance_set_scenario(RID p_instance, RID p_scenario) { instance->spatial_partition_id = 0; } + // handle occlusion changes + if (instance->occlusion_handle) { + _instance_destroy_occlusion_rep(instance); + } + switch (instance->base_type) { case VS::INSTANCE_LIGHT: { InstanceLightData *light = static_cast(instance->base_data); @@ -653,6 +658,9 @@ void VisualServerScene::instance_set_scenario(RID p_instance, RID p_scenario) { } } + // handle occlusion changes if necessary + _instance_create_occlusion_rep(instance); + _instance_queue_update(instance, true, true); } } @@ -889,6 +897,412 @@ void VisualServerScene::instance_set_extra_visibility_margin(RID p_instance, rea _instance_queue_update(instance, true, false); } +// Portals +void VisualServerScene::instance_set_portal_mode(RID p_instance, VisualServer::InstancePortalMode p_mode) { + Instance *instance = instance_owner.get(p_instance); + ERR_FAIL_COND(!instance); + + // no change? + if (instance->portal_mode == p_mode) { + return; + } + + // should this happen? + if (!instance->scenario) { + instance->portal_mode = p_mode; + return; + } + + // destroy previous occlusion instance? + _instance_destroy_occlusion_rep(instance); + instance->portal_mode = p_mode; + _instance_create_occlusion_rep(instance); +} + +void VisualServerScene::_instance_create_occlusion_rep(Instance *p_instance) { + ERR_FAIL_COND(!p_instance); + ERR_FAIL_COND(!p_instance->scenario); + + switch (p_instance->portal_mode) { + default: { + p_instance->occlusion_handle = 0; + } break; + case VisualServer::InstancePortalMode::INSTANCE_PORTAL_MODE_ROAMING: { + p_instance->occlusion_handle = p_instance->scenario->_portal_renderer.instance_moving_create(p_instance, p_instance->self, false, p_instance->transformed_aabb); + } break; + case VisualServer::InstancePortalMode::INSTANCE_PORTAL_MODE_GLOBAL: { + p_instance->occlusion_handle = p_instance->scenario->_portal_renderer.instance_moving_create(p_instance, p_instance->self, true, p_instance->transformed_aabb); + } break; + } +} + +void VisualServerScene::_instance_destroy_occlusion_rep(Instance *p_instance) { + ERR_FAIL_COND(!p_instance); + ERR_FAIL_COND(!p_instance->scenario); + + // not an error, can occur + if (!p_instance->occlusion_handle) { + return; + } + + p_instance->scenario->_portal_renderer.instance_moving_destroy(p_instance->occlusion_handle); + + // unset + p_instance->occlusion_handle = 0; +} + +void *VisualServerScene::_instance_get_from_rid(RID p_instance) { + Instance *instance = instance_owner.get(p_instance); + return instance; +} + +bool VisualServerScene::_instance_get_transformed_aabb(RID p_instance, AABB &r_aabb) { + Instance *instance = instance_owner.get(p_instance); + ERR_FAIL_NULL_V(instance, false); + + r_aabb = instance->transformed_aabb; + + return true; +} + +// the portal has to be associated with a scenario, this is assumed to be +// the same scenario as the portal node +RID VisualServerScene::portal_create() { + Portal *portal = memnew(Portal); + ERR_FAIL_COND_V(!portal, RID()); + RID portal_rid = portal_owner.make_rid(portal); + return portal_rid; +} + +// should not be called multiple times, different scenarios etc, but just in case, we will support this +void VisualServerScene::portal_set_scenario(RID p_portal, RID p_scenario) { + Portal *portal = portal_owner.getornull(p_portal); + ERR_FAIL_COND(!portal); + Scenario *scenario = scenario_owner.getornull(p_scenario); + + // noop? + if (portal->scenario == scenario) { + return; + } + + // if the portal is in a scenario already, remove it + if (portal->scenario) { + portal->scenario->_portal_renderer.portal_destroy(portal->scenario_portal_id); + portal->scenario = nullptr; + portal->scenario_portal_id = 0; + } + + // create when entering the world + if (scenario) { + portal->scenario = scenario; + + // defer the actual creation to here + portal->scenario_portal_id = scenario->_portal_renderer.portal_create(); + } +} + +void VisualServerScene::portal_set_geometry(RID p_portal, const Vector &p_points, float p_margin) { + Portal *portal = portal_owner.getornull(p_portal); + ERR_FAIL_COND(!portal); + ERR_FAIL_COND(!portal->scenario); + portal->scenario->_portal_renderer.portal_set_geometry(portal->scenario_portal_id, p_points); +} + +void VisualServerScene::portal_link(RID p_portal, RID p_room_from, RID p_room_to, bool p_two_way) { + Portal *portal = portal_owner.getornull(p_portal); + ERR_FAIL_COND(!portal); + ERR_FAIL_COND(!portal->scenario); + + Room *room_from = room_owner.getornull(p_room_from); + ERR_FAIL_COND(!room_from); + Room *room_to = room_owner.getornull(p_room_to); + ERR_FAIL_COND(!room_to); + + portal->scenario->_portal_renderer.portal_link(portal->scenario_portal_id, room_from->scenario_room_id, room_to->scenario_room_id, p_two_way); +} + +void VisualServerScene::portal_set_active(RID p_portal, bool p_active) { + Portal *portal = portal_owner.getornull(p_portal); + ERR_FAIL_COND(!portal); + ERR_FAIL_COND(!portal->scenario); + portal->scenario->_portal_renderer.portal_set_active(portal->scenario_portal_id, p_active); +} + +RID VisualServerScene::ghost_create() { + Ghost *ci = memnew(Ghost); + ERR_FAIL_COND_V(!ci, RID()); + RID ci_rid = ghost_owner.make_rid(ci); + return ci_rid; +} + +void VisualServerScene::ghost_set_scenario(RID p_ghost, RID p_scenario, ObjectID p_id, const AABB &p_aabb) { + Ghost *ci = ghost_owner.getornull(p_ghost); + ERR_FAIL_COND(!ci); + + ci->aabb = p_aabb; + ci->object_id = p_id; + + Scenario *scenario = scenario_owner.getornull(p_scenario); + + // noop? + if (ci->scenario == scenario) { + return; + } + + // if the portal is in a scenario already, remove it + if (ci->scenario) { + _ghost_destroy_occlusion_rep(ci); + ci->scenario = nullptr; + } + + // create when entering the world + if (scenario) { + ci->scenario = scenario; + + // defer the actual creation to here + _ghost_create_occlusion_rep(ci); + } +} + +void VisualServerScene::ghost_update(RID p_ghost, const AABB &p_aabb) { + Ghost *ci = ghost_owner.getornull(p_ghost); + ERR_FAIL_COND(!ci); + ERR_FAIL_COND(!ci->scenario); + + ci->aabb = p_aabb; + + if (ci->rghost_handle) { + ci->scenario->_portal_renderer.rghost_update(ci->rghost_handle, p_aabb); + } +} + +void VisualServerScene::_ghost_create_occlusion_rep(Ghost *p_ghost) { + ERR_FAIL_COND(!p_ghost); + ERR_FAIL_COND(!p_ghost->scenario); + + if (!p_ghost->rghost_handle) { + p_ghost->rghost_handle = p_ghost->scenario->_portal_renderer.rghost_create(p_ghost->object_id, p_ghost->aabb); + } +} + +void VisualServerScene::_ghost_destroy_occlusion_rep(Ghost *p_ghost) { + ERR_FAIL_COND(!p_ghost); + ERR_FAIL_COND(!p_ghost->scenario); + + // not an error, can occur + if (!p_ghost->rghost_handle) { + return; + } + + p_ghost->scenario->_portal_renderer.rghost_destroy(p_ghost->rghost_handle); + p_ghost->rghost_handle = 0; +} + +RID VisualServerScene::roomgroup_create() { + RoomGroup *rg = memnew(RoomGroup); + ERR_FAIL_COND_V(!rg, RID()); + RID roomgroup_rid = roomgroup_owner.make_rid(rg); + return roomgroup_rid; +} + +void VisualServerScene::roomgroup_prepare(RID p_roomgroup, ObjectID p_roomgroup_object_id) { + RoomGroup *roomgroup = roomgroup_owner.getornull(p_roomgroup); + ERR_FAIL_COND(!roomgroup); + ERR_FAIL_COND(!roomgroup->scenario); + roomgroup->scenario->_portal_renderer.roomgroup_prepare(roomgroup->scenario_roomgroup_id, p_roomgroup_object_id); +} + +void VisualServerScene::roomgroup_set_scenario(RID p_roomgroup, RID p_scenario) { + RoomGroup *rg = roomgroup_owner.getornull(p_roomgroup); + ERR_FAIL_COND(!rg); + Scenario *scenario = scenario_owner.getornull(p_scenario); + + // noop? + if (rg->scenario == scenario) { + return; + } + + // if the portal is in a scenario already, remove it + if (rg->scenario) { + rg->scenario->_portal_renderer.roomgroup_destroy(rg->scenario_roomgroup_id); + rg->scenario = nullptr; + rg->scenario_roomgroup_id = 0; + } + + // create when entering the world + if (scenario) { + rg->scenario = scenario; + + // defer the actual creation to here + rg->scenario_roomgroup_id = scenario->_portal_renderer.roomgroup_create(); + } +} + +void VisualServerScene::roomgroup_add_room(RID p_roomgroup, RID p_room) { + RoomGroup *roomgroup = roomgroup_owner.getornull(p_roomgroup); + ERR_FAIL_COND(!roomgroup); + ERR_FAIL_COND(!roomgroup->scenario); + + Room *room = room_owner.getornull(p_room); + ERR_FAIL_COND(!room); + ERR_FAIL_COND(!room->scenario); + + ERR_FAIL_COND(roomgroup->scenario != room->scenario); + roomgroup->scenario->_portal_renderer.roomgroup_add_room(roomgroup->scenario_roomgroup_id, room->scenario_room_id); +} + +// Rooms +void VisualServerScene::callbacks_register(VisualServerCallbacks *p_callbacks) { + _visual_server_callbacks = p_callbacks; +} + +// the room has to be associated with a scenario, this is assumed to be +// the same scenario as the room node +RID VisualServerScene::room_create() { + Room *room = memnew(Room); + ERR_FAIL_COND_V(!room, RID()); + RID room_rid = room_owner.make_rid(room); + return room_rid; +} + +// should not be called multiple times, different scenarios etc, but just in case, we will support this +void VisualServerScene::room_set_scenario(RID p_room, RID p_scenario) { + Room *room = room_owner.getornull(p_room); + ERR_FAIL_COND(!room); + Scenario *scenario = scenario_owner.getornull(p_scenario); + + // no change? + if (room->scenario == scenario) { + return; + } + + // if the room has an existing scenario, remove from it + if (room->scenario) { + room->scenario->_portal_renderer.room_destroy(room->scenario_room_id); + room->scenario = nullptr; + room->scenario_room_id = 0; + } + + // create when entering the world + if (scenario) { + room->scenario = scenario; + + // defer the actual creation to here + room->scenario_room_id = scenario->_portal_renderer.room_create(); + } +} + +void VisualServerScene::room_add_ghost(RID p_room, ObjectID p_object_id, const AABB &p_aabb) { + Room *room = room_owner.getornull(p_room); + ERR_FAIL_COND(!room); + ERR_FAIL_COND(!room->scenario); + + room->scenario->_portal_renderer.room_add_ghost(room->scenario_room_id, p_object_id, p_aabb); +} + +void VisualServerScene::room_add_instance(RID p_room, RID p_instance, const AABB &p_aabb, const Vector &p_object_pts) { + Room *room = room_owner.getornull(p_room); + ERR_FAIL_COND(!room); + ERR_FAIL_COND(!room->scenario); + + Instance *instance = instance_owner.getornull(p_instance); + ERR_FAIL_COND(!instance); + + AABB bb = p_aabb; + + // the aabb passed from the client takes no account of the extra cull margin, + // so we need to add this manually. + // It is assumed it is in world space. + if (instance->extra_margin != 0.0) { + bb.grow_by(instance->extra_margin); + } + + bool dynamic = false; + + // don't add if portal mode is not static or dynamic + switch (instance->portal_mode) { + default: { + return; // this should be taken care of by the calling function, but just in case + } break; + case VisualServer::InstancePortalMode::INSTANCE_PORTAL_MODE_DYNAMIC: { + dynamic = true; + } break; + case VisualServer::InstancePortalMode::INSTANCE_PORTAL_MODE_STATIC: { + dynamic = false; + } break; + } + + instance->occlusion_handle = room->scenario->_portal_renderer.room_add_instance(room->scenario_room_id, p_instance, bb, dynamic, p_object_pts); +} + +void VisualServerScene::room_prepare(RID p_room, int32_t p_priority) { + Room *room = room_owner.getornull(p_room); + ERR_FAIL_COND(!room); + ERR_FAIL_COND(!room->scenario); + room->scenario->_portal_renderer.room_prepare(room->scenario_room_id, p_priority); +} + +void VisualServerScene::room_set_bound(RID p_room, ObjectID p_room_object_id, const Vector &p_convex, const AABB &p_aabb, const Vector &p_verts) { + Room *room = room_owner.getornull(p_room); + ERR_FAIL_COND(!room); + ERR_FAIL_COND(!room->scenario); + room->scenario->_portal_renderer.room_set_bound(room->scenario_room_id, p_room_object_id, p_convex, p_aabb, p_verts); +} + +void VisualServerScene::rooms_unload(RID p_scenario) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_unload(); +} + +void VisualServerScene::rooms_and_portals_clear(RID p_scenario) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_and_portals_clear(); +} + +void VisualServerScene::rooms_finalize(RID p_scenario, bool p_generate_pvs, bool p_cull_using_pvs, bool p_use_secondary_pvs, bool p_use_signals, String p_pvs_filename) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_finalize(p_generate_pvs, p_cull_using_pvs, p_use_secondary_pvs, p_use_signals, p_pvs_filename); +} + +void VisualServerScene::rooms_override_camera(RID p_scenario, bool p_override, const Vector3 &p_point, const Vector *p_convex) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_override_camera(p_override, p_point, p_convex); +} + +void VisualServerScene::rooms_set_active(RID p_scenario, bool p_active) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_set_active(p_active); +} + +void VisualServerScene::rooms_set_params(RID p_scenario, int p_portal_depth_limit) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_set_params(p_portal_depth_limit); +} + +void VisualServerScene::rooms_set_debug_feature(RID p_scenario, VisualServer::RoomsDebugFeature p_feature, bool p_active) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + switch (p_feature) { + default: { + } break; + case VisualServer::ROOMS_DEBUG_SPRAWL: { + scenario->_portal_renderer.set_debug_sprawl(p_active); + } break; + } +} + +void VisualServerScene::rooms_update_gameplay_monitor(RID p_scenario, const Vector &p_camera_positions) { + Scenario *scenario = scenario_owner.getornull(p_scenario); + ERR_FAIL_COND(!scenario); + scenario->_portal_renderer.rooms_update_gameplay_monitor(p_camera_positions); +} + Vector VisualServerScene::instances_cull_aabb(const AABB &p_aabb, RID p_scenario) const { Vector instances; Scenario *scenario = scenario_owner.get(p_scenario); @@ -958,6 +1372,40 @@ Vector VisualServerScene::instances_cull_convex(const Vector &p return instances; } +// thin wrapper to allow rooms / portals to take over culling if active +int VisualServerScene::_cull_convex_from_point(Scenario *p_scenario, const Vector3 &p_point, const Vector &p_convex, Instance **p_result_array, int p_result_max, int32_t &r_previous_room_id_hint, uint32_t p_mask) { + int res = -1; + if (p_scenario->_portal_renderer.is_active()) { + // Note that the portal renderer ASSUMES that the planes exactly match the convention in + // CameraMatrix of enum Planes (6 planes, in order, near, far etc) + // If this is not the case, it should not be used. + res = p_scenario->_portal_renderer.cull_convex(p_point, p_convex, (VSInstance **)p_result_array, p_result_max, p_mask, r_previous_room_id_hint); + } + + // fallback to BVH / octree if portals not active + if (res == -1) { + res = p_scenario->sps->cull_convex(p_convex, p_result_array, p_result_max, p_mask); + } + return res; +} + +void VisualServerScene::_rooms_instance_update(Instance *p_instance, const AABB &p_aabb) { + // magic number for instances in the room / portal system, but not requiring an update + // (due to being a STATIC or DYNAMIC object within a room) + // Must match the value in PortalRenderer in VisualServer + const uint32_t OCCLUSION_HANDLE_ROOM_BIT = 1 << 31; + + // if the instance is a moving object in the room / portal system, update it + // Note that if rooms and portals is not in use, occlusion_handle should be zero in all cases unless the portal_mode + // has been set to global or roaming. (which is unlikely as the default is static). + // The exception is editor user interface elements. + // These are always set to global and will always keep their aabb up to date in the portal renderer unnecessarily. + // There is no easy way around this, but it should be very cheap, and have no impact outside the editor. + if (p_instance->occlusion_handle && (p_instance->occlusion_handle != OCCLUSION_HANDLE_ROOM_BIT)) { + p_instance->scenario->_portal_renderer.instance_moving_update(p_instance->occlusion_handle, p_aabb); + } +} + void VisualServerScene::instance_geometry_set_flag(RID p_instance, VS::InstanceFlags p_flags, bool p_enabled) { Instance *instance = instance_owner.get(p_instance); ERR_FAIL_COND(!instance); @@ -1094,6 +1542,9 @@ void VisualServerScene::_update_instance(Instance *p_instance) { p_instance->scenario->sps->move(p_instance->spatial_partition_id, new_aabb); } + + // keep rooms and portals instance up to date if present + _rooms_instance_update(p_instance, new_aabb); } void VisualServerScene::_update_instance_aabb(Instance *p_instance) { @@ -1746,7 +2197,7 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons Vector planes = cm.get_projection_planes(xform); - int cull_count = p_scenario->sps->cull_convex(planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, VS::INSTANCE_GEOMETRY_MASK); + int cull_count = _cull_convex_from_point(p_scenario, light_transform.origin, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK); Plane near_plane(xform.origin, -xform.basis.get_axis(2)); for (int j = 0; j < cull_count; j++) { @@ -1781,7 +2232,7 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons cm.set_perspective(angle * 2.0, 1.0, 0.01, radius); Vector planes = cm.get_projection_planes(light_transform); - int cull_count = p_scenario->sps->cull_convex(planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, VS::INSTANCE_GEOMETRY_MASK); + int cull_count = _cull_convex_from_point(p_scenario, light_transform.origin, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK); Plane near_plane(light_transform.origin, -light_transform.basis.get_axis(2)); for (int j = 0; j < cull_count; j++) { @@ -1851,7 +2302,7 @@ void VisualServerScene::render_camera(RID p_camera, RID p_scenario, Size2 p_view } break; } - _prepare_scene(camera->transform, camera_matrix, ortho, camera->env, camera->visible_layers, p_scenario, p_shadow_atlas, RID()); + _prepare_scene(camera->transform, camera_matrix, ortho, camera->env, camera->visible_layers, p_scenario, p_shadow_atlas, RID(), camera->previous_room_id_hint); _render_scene(camera->transform, camera_matrix, 0, ortho, camera->env, p_scenario, p_shadow_atlas, RID(), -1); #endif } @@ -1930,17 +2381,17 @@ void VisualServerScene::render_camera(Ref &p_interface, ARVRInter mono_transform *= apply_z_shift; // now prepare our scene with our adjusted transform projection matrix - _prepare_scene(mono_transform, combined_matrix, false, camera->env, camera->visible_layers, p_scenario, p_shadow_atlas, RID()); + _prepare_scene(mono_transform, combined_matrix, false, camera->env, camera->visible_layers, p_scenario, p_shadow_atlas, RID(), camera->previous_room_id_hint); } else if (p_eye == ARVRInterface::EYE_MONO) { // For mono render, prepare as per usual - _prepare_scene(cam_transform, camera_matrix, false, camera->env, camera->visible_layers, p_scenario, p_shadow_atlas, RID()); + _prepare_scene(cam_transform, camera_matrix, false, camera->env, camera->visible_layers, p_scenario, p_shadow_atlas, RID(), camera->previous_room_id_hint); } // And render our scene... _render_scene(cam_transform, camera_matrix, p_eye, false, camera->env, p_scenario, p_shadow_atlas, RID(), -1); }; -void VisualServerScene::_prepare_scene(const Transform p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_orthogonal, RID p_force_environment, uint32_t p_visible_layers, RID p_scenario, RID p_shadow_atlas, RID p_reflection_probe) { +void VisualServerScene::_prepare_scene(const Transform p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_orthogonal, RID p_force_environment, uint32_t p_visible_layers, RID p_scenario, RID p_shadow_atlas, RID p_reflection_probe, int32_t &r_previous_room_id_hint) { // Note, in stereo rendering: // - p_cam_transform will be a transform in the middle of our two eyes // - p_cam_projection is a wider frustrum that encompasses both eyes @@ -1960,7 +2411,7 @@ void VisualServerScene::_prepare_scene(const Transform p_cam_transform, const Ca float z_far = p_cam_projection.get_z_far(); /* STEP 2 - CULL */ - instance_cull_count = scenario->sps->cull_convex(planes, instance_cull_result, MAX_INSTANCE_CULL); + instance_cull_count = _cull_convex_from_point(scenario, p_cam_transform.origin, planes, instance_cull_result, MAX_INSTANCE_CULL, r_previous_room_id_hint); light_cull_count = 0; reflection_probe_cull_count = 0; @@ -2339,7 +2790,7 @@ bool VisualServerScene::_render_reflection_probe_step(Instance *p_instance, int shadow_atlas = scenario->reflection_probe_shadow_atlas; } - _prepare_scene(xform, cm, false, RID(), VSG::storage->reflection_probe_get_cull_mask(p_instance->base), p_instance->scenario->self, shadow_atlas, reflection_probe->instance); + _prepare_scene(xform, cm, false, RID(), VSG::storage->reflection_probe_get_cull_mask(p_instance->base), p_instance->scenario->self, shadow_atlas, reflection_probe->instance, reflection_probe->previous_room_id_hint); _render_scene(xform, cm, 0, false, RID(), p_instance->scenario->self, shadow_atlas, reflection_probe->instance, p_step); } else { @@ -3524,6 +3975,22 @@ bool VisualServerScene::free(RID p_rid) { instance_owner.free(p_rid); memdelete(instance); + } else if (room_owner.owns(p_rid)) { + Room *room = room_owner.get(p_rid); + room_owner.free(p_rid); + memdelete(room); + } else if (portal_owner.owns(p_rid)) { + Portal *portal = portal_owner.get(p_rid); + portal_owner.free(p_rid); + memdelete(portal); + } else if (ghost_owner.owns(p_rid)) { + Ghost *ghost = ghost_owner.get(p_rid); + ghost_owner.free(p_rid); + memdelete(ghost); + } else if (roomgroup_owner.owns(p_rid)) { + RoomGroup *roomgroup = roomgroup_owner.get(p_rid); + roomgroup_owner.free(p_rid); + memdelete(roomgroup); } else { return false; } @@ -3540,6 +4007,7 @@ VisualServerScene::VisualServerScene() { render_pass = 1; singleton = this; _use_bvh = GLOBAL_DEF("rendering/quality/spatial_partitioning/use_bvh", true); + _visual_server_callbacks = nullptr; } VisualServerScene::~VisualServerScene() { diff --git a/servers/visual/visual_server_scene.h b/servers/visual/visual_server_scene.h index 7f58b082d40..b53cef3df35 100644 --- a/servers/visual/visual_server_scene.h +++ b/servers/visual/visual_server_scene.h @@ -40,6 +40,7 @@ #include "core/os/thread.h" #include "core/safe_refcount.h" #include "core/self_list.h" +#include "portals/portal_renderer.h" #include "servers/arvr/arvr_interface.h" class VisualServerScene { @@ -54,8 +55,6 @@ public: }; uint64_t render_pass; - bool _use_bvh; - static VisualServerScene *singleton; /* CAMERA API */ @@ -76,6 +75,7 @@ public: RID env; Transform transform; + int32_t previous_room_id_hint; Camera() { visible_layers = 0xFFFFFFFF; @@ -86,6 +86,7 @@ public: size = 1.0; offset = Vector2(); vaspect = false; + previous_room_id_hint = -1; } }; @@ -187,6 +188,7 @@ public: RID self; SpatialPartitioningScene *sps; + PortalRenderer _portal_renderer; List directional_lights; RID environment; @@ -222,6 +224,11 @@ public: RID self; //scenario stuff SpatialPartitionID spatial_partition_id; + + // rooms & portals + OcclusionHandle occlusion_handle; // handle of instance in occlusion system (or 0) + VisualServer::InstancePortalMode portal_mode; + Scenario *scenario; SelfList scenario_item; @@ -272,6 +279,9 @@ public: object_id = 0; visible = true; + occlusion_handle = 0; + portal_mode = VisualServer::InstancePortalMode::INSTANCE_PORTAL_MODE_STATIC; + lod_begin = 0; lod_end = 0; lod_begin_hysteresis = 0; @@ -335,11 +345,13 @@ public: SelfList update_list; int render_step; + int32_t previous_room_id_hint; InstanceReflectionProbeData() : update_list(this) { reflection_dirty = true; render_step = -1; + previous_room_id_hint = -1; } }; @@ -360,12 +372,14 @@ public: List geometries; Instance *baked_light; + int32_t previous_room_id_hint; InstanceLightData() { shadow_dirty = true; D = nullptr; last_version = 0; baked_light = nullptr; + previous_room_id_hint = -1; } }; @@ -515,11 +529,135 @@ public: virtual void instance_set_extra_visibility_margin(RID p_instance, real_t p_margin); + // Portals + virtual void instance_set_portal_mode(RID p_instance, VisualServer::InstancePortalMode p_mode); + bool _instance_get_transformed_aabb(RID p_instance, AABB &r_aabb); + void *_instance_get_from_rid(RID p_instance); + bool _instance_cull_check(VSInstance *p_instance, uint32_t p_cull_mask) const { + uint32_t pairable_type = 1 << ((Instance *)p_instance)->base_type; + return pairable_type & p_cull_mask; + } + ObjectID _instance_get_object_ID(VSInstance *p_instance) const { + if (p_instance) { + return ((Instance *)p_instance)->object_id; + } + return 0; + } + +private: + void _instance_create_occlusion_rep(Instance *p_instance); + void _instance_destroy_occlusion_rep(Instance *p_instance); + +public: + struct Ghost : RID_Data { + // all interations with actual ghosts are indirect, as the ghost is part of the scenario + Scenario *scenario = nullptr; + uint32_t object_id = 0; + RGhostHandle rghost_handle = 0; // handle in occlusion system (or 0) + AABB aabb; + virtual ~Ghost() { + if (scenario) { + if (rghost_handle) { + scenario->_portal_renderer.rghost_destroy(rghost_handle); + rghost_handle = 0; + } + scenario = nullptr; + } + } + }; + RID_Owner ghost_owner; + + virtual RID ghost_create(); + virtual void ghost_set_scenario(RID p_ghost, RID p_scenario, ObjectID p_id, const AABB &p_aabb); + virtual void ghost_update(RID p_ghost, const AABB &p_aabb); + +private: + void _ghost_create_occlusion_rep(Ghost *p_ghost); + void _ghost_destroy_occlusion_rep(Ghost *p_ghost); + +public: + struct Portal : RID_Data { + // all interations with actual portals are indirect, as the portal is part of the scenario + uint32_t scenario_portal_id = 0; + Scenario *scenario = nullptr; + virtual ~Portal() { + if (scenario) { + scenario->_portal_renderer.portal_destroy(scenario_portal_id); + scenario = nullptr; + scenario_portal_id = 0; + } + } + }; + RID_Owner portal_owner; + + virtual RID portal_create(); + virtual void portal_set_scenario(RID p_portal, RID p_scenario); + virtual void portal_set_geometry(RID p_portal, const Vector &p_points, float p_margin); + virtual void portal_link(RID p_portal, RID p_room_from, RID p_room_to, bool p_two_way); + virtual void portal_set_active(RID p_portal, bool p_active); + + // RoomGroups + struct RoomGroup : RID_Data { + // all interations with actual roomgroups are indirect, as the roomgroup is part of the scenario + uint32_t scenario_roomgroup_id = 0; + Scenario *scenario = nullptr; + virtual ~RoomGroup() { + if (scenario) { + scenario->_portal_renderer.roomgroup_destroy(scenario_roomgroup_id); + scenario = nullptr; + scenario_roomgroup_id = 0; + } + } + }; + RID_Owner roomgroup_owner; + + virtual RID roomgroup_create(); + virtual void roomgroup_prepare(RID p_roomgroup, ObjectID p_roomgroup_object_id); + virtual void roomgroup_set_scenario(RID p_roomgroup, RID p_scenario); + virtual void roomgroup_add_room(RID p_roomgroup, RID p_room); + + // Rooms + struct Room : RID_Data { + // all interations with actual rooms are indirect, as the room is part of the scenario + uint32_t scenario_room_id = 0; + Scenario *scenario = nullptr; + virtual ~Room() { + if (scenario) { + scenario->_portal_renderer.room_destroy(scenario_room_id); + scenario = nullptr; + scenario_room_id = 0; + } + } + }; + RID_Owner room_owner; + + virtual RID room_create(); + virtual void room_set_scenario(RID p_room, RID p_scenario); + virtual void room_add_instance(RID p_room, RID p_instance, const AABB &p_aabb, const Vector &p_object_pts); + virtual void room_add_ghost(RID p_room, ObjectID p_object_id, const AABB &p_aabb); + virtual void room_set_bound(RID p_room, ObjectID p_room_object_id, const Vector &p_convex, const AABB &p_aabb, const Vector &p_verts); + virtual void room_prepare(RID p_room, int32_t p_priority); + virtual void rooms_and_portals_clear(RID p_scenario); + virtual void rooms_unload(RID p_scenario); + virtual void rooms_finalize(RID p_scenario, bool p_generate_pvs, bool p_cull_using_pvs, bool p_use_secondary_pvs, bool p_use_signals, String p_pvs_filename); + virtual void rooms_override_camera(RID p_scenario, bool p_override, const Vector3 &p_point, const Vector *p_convex); + virtual void rooms_set_active(RID p_scenario, bool p_active); + virtual void rooms_set_params(RID p_scenario, int p_portal_depth_limit); + virtual void rooms_set_debug_feature(RID p_scenario, VisualServer::RoomsDebugFeature p_feature, bool p_active); + virtual void rooms_update_gameplay_monitor(RID p_scenario, const Vector &p_camera_positions); + + virtual void callbacks_register(VisualServerCallbacks *p_callbacks); + VisualServerCallbacks *get_callbacks() const { return _visual_server_callbacks; } + // don't use these in a game! virtual Vector instances_cull_aabb(const AABB &p_aabb, RID p_scenario = RID()) const; virtual Vector instances_cull_ray(const Vector3 &p_from, const Vector3 &p_to, RID p_scenario = RID()) const; virtual Vector instances_cull_convex(const Vector &p_convex, RID p_scenario = RID()) const; + // internal (uses portals when available) + int _cull_convex_from_point(Scenario *p_scenario, const Vector3 &p_point, const Vector &p_convex, Instance **p_result_array, int p_result_max, int32_t &r_previous_room_id_hint, uint32_t p_mask = 0xFFFFFFFF); + void _rooms_instance_update(Instance *p_instance, const AABB &p_aabb); + virtual void instance_geometry_set_flag(RID p_instance, VS::InstanceFlags p_flags, bool p_enabled); virtual void instance_geometry_set_cast_shadows_setting(RID p_instance, VS::ShadowCastingSetting p_shadow_casting_setting); virtual void instance_geometry_set_material_override(RID p_instance, RID p_material); @@ -534,7 +672,7 @@ public: _FORCE_INLINE_ bool _light_instance_update_shadow(Instance *p_instance, const Transform p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_orthogonal, RID p_shadow_atlas, Scenario *p_scenario); - void _prepare_scene(const Transform p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_orthogonal, RID p_force_environment, uint32_t p_visible_layers, RID p_scenario, RID p_shadow_atlas, RID p_reflection_probe); + void _prepare_scene(const Transform p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_orthogonal, RID p_force_environment, uint32_t p_visible_layers, RID p_scenario, RID p_shadow_atlas, RID p_reflection_probe, int32_t &r_previous_room_id_hint); void _render_scene(const Transform p_cam_transform, const CameraMatrix &p_cam_projection, const int p_eye, bool p_cam_orthogonal, RID p_force_environment, RID p_scenario, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass); void render_empty_scene(RID p_scenario, RID p_shadow_atlas); @@ -590,6 +728,11 @@ public: bool free(RID p_rid); +private: + bool _use_bvh; + VisualServerCallbacks *_visual_server_callbacks; + +public: VisualServerScene(); virtual ~VisualServerScene(); }; diff --git a/servers/visual/visual_server_wrap_mt.cpp b/servers/visual/visual_server_wrap_mt.cpp index 4205050aaf2..1eb3c0f26fd 100644 --- a/servers/visual/visual_server_wrap_mt.cpp +++ b/servers/visual/visual_server_wrap_mt.cpp @@ -140,6 +140,10 @@ void VisualServerWrapMT::finish() { canvas_item_free_cached_ids(); canvas_light_occluder_free_cached_ids(); canvas_occluder_polygon_free_cached_ids(); + room_free_cached_ids(); + roomgroup_free_cached_ids(); + portal_free_cached_ids(); + ghost_free_cached_ids(); } void VisualServerWrapMT::set_use_vsync_callback(bool p_enable) { diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index 16b597a12d8..089dd8e472f 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -481,6 +481,44 @@ public: FUNC2(instance_set_extra_visibility_margin, RID, real_t) + // Portals + FUNC2(instance_set_portal_mode, RID, InstancePortalMode) + + FUNCRID(ghost) + FUNC4(ghost_set_scenario, RID, RID, ObjectID, const AABB &) + FUNC2(ghost_update, RID, const AABB &) + + FUNCRID(portal) + FUNC2(portal_set_scenario, RID, RID) + FUNC3(portal_set_geometry, RID, const Vector &, float) + FUNC4(portal_link, RID, RID, RID, bool) + FUNC2(portal_set_active, RID, bool) + + // Roomgroups + FUNCRID(roomgroup) + FUNC2(roomgroup_prepare, RID, ObjectID) + FUNC2(roomgroup_set_scenario, RID, RID) + FUNC2(roomgroup_add_room, RID, RID) + + // Rooms + FUNCRID(room) + FUNC2(room_set_scenario, RID, RID) + FUNC4(room_add_instance, RID, RID, const AABB &, const Vector &) + FUNC3(room_add_ghost, RID, ObjectID, const AABB &) + FUNC5(room_set_bound, RID, ObjectID, const Vector &, const AABB &, const Vector &) + FUNC2(room_prepare, RID, int32_t) + FUNC1(rooms_and_portals_clear, RID) + FUNC1(rooms_unload, RID) + FUNC6(rooms_finalize, RID, bool, bool, bool, bool, String) + FUNC4(rooms_override_camera, RID, bool, const Vector3 &, const Vector *) + FUNC2(rooms_set_active, RID, bool) + FUNC2(rooms_set_params, RID, int) + FUNC3(rooms_set_debug_feature, RID, RoomsDebugFeature, bool) + FUNC2(rooms_update_gameplay_monitor, RID, const Vector &) + + // Callbacks + FUNC1(callbacks_register, VisualServerCallbacks *) + // don't use these in a game! FUNC2RC(Vector, instances_cull_aabb, const AABB &, RID) FUNC3RC(Vector, instances_cull_ray, const Vector3 &, const Vector3 &, RID) diff --git a/servers/visual_server.cpp b/servers/visual_server.cpp index 00146a7e3d5..b44f9b3c5fb 100644 --- a/servers/visual_server.cpp +++ b/servers/visual_server.cpp @@ -2229,6 +2229,11 @@ RID VisualServer::instance_create2(RID p_base, RID p_scenario) { RID instance = instance_create(); instance_set_base(instance, p_base); instance_set_scenario(instance, p_scenario); + + // instance_create2 is used mainly by editor instances. + // These should not be culled by the portal system when it is active, so we set their mode to global, + // for frustum culling only. + instance_set_portal_mode(instance, VisualServer::INSTANCE_PORTAL_MODE_GLOBAL); return instance; } diff --git a/servers/visual_server.h b/servers/visual_server.h index 73f64303bd5..c1fdc945544 100644 --- a/servers/visual_server.h +++ b/servers/visual_server.h @@ -39,6 +39,8 @@ #include "core/rid.h" #include "core/variant.h" +class VisualServerCallbacks; + class VisualServer : public Object { GDCLASS(VisualServer, Object); @@ -853,6 +855,57 @@ public: virtual void instance_set_extra_visibility_margin(RID p_instance, real_t p_margin) = 0; + /* ROOMS AND PORTALS API */ + // Portals + enum InstancePortalMode { + INSTANCE_PORTAL_MODE_STATIC, // not moving within a room + INSTANCE_PORTAL_MODE_DYNAMIC, // moving within room + INSTANCE_PORTAL_MODE_ROAMING, // moving between rooms + INSTANCE_PORTAL_MODE_GLOBAL, // frustum culled only + INSTANCE_PORTAL_MODE_IGNORE, // don't show at all - e.g. manual bounds, hidden portals + }; + + virtual void instance_set_portal_mode(RID p_instance, InstancePortalMode p_mode) = 0; + + virtual RID ghost_create() = 0; + virtual void ghost_set_scenario(RID p_ghost, RID p_scenario, ObjectID p_id, const AABB &p_aabb) = 0; + virtual void ghost_update(RID p_ghost, const AABB &p_aabb) = 0; + + virtual RID portal_create() = 0; + virtual void portal_set_scenario(RID p_portal, RID p_scenario) = 0; + virtual void portal_set_geometry(RID p_portal, const Vector &p_points, float p_margin) = 0; + virtual void portal_link(RID p_portal, RID p_room_from, RID p_room_to, bool p_two_way) = 0; + virtual void portal_set_active(RID p_portal, bool p_active) = 0; + + // Roomgroups + virtual RID roomgroup_create() = 0; + virtual void roomgroup_prepare(RID p_roomgroup, ObjectID p_roomgroup_object_id) = 0; + virtual void roomgroup_set_scenario(RID p_roomgroup, RID p_scenario) = 0; + virtual void roomgroup_add_room(RID p_roomgroup, RID p_room) = 0; + + // Rooms + enum RoomsDebugFeature { + ROOMS_DEBUG_SPRAWL, + }; + + virtual RID room_create() = 0; + virtual void room_set_scenario(RID p_room, RID p_scenario) = 0; + virtual void room_add_instance(RID p_room, RID p_instance, const AABB &p_aabb, const Vector &p_object_pts) = 0; + virtual void room_add_ghost(RID p_room, ObjectID p_object_id, const AABB &p_aabb) = 0; + virtual void room_set_bound(RID p_room, ObjectID p_room_object_id, const Vector &p_convex, const AABB &p_aabb, const Vector &p_verts) = 0; + virtual void room_prepare(RID p_room, int32_t p_priority) = 0; + virtual void rooms_and_portals_clear(RID p_scenario) = 0; + virtual void rooms_unload(RID p_scenario) = 0; + virtual void rooms_finalize(RID p_scenario, bool p_generate_pvs, bool p_cull_using_pvs, bool p_use_secondary_pvs, bool p_use_signals, String p_pvs_filename) = 0; + virtual void rooms_override_camera(RID p_scenario, bool p_override, const Vector3 &p_point, const Vector *p_convex) = 0; + virtual void rooms_set_active(RID p_scenario, bool p_active) = 0; + virtual void rooms_set_params(RID p_scenario, int p_portal_depth_limit) = 0; + virtual void rooms_set_debug_feature(RID p_scenario, RoomsDebugFeature p_feature, bool p_active) = 0; + virtual void rooms_update_gameplay_monitor(RID p_scenario, const Vector &p_camera_positions) = 0; + + // callbacks are used to send messages back from the visual server to scene tree in thread friendly manner + virtual void callbacks_register(VisualServerCallbacks *p_callbacks) = 0; + // don't use these in a game! virtual Vector instances_cull_aabb(const AABB &p_aabb, RID p_scenario = RID()) const = 0; virtual Vector instances_cull_ray(const Vector3 &p_from, const Vector3 &p_to, RID p_scenario = RID()) const = 0; @@ -1101,6 +1154,7 @@ VARIANT_ENUM_CAST(VisualServer::ViewportRenderInfo); VARIANT_ENUM_CAST(VisualServer::ViewportDebugDraw); VARIANT_ENUM_CAST(VisualServer::ScenarioDebugMode); VARIANT_ENUM_CAST(VisualServer::InstanceType); +VARIANT_ENUM_CAST(VisualServer::InstancePortalMode); VARIANT_ENUM_CAST(VisualServer::NinePatchAxisMode); VARIANT_ENUM_CAST(VisualServer::CanvasLightMode); VARIANT_ENUM_CAST(VisualServer::CanvasLightShadowFilter); diff --git a/servers/visual_server_callbacks.cpp b/servers/visual_server_callbacks.cpp new file mode 100644 index 00000000000..15407ef96d1 --- /dev/null +++ b/servers/visual_server_callbacks.cpp @@ -0,0 +1,65 @@ +/*************************************************************************/ +/* visual_server_callbacks.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 "visual_server_callbacks.h" + +#include "core/object.h" + +void VisualServerCallbacks::lock() { + mutex.lock(); +} + +void VisualServerCallbacks::unlock() { + mutex.unlock(); +} + +void VisualServerCallbacks::flush() { + // should be ok without a lock .. + // is the most common case and should be quicker + if (!messages.size()) { + return; + } + + lock(); + for (int n = 0; n < messages.size(); n++) { + const Message &mess = messages[n]; + + Object *obj = ObjectDB::get_instance(mess.object_id); + if (!obj) { + continue; + } + + obj->notification_callback(mess.type); + } + + messages.clear(); + + unlock(); +} diff --git a/servers/visual_server_callbacks.h b/servers/visual_server_callbacks.h new file mode 100644 index 00000000000..d8dc6e10bfb --- /dev/null +++ b/servers/visual_server_callbacks.h @@ -0,0 +1,66 @@ +/*************************************************************************/ +/* visual_server_callbacks.h */ +/*************************************************************************/ +/* 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. */ +/*************************************************************************/ + +#ifndef VISUAL_SERVER_CALLBACKS_H +#define VISUAL_SERVER_CALLBACKS_H + +#include "core/local_vector.h" +#include "core/object_id.h" +#include "core/os/mutex.h" + +class VisualServerCallbacks { +public: + enum CallbackType { + CALLBACK_NOTIFICATION_ENTER_GAMEPLAY, + CALLBACK_NOTIFICATION_EXIT_GAMEPLAY, + CALLBACK_SIGNAL_ENTER_GAMEPLAY, + CALLBACK_SIGNAL_EXIT_GAMEPLAY, + }; + + struct Message { + CallbackType type; + ObjectID object_id; + }; + + void lock(); + void unlock(); + void flush(); + + void push_message(const Message &p_message) { messages.push_back(p_message); } + int32_t get_num_messages() const { return messages.size(); } + const Message &get_message(int p_index) const { return messages[p_index]; } + void clear() { messages.clear(); } + +private: + LocalVector messages; + Mutex mutex; +}; + +#endif // VISUAL_SERVER_CALLBACKS_H