/*************************************************************************/ /* portal.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.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 "scene/resources/mesh.h" #include "scene/resources/world.h" #include "servers/visual_server.h" bool Portal::_portal_plane_convention = false; bool Portal::_settings_gizmo_show_margins = true; Portal::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.0; _use_default_margin = true; // the visual server portal lifetime is linked to the lifetime of this object _portal_rid = RID_PRIME(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 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((Object::cast_to(p_node) || Object::cast_to(p_node) || Object::cast_to(p_node))); }; if (Room::detect_nodes_using_lambda(this, lambda)) { if (Room::detect_nodes_of_type(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(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(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_point(int p_idx, const Vector2 &p_point) { if (p_idx >= _pts_local_raw.size()) { return; } _pts_local_raw.set(p_idx, p_point); _sanitize_points(); update_gizmo(); } void Portal::set_points(const PoolVector &p_points) { _pts_local_raw = p_points; _sanitize_points(); if (is_inside_tree()) { portal_update(); update_gizmo(); } } PoolVector 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("changed Portal " + get_name()); #endif } void Portal::clear() { _internal = false; _linkedroom_ID[0] = -1; _linkedroom_ID[1] = -1; _importing_portal = false; } 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(); // In theory we shouldn't need to update the gizmo when the transform // changes .. HOWEVER, the portal margin is displayed in world space units, // back transformed to model space. // If the Z scale is changed by the user, the portal margin length can become incorrect // and needs 'resyncing' to the global scale of the portal node. // We really only need to do this when Z scale is changed, but it is easier codewise // to always change it, unless we have evidence this is a performance problem. update_gizmo(); } 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 LocalVector &p_rooms, const RID &p_from_room_rid) { Room *linkedroom = nullptr; if (has_node(_settings_path_linkedroom)) { linkedroom = Object::cast_to(get_node(_settings_path_linkedroom)); // only allow linking to rooms that are part of the roomlist // (already recognised). // If we don't check this, it will start trying to link to Room nodes that are invalid, // and crash. if (linkedroom && (p_rooms.find(linkedroom) == -1)) { // invalid room WARN_PRINT("Portal attempting to link to Room outside the roomlist : " + linkedroom->get_name()); linkedroom = nullptr; } // this should not happen, but just in case if (linkedroom && (linkedroom->_room_ID >= p_rooms.size())) { WARN_PRINT("Portal attempting to link to invalid Room : " + linkedroom->get_name()); linkedroom = nullptr; } } 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) { _settings_path_linkedroom = link_path; // see if the link looks legit Room *linkedroom = nullptr; if (has_node(link_path)) { linkedroom = Object::cast_to(get_node(link_path)); if (linkedroom) { if (linkedroom != get_parent()) { // was ok } else { WARN_PRINT("Linked room cannot be the parent room of a portal."); } } else { WARN_PRINT("Linked room path is not a room."); } } _changed(); } 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 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 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 vertices = arrays[VS::ARRAY_VERTEX]; PoolVector indices = arrays[VS::ARRAY_INDEX]; // 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 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); } } ERR_FAIL_COND_V(pts_world.size() < 3, false); // create the normal from 3 vertices .. either indexed, or use the first 3 Vector3 three_pts[3]; if (indices.size() >= 3) { for (int n = 0; n < 3; n++) { ERR_FAIL_COND_V(indices[n] >= num_source_points, false); three_pts[n] = tr_source.xform(vertices[indices[n]]); } } else { for (int n = 0; n < 3; n++) { three_pts[n] = pts_world[n]; } } Vector3 normal = Plane(three_pts[0], three_pts[1], three_pts[2]).normal; if (_portal_plane_convention) { normal = -normal; } // 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(normal, 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; // prevent warnings when poly normal matches the up vector Vector3 up(0, 1, 0); if (Math::abs(_plane.normal.dot(up)) > 0.9) { up = Vector3(1, 0, 0); } tr_global.set_look_at(Vector3(0, 0, 0), _plane.normal, up); 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(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 RoomManager::_get_default_portal_margin(); } return _margin; } void Portal::_sanitize_points() { // remove duplicates? NYI maybe not necessary Vector 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 peculiarity 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(const Vector3 &p_portal_normal, Vector &r_verts) { // cannot sort less than 3 verts if (r_verts.size() < 3) { return; } // 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 + p_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 (p_portal_normal.dot(plane.normal) < 0.0) { // reverse winding order of verts r_verts.invert(); } } Plane Portal::_plane_from_points_newell(const Vector &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); ClassDB::bind_method(D_METHOD("set_point", "index", "position"), &Portal::set_point); 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"); }