• 3D
  • How to control which way a node rotates by 180?

I'm doing a car chase-camera which circles around the car depending on whether the car is going forward or backward, and I'm having the issue that it always circles around the same way, even when it's the longest way.

Just to explain my setup a bit: the camera is not child of the car, and I'm using two pivots on the camera, one that keeps interpolating itself to match the position and Y rotation of the car; and the other pivot rotates to turn the camera around the car. The camera simply does look_at(car). So my camera setup looks like this:

pivot1 (Spatial)   # always following the car
   pivot2 (Spatial)   # always at local (0,0,0), rotates around Y
      ClippedCamera     # the cam itself is at some height and distance from the pivots

So the rotation of the second pivot is basically just from 0 to 180 and back. But it always goes the same way around.

Under the notion that sometimes the camera is to the left or to the right of the car, due to steering and the smoothing of pivot1 taking time to catch up to it, I tried comparing pivot1.rotation.y with car.rotation.y, to see if one was bigger than the other and vice-versa depending on which side the camera was from the car, but I couldn't get consistent results that I could use to control pivot2's rotation (I may have done something wrong, though).

I also tinkered naively with dot products of their positions and rotations, but that didn't yield any values that seemed useful, either.

My current code looks like this:

func _process(delta: float) -> void:
	# pivot1 (self)
	global_transform.origin = global_transform.origin.linear_interpolate(car.global_transform.origin, 0.075)
	rotation.y = lerp_angle(rotation.y, car.rotation.y, 0.01)

	# pivot2  (car_heading means going forward (1) or backward (-1) or idle (0))
	if rotation_degrees.y-car.rotation_degrees.y < 0:
		if   car_heading  > 0: target_rot = 0
		elif car_heading  < 0: target_rot = deg2rad(180)
	else:
		if   car_heading  > 0: target_rot = deg2rad(360)    # this probably yields zero
		elif car_heading  < 0: target_rot = deg2rad(180)
	
	pivot.rotation.y = lerp_angle(pivot.rotation.y, target_rot, 0.025)

	# camera 
	camera.look_at(car.global_transform.origin, Vector3.UP)

I'm confused, though: the example interpolates between two transforms. I only need to rotate a node by 180º (one transform).

@cybereality said: You can use -180 for the degrees, that should move in the right direction.

I did try that too, but the problem I'm having there is that I can't determine when to use 180 and when to use -180. I couldn't come up with a way to tell reliably which way it should rotate. I feel like there should be a way to compare the rotation of the car with the rotation of one of the pivots, and be able to tell from that which way to go, but I'm not quite getting it...

And I'm also not sure how quaternions can solve that.

By the way, just to be clear, the camera pivot isn't free flowing. It rotates between two positions, one in front of the car and another on the back, never stops in the middle, and if the car changes its heading while the pivot is still rotating, then the pivot has to rotate back from wherever it is.

So I tried quaternions, maybe naively, but the code works. Except I'm still having that problem, it still always rotates the same way around. My current code:

var rotated_quat:Quat
var original_quat:Quat
var target_quat:Quat

func _ready() -> void:
    # (...)

	rotated_quat = Quat(pivot.transform.basis.rotated(Vector3.UP, deg2rad(180)))
	original_quat = Quat(pivot.transform.basis)

func _process(delta: float) -> void:
	# (...)
	
	# check if car is heading the same way or not
	if current_heading != last_heading and car.throttle != 0:
		# this means the pivot will need to change direction of rotation
		last_heading = current_heading

	var current_quat = Quat(pivot.transform.basis)

	# no change if last_heading is zero, just keep rotating the same way
	if   last_heading < 0: target_quat = rotated_quat
	elif last_heading > 0: target_quat = original_quat

	pivot.transform.basis = Basis(current_quat.slerp(target_quat, 0.1))

You still need to determine which way to rotate. That is the issue. Dot product is probably the way to go.

This is never going to work because of the way angles are stored in Godot (usually from -180 to 180, but depending on what you do they can be more):

if rotation_degrees.y-car.rotation_degrees.y < 0:

You have to convert the angle into a vector, normalize it, and then compare the two vectors. Using dot product is one way, however, that still poses issues at 180 degrees because it is difficult to determine the exact way to go. However, it might be close enough as long as the angle is never exactly 180 degrees (and this is unlikely due to floating point error).

To be honest I'm not sure dot product can help. I noticed while debugging that rotations wrap around, so negative values become positive all of a sudden and vice-versa. So I suppose the rotation vectors change too drastically for and may mess up dot products, but I could be wrong.

However -- good news -- I changed my first code to use degrees and lerp, instead of lerp_angle, still using the difference in rotation between the car and pivot1, and it's actually working. Although while testing I realized I should also account for which way the player is steering. E.g., if the player is steering right and reversing, makes more sense that the camera rotates to the front through the left side of the car.

Because of the rotation wrapping problem, I'm getting the difference like this: var rot_diff = wrapf(pivot1_rot - car_rot, -180, 180)

It's being a bit confusing to get the logic right (especially because my car's rear wheels turn too), but I think I'm getting there. :)

EDIT: I hadn't seen your very last post. :S

For the dot product, don't compare just the 2 vectors (as let's say 45 degrees will result in the same number either way). What you want to do is compare to a fixed vector, like whatever is forward in your game (could be the forward of the car, or just forward to the camera, not sure how your game is set up). Then see if the two first vectors are on the same side.

I think I got it. I'm not using dot products, though. It seems to me all that can give me is whether the camera is behind or in front of the car, but I know that from the pivot rotation anyway. I did it through a bunch of checks and angle corrections. Took me a while to fend off edge cases and not-obvious camera intricacies, and to simplify the code, but as far as I can tell it's working flawlessly.

So unless someone has a better idea, I'm going with this.

The code:

func calc_target_rotation():
	var rot = target_rot

	# if turning left at front and right at rear, or vice-versa, then cancel each other out
	# Note: rear wheels steering is inverted
	var steering_left =  (car.front_steering < 0 and not car.rear_steering  < 0) \
					  or (car.rear_steering  > 0 and not car.front_steering > 0)
	var steering_right = (car.front_steering > 0 and not car.rear_steering  > 0) \
					  or (car.rear_steering  < 0 and not car.front_steering < 0)

	if curr_heading < 0:  # reverse
		if   steering_left:	  rot =  180
		elif steering_right:  rot = -180
		elif pivot_diff > 0:  rot =  180
		elif pivot_diff < 0:  rot = -180
	else:                 # forward
		rot = 0
		# correct pivot's starting angle before returning to zero (180 or -180)
		if steering_left:
			if pivot.rotation_degrees.y < 0:
				pivot.rotation_degrees.y = 360 + pivot.rotation_degrees.y
		elif steering_right:
			if pivot.rotation_degrees.y > 0:
				pivot.rotation_degrees.y = -360 + pivot.rotation_degrees.y
		elif pivot_diff > 0 and pivot.rotation_degrees.y < 0:
			pivot.rotation_degrees.y = 360 + pivot.rotation_degrees.y
		elif pivot_diff < 0 and pivot.rotation_degrees.y > 0:
			pivot.rotation_degrees.y = -360 + pivot.rotation_degrees.y

	return rot

func _process(delta: float) -> void:
	(...)
	# pivot2
	var car_rot = rad2deg(car.global_transform.basis.get_euler().y)
	pivot_diff = wrapf(rad2deg(pivot.global_transform.basis.get_euler().y) - car_rot, -180, 180)

	new_heading = sign(car.move_direction)
	if new_heading != curr_heading and car.throttle == new_heading:
		# this means the pivot will need to change direction of rotation
		if new_heading != 0:
			curr_heading = new_heading
			target_rot = calc_target_rotation()

	pivot.rotation_degrees.y = lerp(pivot.rotation_degrees.y, target_rot, lerp_rot_weight)