Carried object rotates to weird angle when picked up

Godot Version

4.3

Question

I am trying to create a first person camera that can pick up and drop objects in the environment. When I pick up the object I want to lock its current rotation along the Y axis relative to the camera. When I move the mouse, I want the object to rotate in global space to remain directly in front of the camera, but its rotation about the y-axis relative to the camera should remain the same, ie: the item should always remain upright.

My character has the structure:

  • Player
    ** PlayerPOV
    *** RayCast3D
    *** HeldItemPosition

The only times that the player.gd code interacts with the carryable object are:

  1. If the user left-clicks while not carrying anything, it gets the first collider along the RayCast3D and if it’s Carryable it calls pick_up() on the collider.
  2. If the user right-clicks while carrying something, it calls drop() on the carried object.
	if Input.is_action_just_pressed("interact"):
		if held_object == null:
			var looking_at = raycast.get_collider()
			if looking_at is Carryable:
				held_object = looking_at
				held_object.pick_up($PlayerPOV/HeldItemPosition)
	if Input.is_action_just_pressed("drop"):
		if held_object:
			held_object.drop()
			held_object = null

Other than that all of the logic is contained within the carryable scene. What I’m trying to do is:

  1. When the item is picked up disable all of its collision physics and get the initial angle in global space between the object’s and carrier’s rotation vectors on the XZ plane.
  2. When the item is dropped, restore all of its collision physics.
  3. On every physics processing tick, a) get the carrier’s global transform origin and store it as the object’s global transform origin, and b) get the camera’s global transform basis, set the y-vector to Vector3.UP to keep it upright, rotate by the angle recorded when the object was picked up, and store it as the object’s global transform basis.

My expectations:

  1. When moving the mouse in any direction, the carried object always remains upright. - WORKING AS DESIRED
  2. When moving the mouse left and right, the object should stay in the same rotation relative to the camera - WORKING AS DESIRED
  3. When first picking up the item it should retain its existing orientation relative to the camera - NOT WORKING

Instead, the object snaps to a seemingly-arbitrary angle about the y axis upon pickup and I can’t figure out why. Here is the code for carryable.gd

extends RigidBody3D
class_name Carryable

var _carrier: Node3D
var _rotation_offset : float
var carried: bool = false
@onready var original_collision_layer = collision_layer
@onready var original_collision_mask = collision_mask

func pick_up(carrier: Node3D) -> void:
	# stop physics interactions while object is carried
	freeze = true
	collision_mask = 0
	collision_layer = 0

	# capture carrier information
	carried = true
	_carrier = carrier

	# get the angle between the direction of the camera and the object on the xz plane
	_rotation_offset = _get_xz_rotation(_carrier.global_rotation, global_rotation)
	
func _get_xz_rotation(a: Vector3, b: Vector3):
	# normalize a and b and project them onto the xz plane
	a = Plane.PLANE_XZ.project(a).normalized()
	b = Plane.PLANE_XZ.project(b).normalized()
	var theta = a.angle_to(b)
	return theta
	
func drop() -> void:
	# restore physics
	freeze = false
	collision_mask = original_collision_mask
	collision_layer = original_collision_layer

	carried = false
	_carrier = null
	
func _physics_process(delta: float):
	if carried:
		# get the carrier's position in global space and snap self to it
		var target_origin = _carrier.global_transform.origin
		global_transform.origin = target_origin
		
		# get the global rotation info for the carrier but maintain the y axis as
		# straight up to keep the object upright
		var target_basis: Basis = Basis(
			_carrier.global_transform.basis.x,
			Vector3.UP,
			_carrier.global_transform.basis.z).orthonormalized()

		# rotate the target basis by the initial angle between the object and the carrier
		target_basis = target_basis.rotated(Vector3.UP, _rotation_offset)

		# store as the new basis
		global_transform.basis = target_basis

My thoughts are that _rotation_offset isn’t being calculated properly but I can’t quite figure out why not unless some function isn’t doing what I expect. I’m very new to godot so I might be missing something obvious.

Thanks in advance please let me know if any extra information is helpful.