virtualx-engine/scene/3d/portal.cpp
lawnjelly b663acef93 Portals - Add configuration warnings for nodes
Checks for invalid children / grandchildren etc.
2021-07-15 08:01:42 +01:00

671 lines
20 KiB
C++

/*************************************************************************/
/* 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_group.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<Vector2> 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);
}
}
String Portal::get_configuration_warning() const {
String warning = Spatial::get_configuration_warning();
auto lambda = [](const Node *p_node) {
return static_cast<bool>((Object::cast_to<RoomManager>(p_node) || Object::cast_to<Room>(p_node) || Object::cast_to<RoomGroup>(p_node)));
};
if (Room::detect_nodes_using_lambda(this, lambda)) {
if (Room::detect_nodes_of_type<RoomManager>(this)) {
if (!warning.empty()) {
warning += "\n\n";
}
warning += TTR("The RoomManager should not be a child or grandchild of a Portal.");
}
if (Room::detect_nodes_of_type<Room>(this)) {
if (!warning.empty()) {
warning += "\n\n";
}
warning += TTR("A Room should not be a child or grandchild of a Portal.");
}
if (Room::detect_nodes_of_type<RoomGroup>(this)) {
if (!warning.empty()) {
warning += "\n\n";
}
warning += TTR("A RoomGroup should not be a child or grandchild of a Portal.");
}
}
return warning;
}
void Portal::set_points(const PoolVector<Vector2> &p_points) {
_pts_local_raw = p_points;
_sanitize_points();
if (is_inside_tree()) {
portal_update();
update_gizmo();
}
}
PoolVector<Vector2> 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<Room>(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<Room>(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<Vector2> 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<Mesh> 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<Vector3> 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<Vector3> 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<Spatial>(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<Vector2> 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<Vector3> &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<Vector3> &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");
}