From 79ce6cc145e85c0d7c26e587d5137c3ffc9ea486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Struh=C3=A1r?= Date: Tue, 8 Jul 2025 08:21:16 +0200 Subject: [PATCH] Update portal plugin to 1.0.1 Includes the flickering fix! --- addons/portals/CHANGELOG.md | 13 +++++ addons/portals/plugin.cfg | 2 +- addons/portals/scripts/portal_3d.gd | 79 ++++++++++++++++++++--------- 3 files changed, 69 insertions(+), 25 deletions(-) create mode 100644 addons/portals/CHANGELOG.md diff --git a/addons/portals/CHANGELOG.md b/addons/portals/CHANGELOG.md new file mode 100644 index 0000000..88fede9 --- /dev/null +++ b/addons/portals/CHANGELOG.md @@ -0,0 +1,13 @@ +### 1.0.1 + +- Fix flickering when going sideways through a portal. PR #4 + +# 1.0.0 + +First public release! This plugin was developed as part of my master's thesis. As such, the thesis +itself is the best documentation at the moment: +[https://is.muni.cz/th/ydltb](https://is.muni.cz/th/ydltb/?lang=en). + +Also check out the included README.md for tips and potentially also the +[GitHub repository](https://github.com/VojtaStruhar/godot-portals-plugin) +for some showcase levels. diff --git a/addons/portals/plugin.cfg b/addons/portals/plugin.cfg index 1e7164f..f2382c8 100644 --- a/addons/portals/plugin.cfg +++ b/addons/portals/plugin.cfg @@ -3,5 +3,5 @@ name="Portals 3D" description="Seamless portals plugin in 3D" author="Vojtech Struhar" -version="1.0" +version="1.0.1" script="plugin.gd" diff --git a/addons/portals/scripts/portal_3d.gd b/addons/portals/scripts/portal_3d.gd index a0159ce..754b8de 100644 --- a/addons/portals/scripts/portal_3d.gd +++ b/addons/portals/scripts/portal_3d.gd @@ -344,6 +344,9 @@ var portal_viewport: SubViewport = null class TeleportableMeta: ## Forward distance from the portal var forward: float = 0 + ## True only if the [member Portal3D.player_camera] is a child of the object being teleported. + ## In that case, we consider it the player. + var is_player: bool = false ## Meshes that the object gave for duplication. Retrieved by the ## [constant Portal3D.DUPLICATE_MESHES_CALLBACK] callback. var meshes: Array[MeshInstance3D] = [] @@ -468,7 +471,7 @@ func _ready() -> void: teleport_area.collision_mask = teleport_collision_mask -func _process(delta: float) -> void: +func _process(_delta: float) -> void: if Engine.is_editor_hint(): return @@ -552,14 +555,13 @@ func _process_teleports() -> void: on_teleport.emit(teleportable) exit_portal.on_teleport_receive.emit(teleportable) - # Force the cameras to refresh if we just teleported a player - var was_player := not str(teleportable.get_path_to(player_camera)).begins_with(".") - if was_player: + + if tp_meta.is_player: _process_cameras() exit_portal._process_cameras() # Resolve teleport interactions - if was_player and _check_tp_interaction(TeleportInteractions.PLAYER_UPRIGHT): + if tp_meta.is_player and _check_tp_interaction(TeleportInteractions.PLAYER_UPRIGHT): get_tree().create_tween().tween_property(teleportable, "rotation:x", 0, 0.3) get_tree().create_tween().tween_property(teleportable, "rotation:z", 0, 0.3) @@ -691,11 +693,22 @@ func _on_window_resize() -> void: #region UTILS func _construct_tp_metadata(node: Node3D) -> void: + var teleportable = node.get_node(node.get_meta(TELEPORT_ROOT_META, ".")) # Usually the node itself + var meta = TeleportableMeta.new() meta.forward = forward_distance(node) + meta.is_player = not str(teleportable.get_path_to(player_camera)).begins_with(".") - if _check_tp_interaction(TeleportInteractions.DUPLICATE_MESHES) and \ - node.has_method(DUPLICATE_MESHES_CALLBACK): + ## This is a workaround to prevent flickering when traversing portals. + ## There is a bit of lag when restarting RTT when the exit portal becomes physically visible. + ## Ensuring both portals are updated regardless of visibility while in the portals prevents flickering. + ## More info: https://github.com/VojtaStruhar/godot-portals-plugin/pull/4 + if meta.is_player: + _set_portal_pair_update_mode(SubViewport.UPDATE_ALWAYS) + + if _check_tp_interaction(TeleportInteractions.DUPLICATE_MESHES)\ + and node.has_method(DUPLICATE_MESHES_CALLBACK): + meta.meshes = node.call(DUPLICATE_MESHES_CALLBACK) for m: MeshInstance3D in meta.meshes: var dupe = m.duplicate(0) @@ -711,11 +724,40 @@ func _erase_tp_metadata(node_id: int) -> void: var meta = _watchlist_teleportables.get(node_id) if meta != null: meta = meta as TeleportableMeta + + if meta.is_player: + _set_portal_pair_update_mode(SubViewport.UPDATE_WHEN_VISIBLE) + for m in meta.meshes: _disable_mesh_clipping(m) for c in meta.mesh_clones: c.queue_free() _watchlist_teleportables.erase(node_id) + +func _transfer_tp_metadata_to_exit(for_body: Node3D) -> void: + if not exit_portal.is_teleport: + return # One-way teleport scenario + + var body_id = for_body.get_instance_id() + var tp_meta = _watchlist_teleportables[body_id] + assert(tp_meta != null, "Attempted to trasfer teleport metadata for a node that is not being watched.") + + tp_meta.forward = exit_portal.forward_distance(for_body) + _enable_mesh_clipping(tp_meta, exit_portal) # Switch, the main mesh is clipped by exit portal! + + exit_portal._watchlist_teleportables.set(body_id, tp_meta) + + if tp_meta.is_player and exit_portal.exit_portal != self: + # Not a portal pair - the transition isn't seamless anyways. Flip the update + # mode of this portal "manually" and enable the next portal pair, since `_construct_tp_metadata` + # will not get called there. Usually portals are symmetric, though. + portal_viewport.set_update_mode(SubViewport.UPDATE_WHEN_VISIBLE) + exit_portal._set_portal_pair_update_mode(SubViewport.UPDATE_ALWAYS) + + # NOTE: Not using '_erase_tp_metadata' here, as it also frees the cloned meshes! + _watchlist_teleportables.erase(body_id) + + func _enable_mesh_clipping(meta: TeleportableMeta, along_portal: Portal3D) -> void: for mi: MeshInstance3D in meta.meshes: var clip_normal = signf(meta.forward) * along_portal.global_basis.z @@ -733,23 +775,6 @@ func _enable_mesh_clipping(meta: TeleportableMeta, along_portal: Portal3D) -> vo func _disable_mesh_clipping(mi: MeshInstance3D) -> void: mi.set_instance_shader_parameter("portal_clip_active", false) -func _transfer_tp_metadata_to_exit(for_body: Node3D) -> void: - if not exit_portal.is_teleport: - return # One-way teleport scenario - - var body_id = for_body.get_instance_id() - var tp_meta = _watchlist_teleportables[body_id] - if tp_meta == null: - push_error("Attempted to trasfer teleport metadata for a node that is not being watched.") - return - - tp_meta.forward = exit_portal.forward_distance(for_body) - _enable_mesh_clipping(tp_meta, exit_portal) # Switch, the main mesh is clipped by exit portal! - - exit_portal._watchlist_teleportables.set(body_id, tp_meta) - # NOTE: Not using '_erase_tp_metadata' here, as it also frees the cloned meshes! - _watchlist_teleportables.erase(body_id) - ## [b]Crucial[/b] piece of a portal - transforming where objects should appear ## on the other side. Used for both cameras and teleports. func to_exit_transform(g_transform: Transform3D) -> Transform3D: @@ -828,6 +853,12 @@ func _calculate_viewport_size() -> Vector2i: func _check_tp_interaction(flag: int) -> bool: return (teleport_interactions & flag) > 0 +func _set_portal_pair_update_mode(mode: SubViewport.UpdateMode) -> void: + assert(is_instance_valid(exit_portal)) + self.portal_viewport.set_update_mode(mode) + if exit_portal.portal_viewport: + exit_portal.portal_viewport.set_update_mode(mode) + ## Get a point where the portal plane intersects a line. Line [param start] and [param end] ## are in global coordinates and so is the result. Used for forwarding raycast queries. func line_intersection(start: Vector3, end: Vector3) -> Vector3: