Parallax background decors in orthographic 3D, position resets incorrectly after player death/respawn

Godot Version 4.6.1

Question : Parallax background decors in orthographic 3D, position resets incorrectly after player death/respawn

Hello there. I’m making a top-down 3D space shooter in Godot 4.6 with an orthographic camera (angle -75°, size-based zoom). I have background decorative meshes in Zone 2 that should parallax slightly as the player moves (mult ~0.97). They work fine on first visit, but after the player dies, respawns in Zone 1, and warps back to Zone 2, the decors appear at a different screen position every time. ( the game is open world with zones far away from each other )

Here what i’ve tried :

Capturing the global_position at _ready() and resetting to it on zone re-entry. But position drifts because the node isn’t fully placed yet at _ready()

Capturing position on first recentrer_sur_joueur() call instead, same results

Using player position as offset reference. Breaks because player dies at different positions each time

Disabling culling via extra_cull_margin since decors were being culled due to Y depth (-1665 vs player Y=0)

Moving decors to Y=0 — player flies through them and gets blinded by their lights

The debug logs show the decors always reset to the exact same global_position, so the script logic is correct. But visually they appear at different screen positions

Is there a clean way to handle parallax background objects in a 3D orthographic top-down game that survives player death/respawn/zoom changes? Or should I approach this differently entirely?

I’ve tried switching to perspective view with a high camera height and a low fov. But the parallax effect is limited, i can’t make objets apprear very far away just like a “mult”: 0.98},” would do.

extends Node3D

@export var mult : float = 0.97

var _joueur : Node3D = null
var _position_precedente : Vector3 = Vector3.ZERO
var _actif : bool = false
var _position_scene : Vector3 = Vector3.ZERO
var _position_capturee : bool = false

func _ready() -> void:
	_joueur = get_tree().get_first_node_in_group("player")
	_desactiver_culling(self)
	print("[DECORS] _ready sur : ", name, " | global_position=", global_position)

func _desactiver_culling(noeud: Node) -> void:
	for child in noeud.get_children():
		if child is GeometryInstance3D:
			child.extra_cull_margin = 10000.0


func recentrer_sur_joueur() -> void:
	print("[DECORS] recentrer_sur_joueur appelé sur : ", name, " | global_position AVANT=", global_position, " | _position_capturee=", _position_capturee)
	if not _position_capturee:
		_position_scene = global_position
		_position_capturee = true
		print("[DECORS] position_scene capturée : ", _position_scene)
	global_position.x = _position_scene.x
	global_position.z = _position_scene.z
	print("[DECORS] global_position APRÈS reset=", global_position)
	_actif = false
	await get_tree().physics_frame
	await get_tree().physics_frame
	if is_instance_valid(_joueur):
		_position_precedente = _joueur.global_position
	_actif = true

func joueur_sorti() -> void:
	_actif = false
	if _position_capturee:
		global_position.x = _position_scene.x
		global_position.z = _position_scene.z

func _process(_delta: float) -> void:
	if not _actif:
		return
	if not is_instance_valid(_joueur):
		_joueur = get_tree().get_first_node_in_group("player")
		return
	var deplacement := _joueur.global_position - _position_precedente
	if deplacement.length_squared() > 0.0001:
		global_position.x += deplacement.x * mult
		global_position.z += deplacement.z * mult
	_position_precedente = _joueur.global_position

It looks like you are doing something additive/stateful, parallax should be as simple as - take one point as center (center of your zone) + vector to player and multiply it by your 0.97, it should be it.
It should work in all cases, and be stable, no need to track previous states.

You’re totaly right, i made it way more complicated than it needed to be, and it wasn’t even working. Thanks you for opening my eyes with your comment. It finally works now.