virtualx-engine/servers/visual/portals/portal_rooms_bsp.cpp
Rémi Verschelde 1426cd3b3a
One Copyright Update to rule them all
As many open source projects have started doing it, we're removing the
current year from the copyright notice, so that we don't need to bump
it every year.

It seems like only the first year of publication is technically
relevant for copyright notices, and even that seems to be something
that many companies stopped listing altogether (in a version controlled
codebase, the commits are a much better source of date of publication
than a hardcoded copyright statement).

We also now list Godot Engine contributors first as we're collectively
the current maintainers of the project, and we clarify that the
"exclusive" copyright of the co-founders covers the timespan before
opensourcing (their further contributions are included as part of Godot
Engine contributors).

Also fixed "cf." Frenchism - it's meant as "refer to / see".

Backported from #70885.
2023-01-10 15:26:54 +01:00

643 lines
19 KiB
C++

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