Weird looking_at() behaviour

Godot Version

4.5

Question

I’m migrating my 3.5 project, where this working fine. In v4.5, when I try to smoothly rotate the player, looking_at() returns to the root scene origin ( global (0,0,0)) although the vector is not (0,0,0), and I cannot understand why.

My code:

func _physics_process(delta: float) -> void:
 bla, bla, bla...

##	Game is dual stick. mov_joy is movement direction stick. rot_joy is attack direction stick
	mov_joy.x = Input.get_axis("LEFT","RIGHT")
	mov_joy.y = Input.get_axis("FORWARD","BACK")
	rot_joy.x = Input.get_axis("ui_left","ui_right")
	rot_joy.y = Input.get_axis("ui_up","ui_down")

	var mirar_target
	direction = Vector3.ZERO
	rot = Vector3.ZERO
	direction = Vector3(mov_joy.x,0,mov_joy.y)
	rot = Vector3(rot_joy.x,0,rot_joy.y)


###	VIVO
	if vida > 0 :
		bla, bla, bla...
			ESTADOS.RELOADING:
				speed = SPEED_IDLE
				mirar_target = global_transform.origin + direction.normalized()

		velocity = lerp(velocity,direction.normalized() * speed, ACEL* delta)
		velocity.x = clamp(velocity.x, -MAX_SPEED, MAX_SPEED)
		velocity.z = clamp(velocity.z, -MAX_SPEED, MAX_SPEED)
		if not is_on_floor():
			velocity += get_gravity() * delta
		move_and_slide()

###		PROBLEM IS HERE, BUT ONLY IF direction is 0:
		if rot.length_squared() > 0 or direction.length_squared() > 0:
			var transf = global_transform.looking_at(mirar_target, Vector3.UP)
			global_transform = global_transform.interpolate_with(transf, ACEL * delta)

if direction is 0, looking_at gives a transform to the root scene (0,0,0) although mirar_target is NOT 0. As after I interpole player transform with this, player moves to scene origin.

I cannot understand why, and in v3.5, same code works fine.

Any insight?

Well if direction is 0, 0, 0 then transform origin and look at target will be the same point and look_at() cannot calculate the aim vector properly. Simply don’t do look at if that’s the case

Ufff, I’ve been from the afternoon with that.

I was just to answer when I understood. I was concerned because mirar_target is never 0, but this is not the issue. I didn’t realize that point was the same.

###		ROTAR PERSONAJE
		if rot.length_squared() > 0 or direction.length_squared() > 0:
			if mirar_target == global_position:
				return
			var transf = global_transform.looking_at(mirar_target, Vector3.UP)
			global_transform = global_transform.interpolate_with(transf, ACEL * delta)

Now works fine as expected. Thanks!

You still have several problems that may haunt you later.

First, never do this:

mirar_target == global_position

Never compare floating point numbers using operator ==. Instead do:

mirar_target.is_equal_approx(global_position)

Second, your code is already testing for the zero status of direction in this line:

if rot.length_squared() > 0 or direction.length_squared() > 0:

Just that if rot is not zero the block will execute even if direction is zero. So your last version tests for practically the same condition twice. Since zero rot is not really relevant here, you can simply do this instead:

if not direction.is_zero_approx():
	var transf = global_transform.looking_at(mirar_target)
	global_transform = global_transform.interpolate_with(transf, ACEL * delta)
1 Like

There’s another possible gotcha: Transform3D::interpolate_with() does linear interpolation of numbers inside transformation matrices. This is not ideal when interpolating rotation and may cause artifacts. Ideally you should spherically interpolate bases. So for the final version:

if not direction.is_zero_approx():
	var basis_wanted = global_transform.looking_at(mirar_target).basis
	global_basis = global_basis.slerp(basis_wanted, ACEL * delta)
1 Like

Thank you very much for the comments!! I’ve learned a lot and applied and tested almost all you advices (but one, because you didn’t see the whole function).

So, my player _physics_process gets in the final way:

func _physics_process(delta: float) -> void:
##	ESTADOS
	if is_rolling:
		estado_actual = ESTADOS.ROLLING
	elif is_reloading:
		estado_actual = ESTADOS.RELOADING
	elif is_blocked:
		estado_actual = ESTADOS.BLOQUEADO
	else:
		estado_actual = ESTADOS.NORMAL

##	MOVIMIENTO
	mov_joy.x = Input.get_axis("LEFT","RIGHT")
	mov_joy.y = Input.get_axis("FORWARD","BACK")
	rot_joy.x = Input.get_axis("ui_left","ui_right")
	rot_joy.y = Input.get_axis("ui_up","ui_down")

	var mirar_target
	direction = Vector3.ZERO
	rot = Vector3.ZERO
	direction = Vector3(mov_joy.x,0,mov_joy.y)
	rot = Vector3(rot_joy.x,0,rot_joy.y)


###	VIVO
	if vida > 0 :
		match estado_actual:
			ESTADOS.ROLLING:
				collision_layer = 0
				direction = roll_direction
				rot = Vector2.ZERO
				mirar_target = global_transform.origin + direction.normalized()
				if rolling_rapido:
					speed = SPEED_ROLL_RAPIDO
				else:
					if mov_joy.is_zero_approx():
						velocity = Vector3.ZERO
					else:
						speed = SPEED_IDLE

			ESTADOS.RELOADING:
				speed = SPEED_IDLE
				mirar_target = global_transform.origin + direction.normalized()

			ESTADOS.BLOQUEADO:
				pass
				
			ESTADOS.NORMAL:
				collision_layer = 4
				if rot.length_squared() == 0:
					speed = SPEED_IDLE
					mirar_target = global_transform.origin + direction.normalized()
				elif rot.length_squared() >0:
					mirar_target = global_transform.origin + rot.normalized() * 300
					anim_tree.disparar()
					if modo == "RIFLE":
						speed = SPEED_DISPARANDO
						insercc.shoot(PVP, rot)
					elif modo == "MAGIA":
						speed = SPEED_MAGIA 

		velocity = lerp(velocity,direction.normalized() * speed, ACEL* delta)
		velocity.x = clamp(velocity.x, -MAX_SPEED, MAX_SPEED)
		velocity.z = clamp(velocity.z, -MAX_SPEED, MAX_SPEED)
		if not is_on_floor():
			velocity += get_gravity() * delta
		move_and_slide()

###		ROTAR PERSONAJE
		if rot.length_squared() > 0 or direction.length_squared() > 0:
			if mirar_target.is_equal_approx(global_position):
				return
			var basis_wanted = global_transform.looking_at(mirar_target).basis
			global_basis = global_basis.slerp(basis_wanted, ACEL * delta)

###	MUERTO
	elif vida <= 0:
		velocity = Vector3.ZERO
		if !morir_once:
			morir_timer.start()
			morir_once = true
			#$Sombra.hide()
			if modo == "MAGIA":
				bola_mano.get_node("GPUParticles3D").emitting = false
				await get_tree().create_timer(1).timeout
				bola_mano.hide()

Working fine

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.