Coordinate Transformation of Path into Accelerated Reference System

I’m a physicist and as a first major project in Godot I am trying to make a ship that flies around the solar system. To accomplish this I have multiple “Orbiter” Object in the Root scene as well as a ship. To show the path the ship is going to take, each physics step I calculate a number of ticks ahead using the same algorithm (rkn-4) as I use to move the ship:

func update_path(line: Line2D):
	line.clear_points()
	
	var pos: Vector2 = global_position
	var vel: Vector2 = current_speed
	
	line.add_point(line.to_local(pos))
	
	for i in range(prediction_steps):
		
		#Calculate position
		var next = rkn(get_gravity_acceleration, i*prediction_step, prediction_step, pos, vel)
		pos = next[0]
		vel = next[1]
		
		line.add_point(line.to_local(pos))

This works great! The line is shown and extremely stable:

However to display the path around the planet as an orbit, each point must be transformed into the local reference frame with the planet in the center. Mathematically this is quite easy, just take the difference of the point and the position of the target origin at time t and then add to it the current position of the target:

func update_path_transformed(line: Line2D, target: Orbiter):
	line.clear_points()
	
	var pos: Vector2 = global_position
	var vel: Vector2 = current_speed
	
	for i in range(0, prediction_steps):
		
		#Calculate position
		var next = rkn(get_gravity_acceleration, i*prediction_step, prediction_step, pos, vel)
		pos = next[0]
		vel = next[1]
		var local_pos: Vector2 = line.to_local(pos - target.get_future_position(i*prediction_step) + target.global_position)
		line.add_point(local_pos)

This unfortunately seems quite unstable. For an orbit around the earth specifically the moon seems to have a significant effect on the orbit, shrinking and stretching it, even though the position in the global frame seems stable. The transform seems to add an accumulating shift to the position. I do not fully understand how the to_local function works, I tried reparenting the line to the scene but this results in the line not being shown at all for some reason.

I would greatly appreciate some help on this topic.

Without transform:

With Transform

The first point on this line should be exactly at the position of the ship marked by the dot. I initially believed this to be due to floating point precision (the solar system is roughly to scale so this shift is very small when compared to the distances between planets) but recompiling for double precision has not fixed this problem. For completeness here are the process function:


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	
	look_at(get_global_mouse_position())
	
	if Input.is_action_pressed("accelerate"):
		var vect = get_global_mouse_position() - position
		current_speed += vect.normalized()*acceleration*delta
	
	#Process Player Movement
	var next = rkn(get_gravity_acceleration, 0, delta, position, current_speed)
	position = next[0]
	current_speed = next[1]
	
	#Modify Prediction line of player
	#var line: Line2D = $Path
	if GlobalSignals.show_trajectory:
		path.show()
		if(reference_target is Orbiter and GlobalSignals.transform_trajectory_to_reference):
			update_path_transformed(path, reference_target)
		else:
			update_path(path)
	else:
		path.hide()

The Gravity-Acceleration Function:


func get_gravity_acceleration(time:float, position:Vector2, velocity:Vector2):
	var planets = get_tree().get_nodes_in_group("planets")
	var grav_accel = Vector2.ZERO

	for planet in planets:
		# Calculate position of planet at time t
		var planet_position: Vector2
		if time == 0:
			planet_position = planet.global_position
		else:
			planet_position = planet.get_future_position(time)
		
		#Calculate position
		var vect = planet_position - position
		var r = vect.length()
		if r>10:
			grav_accel += vect.normalized() * planet.mass * G / (r*r)
	
	return grav_accel

And the Runge-Kutta-Nystroem algorithm:

func rkn(f: Callable,t: float, dt: float, y: Vector2, dy: Vector2) -> Array[Vector2]:
	var K1 = f.call(t, y, dy)
	var K2 = f.call(t + 2./3*dt, y + 2./3*dt*dy - dt**2./9*K1, dy+2./3*Vector2(dt, dt))
	var K3 = f.call(t + 2./3*dt, y + 2./3*dt*dy + dt**2*(2./9*K1), dy+dt*(1./3*K1 + 1./3*K2))
	
	var next_y = y + dt*dy + dt**2 * (K1/4.+K3/4.)
	var next_dy = dy + dt * (K1/4. + 3./4 * K3)
	
	return [next_y, next_dy] #returns next position and velocity

Any ideas what might cause this or how to fix it?

Don’t forget that Godot is a game engine, not a scientific simulation software.

GDScript float type is 64 bits, so standard double precision, however graphics hardware and consequently all rendering/shader code that displays your stuff on the screen uses single precision 32 bit floats. Since VectorX types are optimized for exchange with the rendering hardware, they too use single precision floats. If you’re trying to simulate the actual solar system scale, the problems you’re experiencing may be caused by imprecision of the 32 bit floating point number representation.