virtualx-engine/servers/visual/portals/portal_rooms_bsp.cpp
lawnjelly 88b7d928c5 Portals - Fix invalid room hint when reconverting room graph
In situations where rooms are converted multiple times, the previous room hint ID can reference a room number that is out of range of the new number of rooms. This fixes the bug by checking the room hint ID is within range.
2022-07-24 11:46:38 +01:00

643 lines
19 KiB
C++

/*************************************************************************/
/* 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) {
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);
}