/*************************************************************************/ /* portal_rooms_bsp.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2022 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; 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); } } #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); }