123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- /**************************************************************************/
- /* portal_rooms_bsp.cpp */
- /**************************************************************************/
- /* This file is part of: */
- /* GODOT ENGINE */
- /* https://godotengine.org */
- /**************************************************************************/
- /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
- /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
- /* */
- /* Permission is hereby granted, free of charge, to any person obtaining */
- /* a copy of this software and associated documentation files (the */
- /* "Software"), to deal in the Software without restriction, including */
- /* without limitation the rights to use, copy, modify, merge, publish, */
- /* distribute, sublicense, and/or sell copies of the Software, and to */
- /* permit persons to whom the Software is furnished to do so, subject to */
- /* the following conditions: */
- /* */
- /* The above copyright notice and this permission notice shall be */
- /* included in all copies or substantial portions of the Software. */
- /* */
- /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
- /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
- /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
- /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
- /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
- /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
- /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
- /**************************************************************************/
- #include "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) {
- if (p_previous_room_id < p_portal_renderer.get_num_rooms()) {
- 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;
- }
- } else {
- // previous room was out of range (perhaps due to reconverting room system and the number of rooms decreasing)
- 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<int32_t, int32_t> 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<int32_t, int32_t> p_orig_room_ids) {
- struct Element {
- void clear() { room_ids.clear(); }
- int node_id;
- LocalVector<int32_t, int32_t> room_ids;
- };
- Element first;
- first.node_id = p_start_node_id;
- first.room_ids = p_orig_room_ids;
- LocalVector<Element, int32_t> 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<int32_t, int32_t> &room_ids_back = stack[stack_size - 2].room_ids;
- LocalVector<int32_t, int32_t> &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<Vector3, int32_t> &p_verts_a, const LocalVector<Vector3, int32_t> &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<Vector3, int32_t> &verts_a = p_room_a._verts;
- const LocalVector<Vector3, int32_t> &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<int32_t, int32_t> &p_room_ids, Plane &r_plane, LocalVector<int32_t, int32_t> *r_room_ids_back, LocalVector<int32_t, int32_t> *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<int32_t, int32_t> &p_room_ids, LocalVector<int32_t, int32_t> *r_room_ids_back, LocalVector<int32_t, int32_t> *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<int32_t, int32_t> &p_room_ids, LocalVector<int32_t, int32_t> *r_room_ids_back, LocalVector<int32_t, int32_t> *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);
- }
|