Update portal plugin to 1.0.1

Includes the flickering fix!
This commit is contained in:
Vojtěch Struhár 2025-07-08 08:21:16 +02:00
parent d52d760b18
commit 79ce6cc145
3 changed files with 69 additions and 25 deletions

View File

@ -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.

View File

@ -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"

View File

@ -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: