Reparenting nodes to a subviewport hides them for 1 frame

Godot Version

4.4.1 stable

The Goal

I have a straightforward scene-switching node that I’m working on.

I want to crossfade from old scenes to new ones, with a nice shader-driven animation. Here’s the important part:

## GO TO SCENE
func go_to_scene(new_scene: Node, _shader: String = "default"):
	start_crossfade()
	
	for child in world.get_children():
		child.reparent(sub_viewport) # <- the part that Godot doesn't like
	
	if current_scene:
		prior_scene = current_scene
		current_scene = null
	
	current_scene = new_scene


func start_crossfade(new_duration: float = 2.0):
	crossfade_max_duration = new_duration
	crossfade_duration = crossfade_max_duration
	crossfade_rect.material.set_shader_parameter("completion", 1.0)
	#crossfade_rect.visible = true   # <- I know this is commented, more on this below.
	state = states.CROSSFADING

The “old” children of WorldManager are being stored as sub-children of the SubViewport. As I understand it, this should cause them to be rendered by the SubCamera right away.

Then, I show my CrossfadeRect, which views the SubViewport’s output texture, and a shader makes the CrossfadeRect transparent in a pattern using a shader.

This should result in a nice crossfade from my old scene to my new one, without having to snapshot or pause–I would like water to keep flowing, flags to continue waving, and so on in the old scene, hence this reparenting strategy.

But, seemingly, the SubCamera doesn’t immediately render its newly-assigned children.

The Issue

Right now, the result of this is a brief “blink” back to the previous scene, every time I switch scenes, as seen here:

Animation2

This happens if I uncomment crossfade_rect.visible = true in the above code sample, too. What happens then is that I get the same blinking frame, but the output is pure grey.

The Question

How am I supposed to be doing this cross-fade? As far as I understand, this SubViewport trick is the easiest way to press a group of Node2Ds into one shader-readable texture.

I could defer the reparenting, but I feel like that just results in blinking on a different frame instead.

I could try duplicating my Nodes to the SubViewport, letting the crossfade start, and then freeing the originals after, but that seems like such a boneheaded hack for something that should be simple to fix.

If something is happening wrong for just its first frame, it’s usually an issue related to the order that Godot calls code.

Do you have any of the code related to rendering in _ready() or _process()? If so, it may work if you move some of that to an _enter_tree() function, as that’s called faster. _ready() is only called when all nodes in a scene are loaded, so this can happen the frame after the scene is instantiated. _enter_tree() is called as soon as the node it’s attached to is loaded, so it will start running before the scene is fully loaded.

Another thing that might work is disabling the visibility for your scene, then enabling it using _ready() in a script in the scene’s root node. You can do this by setting its visible to false when instantiating it, then setting self.visible = true in a script in the scene’s root node. This will ensure the scene is invisible for that first frame, then renders normally in subsequent frames.

There’s no such thing happening in any Node’s _ready() or _process().

I think I should be referring to these “sub-scenes” as something else, since Godot appropriates the term “scene”. What I’m doing is this:

func prepare_battle(battle_object: DataBattle):
	var battle_scene = Scene_Battle.new()
	go_to_scene(battle_scene)
	introduce_horsemen(battle_scene) 
	#^- tells the Battle who GameManager, WorldManager, UIManager, and DataManager are.
	battle_scene.set_battle(battle_object)

Very much patterned after RM engines. The Scene_Battle object has functions for handling turn order, moving units around, and taking UI input. Or more correctly, it has functions for delegating those tasks to the WorldManager and UIManager.

The visible map, and the sprites walking on it, are children of WorldManager. This is because all scenes (in the game logic sense) use the same basic parts. A theoretical Scene_Shop would still need to ask the WorldManager “please load this 2D isometric map, and put these sprites at these tile coordinates, and load these windows for me”.

When I try to crossfade scenes, I take all of WorldManager’s current children—the current map, and all of the sprites walking on it—and reparent them to the SubViewport. They should then appear on the SubViewport’s render texture, but they don’t right away. No nodes are created or destroyed yet, and they don’t enter or leave the try (unless reparenting does this inherently).

You raise an interesting question: Is there a way to reparent the nodes after the frame is done rendering, and before the next one starts? Is that happening already?

I solved it by setting my SubViewport’s update mode before re-parenting my nodes:

sub_viewport.set_update_mode(sub_viewport.UPDATE_ALWAYS)

Then, when I’m done with my crossfade, I turn it back off until I need it:

sub_viewport.set_update_mode(sub_viewport.UPDATE_DISABLED)

By default, SubViewports update “only when visible” as described here in the docs. So my SubViewport wasn’t updating until it was told that its viewport texture was showing, resulting in a 1-frame delay in “waking up”.

I hope this helps someone in the future!

1 Like

This is a really neat approach to transitions.