SubViewport Texture position issue in seamless 2D portal effect

Godot Version

v4.5.stable.official [876b29033]

Question

Kia ora all, I’m working on a seamless 2D portal effect where portal A sends a Camera2D (inside a SubViewport) to portal B and uses this texture to display a seamless portal effect. I’m having issues correctly positioning the camera/viewport texture and am trying to get rid of a ‘streaking’ effect where the view through the portal is stretched into a blur of pixels.

The code at the time of writing this is here: actual portal question commit · kit-solent/refrigerate@70e3e9c · GitHub
And can be obtained with: git clone https://github.com/kit-solent/refrigerate.git && cd refrigerate && git reset --hard 70e3e9c

This is how my portal scene is structured.

And these are the relavent parts of my portal script.

extends Node2D

@export var pair:Node2D
@onready var target:Node = Core.main.get_player()

func _ready():
	$sub_viewport.world_2d = get_viewport().world_2d

func _process(_delta:float):
	# every frame we delete all the polygons (if any).
	# this prevents polygons from lingering after the portal
	# has left the screen. These can be picked up by other
	# portals which looks bad.
	if polygons_in_view:
		clear_view()
		
	# the portal is only drawn if on the local screen. This works with multiplayer, only showing the portal to those who can see it.
	# the portal should also only be drawn if it's target is within a certain distance of the portal.
	if pair:
		# Move our camera to the pair portal. Global coordinates must be used because the
		# camera is inside a viewport and so doesn't have coordinates local to the pair (I think).
		$sub_viewport/camera_2d.global_position = pair.global_position + Vector2(256.3478, 254.47421)
		
		# update the view
		if $on_screen_notifier.is_on_screen():
			set_view(target)
	
	# TODO: This hack gives the below positions.
	# adding the difference to the camera position
	# gives the right position but the "magic vector"
	# is only as accurate as the visual mouse calibration.
	
	# DEBUG
	#if Input.is_action_just_pressed("debug key"): # The ` key (backtick)
	#	print("Portal: "+name)
	#	print("  Global Position: "+str(global_position))
	#	print("  Mouse  Position: "+str(get_global_mouse_position()))
	
	# portal                 Vector2(246.0, -625.0)
	# mouse/target portal    Vector2(502.3478, -370.5258)
	# mouse - portal = vector from us to target
	# = Vector2(256.3478, 254.47421)

# keeps trask of if there are currently polygons displayed in the view
var polygons_in_view:bool = false

@warning_ignore("shadowed_variable")
func set_view(target:Node):
	# add new polygons
	var polygons = Core.tools.cast_polygons(to_local(target.global_position), $line.points, get_local_bounds())
	
	for i in polygons:
		var new = Polygon2D.new()
		new.polygon = i
		
		# Copy the viewport texture over from the storage node to the new polygon.
		new.texture = $texture_storage.texture
		
		# add a border for debugging
		var border = Line2D.new()
		border.default_color = Color.RED
		border.width = 4.0
		border.points = new.polygon # use the polygon border to make the line
		border.add_point(new.polygon.get(0)) # and connect it up with the last point again
		new.add_child(border)
		
		$view.add_child(new)
	
	# record the fact that there are polygons in the view that need to
	# be cleared if this portal leaves the screen.
	polygons_in_view = true


func clear_view():
	"""
	Clears the portal view by deleting all view polygons.
	"""
	for i in $view.get_children():
		$view.remove_child(i)
		i.queue_free()
	
	polygons_in_view = false

The streaking/stretching issue can be seen by running the game and moving to the portal located just off screen above the player spawn point. Standing to the left of the portal and looking through it to the right shows a view onto the map around the 2nd portal (located elsewhere on the map). You can see in the below image that pixels below some line (middle of the portal maybe) are shown clearly but those above are stretched into blurry lines streaking upwards.

My other question was about the positioning of the portal texture/camera. I used an approximation calculated from the global mouse position to work out how far off the texture was and move it to roughly the right place. I have no idea why it doesn’t work without this and if a function like get_global_transform() or something would give me the same shift but to more accuracy (and cleaner code). The below image shows what it looks like without the calculated shift applied. The “view” should be “looking out of” the 2nd portal (the vertical blue line).

The portal scene is at res://features/portals/portal.(tscn/gd).

I asked this question previously here but havn’t got any responses and figured a new question with my slightly updated Godot version and what progress I’ve made since then would be clearer than writing updates in a comment on the old question.

Thanks so much in advance for any help with this/insignts as to whats going on and sorry for the huge paragraph of confusion. :slight_smile:

Hi. I think you might get help sooner if you do provide the exact information relevant to the problem, rather than ask someone else to go in and figure out whatever is going on in your project. It’s not entirely impossible to find somebody willing to sit down and do your job for you, but that isn’t how it usually goes here.
More importantly, as you pull the facts together and put your situation into words for others, you might just figure out an answer. At least, walk part of the way to the solution yourself.

For your first question, that looks to me like what happens if you have a texture with “clamp” on it and walk off the edge; the edge pixels are repeated.

Are you sure the camera/texture is positioned correctly?

Thanks for the response. I’ve added the relavent parts of my portal script and a screenshot of the scene structure. Is that what you meant by exact information? I’ve already tried without success to work out which of the many layered transforms might apply to my situation and I don’t even know where to start with the texture stretching thing. Hope the code helps and let me know if there’s any more information that would help.

Ok, thanks, that does make sense. I don’t really know how these texture modes work but I changed texture_repeat property to enabled on all the polygons and now instead of the stretching it repeats from some point below. So I guess the issue is that the texture being picked up by the SubViewport isn’t big enough to fill the polygons without hitting the edge? I tried doubling the size of the SubViewport but that didn’t fix it. Thanks for the response though.

It sounds like it’s definitely a question of how the texture is mapping on to the viewport. Perhaps it wasn’t designed to be skewed like that? I’m not sure.

The strange thing is that the line where the repeating/stretching starts comes out of the middle of the portal. It could be because the origin for the portal is in the middle of the line but the viewport texture probably starts at the top left. Part of my problem is probably that I don’t know how to move the texture relative to the polygons that display it. I can move the camera around but then I just take the texture from the SubViewport and slap it onto the polygons. I’ll see if I can work out how to shift it arround at that end.

You certainly don’t need a magic vector. Place the portal camera exactly at the pair/exit portal, then make sure you stretch the portal view texture over the polygons correctly.

Every polygon vertex has a corresponding UV coordinate in the poygons data, that controls which point of the texture is pinned to that vertex. UV of (0,0) corresponds to the top left corner of the texure and (1,1) corresponds to the bottom right.

It looks like your polygons come with UVs already, only they display the (0,0) top left corner of the texture at the portal point, and the rest of the portal view is displayed to the right and below the portal. You might just need to add(0.5; 0.5) to every UV to have the portal view texture centered around the portal.

Ok, I ended up using new.texture_offset = -$texture_storage.texture.get_size()/2 because I don’t know how to edit UV values but its working now so thank you so much.