Uncontrolled spinning after rotation

Godot Version

v4.2.1

Question

Hello!
I’m making a game where you can pick up RigidBody3D element with multiple children colliders.

When I pick up the element, I apply force and torque to match my holder position and rotation (holder is just for a reference for rotation and position). I can touch or crash into other static and dynamic objects and everything works fine. Picked object always matches holder’s position and rotation, no matter how I move.

After I rotate the holder, picked object matches rotation and position of the holder, but it becomes very unstable, as soon as I touch any other static or dynamic objects, picked object loses control and starts spinning.

I have no idea how to fix this issue.

Animation

extends RigidBody3D

@onready var ray = $RayCast3D
@onready var holder = $Holder/Holder
var input = Vector3()
var pickedObject = null
var pickedObjectCollider = null
var hold_offset = Vector3(0, -3, 0)

var rrotate = 0
var rotate_target = 0

func _process(_delta):
	input.x = Input.get_axis("move_left", "move_right")
	input.z = Input.get_axis("move_up", "move_down")
	input = input.normalized()

	if Input.is_action_just_pressed("pickup"):
		pickup()

	if Input.is_action_just_pressed("drop"):
		drop()
		
	if Input.is_action_just_pressed("ui_left") and pickedObject:
		holder.rotation_degrees.y = 90


func _physics_process(delta):
	apply_central_force(input.normalized() * 1000 * delta)
	
	if pickedObject:
		movePickable(delta)


func pickup():
	if !pickedObject and ray.is_colliding():
		var collider = ray.get_collider()
		if collider.is_in_group("pickable"):
			pickedObject = collider
			pickedObject.can_sleep = false
			
			var shape_id = ray.get_collider_shape()
			var owner_id = collider.shape_find_owner(shape_id)
			pickedObjectCollider = collider.shape_owner_get_owner(owner_id)


func drop():
	if pickedObject:
		pickedObject.can_sleep = true
		pickedObject = null

func movePickable(delta):
	var target_position = global_transform.origin + global_transform.basis.y * hold_offset.y
	var current_position = pickedObjectCollider.global_transform.origin
	var force_direction = (target_position - current_position)
	var force = force_direction * 3000 * delta

	pickedObject.apply_central_force(force)
	pickedObject.linear_velocity *= 0.9
	
	var torque = (holder.global_rotation - pickedObjectCollider.global_rotation) * 1000 * delta
	pickedObject.apply_torque(torque)
	pickedObject.angular_velocity *= 0.9

You should try increasing mass, or inertia.

I would also use linear damp and angular damp instead of messing with these values directly. The physics server may override these values.

1 Like

I tried to increase the mass and add linear and angular damp and I still have the same problem:

pickedObject.linear_damp = 4
pickedObject.angular_damp = 4

Pickable object looks like this:

Darn Okay, I wonder if it’s one of two things.

If you remove the torque, does the spinning stop?

If not, if you increase the offset does it help?

Since you are applying forces to reach a position, have you ever looked into a PID loop?

You are kind of almost there, but there are other things you can add for better damping and force management.

@pennyloafers First, thank you for your help and time.

PID loop looks to hard for me.

I added a new input action that conditionally stops applying torque. When I press Shift to stop applying torque, the picked object stops spinning. Additionally, it no longer tries to match the holder’s rotation.

I tried different approach:

var offset = holder.global_position - pickedObjectCollider.global_position
var force = (offset * 4000) - (pickedObject.linear_velocity * 200)
pickedObject.apply_central_force(force)

var newRotation = holder.global_rotation - pickedObjectCollider.global_rotation
var torque = (newRotation * 4000) - (pickedObject.angular_velocity * 100)
pickedObject.apply_torque(torque)

Everything is working fine until I rotate holder like this:

if Input.is_action_just_pressed("ui_left") and pickedObject:
	holder.rotation_degrees.y = 90

Maybe it has something to do with the origin or center of mass.

Regarding rotation. These will be limited to 180° or 360° depending on the axis and axis order. It could be that when you rotate the y axis it puts the limit into a spot that cant be easily torqued too. Like it may try to rotate 360° in the opposite direction when it only had to go like 1° the other way.

Also euler rotations have a rotate order on the axises. This can be adjusted, but You might want to also try using quaturnians. Which are always better then euler rotations.

I think your clue on the torque forces is because you do not have an appropriate force management that a PID loop could solve. I assume it’s basically running out of control trying to apply more and more forces to get it into the right rotation. Like a mary-go-round

This is a pid loop i use myself.

extends Resource
class_name PID

var kp := 40.0
var ki := 0.0
var kd := 4.0

var totalError := 0.0
var lastError := 0.0
var lastTarget := 0.0123456

func _init(p:float = kp, i:float = ki , d:float = kd):
	set_params(p,i,d)


func set_params(p:float, i:float, d:float):
	kp = p
	ki = i
	kd = d


func calculate(delta_t:float, target:float, current:float) -> float:
	var error = target - current
	totalError += ki * error * delta_t
	var P:float= kp * error
	var I:float = totalError
	var D:float = kd * (error - lastError) / delta_t
	lastTarget = target
	lastError = error
	var impulse = (P + I + D)

#	print("PID:", " time: ", delta_t, " tar: ", target, " cur: ", current,
#			" err: ", error, " tErr: ", totalError, " errDir: ", errorDir,
#			" lastErr: ", lastError, " imp: ", impulse
#		)
	return impulse

This is how i use it I split each axis into its own calculation

func _init():
	xpid_pitch = PID.new(pr_prop, pr_integral,pr_derivative)
	ypid_yaw = PID.new(yaw_prop, yaw_integral, yaw_derivative)
	zpid_roll = PID.new(pr_prop, pr_integral,pr_derivative)

func calculate_correction(delta:float) -> Vector3:
	# reduce cross product area to 0
	const target = 0.0
	# yaw
	if not input_direction.is_zero_approx():
		last_input_direction = input_direction
	var forward = rigid_body.transform.basis * Vector3.FORWARD
	var yaw_cross = Vector2(forward.x, forward.z).cross(Vector2(last_input_direction.x, last_input_direction.z))
	var yaw = ypid_yaw.calculate(delta, target, yaw_cross)
	# pitch and roll
	var up = rigid_body.transform.basis * Vector3.UP
	var pr_cross = up.cross(Vector3.DOWN)
	var pitch = xpid_pitch.calculate(delta, target, pr_cross.x)
	var roll = zpid_roll.calculate(delta, target, pr_cross.z)
	return Vector3(pitch,yaw,roll)

if you want a great visual example I really liked this video

Looks like you ware right, because if I rotate only for example 30 degrees, everything is still working fine.

First I will try with Quaternions, because PID looks much harder to me.

Thank you!

@pennyloafers I got it to work with help of chatGTP heh

If I rotate the holder and the element with quaternion like this, it works perfectly:

if Input.is_action_just_pressed("ui_left") and pickedObject:
	var rotation_angle_degrees = 90
	var rotation_axis = Vector3.UP
	var rotation_angle_radians = deg_to_rad(rotation_angle_degrees)
	var rotation_quaternion = Quaternion(rotation_axis, rotation_angle_radians)
	holder.quaternion = (holder.quaternion * rotation_quaternion).normalized()
func movePickable(delta):
	var target_quat = holder.quaternion 
	var current_quat = pickedObject.quaternion 
	var difference_quat = target_quat * current_quat.inverse()
	var angle = 2 * acos(difference_quat.w)

	if angle > PI:
		angle -= 2 * PI

	var axis = Vector3(difference_quat.x, difference_quat.y, difference_quat.z).normalized()

	if axis.length() > 0:
		var torque = axis * angle / delta
		pickedObject.apply_torque(torque * 10)

	pickedObject.angular_velocity *= 0.95

The problem is that I don’t completely understand this code, but I think this is the main difference:

if angle > PI:
	angle -= 2 * PI

I looks like it’s finding the shorter path with that check.

If the difference is greater then 180° then subtract 360°.

TargetDiff - 2π = x
E.g. 190° - 360° = -170°

So instead of rotating 190° to the clockwise to get the diff to zero, it will rotate 170° counter-clockwise.

And yea I have never understood quats, just know they work better.